{"id":620,"date":"2025-06-23T23:48:13","date_gmt":"2025-06-23T18:18:13","guid":{"rendered":"https:\/\/www.thealteroffice.com\/blog\/?p=620"},"modified":"2025-06-23T23:48:59","modified_gmt":"2025-06-23T18:18:59","slug":"how-to-manipulate-image-pixels-using-node-js-and-sharp","status":"publish","type":"post","link":"https:\/\/www.thealteroffice.com\/blog\/how-to-manipulate-image-pixels-using-node-js-and-sharp","title":{"rendered":"How to Manipulate Image Pixels Using Node.js and Sharp"},"content":{"rendered":"<div id=\"bsf_rt_marker\"><\/div>\n<p>Hi, I\u2019m Sanjay. I\u2019ve been working as an intern at The Alter Office for the past two months. I enjoy exploring new technologies and learning how things work. I&#8217;m currently working as a backend developer using Node.js. This manipulation idea came to me while using the <code>multer<\/code> npm package.<\/p>\n\n\n\n<p>Today, let&#8217;s play with image raw bytes. What I am gonna do is manipulate the image&#8217;s raw pixel bytes and make something silly. This is fun stuff to do. This article contains some theory as well as practical implementation, so less go\u2026<\/p>\n\n\n\n<p>As we know an image is formed by a bunch of pixels together, pixel is nothing but a combination of <strong>RGB (Red, Green, Blue)<\/strong> or <strong>RGBA (Red, Green, Blue, Alpha)<\/strong> each with take 1 byte.<\/p>\n\n\n\n<p>The images we view with extensions like PNG or JPG are the compressed formats of the image, PNG is lossless that PNG uses algorithms like <strong>DEFLATE<\/strong> to compress without losing the pixel and JPG is lossy compression that it will lose some pixel so there will be some loss in the image quality, if we want to view the image without the compression we need to convert the image to the <strong>BMP (Bitmap Image File)<\/strong> or there are also some other formats, if we convert to this we get the uncompressed image. But we don&#8217;t need this we will extract those raw bytes and play with them and we will again convert them back to PNG or JPG.<\/p>\n\n\n\n<p>First, let&#8217;s set the client to upload images, I will set a simple react application for this<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import axios from \"axios\";\nimport { useState } from \"react\";\nimport \".\/App.css\";\n\nfunction App() {\n  const &#091;img, setImg] = useState(null);\n  const &#091;pending, setPending] = useState(false);\n\n  const handleImg = (e) =&gt; {\n    setImg(e.target.files&#091;0]);\n  };\n\n  const handleSubmit = async (e) =&gt; {\n    e.preventDefault();\n    if (!img) return;\n\n    const formData = new FormData();\n    formData.append(\"img\", img);\n\n    try {\n      setPending(true);\n      const response = await axios.post(\"http:\/\/localhost:4000\/img\", formData);\n      console.log(response.data);\n\n    } catch (error) {\n      console.log(\"Error uploading image:\", error);\n    } finally {\n      setPending(false);\n    }\n  };\n\n  return (\n    &lt;div className=\"app-container\"&gt;\n      &lt;div className=\"form-container\"&gt;\n        &lt;form onSubmit={handleSubmit}&gt;\n          &lt;input type=\"file\" name=\"img\" accept=\"image\/*\" onChange={handleImg} \/&gt;\n          &lt;br \/&gt;\n          &lt;button type=\"submit\" disabled={pending}&gt;\n            {pending ? \"Uploading...\" : \"Upload Image\"}\n          &lt;\/button&gt;\n        &lt;\/form&gt;\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n  );\n}\n\nexport default App;<\/code><\/pre>\n\n\n\n<p>So this is simple code, to upload the images, the main part is on the server side.<\/p>\n\n\n\n<p>Now let&#8217;s manually calculate the image bytes and check with the server-side code.<\/p>\n\n\n\n<p>I have chosen the below image.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/articles\/jt3p1z8xkv6owmnnd9ye.png\" alt=\"sample image\" \/><\/figure>\n\n\n\n<p>Let&#8217;s take this image. So this is a PNG file. If we go to the properties section, we can see the width and the height of the image. For this, the width and the height are <code>722 x 407<\/code>, which is equal to <code>293854<\/code> pixels; also, this is not a total number of bytes, it is just a total number of pixels. As we know, each pixel is either 3 or 4 bytes, RGB or RGBA. So if the above image is RGB, the total bytes would be <code>722 x 407 x 3 = 881562<\/code>, or if the image has the alpha channel, then the total bytes would be <code>722 x 407 x 4 = 1175416<\/code>.<\/p>\n\n\n\n<p>Let&#8217;s some to the server side, I am using the node js.<\/p>\n\n\n\n<p>There is a library called multer to parse multiform data.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>app.post(\"\/img\", upload.single(\"img\"), async (req, res) =&gt; {\n  const arr = req.file.buffer\n  console.log(arr.length)    \/\/output: 30929\n  res.send(\"success\")\n});<\/code><\/pre>\n\n\n\n<p>We store the image bytes in the buffer array, if we take the length of the buffer array the answer is <code>30929<\/code>, there are these many bytes in the array, but wait the total number of bytes should be <code>1175416<\/code> right? What happens here is multer doesn&#8217;t do some compression or anything, it just gets the image from the user and stores it in the buffer as it is, so we uploaded the PNG file, the buffer you are seeing is the same size as the PNG image size.<\/p>\n\n\n\n<p>Now let&#8217;s change the bytes in the compressed image byte.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">app.post(\"\/img\", upload.single(\"img\"), async (req, res) =&gt; {\n  const arr = req.file.buffer;\n  console.log(\"multer \" + arr.length);\n  fs.writeFile(\"output.png\", arr, (err) =&gt; {\n    console.log(err);\n  });\n  res.send(\"successfull\");\n});<\/pre>\n\n\n\n<p>I used the fs to create a new image with the existing one. So now if we change the first-byte arr[0] = 231, the image will not open.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/articles\/g9l2fqxpu00pkllv14m8.png\" alt=\"corrupted image\" \/><\/figure>\n\n\n\n<p>Because the first certain bytes are reserved for the metadata, so if we change those metadata, and then the image can corrupt.<\/p>\n\n\n\n<p>So let&#8217;s jump to the 500th byte. arr[500] = 123, then write the image. But now, the image is broke, we should not directly manipulate the compressed image bytes because it can change the compression algorithm encoded data.<\/p>\n\n\n\n<p>We need the raw bytes from the image, and then we can independently manipulate the bytes, and for that, we can use a <strong>sharp<\/strong> library.<\/p>\n\n\n\n<p><code>npm install sharp<\/code><\/p>\n\n\n\n<p>install the sharp, Now I will create a separate file to handle those logics,<\/p>\n\n\n\n<p><code>sharp.js<\/code><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export async function convert(buffer) {\n  try {\n    const data = await sharp(buffer).metadata();\n    console.log(data)\n  }catch(err){\n    console.log(err)\n  }\n}<\/code><\/pre>\n\n\n\n<p>This is an async function, Now let&#8217;s get the metadata from the png we have uploaded.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  format: 'png',\n  size: 30929,\n  width: 722,\n  height: 407,\n  space: 'srgb',\n  channels: 4,\n  depth: 'uchar',\n  density: 72,\n  isProgressive: false,\n  hasProfile: false,\n  hasAlpha: true\n}<\/code><\/pre>\n\n\n\n<p>This is the metadata from the image, as we can see the last data <code>hasAlpha: true<\/code> so it has the alpha channel, so each pixel is 4 bytes.<\/p>\n\n\n\n<p>Now let&#8217;s get the raw bytes from the image.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const rawBytes = await sharp(buffer)\n      .raw()\n      .toBuffer({ resolveWithObject: true });\n\nconsole.log(rawBytes.data.length)  \/\/1175416<\/code><\/pre>\n\n\n\n<p>Now we can see the array length is equal to our calculation. So this image contains <code>1175416<\/code> bytes. <strong>Now we are free..<\/strong> to change any bytes, Now the metadata is not stored in the buffer, the buffer only contains the raw bytes of the image.<\/p>\n\n\n\n<p>Let&#8217;s change only one pixel to red.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>  rawBytes.data&#091;0] = 225;    \/\/red\n  rawBytes.data&#091;1] = 10;     \/\/green\n  rawBytes.data&#091;2] = 10;     \/\/blue\n  rawBytes.data&#091;3] = Math.floor(0.8 * 255);   \/\/alpha<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/articles\/vcxvpmxy2lk0j3sn8dua.png\" alt=\"one pixel\" \/><\/figure>\n\n\n\n<p>As we can one pixel is changed to red, we need to zoom in on the image to see the pixel change.<\/p>\n\n\n\n<p>Now let&#8217;s divide the image and change the color, the top half is yellow and the bottom half is green<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const div = rawBytes.data.length \/ 2;\n    for (let i = 0; i &lt; rawBytes.data.length; i += 4) {\n      if (i &lt;= div) {\n        rawBytes.data&#091;i] = 240;\n        rawBytes.data&#091;i + 1] = 255;\n        rawBytes.data&#091;i + 2] = 21;\n        rawBytes.data&#091;i + 3] = Math.floor(0.8 * 255);\n      } else {\n        rawBytes.data&#091;i] = 84;\n        rawBytes.data&#091;i + 1] = 135;\n        rawBytes.data&#091;i + 2] = 21;\n        rawBytes.data&#091;i + 3] = Math.floor(0.9 * 255);\n      }\n    }<\/code><\/pre>\n\n\n\n<p>We are incrementing the loop by 4 times because we are changing one pixel at each iteration. Now the output will be like this.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/articles\/yztjcy3wi9n4659v2uck.png\" alt=\"final output\" \/><\/figure>\n\n\n\n<p>We can see the transparency in this image because the Alpha channel is set to <code>0.8<\/code><\/p>\n\n\n\n<p>I forgot to tell for writing the image, we don&#8217;t need <code>fs<\/code> to write a new image, we can use the sharp itself.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>await sharp(rawBytes.data, {\n      raw: {\n        width: data.width,\n        height: data.height,\n        channels: data.channels,\n      },\n    })\n      .png()\n      .toFile(\"demo.png\");<\/code><\/pre>\n\n\n\n<p>we are generating the new image with the same metadata.<\/p>\n\n\n\n<p>Here&#8217;s the full server side code,<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/index.js\nimport express from \"express\";\nimport dotenv from \"dotenv\";\nimport multer from \"multer\";\nimport cors from \"cors\";\nimport { convert } from \".\/sharp.js\";\n\nconst app = express();\ndotenv.config();\napp.use(cors({ origin: \"http:\/\/localhost:5173\" }));\nconst storage = multer.memoryStorage();\nconst upload = multer();\n\napp.post(\"\/img\", upload.single(\"img\"), async (req, res) =&gt; {\n  const arr = req.file.buffer;\n  await convert(arr);\n  res.send(\"successful\");\n});\n\napp.listen(process.env.PORT, () =&gt; {\n  console.log(\"server started\");\n});<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/sharp.js\nimport sharp from \"sharp\";\n\nexport async function convert(buffer) {\n  try {\n    const data = await sharp(buffer).metadata();\n    console.log(data);\n    \/\/raw data\n    const rawBytes = await sharp(buffer)\n      .raw()\n      .toBuffer({ resolveWithObject: true });\n    console.log(rawBytes.data.length);\n    const div = rawBytes.data.length \/ 2;\n    for (let i = 0; i &lt; rawBytes.data.length; i += 4) {\n      if (i &lt;= div) {\n        rawBytes.data&#091;i] = 240;\n        rawBytes.data&#091;i + 1] = 255;\n        rawBytes.data&#091;i + 2] = 21;\n        rawBytes.data&#091;i + 3] = Math.floor(0.8 * 255);\n      } else {\n        rawBytes.data&#091;i] = 84;\n        rawBytes.data&#091;i + 1] = 135;\n        rawBytes.data&#091;i + 2] = 21;\n        rawBytes.data&#091;i + 3] = Math.floor(0.9 * 255);\n      }\n    }\n    await sharp(rawBytes.data, {\n      raw: {\n        width: data.width,\n        height: data.height,\n        channels: data.channels,\n      },\n    })\n      .png()\n      .toFile(\"demo.png\");\n  } catch (error) {\n    console.log(error.message);\n  }\n}<\/code><\/pre>\n\n\n\n<p>So this is it, we just played with those pixels. And finally, the below image is made with this one line in the loop.<\/p>\n\n\n\n<p>rawBytes.data[i] = Math.floor(Math.random()*256)<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"635\" height=\"382\" src=\"https:\/\/www.thealteroffice.com\/blog\/wp-content\/uploads\/2025\/06\/1_JZGOosBJoUNgd6-zASoT4w.webp\" alt=\"\" class=\"wp-image-622\" style=\"width:508px;height:auto\" srcset=\"https:\/\/www.thealteroffice.com\/blog\/wp-content\/uploads\/2025\/06\/1_JZGOosBJoUNgd6-zASoT4w.webp 635w, https:\/\/www.thealteroffice.com\/blog\/wp-content\/uploads\/2025\/06\/1_JZGOosBJoUNgd6-zASoT4w-300x180.webp 300w\" sizes=\"auto, (max-width: 635px) 100vw, 635px\" \/><\/figure>\n\n\n\n<pre class=\"wp-block-code\"><code>\nI just randomly changed each byte<\/code><\/pre>\n\n\n\n<p>For the full code, check out the repo: <a href=\"https:\/\/github.com\/sanjayr-12\/pixel-byte-manipulation\">pixel-byte-manipulation<\/a><\/p>\n\n\n\n<p><strong>Thank you!!!<\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hi, I\u2019m Sanjay. I\u2019ve been working as an intern at The Alter Office for the past two months. I enjoy exploring new technologies and learning how things work. I&#8217;m currently working as a backend developer using Node.js. This manipulation idea came to me while using the multer npm package. Today, let&#8217;s play with image raw [&hellip;]<\/p>\n","protected":false},"author":13,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[41],"tags":[43,42],"class_list":["post-620","post","type-post","status-publish","format-standard","hentry","category-dev-diaries","tag-javascript","tag-node"],"aioseo_notices":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.thealteroffice.com\/blog\/wp-json\/wp\/v2\/posts\/620","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.thealteroffice.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.thealteroffice.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.thealteroffice.com\/blog\/wp-json\/wp\/v2\/users\/13"}],"replies":[{"embeddable":true,"href":"https:\/\/www.thealteroffice.com\/blog\/wp-json\/wp\/v2\/comments?post=620"}],"version-history":[{"count":6,"href":"https:\/\/www.thealteroffice.com\/blog\/wp-json\/wp\/v2\/posts\/620\/revisions"}],"predecessor-version":[{"id":627,"href":"https:\/\/www.thealteroffice.com\/blog\/wp-json\/wp\/v2\/posts\/620\/revisions\/627"}],"wp:attachment":[{"href":"https:\/\/www.thealteroffice.com\/blog\/wp-json\/wp\/v2\/media?parent=620"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.thealteroffice.com\/blog\/wp-json\/wp\/v2\/categories?post=620"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.thealteroffice.com\/blog\/wp-json\/wp\/v2\/tags?post=620"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}