* Helper to create a minimal PDF for testing.
(options: {
version?: string;
headerOffset?: number;
garbageBeforeHeader?: string;
objects?: Array<{ objNum: number; content: string }>;
xrefEntries?: Array<{ objNum: number; offset: number; gen?: number; free?: boolean }>;
extraXrefEntries?: Array<{ objNum: number; offset: number; gen?: number; free?: boolean }>;
trailer?: Record<string, string>;
})
| 11 | * Helper to create a minimal PDF for testing. |
| 12 | */ |
| 13 | function createMinimalPdf(options: { |
| 14 | version?: string; |
| 15 | headerOffset?: number; |
| 16 | garbageBeforeHeader?: string; |
| 17 | objects?: Array<{ objNum: number; content: string }>; |
| 18 | xrefEntries?: Array<{ objNum: number; offset: number; gen?: number; free?: boolean }>; |
| 19 | extraXrefEntries?: Array<{ objNum: number; offset: number; gen?: number; free?: boolean }>; |
| 20 | trailer?: Record<string, string>; |
| 21 | }): Uint8Array { |
| 22 | const parts: string[] = []; |
| 23 | |
| 24 | // Add garbage before header if specified |
| 25 | if (options.garbageBeforeHeader) { |
| 26 | parts.push(options.garbageBeforeHeader); |
| 27 | } |
| 28 | |
| 29 | // Header |
| 30 | const version = options.version ?? "1.4"; |
| 31 | parts.push(`%PDF-${version}\n`); |
| 32 | parts.push("%\x80\x81\x82\x83\n"); // Binary marker |
| 33 | |
| 34 | // Track offsets for xref (byte lengths, not string lengths — the binary |
| 35 | // marker encodes to multiple UTF-8 bytes per char) |
| 36 | const byteLength = (s: string): number => new TextEncoder().encode(s).length; |
| 37 | |
| 38 | const offsets: Array<{ objNum: number; offset: number; gen: number; free: boolean }> = []; |
| 39 | let currentOffset = byteLength(parts.join("")); |
| 40 | |
| 41 | // Objects |
| 42 | const objects = options.objects ?? [ |
| 43 | { objNum: 1, content: "<< /Type /Catalog /Pages 2 0 R >>" }, |
| 44 | { objNum: 2, content: "<< /Type /Pages /Kids [] /Count 0 >>" }, |
| 45 | ]; |
| 46 | |
| 47 | for (const obj of objects) { |
| 48 | offsets.push({ objNum: obj.objNum, offset: currentOffset, gen: 0, free: false }); |
| 49 | const objStr = `${obj.objNum} 0 obj\n${obj.content}\nendobj\n`; |
| 50 | parts.push(objStr); |
| 51 | currentOffset += byteLength(objStr); |
| 52 | } |
| 53 | |
| 54 | // Use provided xref entries or build from objects |
| 55 | const xrefEntries = options.xrefEntries ?? [ |
| 56 | { objNum: 0, offset: 0, gen: 65535, free: true }, |
| 57 | ...offsets, |
| 58 | ]; |
| 59 | |
| 60 | if (options.extraXrefEntries) { |
| 61 | xrefEntries.push(...options.extraXrefEntries); |
| 62 | } |
| 63 | |
| 64 | // XRef table |
| 65 | const xrefOffset = currentOffset; |
| 66 | parts.push("xref\n"); |
| 67 | parts.push(`0 ${xrefEntries.length}\n`); |
| 68 | |
| 69 | for (const entry of xrefEntries) { |
| 70 | const offsetStr = entry.offset.toString().padStart(10, "0"); |
no test coverage detected