From 88732ede3071e0c2355098e1588ef910bc32cbd3 Mon Sep 17 00:00:00 2001 From: nvi9 <42123156+nvi9@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:51:17 +0200 Subject: [PATCH 1/4] feat: support individual runtime asset URL attributes Allow PDF.js scripts and stylesheets to use URLs generated by static asset pipelines such as Django's ManifestStaticFilesStorage. Preserve assets-base and bundled fallbacks, centralize worker URL resolution, and add documentation, a demo, and regression tests. --- README.md | 8 +++-- demo/asset-attributes.html | 55 ++++++++++++++++++++++++++++++ src/pdfjs-viewer-element.ts | 28 ++++++++-------- tests/basic.test.ts | 59 +++++++++++++++++++++++++++++++-- types/pdfjs-viewer-element.d.ts | 1 - 5 files changed, 132 insertions(+), 19 deletions(-) create mode 100644 demo/asset-attributes.html diff --git a/README.md b/README.md index 76d3be8..8e31b1a 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,10 @@ The element is block-level and needs an explicit height. | viewer‑css‑theme | Viewer theme: `AUTOMATIC`, `LIGHT`, `DARK`. | `AUTOMATIC` | | assets‑base | Directory URL of the viewer runtime files (`viewer.mjs`, `pdf.mjs`, `pdf.worker.min.mjs`, `viewer.css`, `paper-and-ink.css`). Only needed when your bundler does not handle `new URL('./file', import.meta.url)`, see [Bundlers](#bundlers-and-assets-base). | folder of `pdfjs-viewer-element.js` | | worker‑src | PDF.js worker URL override. | bundled worker (`./build/pdf.worker.mjs` in dev, `pdf.worker.min.mjs` next to the element or in `assets-base`) | +| pdf‑src | PDF.js library module URL override. | bundled `pdf.mjs`, or `assets-base + pdf.mjs` | +| viewer‑src | PDF.js viewer module URL override. | bundled `viewer.mjs`, or `assets-base + viewer.mjs` | +| viewer‑css‑src | PDF.js viewer stylesheet URL override. | bundled `viewer.css`, or `assets-base + viewer.css` | +| theme‑css‑src | Paper & Ink theme stylesheet URL override. | bundled `paper-and-ink.css`, or `assets-base + paper-and-ink.css` | | debugger‑src | PDF.js debugger script URL (`debuggerSrc` option). | `./debugger.mjs` | | c‑map‑url | CMap directory URL (`cMapUrl` option). | `../web/cmaps/` | | icc‑url | ICC profile directory URL (`iccUrl` option). | `../web/iccs/` | @@ -120,7 +124,7 @@ Most attributes can be updated dynamically: - `viewer-css-theme` updates the viewer theme at runtime. - `worker-src`, `debugger-src`, `c-map-url`, `icc-url`, `image-resources-path`, `sandbox-bundle-src`, `standard-font-data-url`, `wasm-url` update viewer options for subsequent document loads. - `locale` rebuilds the viewer so localization resources can be applied. -- `assets-base` and `locale-src-template` are read when the viewer is (re)initialized, so set them before the element is attached. +- `assets-base`, `pdf-src`, `viewer-src`, `viewer-css-src`, `theme-css-src` and `locale-src-template` are read when the viewer is (re)initialized, so set them before the element is attached. Changing them alone does not reload the viewer. ## Bundlers and assets base @@ -136,7 +140,7 @@ Bundlers that do not analyse `new URL(..., import.meta.url)`, esbuild among them ``` - The value is a directory URL, relative to the page or absolute. A trailing slash is added when missing. -- `worker-src` still takes precedence for the worker file. +- Individual runtime URL attributes take precedence over `assets-base` for their respective files, including `worker-src`. - The attribute is read when the viewer is (re)initialized, so set it before the element is attached to the document. PDF.js annotation icons are a separate case: the viewer loads them at runtime as `imageResourcesPath + 'annotation-' + name + '.svg'`, a dynamic name no bundler can emit. If `assets-base` is set, `imageResourcesPath` defaults to `assets-base + 'images/'`. Otherwise serve the package `dist/images` folder and set `image-resources-path` explicitly. diff --git a/demo/asset-attributes.html b/demo/asset-attributes.html new file mode 100644 index 0000000..6d0fccb --- /dev/null +++ b/demo/asset-attributes.html @@ -0,0 +1,55 @@ + + + + + + pdfjs-viewer-element | Static file attributes demo + + + + + + + + + + diff --git a/src/pdfjs-viewer-element.ts b/src/pdfjs-viewer-element.ts index f807eeb..02f1359 100644 --- a/src/pdfjs-viewer-element.ts +++ b/src/pdfjs-viewer-element.ts @@ -140,9 +140,13 @@ export class PdfjsViewerElement extends HTMLElement { // Read when the viewer is (re)built, so the attribute must be present before connection. private getRuntimeFileUrls(): RuntimeFileUrls { const assetsBase = this.getAssetsBase() - if (!assetsBase) return DEFAULT_RUNTIME_FILE_URLS - const resolveFile = (fileName: string) => { + const resolveFile = (field: keyof typeof DIST_FILE_NAMES) => { + const attr = this.getAttribute(field.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)) + if (attr) return this.getFullPath(attr) + if (!assetsBase) return DEFAULT_RUNTIME_FILE_URLS[field] + + const fileName = DIST_FILE_NAMES[field] try { return new URL(fileName, assetsBase).href } catch { @@ -151,18 +155,14 @@ export class PdfjsViewerElement extends HTMLElement { } return { - workerSrc: resolveFile(DIST_FILE_NAMES.workerSrc), - pdfSrc: resolveFile(DIST_FILE_NAMES.pdfSrc), - viewerSrc: resolveFile(DIST_FILE_NAMES.viewerSrc), - viewerCssSrc: resolveFile(DIST_FILE_NAMES.viewerCssSrc), - themeCssSrc: resolveFile(DIST_FILE_NAMES.themeCssSrc) + workerSrc: resolveFile('workerSrc'), + pdfSrc: resolveFile('pdfSrc'), + viewerSrc: resolveFile('viewerSrc'), + viewerCssSrc: resolveFile('viewerCssSrc'), + themeCssSrc: resolveFile('themeCssSrc') } } - private getWorkerSrc() { - return this.getAttribute('worker-src') || this.getRuntimeFileUrls().workerSrc - } - private getImageResourcesPath() { const imageResourcesPath = this.getAttribute('image-resources-path') if (imageResourcesPath) return imageResourcesPath @@ -383,7 +383,7 @@ export class PdfjsViewerElement extends HTMLElement { private applyViewerOptions = () => { const viewerOptions = this.iframe.contentWindow?.PDFViewerApplicationOptions - viewerOptions?.set('workerSrc', this.getWorkerSrc()) + viewerOptions?.set('workerSrc', this.getRuntimeFileUrls().workerSrc) viewerOptions?.set('debuggerSrc', this.getAttribute('debugger-src') || DEFAULTS.debuggerSrc) viewerOptions?.set('cMapUrl', this.getAttribute('c-map-url') || DEFAULTS.cMapUrl) viewerOptions?.set('iccUrl', this.getAttribute('icc-url') || DEFAULTS.iccUrl) @@ -432,8 +432,8 @@ export class PdfjsViewerElement extends HTMLElement { const toSourceList = (sources: string[]) => Array.from(new Set(sources.filter(Boolean))).join(' ') - const { pdfSrc, viewerSrc, viewerCssSrc, themeCssSrc } = this.getRuntimeFileUrls() - const workerSrcOrigin = resolveHttpOrigin(this.getWorkerSrc()) + const { pdfSrc, viewerSrc, viewerCssSrc, themeCssSrc, workerSrc } = this.getRuntimeFileUrls() + const workerSrcOrigin = resolveHttpOrigin(workerSrc) const scriptSources = toSourceList([ "'self'", diff --git a/tests/basic.test.ts b/tests/basic.test.ts index c5d6917..74b202d 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { getFileData, getViewerElement, mountViewer, getIframe } from './utils' -import '../src/pdfjs-viewer-element' +import { PdfjsViewerElement } from '../src/pdfjs-viewer-element' describe('Basic tests', async () => { it('should render the PDF file', async () => { @@ -258,6 +258,29 @@ describe('Basic tests', async () => { } }) + it('should load individual runtime URLs without assets-base', async () => { + await mountViewer(` + + ` + ) + + const doc = getIframe().contentDocument! + for (const path of ['/src/build/pdf.mjs', '/src/web/viewer.mjs']) { + expect(doc.querySelector(`script[src="${location.origin}${path}?manifest=123"]`)).exist + } + for (const path of ['/src/web/viewer.css', '/src/themes/paper-and-ink.css']) { + expect(doc.querySelector(`link[href="${location.origin}${path}?manifest=123"]`)).exist + } + expect(getIframe().contentWindow.PDFViewerApplicationOptions.getAll().workerSrc) + .eq(new URL('/src/build/pdf.worker.mjs?manifest=123', location.href).href) + expect(getViewerElement()).exist + }) + it('should open a new document when src changes at runtime', async () => { const viewerApp = await mountViewer(` ` @@ -282,4 +305,36 @@ describe('Basic tests', async () => { viewerApp.open = originalOpen } }) -}) \ No newline at end of file +}) + +describe('Runtime file URL overrides', () => { + const files = { + workerSrc: ['worker-src', 'pdf.worker.min.mjs'], + pdfSrc: ['pdf-src', 'pdf.mjs'], + viewerSrc: ['viewer-src', 'viewer.mjs'], + viewerCssSrc: ['viewer-css-src', 'viewer.css'], + themeCssSrc: ['theme-css-src', 'paper-and-ink.css'] + } as const + + it.each([undefined, '/static/pdfjs'])('resolves individual URLs with assets-base=%s', (assetsBase) => { + const viewer = new PdfjsViewerElement() + if (assetsBase) viewer.setAttribute('assets-base', assetsBase) + const defaults = viewer['getRuntimeFileUrls']() + + for (const [key, [attribute, file]] of Object.entries(files)) { + const field = key as keyof typeof files + const path = `/static/hashed/${file}?v=123` + viewer.setAttribute(attribute, path) + expect(viewer['getRuntimeFileUrls']()[field]).eq(new URL(path, location.href).href) + viewer.setAttribute(attribute, 'https://cdn.example.com/' + file) + expect(viewer['getRuntimeFileUrls']()[field]).eq('https://cdn.example.com/' + file) + viewer.setAttribute(attribute, '') + expect(viewer['getRuntimeFileUrls']()[field]).eq(defaults[field]) + viewer.removeAttribute(attribute) + expect(viewer['getRuntimeFileUrls']()).toEqual(defaults) + if (assetsBase) { + expect(defaults[field]).eq(new URL(`${assetsBase}/${file}`, location.href).href) + } + } + }) +}) diff --git a/types/pdfjs-viewer-element.d.ts b/types/pdfjs-viewer-element.d.ts index f0668e2..ec81124 100644 --- a/types/pdfjs-viewer-element.d.ts +++ b/types/pdfjs-viewer-element.d.ts @@ -32,7 +32,6 @@ export declare class PdfjsViewerElement extends HTMLElement { private getFullPath; private getAssetsBase; private getRuntimeFileUrls; - private getWorkerSrc; private getImageResourcesPath; private getCssThemeOption; private applyIframeHash; From 6197ac3b345a3e5261810b548a8087729be15a8c Mon Sep 17 00:00:00 2001 From: nvi9 <42123156+nvi9@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:20:27 +0200 Subject: [PATCH 2/4] fix: trim attribute values --- src/pdfjs-viewer-element.ts | 2 +- tests/basic.test.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pdfjs-viewer-element.ts b/src/pdfjs-viewer-element.ts index 02f1359..7f4f704 100644 --- a/src/pdfjs-viewer-element.ts +++ b/src/pdfjs-viewer-element.ts @@ -142,7 +142,7 @@ export class PdfjsViewerElement extends HTMLElement { const assetsBase = this.getAssetsBase() const resolveFile = (field: keyof typeof DIST_FILE_NAMES) => { - const attr = this.getAttribute(field.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)) + const attr = this.getAttribute(field.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`))?.trim() if (attr) return this.getFullPath(attr) if (!assetsBase) return DEFAULT_RUNTIME_FILE_URLS[field] diff --git a/tests/basic.test.ts b/tests/basic.test.ts index 74b202d..0606ec5 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -330,6 +330,10 @@ describe('Runtime file URL overrides', () => { expect(viewer['getRuntimeFileUrls']()[field]).eq('https://cdn.example.com/' + file) viewer.setAttribute(attribute, '') expect(viewer['getRuntimeFileUrls']()[field]).eq(defaults[field]) + viewer.setAttribute(attribute, ' \t\n ') + expect(viewer['getRuntimeFileUrls']()[field]).eq(defaults[field]) + viewer.setAttribute(attribute, ` ${path} `) + expect(viewer['getRuntimeFileUrls']()[field]).eq(new URL(path, location.href).href) viewer.removeAttribute(attribute) expect(viewer['getRuntimeFileUrls']()).toEqual(defaults) if (assetsBase) { From e5afca70f747b483c9c4ac9211d71db94979dc09 Mon Sep 17 00:00:00 2001 From: nvi9 <42123156+nvi9@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:27:15 +0200 Subject: [PATCH 3/4] fix: runtime worker-src update with normalized value --- src/pdfjs-viewer-element.ts | 2 +- tests/basic.test.ts | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pdfjs-viewer-element.ts b/src/pdfjs-viewer-element.ts index 7f4f704..88e8a8b 100644 --- a/src/pdfjs-viewer-element.ts +++ b/src/pdfjs-viewer-element.ts @@ -547,7 +547,7 @@ export class PdfjsViewerElement extends HTMLElement { if (name === 'worker-src') { const viewerOptions = this.iframe.contentWindow?.PDFViewerApplicationOptions - viewerOptions?.set('workerSrc', newValue || this.getRuntimeFileUrls().workerSrc) + viewerOptions?.set('workerSrc', this.getRuntimeFileUrls().workerSrc) return } diff --git a/tests/basic.test.ts b/tests/basic.test.ts index 0606ec5..3ea6fdf 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -108,11 +108,12 @@ describe('Basic tests', async () => { ) const viewer = document.body.querySelector('pdfjs-viewer-element') as HTMLElement - const workerSrc = 'https://example.com/pdf.worker.min.mjs' - viewer.setAttribute('worker-src', workerSrc) + for (const workerSrc of ['https://example.com/pdf.worker.min.mjs', '/static/pdfjs/pdf.worker.abc.mjs', './pdf.worker.mjs']) { + viewer.setAttribute('worker-src', workerSrc) - const options = getIframe().contentWindow.PDFViewerApplicationOptions.getAll() - expect(options.workerSrc).eq(workerSrc) + const options = getIframe().contentWindow.PDFViewerApplicationOptions.getAll() + expect(options.workerSrc).eq(new URL(workerSrc, location.href).href) + } }) it('should include external worker origin in CSP script and worker directives', async () => { From c1eb8591cb6f1c300625a25791fe7f419617c5d9 Mon Sep 17 00:00:00 2001 From: Aleksandr Shevchuk Date: Mon, 19 Oct 2026 14:27:24 +0300 Subject: [PATCH 4/4] refactor: rename pdf-src attribute to pdfjs-lib-src `pdf-src` reads as the PDF document URL next to `src`. `pdfjs-lib-src` names the PDF.js core library module (`pdf.mjs`, which sets `globalThis.pdfjsLib`) and avoids confusion with the pdf-lib package. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- demo/asset-attributes.html | 4 ++-- src/pdfjs-viewer-element.ts | 14 +++++++------- tests/basic.test.ts | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8e31b1a..6a51cff 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ The element is block-level and needs an explicit height. | viewer‑css‑theme | Viewer theme: `AUTOMATIC`, `LIGHT`, `DARK`. | `AUTOMATIC` | | assets‑base | Directory URL of the viewer runtime files (`viewer.mjs`, `pdf.mjs`, `pdf.worker.min.mjs`, `viewer.css`, `paper-and-ink.css`). Only needed when your bundler does not handle `new URL('./file', import.meta.url)`, see [Bundlers](#bundlers-and-assets-base). | folder of `pdfjs-viewer-element.js` | | worker‑src | PDF.js worker URL override. | bundled worker (`./build/pdf.worker.mjs` in dev, `pdf.worker.min.mjs` next to the element or in `assets-base`) | -| pdf‑src | PDF.js library module URL override. | bundled `pdf.mjs`, or `assets-base + pdf.mjs` | +| pdfjs‑lib‑src | PDF.js library module URL override. | bundled `pdf.mjs`, or `assets-base + pdf.mjs` | | viewer‑src | PDF.js viewer module URL override. | bundled `viewer.mjs`, or `assets-base + viewer.mjs` | | viewer‑css‑src | PDF.js viewer stylesheet URL override. | bundled `viewer.css`, or `assets-base + viewer.css` | | theme‑css‑src | Paper & Ink theme stylesheet URL override. | bundled `paper-and-ink.css`, or `assets-base + paper-and-ink.css` | @@ -124,7 +124,7 @@ Most attributes can be updated dynamically: - `viewer-css-theme` updates the viewer theme at runtime. - `worker-src`, `debugger-src`, `c-map-url`, `icc-url`, `image-resources-path`, `sandbox-bundle-src`, `standard-font-data-url`, `wasm-url` update viewer options for subsequent document loads. - `locale` rebuilds the viewer so localization resources can be applied. -- `assets-base`, `pdf-src`, `viewer-src`, `viewer-css-src`, `theme-css-src` and `locale-src-template` are read when the viewer is (re)initialized, so set them before the element is attached. Changing them alone does not reload the viewer. +- `assets-base`, `pdfjs-lib-src`, `viewer-src`, `viewer-css-src`, `theme-css-src` and `locale-src-template` are read when the viewer is (re)initialized, so set them before the element is attached. Changing them alone does not reload the viewer. ## Bundlers and assets base diff --git a/demo/asset-attributes.html b/demo/asset-attributes.html index 6d0fccb..7e86dae 100644 --- a/demo/asset-attributes.html +++ b/demo/asset-attributes.html @@ -27,7 +27,7 @@ const DEFAULT_RUNTIME_FILE_URLS: RuntimeFileUrls = { workerSrc: DEFAULT_BUILT_IN_WORKER_SRC, - pdfSrc: DEFAULT_PDF_SRC, + pdfjsLibSrc: DEFAULT_PDF_SRC, viewerSrc: DEFAULT_VIEWER_SRC, viewerCssSrc: DEFAULT_VIEWER_CSS_SRC, themeCssSrc: DEFAULT_PAPER_AND_INK_THEME_CSS_SRC @@ -156,7 +156,7 @@ export class PdfjsViewerElement extends HTMLElement { return { workerSrc: resolveFile('workerSrc'), - pdfSrc: resolveFile('pdfSrc'), + pdfjsLibSrc: resolveFile('pdfjsLibSrc'), viewerSrc: resolveFile('viewerSrc'), viewerCssSrc: resolveFile('viewerCssSrc'), themeCssSrc: resolveFile('themeCssSrc') @@ -432,13 +432,13 @@ export class PdfjsViewerElement extends HTMLElement { const toSourceList = (sources: string[]) => Array.from(new Set(sources.filter(Boolean))).join(' ') - const { pdfSrc, viewerSrc, viewerCssSrc, themeCssSrc, workerSrc } = this.getRuntimeFileUrls() + const { pdfjsLibSrc, viewerSrc, viewerCssSrc, themeCssSrc, workerSrc } = this.getRuntimeFileUrls() const workerSrcOrigin = resolveHttpOrigin(workerSrc) const scriptSources = toSourceList([ "'self'", window.location.origin, - resolveHttpOrigin(pdfSrc), + resolveHttpOrigin(pdfjsLibSrc), resolveHttpOrigin(viewerSrc), workerSrcOrigin ]) @@ -508,8 +508,8 @@ export class PdfjsViewerElement extends HTMLElement { await this.injectLocaleData() - const { pdfSrc, viewerSrc } = this.getRuntimeFileUrls() - await this.injectScript(pdfSrc) + const { pdfjsLibSrc, viewerSrc } = this.getRuntimeFileUrls() + await this.injectScript(pdfjsLibSrc) await this.injectScript(viewerSrc) return await setupPromise diff --git a/tests/basic.test.ts b/tests/basic.test.ts index 3ea6fdf..7127e97 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -262,7 +262,7 @@ describe('Basic tests', async () => { it('should load individual runtime URLs without assets-base', async () => { await mountViewer(` { describe('Runtime file URL overrides', () => { const files = { workerSrc: ['worker-src', 'pdf.worker.min.mjs'], - pdfSrc: ['pdf-src', 'pdf.mjs'], + pdfjsLibSrc: ['pdfjs-lib-src', 'pdf.mjs'], viewerSrc: ['viewer-src', 'viewer.mjs'], viewerCssSrc: ['viewer-css-src', 'viewer.css'], themeCssSrc: ['theme-css-src', 'paper-and-ink.css']