An exported product PNG can have the wrong pixel dimensions even when its filename and preview look right. If your workflow already knows the required width and height, check those numbers before assembling the listing. Node.js can read the PNG header for this check without decoding pixel data.
1. Define the expected canvas
Use the size in your production brief. For a fictional example, the main image is expected to be 1200 by 900 pixels. The check compares exact dimensions, so a 900 by 1200 export will fail even though it contains the same number of pixels.
2. Read the first 33 bytes
Save the function below as png-size.mjs. A PNG starts with an eight-byte signature, then its IHDR chunk. The first 33 bytes include that chunk's length, type, 13-byte data field and checksum. Node.js 24 provides the CRC calculation used here.
import { closeSync, fstatSync, openSync, readSync } from "node:fs";
import { crc32 } from "node:zlib";
const signature = Buffer.from("89504e470d0a1a0a", "hex");
const limit = 0x7fffffff;
export function parsePngDimensions(bytes) {
if (!Buffer.isBuffer(bytes) || bytes.length < 33 ||
!bytes.subarray(0, 8).equals(signature)) {
throw new Error("Missing PNG signature or complete first chunk");
}
if (bytes.readUInt32BE(8) !== 13 || bytes.toString("latin1", 12, 16) !== "IHDR") {
throw new Error("Expected a 13-byte IHDR first chunk");
}
if (crc32(bytes.subarray(12, 29)) !== bytes.readUInt32BE(29)) {
throw new Error("IHDR checksum mismatch");
}
const width = bytes.readUInt32BE(16);
const height = bytes.readUInt32BE(20);
if (width < 1 || width > limit || height < 1 || height > limit) {
throw new RangeError("PNG dimensions are outside the allowed range");
}
return { width, height };
}
export function readPngDimensions(path) {
const fd = openSync(path, "r");
try {
if (!fstatSync(fd).isFile()) throw new Error("Expected a regular file");
const header = Buffer.alloc(33);
let used = 0;
while (used < header.length) {
const count = readSync(fd, header, used, header.length - used, used);
if (count === 0) break;
used += count;
}
return parsePngDimensions(header.subarray(0, used));
} finally {
closeSync(fd);
}
}
The width and height are stored as big-endian integers in the chunk data. The checksum covers the chunk type and data. This helper checks the dimensions and the first chunk's checksum. Checking the rest of the image requires a PNG decoder or validator.
3. Compare one file with the brief
Save this as check-size.mjs. The command returns 0 for a match, 1 for a size mismatch and 2 when it cannot complete the header check.
import { readPngDimensions } from "./png-size.mjs";
const [file, targetWidth, targetHeight] = process.argv.slice(2);
try {
if (!file || !/^[1-9]\d*$/.test(targetWidth ?? "") ||
!/^[1-9]\d*$/.test(targetHeight ?? "") ||
!Number.isSafeInteger(Number(targetWidth)) ||
!Number.isSafeInteger(Number(targetHeight))) {
throw new Error("Usage: node check-size.mjs image.png width height");
}
const { width, height } = readPngDimensions(file);
const matches = width === Number(targetWidth) && height === Number(targetHeight);
console.log(JSON.stringify({ width, height, matches }));
process.exitCode = matches ? 0 : 1;
} catch (error) {
console.error(error.message);
process.exitCode = 2;
}
4. Run the check at handoff
Run the command below from the directory that contains your file. If the output says matches is false, inspect the crop or export settings, then check the next version. Keep the original image and rerun the command on the new file.
node check-size.mjs hero.png 1200 900
5. Test valid and damaged headers
Save these checks as test.mjs and run node test.mjs. The helper creates header fixtures for the test. These fixtures contain no image data.
import assert from "node:assert/strict";
import { crc32 } from "node:zlib";
import { parsePngDimensions } from "./png-size.mjs";
function header(width, height) {
const bytes = Buffer.alloc(33);
Buffer.from("89504e470d0a1a0a", "hex").copy(bytes);
bytes.writeUInt32BE(13, 8);
bytes.write("IHDR", 12, "latin1");
bytes.writeUInt32BE(width, 16);
bytes.writeUInt32BE(height, 20);
bytes[24] = 8;
bytes[25] = 6;
bytes.writeUInt32BE(crc32(bytes.subarray(12, 29)), 29);
return bytes;
}
const good = header(1200, 900);
assert.deepEqual(parsePngDimensions(good), { width: 1200, height: 900 });
assert.deepEqual(parsePngDimensions(header(1, 1)), { width: 1, height: 1 });
assert.deepEqual(parsePngDimensions(header(0x7fffffff, 2)), { width: 0x7fffffff, height: 2 });
assert.throws(() => parsePngDimensions(good.subarray(0, 32)), /first chunk/);
assert.throws(() => parsePngDimensions(Buffer.alloc(33)), /signature/);
assert.throws(() => parsePngDimensions(header(0, 2)), RangeError);
assert.throws(() => parsePngDimensions(header(1, 0x80000000)), RangeError);
const corrupt = Buffer.from(good);
corrupt[19] ^= 1;
assert.throws(() => parsePngDimensions(corrupt), /checksum/);
const wrongChunk = Buffer.from(good);
wrongChunk.write("IDAT", 12, "latin1");
assert.throws(() => parsePngDimensions(wrongChunk), /IHDR/);
const wrongLength = Buffer.from(good);
wrongLength.writeUInt32BE(12, 8);
assert.throws(() => parsePngDimensions(wrongLength), /13-byte/);
console.log("10 checks passed");
6. Keep the check small
Use the result as an early check in an export script. Keep separate checks for whether the file decodes and whether the final image shows the intended product. A correct width and height can still accompany a crop that hides a label or removes part of the product.
Drafted with AI assistance.
PixMind source: AI Product Photo Generator: Complete Guide for Ecommerce.
Technical references: W3C PNG IHDR and Node.js 24 zlib.
About the author
- The PixMind team shares practical AI image and video workflows. PixMind brings text-to-image, reference-based creation, editing, and video tools into one workspace. Website: https://www.pixmind.io/
Comments
Join the discussionNo comments yet. Be the first to comment.