Product Discussion

 View Only

Quick Start: HTML Templates for Velocity PDF Reports

By Robin Calhoun posted 07-13-2020 07:50

  

Information Updated June 2022


We are now using a library from iText to produce PDF exports and reports from Velocity templates. The new implementation simplifies the templates in many ways. There is no need to use custom header/footer tags, and the new library interprets HTML style parameters correctly such that all the formatting, margin control, page decorations, etc. can be specified in plain HTML and CSS styles. 

This is a quick-start document explaining the basics of writing HTML templates for Velocity PDF output using the new iText library.

Basic HTML Template

A basic template is plain HTML with a <body> section. Opening the HTML file in a browser is the fastest, easiest way to check the appearance and layout (keeping in mind that any embedded Velocity code will not render, and there will be no page breaks, nor will the headers/footers render if they are included). In order to see all the PDF features, the report must be uploaded and run in Connect. Remember that when these HTML templates are uploaded to Connect, they must have a .VM file extension.

Here is a simple template for reporting on all the item names in the current project:

<html>
<head>
    <title>All Item Names</title> 
</head>

<body>
    Project: $project.name
    <ul>
        #foreach( $itemId in $documentSource.getActiveDocumentIdsInProject($project.id) )
           #set ($item = $documentSource.getDocument($itemId))
           <li>$item.name</li>
        #end
    </ul>
</body>
</html>



Styling

CSS styles are applied as usual. These styles should be honored in the PDF. 

This template is the same item name report above, but with style: adding margins, setting the background color, specifying the font, and prepending each item name with a green check mark. 

<html>
<head>
<style>        
  .item-list li {
      position: relative;
      list-style-type: none;
      padding-left: 2.5rem;
      margin-bottom: 0.5rem;
  }
  .item-list li:before {
     content: '';
     display: block;
     position: absolute;
     left: 0;
     top: -2px;
     width: 5px;
     height: 11px;
     border-width: 0 2px 2px 0;
     border-style: solid;
     border-color: #00a8a8;
     transform-origin: bottom left;
     transform: rotate(45deg);
   } 
   html {
      -webkit-font-smoothing: antialiased;
      font-family: "Helvetica Neue", sans-serif;
      font-size: 62.5%;
   }    
   body {
      width: 790px;
      font-size: 1.6rem; /* 18px */
      background-color: #efefef;
      color: #324047
   }
   html, body, section {
      height: 100%;
   }
   section {
      max-width: 400px;
      margin-left: 10px;
      margin-right: 20px;
      display: flex;
      align-items: center;
   }
   div {
      margin: auto;
   }
</style>
    <title>All Item Names</title>
</head>

<body>
<section>
    <div>
        <h2>Items for Project $project.name</h2>
        <ul class="item-list">
            #foreach( $itemId in
                $documentSource.getActiveDocumentIdsInProject($project.id)
            )
                #set ($item =
                    $documentSource.getDocument($itemId))
                <li>$item.name</li>
            #end
        </ul>
    </div>
</section>
</body>
</html>

Setting Paged Media Attributes

The CSS rule @page allows report writers to specify how pages are laid out. Details like page margins, page size and orientation, headers, footers, page numbering, and more can be defined within this rule. 

As an example, adding these @page rules to the style section sets the page size to 8.5 by 11 inches, all margins to 2cm, adds a top margin of 10cm on the first page only, and ensures page breaks never occur immediately after an <h2> element.

<style>
    
    @page {
        size: 8.5in 11in;
        margin: 2cm;
    }

    @page :first {
        margin-top: 10cm;
    }

    h2 { page-break-after : avoid }

</style>

Reports can be generated with a landscape orientation by including the landscape keyword when specifying the page dimensions:

<style>
        @page {
            size: A4 landscape;

            @bottom-right {
                font-family: Arial, Helvetica, sans-serif;
                font-size: 10.0pt;
                content: "Page " counter(page) " of " counter(pages);
            }
        }
</style>

For a full description of the page formatting available, see any of several online resources.



Adding Headers and/or Footers

The @page rule can be used to include custom headers and footers on every page, or just on selected pages. The process is:
  1. Define an element in the HTML body section with a class name uniquely identifying it as a header or footer. This element defines the look of the header/footer.
  2. Include the CSS for the header/footer class in the style section. This style definition must include the position: running(...) attribute.
  3. In the @page rule block, include positioning information for the header/footer.

Here is an implementation example for adding a simple header and footer:

1. Define header/footer HTML elements

In this simple example, these are just text elements that appear on the page. A more complex example is in the next section.

<div class='header'>An Awesome Report</div>
<div class='footer'>Copyright 2021, Jama Software</div>

The divs defining the header/footer should be at the top of the <body> section, appearing immediately after the <body> tag. The class names do not have to be 'header' or 'footer', but do need to be unique for the header/footer elements.

2. Include CSS for header/footer

div.header {
   display: block; text-align: center;
   position: running(header);
}

div.footer {
   font-family: Courier, sans-serif;
   font-size: 10.0pt;
   display: block; text-align: center;
   position: running(footer);
}

Styles for the header/footer HTML elements are defined here. The important elements in these styles are the position properties. They must be set to running with the class name for the header/footer specified. running indicates to the layout system that the class is removed from the normal page layout flow and is available to be referenced in the @page block.

3. Define the header/footer in the @page block

In the @page rule block described above, set the positioning for the header/footer and declare the content that will appear in this position. There are 16 possible position attributes, but the ones most likely to be used for headers/footers are: @top-left, @top-center, @top-right, @bottom-left, @bottom-center, @bottom-right. The content: element attributes must reference the class name for the header/footer.

@page {
   size: A4;
   @top-center { content: element(header) }
   @bottom-center { content: element(footer) }
}

Below is a more complex footer that includes an image, dynamic content, and page info. It follows the same pattern, but includes a couple of special techniques.

This HTML describes a footer that includes an image, static text, dynamic data fetched from Velocity, and paging info:

<div class="footer">
    <table class="footer-table">
        <tr>
            <td rowspan="2">
                <img height="30" width="30" src="https://www.jamasoftware.com/media/gravatar/jama-avatar.png"
                     alt="Jama Logo">
            </td>
            <td>
                Confidential: Jama Software
            </td>
            <td rowspan="2">
                Page <span id="pageNumber"></span> of <span id="totalPages"></span>
            </td>
        </tr>
        <tr>
            <td>Report generated on: $dateTool.getSystemDate()<br>By: $userSource.currentUser.fullName</td>
        </tr>
    </table>
</div>

When rendered in the report, it looks like this:


The only unusual element here is the display of the page numbers. Two components are defined in the <style> section and used on line 12: pageNumber and totalPages.

The page numbering components are defined in the <style> section like this:

#pageNumber::after {
   content: counter(page)
}

#totalPages::after {
   content: counter(pages)
}

This code renders the default page counter after any HTML element with the id pageNumber and renders the total page count after any HTML element with the totalPages id. counter(page) and counter(pages) are page counters that are built into CSS.


Images
Images in Velocity PDFs come from two sources:

    • Images that are part of an item that is stored in Jama Connect, such as WIRIS equations or images uploaded in Rich Text
    • External images

    External images are included using the <img> tag. The src property must refer to a web server or file system that is accessible by Jama Connect when the report is rendered.

    NOTE: Due to an issue in iText, SVG images are automatically converted to the PNG file format.



    Controlling Page Size and Margins

    Page size is specified in the @page rule block as seen above using the size: element. There are several preset values available, but custom widths and heights may also be set.

    Margins are controlled using CSS values as usual with HTML pages. For example, this CSS will narrow the margins for a text-only section of the report:

    .intro-text {
       margin-left: 100px;
       margin-right:200px;
    }

    Embedding Fonts

    Fonts other than the default Connect system fonts may be included by embedding them in the HTML template. Below is an example of embedding and using a font in the <style> section of the HTML template. As with external images, the URL for the font must point to a location that is accessible by Connect when the report is run.

    @font-face {
       font-family: 'rakkasregular';
       src: url('/Users/krichards/Desktop/Rakkas/Rakkas-Regular.ttf') format('ttf'),
       url('/Users/krichards/Desktop/Rakkas/Rakkas-Regular.ttf') format('ttf');
       font-weight: normal;
       font-style: normal;
    }
    
    .intro-text {
       margin-left: 100px;
       margin-right:200px;
       font-family: 'rakkasregular';
    }
    ​​​​​​​

    All the techniques discussed in this document are in this complete Velocity template:

    <html>
    <head>
        <style>
    
            @page {
                size: A4;
                @top-center {
                    content: element(header)
                }
                @bottom-center {
                    content: element(footer)
                }
            }
    
            #pageNumber::after {
                content: counter(page)
            }
    
            #totalPages::after {
                content: counter(pages)
            }
    
    
            div.header {
                display: block;
                text-align: center;
                position: running(header);
            }
    
            div.footer {
                font-family: Arial, Helvetica, sans-serif;
                font-size: 8.0pt;
                display: block;
                text-align: center;
                position: running(footer);
            }
    
            .item-list li {
                position: relative;
                list-style-type: none;
                padding-left: 2.5rem;
                margin-bottom: 0.5rem;
            }
    
            .item-list li:before {
                content: '';
                display: block;
                position: absolute;
                left: 0;
                top: -2px;
                width: 5px;
                height: 11px;
                border-width: 0 2px 2px 0;
                border-style: solid;
                border-color: #00a8a8;
                transform-origin: bottom left;
                transform: rotate(45deg);
            }
    
            html {
                -webkit-font-smoothing: antialiased;
                font-family: "Helvetica Neue", sans-serif;
                font-size: 62.5%;
            }
    
            body {
                width: 790px;
                font-size: 1.6rem; /* 18px */
                background-color: #efefef;
                color: #324047
            }
    
            html,
            body,
            section {
                height: 100%;
            }
    
            section {
                max-width: 400px;
                margin-left: 10px;
                margin-right: 20px;
                display: flex;
                align-items: center;
            }
    
            div {
                margin: auto;
            }
    
            .footer-table  {
                width: 100%;
                height: 100px;
                vertical-align: middle;
                horiz-align: center;
                margin-bottom: 5px;
            }
    
            @font-face {
                font-family: 'rakkasregular';
                src: url('/Users/krichards/Desktop/Rakkas/Rakkas-Regular.ttf') format('ttf'),
                url('/Users/krichards/Desktop/Rakkas/Rakkas-Regular.ttf') format('ttf');
                font-weight: normal;
                font-style: normal;
            }
    
            .report-info {
                margin-left: 150px;
                margin-right: 100px;
                font-family: 'rakkasregular';
                background-color: white;
                page-break-after: always;
            }
    
        </style>
    
        <title>All Item Names</title>
    </head>
    
    <body>
    <div class="header">All Item Names in the Project</div>
    <div class="footer">
        <table class="footer-table">
            <tr>
                <td rowspan="2">
                    <img height="30" width="30" src="https://www.jamasoftware.com/media/gravatar/jama-avatar.png"
                         alt="Jama Logo">
                </td>
                <td>
                    Confidential: Jama Software
                </td>
                <td rowspan="2">
                    Page <span id="pageNumber"></span> of <span id="totalPages"></span>
                </td>
            </tr>
            <tr>
                <td>Report generated on: $dateTool.getSystemDate()<br>By: $userSource.currentUser.fullName</td>
            </tr>
        </table>
    </div>
    <h2>Summary of Report</h2>
    <div class="report-info">
        <p>Text goes here</p>
    </div>
    <section>
        <div>
            <h2>Items for Project $project.name</h2>
            <ul class="item-list">
                #foreach( $itemId in
                    $documentSource.getActiveDocumentIdsInProject($project.id)
                )
                    #set ($item =
                        $documentSource.getDocument($itemId))
                    <li>$item.name</li>
                #end
            </ul>
        </div>
    </section>
    </body>
    </html>
    0 comments
    237 views