declarative-pdf
Version:
A tool for generating PDF documents from declarative HTML templates
409 lines (397 loc) • 11.5 kB
TypeScript
import { Browser, Page } from 'puppeteer';
import { PDFDocument, PDFPage, PDFEmbeddedPage } from 'pdf-lib';
type TPhysicalPageVariant = 'first' | 'last' | 'even' | 'odd' | 'default';
interface SectionSetting {
height: number;
physicalPageIndex?: number;
physicalPageType?: TPhysicalPageVariant;
hasCurrentPageNumber: boolean;
hasTotalPagesNumber: boolean;
}
interface SectionSettings {
headers: SectionSetting[];
footers: SectionSetting[];
backgrounds: SectionSetting[];
}
type PrepareSection = {
documentPageIndex: number;
sectionType?: 'background' | 'header' | 'footer';
physicalPageIndex?: number;
currentPageNumber?: number;
totalPagesNumber?: number;
};
type AnyFunction = (...args: any[]) => any;
type MinimumBrowser = {
newPage: AnyFunction;
isConnected?: AnyFunction;
connected?: boolean;
};
type MinimumPage = {
setContent: AnyFunction;
setViewport: AnyFunction;
evaluate: AnyFunction;
pdf: AnyFunction;
close: AnyFunction;
isClosed: AnyFunction;
};
declare class HTMLAdapter {
private _browser?;
private _page?;
constructor(browser: MinimumBrowser);
get browser(): Browser;
get page(): Page;
newPage(): Promise<void>;
setPage(page: MinimumPage): void;
releasePage(): void;
setContent(content: string): Promise<void>;
setViewport(opts: {
width: number;
height: number;
}): Promise<void>;
normalize(opts?: NormalizeOptions): Promise<void>;
getTemplateSettings(opts: {
width: number;
height: number;
ppi: number;
}): Promise<{
index: number;
width: number;
height: number;
bodyMarginTop: number;
bodyMarginBottom: number;
hasSections: boolean;
}[]>;
getSectionSettings(opts: {
index: number;
}): Promise<SectionSettings>;
prepareSection(opts: PrepareSection): Promise<boolean>;
resetVisibility(): Promise<void>;
/**
* There is some bug in the PDF generation process, where the height and
* the width of the resulting PDF page get smaller by approximate factor
* of 0.75. During this process, some rounding issues occur and sometimes,
* we end up with 2 pages instead of 1. Also, backgrounds sometimes get
* a narrow white line at the bottom.
*
* To mitigate this, we scale up the width and height by 0.75, as well as
* the scale, to keep the same appearance.
*/
pdf(opts: {
width: number;
height: number;
margin?: {
top?: number;
right?: number;
bottom?: number;
left?: number;
};
transparentBg?: boolean;
}): Promise<Uint8Array>;
close(): Promise<void>;
}
interface BodyElementOpts {
buffer: Buffer;
pdf: PDFDocument;
layout: {
width: number;
height: number;
x: number;
y: number;
};
}
declare class BodyElement {
buffer: Buffer;
pdf: PDFDocument;
layout: BodyElementOpts['layout'];
constructor(opts: BodyElementOpts);
get x(): number;
get y(): number;
get width(): number;
get height(): number;
embedPageIdx(targetPage: PDFPage, idx: number): Promise<PDFEmbeddedPage[]>;
}
type SectionType = 'header' | 'footer' | 'background';
interface SectionElementOpts {
buffer: Buffer;
pdf: PDFDocument;
debug: {
type: SectionType;
pageNumber: number;
};
setting: SectionSetting;
layout: {
width: number;
height: number;
x: number;
y: number;
};
}
declare class SectionElement {
buffer: Buffer;
pdf: PDFDocument;
setting: SectionSetting;
layout: SectionElementOpts['layout'];
debug: SectionElementOpts['debug'];
private _name;
private embeddedPage?;
constructor(opts: SectionElementOpts);
get x(): number;
get y(): number;
get width(): number;
get height(): number;
get name(): string;
embedPage(targetPage: PDFPage): Promise<PDFEmbeddedPage>;
}
declare class TimeLogger {
private _nodes;
private _report;
constructor();
private setupNode;
private handleNode;
private startNode;
private endNode;
session(): {
start: (name: string) => void;
end: () => void;
};
level1(): {
start: (name: string) => void;
end: () => void;
};
level2(): {
start: (name: string) => void;
end: () => void;
};
level3(): {
start: (name: string) => void;
end: () => void;
};
private resetTimeObject;
private generateReport;
get report(): string;
}
interface BodyLayout {
width: number;
height: number;
x: number;
y: number;
transparentBg: boolean;
}
interface SectionLayout extends BodyLayout {
hasPageNumbers: boolean;
settings: SectionSetting[];
}
interface PageLayout {
height: number;
width: number;
hasPageNumbers: boolean;
hasAnySection: boolean;
pageCount: number;
body: BodyLayout;
header?: SectionLayout;
footer?: SectionLayout;
background?: SectionLayout;
}
type DocumentPageOpts = {
parent: DeclarativePDF;
/** index of document-page element in DOM */
index: number;
/** whole page width in pixels */
width: number;
/** whole page height in pixels */
height: number;
/** top margin of the page-body element */
bodyMarginTop: number;
/** bottom margin of the page-body element */
bodyMarginBottom: number;
/** do we have any sections other than page-body */
hasSections: boolean;
};
declare class DocumentPage {
parent: DeclarativePDF;
height: number;
width: number;
index: number;
bodyMarginTop: number;
bodyMarginBottom: number;
hasSections: boolean;
/** These two will exist after createLayoutAndBody() method */
layout?: PageLayout;
body?: BodyElement;
sectionElements: SectionElement[];
constructor(opts: DocumentPageOpts);
get viewPort(): {
width: number;
height: number;
};
get html(): HTMLAdapter;
/**
* Create the layout and body element.
*
* This method will figure out heights and positions of
* all existing elements on this page and create a body
* element which will, in turn, give us a total page
* count for this document page.
*
* We need to know the number of pages to be able to
* construct other elements that might need to display
* current page / total page number.
*/
createLayoutAndBody(sectionSettings?: SectionSettings, logger?: TimeLogger): Promise<void>;
get previousDocumentPages(): DocumentPage[];
get pageCountOffset(): number;
}
declare const PAPER_SIZE: {
a0: {
width: number;
height: number;
};
a1: {
width: number;
height: number;
};
a2: {
width: number;
height: number;
};
a3: {
width: number;
height: number;
};
a4: {
width: number;
height: number;
};
a5: {
width: number;
height: number;
};
a6: {
width: number;
height: number;
};
letter: {
width: number;
height: number;
};
legal: {
width: number;
height: number;
};
tabloid: {
width: number;
height: number;
};
ledger: {
width: number;
height: number;
};
};
interface PaperOpts {
ppi?: number;
format?: keyof typeof PAPER_SIZE;
width?: number;
height?: number;
}
declare class PaperDefaults {
readonly ppi: number;
readonly format: keyof typeof PAPER_SIZE | undefined;
readonly width: number;
readonly height: number;
constructor(opts?: PaperOpts);
}
interface DebugOptions {
/** Do we want to log debug information */
timeLog?: boolean;
/** A name to use in header of time log report */
pdfName?: string;
/** Do we want to attach generated segments to the PDF */
attachSegments?: boolean;
}
interface NormalizeOptions {
/** Add 'pdf' to document body classList (default: true) */
addPdfClass?: boolean;
/** Set document body margin to 0 (default: true) */
setBodyMargin?: boolean;
/** Set document body padding to 0 (default: true) */
setBodyPadding?: boolean;
/** Set document body background to transparent (default: true) */
setBodyTransparent?: boolean;
/** Remove any body child that is not 'document-page', 'script' or 'style' (default: true) */
normalizeBody?: boolean;
/** Remove any document-page child that is not 'document-page', 'script' or 'style' (default: true) */
normalizeDocumentPage?: boolean;
}
interface DocumentMeta {
title: string;
author: string;
subject: string;
keywords: string[];
producer: string;
creator: string;
creationDate: Date;
modificationDate: Date;
}
interface DocumentOptions {
/** If exists, will be used to set available metadata fields on the pdf document */
meta?: Partial<DocumentMeta>;
/**
* Controls the minimum space the body section must occupy on each page.
* Value is a decimal factor of the total page height (0.0 to 1.0).
* - Default: 1/3 (a third of the page)
* - Example: 0.25 means body must be at least 25% of page height
*/
bodyHeightMinimumFactor?: number;
}
interface DeclarativePDFOpts {
/** Should we normalize the content (remove excess elements, set some defaults...) */
normalize?: NormalizeOptions;
/** Override for paper defaults (A4 / 72ppi) */
defaults?: PaperOpts;
/** Debug options (attaches parts, logs timings) */
debug?: DebugOptions;
/** Override for pdf document metadata and rules */
document?: DocumentOptions;
}
declare class DeclarativePDF {
html: HTMLAdapter;
defaults: PaperDefaults;
normalize?: NormalizeOptions;
debug: DebugOptions;
documentOptions?: DocumentOptions;
documentPages: DocumentPage[];
/**
*
* @param browser A puupeteer browser instance, prepared for use
* @param opts Various options for the PDF generator
*/
constructor(browser: MinimumBrowser, opts?: DeclarativePDFOpts);
get totalPagesNumber(): number;
/**
* Generates a pdf buffer from string containing html template.
*
* When calling this method, it is expected that:
* - the browser is initialized and ready
* - the template is a valid HTML document -OR- a valid Page instance
*/
generate(input: string | MinimumPage): Promise<Buffer>;
/**
* Creates the document page models.
*
* This method will evaluate the template settings and create a new
* document page model for each setting parsed from the HTML template.
*/
private getDocumentPageSettings;
/**
* Initializes the document page models.
*
* For every created document page model, this method sets desired
* viewport and gets section settings from the DOM to create layout
* (heights and positions). It then convert to pdf the body element
* from which we get the number of pages and finally have all the
* information needed to build the final PDF.
*/
private buildLayoutForEachDocumentPage;
private buildPDF;
}
export { type DocumentMeta, type DocumentOptions, type NormalizeOptions, DeclarativePDF as default };