22

I'm trying to use Google Chrome as a replacement of PhantomJS to render HTML into PDF. So far it's been working well for me. The only issue I have that I have not found any way to translate the following PhantomJS code:

page.paperSize = {
  footer: {
    contents: phantom.callback(function(pageNum, numPages) {
      return "Power by MyWebsite. Created on "+formatDate(new Date())+"<span style='float:right'>" + pageNum + " / " + numPages + "</span>";
    })
  }
}

Format date is the same function as in question How to format a JavaScript date

However I have not found a way to replicate this behavior in Google Chrome in headless. I am using Chrome remote interface (CDP) from https://github.com/cyrus-and/chrome-remote-interface

This is an outline of my chrome remote interface code:

return new Promise(async function (resolve, reject) {
    const url = "<MyURL here>";
    const [tab] = await Cdp.List()
    const client = await Cdp({ host: '127.0.0.1', target: tab });
    await Promise.all([
       Network.enable(),
       Page.enable()
    ]);

    Page.loadEventFired(function () { 
         setTimeout(function () {
             resolve(Page.printToPDF({displayHeaderFooter:true}))); //https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF
         }, 3000);
    });
    await Page.navigate({ url }); 
};

I'm getting the PDF just fine but can only get the default Chrome headers and footers. Any way to modify them?

Note: I realise that I can use JavaScript in my page to append elements to the bottom of each page, but I'd prefer to alter the footer that is appended by the browser when exporting/printing as I've found it's much more reliable to get placed correctly and without causing any strange re-flowing of other divs in the page.

apokryfos
  • 30,388
  • 6
  • 55
  • 83
  • When chrome is in window mode, then also it does not support changing header and footer content, so I think headless method won't support that too. It is clear from the link (https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF) in your code that there are no such parameters – Sumit Kumar Jun 20 '17 at 18:56
  • with javascript how would you go about adding elements to the top of each page? I understand we can add the elements on the top of the document but how will we target to top of each page in the final PDF ? – kyriakos Aug 15 '17 at 23:09
  • @kyriakos You'd need each page element to be in its own `div` for that to be possible. It's a solution, but not a good solution. – apokryfos Aug 16 '17 at 07:52

7 Answers7

17

This is an update/answer to the question. As of Chromium 64 it is possible using the headerTemplate and footerTemplate parameters to printToPDF

Using chrome remote interface here's example code that should work:

return new Promise(async function (resolve, reject) {
    const url = "<MyURL here>";
    const [tab] = await Cdp.List()
    const client = await Cdp({ host: '127.0.0.1', target: tab });
    await Promise.all([
       Network.enable(),
       Page.enable()
    ]);

    Page.loadEventFired(function () { 
         setTimeout(function () {
    //https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF
             resolve(Page.printToPDF({
                  displayHeaderFooter:true,
                  footerTemplate: "<span class='pageNumber'> of <span class='totalPages'>"
             }))); 
         }, 3000);
    });
    await Page.navigate({ url }); 
};
apokryfos
  • 30,388
  • 6
  • 55
  • 83
  • 1
    Note there is no way to pass argument `displayHeaderFooter` to chrome CLI. list of chrome headless cli arguments supported in https://cs.chromium.org/chromium/src/headless/app/headless_shell_switches.cc?sq=package:chromium&type=cs&g=0 . Using puppeteer in nodejs or connect to DevTools via websocket in any other language. – Zero Zhang Jun 14 '18 at 10:43
  • @ZeroZhang Yes, the code is for the chrome remote interface (which basically is an SDK for the chrome devtools protocol) but it may not have been emphasised properly and I can see how someone can easily miss it. I've updated with links to the github repo. – apokryfos Jun 14 '18 at 13:51
  • for someone else who are looking for these options in command line tools. I spent some times. – Zero Zhang Jun 14 '18 at 13:57
  • @VaibhavBhatia I use puppeteer finally. – Zero Zhang May 06 '19 at 10:08
16

It is possible to create custom headers and footer by using <header> and <footer> tags. I use this for generating PDF's using Chrome Headless. I have not tested it in Firefox, IE etc...

<header>
  Custom Header
  <img src="http://imageurl.com/image.jpg"/>
</header>
<div class="content">Page Content - as long as you want</div>
<footer>
  Footer Content
</footer>

the CSS

@page {
  margin: 0;
}
@media print {
  footer {
    position: fixed;
    bottom: 0;
  }
  header {
    position: fixed;
    top: 0;
  }
}

The @page { margin: 0 } removes the default header and footer.

Hope this helps.

Darren Hall
  • 772
  • 6
  • 13
9

There are two solutions to your problem

A) Push the chrome-header out by leaving no margin :

 @page { 
     margin: 0;
     size: auto;
 }

or

 @media print {
   @page { margin: 0; }
   body { margin: 1.6cm; }
 }

B) Originally a Firefox solution which should world for Chrome

 <html moznomarginboxes mozdisallowselectionprint>

some sample:

<!DOCTYPE html>
<html moznomarginboxes mozdisallowselectionprint>
<head>
<title>Print PDF without header</title>
<style>
@media print {
    @page { margin: 0; }
    body { margin: 1.6cm; }
}
</style>
</head>
<body>
<p>Some Text in Paragraph to print!</p>
<a href="javascript:print()">Print</a>
</body>
</html>
Mason.Chase
  • 780
  • 6
  • 18
  • 3
    The question is not regarding the pushing out of the default chrome headers/footers it is about customizing them. – apokryfos Jun 26 '17 at 06:32
  • Well Chrome behavior is when you push it out , the header option will be eliminated, it is weird but it's true, try this and will see the header checkbox is gone – Mason.Chase Jun 26 '17 at 07:50
  • 1
    This really only works for a single page, otherwise the top and bottom margins on the body will not produce a top and bottom margin on subsequent pages. – Alec Jacobson Sep 25 '18 at 13:13
8

Update

You can use the headerTemplate and footerTemaplate parameters in printToPDF to customize the header and footer when printing to PDF.

headerTemplate and footerTemaplate accept valid HTML markup, and you can use the following classes to inject printing values into your HTML elements:

  • date - will inject the current date in printable format into the HTML element containing the class
  • title - document title
  • url - document location
  • pageNumber - current page number
  • totalPages - total number of pages

For example, to print the page number and total number of pages:

Page.printToPDF({
    displayHeaderFooter: true,
    footerTemplate: "<span class='pageNumber'></span> <span>out of</span> <span class='totalPages'></span>"
})

Original

(At the time, this was not possible to achieve with Chrome DevTools.)

According to this forum, there is currently no way to do this in google chrome. All you can do is turn the header/footer on or off. This is indicated by the comment:

Currently there isn't a way to edit the header when printing a document. You can currently only turn the header and footer on or off which includes the date, name of the web page, the page URL and how many pages the document you're printing. You may want to check out the Chrome Web Store to see if there are any handy third party extensions that you can install on Chrome that may fit what you're looking for in terms of printing -- Source There may be third-party extensions to get the functionality you are looking for, or as you suggest, you can use JavaScript to append the elements you want to print.

Chava Geldzahler
  • 3,225
  • 1
  • 14
  • 30
4

If you really want to do this in a non-hacky way, you have to get down to the chrome devtools protocol since the command line interface doesn't support much (aka --print-to-pdf is all you get, no print options)

The protocol is documented here: https://chromedevtools.github.io/devtools-protocol/tot

Client libraries are available in many languages, available via package managers.

Here's some examples I put together to demonstrate usage:

The Page.printToPDF protocol method supports arguments for passing custom markup for header and footer.

Basically, the protocol is defined in a protocol.json, which client libraries consume to generate the classes/methods for use in any application. These methods encapsulate the lower level communications with the protocol.

All I had to do was install a client library package (via npm or composer), make sure chrome was installed, write a little code, and I'm generating pdfs with no header/footer! Fantastic.

Stoutie
  • 1,684
  • 20
  • 15
2

For those of you who just want something that works out of the box, I would like to share my script I wrote today, based on the answer of apokryfos.

At first you need to install the dependencies

yarn global add chrome-remote-interface

Next you need to start a headless chromium with a debugging port enabled

chromium-browser --headless --disable-gpu --run-all-compositor-stages-before-draw --remote-debugging-port=9222

Now you have to save my script i.e. as print-via-chrome.js:

#!/usr/bin/env node

const homedir = require('os').homedir();
const CDP = require(homedir+'/.config/yarn/global/node_modules/chrome-remote-interface/');
const fs = require('fs');

const port = process.argv[2];
const htmlFilePath = process.argv[3];
const pdfFilePath = process.argv[4];

(async function() {

        const protocol = await CDP({port: port});

        // Extract the DevTools protocol domains we need and enable them.
        // See API docs: https://chromedevtools.github.io/devtools-protocol/
        const {Page} = protocol;
        await Page.enable();

        Page.loadEventFired(function () {
                console.log("Waiting 100ms just to be sure.")
                setTimeout(function () {
                        //https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF
                        console.log("Printing...")
                        Page.printToPDF({
                                displayHeaderFooter: true,
                                headerTemplate: '<div></div>',
                                footerTemplate: '<div class="text center"><span class="pageNumber"></span></div>',
                                //footerTemplate: '<div class="text center"><span class="pageNumber"></span> of <span class="totalPages"></span></div>'
                        }).then((base64EncodedPdf) => {
                                fs.writeFileSync(pdfFilePath, Buffer.from(base64EncodedPdf.data, 'base64'), 'utf8');
                                console.log("Done")
                                protocol.close();
                        });
                }, 100);
        });

        Page.navigate({url: 'file://'+htmlFilePath});
})();

After making it executable with chmod +x print-via-chrome.js you should be able to convert html files to pdf files like so:

./print-via-chrome.js 9222 my.html my.pdf

Don't forget to quit the chromium after you finished your transformation.

I am pretty sure that this solution is far from perfect, but at least it works and as I saw a lot of questions about this feature and had to invest a few hours of my own time to get it working I wanted to share my solution. Some of the problems I had were related to the header- and footerTemplates as it seems that empty templates do not replace the existing ones (you need <div></div>) and while differently documented the new templates do not appear in a visible region until wrapped with a <div class="text center"> or something similar.

JepZ
  • 933
  • 12
  • 24
  • When I run all of this I get a `Cannot find module '/root/.config/yarn/global/node_modules/chrome-remote-interface/'` when logged into root and/or my regular non-root user – Brian Leishman Sep 20 '18 at 22:55
  • 1
    @BrianLeishman Please do not use root for this (it's neither necessary nor wise). Your error sounds like you skipped the `yarn global add chrome-remote-interface` command in the beginning or it failed? Are you using yarn or npm? – JepZ Sep 21 '18 at 23:45
2

As mentioned by Alec Jacobson in the comments, using margin:0 on the page along with margin:1.6cm on the body works only for one page.

What worked for me was to wrap my content in a table, using thead for the top margin and tfoot for the bottom margin. Thead and tfoot are repeated on all pages and your main page content goes in the tbody of the table.

Example:

`

    <thead> 
        <tr> 
            <th style="height: 1cm">
                Header    
            </th>
        </tr>
    </thead>

    <tbody>
        <tr>
            <td>

                Page Content

            </td>
        </tr>
    </tbody>

    <tfoot>
        <tr>
            <td style="height: 1cm">

                Footer

            </td>
        </tr>
    </tfoot>

</table>`

Would have liked to have added to the comment thread but don't have enough reputation.

Ranjit Chawla
  • 63
  • 2
  • 5
  • I like your solution very much, it should meet most of needs. You should and a – SCO May 28 '20 at 12:53