@reportwright/pdf 0.0.0-stage → 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +772 -2
- package/THIRD-PARTY-NOTICES.md +38 -0
- package/dist/index.js +18137 -0
- package/examples/factur-x.mjs +37 -0
- package/examples/harfbuzz-shaper.mjs +27 -0
- package/index.d.ts +847 -0
- package/package.json +19 -4
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// A Factur-X (ZUGFeRD 2.1+) invoice: a tagged PDF/A-3a page with the CII XML embedded as factur-x.xml.
|
|
2
|
+
//
|
|
3
|
+
// node examples/factur-x.mjs invoice.xml font.ttf out.pdf [profile]
|
|
4
|
+
//
|
|
5
|
+
// The XML must be a UN/CEFACT CrossIndustryInvoice whose GuidelineSpecifiedDocumentContextParameter ID names the
|
|
6
|
+
// profile (EN 16931 by default: urn:cen.eu:en16931:2017). pdf.facturX checks that, refuses any DOCTYPE or entity, and
|
|
7
|
+
// writes the fx XMP schema; it does not validate the invoice itself (run the Factur-X XSD and Schematron for that).
|
|
8
|
+
// The page here only shows a few fields; a real one draws the whole invoice from the same data as the XML.
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import { createPdf } from '../dist/index.js';
|
|
11
|
+
|
|
12
|
+
const [xmlFile, fontFile, outFile, profile = 'EN 16931'] = process.argv.slice(2);
|
|
13
|
+
if (!outFile) { console.error('node examples/factur-x.mjs invoice.xml font.ttf out.pdf [profile]'); process.exit(2); }
|
|
14
|
+
const xml = fs.readFileSync(xmlFile);
|
|
15
|
+
const text = xml.toString('utf8');
|
|
16
|
+
/** the first <ram:tag> after anchor (display only: the page shows a few fields of the XML) */
|
|
17
|
+
const field = (tag, anchor = '') => new RegExp(`${anchor}[\\s\\S]*?<ram:${tag}[^>]*>([^<]*)<`).exec(text)?.[1] ?? '';
|
|
18
|
+
const number = field('ID', '<rsm:ExchangedDocument>'), seller = field('Name', '<ram:SellerTradeParty>');
|
|
19
|
+
|
|
20
|
+
const pdf = createPdf(fs.createWriteStream(outFile), {
|
|
21
|
+
title: `Invoice ${number}`, author: seller, subject: 'Factur-X invoice', keywords: `invoice, Factur-X, ${profile}`,
|
|
22
|
+
pdfa: { part: 3, conformance: 'a' }, tagged: { lang: 'en' },
|
|
23
|
+
});
|
|
24
|
+
const font = await pdf.embedFont(fs.readFileSync(fontFile));
|
|
25
|
+
const page = pdf.addPage({ trimBox: [0, 0, 595.28, 841.89] });
|
|
26
|
+
const line = (s, y, size = 11) => page.text(s, { x: 56, y, font, size });
|
|
27
|
+
page.artifact({ type: 'Pagination', subtype: 'Header' }, () => line('Factur-X invoice: the XML inside this PDF is the legal invoice', 24, 8));
|
|
28
|
+
page.tag('H1', () => line(`Invoice ${number}`, 60, 22));
|
|
29
|
+
page.tag('P', () => line(`Seller: ${seller}`, 110));
|
|
30
|
+
page.tag('Table', () => {
|
|
31
|
+
page.tag('TR', () => ['Net', 'VAT', 'Total'].forEach((h, i) => page.tag('TH', () => page.text(h, { x: 56 + i * 140, y: 150, font }), { scope: 'Column', id: `h${i}` })));
|
|
32
|
+
page.tag('TR', () => [field('TaxBasisTotalAmount'), field('TaxTotalAmount'), field('GrandTotalAmount')].forEach((v, i) =>
|
|
33
|
+
page.tag('TD', () => page.text(`${v} ${field('InvoiceCurrencyCode')}`, { x: 56 + i * 140, y: 170, font }), { headers: [`h${i}`] })));
|
|
34
|
+
});
|
|
35
|
+
await pdf.facturX(xml, { profile });
|
|
36
|
+
await pdf.end();
|
|
37
|
+
console.log(`wrote ${outFile}: PDF/A-3a with factur-x.xml (${profile})`);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// A shaper for @reportwright/pdf's embedFont(bytes, { shaper }) built on harfbuzzjs (npm i harfbuzzjs). harfbuzzjs is
|
|
2
|
+
// not a dependency of @reportwright/pdf: copy this file, or import it where harfbuzzjs is installed.
|
|
3
|
+
//
|
|
4
|
+
// import { harfbuzzShaper } from './harfbuzz-shaper.mjs';
|
|
5
|
+
// const bytes = fs.readFileSync('NotoSansArabic-Regular.ttf');
|
|
6
|
+
// const arabic = await pdf.embedFont(bytes, { shaper: harfbuzzShaper(bytes) });
|
|
7
|
+
// page.text('مرحبا بالعالم', { x: 40, y: 40, font: arabic, size: 18, direction: 'rtl' });
|
|
8
|
+
//
|
|
9
|
+
// The shaper gets one run (one font, one direction) and returns its glyphs in visual order, in font units:
|
|
10
|
+
// { g: glyph ID, cl: cluster (UTF-16 index of its first character), ax: x advance, dx, dy: offsets }.
|
|
11
|
+
import * as hb from 'harfbuzzjs';
|
|
12
|
+
|
|
13
|
+
const NO_KERN = [new hb.Feature('kern', 0)];
|
|
14
|
+
|
|
15
|
+
/** @param {Uint8Array} fontBytes the same bytes passed to embedFont */
|
|
16
|
+
export function harfbuzzShaper(fontBytes) {
|
|
17
|
+
const font = new hb.Font(new hb.Face(new hb.Blob(fontBytes))); // scale: units per em, so positions are font units
|
|
18
|
+
return (text, _font, { direction, kerning }) => {
|
|
19
|
+
const b = new hb.Buffer();
|
|
20
|
+
b.addText(text); // clusters are UTF-16 indices, as JavaScript strings count
|
|
21
|
+
b.guessSegmentProperties(); // script and language from the text
|
|
22
|
+
b.setDirection(direction === 'rtl' ? hb.Direction.RTL : hb.Direction.LTR);
|
|
23
|
+
hb.shape(font, b, kerning ? [] : NO_KERN);
|
|
24
|
+
const pos = b.getGlyphPositions();
|
|
25
|
+
return b.getGlyphInfos().map((g, i) => ({ g: g.codepoint, cl: g.cluster, ax: pos[i].xAdvance, dx: pos[i].xOffset, dy: pos[i].yOffset }));
|
|
26
|
+
};
|
|
27
|
+
}
|