// Pure JPEG DPI metadata patcher. // Re-encoding a JPEG through Skia drops the original EXIF/JFIF headers, so the // DPI stamp ("72 dpi") is lost or defaulted. DPI is header metadata only — pixel // dimensions never change — but print pipelines read it, so this rewrites the // JFIF density (and EXIF XResolution/YResolution when present) to the requested // value. All writes are same-length, so no segment offsets move. // // If the JPEG carries neither JFIF nor EXIF, a minimal JFIF APP0 segment is // inserted right after SOI. function u16(bytes: Uint8Array, p: number): number { return (bytes[p] << 8) | bytes[p + 1]; } function setU16(bytes: Uint8Array, p: number, v: number): void { bytes[p] = (v >> 8) & 0xff; bytes[p + 1] = v & 0xff; } // Patches `bytes` in place for JFIF; returns the (possibly rebuilt) array. export function patchJpegDpi(input: Uint8Array, dpi: number): Uint8Array { if (input.length < 2 || input[0] !== 0xff || input[1] !== 0xd8) return input; const bytes = input.slice(); // work on a copy; insertion path rebuilds anyway const dpiLo = dpi & 0xff; const dpiHi = (dpi >> 8) & 0xff; let sawJfif = false; let sawExif = false; let i = 2; while (i + 1 < bytes.length && bytes[i] === 0xff) { while (bytes[i] === 0xff) i++; const marker = bytes[i]; i++; if (marker === 0xd9 || marker === 0xda) break; // EOI / SOS — headers only if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) continue; if (i + 2 > bytes.length) break; const len = u16(bytes, i); if (len < 2 || i + len > bytes.length) break; const dataStart = i + 2; const payloadLen = len - 2; // APP0 JFIF: [FF E0] len [5x id "JFIF\0"][2 ver][1 units][2 Xd][2 Yd][1 thW][1 thH]... if (marker === 0xe0 && payloadLen >= 14 && bytes[dataStart] === 0x4a && bytes[dataStart + 1] === 0x46 && bytes[dataStart + 2] === 0x49 && bytes[dataStart + 3] === 0x46 && bytes[dataStart + 4] === 0x00) { sawJfif = true; bytes[dataStart + 7] = 1; // units = dots per inch bytes[dataStart + 8] = dpiHi; bytes[dataStart + 9] = dpiLo; bytes[dataStart + 10] = dpiHi; bytes[dataStart + 11] = dpiLo; } // APP1 EXIF: [FF E1] len "Exif\0\0" then TIFF if (marker === 0xe1 && payloadLen >= 14 && bytes[dataStart] === 0x45 && bytes[dataStart + 1] === 0x78 && bytes[dataStart + 2] === 0x69 && bytes[dataStart + 3] === 0x66 && bytes[dataStart + 4] === 0x00 && bytes[dataStart + 5] === 0x00) { sawExif = true; const tiff = dataStart + 6; const le = bytes[tiff] === 0x49 && bytes[tiff + 1] === 0x49; const be = bytes[tiff] === 0x4d && bytes[tiff + 1] === 0x4d; if ((le || be) && tiff + 8 <= bytes.length) { const read32 = (p: number): number => le ? bytes[p] | (bytes[p + 1] << 8) | (bytes[p + 2] << 16) | (bytes[p + 3] << 24) : ((bytes[p] << 24) | (bytes[p + 1] << 16) | (bytes[p + 2] << 8) | bytes[p + 3]) >>> 0; const write32 = (p: number, v: number): void => { if (le) { bytes[p] = v & 0xff; bytes[p + 1] = (v >> 8) & 0xff; bytes[p + 2] = (v >> 16) & 0xff; bytes[p + 3] = (v >> 24) & 0xff; } else { bytes[p] = (v >> 24) & 0xff; bytes[p + 1] = (v >> 16) & 0xff; bytes[p + 2] = (v >> 8) & 0xff; bytes[p + 3] = v & 0xff; } }; // TIFF integer fields (tag/type/count/values) follow the file byte order const u16r = (p: number): number => le ? bytes[p] | (bytes[p + 1] << 8) : (bytes[p] << 8) | bytes[p + 1]; const u16w = (p: number, v: number): void => { if (le) { bytes[p] = v & 0xff; bytes[p + 1] = (v >> 8) & 0xff; } else { bytes[p] = (v >> 8) & 0xff; bytes[p + 1] = v & 0xff; } }; const ifdOff = read32(tiff + 4); if (ifdOff + 2 <= bytes.length - tiff) { const ifd = tiff + ifdOff; const count = u16r(ifd); for (let e = 0; e < count; e++) { const entry = ifd + 2 + e * 12; if (entry + 12 > bytes.length) break; const tag = u16r(entry); const type = u16r(entry + 2); const cnt = read32(entry + 4); if ((tag === 0x011a || tag === 0x011b) && type === 5 && cnt === 1) { // RATIONAL: entry+8 holds offset to (num, den), relative to TIFF start const rat = tiff + read32(entry + 8); if (rat + 8 <= bytes.length) write32(rat, dpi); // keep denominator } else if (tag === 0x0128 && type === 3 && cnt === 1) { // ResolutionUnit (2 = inch): normalize to inch so the new density reads correctly u16w(entry + 8, 2); } } } } } i += len; } if (sawJfif || sawExif) return bytes; // Neither segment exists — insert a minimal JFIF APP0 (density = dpi inch) after SOI. const app0 = new Uint8Array([ 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, // "JFIF\0" 0x01, 0x01, // version 1.01 0x01, // units: inch dpiHi, dpiLo, dpiHi, dpiLo, 0x00, 0x00, // no thumbnail ]); const out = new Uint8Array(bytes.length + app0.length); out[0] = 0xff; out[1] = 0xd8; out.set(app0, 2); out.set(bytes.subarray(2), 2 + app0.length); return out; }