Skip to content

Building Templates

Template design considerations

Typical report template design will require external resources, such as Javascript libraries and web fonts. These resources can be added locally or referenced externally from eg. CDNs, as any regular web page. However, keep in mind that referencing external resources may pose a security risk, and requires internet access when generating PDFs.

Having these resources locally will reduce the PDF rendering time and minimize or eliminate the need to have internet access, but will increase the packaged report size and the effort required to maintain reports. Some assets may also have licensing limitations that prevent usage and redistribution in a packaged format.

The recommended approach depends on the use case. Reports where PDF rendering time is not critical can reference resources externally as needed; Reports where rendering time is critical (such as generated in response to a user interaction) should have these resources referenced locally, as much as possible.

ZipReport Jinja considerations

ZipReport allows most Jinja features, including adding your own filters. However, you can only import templates within your report folder path (including subfolders). Adding extensions to Jinja is also supported, when using the JinjaRender class directly. It works by instantiating zipreport.template.JinjaRender with the required options:

from zipreport.report import ReportFileLoader
from zipreport.template import JinjaRender

# load report from file
zpt = ReportFileLoader.load("simple.zpt")

# extensions for jinja usage
jinja_options = {
    "extensions": ['jinja2.ext.i18n'],
}

# initialize JinjaRender() with the desired options
renderer = JinjaRender(zpt, jinja_options)

CSS considerations

When using zipreport-server, be aware that the media type print will be used for PDF generation.

Waiting for JavaScript execution before rendering

Some reports may contain complex JavaScript logic for page composition, and timing the finalization of these operations are a challenge. ZipReport provides a more reliable alternative to relying on waiting a predefined amount of time before triggering the render - a JavaScript event notification system that explicitly notifies the supervisor application that client-side rendering is finalized and PDF generation can be done.

The notification mechanism works by writing the string 'zpt-view-ready' to the console. As an example, this can be used to signal paged.js end of operations:

<!DOCTYPE html PUBLIC>
<html lang="en" lang="en">
<head>
    <script src="https://s3.amazonaws.com/pagedmedia/pagedjs/dist/paged.js"></script>
    <script>
        // custom paged.js handler
        class handlers extends Paged.Handler {
            constructor(chunker, polisher, caller) {
                super(chunker, polisher, caller);
            }

            afterPreview(pages) {
                // event signaling readiness for PDF generation
                console.log('zpt-view-ready')
            }
        }

        // Register handler on paged.js library
        Paged.registerHandlers(handlers);
    </script>
</head>
<body>

Generating images dynamically

ZipReport provides several filters that allow the generation and inclusion of images generated in runtime, without requiring client-side composition. This is quite useful to add data-driven graphics to the reports, both for PDF and MIME generation.

The image is generated by calling a function that will save the results to a io.BytesIO memory buffer. This buffer will be added to the in-memory representation of the report file as an image with a random name by the filter, and then the appropriate html image tag will be generated, for rendering purposes.

Bundled Jinja filters for images

Filter name Description
png Generates dynamic PNG image
gif Generates dynamic GIF image
jpg Generates dynamic JPEG image
svg Generates dynamic SVG image

Call syntax:

Positional args:
    {{ callable | filter(data_source, alt_text, width, height, css_class }}

Named args:
    {{ callable | filter(data=data_source, alt=alt_text, width=width, height=height, class=css_class }}

Where:

Name Description
callable function that will generate the specific graphic; it is passed as a template variable
filter bundled jinja filter to generate the appropriate img tag; see below for available filters
data_source variable with data to be passed to the callable as a parameter
alt_text optional img tag alt text
width optional image tag width
height optional image tag height
css_class optional image tag css classes

Writing an image generation function for an image filter

Writing a function to be used dynamically in the report is quite simple; it is a regular function that receives an argument and returns a io.BytesIO buffer object.

Below we'll see a complete example on how to generate and embed a simple image on a template. The complete code is available on the examples/filter_simple folder.

Example file list:

sample_report/index.html
sample_report/manifest.json
main.py

We start with a simple html template and a manifest file into the sample_report folder:

Html template (index.html):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Zipreport jinja filter example</title>
</head>
<body>
<h1>PNG Example</h1>
{{ colored_rectangle_fn|png(rectangle_color, "label for png") }}
</body>
</html>

Manifest file(manifest.json):

{
  "author": "zipreport",
  "title": "Jinja Filter example",
  "description": "Dynamic jinja filter example",
  "version": "1.0",
  "params": [
    "colored_rectangle_fn",
    "rectangle_color"
  ]
}

We can now build the report:

$ zipreport build sample_report

== Building Report simple_report.zpt ==
Checking manifest & index file...
Building...
Copying manifest.json...
Copying index.html...
Generating simple_report.zpt...
Done!

Then, we start writing our main.py code. First, we create our image generation function. This function will generate a png colored rectangle using PIL, based on the specified color passed as argument (in this case, the value will come from the rectangle_color template variable), and returns a io.BytesIO buffer.

import io
from PIL import Image


def render_image(color='red') -> io.BytesIO:
    # generate a rectangle with the specified color
    img = Image.new('RGB', (256, 256), color=color)
    # save generated image to a memory buffer
    buffer = io.BytesIO()
    img.save(buffer, format='PNG')
    # rewind to the beginning of the buffer
    buffer.seek(0)
    return buffer

Lastly, we add some boilerplate to load, process and save the report. This example relies on zipreport-server for rendering (Some additional validations were omitted for readability):

if __name__ == "__main__":

    args = sys.argv[1:]
    pdf_name = Path(args[0])  # output file path

    report_name = "simple_report.zpt"
    report = ReportFileLoader.load(report_name)

    # template variables
    report_data = {
        # our callback function to generate the image
        'colored_rectangle_fn': render_image,
        # desired color to use
        'rectangle_color': 'pink',
    }

    # render using zipreport-cli processor
    result = ZipReport("https://127.0.0.1:6543", "somePassword").render_defaults(report, report_data)
    if not result.success:
        print("An error occured while generating the pdf:", result.error)
        exit(1)

    # save io.BytesIO buffer to file
    with open(pdf_name, 'wb') as f:
        f.write(result.report.read())

We can now run our example program:

$ python3 main.py result.pdf
Report generated to result.pdf
$

Expected result contents:

PDF generated from the example

Using placeholders for previewing purposes

Due to the dynamic nature of the dynamic image generation, it is not possible to preview the report correctly with zipreport debug. To work around this limitation, it is possible to specify a placeholder image (local or remote) instead of the callable parameter. The data source parameter, while mandatory, is ignored.

Following on the previous example, we can add a data.json to simple_report1 with a string url for an image instead of the callable function. This url can either be external (ex. a placeholder generation site) or local (a local image within the template structure):

data.json contents with a placeholder url:

{
  "colored_rectangle_fn": "https://placehold.co/400",
  "rectangle_color": ""
}

data.json contents with a local image:

{
  "colored_rectangle_fn": "/images/png_graphic.png",
  "rectangle_color": ""
}

Extend Jinja2 by using a custom Environment wrapper class

Starting with version 2.2.0, zipreport supports programmatic customization of the Environment() used by Jinja2 for template rendering, allowing injection or override of custom definitions such as filters, tests, etc.

To create a custom Environment() wrapper, create a class inheriting from zipreport.template.EnvironmentWrapper, and override the method wrap() to provide the required behaviour:

import markupsafe
from jinja2 import Environment, pass_environment
from zipreport.template import EnvironmentWrapper

@pass_environment
def defoxifier(*args, **kwargs) -> markupsafe.Markup:
    """
    Example filter that replaces "fox" with "racoon"
    """
    al = len(args)
    if al < 2:
        raise RuntimeError("Invalid number of arguments")

    text = args[1].replace("fox", "racoon")
    return markupsafe.Markup(text)


class MyCustomWrapper(EnvironmentWrapper):
    def wrap(self, e: Environment) -> Environment:
        # perform Environment customization here
        # in this case, register a custom filter
        e.filters["defoxifier"] = defoxifier

        # return Environment
        return e

The class can then be used to create the wrapper object, and passed to an existing processor:

# create custom wrapper object
my_wrapper = MyCustomWrapper()

# render using zipreport processor
processor = ZipReport("https://127.0.0.1:6543", "somePassword") 
result = processor.render_defaults(
    report, 
    report_data, 
    wrapper=my_wrapper # our custom Environment wrapper 
)

Page numbers, headers and footers

Page numbers, headers and footers can be generated automatically using paged.js. Please check paged.js documentation for detailed information on available formatting options and advanced usage.

To ensure correct PDF generation, specially on lengthy or complex documents, it is recommended to always enable the js signaling functionality.

Including paged.js on your report

The most convenient way of including paged.js is to use the CDN link in your html report page:

<!DOCTYPE html>
<html>
<head>
    <!-- PagedJS -->
    <script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"></script>
    <style type="text/css">
        <!--
        custom CSS rules goes here

        ;
        see below

        -->
    </style>
</head>
<body>
<!-- page content goes here -->
</body>
</html>

Adding page numbers

Extending on our previous example, we can now add page numbers to the bottom right corner of the page via css:

@page {
    /* page footer */
    @bottom-right {
        /* page numbers will be prefixed by string "page" */
        content: "page " counter(page);
    }
}

Configuring page breaks

Forcing page breaks on specific sections is also possible. In this example, we will add page breaks for

tags:

/* section tag triggers page break */
section {
    break-before: page;
}

Now each section block will trigger a page break on our report:

(...)
<body>
<section>
    <h1>this is page 1</h1>
</section>

<section>
    <h1>this is page 2</h1>
</section>
</body>

Adding a header

To add a header with the current chapter name, we map the heading tag (in this case, H1) to a variable, and then we extend our @page CSS definition to display that var on the top right corner of the page:

(
...

)

@page {
    /* display 'title' contents in the top right corner of the page, in uppercase */
    @top-right {
        content: string(title);
        text-transform: uppercase;
    }
(...)
}

Adding a static footer is similar to the approach used for page numbering. Just choose the desired page location and specify the desired content:

(
...

)
@page {
    /* display static text on the footer */
    @bottom-center {
        content: "sample footer"
    }
(...)
}

Complete example

This is how our final version of a two-page report, with page numbers in the bottom right corner, a page header with the current chapter name in uppercase, and a fixed footer with a sample text looks like:

<!DOCTYPE html>
<html>
<head>
    <!-- PagedJS -->
    <script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"></script>
    <style type="text/css">
        /* 'title' variable comes from h1 contents */
        h1 {
            string-set: title content(text);
        }

        /* section tag triggers page break */
        section {
            break-before: page;
        }

        @page {
            /* display 'title' contents in the top right corner of the page, in uppercase */
            @top-right {
                content: string(title);
                text-transform: uppercase;
            }

            /* display static text on the footer */
            @bottom-center {
                content: "sample footer"
            }

            /* page footer */
            @bottom-right {
                /* page numbers will be prefixed by string "page" */
                content: "page " counter(page);
            }
        }
    </style>
</head>
<body>
<section>
    <h1>this is page 1</h1>
</section>

<section>
    <h1>this is page 2</h1>
</section>
</body>
</html>

Advanced usage

Visit paged.js documentation for more details on available features. An advanced example, with a cover page, sections and a table of contents can be found in the available examples.

Generating table of contents (ToC)

ZipReport provides a simple example of automatic table of content generation with JavaScript and CSS in the examples section. It works by populating a div element with the desired index entries, and then use the CSS target-counter property to generate the page numbers.

Lets build an example reusing both the js and css example files (originally they were provided by paged.js, but are no longer available). Copy both files to your report folder, and rename them to toc.js and toc.css respectively:

$ wget https://github.com/zipreport/zipreport/tree/master/examples/pagedjs/toc_example_report/js/toc.js -O toc.js
$ wget https://github.com/zipreport/zipreport/tree/master/examples/pagedjs/toc_example_report/css/toc.css -O toc.css

We can then revisit and extend our previous example from page numbers:

(...)
<!-- PagedJS -->
<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"></script>
<!-- ToC script -->
<script src="toc.js"></script>
<link href="toc.css" rel="stylesheet" type="text/css">
(..)

And add the index page to the body, and add a css class 'toc-element' to mark desired toc entries:

<body>
<!-- INDEX PAGE -->
<section class="toc">
    <h1>Index</h1>
    <div id="toc"></div>
</section>
<section>
    <h1 class="toc-element">this is page 1</h1>
</section>

<section>
    <h1 class="toc-element">this is page 2</h1>
</section>
</body>

And finally, hook both the dynamic ToC generation and ZipReport js signaling to the paged.js event handlers, just before closing the body tag:

    (...)
<script>
    class handlers extends Paged.Handler {
        constructor(chunker, polisher, caller) {
            super(chunker, polisher, caller);
        }

        afterPreview(pages) {
            // signal zipreport PDF generation engine
            console.log('zpt-view-ready')
        }

        beforeParsed(content) {
            createToc({
                content: content,
                tocElement: '#toc', // our target element
                titleElements: ['.toc-element'] // CSS classes of elements to add to the index
            });
        }

    }

    Paged.registerHandlers(handlers);
</script>
</body>