pdf.ts 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. import { PDFDocument, StandardFonts } from 'pdf-lib'
  2. import { extractText } from 'unpdf'
  3. /**
  4. * Reading what a PDF actually says.
  5. *
  6. * Page count and byte size prove two copies of an invoice are not the same
  7. * document; they say nothing about whether either one is right. The sheet is
  8. * drawn by react-pdf with embedded fonts, and it comes back out as real text
  9. * — the figures formatted exactly as the screen shows them, and a description
  10. * typed over three lines still on three lines.
  11. */
  12. export interface PdfContent {
  13. pages: number
  14. /**
  15. * The file's own size. Text alone cannot see a missing logo or QR code, and
  16. * that is precisely what the emailed copy was once missing, so the weight is
  17. * kept alongside the words.
  18. */
  19. size: number
  20. /** As extracted, one line per line of the sheet. */
  21. text: string
  22. /** The same with every run of whitespace collapsed, for phrase assertions. */
  23. flat: string
  24. }
  25. export async function pdfContent(bytes: Buffer | Uint8Array): Promise<PdfContent> {
  26. const { totalPages, text } = await extractText(new Uint8Array(bytes), { mergePages: true })
  27. const merged = String(text)
  28. return {
  29. pages: totalPages,
  30. size: bytes.byteLength,
  31. text: merged,
  32. flat: merged.replace(/\s+/g, ' ').trim(),
  33. }
  34. }
  35. /**
  36. * A small PDF with words on it, for the tests that attach one to a job. Built
  37. * here rather than committed as a fixture so that what it says is visible to
  38. * whoever reads the spec — and what it says is the point, since the invoice
  39. * appends these pages and the test looks for them.
  40. */
  41. export async function makePdf(pages: string[]): Promise<Buffer> {
  42. const document = await PDFDocument.create()
  43. const font = await document.embedFont(StandardFonts.Helvetica)
  44. for (const line of pages) {
  45. const page = document.addPage([595, 842])
  46. page.drawText(line, { x: 60, y: 700, size: 24, font })
  47. }
  48. return Buffer.from(await document.save())
  49. }
  50. /**
  51. * A 1×1 PNG: the smallest thing the invoice will accept as a photograph.
  52. * Signature, IHDR, a deflated red pixel and IEND, each with its CRC — the
  53. * one that circulates as "the smallest PNG" has a broken IDAT checksum, and
  54. * a broken image is a different test (see BROKEN_PNG).
  55. */
  56. export const TINY_PNG = Buffer.from(
  57. 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC',
  58. 'base64'
  59. )
  60. /**
  61. * A PNG that says it is one and is not: the header parses, the pixels do not
  62. * inflate. What a truncated upload from a phone looks like on disk.
  63. */
  64. export const BROKEN_PNG = Buffer.from(
  65. 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFAAH/q842iQAAAABJRU5ErkJggg==',
  66. 'base64'
  67. )