pdf-smoke.client.spec.ts 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. /** Installed PDF.js parses and draws a deterministic fixture using a real worker thread. */
  2. import { Worker as Thread, type Transferable } from 'node:worker_threads'
  3. import { describe, expect, it } from 'vitest'
  4. import { getDocument, PDFWorker } from 'pdfjs-dist/legacy/build/pdf.mjs'
  5. import { renderPdfPage } from '../src/client/pdf/document.ts'
  6. import { pdfFixture } from './pdf-fixture.ts'
  7. describe('PDF.js real-library smoke', () => {
  8. it('parses two pages and draws their distinct vector colors in a real worker', async () => {
  9. const thread = new Thread(new URL('./pdf-worker.fixture.mjs', import.meta.url), {
  10. workerData: { workerUrl: import.meta.resolve('pdfjs-dist/legacy/build/pdf.worker.mjs') },
  11. })
  12. const ready = Promise.withResolvers<undefined>()
  13. const failed = Promise.withResolvers<never>()
  14. thread.once('message', () => { ready.resolve(undefined) })
  15. thread.once('error', (error) => { failed.reject(error) })
  16. const listeners = new Map<EventListenerOrEventListenerObject, (data: unknown) => void>()
  17. const port = {
  18. postMessage: (message: unknown, transfer: readonly Transferable[]) => { thread.postMessage(message, transfer) },
  19. addEventListener: (_type: string, listener: EventListener) => {
  20. const forward = (data: unknown): void => { listener({ data } as unknown as Event) }
  21. listeners.set(listener, forward)
  22. thread.on('message', forward)
  23. },
  24. removeEventListener: (_type: string, listener: EventListener) => {
  25. const forward = listeners.get(listener)
  26. if (forward !== undefined) thread.off('message', forward)
  27. listeners.delete(listener)
  28. },
  29. }
  30. let bridge: PDFWorker | undefined
  31. let loading: ReturnType<typeof getDocument> | undefined
  32. try {
  33. await Promise.race([ready.promise, failed.promise])
  34. // The adapter carries browser-style messages across an actual Node worker thread.
  35. bridge = PDFWorker.create({ port: port as unknown as Worker })
  36. loading = getDocument({ data: pdfFixture(), worker: bridge })
  37. const pdf = await Promise.race([loading.promise, failed.promise])
  38. expect(pdf.numPages).toBe(2)
  39. const factory = pdf.canvasFactory as {
  40. create(width: number, height: number): { canvas: HTMLCanvasElement; context: CanvasRenderingContext2D }
  41. destroy(value: { canvas: HTMLCanvasElement; context: CanvasRenderingContext2D }): void
  42. }
  43. for (const page of [1, 2]) {
  44. const target = factory.create(1, 1)
  45. Object.defineProperty(target.canvas, 'style', { value: { setProperty: () => {} } })
  46. try {
  47. await renderPdfPage(pdf, page, target.canvas, new AbortController().signal, 1)
  48. const pixel = target.context.getImageData(Math.floor(target.canvas.width / 2), Math.floor(target.canvas.height / 2), 1, 1).data
  49. expect(pixel[3]).toBe(255)
  50. if (page === 1) expect(pixel[0]! - pixel[2]!).toBeGreaterThan(150)
  51. else expect(pixel[2]! - pixel[0]!).toBeGreaterThan(150)
  52. } finally {
  53. factory.destroy(target)
  54. }
  55. }
  56. } finally {
  57. try { await loading?.destroy() } finally {
  58. bridge?.destroy()
  59. await thread.terminate()
  60. }
  61. }
  62. })
  63. })