Skip to content

fix: render PDFs with pdf.js instead of a native iframe#2

Open
shanzez wants to merge 3 commits into
Platform-Collective:developfrom
shanzez:fix/embedded-pdf-pdfjs
Open

fix: render PDFs with pdf.js instead of a native iframe#2
shanzez wants to merge 3 commits into
Platform-Collective:developfrom
shanzez:fix/embedded-pdf-pdfjs

Conversation

@shanzez

@shanzez shanzez commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #1.

EmbeddedPDF.svelte rendered PDFs with a native <iframe src={fileUrl}>. That is cross-origin-fragile: the browser refuses to render the framed content when the file server sends X-Frame-Options: SAMEORIGIN (this breaks the desktop app, whose UI runs on a local origin and fetches files from a separate origin), and it renders blank in Safari/WebKit, which does not support PDF-in-iframe at all.

This PR switches EmbeddedPDF to render with pdf.js (fetch bytes into a <canvas> with a real text layer), which is immune to X-Frame-Options / CSP frame-ancestors and to per-browser native-PDF-viewer differences.

What changed

  • EmbeddedHTML.svelte — previously delegated to EmbeddedPDF (passing a blob: URL of converted HTML + a css string injected into the iframe <head>, which is how DOCX-to-HTML preview works). Decoupled first: EmbeddedHTML now owns its own small native-iframe implementation (same behavior). HTML content is same-origin blob:, so an iframe is correct there; this frees EmbeddedPDF of the css prop.
  • EmbeddedPDF.svelte — rewritten to render via pdfjs-dist:
    • Auth via token query param, not credentials. pdf.js fetches the file cross-origin; the file server responds Access-Control-Allow-Origin: * with no Access-Control-Allow-Credentials, so a credentialed (withCredentials) fetch is CORS-blocked. Instead the client token (presentation.metadata.Token) is appended as ?token= (server-supported), which a plain non-credentialed cross-origin request can carry. New token prop, read by both callers, appended via the pure, unit-tested appendToken().
    • Assets addressed via webpack public path. cmaps, standard fonts, and the fallback worker chunk are resolved through getAssetBase() (webpack's __webpack_public_path__), not document.baseURI/route-relative paths — the app uses path-based pushState routing, under which those would 404 off the root. cmaps/standard_fonts are emitted at the bundle root via CopyPlugin (desktop + web webpack configs).
    • Real text layer — selectable, Ctrl+F-searchable, screen-reader-accessible overlay per page (pdf.js TextLayer + --scale-factor).
    • HiDPI canvas, fit-aware scaling, loading + error states (with a download fallback), and full cleanup (cancel render tasks, destroy the document/loading task, terminate the fromPort worker) on unmount / src change. Wide pages scroll rather than being scaled down (which would distort the page and misalign the text layer). Fetch/parse runs only on src/token change; fit toggles repaint without refetching.
  • Worker mirrors the existing recorder-worker idiom (new Worker(new URL(..., import.meta.url))) with a try/catch fallback plus an explicit pdfjs-worker webpack entry in desktop/webpack.config.js.
  • common-versions.jsonpdfjs-dist added to allowedAlternativeVersions (services/rekoni already vendors 2.12.313 server-side; the UI uses 4.10.38). rush check passes.

Verification

  • rush check, rush validate, rush svelte-check, rush fast-format, and packages/ui tests pass; full desktop UI webpack build emits the worker chunk + cmaps/ + standard_fonts/.
  • Reproduced the exact failure and fix out-of-process: two origins, a file server mirroring cors() defaults + ?token= auth + X-Frame-Options: SAMEORIGIN. Credentialed cross-origin fetch is blocked; token-in-URL fetch is 200; pdf.js renders to canvas with a working text layer; the native iframe is blank.

Notes

  • Behavior is unchanged for the web app (same-origin) beyond the switch to pdf.js.
  • services/rekoni's older server-side pdfjs-dist is unrelated (PDF text extraction), hence the allowlist entry rather than a forced single version.

Credit: this fix was put together by the Exaviz team.

shanzez added 3 commits July 8, 2026 15:10
EmbeddedPDF.svelte used a native iframe pointed at the file URL, which the
browser leaves blank when the file server sends X-Frame-Options: SAMEORIGIN
(the desktop app runs its UI on a local origin and fetches files
cross-origin) and which does not display PDFs at all in Safari/WebKit.
Render with pdf.js (canvas) instead, which is immune to X-Frame-Options,
CSP frame-ancestors, and per-browser native PDF viewer differences.

- EmbeddedHTML.svelte no longer delegates to EmbeddedPDF; it owns its own
  native iframe plus css-injection implementation (used only for the
  DOCX-converted-to-HTML preview, which is same-origin blob content and
  unaffected by the PDF cross-origin issue).
- EmbeddedPDF.svelte now loads documents via pdfjs-dist, with
  withCredentials true (cookie auth, no token in the URL), cMaps and
  standard fonts for CJK and non-embedded standard-font rendering,
  loading and error states, and full cleanup (cancel render tasks,
  destroy the document/loading task, terminate the worker) on unmount
  and on src change. The css prop, only ever needed by EmbeddedHTML,
  is removed from its public API.
- The pdf.js worker follows the same resilience pattern already used
  for the recorder worker: primary construction via new Worker with a
  new URL built from import.meta.url, falling back to a fixed path
  emitted by an explicit webpack entry if that construction throws.
  desktop/webpack.config.js gains that entry plus CopyPlugin patterns
  for pdfjs-dist's cmaps and standard_fonts directories; the plain web
  build config gains the same cmaps/standard_fonts copying.
- Added pdfjs-dist 4.10.38 to packages/ui. services/rekoni already
  vendors an older pdfjs-dist server-side for unrelated PDF text
  extraction, so two majors coexisting in the repo is expected.
- Added a colocated unit test for the extractable render-scale helper.
  The worker construction stays inline in the component, matching the
  pre-existing recorder worker, since import.meta.url is ESM-only
  syntax that this package's ts-jest/CommonJS test transform cannot
  parse.

Fixes Platform-Collective#1
Follow-up to the pdf.js EmbeddedPDF rewrite, resolving CI and review findings.

- rush check: allow pdfjs-dist's two versions (rekoni's 2.12.313 and the new
  4.10.38 in packages/ui) via allowedAlternativeVersions in common-versions.json.
- Cross-origin auth: drop withCredentials (it maps to credentials:'include',
  which the file server rejects cross-origin — it responds with
  Access-Control-Allow-Origin: * and no Access-Control-Allow-Credentials).
  Instead append the client token as a ?token= query parameter (the server
  accepts it, and a plain non-credentialed request passes CORS). The token is
  read from presentation.metadata.Token by the two callers and passed via a new
  EmbeddedPDF `token` prop; appendToken() adds it with the right separator.
- Asset paths: resolve cmaps/, standard_fonts/ and the worker fallback from the
  webpack public path (getAssetBase) instead of document.baseURI/route-relative
  paths, which 404 under the app's path-based pushState routing.
- Text layer: render a pdf.js TextLayer over each page canvas so text is
  selectable, searchable (Ctrl+F) and screen-reader accessible.
- Rescale vs refetch: split the reactive flow so the document is fetched/parsed
  only when src/token change; toggling `fit` re-paints existing pages at the new
  scale without refetching.
- Pages render at their exact CSS size and scroll when wider than the viewport
  (with `align-items: safe center`) rather than being scaled down, which would
  distort the page and misalign the text layer.
- Suppress the load-error console log for renders superseded by a newer
  generation (no console spam on rapid navigation).
- Remove the leftover #view=FitH&navpanes=0 hash from EmbeddedHTML's iframe
  (it now loads HTML, not a PDF), and drop the now-unused css prop from
  presentation's PDFViewer.
- Disable the naming-convention rule for the `__webpack_public_path__`
  ambient declaration (webpack magic global, name is fixed by webpack).
- Apply prettier line-wrap and prefer-const auto-fixes flagged by
  `rush fast-format`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop app: PDF/image preview breaks when the file server sends X-Frame-Options (native-iframe embed is cross-origin-fragile)

1 participant