pdf-fixture.ts 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
  1. /** Deterministic two-page PDF with colored rectangles, selectable text, and an explicit xref table. */
  2. /**
  3. * @param userUnit - Page-coordinate unit size as a multiple of 1/72 inch.
  4. * @param rotation - Clockwise page rotation in degrees.
  5. * @returns complete PDF bytes; no clocks, external fonts, images, or network references.
  6. */
  7. export function pdfFixture(userUnit = 1, rotation = 0): Uint8Array {
  8. const streams = ['0.9 0.1 0.1 rg 10 10 100 80 re f', '0.1 0.1 0.9 rg 10 10 100 80 re f']
  9. .map(stream => `${stream}\nBT /F1 8 Tf 0 0 0 rg 10 92 Td (Selectable PDF text) Tj ET`)
  10. const objects = [
  11. '<< /Type /Catalog /Pages 2 0 R >>',
  12. '<< /Type /Pages /Kids [3 0 R 5 0 R] /Count 2 >>',
  13. `<< /Type /Page /Parent 2 0 R /UserUnit ${userUnit} /Rotate ${rotation} /MediaBox [0 0 120 100] /Resources << /Font << /F1 7 0 R >> >> /Contents 4 0 R >>`,
  14. `<< /Length ${streams[0]!.length} >>\nstream\n${streams[0]}\nendstream`,
  15. `<< /Type /Page /Parent 2 0 R /UserUnit ${userUnit} /Rotate ${rotation} /MediaBox [0 0 120 100] /Resources << /Font << /F1 7 0 R >> >> /Contents 6 0 R >>`,
  16. `<< /Length ${streams[1]!.length} >>\nstream\n${streams[1]}\nendstream`,
  17. '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
  18. ]
  19. return pdfObjects(objects)
  20. }
  21. /** @returns a journal-like page with table cells, spaced text fragments, and blank regions for drag selection. */
  22. export function selectionPdfFixture(): Uint8Array {
  23. const lines: Array<[number, number, string]> = [
  24. [40, 460, 'JOURNAL'], [40, 415, 'TODAY'],
  25. [40, 385, '*'], [40, 355, '*'], [40, 325, '*'],
  26. [40, 275, 'THREE TASKS'], [50, 245, 'NUMBER'], [180, 245, 'PRIORITY'], [310, 245, 'DONE'],
  27. [50, 215, '1'], [180, 215, 'HIGH / MEDIUM / LOW'], [310, 215, '[ ]'],
  28. [50, 185, '2'], [180, 185, 'HIGH / MEDIUM / LOW'], [310, 185, '[ ]'],
  29. [50, 155, '3'], [180, 155, 'HIGH / MEDIUM / LOW'], [310, 155, '[ ]'],
  30. [40, 105, 'REFLECTION'], [40, 65, '*'], [40, 35, 'AFTER TABLE'],
  31. ]
  32. const stream = lines.map(([x, y, value]) => `BT /F1 10 Tf ${x} ${y} Td (${value}) Tj ET`).join('\n')
  33. return pdfObjects([
  34. '<< /Type /Catalog /Pages 2 0 R >>',
  35. '<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
  36. '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 500] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>',
  37. `<< /Length ${stream.length} >>\nstream\n${stream}\nendstream`,
  38. '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
  39. ])
  40. }
  41. function pdfObjects(objects: readonly string[]): Uint8Array {
  42. let text = '%PDF-1.7\n'
  43. const offsets = [0]
  44. objects.forEach((object, index) => {
  45. offsets.push(text.length)
  46. text += `${index + 1} 0 obj\n${object}\nendobj\n`
  47. })
  48. const xref = text.length
  49. text += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`
  50. for (const offset of offsets.slice(1)) text += `${String(offset).padStart(10, '0')} 00000 n \n`
  51. text += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`
  52. return new TextEncoder().encode(text)
  53. }