Client-side PDF Export

Last modified by Vincent Massol on 2024/11/19 16:14

Description

The goal of this design page is to list the requirements of the PDF export feature and to document possible implementation solutions.

Requirements

  • R1 Content Export the content of a given wiki page to PDF.
    • R1.1 Style Preserve the content style: the exported content should have the same style in the generated PDF as when the wiki page is viewed in the browser.
    • R1.2 Custom Style Be able to customize the content style (allow users to use different styles in the PDF export than the ones used when viewing the page in the browser).
    • R1.3 JavaScript Include content generated with JavaScript.
    • R1.3 Links Generate internal links whenever possible (e.g. whenever the target section is included in the PDF).
    • R1.4 Comments Include the wiki page comments at the end.
    • R1.5 Attachments Include the wiki page (image) attachments at the end.
    • R1.6 Linked Pages Include the linked wiki pages.
    • R1.7 Child Pages Include the child wiki pages (depth-first traversal).
  • R2 Cover Be able to generate a cover page.
    • R2.1 The cover page should display by default the title of the current wiki page.
    • R2.2 The cover page shouldn't display the header and the footer.
    • R2.3 Custom Cover Be able to customize the cover page.
  • R3 ToC Be able to show the table of contents.
    • R3.1 Show the page number for each section and link to the corresponding section within the PDF.
    • R3.2 If multiple wiki pages are exported, each should have a top level entry in the table of contents.
  • R4 Header & Footer Be able to generate a header and a footer for PDF pages (excluding the PDF cover page).
    • R4.1 Show the title of the current wiki page in the PDF header by default. This means we need to be able to have a different PDF header on different pages.
    • R4.2 Page Number Show the current page number and the total number of pages in the PDF footer by default.
    • R4.3 Custom Header & Footer Be able to customize the PDF header and footer.
    • R4.4 Skip Header & Footer Be able to skip the PDF header and footer on some pages (e.g. the cover page).
  • R5 Multipage Be able to export multiple wiki pages to PDF.
    • R5.1 Use a depth-first traversal of the hierarchy tree when an explicit order is not provided.
    • R5.2 Start each wiki page on a new PDF page (page break before).
    • R5.3 Show wiki page titles in the Table of Contents.
    • R5.4 Be able to have a cover page for each wiki page (similar to a new book chapter), showing the wiki page title by default.
    • R5.5 The PDF header and footer should match the corresponding wiki page (e.g. title, author, modification date, etc.)

Implementation Alternatives

There are multiple ways in which we can export a wiki page to PDF, each with its own pros and cons.

From HTML+CSS to XSL-FO and PDF

This solution involves the following steps:

  1. Render the wiki page to XHTML using a dedicated PDF export Velocity template that has support for cover page, table of contents, header and footer.
  2. Inline the CSS.
  3. Transform the XHTML with in-lined CSS into XSL-FO using a dedicated XSL transformation file
  4. Use a tool that can convert the XSL-FO to PDF (e.g. Apache FOP)

Pros:

  • Packaging Very good support for generating cover pages, table of contents, page header and footer, page numbers. This solution is very good if you need to package the wiki content in a very specific way in PDF (e.g. create custom and complex page headers).
  • Consistency Performed on the server so the result is consistent (predictable and reproducible) for all the users.
  • Scalability Scales well if the exported content is very large (in the worse case the administrator can choose to allocate more resources to the XWiki instance).

Cons:

  • Styles The conversion from CSS to XSL-FO styles is far from perfect. This makes it hard to preserve the content style as seen by the user in their Web browser.
  • JavaScript Dynamic content generated by JavaScript on the client side is not included. In order to support this we would have to use a Web browser to render the XHTML from step one and to execute the JavaScript, retrieving the final HTML after the JavaScript code is executed. The Web browser could be either the one used by the user or another one running in a Docker container on the server side. But if we rely on the user web browser we lose the consistency.

From HTML+CSS directly to PDF

This solution involves rendering the wiki page to HTML and converting the result directly the PDF.

Using Browser Print

We can use the browser's print function to convert a web page to PDF. These could be the steps:

  1. Render the wiki page to HTML using a dedicated PDF export (Velocity) template that has support for cover page, table of contents, header and footer.
  2. Wait for the web page to be fully loaded (ready for user interaction); this means waiting for the JavaScript initialization code to be executed.
  3. Call window.print() to open the Print dialog which allows the user to convert the web page to PDF.

Pros:

  • Styles The content style is (almost) the same in the PDF as in the web page.
  • JavaScript Dynamic content generated by JavaScript can be included in the PDF.

Cons:

  • Consistency Potential issues:
    • The result may be different for different users because they use different browsers, or different versions of the same browser. The inconsistency can be reduced by doing the export on the server-side using a web browser (e.g. headless Chrome) that runs inside a Docker container.
    • When exporting multiple wiki pages we generate a single web page with all their content which means CSS and JavaScript from one wiki page can interfere with content from a different wiki page.
      • The CSS loaded by one wiki page is now applied to all the rest of the wiki pages from the export so if the CSS selectors are not very specific then the result could be different than what you see when you view the wiki pages separately.
      • The execution scope for the JavaScript code is larger so the behavior could be different (e.g. some JavaScript code that looks for images within the wiki page may find more images during the PDF export because it looks in all the pages included in the export).
    • Waiting for the JavaScript initialization code to be executed (in order for the web page to be fully loaded) might not be very reliable (all the time). The result could be different depending on how fast the web browser is but also depending on the execution order of various JavaScript modules.
  • Scalability The browser has to load the entire HTML before print; if the HTML is huge (e.g. if you want to print the entire wiki to PDF) then this could possibly crash the browser.
  • Packaging CSS has limited support for generating PDF headers, footers and table of contents. We basically have two options:

    1. CSS Page Use the CSS Paged Media Module. You can find an example at Designing For Print With CSS or at HTML and CSS for printing books.

      Pros:

      • It allows us to define different page types (cover, table of contents, left, right, etc.), and we can control how each page type is printed.
      • We can specify on which page type a given HTML element should be printed (e.g. only on the cover page).
      • We can specify on which page type to show the header and footer and on which page type to hide (skip) them.
      • We can put complex content in the page header and footer using running elements
      • We can generate the table of contents with page numbers (checkout the target-counter() CSS function).

      Cons:

      • It's not implemented in standard web browsers. We'd have to use either a polyfill such as Paged.js or an user agent that is specialized in print (e.g. Prince, which is proprietary).
      • Using a special user agent probably means limited support for JavaScript (not a problem when using the Paged.js polyfill though).
    2. CSS3 Use standard web-oriented CSS that produces acceptable results in print:

      Pros:

      • We can use any main-stream web browser (we don't need one specialized in print).
      • We can include whatever we want in the PDF header and footer so this allows for complex customizations.

      Cons:

      • With standard CSS you can force some HTML element to appear on all printed pages (e.g. as a header or footer) but you cannot skip some printed pages: it's all or none. This makes it impossible to have both a cover page and pages with header and footer.
      • Page Numbers It's not possible to show page numbers, neither in the footer nor in the table of contents. There is a hack for Firefox, but it doesn't work in Chrome.
      • It's not possible to have a different header or footer on different pages (e.g. show the title of the current wiki page in the header, when multiple wiki pages are exported), unless we manually split the content in print pages (basically re-implement what Paged.js does).

Proposed Implementation

The best solution (that has the most pros and the least cons) seems to be to use Paged.js as a polyfill for CSS Paged Media Module. Our experiments showed that it leads to good results overall but is has some notable limitations:

  • The browser adds some CSS that hides background images and colors and modifies the look & feel in general in order to produce print-friendly results. This can be overwritten from our styles using !important to force the background image for instance, and also by using print-color-adjust but it's tedious and it sometimes requires to duplicate CSS.
  • Firefox doesn't generate internal links (e.g. for the Table of Contents) but Chrome does.
  • Paged.js doesn't split very well long tables on multiple print pages (#289) but I believe they are actively working on improving the table handling. See XWIKI-19555.
  • Mixing different page orientation (e.g. mixing portrait and landscape) is not well supported (neither by Paged.js nor by the browser). See XWIKI-17105.
  • The generated PDF doesn't pass accessibility checks (WCAG 2.0). See XWIKI-19560.

 

Get Connected