pdf-oxide 0.3.38 → 0.3.39

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.
@@ -23,6 +23,8 @@
23
23
  * const bytes = builder.build(); // consumes the builder
24
24
  * ```
25
25
  */
26
+ import { Align, type Column, type SpanCell, type StreamingTableConfig, type TableMode, type TableSpec } from '../types/common.js';
27
+ import { StreamingTable } from './streaming-table.js';
26
28
  /**
27
29
  * TTF/OTF font handle registerable with {@link DocumentBuilder}. Single-use:
28
30
  * after `registerEmbeddedFont` the native handle is moved into the builder
@@ -69,6 +71,29 @@ export declare class DocumentBuilder {
69
71
  keywords(keywords: string): this;
70
72
  /** Set the creator application name. */
71
73
  creator(creator: string): this;
74
+ /** Run a JavaScript script when the document is opened (/OpenAction). */
75
+ onOpen(script: string): this;
76
+ /**
77
+ * Enable PDF/UA-1 tagged PDF mode.
78
+ *
79
+ * When enabled, `build()` emits `/MarkInfo`, `/StructTreeRoot`, `/Lang`, and
80
+ * `/ViewerPreferences` in the catalog. Opt-in — no effect unless called.
81
+ * Bundle F-1/F-2.
82
+ */
83
+ taggedPdfUa1(): this;
84
+ /**
85
+ * Set the document's natural language tag, e.g. `"en-US"`.
86
+ *
87
+ * Emitted as `/Lang` in the catalog when `taggedPdfUa1()` is set. Bundle F-2.
88
+ */
89
+ language(lang: string): this;
90
+ /**
91
+ * Add a role-map entry: custom structure type → standard PDF structure type.
92
+ *
93
+ * Emitted in `/RoleMap` inside the StructTreeRoot when `taggedPdfUa1()` is
94
+ * set. Multiple calls accumulate entries. Bundle F-4.
95
+ */
96
+ roleMap(custom: string, standard: string): this;
72
97
  /**
73
98
  * Register a TTF / OTF font under `name`. CONSUMES `font` on success —
74
99
  * do not call `close()` on the font afterwards.
@@ -125,6 +150,20 @@ export declare class PageBuilder {
125
150
  linkPage(pageIndex: number): this;
126
151
  /** Link the previous text to a named destination. */
127
152
  linkNamed(destination: string): this;
153
+ /** Link the previous text to a JavaScript action. */
154
+ linkJavascript(script: string): this;
155
+ /** Run JavaScript when this page is opened (/AA /O). */
156
+ onOpen(script: string): this;
157
+ /** Run JavaScript when this page is closed (/AA /C). */
158
+ onClose(script: string): this;
159
+ /** Set a keystroke JS action (/AA /K) on the last form field. */
160
+ fieldKeystroke(script: string): this;
161
+ /** Set a format JS action (/AA /F) on the last form field. */
162
+ fieldFormat(script: string): this;
163
+ /** Set a validate JS action (/AA /V) on the last form field. */
164
+ fieldValidate(script: string): this;
165
+ /** Set a calculate JS action (/AA /C) on the last form field. */
166
+ fieldCalculate(script: string): this;
128
167
  /** Highlight the previous text with an RGB colour (channels 0-1). */
129
168
  highlight(r: number, g: number, b: number): this;
130
169
  /** Underline the previous text. */
@@ -177,12 +216,122 @@ export declare class PageBuilder {
177
216
  radioGroup(name: string, buttons: Array<[string, number, number, number, number]>, selected?: string): this;
178
217
  /** Add a clickable push button with a visible caption. */
179
218
  pushButton(name: string, x: number, y: number, w: number, h: number, caption: string): this;
219
+ /** Add an unsigned signature placeholder field (/FT /Sig) at the given bounds. */
220
+ signatureField(name: string, x: number, y: number, w: number, h: number): this;
221
+ /**
222
+ * Add a footnote: inline `refMark` emitted at the cursor position, and
223
+ * `noteText` placed near the page bottom with a separator artifact line.
224
+ */
225
+ footnote(refMark: string, noteText: string): this;
226
+ /**
227
+ * Lay out `text` as balanced multi-column flow.
228
+ * `columnCount` columns separated by `gapPt` points.
229
+ * Paragraphs in `text` are delimited by `"\n\n"`.
230
+ */
231
+ columns(columnCount: number, gapPt: number, text: string): this;
232
+ /**
233
+ * Emit `text` inline at the current cursor position without advancing
234
+ * to a new line. The cursor advances horizontally so the next `inline`
235
+ * call follows on the same line.
236
+ */
237
+ inline(text: string): this;
238
+ /** Emit `text` inline in bold weight. */
239
+ inlineBold(text: string): this;
240
+ /** Emit `text` inline in italic style. */
241
+ inlineItalic(text: string): this;
242
+ /** Emit `text` inline in an RGB colour (channels 0–1). */
243
+ inlineColor(r: number, g: number, b: number, text: string): this;
244
+ /** Advance the cursor to the start of the next line. */
245
+ newline(): this;
246
+ /**
247
+ * Place a 1-D barcode image on the page at `(x, y, w, h)`.
248
+ * `barcodeType`: 0=Code128 1=Code39 2=EAN13 3=EAN8 4=UPCA 5=ITF
249
+ * 6=Code93 7=Codabar.
250
+ */
251
+ barcode1d(barcodeType: number, data: string, x: number, y: number, w: number, h: number): this;
252
+ /** Place a QR-code image on the page (square: `size × size` pt). */
253
+ barcodeQr(data: string, x: number, y: number, size: number): this;
254
+ /**
255
+ * Embed an image with an accessibility alt text (PDF/UA-1 §Figure).
256
+ * `bytes` must contain raw JPEG/PNG/WebP image data.
257
+ */
258
+ imageWithAlt(bytes: Buffer | Uint8Array, x: number, y: number, w: number, h: number, altText: string): this;
259
+ /**
260
+ * Embed a decorative image as an /Artifact (no alt text, PDF/UA-1 §Artifact).
261
+ * `bytes` must contain raw JPEG/PNG/WebP image data.
262
+ */
263
+ imageArtifact(bytes: Buffer | Uint8Array, x: number, y: number, w: number, h: number): this;
180
264
  /** Draw a stroked rectangle outline (1pt black). */
181
265
  rect(x: number, y: number, w: number, h: number): this;
182
266
  /** Draw a filled rectangle in RGB colour (channels 0–1). */
183
267
  filledRect(x: number, y: number, w: number, h: number, r: number, g: number, b: number): this;
184
268
  /** Draw a line from `(x1, y1)` to `(x2, y2)` with 1pt black stroke. */
185
269
  line(x1: number, y1: number, x2: number, y2: number): this;
270
+ /**
271
+ * Draw a stroked rectangle outline with caller-supplied width + RGB
272
+ * colour (channels 0–1). Underlies the Table surface.
273
+ */
274
+ strokeRect(x: number, y: number, w: number, h: number, style?: {
275
+ width?: number;
276
+ color?: [number, number, number];
277
+ }): this;
278
+ /**
279
+ * Draw a straight line with caller-supplied width + RGB colour.
280
+ */
281
+ strokeLine(x1: number, y1: number, x2: number, y2: number, style?: {
282
+ width?: number;
283
+ color?: [number, number, number];
284
+ }): this;
285
+ /**
286
+ * Place wrapped text inside the rectangle (x, y, w, h) with the
287
+ * given horizontal alignment. Uses the current font + size. Text
288
+ * that does not fit is clipped to the rectangle height.
289
+ */
290
+ textInRect(x: number, y: number, w: number, h: number, text: string, align?: Align): this;
291
+ /**
292
+ * Start a new page with the *same* dimensions as the current one.
293
+ * Text config (font + size) carries over; the cursor resets to the
294
+ * top-left margin. Callers wanting header-repeat-on-break must
295
+ * re-emit the header explicitly.
296
+ */
297
+ newPageSameSize(): this;
298
+ /**
299
+ * Measure the width of `text` in the current font and size.
300
+ *
301
+ * Note: v0.3.39 ships a JS-side approximation (0.55em per glyph for
302
+ * ASCII / typical Latin-1 characters). A true per-glyph measurement
303
+ * FFI is pending. The result is in PDF points.
304
+ */
305
+ measure(text: string): number;
306
+ /**
307
+ * Estimate the remaining vertical space on the page at the current
308
+ * cursor position. v0.3.39 returns `null` when the native cursor is
309
+ * unknown — callers should treat `null` as "unknown; assume fresh
310
+ * page". A real FFI hook is pending in a follow-up release.
311
+ */
312
+ remainingSpace(): number | null;
313
+ /**
314
+ * Emit a buffered table at the current cursor. All rows are
315
+ * marshalled into a single FFI call — memory scales with the row
316
+ * count. For million-row streams use {@link streamingTable}.
317
+ */
318
+ table(spec: TableSpec): this;
319
+ /**
320
+ * Begin a streaming table. Uses the native row-at-a-time FFI
321
+ * (`pdf_page_builder_streaming_table_begin_v2`). Pass a `mode` in
322
+ * `config` to control column-sizing strategy (default: fixed widths).
323
+ */
324
+ streamingTable(config: StreamingTableConfig): StreamingTable;
325
+ /** @internal — open streaming table FFI handle on this page. */
326
+ _streamingTableBeginV2(headers: string[], widths: number[], aligns: number[], repeatHeader: boolean, mode: TableMode | undefined, maxRowspan: number): void;
327
+ /** @internal — push one row into the open streaming table (all rowspan=1). */
328
+ _streamingTablePushRow(cells: Array<string | null>): void;
329
+ /** @internal — push one row with per-cell rowspan values. */
330
+ _streamingTablePushRowV2(cells: Array<[string | null, number]>): void;
331
+ /** @internal — close the open streaming table. */
332
+ _streamingTableFinish(): void;
333
+ /** @internal — track the last font size for JS-side `measure()`. */
334
+ _lastFontSize?: number;
186
335
  /**
187
336
  * Commit the page's buffered operations to the parent builder and
188
337
  * return the parent for chaining. After `done()` this PageBuilder is
@@ -197,3 +346,5 @@ export declare class PageBuilder {
197
346
  /** Symbol.dispose support for `using`. */
198
347
  [Symbol.dispose](): void;
199
348
  }
349
+ export { StreamingTable } from './streaming-table.js';
350
+ export { Align, type Column, type SpanCell, type StreamingTableConfig, type TableMode, type TableSpec, };
@@ -27,6 +27,8 @@
27
27
  // against `prebuilds/<triple>/pdf_oxide.node` in the published
28
28
  // package and the in-tree `build/Release/` output in dev mode.
29
29
  import { loadNative } from '../native.js';
30
+ import { Align, } from '../types/common.js';
31
+ import { StreamingTable } from './streaming-table.js';
30
32
  const native = loadNative();
31
33
  /**
32
34
  * TTF/OTF font handle registerable with {@link DocumentBuilder}. Single-use:
@@ -123,6 +125,41 @@ export class DocumentBuilder {
123
125
  native.documentBuilderSetCreator(this.checkUsable(), creator);
124
126
  return this;
125
127
  }
128
+ /** Run a JavaScript script when the document is opened (/OpenAction). */
129
+ onOpen(script) {
130
+ native.documentBuilderOnOpen(this.checkUsable(), script);
131
+ return this;
132
+ }
133
+ /**
134
+ * Enable PDF/UA-1 tagged PDF mode.
135
+ *
136
+ * When enabled, `build()` emits `/MarkInfo`, `/StructTreeRoot`, `/Lang`, and
137
+ * `/ViewerPreferences` in the catalog. Opt-in — no effect unless called.
138
+ * Bundle F-1/F-2.
139
+ */
140
+ taggedPdfUa1() {
141
+ native.documentBuilderTaggedPdfUa1(this.checkUsable());
142
+ return this;
143
+ }
144
+ /**
145
+ * Set the document's natural language tag, e.g. `"en-US"`.
146
+ *
147
+ * Emitted as `/Lang` in the catalog when `taggedPdfUa1()` is set. Bundle F-2.
148
+ */
149
+ language(lang) {
150
+ native.documentBuilderLanguage(this.checkUsable(), lang);
151
+ return this;
152
+ }
153
+ /**
154
+ * Add a role-map entry: custom structure type → standard PDF structure type.
155
+ *
156
+ * Emitted in `/RoleMap` inside the StructTreeRoot when `taggedPdfUa1()` is
157
+ * set. Multiple calls accumulate entries. Bundle F-4.
158
+ */
159
+ roleMap(custom, standard) {
160
+ native.documentBuilderRoleMap(this.checkUsable(), custom, standard);
161
+ return this;
162
+ }
126
163
  /**
127
164
  * Register a TTF / OTF font under `name`. CONSUMES `font` on success —
128
165
  * do not call `close()` on the font afterwards.
@@ -235,6 +272,7 @@ export class PageBuilder {
235
272
  /** Set font + size for subsequent text. */
236
273
  font(name, size) {
237
274
  native.pageBuilderFont(this.h(), name, size);
275
+ this._lastFontSize = size;
238
276
  return this;
239
277
  }
240
278
  /** Move the cursor to absolute coordinates. */
@@ -283,6 +321,41 @@ export class PageBuilder {
283
321
  native.pageBuilderLinkNamed(this.h(), destination);
284
322
  return this;
285
323
  }
324
+ /** Link the previous text to a JavaScript action. */
325
+ linkJavascript(script) {
326
+ native.pageBuilderLinkJavascript(this.h(), script);
327
+ return this;
328
+ }
329
+ /** Run JavaScript when this page is opened (/AA /O). */
330
+ onOpen(script) {
331
+ native.pageBuilderOnOpen(this.h(), script);
332
+ return this;
333
+ }
334
+ /** Run JavaScript when this page is closed (/AA /C). */
335
+ onClose(script) {
336
+ native.pageBuilderOnClose(this.h(), script);
337
+ return this;
338
+ }
339
+ /** Set a keystroke JS action (/AA /K) on the last form field. */
340
+ fieldKeystroke(script) {
341
+ native.pageBuilderFieldKeystroke(this.h(), script);
342
+ return this;
343
+ }
344
+ /** Set a format JS action (/AA /F) on the last form field. */
345
+ fieldFormat(script) {
346
+ native.pageBuilderFieldFormat(this.h(), script);
347
+ return this;
348
+ }
349
+ /** Set a validate JS action (/AA /V) on the last form field. */
350
+ fieldValidate(script) {
351
+ native.pageBuilderFieldValidate(this.h(), script);
352
+ return this;
353
+ }
354
+ /** Set a calculate JS action (/AA /C) on the last form field. */
355
+ fieldCalculate(script) {
356
+ native.pageBuilderFieldCalculate(this.h(), script);
357
+ return this;
358
+ }
286
359
  /** Highlight the previous text with an RGB colour (channels 0-1). */
287
360
  highlight(r, g, b) {
288
361
  native.pageBuilderHighlight(this.h(), r, g, b);
@@ -384,6 +457,88 @@ export class PageBuilder {
384
457
  native.pageBuilderPushButton(this.h(), name, x, y, w, h, caption);
385
458
  return this;
386
459
  }
460
+ /** Add an unsigned signature placeholder field (/FT /Sig) at the given bounds. */
461
+ signatureField(name, x, y, w, h) {
462
+ native.pageBuilderSignatureField(this.h(), name, x, y, w, h);
463
+ return this;
464
+ }
465
+ /**
466
+ * Add a footnote: inline `refMark` emitted at the cursor position, and
467
+ * `noteText` placed near the page bottom with a separator artifact line.
468
+ */
469
+ footnote(refMark, noteText) {
470
+ native.pageBuilderFootnote(this.h(), refMark, noteText);
471
+ return this;
472
+ }
473
+ /**
474
+ * Lay out `text` as balanced multi-column flow.
475
+ * `columnCount` columns separated by `gapPt` points.
476
+ * Paragraphs in `text` are delimited by `"\n\n"`.
477
+ */
478
+ columns(columnCount, gapPt, text) {
479
+ native.pageBuilderColumns(this.h(), columnCount, gapPt, text);
480
+ return this;
481
+ }
482
+ /**
483
+ * Emit `text` inline at the current cursor position without advancing
484
+ * to a new line. The cursor advances horizontally so the next `inline`
485
+ * call follows on the same line.
486
+ */
487
+ inline(text) {
488
+ native.pageBuilderInline(this.h(), text);
489
+ return this;
490
+ }
491
+ /** Emit `text` inline in bold weight. */
492
+ inlineBold(text) {
493
+ native.pageBuilderInlineBold(this.h(), text);
494
+ return this;
495
+ }
496
+ /** Emit `text` inline in italic style. */
497
+ inlineItalic(text) {
498
+ native.pageBuilderInlineItalic(this.h(), text);
499
+ return this;
500
+ }
501
+ /** Emit `text` inline in an RGB colour (channels 0–1). */
502
+ inlineColor(r, g, b, text) {
503
+ native.pageBuilderInlineColor(this.h(), r, g, b, text);
504
+ return this;
505
+ }
506
+ /** Advance the cursor to the start of the next line. */
507
+ newline() {
508
+ native.pageBuilderNewline(this.h());
509
+ return this;
510
+ }
511
+ // --- Barcode / QR-code placement ------------------------------------
512
+ /**
513
+ * Place a 1-D barcode image on the page at `(x, y, w, h)`.
514
+ * `barcodeType`: 0=Code128 1=Code39 2=EAN13 3=EAN8 4=UPCA 5=ITF
515
+ * 6=Code93 7=Codabar.
516
+ */
517
+ barcode1d(barcodeType, data, x, y, w, h) {
518
+ native.pageBuilderBarcode1d(this.h(), barcodeType, data, x, y, w, h);
519
+ return this;
520
+ }
521
+ /** Place a QR-code image on the page (square: `size × size` pt). */
522
+ barcodeQr(data, x, y, size) {
523
+ native.pageBuilderBarcodeQr(this.h(), data, x, y, size);
524
+ return this;
525
+ }
526
+ /**
527
+ * Embed an image with an accessibility alt text (PDF/UA-1 §Figure).
528
+ * `bytes` must contain raw JPEG/PNG/WebP image data.
529
+ */
530
+ imageWithAlt(bytes, x, y, w, h, altText) {
531
+ native.pageBuilderImageWithAlt(this.h(), bytes, x, y, w, h, altText);
532
+ return this;
533
+ }
534
+ /**
535
+ * Embed a decorative image as an /Artifact (no alt text, PDF/UA-1 §Artifact).
536
+ * `bytes` must contain raw JPEG/PNG/WebP image data.
537
+ */
538
+ imageArtifact(bytes, x, y, w, h) {
539
+ native.pageBuilderImageArtifact(this.h(), bytes, x, y, w, h);
540
+ return this;
541
+ }
387
542
  // --- Low-level graphics primitives ---------------------------------
388
543
  /** Draw a stroked rectangle outline (1pt black). */
389
544
  rect(x, y, w, h) {
@@ -400,6 +555,136 @@ export class PageBuilder {
400
555
  native.pageBuilderLine(this.h(), x1, y1, x2, y2);
401
556
  return this;
402
557
  }
558
+ // --- v0.3.39 primitives (#393) -------------------------------------
559
+ /**
560
+ * Draw a stroked rectangle outline with caller-supplied width + RGB
561
+ * colour (channels 0–1). Underlies the Table surface.
562
+ */
563
+ strokeRect(x, y, w, h, style) {
564
+ const width = style?.width ?? 1;
565
+ const [r, g, b] = style?.color ?? [0, 0, 0];
566
+ native.pageBuilderStrokeRect(this.h(), x, y, w, h, width, r, g, b);
567
+ return this;
568
+ }
569
+ /**
570
+ * Draw a straight line with caller-supplied width + RGB colour.
571
+ */
572
+ strokeLine(x1, y1, x2, y2, style) {
573
+ const width = style?.width ?? 1;
574
+ const [r, g, b] = style?.color ?? [0, 0, 0];
575
+ native.pageBuilderStrokeLine(this.h(), x1, y1, x2, y2, width, r, g, b);
576
+ return this;
577
+ }
578
+ /**
579
+ * Place wrapped text inside the rectangle (x, y, w, h) with the
580
+ * given horizontal alignment. Uses the current font + size. Text
581
+ * that does not fit is clipped to the rectangle height.
582
+ */
583
+ textInRect(x, y, w, h, text, align = Align.Left) {
584
+ native.pageBuilderTextInRect(this.h(), x, y, w, h, text, align);
585
+ return this;
586
+ }
587
+ /**
588
+ * Start a new page with the *same* dimensions as the current one.
589
+ * Text config (font + size) carries over; the cursor resets to the
590
+ * top-left margin. Callers wanting header-repeat-on-break must
591
+ * re-emit the header explicitly.
592
+ */
593
+ newPageSameSize() {
594
+ native.pageBuilderNewPageSameSize(this.h());
595
+ return this;
596
+ }
597
+ /**
598
+ * Measure the width of `text` in the current font and size.
599
+ *
600
+ * Note: v0.3.39 ships a JS-side approximation (0.55em per glyph for
601
+ * ASCII / typical Latin-1 characters). A true per-glyph measurement
602
+ * FFI is pending. The result is in PDF points.
603
+ */
604
+ measure(text) {
605
+ // Retrieve current font size if the user tracked it via `font(...)`.
606
+ // We have no FFI query for the size; fall back to the 10pt default.
607
+ const size = this._lastFontSize ?? 10;
608
+ // ~0.55 em is a reasonable average for proportional fonts.
609
+ return text.length * size * 0.55;
610
+ }
611
+ /**
612
+ * Estimate the remaining vertical space on the page at the current
613
+ * cursor position. v0.3.39 returns `null` when the native cursor is
614
+ * unknown — callers should treat `null` as "unknown; assume fresh
615
+ * page". A real FFI hook is pending in a follow-up release.
616
+ */
617
+ remainingSpace() {
618
+ return null;
619
+ }
620
+ /**
621
+ * Emit a buffered table at the current cursor. All rows are
622
+ * marshalled into a single FFI call — memory scales with the row
623
+ * count. For million-row streams use {@link streamingTable}.
624
+ */
625
+ table(spec) {
626
+ const columns = spec.columns;
627
+ if (!columns || columns.length === 0) {
628
+ throw new Error('table spec must contain at least one column');
629
+ }
630
+ const widths = columns.map((c) => c.width);
631
+ const aligns = columns.map((c) => (c.align ?? Align.Left));
632
+ const hasHeader = spec.hasHeader !== false;
633
+ const rows = spec.rows ?? [];
634
+ const cells = [];
635
+ if (hasHeader) {
636
+ for (const c of columns)
637
+ cells.push(c.header);
638
+ }
639
+ const bodyRowCount = rows.length;
640
+ for (const row of rows) {
641
+ if (row.length !== columns.length) {
642
+ throw new Error(`row width ${row.length} does not match column count ${columns.length}`);
643
+ }
644
+ for (const cell of row)
645
+ cells.push(cell ?? null);
646
+ }
647
+ const totalRows = (hasHeader ? 1 : 0) + bodyRowCount;
648
+ native.pageBuilderTable(this.h(), widths, aligns, totalRows, cells, hasHeader);
649
+ return this;
650
+ }
651
+ /**
652
+ * Begin a streaming table. Uses the native row-at-a-time FFI
653
+ * (`pdf_page_builder_streaming_table_begin_v2`). Pass a `mode` in
654
+ * `config` to control column-sizing strategy (default: fixed widths).
655
+ */
656
+ streamingTable(config) {
657
+ return new StreamingTable(this, config);
658
+ }
659
+ /** @internal — open streaming table FFI handle on this page. */
660
+ _streamingTableBeginV2(headers, widths, aligns, repeatHeader, mode, maxRowspan) {
661
+ let modeInt = 0;
662
+ let sampleRows = 20;
663
+ let minW = 0;
664
+ let maxW = 9999;
665
+ if (mode?.kind === 'sample') {
666
+ modeInt = 1;
667
+ if (mode.sampleRows != null)
668
+ sampleRows = mode.sampleRows;
669
+ if (mode.minColWidthPt != null)
670
+ minW = mode.minColWidthPt;
671
+ if (mode.maxColWidthPt != null)
672
+ maxW = mode.maxColWidthPt;
673
+ }
674
+ native.pageBuilderStreamingTableBeginV2(this.h(), headers, widths, aligns, repeatHeader, modeInt, sampleRows, minW, maxW, maxRowspan);
675
+ }
676
+ /** @internal — push one row into the open streaming table (all rowspan=1). */
677
+ _streamingTablePushRow(cells) {
678
+ native.pageBuilderStreamingTablePushRow(this.h(), cells);
679
+ }
680
+ /** @internal — push one row with per-cell rowspan values. */
681
+ _streamingTablePushRowV2(cells) {
682
+ native.pageBuilderStreamingTablePushRowV2(this.h(), cells);
683
+ }
684
+ /** @internal — close the open streaming table. */
685
+ _streamingTableFinish() {
686
+ native.pageBuilderStreamingTableFinish(this.h());
687
+ }
403
688
  /**
404
689
  * Commit the page's buffered operations to the parent builder and
405
690
  * return the parent for chaining. After `done()` this PageBuilder is
@@ -432,3 +717,8 @@ export class PageBuilder {
432
717
  this.close();
433
718
  }
434
719
  }
720
+ export { StreamingTable } from './streaming-table.js';
721
+ // Re-export the v0.3.39 table surface so users can `import { Align,
722
+ // StreamingTable } from 'pdf-oxide'` without reaching into ./types or
723
+ // ./builders/streaming-table.
724
+ export { Align, };
@@ -10,3 +10,4 @@ export * from './document-builder';
10
10
  export * from './metadata-builder';
11
11
  export * from './pdf-builder';
12
12
  export * from './search-options-builder';
13
+ export * from './streaming-table';
@@ -10,3 +10,4 @@ export * from './document-builder.js';
10
10
  export * from './metadata-builder.js';
11
11
  export * from './pdf-builder.js';
12
12
  export * from './search-options-builder.js';
13
+ export * from './streaming-table.js';
@@ -156,6 +156,8 @@ export declare class PdfBuilder {
156
156
  * ```
157
157
  */
158
158
  fromText(text: string): any;
159
+ fromHtmlCss(html: string, css: string, fontBytes: Buffer | Uint8Array): any;
160
+ fromHtmlCssWithFonts(html: string, css: string, families: string[], fonts: (Buffer | Uint8Array)[]): any;
159
161
  /**
160
162
  * Asynchronously creates a PDF document from Markdown content
161
163
  * @param markdown - Markdown formatted content
@@ -231,6 +231,18 @@ export class PdfBuilder {
231
231
  this._applyConfiguration(pdf);
232
232
  return pdf;
233
233
  }
234
+ fromHtmlCss(html, css, fontBytes) {
235
+ const { Pdf } = require('../index.js');
236
+ const pdf = Pdf.fromHtmlCss(html, css, fontBytes);
237
+ this._applyConfiguration(pdf);
238
+ return pdf;
239
+ }
240
+ fromHtmlCssWithFonts(html, css, families, fonts) {
241
+ const { Pdf } = require('../index.js');
242
+ const pdf = Pdf.fromHtmlCssWithFonts(html, css, families, fonts);
243
+ this._applyConfiguration(pdf);
244
+ return pdf;
245
+ }
234
246
  /**
235
247
  * Asynchronously creates a PDF document from Markdown content
236
248
  * @param markdown - Markdown formatted content
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Streaming-table adapter backed by the native row-at-a-time FFI
3
+ * (`pdf_page_builder_streaming_table_begin_v2` / `_push_row` / `_finish`).
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * const t = page.streamingTable({
8
+ * columns: [
9
+ * { header: 'SKU', width: 72 },
10
+ * { header: 'Item', width: 200 },
11
+ * { header: 'Qty', width: 48, align: Align.Right },
12
+ * ],
13
+ * repeatHeader: true,
14
+ * mode: { kind: 'sample', sampleRows: 30 },
15
+ * });
16
+ * for await (const row of readRowsFromDb()) {
17
+ * t.pushRow([row.sku, row.item, String(row.qty)]);
18
+ * }
19
+ * await t.finish();
20
+ * ```
21
+ */
22
+ import type { SpanCell, StreamingTableConfig } from '../types/common.js';
23
+ import type { PageBuilder } from './document-builder.js';
24
+ export declare class StreamingTable {
25
+ private _page;
26
+ private _columns;
27
+ private _opened;
28
+ private _finished;
29
+ /** @internal — constructed via `PageBuilder.streamingTable(...)`. */
30
+ constructor(page: PageBuilder, config: StreamingTableConfig);
31
+ /** Push a single row (all rowspan=1). Throws if `cells.length !== columns.length`. */
32
+ pushRow(cells: Array<string | null | undefined>): this;
33
+ /**
34
+ * Push a single row with per-cell rowspan values. Each element is either
35
+ * a `SpanCell` (`{ text, rowspan }`) or a plain string (rowspan=1).
36
+ * Requires `maxRowspan ≥ 2` in the `StreamingTableConfig`.
37
+ */
38
+ pushRowSpan(cells: Array<SpanCell | string | null | undefined>): this;
39
+ /**
40
+ * Convenience: consume a sync or async iterable and push each row.
41
+ */
42
+ pushAll(rows: Iterable<Array<string | null | undefined>> | AsyncIterable<Array<string | null | undefined>>): Promise<this>;
43
+ /**
44
+ * Close the streaming table and return the parent PageBuilder for chaining.
45
+ */
46
+ finish(): Promise<PageBuilder>;
47
+ /** Number of the columns this table was opened with. */
48
+ get columnCount(): number;
49
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Streaming-table adapter backed by the native row-at-a-time FFI
3
+ * (`pdf_page_builder_streaming_table_begin_v2` / `_push_row` / `_finish`).
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * const t = page.streamingTable({
8
+ * columns: [
9
+ * { header: 'SKU', width: 72 },
10
+ * { header: 'Item', width: 200 },
11
+ * { header: 'Qty', width: 48, align: Align.Right },
12
+ * ],
13
+ * repeatHeader: true,
14
+ * mode: { kind: 'sample', sampleRows: 30 },
15
+ * });
16
+ * for await (const row of readRowsFromDb()) {
17
+ * t.pushRow([row.sku, row.item, String(row.qty)]);
18
+ * }
19
+ * await t.finish();
20
+ * ```
21
+ */
22
+ export class StreamingTable {
23
+ /** @internal — constructed via `PageBuilder.streamingTable(...)`. */
24
+ constructor(page, config) {
25
+ this._opened = false;
26
+ this._finished = false;
27
+ if (!config || !Array.isArray(config.columns) || config.columns.length === 0) {
28
+ throw new Error('StreamingTable requires at least one column');
29
+ }
30
+ this._page = page;
31
+ this._columns = config.columns;
32
+ const headers = config.columns.map((c) => c.header ?? '');
33
+ const widths = config.columns.map((c) => c.width);
34
+ const aligns = config.columns.map((c) => (c.align ?? 0));
35
+ const repeat = config.repeatHeader !== false;
36
+ const maxRowspan = config.maxRowspan != null && config.maxRowspan >= 2 ? config.maxRowspan : 1;
37
+ this._page._streamingTableBeginV2(headers, widths, aligns, repeat, config.mode, maxRowspan);
38
+ this._opened = true;
39
+ }
40
+ /** Push a single row (all rowspan=1). Throws if `cells.length !== columns.length`. */
41
+ pushRow(cells) {
42
+ if (this._finished) {
43
+ throw new Error('StreamingTable already finished');
44
+ }
45
+ if (cells.length !== this._columns.length) {
46
+ throw new Error(`row width ${cells.length} does not match column count ${this._columns.length}`);
47
+ }
48
+ this._page._streamingTablePushRow(cells.map((c) => (c == null ? null : String(c))));
49
+ return this;
50
+ }
51
+ /**
52
+ * Push a single row with per-cell rowspan values. Each element is either
53
+ * a `SpanCell` (`{ text, rowspan }`) or a plain string (rowspan=1).
54
+ * Requires `maxRowspan ≥ 2` in the `StreamingTableConfig`.
55
+ */
56
+ pushRowSpan(cells) {
57
+ if (this._finished) {
58
+ throw new Error('StreamingTable already finished');
59
+ }
60
+ if (cells.length !== this._columns.length) {
61
+ throw new Error(`row width ${cells.length} does not match column count ${this._columns.length}`);
62
+ }
63
+ const normalized = cells.map((c) => {
64
+ if (c == null)
65
+ return [null, 1];
66
+ if (typeof c === 'string')
67
+ return [c, 1];
68
+ return [c.text, c.rowspan];
69
+ });
70
+ this._page._streamingTablePushRowV2(normalized);
71
+ return this;
72
+ }
73
+ /**
74
+ * Convenience: consume a sync or async iterable and push each row.
75
+ */
76
+ async pushAll(rows) {
77
+ if (this._finished) {
78
+ throw new Error('StreamingTable already finished');
79
+ }
80
+ const anyRows = rows;
81
+ if (typeof anyRows[Symbol.asyncIterator] === 'function') {
82
+ for await (const row of rows) {
83
+ this.pushRow(row);
84
+ }
85
+ }
86
+ else {
87
+ for (const row of rows) {
88
+ this.pushRow(row);
89
+ }
90
+ }
91
+ return this;
92
+ }
93
+ /**
94
+ * Close the streaming table and return the parent PageBuilder for chaining.
95
+ */
96
+ async finish() {
97
+ if (this._finished) {
98
+ throw new Error('StreamingTable already finished');
99
+ }
100
+ this._finished = true;
101
+ if (this._opened) {
102
+ this._page._streamingTableFinish();
103
+ }
104
+ return this._page;
105
+ }
106
+ /** Number of the columns this table was opened with. */
107
+ get columnCount() {
108
+ return this._columns.length;
109
+ }
110
+ }
@@ -14,6 +14,8 @@ export declare class DocumentEditor {
14
14
  private constructor();
15
15
  /** Open a PDF file for editing. */
16
16
  static open(path: string): DocumentEditor;
17
+ /** Open a PDF from an in-memory buffer for editing. */
18
+ static openFromBytes(data: Buffer | Uint8Array): DocumentEditor;
17
19
  /** True if the editor has been closed. Subsequent calls will throw. */
18
20
  get closed(): boolean;
19
21
  private _throwIfClosed;
@@ -23,6 +25,9 @@ export declare class DocumentEditor {
23
25
  isModified(): boolean;
24
26
  setTitle(title: string): void;
25
27
  setAuthor(author: string): void;
28
+ setSubject(subject: string): void;
29
+ getKeywords(): string | null;
30
+ setKeywords(keywords: string): void;
26
31
  getProducer(): string;
27
32
  setProducer(producer: string): void;
28
33
  getCreationDate(): string;
@@ -39,6 +44,12 @@ export declare class DocumentEditor {
39
44
  mergeFrom(sourcePath: string): void;
40
45
  /** Flatten form fields across the entire document. */
41
46
  flattenForms(): void;
47
+ /**
48
+ * Return warnings collected during the last form-flattening save.
49
+ * Each entry names a widget field that had no `/AP` appearance stream;
50
+ * flattening it produces a blank rectangle.
51
+ */
52
+ flattenWarnings(): string[];
42
53
  /** Flatten annotations. If `pageIndex` is omitted, flattens all pages. */
43
54
  flattenAnnotations(pageIndex?: number): void;
44
55
  /** Set a form field value by fully-qualified field name. */
@@ -47,10 +58,64 @@ export declare class DocumentEditor {
47
58
  importFdfBytes(fdf: Buffer | Uint8Array): void;
48
59
  /** Import an XFDF file (bytes) into the document's form. */
49
60
  importXfdfBytes(xfdf: Buffer | Uint8Array): void;
61
+ /**
62
+ * Append every page of another PDF (supplied as bytes) to this document.
63
+ * Returns the number of pages added.
64
+ */
65
+ mergeFromBytes(data: Buffer | Uint8Array): number;
66
+ /** Embed a file attachment into the document. */
67
+ embedFile(name: string, data: Buffer | Uint8Array): void;
68
+ /** Burn in redaction annotations on a single page (zero-based). */
69
+ applyPageRedactions(pageIndex: number): void;
70
+ /** Burn in all pending redaction annotations across the document. */
71
+ applyAllRedactions(): void;
72
+ /** Rotate all pages by `degrees` (additive). */
73
+ rotateAllPages(degrees: number): void;
74
+ /** Rotate a single page by `degrees` (additive). */
75
+ rotatePageBy(pageIndex: number, degrees: number): void;
76
+ /** Get the MediaBox of a page as `{x, y, width, height}`. */
77
+ getPageMediaBox(pageIndex: number): {
78
+ x: number;
79
+ y: number;
80
+ width: number;
81
+ height: number;
82
+ };
83
+ /** Set the MediaBox of a page. */
84
+ setPageMediaBox(pageIndex: number, x: number, y: number, width: number, height: number): void;
85
+ /** Get the CropBox of a page. Returns `{x:0,y:0,width:0,height:0}` if none set. */
86
+ getPageCropBox(pageIndex: number): {
87
+ x: number;
88
+ y: number;
89
+ width: number;
90
+ height: number;
91
+ };
92
+ /** Set the CropBox of a page. */
93
+ setPageCropBox(pageIndex: number, x: number, y: number, width: number, height: number): void;
94
+ /**
95
+ * Erase rectangular regions on a page.
96
+ * `rects` is an array of `[x, y, w, h]` tuples.
97
+ */
98
+ eraseRegions(pageIndex: number, rects: [number, number, number, number][]): void;
99
+ /** Clear all pending erase-region entries for a page. */
100
+ clearEraseRegions(pageIndex: number): void;
101
+ /** Flatten form fields on a single page. */
102
+ flattenFormsOnPage(pageIndex: number): void;
103
+ /** True if the page is marked for annotation-flatten. */
104
+ isPageMarkedForFlatten(pageIndex: number): boolean;
105
+ /** Remove the flatten mark from a page. */
106
+ unmarkPageForFlatten(pageIndex: number): void;
107
+ /** True if the page is marked for redaction. */
108
+ isPageMarkedForRedaction(pageIndex: number): boolean;
109
+ /** Remove the redaction mark from a page. */
110
+ unmarkPageForRedaction(pageIndex: number): void;
50
111
  /** Save the document to `path`. */
51
112
  save(path: string): void;
52
113
  /** Save with AES-256 encryption (user + owner passwords). */
53
114
  saveEncrypted(path: string, userPassword: string, ownerPassword: string): void;
115
+ /** Save the document to an in-memory Buffer. */
116
+ saveToBytes(): Buffer;
117
+ /** Save to an in-memory Buffer with explicit compression / GC / linearize flags. */
118
+ saveToBytesWithOptions(compress: boolean, garbageCollect: boolean, linearize: boolean): Buffer;
54
119
  /** Release the native handle. Safe to call multiple times. */
55
120
  close(): void;
56
121
  }
@@ -48,6 +48,14 @@ export class DocumentEditor {
48
48
  const handle = native.editorOpen(path);
49
49
  return new DocumentEditor(handle);
50
50
  }
51
+ /** Open a PDF from an in-memory buffer for editing. */
52
+ static openFromBytes(data) {
53
+ if (!data || data.length === 0) {
54
+ throw new TypeError('data must be a non-empty Buffer or Uint8Array');
55
+ }
56
+ const handle = native.editorOpenFromBytes(data);
57
+ return new DocumentEditor(handle);
58
+ }
51
59
  /** True if the editor has been closed. Subsequent calls will throw. */
52
60
  get closed() {
53
61
  return this._closed;
@@ -75,18 +83,25 @@ export class DocumentEditor {
75
83
  this._throwIfClosed();
76
84
  native.editorSetAuthor(this._handle, author);
77
85
  }
86
+ setSubject(subject) {
87
+ this._throwIfClosed();
88
+ native.editorSetSubject(this._handle, subject);
89
+ }
90
+ getKeywords() {
91
+ this._throwIfClosed();
92
+ return native.editorGetKeywords(this._handle);
93
+ }
94
+ setKeywords(keywords) {
95
+ this._throwIfClosed();
96
+ native.editorSetKeywords(this._handle, keywords);
97
+ }
78
98
  getProducer() {
79
99
  this._throwIfClosed();
80
100
  return native.editorGetProducer(this._handle);
81
101
  }
82
102
  setProducer(producer) {
83
103
  this._throwIfClosed();
84
- // NOTE: today this is a no-op in Rust core (src/ffi.rs:532-586).
85
- // The wrapper is in place so the API surface matches
86
- // Python / C# / Go.
87
- if (native.editorSetProducer) {
88
- native.editorSetProducer(this._handle, producer);
89
- }
104
+ native.editorSetProducer(this._handle, producer);
90
105
  }
91
106
  getCreationDate() {
92
107
  this._throwIfClosed();
@@ -94,10 +109,7 @@ export class DocumentEditor {
94
109
  }
95
110
  setCreationDate(date) {
96
111
  this._throwIfClosed();
97
- // Same Rust-core stub note as setProducer.
98
- if (native.editorSetCreationDate) {
99
- native.editorSetCreationDate(this._handle, date);
100
- }
112
+ native.editorSetCreationDate(this._handle, date);
101
113
  }
102
114
  // ----- page mutations ---------------------------------------------
103
115
  /** Delete the page at `pageIndex` (zero-based). */
@@ -131,6 +143,15 @@ export class DocumentEditor {
131
143
  this._throwIfClosed();
132
144
  native.editorFlattenForms(this._handle);
133
145
  }
146
+ /**
147
+ * Return warnings collected during the last form-flattening save.
148
+ * Each entry names a widget field that had no `/AP` appearance stream;
149
+ * flattening it produces a blank rectangle.
150
+ */
151
+ flattenWarnings() {
152
+ this._throwIfClosed();
153
+ return native.editorFlattenWarnings(this._handle);
154
+ }
134
155
  /** Flatten annotations. If `pageIndex` is omitted, flattens all pages. */
135
156
  flattenAnnotations(pageIndex) {
136
157
  this._throwIfClosed();
@@ -156,6 +177,104 @@ export class DocumentEditor {
156
177
  this._throwIfClosed();
157
178
  native.editorImportXfdfBytes(this._handle, xfdf);
158
179
  }
180
+ // ----- byte-level merge / embed -----------------------------------
181
+ /**
182
+ * Append every page of another PDF (supplied as bytes) to this document.
183
+ * Returns the number of pages added.
184
+ */
185
+ mergeFromBytes(data) {
186
+ this._throwIfClosed();
187
+ return native.editorMergeFromBytes(this._handle, data);
188
+ }
189
+ /** Embed a file attachment into the document. */
190
+ embedFile(name, data) {
191
+ this._throwIfClosed();
192
+ native.editorEmbedFile(this._handle, name, data);
193
+ }
194
+ // ----- redactions -------------------------------------------------
195
+ /** Burn in redaction annotations on a single page (zero-based). */
196
+ applyPageRedactions(pageIndex) {
197
+ this._throwIfClosed();
198
+ native.editorApplyPageRedactions(this._handle, pageIndex);
199
+ }
200
+ /** Burn in all pending redaction annotations across the document. */
201
+ applyAllRedactions() {
202
+ this._throwIfClosed();
203
+ native.editorApplyAllRedactions(this._handle);
204
+ }
205
+ // ----- rotation (additive) ----------------------------------------
206
+ /** Rotate all pages by `degrees` (additive). */
207
+ rotateAllPages(degrees) {
208
+ this._throwIfClosed();
209
+ native.editorRotateAllPages(this._handle, degrees);
210
+ }
211
+ /** Rotate a single page by `degrees` (additive). */
212
+ rotatePageBy(pageIndex, degrees) {
213
+ this._throwIfClosed();
214
+ native.editorRotatePageBy(this._handle, pageIndex, degrees);
215
+ }
216
+ // ----- page boxes -------------------------------------------------
217
+ /** Get the MediaBox of a page as `{x, y, width, height}`. */
218
+ getPageMediaBox(pageIndex) {
219
+ this._throwIfClosed();
220
+ return native.editorGetPageMediaBox(this._handle, pageIndex);
221
+ }
222
+ /** Set the MediaBox of a page. */
223
+ setPageMediaBox(pageIndex, x, y, width, height) {
224
+ this._throwIfClosed();
225
+ native.editorSetPageMediaBox(this._handle, pageIndex, x, y, width, height);
226
+ }
227
+ /** Get the CropBox of a page. Returns `{x:0,y:0,width:0,height:0}` if none set. */
228
+ getPageCropBox(pageIndex) {
229
+ this._throwIfClosed();
230
+ return native.editorGetPageCropBox(this._handle, pageIndex);
231
+ }
232
+ /** Set the CropBox of a page. */
233
+ setPageCropBox(pageIndex, x, y, width, height) {
234
+ this._throwIfClosed();
235
+ native.editorSetPageCropBox(this._handle, pageIndex, x, y, width, height);
236
+ }
237
+ // ----- erase regions ----------------------------------------------
238
+ /**
239
+ * Erase rectangular regions on a page.
240
+ * `rects` is an array of `[x, y, w, h]` tuples.
241
+ */
242
+ eraseRegions(pageIndex, rects) {
243
+ this._throwIfClosed();
244
+ native.editorEraseRegions(this._handle, pageIndex, rects);
245
+ }
246
+ /** Clear all pending erase-region entries for a page. */
247
+ clearEraseRegions(pageIndex) {
248
+ this._throwIfClosed();
249
+ native.editorClearEraseRegions(this._handle, pageIndex);
250
+ }
251
+ // ----- form flattening on single page ------------------------------
252
+ /** Flatten form fields on a single page. */
253
+ flattenFormsOnPage(pageIndex) {
254
+ this._throwIfClosed();
255
+ native.editorFlattenFormsOnPage(this._handle, pageIndex);
256
+ }
257
+ // ----- page-mark state queries ------------------------------------
258
+ /** True if the page is marked for annotation-flatten. */
259
+ isPageMarkedForFlatten(pageIndex) {
260
+ this._throwIfClosed();
261
+ return native.editorIsPageMarkedForFlatten(this._handle, pageIndex);
262
+ }
263
+ /** Remove the flatten mark from a page. */
264
+ unmarkPageForFlatten(pageIndex) {
265
+ this._throwIfClosed();
266
+ native.editorUnmarkPageForFlatten(this._handle, pageIndex);
267
+ }
268
+ /** True if the page is marked for redaction. */
269
+ isPageMarkedForRedaction(pageIndex) {
270
+ this._throwIfClosed();
271
+ return native.editorIsPageMarkedForRedaction(this._handle, pageIndex);
272
+ }
273
+ /** Remove the redaction mark from a page. */
274
+ unmarkPageForRedaction(pageIndex) {
275
+ this._throwIfClosed();
276
+ native.editorUnmarkPageForRedaction(this._handle, pageIndex);
277
+ }
159
278
  // ----- save paths -------------------------------------------------
160
279
  /** Save the document to `path`. */
161
280
  save(path) {
@@ -170,6 +289,16 @@ export class DocumentEditor {
170
289
  this._throwIfClosed();
171
290
  native.editorSaveEncrypted(this._handle, path, userPassword, ownerPassword);
172
291
  }
292
+ /** Save the document to an in-memory Buffer. */
293
+ saveToBytes() {
294
+ this._throwIfClosed();
295
+ return native.editorSaveToBytes(this._handle);
296
+ }
297
+ /** Save to an in-memory Buffer with explicit compression / GC / linearize flags. */
298
+ saveToBytesWithOptions(compress, garbageCollect, linearize) {
299
+ this._throwIfClosed();
300
+ return native.editorSaveToBytesWithOptions(this._handle, compress, garbageCollect, linearize);
301
+ }
173
302
  // ----- lifecycle --------------------------------------------------
174
303
  /** Release the native handle. Safe to call multiple times. */
175
304
  close() {
package/lib/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { AnnotationBuilder, ConversionOptionsBuilder, DocumentBuilder, EmbeddedFont, MetadataBuilder, PageBuilder, PdfBuilder, SearchOptionsBuilder } from './builders/index';
1
+ import { Align, AnnotationBuilder, ConversionOptionsBuilder, DocumentBuilder, EmbeddedFont, MetadataBuilder, PageBuilder, PdfBuilder, SearchOptionsBuilder, StreamingTable } from './builders/index';
2
2
  import { DocumentEditor } from './document-editor';
3
3
  import { AccessibilityException, CertificateLoadFailed, ComplianceException, EncryptionException, ErrorCategory, ErrorSeverity, InvalidStateException, IoException, mapFfiErrorCode, OcrException, OptimizationException, ParseException, PdfException, RedactionException, RenderingException, SearchException, SignatureException, SigningFailed, UnknownError, UnsupportedFeatureException, ValidationException, wrapAsyncMethod, wrapError, wrapMethod } from './errors';
4
4
  import { AnnotationManager, type BatchDocument, BatchManager, type BatchOptions, type BatchProgress, type BatchResult, type BatchStatistics, createExtractionStream, createMetadataStream, createSearchStream, ExtractionManager, ExtractionStream, LayerManager, MetadataManager, MetadataStream, OutlineManager, RenderingManager, SearchManager, SearchStream, SecurityManager } from './managers/index';
5
- import type { Table } from './types/common.js';
5
+ import type { Column, SpanCell, StreamingTableConfig, Table, TableMode, TableSpec } from './types/common.js';
6
6
  import type { WorkerResult, WorkerTask } from './workers/index';
7
7
  import { WorkerPool, workerPool } from './workers/index';
8
8
  declare const OcrManager: any, OCRManager: any, OCRDetectionMode: any, ComplianceManager: any, PdfALevel: any, PdfXLevel: any, PdfUALevel: any, ComplianceIssueType: any, IssueSeverity: any, SignatureManager: any, SignatureAlgorithm: any, DigestAlgorithm: any, BarcodeManager: any, BarcodeFormat: any, BarcodeErrorCorrection: any, FormFieldManager: any, FormFieldType: any, FieldVisibility: any, ResultAccessorsManager: any, SearchResultProperties: any, FontProperties: any, ImageProperties: any, AnnotationProperties: any, ThumbnailManager: any, ThumbnailSize: any, ImageFormat: any, HybridMLManager: any, PageComplexity: any, ContentType: any, XfaManager: any, XfaFormType: any, XfaFieldType: any, CacheManager: any, EditingManager: any, AccessibilityManager: any, OptimizationManager: any, EnterpriseManager: any;
@@ -122,5 +122,5 @@ declare const SearchResult: any;
122
122
  declare const TextSearcher: any;
123
123
  export { Timestamp, TimestampHashAlgorithm } from './timestamp.js';
124
124
  export { TsaClient, type TsaClientOptions } from './tsa-client.js';
125
- export type { BatchDocument, BatchOptions, BatchProgress, BatchResult, BatchStatistics, Table, WorkerResult, WorkerTask, };
126
- export { AccessibilityException, AccessibilityManager, AnnotationBuilder, AnnotationManager, AnnotationProperties, BarcodeErrorCorrection, BarcodeFormat, BarcodeManager, BatchManager, CacheManager, CertificateLoadFailed, Color, ComplianceException, ComplianceIssueType, ComplianceManager, ContentType, ConversionOptions, ConversionOptionsBuilder, createExtractionStream, createMetadataStream, createSearchStream, DigestAlgorithm, DocumentBuilder, DocumentEditor, EditingManager, EmbeddedFont, EncryptionException, EnterpriseManager, ErrorCategory, ErrorSeverity, ExtractionManager, ExtractionStream, FieldVisibility, FontProperties, FormFieldManager, FormFieldType, getPdfOxideVersion, getVersion, HybridMLManager, ImageFormat, ImageProperties, InvalidStateException, IoException, IssueSeverity, LayerManager, MetadataBuilder, MetadataManager, MetadataStream, mapFfiErrorCode, OCRDetectionMode, OCRLanguage, OCRManager, OcrException, OcrManager, OptimizationException, OptimizationManager, OutlineManager, Page, PageBuilder, PageComplexity, PageSize, ParseException, Pdf, PdfALevel, PdfBuilder, PdfDocument, PdfError, PdfException, PdfUALevel, PdfXLevel, Point, Rect, RedactionException, RenderingException, RenderingManager, ResultAccessorsManager, SearchException, SearchManager, SearchOptions, SearchOptionsBuilder, SearchResult, SearchResultProperties, SearchStream, SecurityManager, SignatureAlgorithm, SignatureException, SignatureManager, SigningFailed, TextSearcher, ThumbnailManager, ThumbnailSize, UnknownError, UnsupportedFeatureException, ValidationException, WorkerPool, workerPool, wrapAsyncMethod, wrapError, wrapMethod, XfaFieldType, XfaFormType, XfaManager, };
125
+ export type { BatchDocument, BatchOptions, BatchProgress, BatchResult, BatchStatistics, Column, SpanCell, StreamingTableConfig, Table, TableMode, TableSpec, WorkerResult, WorkerTask, };
126
+ export { AccessibilityException, AccessibilityManager, Align, AnnotationBuilder, AnnotationManager, AnnotationProperties, BarcodeErrorCorrection, BarcodeFormat, BarcodeManager, BatchManager, CacheManager, CertificateLoadFailed, Color, ComplianceException, ComplianceIssueType, ComplianceManager, ContentType, ConversionOptions, ConversionOptionsBuilder, createExtractionStream, createMetadataStream, createSearchStream, DigestAlgorithm, DocumentBuilder, DocumentEditor, EditingManager, EmbeddedFont, EncryptionException, EnterpriseManager, ErrorCategory, ErrorSeverity, ExtractionManager, ExtractionStream, FieldVisibility, FontProperties, FormFieldManager, FormFieldType, getPdfOxideVersion, getVersion, HybridMLManager, ImageFormat, ImageProperties, InvalidStateException, IoException, IssueSeverity, LayerManager, MetadataBuilder, MetadataManager, MetadataStream, mapFfiErrorCode, OCRDetectionMode, OCRLanguage, OCRManager, OcrException, OcrManager, OptimizationException, OptimizationManager, OutlineManager, Page, PageBuilder, PageComplexity, PageSize, ParseException, Pdf, PdfALevel, PdfBuilder, PdfDocument, PdfError, PdfException, PdfUALevel, PdfXLevel, Point, Rect, RedactionException, RenderingException, RenderingManager, ResultAccessorsManager, SearchException, SearchManager, SearchOptions, SearchOptionsBuilder, SearchResult, SearchResultProperties, SearchStream, SecurityManager, SignatureAlgorithm, SignatureException, SignatureManager, SigningFailed, StreamingTable, TextSearcher, ThumbnailManager, ThumbnailSize, UnknownError, UnsupportedFeatureException, ValidationException, WorkerPool, workerPool, wrapAsyncMethod, wrapError, wrapMethod, XfaFieldType, XfaFormType, XfaManager, };
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from 'node:module';
3
3
  import { dirname } from 'node:path';
4
4
  import { arch, platform } from 'node:process';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { AnnotationBuilder, ConversionOptionsBuilder, DocumentBuilder, EmbeddedFont, MetadataBuilder, PageBuilder, PdfBuilder, SearchOptionsBuilder, } from './builders/index.js';
6
+ import { Align, AnnotationBuilder, ConversionOptionsBuilder, DocumentBuilder, EmbeddedFont, MetadataBuilder, PageBuilder, PdfBuilder, SearchOptionsBuilder, StreamingTable, } from './builders/index.js';
7
7
  import { DocumentEditor } from './document-editor.js';
8
8
  import { AccessibilityException, CertificateLoadFailed, ComplianceException, EncryptionException, ErrorCategory, ErrorSeverity, InvalidStateException, IoException, mapFfiErrorCode, OcrException, OptimizationException, ParseException, PdfException, RedactionException, RenderingException, SearchException, SignatureException, SigningFailed, UnknownError, UnsupportedFeatureException, ValidationException, wrapAsyncMethod, wrapError, wrapMethod, } from './errors.js';
9
9
  import { AnnotationManager, BatchManager, createExtractionStream, createMetadataStream, createSearchStream, ExtractionManager, ExtractionStream, LayerManager, MetadataManager, MetadataStream, OutlineManager, RenderingManager, SearchManager, SearchStream, SecurityManager, } from './managers/index.js';
@@ -429,6 +429,15 @@ class PdfImpl {
429
429
  static fromImageBytes(data) {
430
430
  return new PdfImpl(native.pdfFromImageBytes(data));
431
431
  }
432
+ static fromHtmlCss(html, css, fontBytes) {
433
+ return new PdfImpl(native.pdfFromHtmlCss(html, css, fontBytes));
434
+ }
435
+ static fromHtmlCssWithFonts(html, css, families, fonts) {
436
+ if (families.length !== fonts.length) {
437
+ throw new Error(`fromHtmlCssWithFonts: families.length (${families.length}) must equal fonts.length (${fonts.length})`);
438
+ }
439
+ return new PdfImpl(native.pdfFromHtmlCssWithFonts(html, css, families, fonts));
440
+ }
432
441
  ensureOpen() {
433
442
  if (this._closed)
434
443
  throw new Error('PDF handle is closed');
@@ -473,7 +482,9 @@ const TextSearcher = native.TextSearcher;
473
482
  // their own modules so downstream users get the full API surface.
474
483
  export { Timestamp, TimestampHashAlgorithm } from './timestamp.js';
475
484
  export { TsaClient } from './tsa-client.js';
476
- export { AccessibilityException, AccessibilityManager, AnnotationBuilder, AnnotationManager, AnnotationProperties, BarcodeErrorCorrection, BarcodeFormat, BarcodeManager,
485
+ export { AccessibilityException, AccessibilityManager,
486
+ // v0.3.39 — DocumentBuilder tables (#393)
487
+ Align, AnnotationBuilder, AnnotationManager, AnnotationProperties, BarcodeErrorCorrection, BarcodeFormat, BarcodeManager,
477
488
  // Phase 2.5: Batch Processing API
478
489
  BatchManager, CacheManager, CertificateLoadFailed, Color, ComplianceException, ComplianceIssueType, ComplianceManager, ContentType, ConversionOptions, ConversionOptionsBuilder, createExtractionStream, createMetadataStream, createSearchStream, DigestAlgorithm,
479
490
  // Write-side fluent API
@@ -498,6 +509,8 @@ PdfDocument,
498
509
  PdfError, PdfException, PdfUALevel, PdfXLevel, Point, Rect, RedactionException, RenderingException, RenderingManager, ResultAccessorsManager, SearchException, SearchManager, SearchOptions, SearchOptionsBuilder, SearchResult, SearchResultProperties,
499
510
  // Phase 2.4: Stream API
500
511
  SearchStream, SecurityManager, SignatureAlgorithm, SignatureException, SignatureManager, SigningFailed,
512
+ // v0.3.39 — managed streaming-table adapter (#393)
513
+ StreamingTable,
501
514
  // Utilities
502
515
  TextSearcher, ThumbnailManager, ThumbnailSize, UnknownError, UnsupportedFeatureException, ValidationException,
503
516
  // Worker Threads API
@@ -718,6 +718,20 @@ export declare class SignatureManager extends EventEmitter {
718
718
  clearCache(): void;
719
719
  getCacheStats(): Record<string, any>;
720
720
  destroy(): void;
721
+ /**
722
+ * Sign a PDF from raw bytes using PEM credentials.
723
+ *
724
+ * Calls the native `signPdfBytes` FFI function (two-pass ByteRange writer).
725
+ * Credentials are loaded and freed within this call.
726
+ *
727
+ * @param pdfData - Buffer containing the PDF document bytes
728
+ * @param certPem - PEM-encoded certificate string
729
+ * @param keyPem - PEM-encoded private key string
730
+ * @param reason - Optional signature reason
731
+ * @param location - Optional signature location
732
+ * @returns Buffer containing the signed PDF
733
+ */
734
+ signPdfData(pdfData: Buffer, certPem: string, keyPem: string, reason?: string, location?: string): Promise<Buffer>;
721
735
  private setCached;
722
736
  private clearCachePattern;
723
737
  }
@@ -1454,6 +1454,42 @@ export class SignatureManager extends EventEmitter {
1454
1454
  this.resultCache.clear();
1455
1455
  this.removeAllListeners();
1456
1456
  }
1457
+ /**
1458
+ * Sign a PDF from raw bytes using PEM credentials.
1459
+ *
1460
+ * Calls the native `signPdfBytes` FFI function (two-pass ByteRange writer).
1461
+ * Credentials are loaded and freed within this call.
1462
+ *
1463
+ * @param pdfData - Buffer containing the PDF document bytes
1464
+ * @param certPem - PEM-encoded certificate string
1465
+ * @param keyPem - PEM-encoded private key string
1466
+ * @param reason - Optional signature reason
1467
+ * @param location - Optional signature location
1468
+ * @returns Buffer containing the signed PDF
1469
+ */
1470
+ async signPdfData(pdfData, certPem, keyPem, reason, location) {
1471
+ if (!this.native?.certificateLoadFromPem) {
1472
+ throw new SignatureException('Native signing not available: certificateLoadFromPem not found');
1473
+ }
1474
+ if (!this.native?.signPdfBytes) {
1475
+ throw new SignatureException('Native signing not available: signPdfBytes not found');
1476
+ }
1477
+ const certHandle = this.native.certificateLoadFromPem(certPem, keyPem);
1478
+ if (!certHandle) {
1479
+ throw new SignatureException('Failed to load PEM certificate');
1480
+ }
1481
+ try {
1482
+ const result = this.native.signPdfBytes(pdfData, certHandle, reason ?? null, location ?? null);
1483
+ if (!result)
1484
+ throw new SignatureException('signPdfBytes returned null');
1485
+ return Buffer.from(result);
1486
+ }
1487
+ finally {
1488
+ if (this.native?.pdf_certificate_free) {
1489
+ this.native.pdf_certificate_free(certHandle);
1490
+ }
1491
+ }
1492
+ }
1457
1493
  // Private helpers
1458
1494
  setCached(key, value) {
1459
1495
  this.resultCache.set(key, value);
@@ -103,3 +103,82 @@ export type StreamErrorCallback = (error: Error) => void;
103
103
  * Stream end callback
104
104
  */
105
105
  export type StreamEndCallback = () => void;
106
+ /**
107
+ * Horizontal alignment for wrapped text and table cells.
108
+ * Matches the C FFI integer encoding used by
109
+ * `pdf_page_builder_text_in_rect` and `pdf_page_builder_table`.
110
+ */
111
+ export declare enum Align {
112
+ Left = 0,
113
+ Center = 1,
114
+ Right = 2
115
+ }
116
+ /**
117
+ * Column descriptor for {@link TableSpec} / {@link StreamingTableConfig}.
118
+ */
119
+ export interface Column {
120
+ /** Header label rendered in bold (used only when `hasHeader`/`repeatHeader`). */
121
+ header: string;
122
+ /** Column width in PDF points. */
123
+ width: number;
124
+ /** Cell alignment (default {@link Align.Left}). */
125
+ align?: Align;
126
+ }
127
+ /**
128
+ * Buffered-table spec passed to `PageBuilder.table(...)`.
129
+ *
130
+ * All rows are held in JS memory and flushed to the native
131
+ * `pdf_page_builder_table` call in a single step.
132
+ */
133
+ export interface TableSpec {
134
+ /** Column layout — widths, alignments, and header labels. */
135
+ columns: Column[];
136
+ /** Body rows, each row has `columns.length` cells (nullable = empty). */
137
+ rows: Array<Array<string | null | undefined>>;
138
+ /** Promote the column headers to a styled first row. Defaults to true. */
139
+ hasHeader?: boolean;
140
+ }
141
+ /**
142
+ * Column-sizing strategy for streaming tables (issue #400).
143
+ *
144
+ * - `fixed` — use the `width` from each `Column` as-is (default).
145
+ * - `sample` — buffer the first N rows, measure content, then freeze
146
+ * column widths automatically. Supply `sampleRows`, `minColWidth`,
147
+ * and `maxColWidth` to tune.
148
+ */
149
+ export type TableMode = {
150
+ kind: 'fixed';
151
+ } | {
152
+ kind: 'sample';
153
+ sampleRows?: number;
154
+ minColWidthPt?: number;
155
+ maxColWidthPt?: number;
156
+ };
157
+ /**
158
+ * Configuration for the managed streaming-table adapter.
159
+ */
160
+ export interface StreamingTableConfig {
161
+ /** Column layout — widths, alignments, and header labels. */
162
+ columns: Column[];
163
+ /**
164
+ * Whether to repeat the header row on every page break. Defaults to true.
165
+ */
166
+ repeatHeader?: boolean;
167
+ /**
168
+ * Column-sizing mode. Defaults to `{ kind: 'fixed' }`.
169
+ */
170
+ mode?: TableMode;
171
+ /**
172
+ * Maximum rowspan a cell may carry. 0 or 1 (default) disables rowspan.
173
+ * Set to ≥2 to allow `pushRowSpan` cells to span multiple rows.
174
+ */
175
+ maxRowspan?: number;
176
+ }
177
+ /**
178
+ * A single cell value for `StreamingTable.pushRowSpan`.
179
+ * `rowspan == 1` is equivalent to a normal cell.
180
+ */
181
+ export interface SpanCell {
182
+ text: string;
183
+ rowspan: number;
184
+ }
@@ -1,4 +1,17 @@
1
1
  /**
2
2
  * Common type definitions and utilities
3
3
  */
4
- export {};
4
+ // ============================================================================
5
+ // DocumentBuilder — table primitives (v0.3.39, issue #393)
6
+ // ============================================================================
7
+ /**
8
+ * Horizontal alignment for wrapped text and table cells.
9
+ * Matches the C FFI integer encoding used by
10
+ * `pdf_page_builder_text_in_rect` and `pdf_page_builder_table`.
11
+ */
12
+ export var Align;
13
+ (function (Align) {
14
+ Align[Align["Left"] = 0] = "Left";
15
+ Align[Align["Center"] = 1] = "Center";
16
+ Align[Align["Right"] = 2] = "Right";
17
+ })(Align || (Align = {}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-oxide",
3
- "version": "0.3.38",
3
+ "version": "0.3.39",
4
4
  "type": "module",
5
5
  "description": "High-performance PDF parsing and text extraction library — prebuilt native bindings, no build toolchain required",
6
6
  "main": "lib/index.js",
@@ -20,7 +20,7 @@
20
20
  "check:publint": "publint",
21
21
  "check:types": "attw --pack . --ignore-rules=cjs-resolves-to-esm",
22
22
  "audit:prod": "npm audit --omit=dev --audit-level=high",
23
- "test": "node --test tests/smoke.test.mjs"
23
+ "test": "node --test tests/smoke.test.mjs tests/feature-guard.test.mjs tests/api-coverage.test.mjs tests/html-css.test.mjs tests/document-builder.test.mjs tests/document-editor.test.mjs tests/render-options.test.mjs tests/tables.test.mjs"
24
24
  },
25
25
  "files": [
26
26
  "lib/",
Binary file
Binary file
Binary file