Skip to content

Commit 8fa8fc3

Browse files
authored
Merge pull request #57 from nvi9/asset-attrs
Support individual runtime asset URLs for hashed static files
2 parents 08768ca + c1eb859 commit 8fa8fc3

5 files changed

Lines changed: 147 additions & 29 deletions

File tree

‎README.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ The element is block-level and needs an explicit height.
101101
| viewer‑css‑theme | Viewer theme: `AUTOMATIC`, `LIGHT`, `DARK`. | `AUTOMATIC` |
102102
| 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` |
103103
| 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`) |
104+
| pdfjs‑lib‑src | PDF.js library module URL override. | bundled `pdf.mjs`, or `assets-base + pdf.mjs` |
105+
| viewer‑src | PDF.js viewer module URL override. | bundled `viewer.mjs`, or `assets-base + viewer.mjs` |
106+
| viewer‑css‑src | PDF.js viewer stylesheet URL override. | bundled `viewer.css`, or `assets-base + viewer.css` |
107+
| theme‑css‑src | Paper & Ink theme stylesheet URL override. | bundled `paper-and-ink.css`, or `assets-base + paper-and-ink.css` |
104108
| debugger‑src | PDF.js debugger script URL (`debuggerSrc` option). | `./debugger.mjs` |
105109
| c‑map‑url | CMap directory URL (`cMapUrl` option). | `../web/cmaps/` |
106110
| icc‑url | ICC profile directory URL (`iccUrl` option). | `../web/iccs/` |
@@ -120,7 +124,7 @@ Most attributes can be updated dynamically:
120124
- `viewer-css-theme` updates the viewer theme at runtime.
121125
- `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.
122126
- `locale` rebuilds the viewer so localization resources can be applied.
123-
- `assets-base` and `locale-src-template` are read when the viewer is (re)initialized, so set them before the element is attached.
127+
- `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.
124128

125129
## Bundlers and assets base
126130

@@ -136,7 +140,7 @@ Bundlers that do not analyse `new URL(..., import.meta.url)`, esbuild among them
136140
```
137141

138142
- The value is a directory URL, relative to the page or absolute. A trailing slash is added when missing.
139-
- `worker-src` still takes precedence for the worker file.
143+
- Individual runtime URL attributes take precedence over `assets-base` for their respective files, including `worker-src`.
140144
- The attribute is read when the viewer is (re)initialized, so set it before the element is attached to the document.
141145

142146
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.

‎demo/asset-attributes.html‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>pdfjs-viewer-element | Static file attributes demo</title>
7+
<script type="module" src="../dist/pdfjs-viewer-element.js"></script>
8+
<style>
9+
body {
10+
margin: 0;
11+
padding: 0;
12+
}
13+
</style>
14+
</head>
15+
16+
<body>
17+
<!--
18+
Build the package before serving this demo over HTTP.
19+
Each runtime file URL is supplied independently, without assets-base.
20+
Replace these URLs with your static pipeline's eg. content-hashed URLs.
21+
Use files from the same package release and set attributes before connection.
22+
23+
For example for Django ManifestStaticFilesStorage, copy dist into static/pdfjs/,
24+
run collectstatic and use the following in a Django template:
25+
26+
{% load static %}
27+
<script type="module" src="{% static 'pdfjs/pdfjs-viewer-element.js' %}"></script>
28+
<pdfjs-viewer-element
29+
src="/file.pdf"
30+
pdfjs-lib-src="{% static 'pdfjs/pdf.mjs' %}"
31+
viewer-src="{% static 'pdfjs/viewer.mjs' %}"
32+
worker-src="{% static 'pdfjs/pdf.worker.min.mjs' %}"
33+
viewer-css-src="{% static 'pdfjs/viewer.css' %}"
34+
theme-css-src="{% static 'pdfjs/paper-and-ink.css' %}"
35+
image-resources-path="{% get_static_prefix %}pdfjs/images/"
36+
style="height: 100dvh">
37+
</pdfjs-viewer-element>
38+
39+
Adjust the static prefix to your deployment. Keep original filenames available
40+
for resources loaded by directory and filename, such as annotation icons,
41+
CMaps, fonts and WASM; configure their resource path attributes as needed.
42+
For a separate static host, configure module script CORS and the page's CSP.
43+
-->
44+
<pdfjs-viewer-element
45+
src="/public/sample-pdf-with-images.pdf"
46+
pdfjs-lib-src="../dist/pdf.mjs"
47+
viewer-src="../dist/viewer.mjs"
48+
worker-src="../dist/pdf.worker.min.mjs"
49+
viewer-css-src="../dist/viewer.css"
50+
theme-css-src="../dist/paper-and-ink.css"
51+
image-resources-path="../dist/images/"
52+
style="height: 100dvh">
53+
</pdfjs-viewer-element>
54+
</body>
55+
</html>

‎src/pdfjs-viewer-element.ts‎

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const DEFAULT_PAPER_AND_INK_THEME_CSS_SRC = import.meta.env.DEV
3232
// File names inside the package `dist` folder, used when `assets-base` is set.
3333
const DIST_FILE_NAMES = {
3434
workerSrc: 'pdf.worker.min.mjs',
35-
pdfSrc: 'pdf.mjs',
35+
pdfjsLibSrc: 'pdf.mjs',
3636
viewerSrc: 'viewer.mjs',
3737
viewerCssSrc: 'viewer.css',
3838
themeCssSrc: 'paper-and-ink.css'
@@ -42,7 +42,7 @@ type RuntimeFileUrls = Record<keyof typeof DIST_FILE_NAMES, string>
4242

4343
const DEFAULT_RUNTIME_FILE_URLS: RuntimeFileUrls = {
4444
workerSrc: DEFAULT_BUILT_IN_WORKER_SRC,
45-
pdfSrc: DEFAULT_PDF_SRC,
45+
pdfjsLibSrc: DEFAULT_PDF_SRC,
4646
viewerSrc: DEFAULT_VIEWER_SRC,
4747
viewerCssSrc: DEFAULT_VIEWER_CSS_SRC,
4848
themeCssSrc: DEFAULT_PAPER_AND_INK_THEME_CSS_SRC
@@ -140,9 +140,13 @@ export class PdfjsViewerElement extends HTMLElement {
140140
// Read when the viewer is (re)built, so the attribute must be present before connection.
141141
private getRuntimeFileUrls(): RuntimeFileUrls {
142142
const assetsBase = this.getAssetsBase()
143-
if (!assetsBase) return DEFAULT_RUNTIME_FILE_URLS
144143

145-
const resolveFile = (fileName: string) => {
144+
const resolveFile = (field: keyof typeof DIST_FILE_NAMES) => {
145+
const attr = this.getAttribute(field.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`))?.trim()
146+
if (attr) return this.getFullPath(attr)
147+
if (!assetsBase) return DEFAULT_RUNTIME_FILE_URLS[field]
148+
149+
const fileName = DIST_FILE_NAMES[field]
146150
try {
147151
return new URL(fileName, assetsBase).href
148152
} catch {
@@ -151,18 +155,14 @@ export class PdfjsViewerElement extends HTMLElement {
151155
}
152156

153157
return {
154-
workerSrc: resolveFile(DIST_FILE_NAMES.workerSrc),
155-
pdfSrc: resolveFile(DIST_FILE_NAMES.pdfSrc),
156-
viewerSrc: resolveFile(DIST_FILE_NAMES.viewerSrc),
157-
viewerCssSrc: resolveFile(DIST_FILE_NAMES.viewerCssSrc),
158-
themeCssSrc: resolveFile(DIST_FILE_NAMES.themeCssSrc)
158+
workerSrc: resolveFile('workerSrc'),
159+
pdfjsLibSrc: resolveFile('pdfjsLibSrc'),
160+
viewerSrc: resolveFile('viewerSrc'),
161+
viewerCssSrc: resolveFile('viewerCssSrc'),
162+
themeCssSrc: resolveFile('themeCssSrc')
159163
}
160164
}
161165

162-
private getWorkerSrc() {
163-
return this.getAttribute('worker-src') || this.getRuntimeFileUrls().workerSrc
164-
}
165-
166166
private getImageResourcesPath() {
167167
const imageResourcesPath = this.getAttribute('image-resources-path')
168168
if (imageResourcesPath) return imageResourcesPath
@@ -383,7 +383,7 @@ export class PdfjsViewerElement extends HTMLElement {
383383

384384
private applyViewerOptions = () => {
385385
const viewerOptions = this.iframe.contentWindow?.PDFViewerApplicationOptions
386-
viewerOptions?.set('workerSrc', this.getWorkerSrc())
386+
viewerOptions?.set('workerSrc', this.getRuntimeFileUrls().workerSrc)
387387
viewerOptions?.set('debuggerSrc', this.getAttribute('debugger-src') || DEFAULTS.debuggerSrc)
388388
viewerOptions?.set('cMapUrl', this.getAttribute('c-map-url') || DEFAULTS.cMapUrl)
389389
viewerOptions?.set('iccUrl', this.getAttribute('icc-url') || DEFAULTS.iccUrl)
@@ -432,13 +432,13 @@ export class PdfjsViewerElement extends HTMLElement {
432432
const toSourceList = (sources: string[]) =>
433433
Array.from(new Set(sources.filter(Boolean))).join(' ')
434434

435-
const { pdfSrc, viewerSrc, viewerCssSrc, themeCssSrc } = this.getRuntimeFileUrls()
436-
const workerSrcOrigin = resolveHttpOrigin(this.getWorkerSrc())
435+
const { pdfjsLibSrc, viewerSrc, viewerCssSrc, themeCssSrc, workerSrc } = this.getRuntimeFileUrls()
436+
const workerSrcOrigin = resolveHttpOrigin(workerSrc)
437437

438438
const scriptSources = toSourceList([
439439
"'self'",
440440
window.location.origin,
441-
resolveHttpOrigin(pdfSrc),
441+
resolveHttpOrigin(pdfjsLibSrc),
442442
resolveHttpOrigin(viewerSrc),
443443
workerSrcOrigin
444444
])
@@ -508,8 +508,8 @@ export class PdfjsViewerElement extends HTMLElement {
508508

509509
await this.injectLocaleData()
510510

511-
const { pdfSrc, viewerSrc } = this.getRuntimeFileUrls()
512-
await this.injectScript(pdfSrc)
511+
const { pdfjsLibSrc, viewerSrc } = this.getRuntimeFileUrls()
512+
await this.injectScript(pdfjsLibSrc)
513513
await this.injectScript(viewerSrc)
514514

515515
return await setupPromise
@@ -547,7 +547,7 @@ export class PdfjsViewerElement extends HTMLElement {
547547

548548
if (name === 'worker-src') {
549549
const viewerOptions = this.iframe.contentWindow?.PDFViewerApplicationOptions
550-
viewerOptions?.set('workerSrc', newValue || this.getRuntimeFileUrls().workerSrc)
550+
viewerOptions?.set('workerSrc', this.getRuntimeFileUrls().workerSrc)
551551
return
552552
}
553553

‎tests/basic.test.ts‎

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from 'vitest'
22
import { getFileData, getViewerElement, mountViewer, getIframe } from './utils'
3-
import '../src/pdfjs-viewer-element'
3+
import { PdfjsViewerElement } from '../src/pdfjs-viewer-element'
44

55
describe('Basic tests', async () => {
66
it('should render the PDF file', async () => {
@@ -108,11 +108,12 @@ describe('Basic tests', async () => {
108108
)
109109

110110
const viewer = document.body.querySelector('pdfjs-viewer-element') as HTMLElement
111-
const workerSrc = 'https://example.com/pdf.worker.min.mjs'
112-
viewer.setAttribute('worker-src', workerSrc)
111+
for (const workerSrc of ['https://example.com/pdf.worker.min.mjs', '/static/pdfjs/pdf.worker.abc.mjs', './pdf.worker.mjs']) {
112+
viewer.setAttribute('worker-src', workerSrc)
113113

114-
const options = getIframe().contentWindow.PDFViewerApplicationOptions.getAll()
115-
expect(options.workerSrc).eq(workerSrc)
114+
const options = getIframe().contentWindow.PDFViewerApplicationOptions.getAll()
115+
expect(options.workerSrc).eq(new URL(workerSrc, location.href).href)
116+
}
116117
})
117118

118119
it('should include external worker origin in CSP script and worker directives', async () => {
@@ -258,6 +259,29 @@ describe('Basic tests', async () => {
258259
}
259260
})
260261

262+
it('should load individual runtime URLs without assets-base', async () => {
263+
await mountViewer(`
264+
<pdfjs-viewer-element
265+
pdfjs-lib-src="/src/build/pdf.mjs?manifest=123"
266+
viewer-src="/src/web/viewer.mjs?manifest=123"
267+
worker-src="/src/build/pdf.worker.mjs?manifest=123"
268+
viewer-css-src="/src/web/viewer.css?manifest=123"
269+
theme-css-src="/src/themes/paper-and-ink.css?manifest=123">
270+
</pdfjs-viewer-element>`
271+
)
272+
273+
const doc = getIframe().contentDocument!
274+
for (const path of ['/src/build/pdf.mjs', '/src/web/viewer.mjs']) {
275+
expect(doc.querySelector(`script[src="${location.origin}${path}?manifest=123"]`)).exist
276+
}
277+
for (const path of ['/src/web/viewer.css', '/src/themes/paper-and-ink.css']) {
278+
expect(doc.querySelector(`link[href="${location.origin}${path}?manifest=123"]`)).exist
279+
}
280+
expect(getIframe().contentWindow.PDFViewerApplicationOptions.getAll().workerSrc)
281+
.eq(new URL('/src/build/pdf.worker.mjs?manifest=123', location.href).href)
282+
expect(getViewerElement()).exist
283+
})
284+
261285
it('should open a new document when src changes at runtime', async () => {
262286
const viewerApp = await mountViewer(`
263287
<pdfjs-viewer-element src="/sample-pdf-10MB.pdf"></pdfjs-viewer-element>`
@@ -282,4 +306,40 @@ describe('Basic tests', async () => {
282306
viewerApp.open = originalOpen
283307
}
284308
})
285-
})
309+
})
310+
311+
describe('Runtime file URL overrides', () => {
312+
const files = {
313+
workerSrc: ['worker-src', 'pdf.worker.min.mjs'],
314+
pdfjsLibSrc: ['pdfjs-lib-src', 'pdf.mjs'],
315+
viewerSrc: ['viewer-src', 'viewer.mjs'],
316+
viewerCssSrc: ['viewer-css-src', 'viewer.css'],
317+
themeCssSrc: ['theme-css-src', 'paper-and-ink.css']
318+
} as const
319+
320+
it.each([undefined, '/static/pdfjs'])('resolves individual URLs with assets-base=%s', (assetsBase) => {
321+
const viewer = new PdfjsViewerElement()
322+
if (assetsBase) viewer.setAttribute('assets-base', assetsBase)
323+
const defaults = viewer['getRuntimeFileUrls']()
324+
325+
for (const [key, [attribute, file]] of Object.entries(files)) {
326+
const field = key as keyof typeof files
327+
const path = `/static/hashed/${file}?v=123`
328+
viewer.setAttribute(attribute, path)
329+
expect(viewer['getRuntimeFileUrls']()[field]).eq(new URL(path, location.href).href)
330+
viewer.setAttribute(attribute, 'https://cdn.example.com/' + file)
331+
expect(viewer['getRuntimeFileUrls']()[field]).eq('https://cdn.example.com/' + file)
332+
viewer.setAttribute(attribute, '')
333+
expect(viewer['getRuntimeFileUrls']()[field]).eq(defaults[field])
334+
viewer.setAttribute(attribute, ' \t\n ')
335+
expect(viewer['getRuntimeFileUrls']()[field]).eq(defaults[field])
336+
viewer.setAttribute(attribute, ` ${path} `)
337+
expect(viewer['getRuntimeFileUrls']()[field]).eq(new URL(path, location.href).href)
338+
viewer.removeAttribute(attribute)
339+
expect(viewer['getRuntimeFileUrls']()).toEqual(defaults)
340+
if (assetsBase) {
341+
expect(defaults[field]).eq(new URL(`${assetsBase}/${file}`, location.href).href)
342+
}
343+
}
344+
})
345+
})

‎types/pdfjs-viewer-element.d.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ export declare class PdfjsViewerElement extends HTMLElement {
3232
private getFullPath;
3333
private getAssetsBase;
3434
private getRuntimeFileUrls;
35-
private getWorkerSrc;
3635
private getImageResourcesPath;
3736
private getCssThemeOption;
3837
private applyIframeHash;

0 commit comments

Comments
 (0)