Pdf.js: Font lost when rendered in an iframe

Created on 11 Apr 2017  路  9Comments  路  Source: mozilla/pdf.js

I'm trying to use an iframe to print a PDF easily, everything is ok but the fonts.

Configuration:

  • Web browser and its version: chrome 57
  • Operating system and its version: MacOS Sierra 10.12.7
  • PDF.js version: pdfjs-dist 1.8.177

Steps to reproduce the problem:

  1. Dynamically create an iframe with a canvas, render the pdf in the canvas

Here is my print function which takes the pdfDoc as parameter (from PDFJS.getDocument(url))

function print(pdfDoc) {
      if (!pdfDoc || !pdfDoc.numPages) {
        return ;
      }

      var scale = 5;
      var iframe = window.document.createElement('iframe');
      var doc = iframe.contentDocument || iframe.contentWindow.document;

      window.document.body.append(iframe);

      doc.open();
      doc.write(
        '<!DOCTYPE html>' +
          '<html>' +
            '<head>' +
              '<style>' +
                '* {margin: 0 !important; padding: 0 !important}' +
                'div{width:612pt; height:792pt;}' +
                'canvas{width:100%; height:100%;}' +
            '</style>' +
          '</head>' +
          '<body></body>' +
        '</html>'
      );
      doc.close();

      function renderPage(num) {
        console.log('Rendering page %d of %d', num, pdfDoc.numPages);

        return pdfDoc
          .getPage(num)
          .then(function (page) {
            var div = doc.createElement('div');
            var canvas = doc.createElement('canvas');
            div.appendChild(canvas);
            doc.body.appendChild(div);
            var viewport = page.getViewport(scale);
            var ctx = canvas.getContext('2d');
            canvas.width = viewport.width;
            canvas.height = viewport.height;
            return page.render({canvasContext: ctx, viewport: viewport}).promise;
          })
          .then(function () {
            if (num < pdfDoc.numPages) {
              return renderPage(num + 1);
            }
          });
      }

      renderPage(1)
        .then(function () {
          iframe.contentWindow.print();
          iframe.remove();
        });
    }

On the left, the main page canvas, on the right, the iframe canvas.

image

The rendering in the iframe seems to not embed the fonts.

1-other

Most helpful comment

Hi, if somebody visits this issue again, here is a workarround. I followed the steps @Rob--W provided above and can confirm it works on all browsers (Safari, Chrome, Firefox and Edge).

Here is the code snippet (Renders all pages at once):

import {PDFPageViewport} from 'pdfjs-dist';
import pdfjsLib from 'pdfjs-dist/webpack';
import {range} from 'lodash';

const renderPage = async (page: any, container: HTMLElement) => {
    // This gives us the page's dimensions at full scale
    const viewport: PDFPageViewport = page.getViewport({ scale: 1 });

    // We'll create a canvas for each page to draw it on
    const canvas = document.createElement('canvas');
    canvas.style.display = 'block';
    container.appendChild(canvas);

    const context = canvas.getContext('2d');
    canvas.height = viewport.height;
    canvas.width = viewport.width;

    // Draw it on the canvas
    await page.render({ canvasContext: context, viewport }).promise;

    // eslint-disable-next-line max-len
    return new Promise<{ blob: Blob, height: number, width: number }>(((resolve) => canvas.toBlob((result) => resolve({ blob: result!, height: viewport.height, width: viewport.width }))));
};

// Render a hidden container element to add all canvases (canvas per page in the pdf) 
const container = document.createElement('div') as HTMLDivElement;
container.style.visibility = 'hidden';
document.body.appendChild(container);

// Create the iframe
const iframe = document.createElement('iframe') as HTMLIFrameElement;
iframe.style.visibility = 'hidden';
iframe.src = 'about:blank';
document.body.appendChild(iframe);
iframe.contentWindow!.onbeforeunload = () => document.body.removeChild(iframe);
iframe.contentWindow!.onafterprint = () => document.body.removeChild(iframe);

const pdf = await pdfjsLib.getDocument(source).promise;
const numPages = pdf.numPages;

const pageNumbers = range(1, numPages + 1);
// Create the canvas for each page and retrieve the image blob data
const imageBlobData = await Promise.all(pageNumbers.map(async number => {
    return renderPage(await pdf.getPage(number), container);
}));
// Due to a bug we need to copy the canvas image to the iframe instead of writing the canvas directly
// in the iframe. (https://github.com/mozilla/pdf.js/issues/8271)
imageBlobData.forEach((blobData) => {
    const image = document.createElement('img');
    image.style.height = `${blobData.height}`;
    image.style.width = `${blobData.width}`;
    image.src = URL.createObjectURL(blobData.blob);
    iframe.contentWindow!.document.body.appendChild(image);
});
// Remove the intermediary container element
document.body.removeChild(container);

// Wait for the content to be fully rendered and print
await setTimeout(() => {
                iframe.contentWindow!.focus();
                iframe.contentWindow!.print();
            }, 500);

All 9 comments

can I work on this bug ?

Yes, please feel free to work on this.

@amitsin6h Let me know if you need anything else

This is likely caused by the fact that src/display/font_loader.js uses document.fonts to register fonts, while you are trying to print in another document.

Try serializing the canvas, e.g. via canvas.toDataURL or canvas.toBlob + URL.createObject, and then create the image in the child frame.
In the past I created a code snippet to render PDFs in a child frame. This is the logic to render the pages in a (different) document: https://github.com/mozilla/pdf.js/pull/7721/files#diff-cb42f7bfbf80ff0d0b4c724e6171aab9R253 (to get the blob, use .toBlob on the canvas. Instead of using blobs, you can also generate data:-URLs, but that consumes more memory)

To fix this at its core, we should not rely on a global document or window object, but add a parameter to the API to allow the caller to specify document, and use that instead. Then the PDF.js library can also be used on Node.js without requiring a global assignment of document, e.g. as done in examples/node/pdf2svg.js. (via domutils.js)

I'm gonna test, thanks

Any update on it?

Hi all! Anyone tried to get some luck on making the PDF fonts rendered correctly in an iframe?

Hi, if somebody visits this issue again, here is a workarround. I followed the steps @Rob--W provided above and can confirm it works on all browsers (Safari, Chrome, Firefox and Edge).

Here is the code snippet (Renders all pages at once):

import {PDFPageViewport} from 'pdfjs-dist';
import pdfjsLib from 'pdfjs-dist/webpack';
import {range} from 'lodash';

const renderPage = async (page: any, container: HTMLElement) => {
    // This gives us the page's dimensions at full scale
    const viewport: PDFPageViewport = page.getViewport({ scale: 1 });

    // We'll create a canvas for each page to draw it on
    const canvas = document.createElement('canvas');
    canvas.style.display = 'block';
    container.appendChild(canvas);

    const context = canvas.getContext('2d');
    canvas.height = viewport.height;
    canvas.width = viewport.width;

    // Draw it on the canvas
    await page.render({ canvasContext: context, viewport }).promise;

    // eslint-disable-next-line max-len
    return new Promise<{ blob: Blob, height: number, width: number }>(((resolve) => canvas.toBlob((result) => resolve({ blob: result!, height: viewport.height, width: viewport.width }))));
};

// Render a hidden container element to add all canvases (canvas per page in the pdf) 
const container = document.createElement('div') as HTMLDivElement;
container.style.visibility = 'hidden';
document.body.appendChild(container);

// Create the iframe
const iframe = document.createElement('iframe') as HTMLIFrameElement;
iframe.style.visibility = 'hidden';
iframe.src = 'about:blank';
document.body.appendChild(iframe);
iframe.contentWindow!.onbeforeunload = () => document.body.removeChild(iframe);
iframe.contentWindow!.onafterprint = () => document.body.removeChild(iframe);

const pdf = await pdfjsLib.getDocument(source).promise;
const numPages = pdf.numPages;

const pageNumbers = range(1, numPages + 1);
// Create the canvas for each page and retrieve the image blob data
const imageBlobData = await Promise.all(pageNumbers.map(async number => {
    return renderPage(await pdf.getPage(number), container);
}));
// Due to a bug we need to copy the canvas image to the iframe instead of writing the canvas directly
// in the iframe. (https://github.com/mozilla/pdf.js/issues/8271)
imageBlobData.forEach((blobData) => {
    const image = document.createElement('img');
    image.style.height = `${blobData.height}`;
    image.style.width = `${blobData.width}`;
    image.src = URL.createObjectURL(blobData.blob);
    iframe.contentWindow!.document.body.appendChild(image);
});
// Remove the intermediary container element
document.body.removeChild(container);

// Wait for the content to be fully rendered and print
await setTimeout(() => {
                iframe.contentWindow!.focus();
                iframe.contentWindow!.print();
            }, 500);

Nice workaround but it would still be great to be fixed in core PDF.js.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

AlexP3 picture AlexP3  路  3Comments

hp011235 picture hp011235  路  4Comments

patelsumit5192 picture patelsumit5192  路  3Comments

BrennanDuffey picture BrennanDuffey  路  3Comments

kleins05 picture kleins05  路  3Comments