django-weasyprint icon indicating copy to clipboard operation
django-weasyprint copied to clipboard

Insert header / footer on every pdf pages

Open bastien34 opened this issue 5 years ago • 8 comments

Is there a way to generate pages with arbitrary complex footer or header (ie.rendered template)?

bastien34 avatar May 07 '20 09:05 bastien34

Here is the snippet I use to do that:

def build_pdf(html_content, header_html=None, footer_html=None):
    """
    Build a pdf and returns its binary content

    For header and footer, CURRENT_PAGE and PAGES_COUNT are replaced by the actual numbers.

    """

    def get_page_body(boxes):
        for box in boxes:
            if box.element_tag == "body":
                return box

            return get_page_body(box.all_children())

    def get_page_and_body(html, css):
        if html is None:
            return None
        html = weasyprint.HTML(
            string=html,
            base_url=getattr(settings, "WEASYPRINT_BASEURL", None),
            url_fetcher=django_url_fetcher,
        )
        css += "@page { margin 0 !important; }"
        document = html.render(stylesheets=[weasyprint.CSS(string=css)])

        document_page = document.pages[0]
        document_body = get_page_body(document_page._page_box.all_children())
        return (
            document_page,
            document_body.copy_with_children(document_body.all_children()),
        )

    def preprocess_html(html, context):
        for key, value in context.items():
            html = html.replace(key, str(value))
        return html

    document = weasyprint.HTML(
        string=html_content,
        base_url=getattr(settings, "WEASYPRINT_BASEURL", None),
        url_fetcher=django_url_fetcher,
    ).render()

    if header_html is not None or footer_html is not None:

        pages_count = len(document.pages)
        for current_page_number, page in enumerate(document.pages, start=1):
            context = {"CURRENT_PAGE": current_page_number, "PAGES_COUNT": pages_count}
            header_page, header_body = get_page_and_body(
                preprocess_html(header_html, context),
                css="body {position: fixed; top: -1.5cm;}",
            )
            footer_page, footer_body = get_page_and_body(
                preprocess_html(footer_html, context),
                css="body {position: fixed; bottom: -1.5cm;}",
            )

            page_body = get_page_body(page._page_box.all_children())

            if header_body is not None:
                page_body.children += header_body.all_children()

            if footer_body is not None:
                page_body.children += footer_body.all_children()

            page.links.extend(header_page.links)
            page.links.extend(footer_page.links)

    return document.write_pdf()

Gagaro avatar May 13 '20 13:05 Gagaro

Using css for this might be an easier approach.

Change margins according to your needs.

@charset "UTF-8";
@page {
  margin: 1cm;
  margin-top: 6cm;
  margin-bottom: 1cm;
  size: landscape;
}

.footer {
  position: fixed;
  bottom: 0;
  width: 100%;
  margin-bottom: -1cm;
}
.header {
  position: fixed;
  top: 0;
  margin-top: -5.5cm;
  width: 100%;
}

Here is the minimal html.

{% load static %}
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" media="print" href="{% static 'stylesheet.css' %}">
    <title>My title</title>
    <meta name="description" content="pdf template">
    <meta name="author" content="bars">
  </head>
  <body>
    <div class="header">
    Header content
    </div>
    
    ....
    Other Content
    ....

    <div class="footer">
    Footer content
    </div>
  </body>
</html>

barslmn avatar Dec 25 '20 11:12 barslmn

Hi @barslmn thanks for this trick. It works fine with text header, but I would like to use an image instead. Do you have an idea on how I can accomplish this?

I am trying to position the image to the right (it's a small rectangular logo about 250 * 90px). My problem is that when I "position: fixed; right: 0" it, it goes into the same line with the body of text on each page. However, when I "float: right", I am able to have it where I want it to be, but then it only appears on the first page.

If I try to move the image above the text by reducing margin-top, part of the image gets cropped, and the further I keep decreasing the margin-top, the image eventually ends up moving to the previous page (meaning it starts showing on the bottom of of each previous page)

kachmul2004 avatar Jun 01 '22 11:06 kachmul2004

Hi @kachmul2004, Looking back at it I can see that I used columns and rows to position the image in the header. So aligning images can be done like this:

CSS:

@charset "UTF-8";
@page {
  margin: 1cm;
  margin-top: 6cm;
  margin-bottom: 1cm;
  size: landscape;
}

.footer {
  position: fixed;
  bottom: 0;
  width: 100%;
  margin-bottom: -1cm;
}
.header {
  position: fixed;
  top: 0;
  margin-top: -5.5cm;
  width: 100%;
}
.row {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  width: 100%;
}
.column {
  flex-direction: column;
  flex-basis: 100%;
  flex: 1;
}

and HTML:

{% load static %}
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" media="print" href="{% static 'stylesheet.css' %}">
    <title>My title</title>
    <meta name="description" content="pdf template">
    <meta name="author" content="bars">
  </head>
  <body>
    <div class="header">
      <div class="row">
        <div class="column">
        </div>
        <div class="column">
        </div>
        <div class="column">
          <img id="myrightalignedlogo" src="{% static 'path/to/my/myrightalignedlogo.png' %}">
        </div>
      </div>
    </div>
    
    ....
    Other Content
    ....

    <div class="footer">
    Footer content
    </div>
  </body>
</html>

barslmn avatar Jun 01 '22 13:06 barslmn

@barslmn unfortunately, I am still getting the same results as before. I have even tried using pure html (without Jinja templates) and the result is still the same. Here is a jsfiddle code that will show you what I am getting.

https://jsfiddle.net/o5svhd7f/

kachmul2004 avatar Jun 01 '22 17:06 kachmul2004

Sorry I got some attribute in the css wrong. .column doesnt have width it has flex:1. I edited the above comment. And your jsfiddle: https://jsfiddle.net/5kphbo7x/

barslmn avatar Jun 03 '22 10:06 barslmn

I ended up using a table. Hacky, but easier to manipulate. Thanks

kachmul2004 avatar Jul 26 '22 08:07 kachmul2004

Hi! I found a solution than not require change the position.

That's is my CSS stylesheet: _.header { position: running(header); }

.footer { position: running(footer); text-align: right; }

@page { @top-right { content: element(header, first-except); height: -4cm; width: 100%; } @bottom-center { color: #014e04; content: counter(page); height: 1cm; text-align: center; width: 1cm; } @bottom-left { content: element(footer, first-except); height: 1cm; width: 100%; } }_

My Template (HTML) is:

_

{% block title %}{% endblock %} {% block styles %} {% endblock %}

Custom header

{{ url_your_image }}
{% block content %} {% endblock %}
_

edaagapu avatar Aug 30 '22 09:08 edaagapu