UNPKG

declarative-pdf

Version:

A tool for generating PDF documents from declarative HTML templates

409 lines (397 loc) 11.5 kB
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 };