npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ekwo-ai/invoice-pdf

v0.24.0

Published

The visual PDF of an invoice or a credit note, in TypeScript, from the rows a set of books publishes: A4 or Letter, multi-page, any language Intl knows, Noto Sans embedded. Ready to carry a Factur-X XML.

Readme

@ekwo-ai/invoice-pdf

The visual PDF of a sale invoice or credit note, in TypeScript, from the rows a set of books publishes.

  • One sober layout, A4 or Letter: the seller (its logo and its name), the title, number, dates and references; the buyer and, where the goods went elsewhere, the delivery address, on shaded panels; the lines, with the column headings repeated on every page they run onto; the tax summary, one row per tax, with the sentence that says why a group charges nothing; the totals ending on what is still due, and beside them how to pay (terms, due date, IBAN, BIC, reference) in a frame; the note; the legal mentions; and on every page the seller's identity and the page number.
  • A small theme: an accent colour, a typeface (Noto Sans or the caller's), the logo on the left or the right — see Theme.
  • Nothing country-specific in the layout. The words it prints of its own are labels, English by default and the caller's in any other language; the sentences a country requires come in as rows; amounts, dates, rates and country names are written by Intl in the document's language, with the currency's own minor units.
  • Any script Noto Sans has, embedded: Latin, Greek, Cyrillic, Thai; and any other through a font the caller adds. Right to left is refused by name, never drawn reversed.
  • PDF/A-3b, measured. veraPDF finds no failed rule of PDF/A-3b in the PDF it renders, nor in the Factur-X that embedFacturX of @ekwo-ai/factur-x makes of it — see PDF/A-3 and Factur-X.
  • Pure TypeScript. Runs in Node, Deno and a browser. Reads no database, fetches nothing, writes no file: rows and bytes in, bytes out.

Open examples/invoice-english.pdf, examples/invoice-greek.pdf and examples/invoice-english-factur-x.pdf to see it; every name and number in them is invented. The Greek one passes only some of the words in Greek, on purpose: the rest falls back to English, which is what a caller who passes an incomplete set gets.

Install

npm install @ekwo-ai/invoice-pdf

Usage

import { renderInvoicePdf } from '@ekwo-ai/invoice-pdf';

const { file, filename, pageCount } = await renderInvoicePdf(
  {
    header,               // one row of document_header
    lines,                // the rows of document_line_items, in their order
    taxes,                // the rows of document_tax_summary
    mentions,             // the rows of document_legal_mentions
    logo: logoBytes,      // PNG or JPEG bytes, or nothing
  },
  {
    pageSize: 'A4',       // or 'Letter'
    labels: { invoice: 'Factura', amountDue: 'Importe pendiente' },  // the rest stays English
    theme: { accent: '#7A1F2B', logoPosition: 'right' },              // or nothing: DEFAULT_THEME
  },
);
// file: Uint8Array of a PDF; filename: 'invoice-INV-2026-0042.pdf'

The input

What the books already publish, and nothing else: one field per column of four views of Ekwo OS, declared in src/types.ts so that nothing is imported from it. Any invoicing system that produces the same rows can use this package. A column that is null is not printed; nothing is filled in for it.

| Field | View | What is printed from it | |---|---|---| | header | document_header | The seller (seller_*: name, legal name and form, address, VAT and registration numbers, e-mail, phone, website, electronic address, share capital), the buyer (buyer_*), the delivery address (delivery_*), the number, the dates (document_date, due_date, delivery_date, tax_point_date), the references, the totals (amount_untaxed, amount_tax, amount_total, amount_paid), what is still due (amount_residual), the payment (payment_terms, payee_iban, payee_bic, payment_reference), the note, the currency_code and the language. doc_type and state choose the title. | | lines | document_line_items | item_name, seller_item_identifier, item_description, quantity and unit_code, unit_price (marked * where unit_price_includes_tax), discount_percent, vat_rate, amount_untaxed. A section line is a heading across the table, a note line a sentence across it. | | taxes | document_tax_summary | tax_name, tax_rate, base_amount, tax_charged — what the customer pays — and exemption_reason under its group. Not legal_reference, written for whoever reviews a pack and not for a customer. | | mentions | document_legal_mentions | text, already in the document's language and valid on its date, in the order of sequence. | | logo | — | The bytes of the logo, PNG or JPEG. A PNG's transparency is flattened on white. document_header.seller_logo_url is never fetched here: the caller fetches it, or not. | | verification | — | The tax authority's verification of the invoice, where it has one: the QR code of url (ISO/IEC 18004, bytes in UTF-8, at errorCorrection, M by default, sizeMm wide, 30 by default, with a quiet zone of at least four modules or quietZoneMm), and beside it a heading, fields — each a label and a value, a datetime written in UTC — and notes. A heading or a label that is a key of the labels is printed in the caller's words; anything else is printed as it is, which is how a country's prescribed words arrive. |

Figures arrive as the decimal text the database holds and are handed to Intl.NumberFormat as text, which writes them digit for digit — never through a JavaScript number. Nothing is computed: every total printed is a column.

Words and languages

ENGLISH_LABELS holds every word the layout prints of its own — "Invoice", "Credit note", "Due date", "Amount due", "Page {page} of {pages}"… — and units maps UN/ECE Recommendation 20 codes to what is printed after a quantity (DAY → days, C62 → nothing). English is the only language this package carries. A document in another language gets its words from the caller, options.labels, whole or in part; what is left out is English. A country's own words — what its tax is called, what the law wants printed — are not labels: they are tax_name and the mentions, which the books give in the document's language.

The language amounts, dates and country names are written in is options.locale, else the document's language, else en. A tag Intl does not accept is refused (invalid_language).

Fonts

| Font | Covers | Licence | |---|---|---| | Noto Sans, Regular and Bold | Latin (with its extensions and Vietnamese), Greek, Cyrillic | SIL Open Font License 1.1 — licenses/OFL-NotoSans.txt | | Noto Sans Thai, Regular and Bold | Thai | SIL Open Font License 1.1 — licenses/OFL-NotoSansThai.txt | | Inter 4.1, Regular and Bold — typeface elegant | Latin, Greek, Cyrillic | SIL Open Font License 1.1 — licenses/OFL-Inter.txt | | Fraunces, SemiBold — typeface elegant, headings | Latin | SIL Open Font License 1.1 — licenses/OFL-Fraunces.txt |

The Noto fonts are the hinted TrueType builds of the Noto project; Inter comes from its own release (the build served by the Google Fonts API loses glyphs when its subset is cut), and Fraunces is the static SemiBold the Google Fonts API serves. All are embedded in the package as base64 modules by scripts/embed-fonts.mjs, which checks each file's SHA-256. The OFL allows them to be bundled and embedded in documents; their names are reserved, and they are not modified. In a PDF, each is embedded as a subset holding only the glyphs drawn, and only when one of its characters is drawn: a one-page invoice weighs about 30 kB.

Noto Sans was chosen because it is one design across the scripts a customer's name is most often written in, and because the family extends to nearly every other one — Noto Sans CJK, Devanagari, Hangul… — which a caller adds through options.fonts ({ regular, bold? }, TrueType or OpenType bytes) without the package growing by megabytes for everybody.

Each character is drawn by the first font that has it, embedded ones first, so a line may mix scripts. A character no font has is refused, glyph_not_covered, with its code point: an empty box where a name should be is worse than no PDF.

Right to left. Hebrew, Arabic, Syriac, Thaana, N'Ko and the other scripts of the right-to-left blocks of Unicode need the bidirectional algorithm and a mirrored layout, which this package does not have. A string holding one — in the rows or in the labels — is refused, right_to_left_text, naming the field. It is never drawn in the wrong order.

The colour profile declared as the output intent is the compact sRGB profile of Compact-ICC-Profiles, CC0 (licenses/CC0-sRGB-profile.txt).

Theme

import { DEFAULT_THEME, renderInvoicePdf } from '@ekwo-ai/invoice-pdf';

await renderInvoicePdf(input, { theme: { accent: '#7A1F2B', typeface: 'elegant', logoPosition: 'right' } });
// DEFAULT_THEME: { accent: '#1F3A5F', logoPosition: 'left', typeface: 'elegant' }

A few fields, all optional, and nothing else, on purpose: they are what a customer's own invoice sets — its colour, its typefaces, which side its logo is on.

| Field | What it changes | |---|---| | accent | #rrggbb. The title, the rule under the heading, the band of the column headings and of the amount due. The text on a band is white, or ink where the colour is light — whichever contrasts more. | | font | 'Noto Sans', or the caller's TrueType or OpenType bytes, { regular, bold? }, drawn first, with the embedded fonts for any character they lack. Embedded as a subset like the others. | | logoPosition | left: the logo above the seller's address. right: above the title, the seller's name on the left. | | typeface | A pair the package embeds, by name. elegant, the default: Inter for the text and every figure, Fraunces for the title, the seller's and the buyer's names, the headings of the sections and the labels of the totals. classic: Noto Sans everywhere. font and headingFont, where given, take the place of the pair's. | | headingFont | The caller's bytes, { regular, bold? }, for the title, the names and the headings; font draws any character it lacks. |

A credit. options.credit: { text: 'Ekwo.ai', url: 'https://ekwo.ai' } prints its words small, right-aligned, under the foot of every page, with a link over them; the address is https or refused (invalid_value). Without the option nothing is printed.

A colour that is not #rrggbb, a side that is neither or a typeface the package does not embed is refused, invalid_value. Nothing in the theme changes what is printed, only how.

Ekwo OS keeps the accent, the side and the typeface per company, in companies.invoice_accent_color, .invoice_logo_position and .invoice_typeface; ekwo doc pdf and the MCP tool render_invoice_pdf pass them as the theme.

The layout

The order of the page, top to bottom: the seller and the document's title, number, dates and references side by side; a rule; the buyer and the delivery address; the lines; the tax summary; the totals with, on their left, how to pay; the note; the authority's verification, its QR code on the left and what the authority gave the invoice on its right; the legal mentions; the foot.

What the layout keeps from earlier renderers

Two invoice renderers already in production elsewhere were read, section by section, before this layout was settled. Both draw with the standard PDF fonts without embedding them, which PDF/A forbids: neither is a strict PDF/A-3. What was clearly better in them is kept; nothing of their code is.

| Section | An earlier renderer did | Here | |---|---|---| | Header | Put the logo top left, scaled within a box, the document's title, number and dates on the right; one offered the logo on the left, the right or the centre. | Kept: logo within 170 × 60 points, on the left or the right (theme.logoPosition); the centre was left out, as it collides with the title. | | Header | Drew a separator line between the header and the customer. | Kept, in the accent colour. | | Customer | Set the customer apart in a filled or bordered box (both renderers). | Kept: the buyer, and the delivery address beside it, on shaded panels. | | Lines | Headed the table with a band of the theme's colour, in white. | Kept, with ink instead of white on a light colour. | | Totals | Showed what was already paid right under the total, then what remains due, "so that it cannot be missed". | Already so here; the amount due is now a band of the accent colour. | | Payment | Gathered the bank details — bank, holder, IBAN, BIC — in a framed box. | Kept, framed, beside the totals; the views carry no bank name or holder yet (see EN 16931). | | Credit note | Ended a negative balance on "in your favour" rather than "to pay". | Already so here: a credit note ends on amountCredited and gives no payment instructions. | | Theme | Several fixed themes, and one derived from the analysis of a customer's own invoice (colours, font style, title size, logo position, box styles). | Reduced to the three fields of Theme; no gallery. | | Lines | Added subtotals per section, zebra rows, a "paid" watermark. | Not kept: a subtotal would be computed here, and nothing printed is; zebra rows and a watermark add ink, not information. |

PDF/A-3

The PDF it renders declares itself PDF/A-3b, and is one by veraPDF's measure:

  • every font embedded, as a subset, the theme's included;
  • colours in DeviceRGB under an sRGB output intent;
  • nothing transparent: a PNG logo's alpha channel is flattened on white before it is embedded, and a CMYK JPEG is refused;
  • an XMP packet that says what the document information dictionary says — title, author, subject, creator, producer, creation and modification dates;
  • the language in the catalogue, and an identifier in the trailer.

veraPDF 1.30.3, profile PDF/A-3b (ISO 19005-3), on the files of examples/, as rendered by npm run examples:

| File | Rules passed | Rules failed | |---|---|---| | invoice-english.pdf | 146 | 0 | | invoice-greek.pdf | 146 | 0 | | invoice-english-factur-x.pdf (with embedFacturX) | 146 | 0 |

Before this was measured, the two plain PDFs failed one rule (6.6.2.1: no XMP metadata stream) and the Factur-X one passed with an XMP that did not mirror the dictionary; a credit note rendered as Factur-X, run as a further case, failed 6.1.6 on an identifier written with a minus sign. All three are fixed and tested. Also run, and with no failed rule: a 120-line invoice on six Letter pages, a Thai invoice, a logo with an alpha channel on the right with a light accent colour, a caller's TrueType font as the theme's typeface.

veraPDF is Java and is not a dependency: install it, then

VERAPDF=/path/to/verapdf npm run verapdf                 # the examples, PDF/A-3b
VERAPDF=/path/to/verapdf npm run verapdf -- --flavour 3b a.pdf b.pdf

which prints the rules passed and failed for each file and exits 1 when one fails. No test runs it, and a PDF made with caller fonts or logos this table does not cover is the caller's to validate.

Factur-X

import { renderFacturXPdf } from '@ekwo-ai/invoice-pdf';
import { embedFacturX } from '@ekwo-ai/factur-x/pdf';

const { file } = await renderFacturXPdf(input, { xml: ciiXml, profile: 'en16931', embed: embedFacturX });

renderFacturXPdf renders the PDF and hands it, unchanged, with the CII XML of the same document, to the embed function — embedFacturX, or anything of its shape. This package does not import @ekwo-ai/factur-x: a brick depends on no other brick.

The rendered PDF is already a PDF/A-3b; embedFacturX adds the XML and replaces the XMP packet with its own, which declares the Factur-X extension schema and is written from the same document information dictionary. veraPDF finds no failed rule of PDF/A-3b in the result (PDF/A-3). The test suite reads it back with readFacturX and gets the invoice, its profile and no violation, and checks that the totals, the tax per rate and the amount due printed on the page — read out of the PDF — are the figures of the CII inside it, on an invoice under two rates and on a credit note.

What it refuses

An InvoicePdfError with a code, before anything is drawn wrong:

| Code | When | |---|---| | not_a_sale_document | A purchase, a quote, an order: this package renders what a seller issues. | | missing_field | No currency, no date, no total, a tax group without its base or its tax. | | invalid_value | A figure that is not a decimal, a currency that is not ISO 4217, a date that is not YYYY-MM-DD; a theme colour that is not #rrggbb, a logo position that is neither left nor right. | | invalid_language | A language tag Intl does not accept. | | glyph_not_covered | A character no font has. | | right_to_left_text | A string written right to left. | | unsupported_logo, unsupported_font | A logo that is not a PNG or a JPEG, or a CMYK JPEG; a font that cannot be read. |

A draft is rendered, titled as a draft and without a number; a cancelled document says so under its title.

What it does not do

  • Keep the copy that was sent. A posted document is reproducible from the books (decision 0015), but the copy a customer received is the one that counts, and it should be kept as it was — an attachment of the document, with its checksum. Next.
  • Send it by e-mail. Next, beside it.
  • The button in the hosted web application. Next; it calls the same function the command line and the MCP server call.
  • Read a supplier's PDF. Receiving purchase invoices as PDF is another brick; the Factur-X reader already reads the XML inside one.
  • Right-to-left layouts, vertical text, other templates than this one: the theme changes its colour, typeface and logo side, not its layout.
  • Print what the views do not carry yet — see EN 16931 on the page.

EN 16931 on the page

Every business term of EN 16931 the rows hold is printed, and test/en16931.test.ts reads each one out of the PDF. The terms the standard makes mandatory:

| Term | Printed as | |---|---| | BT-1, BT-2, BT-3 | The number, the date, the title (Invoice, Credit note). | | BT-5 | The currency, in every amount, as Intl writes it (€2,601.50). | | BT-27, BT-40 | The seller's name and country (with its legal name, address, VAT and registration numbers when the rows hold them). | | BT-44, BT-55 | The buyer's name and country (with its address, VAT and registration numbers, e-mail, electronic address). | | BT-109, BT-112, BT-115 | Total excluding tax, total, amount due — and BT-110, BT-113 when given. | | BT-116, BT-117 | Each tax group's base and tax, with its rate (BT-119) and the reason it charges nothing (BT-120). | | BT-129, BT-130, BT-131, BT-146, BT-153 | Each line's quantity and unit, net amount, price and name. The price is the one keyed: before the discount, printed beside it, and with the tax in it where it is marked *. |

Not printed, because they are codes written for a machine, which the CII of a Factur-X carries and a reader of the page does not need: BT-24 (the specification identifier), BT-81 (the payment means code — the IBAN says transfer), BT-118 and BT-151 (the VAT category code — the tax's name says it), BT-126 (the line identifier — the lines are printed in its order). BT-106, the sum of the line amounts, is BT-109 as long as the views carry no allowance or charge on the document, which they do not.

What the views do not carry yet, and the page therefore cannot print:

  • BT-25, BT-26: the invoice a credit note credits, and its date (only documents.reversed_document_id has it);
  • BT-45, BT-56, BT-57: the buyer's trading name beside its legal name, its contact person and its phone;
  • BT-76, BT-79, BT-70: a second line and a region for the delivery address, and the name of the party delivered to;
  • BT-85: the holder of the payee's account — and its bank's name, for which EN 16931 has no term; and an account that is not an IBAN (BT-84 is read from payee_iban alone);
  • the words of the layout in the document's language, which no pack holds.

In Ekwo OS

ekwo doc pdf <document> [--factur-x] [--out <file>] and the MCP tool render_invoice_pdf read the four views as the person signed in, fetch the logo the company names by URL (from a public http(s) address only), and call this package — through renderDocumentPdf() of @ekwo-ai/core, which writes the CII with the adapter of the einvoicing formats when Factur-X is asked for. Where the company's country pack declares a fiscalisation regime and the einvoicing module validated the document (a clearance) or chained its record (a chained regime), readVerification() hands this package the link recorded and what the pack says to print beside it; nothing here knows which authority it is.

Development

npm install
npm run typecheck
npm test
npm run build
npm run examples     # rewrites examples/*.pdf
npm run verapdf      # needs veraPDF, see PDF/A-3

License

MIT © Ekwo AI. The fonts and the colour profile it embeds keep their own licences, in licenses/.