v2.0.0

<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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
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

MethodSignatureDescription
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

SlotDescription
empty What to show when there is no document.
toolbar-end Extra controls at the end of the toolbar.

CSS shadow parts

PartDescription
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

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