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);
};