@reportwright/pdf 0.1.0-beta.1 → 0.1.0-beta.10

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.
@@ -0,0 +1,28 @@
1
+ // PDF/UA-1 and PDF/A-2a in one file: a structure tree (headings, a list, a figure), artifacts for decoration.
2
+ //
3
+ // node examples/accessible.mjs font.ttf [out.pdf]
4
+ import fs from 'node:fs';
5
+ import { createPdf } from '../dist/index.js';
6
+
7
+ const [fontFile, out = 'accessible.pdf'] = process.argv.slice(2);
8
+ if (!fontFile) { console.error('node examples/accessible.mjs font.ttf [out.pdf]'); process.exit(2); }
9
+ const pdf = createPdf(fs.createWriteStream(out), {
10
+ title: 'Leave policy', tagged: { lang: 'en-GB' }, // PDF/UA-1: a title and tags are required
11
+ pdfa: { part: 2, conformance: 'a' }, // PDF/A-2a: archival and tagged
12
+ });
13
+ const font = await pdf.embedFont(fs.readFileSync(fontFile));
14
+ const page = pdf.addPage();
15
+ page.artifact({ type: 'Pagination', subtype: 'Header' }, () => page.text('Example Ltd · HR policies', { x: 40, y: 20, font, size: 8 }));
16
+ page.tag('H1', () => page.text('Leave policy', { x: 40, y: 50, font, size: 22 }));
17
+ page.tag('P', () => page.textBox('Every employee gets the following leave each calendar year.', { x: 40, y: 90, width: 500, font }));
18
+ page.tag('L', () => ['18 days of annual leave', '12 days of sick leave'].forEach((s, i) => page.tag('LI', () => {
19
+ page.tag('Lbl', () => page.text('•', { x: 40, y: 120 + i * 18, font }));
20
+ page.tag('LBody', () => page.text(s, { x: 56, y: 120 + i * 18, font }));
21
+ })), { listNumbering: 'Disc' });
22
+ page.figure({ alt: 'Bar chart: annual leave 18 days, sick leave 12 days' }, () => {
23
+ page.fill(page.path().rect(40, 180, 180, 16), { fill: '#1f4e79' });
24
+ page.fill(page.path().rect(40, 200, 120, 16), { fill: '#5b9bd5' });
25
+ });
26
+ await page.end();
27
+ const { warnings } = await pdf.end();
28
+ console.log(`wrote ${out}: PDF/UA-1 + PDF/A-2a`, warnings.length ? warnings : '');
@@ -0,0 +1,19 @@
1
+ // Many files at once: extract text and encrypt in worker threads; a broken file fails alone.
2
+ //
3
+ // node examples/bulk.mjs [outDir] writes locked-1.pdf … locked-N.pdf
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { toBytes, bulk } from '../dist/index.js';
7
+
8
+ const dir = process.argv[2] ?? '.';
9
+ const make = (label) => toBytes(async (pdf) => {
10
+ const p = pdf.addPage(); p.text(`${label}: total due 1,250.00`, { x: 40, y: 40, font: pdf.standardFont('Helvetica'), size: 14 }); await p.end();
11
+ });
12
+ const files = [await make('Invoice 1'), new Uint8Array([0x25, 0x50]), await make('Invoice 3')]; // the second is not a PDF
13
+
14
+ const texts = await bulk(files, 'extractText', { workers: 2 });
15
+ texts.forEach((r, i) => console.log(i + 1, r.ok ? r.value : `failed: ${r.error.code} ${r.error.message}`));
16
+
17
+ const locked = await bulk(files, 'encrypt', { workers: 2, encrypt: { userPassword: 'open me', ownerPassword: 'full access' } });
18
+ locked.forEach((r, i) => { if (r.ok) fs.writeFileSync(path.join(dir, `locked-${i + 1}.pdf`), r.value); });
19
+ console.log(`encrypted ${locked.filter((r) => r.ok).length} of ${files.length}`);
@@ -0,0 +1,23 @@
1
+ // AES-256 password protection with permissions, then reading it back with the password.
2
+ //
3
+ // node examples/encrypt.mjs [out.pdf]
4
+ import fs from 'node:fs';
5
+ import { toBytes, loadPdf } from '../dist/index.js';
6
+
7
+ const out = process.argv[2] ?? 'encrypted.pdf';
8
+ const bytes = await toBytes(async (pdf) => {
9
+ const page = pdf.addPage();
10
+ page.text('Salary statement: confidential', { x: 40, y: 40, font: pdf.standardFont('Helvetica'), size: 14 });
11
+ await page.end();
12
+ }, {
13
+ title: 'Salary statement',
14
+ encrypt: {
15
+ userPassword: 'open me', // asked when the file opens ('' opens without a prompt)
16
+ ownerPassword: 'full access', // lifts the permissions; left out: random
17
+ algorithm: 'aes-256', // or 'aes-128' for very old readers
18
+ permissions: { print: true, copy: false, modify: false }, // each defaults to true
19
+ },
20
+ });
21
+ fs.writeFileSync(out, bytes);
22
+ const doc = await loadPdf(bytes, { password: 'open me' });
23
+ console.log(`wrote ${out}: ${doc.encryption.algorithm}, copy allowed: ${doc.encryption.permissions.copy}; text: ${doc.extractText().trim()}`);
@@ -0,0 +1,33 @@
1
+ // Forms: create a fillable PDF, fill it after loading, and flatten it into a plain PDF.
2
+ //
3
+ // node examples/fill-form.mjs [outDir] writes form.pdf, filled.pdf, flattened.pdf
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { toBytes, loadPdf } from '../dist/index.js';
7
+
8
+ const dir = process.argv[2] ?? '.';
9
+ // 1. Create: every field gets its appearance streams, so every viewer shows the same thing.
10
+ const form = await toBytes(async (pdf) => {
11
+ const font = pdf.standardFont('Helvetica');
12
+ const page = pdf.addPage();
13
+ page.text('Leave request', { x: 40, y: 40, font: pdf.standardFont('Helvetica-Bold'), size: 18 });
14
+ const f = pdf.form;
15
+ f.textField('name', { page, x: 40, y: 80, width: 240, height: 22, font, tooltip: 'Full name', required: true });
16
+ f.comboBox('type', { page, x: 40, y: 115, width: 160, height: 22, font, options: ['Annual', 'Sick', 'Unpaid'], value: 'Annual', tooltip: 'Leave type' });
17
+ f.checkbox('approved', { page, x: 40, y: 150, width: 14, height: 14, tooltip: 'Approved by manager' });
18
+ page.text('Approved', { x: 60, y: 151, font, size: 11 });
19
+ await page.end();
20
+ });
21
+ fs.writeFileSync(path.join(dir, 'form.pdf'), form);
22
+
23
+ // 2. Fill: values by full field name; the appearances are drawn again by the writer.
24
+ const doc = await loadPdf(form);
25
+ console.log('fields:', doc.fields.map((x) => `${x.name} (${x.type})`).join(', '));
26
+ doc.fill({ name: 'Aanya Sharma', type: 'Sick', approved: true });
27
+ fs.writeFileSync(path.join(dir, 'filled.pdf'), (await doc.save()).bytes);
28
+
29
+ // 3. Flatten: the fields become page content; no AcroForm is left.
30
+ const flat = (await loadPdf(form)).fill({ name: 'Aanya Sharma', type: 'Sick', approved: true }).flatten();
31
+ const flattened = (await flat.save()).bytes;
32
+ fs.writeFileSync(path.join(dir, 'flattened.pdf'), flattened);
33
+ console.log('flattened text:', (await loadPdf(flattened)).page(0).extractText().replace(/\s+/g, ' '));
@@ -0,0 +1,20 @@
1
+ // Hindi (Devanagari) text shaped by HarfBuzz: conjuncts and reordered matras drawn right, copied text in logical order.
2
+ //
3
+ // npm i harfbuzzjs
4
+ // node examples/indic.mjs NotoSansDevanagari-Regular.ttf [out.pdf]
5
+ import fs from 'node:fs';
6
+ import { createPdf } from '../dist/index.js';
7
+ import { harfbuzzShaper } from './harfbuzz-shaper.mjs';
8
+
9
+ const [fontFile, out = 'indic.pdf'] = process.argv.slice(2);
10
+ if (!fontFile) { console.error('node examples/indic.mjs NotoSansDevanagari-Regular.ttf [out.pdf]'); process.exit(2); }
11
+ const pdf = createPdf(fs.createWriteStream(out), { title: 'हिन्दी' });
12
+ const bytes = fs.readFileSync(fontFile);
13
+ const hindi = await pdf.embedFont(bytes, { shaper: harfbuzzShaper(bytes) });
14
+ const latin = pdf.standardFont('Helvetica');
15
+ const page = pdf.addPage();
16
+ page.text('किताब हिन्दी क्षत्रिय', { x: 40, y: 40, font: hindi, size: 24 });
17
+ page.textBox('Mixed text: इस किताब में हिन्दी और English दोनों हैं।', { x: 40, y: 90, width: 400, font: hindi, fallback: [latin], size: 14 });
18
+ await page.end();
19
+ const { warnings } = await pdf.end();
20
+ console.log(`wrote ${out}`, warnings.length ? warnings : '');
@@ -0,0 +1,40 @@
1
+ // A self-contained Factur-X invoice: a tagged PDF/A-3a page drawn from the same data as the CII XML embedded in it.
2
+ // (examples/factur-x.mjs does the same from an XML file you already have.)
3
+ //
4
+ // node examples/invoice-facturx.mjs font.ttf [out.pdf] any TrueType font, e.g. Inter-Regular.ttf
5
+ //
6
+ // The XML below is a short EN 16931 skeleton for the example. pdf.facturX checks its root, profile ID and that it has
7
+ // no DOCTYPE or entities; it does not validate the invoice: run the Factur-X XSD and Schematron on real invoices.
8
+ import fs from 'node:fs';
9
+ import { createPdf } from '../dist/index.js';
10
+
11
+ const [fontFile, out = 'invoice-facturx.pdf'] = process.argv.slice(2);
12
+ if (!fontFile) { console.error('node examples/invoice-facturx.mjs font.ttf [out.pdf]'); process.exit(2); }
13
+ const inv = { number: 'INV-2026-001', date: '20261009', seller: 'Example Ltd', buyer: 'Acme GmbH', currency: 'EUR',
14
+ lines: [['Consulting, 10 h', '1000.00'], ['Licence', '250.00']], net: '1250.00', vat: '237.50', total: '1487.50' };
15
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
16
+ <rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
17
+ <rsm:ExchangedDocumentContext><ram:GuidelineSpecifiedDocumentContextParameter><ram:ID>urn:cen.eu:en16931:2017</ram:ID></ram:GuidelineSpecifiedDocumentContextParameter></rsm:ExchangedDocumentContext>
18
+ <rsm:ExchangedDocument><ram:ID>${inv.number}</ram:ID><ram:TypeCode>380</ram:TypeCode><ram:IssueDateTime><udt:DateTimeString format="102">${inv.date}</udt:DateTimeString></ram:IssueDateTime></rsm:ExchangedDocument>
19
+ <rsm:SupplyChainTradeTransaction><ram:ApplicableHeaderTradeAgreement><ram:SellerTradeParty><ram:Name>${inv.seller}</ram:Name></ram:SellerTradeParty><ram:BuyerTradeParty><ram:Name>${inv.buyer}</ram:Name></ram:BuyerTradeParty></ram:ApplicableHeaderTradeAgreement>
20
+ <ram:ApplicableHeaderTradeSettlement><ram:InvoiceCurrencyCode>${inv.currency}</ram:InvoiceCurrencyCode><ram:SpecifiedTradeSettlementHeaderMonetarySummation><ram:TaxBasisTotalAmount>${inv.net}</ram:TaxBasisTotalAmount><ram:TaxTotalAmount currencyID="${inv.currency}">${inv.vat}</ram:TaxTotalAmount><ram:GrandTotalAmount>${inv.total}</ram:GrandTotalAmount><ram:DuePayableAmount>${inv.total}</ram:DuePayableAmount></ram:SpecifiedTradeSettlementHeaderMonetarySummation></ram:ApplicableHeaderTradeSettlement></rsm:SupplyChainTradeTransaction>
21
+ </rsm:CrossIndustryInvoice>`;
22
+
23
+ const pdf = createPdf(fs.createWriteStream(out), {
24
+ title: `Invoice ${inv.number}`, author: inv.seller, pdfa: { part: 3, conformance: 'a' }, tagged: { lang: 'en' },
25
+ });
26
+ const font = await pdf.embedFont(fs.readFileSync(fontFile)); // PDF/A: every font embedded
27
+ const page = pdf.addPage();
28
+ const t = (s, x, y, size = 11) => page.text(s, { x, y, font, size });
29
+ page.tag('H1', () => t(`Invoice ${inv.number}`, 56, 56, 22));
30
+ page.tag('P', () => t(`From ${inv.seller} to ${inv.buyer}`, 56, 96));
31
+ page.tag('Table', () => {
32
+ page.tag('TR', () => ['Item', `Amount (${inv.currency})`].forEach((h, i) => page.tag('TH', () => t(h, 56 + i * 300, 140), { scope: 'Column', id: `h${i}` })));
33
+ [...inv.lines, ['Net', inv.net], ['VAT 19%', inv.vat], ['Total', inv.total]].forEach(([item, amount], r) =>
34
+ page.tag('TR', () => [item, amount].forEach((v, i) => page.tag('TD', () => t(v, 56 + i * 300, 162 + r * 20), { headers: [`h${i}`] }))));
35
+ });
36
+ page.stroke(page.path().moveTo(56, 156).lineTo(540, 156), { width: 0.5 }); // outside any tag: an artifact
37
+ await pdf.facturX(new TextEncoder().encode(xml), { profile: 'EN 16931' });
38
+ await page.end();
39
+ const { warnings } = await pdf.end();
40
+ console.log(`wrote ${out}: PDF/A-3a + factur-x.xml (EN 16931)`, warnings.length ? warnings : '');
@@ -0,0 +1,29 @@
1
+ // Read and modify: merge, split, extract text, and an incremental update that keeps the original bytes.
2
+ //
3
+ // node examples/merge-split.mjs [outDir] writes merged.pdf, page-1.pdf … page-N.pdf, updated.pdf
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { toBytes, loadPdf, mergePdfs } from '../dist/index.js';
7
+
8
+ const dir = process.argv[2] ?? '.';
9
+ const make = (label, n) => toBytes(async (pdf) => {
10
+ const font = pdf.standardFont('Helvetica');
11
+ for (let i = 1; i <= n; i++) { const p = pdf.addPage(); p.text(`${label}, page ${i}`, { x: 40, y: 40, font, size: 16 }); await p.end(); }
12
+ }, { title: label });
13
+ const a = await loadPdf(await make('Contract', 2)), b = await loadPdf(await make('Annexure', 1));
14
+
15
+ const merged = await mergePdfs([a, b]); // a new file, every page of each
16
+ fs.writeFileSync(path.join(dir, 'merged.pdf'), merged.bytes);
17
+ const all = await loadPdf(merged.bytes);
18
+ console.log(`merged: ${all.pageCount} pages; text:`, all.extractText().split('\f').map((t) => t.trim()));
19
+
20
+ for (let i = 0; i < all.pageCount; i++) { // split: a fork per page
21
+ const { bytes } = await all.fork().reorder([i]).save();
22
+ fs.writeFileSync(path.join(dir, `page-${i + 1}.pdf`), bytes);
23
+ }
24
+
25
+ const doc = await loadPdf(merged.bytes);
26
+ doc.setInfo({ title: 'Contract (reviewed)' });
27
+ const update = await doc.saveIncremental(); // original bytes + an appended update
28
+ fs.writeFileSync(path.join(dir, 'updated.pdf'), update.bytes);
29
+ console.log(`split into ${all.pageCount} files; update appended ${update.size - merged.bytes.length} bytes`);
@@ -0,0 +1,29 @@
1
+ // PAdES B-B signature while the PDF streams out, then a second signature appended with saveIncremental.
2
+ //
3
+ // openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365 -subj "/CN=Example Signer"
4
+ // node examples/sign.mjs key.pem cert.pem [out.pdf]
5
+ // For a real signature use a certificate from a CA your readers trust, and a timestamp (RFC 3161) for PAdES B-T.
6
+ import fs from 'node:fs';
7
+ import { createPdf, loadPdf, nodeSigner } from '../dist/index.js';
8
+
9
+ const [keyFile, certFile, out = 'signed.pdf'] = process.argv.slice(2);
10
+ if (!certFile) { console.error('node examples/sign.mjs key.pem cert.pem [out.pdf]'); process.exit(2); }
11
+ const signer = nodeSigner({ key: fs.readFileSync(keyFile, 'utf8'), certs: fs.readFileSync(certFile, 'utf8') });
12
+
13
+ const chunks = [];
14
+ const pdf = createPdf({ write: (b) => { chunks.push(Buffer.from(b)); } }, { title: 'Offer letter', sign: true });
15
+ const font = pdf.standardFont('Helvetica');
16
+ const page = pdf.addPage();
17
+ page.text('Offer letter', { x: 40, y: 40, font, size: 18 });
18
+ const hr = pdf.form.signature('hr', { page, x: 40, y: 700, width: 200, height: 40, tooltip: 'HR signature' });
19
+ pdf.form.signature('candidate', { page, x: 300, y: 700, width: 200, height: 40, tooltip: 'Candidate signature' });
20
+ await pdf.sign(hr, { signer, reason: 'Issued', location: 'Bengaluru', name: 'HR', certify: 2 }); // DocMDP P=2: signing allowed later
21
+ await page.end();
22
+ await pdf.end(); // the signer runs here, on the streamed bytes' digest
23
+
24
+ // Countersign: appends an update, so the first signature stays valid.
25
+ const doc = await loadPdf(Buffer.concat(chunks));
26
+ const { bytes } = await doc.saveIncremental({ sign: { field: 'candidate', signer, reason: 'Accepted' } });
27
+ fs.writeFileSync(out, bytes);
28
+ const signatures = (await loadPdf(bytes)).signatures;
29
+ console.log(`wrote ${out}:`, signatures.map((s) => `${s.field} (revision ${s.revision}, covers the whole file: ${s.coversWholeFile})`).join('; '));
@@ -0,0 +1,53 @@
1
+ // Text, an image, a table drawn with paths, a link and bookmarks, using the standard fonts (no files needed).
2
+ //
3
+ // node examples/write.mjs [out.pdf]
4
+ import fs from 'node:fs';
5
+ import zlib from 'node:zlib';
6
+ import { createPdf } from '../dist/index.js';
7
+
8
+ const out = process.argv[2] ?? 'write.pdf';
9
+ const pdf = createPdf(fs.createWriteStream(out), { title: 'Quarterly report', author: 'Example Ltd' });
10
+ const regular = pdf.standardFont('Helvetica'), bold = pdf.standardFont('Helvetica-Bold');
11
+
12
+ // A 32 x 32 RGBA PNG made here so the example needs no image file (use fs.readFileSync('logo.png') in real code).
13
+ function png(w, h, pixel) {
14
+ const chunk = (type, data) => {
15
+ const b = Buffer.alloc(12 + data.length); b.writeUInt32BE(data.length, 0); b.write(type, 4, 'latin1'); data.copy(b, 8);
16
+ b.writeUInt32BE(zlib.crc32(b.subarray(4, 8 + data.length)), 8 + data.length); return b;
17
+ };
18
+ const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr.set([8, 6, 0, 0, 0], 8);
19
+ const raw = Buffer.alloc(h * (1 + w * 4));
20
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) raw.set(pixel(x, y), y * (1 + w * 4) + 1 + x * 4);
21
+ return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk('IHDR', ihdr), chunk('IDAT', zlib.deflateSync(raw)), chunk('IEND', Buffer.alloc(0))]);
22
+ }
23
+ const logo = await pdf.embedImage(png(32, 32, (x, y) => [31, 78, 121, (x - 16) ** 2 + (y - 16) ** 2 < 256 ? 255 : 0]));
24
+
25
+ const page = pdf.addPage(); // A4; origin top-left, y down, in points
26
+ page.image(logo, { x: 40, y: 40, width: 32 });
27
+ page.text('Quarterly report', { x: 84, y: 44, font: bold, size: 22, color: '#1f4e79' });
28
+ const box = page.textBox('Revenue grew in every region. The table below is drawn with paths: a filled header row, ' +
29
+ 'rules between rows and right-aligned numbers.', { x: 40, y: 90, width: 515, font: regular, size: 11, align: 'justify' });
30
+
31
+ // A table from paths: no table primitive is needed.
32
+ const rows = [['Region', 'Q1', 'Q2'], ['North', '1,204', '1,390'], ['South', '980', '1,122'], ['West', '1,510', '1,488']];
33
+ const cols = [40, 300, 430], right = 555, rowH = 22, top = 100 + box.height;
34
+ page.fill(page.path().rect(40, top, right - 40, rowH), { fill: '#1f4e79' });
35
+ rows.forEach((r, i) => {
36
+ const y = top + i * rowH;
37
+ r.forEach((cell, c) => {
38
+ const font = i === 0 ? bold : regular, size = 11;
39
+ const x = c === 0 ? cols[c] + 6 : (cols[c + 1] ?? right) - 6 - font.widthOfText(cell, size);
40
+ page.text(cell, { x, y: y + 5, font, size, color: i === 0 ? '#ffffff' : 0 });
41
+ });
42
+ if (i > 0) page.stroke(page.path().moveTo(40, y + rowH).lineTo(right, y + rowH), { stroke: 0.75, width: 0.5 });
43
+ });
44
+
45
+ page.text('Read the full report online', { x: 40, y: top + rows.length * rowH + 20, font: regular, color: '#0645ad' });
46
+ page.link(40, top + rows.length * rowH + 20, 160, 14, { url: 'https://example.com/report' });
47
+
48
+ const p2 = pdf.addPage();
49
+ p2.text('Appendix', { x: 40, y: 40, font: bold, size: 18 });
50
+ pdf.outline([{ title: 'Quarterly report', page: 1 }, { title: 'Appendix', page: 2, level: 1 }]);
51
+ await page.end(); await p2.end();
52
+ const { pages, bytes, warnings } = await pdf.end();
53
+ console.log(`wrote ${out}: ${pages} pages, ${bytes} bytes`, warnings.length ? warnings : '');
package/fflate.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ // @reportwright/pdf/fflate: the fflate (MIT) this package bundles, for a bundle that also uses fflate (the ReportWright
2
+ // engine) to share one copy. Its types are fflate's own (npm i -D fflate@0.8.3 to have them).
3
+ export * from 'fflate';
package/index.d.ts CHANGED
@@ -64,6 +64,13 @@ export interface PdfOptions {
64
64
  creationDate?: Date;
65
65
  /** Flate-compress content, fonts, object streams (default true). */
66
66
  compress?: boolean;
67
+ /**
68
+ * The zlib level (0–9) the object streams (the small objects, packed together) and the cross-reference stream are
69
+ * compressed at. Unset: the object streams at 6 and the cross-reference stream by the bundled fflate at 6, as before
70
+ * beta.7. Set, both go through the platform's zlib (Node's) at that level: 9 makes the file a little smaller and
71
+ * slower. The same input and options give the same bytes.
72
+ */
73
+ objectStreamLevel?: number;
67
74
  /**
68
75
  * PDF/A: an output intent (sRGB, or outputIntent: your RGB or CMYK ICC output/monitor profile) and XMP
69
76
  * identification. Fonts must be embedded; device colour must match the intent (CMYK needs a CMYK intent, RGB an RGB one).
@@ -646,10 +653,11 @@ export interface FormScope {
646
653
 
647
654
  export interface Pdf {
648
655
  standardFont(name: StandardFontName): Font;
649
- /** a TrueType or OpenType/CFF font; subset by default; shaper: text goes through it (complex scripts) */
650
- embedFont(bytes: Uint8Array | ArrayBuffer, o?: { subset?: boolean | Subsetter; shaper?: Shaper }): Promise<Font>;
656
+ /** a TrueType or OpenType/CFF font; subset by default; shaper: text goes through it (complex scripts);
657
+ * textMapping: 'carrier' (default: Firefox, pdf.js, search) or 'actualText' (a shaped cluster's text only in its ActualText: MuPDF, AI pipelines) */
658
+ embedFont(bytes: Uint8Array | ArrayBuffer, o?: { subset?: boolean | Subsetter; shaper?: Shaper; textMapping?: 'carrier' | 'actualText' }): Promise<Font>;
651
659
  /** a JPEG or PNG, written now as an image XObject. colorSpace: an ICC colour space with the image's component count */
652
- embedImage(bytes: Uint8Array | ArrayBuffer, o?: { colorSpace?: IccColorSpace; /** the decoded size a PNG with alpha or interlacing may have; default 64 MB */ maxDecodedBytes?: number }): Promise<Image>;
660
+ embedImage(bytes: Uint8Array | ArrayBuffer, o?: { colorSpace?: IccColorSpace; /** the decoded size a PNG with alpha or interlacing may have; default 64 MB */ maxDecodedBytes?: number; /** zlib level for a re-compressed PNG (alpha, interlacing): 'default' = 6, or 3 above 2 megapixels; 'fast' = 1; or 1–9 */ compression?: 'default' | 'fast' | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 }): Promise<Image>;
653
661
  /** a spot colour: its name and the device colour (gray, RGB, CMYK) shown for it at full tint. One definition per name */
654
662
  spotColor(name: string, alternate: number | string | readonly number[]): SpotColor;
655
663
  /** an ICC-based colour space from a gray, RGB or CMYK profile (its header is checked) */
@@ -845,3 +853,38 @@ export interface LoadedPdf {
845
853
  export function loadPdf(bytes: Uint8Array | ArrayBuffer, options?: LoadOptions): Promise<LoadedPdf>;
846
854
  /** every page of each document, in order, in a new file (the first one's catalog, with what the others bring) */
847
855
  export function mergePdfs(docs: LoadedPdf[], options?: SaveOptions): Promise<SaveResult & { bytes: Uint8Array }>;
856
+ /** the tasks bulk() runs, by name (no functions cross to a worker) */
857
+ export type BulkTask = 'extractText' | 'merge' | 'split' | 'encrypt';
858
+ export const BULK_TASKS: readonly BulkTask[];
859
+ /** what each task takes per item, and gives back */
860
+ export interface BulkTaskTypes {
861
+ extractText: { input: Uint8Array | ArrayBuffer; value: string };
862
+ merge: { input: (Uint8Array | ArrayBuffer)[]; value: Uint8Array };
863
+ split: { input: Uint8Array | ArrayBuffer; value: Uint8Array[] };
864
+ encrypt: { input: Uint8Array | ArrayBuffer; value: Uint8Array };
865
+ }
866
+ export type BulkResult<V> = { ok: true; value: V } | { ok: false; error: { name: string; code?: string; message: string } };
867
+ export interface BulkOptions {
868
+ /** worker threads (1 to 64); default os.availableParallelism() - 1 (at least 1). 1, or no node:worker_threads (browsers): one at a time, in this thread.
869
+ * Workers are kept warm (unref'd) for 2 s after a call, for the next; small files are sent several at a time */
870
+ workers?: number;
871
+ /** each worker's heap cap in MB (resourceLimits.maxOldGenerationSizeMb), 16 to 16384, default 512: an item past it fails with E_WORKER, alone */
872
+ memoryMb?: number;
873
+ /** loadPdf's options, for every input */
874
+ password?: string;
875
+ budget?: ReadBudget;
876
+ timeoutMs?: number;
877
+ /** the encrypt task's encryption */
878
+ encrypt?: EncryptOptions;
879
+ /** save()'s options for merge, split and encrypt */
880
+ save?: SaveOptions;
881
+ /** each item's result as it finishes (index in inputs) */
882
+ onResult?: (index: number, result: BulkResult<unknown>) => void;
883
+ /** each item's time limit in a worker, default 120000, at most 2147483647: past it the worker is ended, the item fails with E_TIMEOUT and the rest go on.
884
+ * With one input, workers: 1, or a host where workers resolve to 1 (2 CPUs), items run in the calling thread: no time limit and no heap cap apply there. */
885
+ itemTimeoutMs?: number;
886
+ /** stops the call: its workers are ended (not kept warm) and bulk rejects with the signal's reason (AbortSignal.timeout(ms) for a time limit) */
887
+ signal?: AbortSignal;
888
+ }
889
+ /** One named task over many inputs, in parallel worker threads (Node); results in input order, a failing item never stops the rest. */
890
+ export function bulk<T extends BulkTask>(inputs: Iterable<BulkTaskTypes[T]['input']>, task: T, options?: BulkOptions): Promise<BulkResult<BulkTaskTypes[T]['value']>[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reportwright/pdf",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.10",
4
4
  "description": "A streaming PDF writer for Node and browsers with no runtime dependencies: flat memory, PDF/A-1b to 3a, PDF/UA-1, Factur-X, embedded TrueType with Unicode text.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -12,10 +12,12 @@
12
12
  "publishConfig": { "access": "public" },
13
13
  "exports": {
14
14
  ".": { "types": "./index.d.ts", "default": "./dist/index.js" },
15
+ "./fflate": { "types": "./fflate.d.ts", "default": "./dist/fflate.js" },
16
+ "./examples/*": "./examples/*",
15
17
  "./package.json": "./package.json"
16
18
  },
17
19
  "types": "./index.d.ts",
18
- "files": ["dist", "index.d.ts", "examples", "README.md", "LICENSE", "THIRD-PARTY-NOTICES.md"],
20
+ "files": ["dist", "index.d.ts", "fflate.d.ts", "examples", "README.md", "LICENSE", "THIRD-PARTY-NOTICES.md"],
19
21
  "engines": { "node": ">=20.16" },
20
22
  "sideEffects": false
21
23
  }