Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`) |
| 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` |
| 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/` |
Expand All @@ -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`, `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

Expand All @@ -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.
Expand Down
55 changes: 55 additions & 0 deletions demo/asset-attributes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>pdfjs-viewer-element | Static file attributes demo</title>
<script type="module" src="../dist/pdfjs-viewer-element.js"></script>
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>

<body>
<!--
Build the package before serving this demo over HTTP.
Each runtime file URL is supplied independently, without assets-base.
Replace these URLs with your static pipeline's eg. content-hashed URLs.
Use files from the same package release and set attributes before connection.

For example for Django ManifestStaticFilesStorage, copy dist into static/pdfjs/,
run collectstatic and use the following in a Django template:

{% load static %}
<script type="module" src="{% static 'pdfjs/pdfjs-viewer-element.js' %}"></script>
<pdfjs-viewer-element
src="/file.pdf"
pdfjs-lib-src="{% static 'pdfjs/pdf.mjs' %}"
viewer-src="{% static 'pdfjs/viewer.mjs' %}"
worker-src="{% static 'pdfjs/pdf.worker.min.mjs' %}"
viewer-css-src="{% static 'pdfjs/viewer.css' %}"
theme-css-src="{% static 'pdfjs/paper-and-ink.css' %}"
image-resources-path="{% get_static_prefix %}pdfjs/images/"
style="height: 100dvh">
</pdfjs-viewer-element>

Adjust the static prefix to your deployment. Keep original filenames available
for resources loaded by directory and filename, such as annotation icons,
CMaps, fonts and WASM; configure their resource path attributes as needed.
For a separate static host, configure module script CORS and the page's CSP.
-->
<pdfjs-viewer-element
src="/public/sample-pdf-with-images.pdf"
pdfjs-lib-src="../dist/pdf.mjs"
viewer-src="../dist/viewer.mjs"
worker-src="../dist/pdf.worker.min.mjs"
viewer-css-src="../dist/viewer.css"
theme-css-src="../dist/paper-and-ink.css"
image-resources-path="../dist/images/"
style="height: 100dvh">
</pdfjs-viewer-element>
</body>
</html>
40 changes: 20 additions & 20 deletions src/pdfjs-viewer-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const DEFAULT_PAPER_AND_INK_THEME_CSS_SRC = import.meta.env.DEV
// File names inside the package `dist` folder, used when `assets-base` is set.
const DIST_FILE_NAMES = {
workerSrc: 'pdf.worker.min.mjs',
pdfSrc: 'pdf.mjs',
pdfjsLibSrc: 'pdf.mjs',
viewerSrc: 'viewer.mjs',
viewerCssSrc: 'viewer.css',
themeCssSrc: 'paper-and-ink.css'
Expand All @@ -42,7 +42,7 @@ type RuntimeFileUrls = Record<keyof typeof DIST_FILE_NAMES, string>

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
Expand Down Expand Up @@ -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()}`))?.trim()
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 {
Expand All @@ -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'),
pdfjsLibSrc: resolveFile('pdfjsLibSrc'),
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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 } = this.getRuntimeFileUrls()
const workerSrcOrigin = resolveHttpOrigin(this.getWorkerSrc())
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
])
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
72 changes: 66 additions & 6 deletions tests/basic.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -258,6 +259,29 @@ describe('Basic tests', async () => {
}
})

it('should load individual runtime URLs without assets-base', async () => {
await mountViewer(`
<pdfjs-viewer-element
pdfjs-lib-src="/src/build/pdf.mjs?manifest=123"
viewer-src="/src/web/viewer.mjs?manifest=123"
worker-src="/src/build/pdf.worker.mjs?manifest=123"
viewer-css-src="/src/web/viewer.css?manifest=123"
theme-css-src="/src/themes/paper-and-ink.css?manifest=123">
</pdfjs-viewer-element>`
)

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(`
<pdfjs-viewer-element src="/sample-pdf-10MB.pdf"></pdfjs-viewer-element>`
Expand All @@ -282,4 +306,40 @@ describe('Basic tests', async () => {
viewerApp.open = originalOpen
}
})
})
})

describe('Runtime file URL overrides', () => {
const files = {
workerSrc: ['worker-src', 'pdf.worker.min.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']
} 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.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) {
expect(defaults[field]).eq(new URL(`${assetsBase}/${file}`, location.href).href)
}
}
})
})
1 change: 0 additions & 1 deletion types/pdfjs-viewer-element.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export declare class PdfjsViewerElement extends HTMLElement {
private getFullPath;
private getAssetsBase;
private getRuntimeFileUrls;
private getWorkerSrc;
private getImageResourcesPath;
private getCssThemeOption;
private applyIframeHash;
Expand Down
Loading