Get Image dimensions without dependencies – node or deno
Published on 28 November 2023
I was building a pre-rendered website recently. The client (my dad) could upload images and I needed a way to use their width and height in the HTML. I am trying to not use npm and rather write all the code by hand (and yes, also copying and pasting bits from various open source projects) so that I have at least attentively read every line. The thinking I have is always – what’s the minimum amount of straightforward code that can achieve what I need, with API that makes sense for my work, and ideally using platform features so I learn more about the standard environment in which my code runs.
So here’s the `util/get-image-dimensions.js` that works for JPEG, PNG, GIF and WebP image formats:
So here’s the `util/get-image-dimensions.js` that works for JPEG, PNG, GIF and WebP image formats:
import fs from 'node:fs'; // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation // files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following // conditions: // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. export function getImageDimensions(imagePath) { const data = fs.readFileSync(imagePath); const header = data.toString('hex', 0, 12); const type = getImageType(header); let width, height; switch (type) { case 'jpeg': const dimensions = getJpegDimensions(data); width = dimensions.width; height = dimensions.height; break; case 'png': width = data.readUInt32BE(16); height = data.readUInt32BE(20); break; case 'gif': width = data.readUInt16LE(6); height = data.readUInt16LE(8); break; case 'webp': const vp8Header = data.slice(12, 16).toString('hex'); if (vp8Header === '9d012a') { width = data.readUIntLE(26, 2); height = data.readUIntLE(28, 2); } else if (vp8Header === '2a012a') { const alphaOffset = data.indexOf('414c4641', 20, 'hex'); if (alphaOffset !== -1) { width = data.readUIntLE(26, 2); height = data.readUIntLE(alphaOffset + 16, 2); } } break; default: throw new Error('Unsupported image type'); } return { width, height, type }; } function getImageType(header) { if (header.startsWith('ffd8')) { return 'jpeg'; } else if (header.startsWith('89504e470d0a1a0a')) { return 'png'; } else if (header.startsWith('47494638')) { return 'gif'; } else if (header.startsWith('52494646') && header.endsWith('57454250')) { return 'webp'; } else { throw new Error('Unsupported image type'); } } // Copyright (c) 2015 Vitaly Puzrin. // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation // files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following // conditions: // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // // Following is extract from probe-image-size library function getJpegDimensions(data) { if (data.length < 2) return; // first marker of the file MUST be 0xFFD8, // following by either 0xFFE0, 0xFFE2 or 0xFFE3 if (data[0] !== 0xFF || data[1] !== 0xD8 || data[2] !== 0xFF) return; var offset = 2; for (;;) { // skip until we see 0xFF, see https://github.com/nodeca/probe-image-size/issues/68 for (;;) { if (data.length - offset < 2) return; if (data[offset++] === 0xFF) break; } var code = data[offset++]; var length; // skip padding bytes while (code === 0xFF) code = data[offset++]; // standalone markers, according to JPEG 1992, // http://www.w3.org/Graphics/JPEG/itu-t81.pdf, see Table B.1 if ((0xD0 <= code && code <= 0xD9) || code === 0x01) { length = 0; } else if (0xC0 <= code && code <= 0xFE) { // the rest of the unreserved markers if (data.length - offset < 2) return; length = readUInt16BE(data, offset) - 2; offset += 2; } else { // unknown markers return; } if (code === 0xD9 /* EOI */ || code === 0xDA /* SOS */) { // end of the datastream return; } if (length >= 5 && (0xC0 <= code && code <= 0xCF) && code !== 0xC4 && code !== 0xC8 && code !== 0xCC) { if (data.length - offset < length) return; var result = { width: readUInt16BE(data, offset + 3), height: readUInt16BE(data, offset + 1), }; return result; } offset += length; } } function readUInt16BE(data, offset) { return data[offset + 1] | (data[offset] << 8); };