| 1 | import { crc32, inflateSync } from "node:zlib"; |
| 2 | import { browserFailure } from "./errors.js"; |
| 3 | |
| 4 | export const MAX_CAPTURE_PIXELS = 16_777_216; |
| 5 | const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); |
| 6 | |
| 7 | export function pngSize(data: Buffer): { width: number; height: number } { |
| 8 | if (data.length < 33 || !data.subarray(0, 8).equals(signature) || data.toString("ascii", 12, 16) !== "IHDR") { |
| 9 | throw browserFailure("invalid_image", "capture did not return a PNG image"); |
| 10 | } |
| 11 | const width = data.readUInt32BE(16), height = data.readUInt32BE(20); |
| 12 | if (width === 0 || height === 0 || width * height > MAX_CAPTURE_PIXELS) { |
| 13 | throw browserFailure("invalid_image", "capture has empty or excessive pixel dimensions; capture a smaller region"); |
| 14 | } |
| 15 | return { width, height }; |
| 16 | } |
| 17 | |
| 18 | export function validatePNG(data: Buffer): { width: number; height: number } { |
| 19 | const size = pngSize(data); |
| 20 | const compressed: Buffer[] = []; |
| 21 | let ended = false; |
| 22 | for (let offset = 8; offset + 12 <= data.length;) { |
| 23 | const length = data.readUInt32BE(offset); |
| 24 | const end = offset + 8 + length; |
| 25 | if (end + 4 > data.length || crc32(data.subarray(offset + 4, end)) !== data.readUInt32BE(end)) { |
| 26 | throw browserFailure("invalid_image", "capture returned a truncated or corrupt PNG"); |
| 27 | } |
| 28 | const kind = data.toString("ascii", offset + 4, offset + 8); |
| 29 | if (kind === "IDAT") compressed.push(data.subarray(offset + 8, end)); |
| 30 | if (kind === "IEND") { ended = length === 0 && end + 4 === data.length; break; } |
| 31 | offset = end + 4; |
| 32 | } |
| 33 | if (!ended || compressed.length === 0) throw browserFailure("invalid_image", "capture returned an incomplete PNG"); |
| 34 | try { |
| 35 | if (inflateSync(Buffer.concat(compressed), { maxOutputLength: MAX_CAPTURE_PIXELS * 8 + 65536 }).length === 0) throw new Error("empty raster"); |
| 36 | } catch { |
| 37 | throw browserFailure("invalid_image", "capture returned an undecodable PNG raster"); |
| 38 | } |
| 39 | return size; |
| 40 | } |
| 41 |