Files
Voiced/src/lib/imageSize.ts
T
xavierk f7a1302e94 Fix header overlap in invoice PDF and Bongbetic wordmark on light theme
react-pdf wraps text once at the first flex measure; when yoga then shrank
the vendor column the cached lines ran under the INVOICE meta column. The
header now pins column widths (299 + 24 + 192 pt), sizes the logo from its
own aspect (stacked above the text when it would leave under 160 pt), uses
absolute per-Text line heights, and turns hyphenation off.

Also: repeating table header with an orphan-header guard, wrap=false rows,
Page n of N footer, GSTIN printed only when present, one Text per address
line, HTML twin header grid. The light-theme header showed the white-text
wordmark; the ternary was inverted.

Adds vitest + pdfjs-dist harness that fails on the old document (address
overlaps Invoice No.) and passes on the new one.
2026-10-04 03:57:47 +05:30

63 lines
1.9 KiB
TypeScript

export interface ImageSize {
width: number;
height: number;
}
function base64ToBytes(b64: string): Uint8Array {
const bin = atob(b64);
const out = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
return out;
}
const u16 = (b: Uint8Array, i: number) => (b[i] << 8) | b[i + 1];
const u32 = (b: Uint8Array, i: number) => ((b[i] << 24) | (b[i + 1] << 16) | (b[i + 2] << 8) | b[i + 3]) >>> 0;
function pngSize(b: Uint8Array): ImageSize | null {
const sig = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
if (b.length < 24 || sig.some((v, i) => b[i] !== v)) return null;
return { width: u32(b, 16), height: u32(b, 20) };
}
function jpegSize(b: Uint8Array): ImageSize | null {
if (b.length < 4 || b[0] !== 0xff || b[1] !== 0xd8) return null;
let i = 2;
while (i + 3 < b.length) {
if (b[i] !== 0xff) {
i++;
continue;
}
const marker = b[i + 1];
if (marker === 0xff) {
i++; // fill byte
continue;
}
// Standalone markers carry no length.
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd8)) {
i += 2;
continue;
}
const isSof =
marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
if (isSof) {
if (i + 8 >= b.length) return null;
return { height: u16(b, i + 5), width: u16(b, i + 7) };
}
i += 2 + u16(b, i + 2);
}
return null;
}
/** Pixel size of a PNG or JPEG data URI, or null when it is another format or malformed. */
export function imageSizeFromDataUri(uri: string): ImageSize | null {
const comma = uri.indexOf(",");
if (!uri.startsWith("data:") || comma < 0) return null;
try {
const bytes = base64ToBytes(uri.slice(comma + 1));
const size = pngSize(bytes) ?? jpegSize(bytes);
return size && size.width > 0 && size.height > 0 ? size : null;
} catch {
return null;
}
}