<bmx-pdf-viewer>
A PDF, shown in the page: scroll through it, zoom, rotate, search it, select and copy its text, follow its links and outline, read its comments and form fields, print it and download it. The file is read and drawn in the browser by the library's own PDF engine - no plug-in, no server, no third-party code.
15 properties · 4 events · 11 methods · 28 parts
Example
A three-page sample, written by this page: try find, the outline, the links, rotation, and the comment on page 2.
Show markup
<bmx-pdf-viewer id="ex-pdf" sidebar="outline" style="--bmx-pdf-viewer-height: 36rem"></bmx-pdf-viewer>
<p class="note" id="ex-pdf-out" role="status">A three-page sample, written by this page: try find, the outline, the links, rotation, and the comment on page 2.</p>
<div class="row" style="gap: 0.75rem; flex-wrap: wrap; align-items: center; margin-block: 0.5rem 1.5rem">
<label>Open a PDF of your own <input type="file" id="ex-pdf-file" accept="application/pdf,.pdf" /></label>
<label><input type="checkbox" id="ex-pdf-notes" checked /> Show comments</label>
</div>
<script type="module">
await customElements.whenDefined('bmx-pdf-viewer');
const viewer = document.getElementById('ex-pdf');
const out = document.getElementById('ex-pdf-out');
// A small PDF, written out object by object: three pages in Helvetica, an outline, links and a comment.
function samplePdf() {
const pages = [
['Quarterly report', 'Revenue grew in every region this quarter.', 'See the regional detail on page 3.', 'Visit binarymission.co.uk for more.'],
['Summary', 'Costs held steady while orders rose.', 'The comment on this page has a note.'],
['Regional detail', 'North: up 12%.', 'South: up 9%.', 'East: up 15%.', 'West: up 7%.'],
];
const objects = [];
const add = text => objects.push(text) && objects.length;
const esc = s => s.replace(/[\\()]/g, c => '\\' + c);
const catalog = add('');
const pageTree = add('');
const font = add('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>');
const bold = add('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>');
const pageIds = pages.map(() => add(''));
pages.forEach((lines, i) => {
const body = lines.map((line, k) => `BT /${k ? 'F1 12' : 'F2 22'} Tf 72 ${720 - k * 28 - (k ? 20 : 0)} Td (${esc(line)}) Tj ET`).join('\n');
const contents = add(`<< /Length ${body.length} >>\nstream\n${body}\nendstream`);
const annots = [];
if (i === 0) {
annots.push(add(`<< /Type /Annot /Subtype /Link /Rect [72 636 290 652] /Border [0 0 0] /Dest [${pageIds[2]} 0 R /XYZ 0 792 null] >>`));
annots.push(add('<< /Type /Annot /Subtype /Link /Rect [72 608 300 624] /Border [0 0 0] /A << /S /URI /URI (https://www.binarymission.co.uk/) >> >>'));
}
if (i === 1) {
annots.push(add('<< /Type /Annot /Subtype /Highlight /Rect [70 660 290 676] /QuadPoints [72 676 288 676 72 662 288 662] /C [1 0.9 0.2] /T (Finance) /Contents (Check this against the March figures.) >>'));
annots.push(add('<< /Type /Annot /Subtype /Text /Rect [500 700 520 720] /C [1 0.8 0] /T (Reviewer) /Contents (Looks good to me.) /Name /Comment >>'));
}
objects[pageIds[i] - 1] = `<< /Type /Page /Parent ${pageTree} 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 ${font} 0 R /F2 ${bold} 0 R >> >> /Contents ${contents} 0 R${annots.length ? ` /Annots [${annots.map(a => `${a} 0 R`).join(' ')}]` : ''} >>`;
});
objects[pageTree - 1] = `<< /Type /Pages /Kids [${pageIds.map(p => `${p} 0 R`).join(' ')}] /Count ${pageIds.length} >>`;
const outline = add('');
const items = pages.map(() => add(''));
items.forEach((id, i) => {
objects[id - 1] = `<< /Title (${esc(pages[i][0])}) /Parent ${outline} 0 R${i ? ` /Prev ${items[i - 1]} 0 R` : ''}${i < items.length - 1 ? ` /Next ${items[i + 1]} 0 R` : ''} /Dest [${pageIds[i]} 0 R /XYZ 0 792 null] >>`;
});
objects[outline - 1] = `<< /Type /Outlines /First ${items[0]} 0 R /Last ${items[items.length - 1]} 0 R /Count ${items.length} >>`;
objects[catalog - 1] = `<< /Type /Catalog /Pages ${pageTree} 0 R /Outlines ${outline} 0 R >>`;
const info = add('<< /Title (Quarterly report) /Author (Binarymission) >>');
let pdf = '%PDF-1.7\n';
const offsets = objects.map((body, i) => {
const at = pdf.length;
pdf += `${i + 1} 0 obj\n${body}\nendobj\n`;
return at;
});
const xref = pdf.length;
pdf += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n${offsets.map(o => `${String(o).padStart(10, '0')} 00000 n \n`).join('')}`;
pdf += `trailer\n<< /Size ${objects.length + 1} /Root ${catalog} 0 R /Info ${info} 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
return Uint8Array.from(pdf, c => c.charCodeAt(0));
}
await viewer.load(samplePdf());
viewer.fileName = 'quarterly-report.pdf';
viewer.addEventListener('bmxPageChange', e => (out.textContent = `Page ${e.detail.page} of ${e.detail.pageCount}.`));
viewer.addEventListener('bmxPdfLoad', e => (out.textContent = `Opened ${e.detail.title || 'the document'}: ${e.detail.pageCount} page${e.detail.pageCount === 1 ? '' : 's'}.`));
viewer.addEventListener('bmxPdfError', e => (out.textContent = e.detail.passwordRequired ? 'This document has a password: enter it in the viewer.' : `Could not open it: ${e.detail.message}`));
document.getElementById('ex-pdf-file').addEventListener('change', e => {
const file = e.target.files[0];
if (file) viewer.load(file).catch(() => {});
});
document.getElementById('ex-pdf-notes').addEventListener('change', e => (viewer.annotations = e.target.checked));
</script>
<bmx-pdf-viewer src="/files/annual-report.pdf"></bmx-pdf-viewer>
Or from script, from a URL, a File, a Blob or bytes:
await viewer.load(fileInput.files[0]);
WHAT IT READS
PDF 1.0 to 2.0: compressed and cross-reference streams, incremental updates, damaged files (read by scanning them), and documents protected with RC4 or AES encryption up to AES-256 - the viewer asks for the password when a document has one. Embedded TrueType, OpenType, Type 1 and Type 3 fonts, the standard fonts, CJK text, images, transparency, gradients and patterns. JPEG 2000, CCITT and JBIG2 images show as a grey box.
WHAT THE READER GETS
A toolbar with page navigation, zoom (page width, whole page, or a percentage), rotation, find with every match highlighted, print and download. A side panel with page thumbnails and the document's outline. Comments and form fields are shown as the document has them, read-only; a comment with a note opens it in a popup.
ACCESSIBLE
Each page carries its text as real, selectable text over the picture, in reading order, so a screen reader can read it and find can highlight it. Links are links, notes are buttons, the toolbar is named buttons, and page changes and search results are announced.
LARGE DOCUMENTS
Only the pages in view are drawn and kept, so a document of thousands of pages opens as quickly as one of ten.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
allowDownload |
allow-download |
boolean |
true |
Offers Download in the toolbar. |
allowPrint |
allow-print |
boolean |
true |
Offers Print in the toolbar. |
annotations |
annotations |
boolean |
true |
Shows the document's comments, highlights, shapes and stamps. |
fileName |
file-name |
string |
— | The name a download is saved under. Defaults to the name in src, then the document's title. |
forms |
forms |
boolean |
true |
Shows the document's form fields and their values. |
label |
label |
string |
— | The viewer's accessible name. Defaults to the document's title. |
page |
page |
number |
1 |
The page in view, 1-based. Mutable: scrolling updates it. |
password |
password |
string |
— | The password to open a protected document with, when the page knows it. Script only. |
requestInit |
property only | RequestInit |
— | Options for fetching src - headers, credentials. Script only. |
rotation |
rotation |
number |
0 |
Extra rotation for every page, clockwise: 0, 90, 180 or 270. |
sidebar |
sidebar |
'pages' | 'outline' | 'none' |
'none' |
The side panel: pages (thumbnails), outline, or none. |
src |
src |
string |
— | The document's URL. A data: or blob: URL works too. |
strings |
strings |
Partial<BmxPdfViewerStrings> | string |
— | Words the viewer shows, for translation: an object, or JSON text. |
toolbar |
toolbar |
boolean |
true |
Shows the toolbar. |
zoom |
zoom |
string | number |
'page-width' |
page-width, whole-page, or a scale: 1 is actual size, 1.5 is 150%. |
Events
| Event | Detail | Description |
|---|---|---|
bmxPageChange |
BmxPdfPageDetail |
The page in view changed. |
bmxPdfError |
BmxPdfErrorDetail |
A document could not be opened, or is waiting for its password. |
bmxPdfLinkClick |
BmxPdfLinkDetail |
The reader followed a link out of the document. Cancel it to handle the link yourself. |
bmxPdfLoad |
BmxPdfLoadDetail |
A document opened. |
Methods
| Method | Signature | Description |
|---|---|---|
download |
download(fileName?: string) => Promise<void> |
Saves the document as it was opened, under fileName. |
find |
find(text: string) => Promise<number> |
Finds text in the document and shows the first match. Resolves to the number of matches. |
getInfo |
getInfo() => Promise<BmxPdfInfo | null> |
What the document says about itself, or null with no document open. |
getOutline |
getOutline() => Promise<BmxPdfOutlineItem[]> |
The document's outline (bookmarks), as a tree. |
getText |
getText(page: number) => Promise<string> |
The text of a page, 1-based, in reading order. |
goToPage |
goToPage(page: number) => Promise<void> |
Scrolls to a page, 1-based. |
load |
load(source: BmxPdfSource, password?: string) => Promise<number> |
Opens a document from a URL, a File or Blob, or bytes. Resolves to its page count, or 0 while it waits for its password; rejects when it cannot be opened. |
print |
print() => Promise<void> |
Prints the document: every page, at its own paper size. |
rotate |
rotate(degrees?: number) => Promise<void> |
Turns every page clockwise by a quarter turn, or by degrees. |
zoomIn |
zoomIn() => Promise<void> |
Zooms in a step. |
zoomOut |
zoomOut() => Promise<void> |
Zooms out a step. |
Slots
| Slot | Description |
|---|---|
empty |
What to show when there is no document. |
toolbar-end |
Extra controls at the end of the toolbar. |
CSS shadow parts
| Part | Description |
|---|---|
body |
|
button |
|
empty |
|
error |
|
find |
|
find-count |
|
find-input |
|
link |
|
note |
|
note-button |
|
note-close |
|
outline |
|
outline-item |
|
outline-toggle |
|
page |
|
page-input |
|
password |
|
password-button |
|
password-input |
|
sidebar |
|
sidebar-tab |
|
status |
|
text-layer |
|
thumbnail |
|
thumbnails |
|
toolbar |
|
viewport |
|
zoom |
CSS custom properties
| Property | Description |
|---|---|
--bmx-pdf-viewer-canvas |
The area behind the pages. |
--bmx-pdf-viewer-height |
How tall the viewer is. |
--bmx-pdf-viewer-link-hover |
The wash over a link under the pointer. |
--bmx-pdf-viewer-match |
A search match on the page. |
--bmx-pdf-viewer-match-current |
The match being shown. |
--bmx-pdf-viewer-note |
The colour of a comment's badge. @part toolbar - the row of controls. @part button - a toolbar button. @part page-input - the page number field. @part zoom - the zoom list. @part find - the search group. @part find-input - the search field. @part find-count - "3/12" beside it. @part body - the side panel and the pages. @part sidebar - the side panel. @part sidebar-tab - its Pages and Outline buttons. @part thumbnails - the list of page thumbnails. @part thumbnail - one of them. @part outline - the outline. @part outline-item - one entry in it. @part outline-toggle - the button that opens or closes an entry's children. @part viewport - the scrolling area that holds the pages. @part page - one page. Pages are paper: white in every theme. @part text-layer - the page's text over its picture, transparent until selected. @part match - a search match. Also match-current. @part link - a link on the page. @part note-button - a comment's button: a sticky note, or the badge on a commented mark. @part note - a comment's popup. @part note-close - its close button. @part password - the form that asks for a protected document's password. @part password-input - its field. @part password-button - its button. @part status - the "Opening" and "Preparing to print" notice. @part empty - what is shown when there is no document. @part error - the message when the document could not be opened. |
--bmx-pdf-viewer-page-shadow |
The shadow under each page. |
--bmx-pdf-viewer-selection |
Selected text on a page. |
--bmx-pdf-viewer-sidebar-width |
How wide the side panel is. |