Sample PNG file (valid 256)
256x256 RGBA checkerboard, non-interlaced, 8-bit.
↓ Download png-valid-256.png| Filename | png-valid-256.png |
| Format | PNG |
| Size | 1.3 KB (1,309 bytes) |
| MIME type | image/png |
| SHA-256 | febec136e1589e5b7a53bfedddf957a01222b5722edb3735560e5d3ffebe134a |
Handle it in code
Each snippet below was run against this exact file with Python 3.14.4 and Node 24.15.0 when this page was built, and the output shown is what it printed.
Python
Walk the chunks and verify each CRC. Browsers tolerate damage that strict decoders reject, so check before you store or process the image.
import struct, zlib
with open("png-valid-256.png", "rb") as f:
data = f.read()
if data[:8] != b"\x89PNG\r\n\x1a\n":
raise SystemExit("not a PNG")
pos = 8
while True:
if pos + 8 > len(data):
raise SystemExit("truncated: the file ends before an IEND chunk")
length, ctype = struct.unpack(">I4s", data[pos:pos + 8])
end = pos + 12 + length
if end > len(data):
raise SystemExit(f"truncated: the {ctype.decode()} chunk needs {length} bytes")
if zlib.crc32(data[pos + 4:end - 4]) != struct.unpack(">I", data[end - 4:end])[0]:
raise SystemExit(f"bad CRC in the {ctype.decode()} chunk")
if ctype == b"IEND":
break
pos = end
print("chunk structure is valid")Run against this file, it prints: chunk structure is valid
Python
Read the width and height from the header and refuse oversized images before any decoder allocates memory. A 265 KB PNG can declare 8000x8000 pixels, which decodes to 256 MB of RGBA. Pillow's default guard (Image.MAX_IMAGE_PIXELS, about 89 million pixels) lets a 64-million-pixel image through without a warning, so set your own limit.
import struct
MAX_PIXELS = 25_000_000 # a decoder needs about 4 bytes per pixel
def image_size(path):
with open(path, "rb") as f:
head = f.read(64 * 1024)
if head[:8] == b"\x89PNG\r\n\x1a\n":
return "png", *struct.unpack(">II", head[16:24])
if head[:6] in (b"GIF87a", b"GIF89a"):
return "gif", *struct.unpack("<HH", head[6:10])
if head[:2] == b"BM":
w, h = struct.unpack("<ii", head[18:26])
return "bmp", w, abs(h) # a negative height means rows are stored top-down
if head[:4] == b"RIFF" and head[8:12] == b"WEBP":
chunk = head[12:16]
if chunk == b"VP8 ":
w, h = struct.unpack("<HH", head[26:30])
return "webp", w & 0x3FFF, h & 0x3FFF
if chunk == b"VP8L":
bits = int.from_bytes(head[21:25], "little")
return "webp", (bits & 0x3FFF) + 1, (bits >> 14 & 0x3FFF) + 1
if chunk == b"VP8X":
return "webp", int.from_bytes(head[24:27], "little") + 1, int.from_bytes(head[27:30], "little") + 1
if head[:3] == b"\xff\xd8\xff":
i = 2
while i + 9 <= len(head) and head[i] == 0xFF:
marker, length = head[i + 1], int.from_bytes(head[i + 2:i + 4], "big")
if 0xC0 <= marker <= 0xCF and marker not in (0xC4, 0xC8, 0xCC): # start of frame
h, w = struct.unpack(">HH", head[i + 5:i + 9])
return "jpg", w, h
i += 2 + length
raise ValueError("no JPEG frame header in the first 64 KB")
raise ValueError("not a PNG, GIF, BMP, WebP or JPEG")
try:
kind, w, h = image_size("png-valid-256.png")
except ValueError as e:
print("rejected:", e)
else:
if w * h > MAX_PIXELS:
print(f"refused: {kind} {w}x{h} is {w * h:,} pixels, over the {MAX_PIXELS:,} limit")
else:
print(f"ok: {kind} {w}x{h}")Run against this file, it prints: ok: png 256x256
Node.js
The same header check in Node, before the upload reaches sharp, jimp or a canvas.
const fs = require('node:fs');
const MAX_PIXELS = 25_000_000; // a decoder needs about 4 bytes per pixel
function imageSize(file) {
const buf = Buffer.alloc(64 * 1024);
const fd = fs.openSync(file, 'r');
const h = buf.subarray(0, fs.readSync(fd, buf, 0, buf.length, 0));
fs.closeSync(fd);
const ascii = (a, b) => h.toString('latin1', a, b);
if (h.subarray(0, 8).equals(Buffer.from('89504e470d0a1a0a', 'hex'))) return ['png', h.readUInt32BE(16), h.readUInt32BE(20)];
if (ascii(0, 6) === 'GIF87a' || ascii(0, 6) === 'GIF89a') return ['gif', h.readUInt16LE(6), h.readUInt16LE(8)];
if (ascii(0, 2) === 'BM') return ['bmp', h.readInt32LE(18), Math.abs(h.readInt32LE(22))];
if (ascii(0, 4) === 'RIFF' && ascii(8, 12) === 'WEBP') {
const chunk = ascii(12, 16);
if (chunk === 'VP8 ') return ['webp', h.readUInt16LE(26) & 0x3fff, h.readUInt16LE(28) & 0x3fff];
if (chunk === 'VP8L') {
const bits = h.readUInt32LE(21);
return ['webp', (bits & 0x3fff) + 1, ((bits >>> 14) & 0x3fff) + 1];
}
if (chunk === 'VP8X') return ['webp', h.readUIntLE(24, 3) + 1, h.readUIntLE(27, 3) + 1];
}
if (h[0] === 0xff && h[1] === 0xd8 && h[2] === 0xff) {
for (let i = 2; i + 9 <= h.length && h[i] === 0xff; i += 2 + h.readUInt16BE(i + 2)) {
const m = h[i + 1];
if (m >= 0xc0 && m <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(m)) return ['jpg', h.readUInt16BE(i + 7), h.readUInt16BE(i + 5)];
}
throw new Error('no JPEG frame header in the first 64 KB');
}
throw new Error('not a PNG, GIF, BMP, WebP or JPEG');
}
try {
const [kind, w, h] = imageSize('png-valid-256.png');
const n = (x) => x.toLocaleString('en-US');
console.log(w * h > MAX_PIXELS
? 'refused: ' + kind + ' ' + w + 'x' + h + ' is ' + n(w * h) + ' pixels, over the ' + n(MAX_PIXELS) + ' limit'
: 'ok: ' + kind + ' ' + w + 'x' + h);
} catch (e) {
console.log('rejected:', e.message);
}Run against this file, it prints: ok: png 256x256