Skip to content

Instantly share code, notes, and snippets.

@doekman
Last active June 12, 2019 02:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save doekman/4674dd2d05b643ef57e747cfdc81c3c9 to your computer and use it in GitHub Desktop.
Save doekman/4674dd2d05b643ef57e747cfdc81c3c9 to your computer and use it in GitHub Desktop.
Add support for page overlays to support non-trivial page headers/footers in Flask-WeasyPrint

Paged media CSS doesn't support more complex page header/footer requirements, like tables. A solution to this is to create a separate page with such a header, and merge this into the PDF on the required pages. A proof of concept was written by pikhovkin in this Gist.

Overlay: Represents one HTML page to be overlaid on a PDF document.

  • template_name_or_list: the name of the HTML template, which is resolved with jinja2's get_or_select_template method.
  • fn_include_on_page: a function that takes a 0-based page number, and returns a boolean that indicates if it needs to be included on that page.
  • kwargs: context used to render the overlay. Can also be used to dynamically position content.

render_pdf_with_overlays renders the PDF like Flask-WeasyPrint's render_pdf, but with overlays.

  • stylesheets are also used to render the Overlay, so if any FontConfiguration is included, it is also included in the Overlays.

  • the overlays templates are rendered to HTML with the supplied context, and this context is extended with the page and pages entries, indicating current 1-based page number and number of pages (CSS page and pages counters don't work with single page overlays)

      #example:
      html = flask.render_template('my_report.html')
      first_page_overlay = Overlay('first_page_overlay.html', fn_include_on_page=first_page, left='10cm', top='5cm')
      default_page_overlay = Overlay('default_page_overlay.html', fn_include_on_page=not_first_page)
      pdf_response = render_pdf_with_overlays(HTML(string=html), first_page_overlay, default_page_overlay)
    
from flask_weasyprint import HTML, CSS
from flask import current_app
#Based on: <https://gist.github.com/pikhovkin/5642563#file-weasyprint_complex_headers-py-L22>
def get_page_body(boxes):
for box in boxes:
if box.element_tag == 'body':
return box
return get_page_body(box.all_children())
first_page = lambda page_nr: page_nr==0
not_first_page = lambda page_nr: page_nr!=0
odd_page = lambda page_nr: page_nr%2==1
even_page = lambda page_nr: page_nr%2==0
PAGE_CONTEXT_NAME = "page"
PAGES_CONTEXT_NAME = "pages"
class Overlay(object):
def __init__(self, template_name_or_list, fn_include_on_page=not_first_page, **kwargs):
self.template_name_or_list = template_name_or_list
self.context = kwargs
self.fn = fn_include_on_page
self.name = template_name_or_list
class OverlayObject(object):
def __init__(self, overlay, stylesheets=None, **kwargs):
exists_links = False
context = {**overlay.context, **kwargs}
template = current_app.jinja_env.get_or_select_template(overlay.template_name_or_list)
html = HTML(string=template.render(context))
overlay_doc = html.render(stylesheets=stylesheets)
overlay_page = overlay_doc.pages[0]
exists_links = exists_links or overlay_page.links
overlay_body = get_page_body(overlay_page._page_box.all_children())
overlay_body = overlay_body.copy_with_children(overlay_body.all_children())
self.stylesheets = stylesheets
self.exists_links = exists_links
self.overlay_page = overlay_page
self.overlay_body = overlay_body
self.fn = overlay.fn
self.name = overlay.name
def render_pdf_with_overlays(html, *overlays, stylesheets=None, download_filename=None):
# Main template
if not hasattr(html, 'write_pdf'):
html = HTML(html)
main_doc = html.render(stylesheets=stylesheets)
#TODO: put conversion from Overlay to OverlayObject into the for-loop, so page numbers work
# (will this be stable and not result in CAIRO out of memory, or Pango this will be ugly errors?)
context = {PAGE_CONTEXT_NAME: len(main_doc.pages)}
overlay_objects = [OverlayObject(overlay, stylesheets=stylesheets, **context) for overlay in overlays]
# Insert overlays in main doc
for page_nr, page in enumerate(main_doc.pages):
for overlay_object in overlay_objects:
if overlay_object.fn(page_nr):
print(f'#{page_nr}, add overlay:{overlay_object.name}')
else:
continue
page_body = get_page_body(page._page_box.all_children())
print('-appending overlay children')
page_body.children += overlay_object.overlay_body.all_children()
if overlay_object.exists_links:
print('-appending overlay links')
page.links.extend(overlay_object.overlay_page.links)
print('-writing PDF')
pdf = main_doc.write_pdf()
response = current_app.response_class(pdf, mimetype='application/pdf')
if download_filename:
response.headers.add('Content-Disposition', 'attachment', filename=download_filename)
return response
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment