@replytechnologies/reply-pdf

0.0.8 • Public • Published

ReplyPDF

BADGE_NPM_DOWNLOADS BADGE_NPM_DOWNLOADS BADGE_NPM_VERSION BADGE_NPM_LICENCE BADGE_PAYPAL

Process Infographic

Table of Contents

[TOC]

About

If you ever had to create a PDF document you would be familiar with the struggles to get headers, footers, and tables to layout correctly. This library aims to remove the complexity of creating PDF documents by providing purpose built components that handle rendering and data binding.

Interactive demo coming soon

Installation

Node

npm install @replytechnologies/reply-pdf

Usage

Import

CommonJS: Node.js <= 12

const ReplyPDF = require('@replytechnologies/reply-pdf');
const { ...components... } = ReplyPDF.components;
const { ...enums... } = ReplyPDF.enums;
const { ...models... } = ReplyPDF.models;

CommonJS: Node.js > 12

const ReplyPDF = require('@replytechnologies/reply-pdf');
const { ...components... } = require('@replytechnologies/reply-pdf/components');
const { ...enums... } = require('@replytechnologies/reply-pdf/enums');
const { ...models... } = require('@replytechnologies/reply-pdf/models');

EJS

import ReplyPDF from '@replytechnologies/reply-pdf';
import { ...components... } from '@replytechnologies/reply-pdf/components';
import { ...enums... } from '@replytechnologies/reply-pdf/enums';
import { ...models... } from '@replytechnologies/reply-pdf/models';

PDF Generation

The basic steps that should be taken to generate a PDF are outlined in the example below:

// create document template
const template = new Page({ ...options... });

// create binding data
const data = { ..binding data... };

// generate PDF
let doc = ReplyPDF.generateDocument({
    data: data,
    template: template
});

// handle document object (review PDFKit documentation for alternatives)
doc.pipe(fs.createWriteStream(outputFilePath));

// close document object
doc.close();

PDF usage

The output object returned from ReplyPDF.generateDocument({...}) is of type PDFDocument. For different methods of handling the returned object, refer to the PDFKit documentation found here.

Under the Hood

PDFKit: renders native elements to the PDF document.

SVG-To-PDFKit: renders SVG images to the PDF document.

Components

component_hierarchy

BaseComponent

BaseComponent registers properties and functions shared by all components

Hierarchy: BaseComponent

Property Type Description
binding string Property of the provided data object to use as data source for bindings
debug bool Indicates whether the component should render rectangles for outline and margin

BaseLayoutComponent

BaseLayoutComponent registers properties and functions shared by all components that have a physical layout on the document.

Hierarchy: BaseComponent / BaseLayoutComponent

Property Type Description
width number Width available to render content
height number Height available to render content
x number Horizontal content offset
y number Vertical content offset
margin Offset Content inset from provided width and height
border Border Border to draw around component
verticalAlignment Alignment Vertical alignment within parent component
horizontalAlignment Alignment Horizontal alignment within parent component
backgroundColor string Color of background fill
link string Link to navigate to on click

BaseContainerComponent

BaseContainerComponent registers properties and functions shared by all components that contain explicit child elements.

Hierarchy: BaseComponent / BaseLayoutComponent / BaseContainerComponent

Property Type Description
children [BaseComponent] Array of children to render as content

BaseStackComponent

BaseStackComponent registers properties and functions shared by all components that perform stacking of child components.

Hierarchy: BaseComponent / BaseLayoutComponent / BaseContainerComponent / BaseStackComponent

Property Type Description
spacing number Spacing between child components
layout Layout Layout mode of child components

BaseTextComponent

BaseTextComponent registers properties and functions shared by all text oriented components.

Hierarchy: BaseComponent / BaseLayoutComponent / BaseTextComponent

Property Type Description
text string Text to render as content
textAlignment TextAlignment Text alignment of content
fontSize number Font size of rendered text
fontFamily string Font family of rendered text
fontWeight FontWeight Font weight of rendered text
lineBreak bool Indicates whether text is allowed to span
multiple lines
ellipsis string Indicates the character(s) to append to the end of a string when lineBreak is set to false and the text tries to wrap into the next line

Page

Page should always be the base component of a template. All child components of a page are provided with the entire page content size to render within.

Hierarchy: BaseComponent / BaseLayoutComponent / BaseContainerComponent / Page

Property Type Description
header BaseComponent Component template to render as header
footer BaseComponent Component template to render as footer
firstPageHeader BaseComponent Component template to render as header for first page
size Size Defines the page size

Example

Go to usage example | Go to sample PDF

const template = new Page({
    size: PageSize.A4,
    margin: new Offset(25),
    header: new Container({
        height: 50,
        backgroundColor: 'sky',
        border: new Border(),
        children: [
            ...
        ],
    }),
    footer: new Container({
        height: 50,
        children: [
            ...
        ],
    }),
    children: [
        ...
    ],
});

component_example_page

Container

Container provides functionality to render child components according to the values of their respective verticalAlignment and horizontalAlignment values.

Hierarchy: BaseComponent / BaseLayoutComponent / BaseContainerComponent / Component

Example

Go to usage example | Go to sample PDF

const container = new Container({
    children: [
        new Text({
            text: 'Lorem ipsum ... anim id est laborum.',
            y: 50,
            width: 400,
            textAlignment: TextAlignment.center,
            horizontalAlignment: Alignment.middle,
            verticalAlignment: Alignment.start,
        }),
        new Text({
            text: 'top left',
            horizontalAlignment: Alignment.start,
            verticalAlignment: Alignment.start,
        }),
        new Text({
            text: 'top center',
            horizontalAlignment: Alignment.middle,
            verticalAlignment: Alignment.start,
        }),
        new Text({
            text: 'top right',
            horizontalAlignment: Alignment.end,
            verticalAlignment: Alignment.start,
        }),
    ],
}),

component_example_container

StackHorizontal

StackHorizontal provides functionality to layout its children horizontally. StackHorizontal is sized automatically according to the size of its children. StackHorizontal does not overflow children into a new row, and does not request a new page when content reaches the end of the page.

Hierarchy: BaseComponent / BaseLayoutComponent / BaseContainerComponent / BaseStackComponent / StackHorizontal

Example

Go to usage example | Go to sample PDF

new StackHorizontal({
    children: [
        new Text({ text: '1', width: 50 }),
        new Text({ text: '2' }),
        new Text({
            backgroundColor: 'orange',
            text: '3',
            margin: new Offset(15)
        }),
        new Text({ text: '4' }),
        new Text({ text: '5' }),
    ],
}),

component_example_stack_horizontal

StackVertical

StackVertical provides functionality to layout its children vertically. StackVertival is sized automatically according to the size of its children. StackVertical does not overflow children into a new column, and does not request a new page when content reaches the end of the page.

Hierarchy: BaseComponent / BaseLayoutComponent / BaseContainerComponent / BaseStackComponent / StackVertical

Example

Go to usage example | Go to sample PDF

new StackVertical({
    children: [
        new Text({ text: '1', height: 50 }),
        new Text({ text: '2' }),
        new Text({
            backgroundColor: 'pink',
            text: '3',
            margin: new Offset(15)
        }),
        new Text({ text: '4' }),
        new Text({ text: '5' }),
    ],
}),

component_example_stack_vertical

GridLayout

GridLayout provides functionality to layout components horizontally in a specified number of columns. When the number of columns is reached, components are added to a new row. When content reaches the end of the page, it will continue to the next page.

Hierarchy: BaseComponent / BaseLayoutComponent / BaseContainerComponent / GridLayout

Property Type Description
columns number Number of columns in which to display content

Example

Go to usage example | Go to sample PDF

const children = [];
for (var i = 0; i < 5; i++) {
    children.push(
        new Text({
            margin: new Offset(10),
            text: `Item: ${i}`,
            fontSize: 20
        }),
    );
}
...
new GridLayout({
    columns: 3,
    children: children
})

component_example_grid_layout

Text

Text provides functionality to render text to the document.

Hierarchy: BaseComponent / BaseLayoutComponent / BaseTextComponent / Text

Property Type Description
linkStyle object Style to apply to text if value of link is present
color string Text color
underline bool Indicates whether the text should be underlined
strikethrough bool Indicates whether the text should be striked
italic bool / number Indicates whether text should be rendered at an angle, uses a specific angle if italic is set to a number

Example

Go to usage example | Go to sample PDF

 new Text({
     margin: new Offset(10),
     fontWeight: FontWeight.bold,
     text: 'Go to Google',
     link: 'https://www.google.com'
 }),

component_example_text

Image

Image provides functionality to render an image from a file, string (SVG), buffer, or URL. Base64 strings need to be converted into a Buffer before they can be consumed by an Image component.

Hierarchy: BaseComponent / BaseLayoutComponent / Image

Property Type Description
source string | Buffer Image source to render
stretch bool Indicates whether the image should be stretched to the provided width and height

Example

Go to usage example | Go to sample PDF

const image = new Image({
    width: 100,
    height: 100,
    source: 'https://picsum.photos/300/300',
}),

component_example_image

RepeatVertical

RepeatVertical provides functionality to repeat a template for each value of a provided binding array. RepeatVertical requests new pages when its component generation functionality reaches the end of a page.

Hierarchy: BaseComponent / BaseLayoutComponent / RepeatVertical

Property Type Description
template BaseComponent Template to render

Example

Go to usage example | Go to sample PDF

new RepeatVertical({
    binding: 'arrayValues',
    template: new Container({
        margin: new Offset({
            top: 5,
        }),
        border: new Border(),
        children: [
            new Text({
                margin: new Offset(5),
                text: 'Array value: {{.}}',
            }),
        ],
    }),
})
...
const array = [];
for (let i = 0; i < 5; i++) {
    array.push(i);
}
...
const data = {
    arrayValues: array,
};

component_example_repeat_vertical

Table

Table provides functionality to render a table to the document. Table requests new pages when its component generation functionality reaches the end of a page. Components without specified widths are sized automatically according to available space.

Hierarchy: BaseComponent / BaseLayoutComponent / Table

Property Type Description
headerStyle Style Style to apply to header cells
cellStyle Style Style to apply to content cells
alternativeCellStyle Style Style override to apply to content cells of even rows
columns [Column] Defines table columns and data bindings

Example

Go to usage example | Go to sample PDF

new Table({
    binding: 'tableValues',
    border: new Border(new BorderSide({ thickness: 1 })),
    headerStyle: {
        fontWeight: FontWeight.bold,
        border: new Border(),
    },
    alternativeCellStyle: {
        backgroundColor: '#ccc',
    },
    columns: [
        {
            property: 'id',
            width: 50,
            text: '#',
        },
        {
            property: 'invoice_id',
            text: 'Invoice',
            fx: (index, record, value) => {
                return '#' + value.toString().padStart(6, '0');
            },
        },
        {
            property: 'date_created',
            width: 100,
            text: 'Date',
        },
        {
            property: 'amount',
            width: 70,
            cellStyle: {
                textAlignment: TextAlignment.right,
            },
            text: 'Amount',
            fx: (index, record, value) => {
                return value.toFixed(2);
            }
        }
    ]
})
...
const data = {
    tableValues: [
        { "id": 12, "invoice_id": 23, "date_created": "2021-04-09T05:54:03.000Z", "amount": 60 },
        ...
    ]
};

component_example_table

Enums

Alignment

Alignment.start
Alignment.end
Alignment.middle
Alignment.fill

FontWeight

FontWeight.bold
FontWeight.normal

Layout

Layout.none
Layout.spaceEvenly
Layout.sizeEvenly

TextAlignment

TextAlignment.left
TextAlignment.right
TextAlignment.center
TextAlignment.justify

PageSize

PageSize defines preset page dimensions to apply to the size property of a Page component.

A B C R & SRA Other
PageSize.A0 PageSize.B0 PageSize.C0 PageSize.RA0 PageSize.EXECUTIVE
PageSize.A1 PageSize.B1 PageSize.C1 PageSize.RA1 PageSize.LEGAL
PageSize.A2 PageSize.B2 PageSize.C2 PageSize.RA2 PageSize.LETTER
PageSize.A3 PageSize.B3 PageSize.C3 PageSize.RA3 PageSize.TABLOID
PageSize.A4 PageSize.B4 PageSize.C4 PageSize.RA4 PageSize.FOLIO
PageSize.A5 PageSize.B5 PageSize.C5 PageSize.SRA0 PageSize.2A0
PageSize.A6 PageSize.B6 PageSize.C6 PageSize.SRA1 PageSize.4A0
PageSize.A7 PageSize.B7 PageSize.C7 PageSize.SRA2
PageSize.A8 PageSize.B8 PageSize.C8 PageSize.SRA3
PageSize.A9 PageSize.B9 PageSize.C9 PageSize.SRA4
PageSize.A10 PageSize.B10 PageSize.C10

Models

Border

Border constructor requires an object containing the properties listed below. Alternatively the Border constructor can be supplied with a single parameter of type or schema BorderSide to apply to all sides of the border.

Property Type Description
left BorderSide Left border to render
top BorderSide Top border to render
right BorderSide Right border to render
bottom BorderSide Bottom border to render

BorderSide

Property Type Description
thickness number Thickness of the border side
color string Color of the border side

Column

Property Type Description
property string Property of table record to display
text string Heading value to display
width number Width of column
A value of 1 is used if omitted
Values <= 1 are used to determine fractional size of column
headerStyle Style Style to apply to header cell of the column
cellStyle Style Style to apply to data cells of the column
alternativeCellStyle Style Style to apply to data cells of even rows of the column
fx function Preprocessor function to run before data is displayed
(index, record, value) => alteredValue

Offset

Offset constructor requires an object containing the properties listed below. Alternatively the Offset constructor can be supplied with a single parameter of type number to apply to all sides of the offset.

Property Type Description
left number Left offset to apply
top number Top offset to apply
right number Right offset to apply
bottom number Bottom offset to apply

Size

Property Type Description
width number Width value
height number Height value

Style

Property Type Description
border Border Border value
backgroundColor string Background color value
color string Foreground color value
fontSize number Font size value
fontWeight FontWeight Font weight value
textAlignment TextAlignment Horizontal text alignment value

Binding

Components can bind to a specific property in a data source and provide that binding to its content. String interpolation can be performed by enclosing the binding string in curly brace sets ({{binding_string}}).

For the purposes of binding page information to the header and footer components, the data source should not contain properties named pageNumber or pageCount.

An example of binding can be found here.

new Container({
    binding: 'tier1',
    children: [
        new StackVertical({
            binding: 'tier2',
            children: [
                new Text({
                    binding: 'tier3.tier4',
                    margin: new Offset(10),
                    text: 'date = {{tier5.date}}',
                })
            ]
        })
    ]
})
...
const data = {
    tier1: {
        tier2: {
            tier3: {
                tier4: {
                    tier5: {
	                    date: new Date(),
                    }
                }
            }
        }
    }
};

component_example_binding

Header & Footer

Header templates should set in the firstPageHeader and header properties of a Page component. If firstPageHeader has a value, its template will be rendered on the first page of the document, subsequent pages will use header if present. The header template must have an explicit height value set.

The footer template should be set in the footer property of a Page component. The footer template must have an explicit height value set.

Components within the header and footer templates can bind to the pageNumber and pageCount properties of the data source to display the current page information.

...
footer: new Container({
    height: 50,
    children: [
        new Text({
            text: 'Page {{pageNumber}} of {{pageCount}}',
        }),
    ],
}),
...

example_footer_page_number

An example of a header and footer can be found here (PDF).

Multiple Templates

Rendering multiple templates to a single document can be achieved by defining multiple Page templates, and passing the resulting doc as a parameter to the next rendering. Only the last template should define header and footer content, preceding templates should contain empty components which define only the height property. All headers and footers should share the same respective height values. As an alternative to creating empty header and footer components, the preceding pages may specify a larger margin to consume the space.

const template1 = new Page({
    size: PageSize.A4,
    margin: new Offset(50),
    header: new Container({
        height: 50,
    }),
    footer: new Container({
        height: 50,
    }),
    children: [
        ...
    ],
});
const template2 = new Page({
    size: PageSize.A4,
    margin: new Offset(50),
    header: new Container({
        height: 50,
        children: [
            ...
        ],
    }),
    footer: new Container({
        height: 50,
        children: [
            ...
        ],
    }),
    children: [
        ...
    ],
});
...
let doc = ReplyPDF.generateDocument({
    data: data1,
    template: template1,
});

doc = ReplyPDF.generateDocument({
    doc: doc, // provide existing document object
    data: data2,
    template: template2,
});

An example of using multiple templates can be found here (PDF).

Custom Fonts

Custom fonts can be embedded into the PDF document by supplying font names with accompanying buffers containing the content of the font. The buffers can be loaded from a file or from a URL by making a HTTP request. For the fontWeight property to function correctly, the bold version of a font should be named after the regular version + '-Bold'.

Font buffers should be added to the generateDocument function parameter as shown in the example below:

let doc = ReplyPDF.generateDocument({
    fonts: {
        'Robinette': fs.readFileSync('examples/fonts/Robinette.ttf'),
        'GrandAutoDemo': fs.readFileSync('examples/fonts/GrandAutoDemoRegular.ttf'),
        'PlayfairDisplay': fs.readFileSync('examples/fonts/PlayfairDisplay-Regular.ttf'),
        'PlayfairDisplay-Bold': fs.readFileSync('examples/fonts/PlayfairDisplay-Bold.ttf'),
    },
    template: template,
    debug: false,
});

An example of using custom fonts can be found here.

Charts

Charts can be included into the PDF document by including the chart as an image. Chart images can be generated by using some of the chart-to-image services listed below:

An example of including a chart into a PDF can be found here ( PDF).

Debugging

Debugging can be enabled on a document or component level. When debugging is enabled, a green border is drawn around the component outline, and a red border around the component margin.

debugging_example

When enabling debugging on component level, an annotation is added to the document describing the component and layout information.

debugging_annotations

Enabling debugging on document level:

let doc = ReplyPDF.generateDocument({
    ...
    debug: true,
    ...
});

Enabling debugging on component level:

var component = new ComponentTypeGoesHere({
    ...
    debug: true,
    ...
});

Tests

The file test.js contains code to run all the examples in this project. Each example generates a PDF file as output into the examples/outputs directory. To execute the tests, run the following command after cloning:

npm test

Pitfalls

Components that do not request new pages when content tries to overflow will continue to render content on the same page. The content of these components should ideally be assigned fixed sizes to prevent rendering content off of the page or content that overflows into the footer section.

Image rendering from URL is performed by making synchronous http calls. You may experience delays when building a document with a large number of URL based images.

Roadmap

General Enhancements

Improve multiple template rendering process to be able to render different headers and footers for each template.

Implement functionality to use existing PDF documents as the base to render the template onto. As this functionality is not supported by PDFKit, the merge functionality may be delegated to Hummus or HummusRecipe.

New Components

Barcode: Render a barcode to the document

PageBreak: Continue content rendering on a new page

Component Enhancements

Component Enhancements
Table - Render column content based on a template, currently renders all cells as text
- Configuration to prevent reprinting of header on new pages, currently headers are repeated on each new page
Image - Restructure image loading process to enable asynchronous calls to read data from a URL

Package Sidebar

Install

npm i @replytechnologies/reply-pdf

Weekly Downloads

2

Version

0.0.8

License

MIT

Unpacked Size

310 kB

Total Files

51

Last publish

Collaborators

  • danieleeuwner