@qrcommunication/gigapdf-lib 0.1.0 → 0.8.0

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/dist/index.js CHANGED
@@ -84,6 +84,10 @@ var GigaPdfEngine = class _GigaPdfEngine {
84
84
  this._free(ptr, b.length);
85
85
  }
86
86
  }
87
+ /** Pass an optional string; an absent/empty value runs `fn(0, 0)` (no alloc). */
88
+ _withOptStr(s, fn) {
89
+ return s ? this._withStr(s, fn) : fn(0, 0);
90
+ }
87
91
  /** Pass a bytes argument; runs `fn(ptr, len)` then frees. */
88
92
  _withBytes(bytes, fn) {
89
93
  const ptr = this._toWasm(bytes);
@@ -165,8 +169,194 @@ var GigaPdfEngine = class _GigaPdfEngine {
165
169
  parseCssFontUrl(css) {
166
170
  return this._withStr(css, (p, l) => this._str((o) => this.ex.gp_parse_css_font_url(p, l, o)));
167
171
  }
172
+ // ── JavaScript engine (no headless browser) ────────────────────────────────
173
+ /**
174
+ * Evaluate a JavaScript snippet with the built-in engine and return the
175
+ * result value as a string (or `Uncaught …` / `SyntaxError: …`).
176
+ */
177
+ evalJs(src) {
178
+ return this._withStr(src, (p, l) => this._str((o) => this.ex.gp_js_eval(p, l, o)));
179
+ }
180
+ /**
181
+ * Run a document's inline `<script>`s and return the resulting HTML. The
182
+ * `htmlRender`/`htmlNeededFonts` paths already do this automatically; use this
183
+ * only when you want the post-script HTML on its own.
184
+ */
185
+ runInlineScripts(html) {
186
+ return this._withStr(
187
+ html,
188
+ (p, l) => this._str((o) => this.ex.gp_run_inline_scripts(p, l, o))
189
+ );
190
+ }
191
+ // ── HTML rendering engine (replaces a headless browser for HTML→PDF) ───────
192
+ /**
193
+ * Phase 1 — the Google fonts the document references. Download each `url`
194
+ * (→ TTF) and pass the bytes back to {@link htmlRender} for an identical render.
195
+ */
196
+ htmlNeededFonts(html) {
197
+ return this._withStr(
198
+ html,
199
+ (p, l) => this._json((o) => this.ex.gp_html_needed_fonts(p, l, o))
200
+ );
201
+ }
202
+ /**
203
+ * Phase 2 — render HTML + CSS to PDF, with the supplied fonts embedded (real
204
+ * Google fonts, real metrics → identical or nearest match). Block, inline and
205
+ * table layout with pagination. Page size and margin are in points
206
+ * (US-Letter portrait, 0.5in margins by default). JavaScript is not executed.
207
+ */
208
+ htmlRender(html, fonts = [], pageWidth = 612, pageHeight = 792, margin = 36) {
209
+ const blob = packHtmlFonts(fonts);
210
+ return this._withStr(
211
+ html,
212
+ (hp, hl) => this._withBytes(
213
+ blob,
214
+ (fp, fl) => this._buffer((o) => this.ex.gp_html_render(hp, hl, fp, fl, pageWidth, pageHeight, margin, o))
215
+ )
216
+ );
217
+ }
218
+ /**
219
+ * Resolve a named paper size — `"A4"`, `"a3-landscape"`, `"letter"`, `"legal"`,
220
+ * `"tabloid"`, `"b5"`, … — to `{ w, h }` in points (portrait unless the name
221
+ * has a `-landscape` suffix). Returns `null` for an unknown name.
222
+ */
223
+ pageSize(name) {
224
+ const outPtr = this.ex.gp_alloc(16);
225
+ try {
226
+ const ok = this._withStr(
227
+ name,
228
+ (p, l) => this.ex.gp_page_size(p, l, outPtr, outPtr + 8)
229
+ );
230
+ if (!ok) return null;
231
+ const dv = this.dv();
232
+ return { w: dv.getFloat64(outPtr, true), h: dv.getFloat64(outPtr + 8, true) };
233
+ } finally {
234
+ this._free(outPtr, 16);
235
+ }
236
+ }
237
+ /**
238
+ * Phase 1 variant that also scans the running `header`/`footer` HTML, so the
239
+ * Google fonts they reference are requested alongside the body's.
240
+ */
241
+ htmlNeededFontsWith(html, header, footer) {
242
+ return this._withStr(
243
+ html,
244
+ (hp, hl) => this._withOptStr(
245
+ header,
246
+ (hdp, hdl) => this._withOptStr(
247
+ footer,
248
+ (ftp, ftl) => this._json((o) => this.ex.gp_html_needed_fonts_ex(hp, hl, hdp, hdl, ftp, ftl, o))
249
+ )
250
+ )
251
+ );
252
+ }
253
+ /**
254
+ * Phase 2 with full page control: named/explicit size, per-side margins, and a
255
+ * running header/footer painted in the page margins. `{{page}}` and `{{pages}}`
256
+ * in the header/footer are replaced with the current / total page number.
257
+ *
258
+ * ```ts
259
+ * const fonts = await fetchFonts(giga.htmlNeededFontsWith(html, header, footer));
260
+ * const pdf = giga.htmlRenderWith(html, fonts, {
261
+ * pageSize: "A4",
262
+ * margin: { top: 72, bottom: 72, left: 54, right: 54 },
263
+ * header: `<div style="text-align:center">My Report</div>`,
264
+ * footer: `<div style="text-align:center">Page {{page}} / {{pages}}</div>`,
265
+ * });
266
+ * ```
267
+ */
268
+ htmlRenderWith(html, fonts = [], options = {}) {
269
+ let pw = options.pageWidth ?? 612;
270
+ let ph = options.pageHeight ?? 792;
271
+ if (options.pageSize) {
272
+ const sz = this.pageSize(options.pageSize);
273
+ if (!sz) throw new Error(`gigapdf: unknown page size "${options.pageSize}"`);
274
+ pw = sz.w;
275
+ ph = sz.h;
276
+ }
277
+ const m = options.margin ?? 36;
278
+ const mg = typeof m === "number" ? { top: m, right: m, bottom: m, left: m } : { top: m.top ?? 36, right: m.right ?? 36, bottom: m.bottom ?? 36, left: m.left ?? 36 };
279
+ const headerOffset = options.headerOffset ?? 18;
280
+ const footerOffset = options.footerOffset ?? 18;
281
+ const start = options.startPageNumber ?? 1;
282
+ const blob = packHtmlFonts(fonts);
283
+ return this._withStr(
284
+ html,
285
+ (hp, hl) => this._withBytes(
286
+ blob,
287
+ (fp, fl) => this._withOptStr(
288
+ options.header,
289
+ (hdp, hdl) => this._withOptStr(
290
+ options.footer,
291
+ (ftp, ftl) => this._buffer(
292
+ (o) => this.ex.gp_html_render_opts(
293
+ hp,
294
+ hl,
295
+ fp,
296
+ fl,
297
+ pw,
298
+ ph,
299
+ mg.top,
300
+ mg.right,
301
+ mg.bottom,
302
+ mg.left,
303
+ hdp,
304
+ hdl,
305
+ ftp,
306
+ ftl,
307
+ headerOffset,
308
+ footerOffset,
309
+ start,
310
+ o
311
+ )
312
+ )
313
+ )
314
+ )
315
+ )
316
+ );
317
+ }
168
318
  };
319
+ function packHtmlFonts(fonts) {
320
+ let size = 4;
321
+ for (const f of fonts) size += 4 + enc.encode(f.family).length + 2 + 1 + 4 + f.ttf.length;
322
+ const buf = new Uint8Array(size);
323
+ const dv = new DataView(buf.buffer);
324
+ let o = 0;
325
+ dv.setUint32(o, fonts.length, true);
326
+ o += 4;
327
+ for (const f of fonts) {
328
+ const fam = enc.encode(f.family);
329
+ dv.setUint32(o, fam.length, true);
330
+ o += 4;
331
+ buf.set(fam, o);
332
+ o += fam.length;
333
+ dv.setUint16(o, f.weight, true);
334
+ o += 2;
335
+ buf[o] = f.italic ? 1 : 0;
336
+ o += 1;
337
+ dv.setUint32(o, f.ttf.length, true);
338
+ o += 4;
339
+ buf.set(f.ttf, o);
340
+ o += f.ttf.length;
341
+ }
342
+ return buf;
343
+ }
169
344
  var RGB = (rgb) => rgb & 16777215;
345
+ function styleArgs(s = {}) {
346
+ const hasBorder = s.border === null ? 0 : 1;
347
+ const borderRgb = s.border == null ? 0 : s.border;
348
+ const hasBg = s.background == null ? 0 : 1;
349
+ const bgRgb = s.background == null ? 0 : s.background;
350
+ return [
351
+ s.fontSize ?? 0,
352
+ RGB(s.color ?? 0),
353
+ RGB(borderRgb),
354
+ hasBorder,
355
+ RGB(bgRgb),
356
+ hasBg,
357
+ s.borderWidth ?? 1
358
+ ];
359
+ }
170
360
  var GigaPdfDoc = class {
171
361
  constructor(g, h) {
172
362
  this.g = g;
@@ -233,7 +423,7 @@ var GigaPdfDoc = class {
233
423
  * Draw a vector rectangle. Pass an `0xRRGGBB` colour for `stroke`/`fill`, or
234
424
  * `null` to omit that paint. 0 → success.
235
425
  */
236
- addRectangle(page, x, y, w, h, stroke = null, fill = 0, lineWidth = 1) {
426
+ addRectangle(page, x, y, w, h, stroke = null, fill = 0, lineWidth = 1, opacity = 1) {
237
427
  return this.ex().gp_add_rectangle(
238
428
  this.h,
239
429
  page,
@@ -245,9 +435,99 @@ var GigaPdfDoc = class {
245
435
  stroke === null ? 0 : 1,
246
436
  RGB(fill ?? 0),
247
437
  fill === null ? 0 : 1,
248
- lineWidth
438
+ lineWidth,
439
+ opacity
440
+ ) === 0;
441
+ }
442
+ /** Draw a straight line from `(x1,y1)` to `(x2,y2)`. `stroke` is `0xRRGGBB`. */
443
+ drawLine(page, x1, y1, x2, y2, stroke = 0, lineWidth = 1, opacity = 1) {
444
+ return this.ex().gp_draw_line(this.h, page, x1, y1, x2, y2, RGB(stroke), lineWidth, opacity) === 0;
445
+ }
446
+ /**
447
+ * Draw an ellipse (circle when `rx === ry`) centred at `(cx,cy)`. Pass an
448
+ * `0xRRGGBB` colour for `stroke`/`fill`, or `null` to omit that paint.
449
+ */
450
+ addEllipse(page, cx, cy, rx, ry, stroke = null, fill = 0, lineWidth = 1, opacity = 1) {
451
+ return this.ex().gp_add_ellipse(
452
+ this.h,
453
+ page,
454
+ cx,
455
+ cy,
456
+ rx,
457
+ ry,
458
+ RGB(stroke ?? 0),
459
+ stroke === null ? 0 : 1,
460
+ RGB(fill ?? 0),
461
+ fill === null ? 0 : 1,
462
+ lineWidth,
463
+ opacity
464
+ ) === 0;
465
+ }
466
+ /**
467
+ * Draw a polyline/polygon through flat `[x0,y0,x1,y1,…]` points. `close` joins
468
+ * the last vertex back to the first. `0xRRGGBB` colours, or `null` to omit.
469
+ */
470
+ addPolygon(page, points, close = true, stroke = null, fill = 0, lineWidth = 1, opacity = 1) {
471
+ return this.g._withF64(
472
+ points,
473
+ (p, c) => this.ex().gp_add_polygon(
474
+ this.h,
475
+ page,
476
+ p,
477
+ c,
478
+ close ? 1 : 0,
479
+ RGB(stroke ?? 0),
480
+ stroke === null ? 0 : 1,
481
+ RGB(fill ?? 0),
482
+ fill === null ? 0 : 1,
483
+ lineWidth,
484
+ opacity
485
+ )
486
+ ) === 0;
487
+ }
488
+ /**
489
+ * Draw an SVG path (`M`/`L`/`C`/`Q`/`Z`…) anchored so the SVG origin maps to
490
+ * `(ox,oy)` with the Y axis flipped — same convention as `pdf-lib`'s
491
+ * `drawSvgPath`. Covers freeform/polygon/triangle shapes.
492
+ */
493
+ addPath(page, svgPath, ox, oy, stroke = null, fill = 0, lineWidth = 1, opacity = 1) {
494
+ return this.g._withStr(
495
+ svgPath,
496
+ (p, l) => this.ex().gp_add_path(
497
+ this.h,
498
+ page,
499
+ p,
500
+ l,
501
+ ox,
502
+ oy,
503
+ RGB(stroke ?? 0),
504
+ stroke === null ? 0 : 1,
505
+ RGB(fill ?? 0),
506
+ fill === null ? 0 : 1,
507
+ lineWidth,
508
+ opacity
509
+ )
510
+ ) === 0;
511
+ }
512
+ /**
513
+ * Embed a raster image (PNG or JPEG bytes) at `(x,y)` sized `(w,h)` in PDF
514
+ * user space. PNG alpha is honoured; `opacity` (0..1) sets an overall alpha.
515
+ */
516
+ addImage(page, data, x, y, w, h, opacity = 1) {
517
+ return this.g._withBytes(
518
+ data,
519
+ (p, l) => this.ex().gp_add_image(this.h, page, p, l, x, y, w, h, opacity)
249
520
  ) === 0;
250
521
  }
522
+ /**
523
+ * Draw SVG markup on a page as **native vector paths** (crisp at any zoom, not
524
+ * rasterized), fitting its `viewBox` into the box `(x, y, w, h)` in PDF points
525
+ * (origin bottom-left). Supports shapes, `<path>`, groups, transforms and
526
+ * fill/stroke/opacity. Returns `false` if the SVG can't be parsed.
527
+ */
528
+ addSvg(page, svg, x, y, w, h) {
529
+ return this.g._withStr(svg, (p, l) => this.ex().gp_add_svg(this.h, page, p, l, x, y, w, h)) === 0;
530
+ }
251
531
  /** True redaction: delete content intersecting the region (no opaque cover by default). */
252
532
  redact(page, x, y, w, h, coverRgb = 0, hasCover = false) {
253
533
  return this.ex().gp_redact_region(this.h, page, x, y, w, h, RGB(coverRgb), hasCover ? 1 : 0);
@@ -325,6 +605,10 @@ var GigaPdfDoc = class {
325
605
  toPptx() {
326
606
  return this.g._buffer((o) => this.ex().gp_to_pptx(this.h, o));
327
607
  }
608
+ /** Convert to an editable OpenDocument Presentation (`.odp`). */
609
+ toOdp() {
610
+ return this.g._buffer((o) => this.ex().gp_to_odp(this.h, o));
611
+ }
328
612
  toOdt() {
329
613
  return this.g._buffer((o) => this.ex().gp_to_odt(this.h, o));
330
614
  }
@@ -341,13 +625,54 @@ var GigaPdfDoc = class {
341
625
  return this.g._buffer((o) => this.ex().gp_to_pdfa(this.h, o));
342
626
  }
343
627
  // security
344
- saveEncrypted(password, fileId, permissions = -44) {
628
+ /**
629
+ * Serialize the document encrypted with the PDF Standard Security Handler.
630
+ * Defaults to **AES-256 (R6)**. `fileId` is the document `/ID` (any stable
631
+ * hex/string). For AES-256 a **secret 32-byte key** is required — it is taken
632
+ * from `opts.keySeed` or generated with Web Crypto; RC4/AES-128 derive their
633
+ * key from the password and ignore it.
634
+ */
635
+ saveEncrypted(password, fileId, opts = {}) {
636
+ const algo = opts.algorithm ?? "aes256";
637
+ const algoCode = algo === "rc4" ? 0 : algo === "aes128" ? 1 : 2;
638
+ const permissions = opts.permissions ?? -44;
639
+ let key = opts.keySeed ?? new Uint8Array(0);
640
+ if (algoCode === 2 && key.length < 32) {
641
+ const c = globalThis.crypto;
642
+ if (!c?.getRandomValues) {
643
+ throw new Error(
644
+ "AES-256 encryption needs Web Crypto (globalThis.crypto.getRandomValues) or an explicit opts.keySeed"
645
+ );
646
+ }
647
+ const fresh = new Uint8Array(32);
648
+ c.getRandomValues(fresh);
649
+ key = fresh;
650
+ }
345
651
  return this.g._withStr(
346
652
  password,
347
- (pwP, pwL) => this.g._withStr(
348
- fileId,
349
- (idP, idL) => this.g._buffer(
350
- (o) => this.ex().gp_save_encrypted(this.h, pwP, pwL, idP, idL, permissions, o)
653
+ (pwP, pwL) => this.g._withOptStr(
654
+ opts.ownerPassword,
655
+ (oP, oL) => this.g._withStr(
656
+ fileId,
657
+ (idP, idL) => this.g._withBytes(
658
+ key,
659
+ (kP, kL) => this.g._buffer(
660
+ (o) => this.ex().gp_save_encrypted(
661
+ this.h,
662
+ pwP,
663
+ pwL,
664
+ oP,
665
+ oL,
666
+ idP,
667
+ idL,
668
+ kP,
669
+ kL,
670
+ algoCode,
671
+ permissions,
672
+ o
673
+ )
674
+ )
675
+ )
351
676
  )
352
677
  )
353
678
  );
@@ -491,6 +816,141 @@ var GigaPdfDoc = class {
491
816
  (nP, nL) => this.g._withStr(values.join("\n"), (vP, vL) => this.ex().gp_set_choice(this.h, nP, nL, vP, vL))
492
817
  ) === 0;
493
818
  }
819
+ // ── form field creation ──────────────────────────────────────────────────
820
+ /**
821
+ * Create a text field on `page` covering `rect` = `[x0, y0, x1, y1]` (PDF
822
+ * user space). Options: `maxLen` character cap, `multiline`, `password`,
823
+ * and visual `style`.
824
+ */
825
+ addTextField(page, name, rect, value = "", opts = {}) {
826
+ const st = styleArgs(opts.style);
827
+ return this.g._withStr(
828
+ name,
829
+ (nP, nL) => this.g._withStr(
830
+ value,
831
+ (vP, vL) => this.ex().gp_add_text_field(
832
+ this.h,
833
+ page,
834
+ nP,
835
+ nL,
836
+ rect[0],
837
+ rect[1],
838
+ rect[2],
839
+ rect[3],
840
+ vP,
841
+ vL,
842
+ opts.maxLen ?? -1,
843
+ opts.multiline ? 1 : 0,
844
+ opts.password ? 1 : 0,
845
+ ...st
846
+ )
847
+ )
848
+ ) === 0;
849
+ }
850
+ /** Create a checkbox. `export` is the on-state name (default `On`). */
851
+ addCheckbox(page, name, rect, checked = false, opts = {}) {
852
+ const st = styleArgs(opts.style);
853
+ return this.g._withStr(
854
+ name,
855
+ (nP, nL) => this.g._withStr(
856
+ opts.export ?? "On",
857
+ (eP, eL) => this.ex().gp_add_checkbox(
858
+ this.h,
859
+ page,
860
+ nP,
861
+ nL,
862
+ rect[0],
863
+ rect[1],
864
+ rect[2],
865
+ rect[3],
866
+ checked ? 1 : 0,
867
+ eP,
868
+ eL,
869
+ ...st
870
+ )
871
+ )
872
+ ) === 0;
873
+ }
874
+ /**
875
+ * Create a radio-button group: one logical field whose `options` are the
876
+ * individual buttons. `selected` is the initially-chosen export value.
877
+ */
878
+ addRadioGroup(page, name, options, opts = {}) {
879
+ const st = styleArgs(opts.style);
880
+ const exports = options.map((o) => o.export).join("\n");
881
+ const rects = options.flatMap((o) => o.rect).join(",");
882
+ return this.g._withStr(
883
+ name,
884
+ (nP, nL) => this.g._withStr(
885
+ exports,
886
+ (eP, eL) => this.g._withStr(
887
+ rects,
888
+ (rP, rL) => this.g._withStr(
889
+ opts.selected ?? "",
890
+ (sP, sL) => this.ex().gp_add_radio_group(this.h, page, nP, nL, eP, eL, rP, rL, sP, sL, ...st)
891
+ )
892
+ )
893
+ )
894
+ ) === 0;
895
+ }
896
+ /** Create a drop-down combo box. `editable` permits values outside `options`. */
897
+ addComboBox(page, name, rect, options, opts = {}) {
898
+ const st = styleArgs(opts.style);
899
+ return this.g._withStr(
900
+ name,
901
+ (nP, nL) => this.g._withStr(
902
+ options.join("\n"),
903
+ (oP, oL) => this.g._withStr(
904
+ opts.selected ?? "",
905
+ (sP, sL) => this.ex().gp_add_combo_box(
906
+ this.h,
907
+ page,
908
+ nP,
909
+ nL,
910
+ rect[0],
911
+ rect[1],
912
+ rect[2],
913
+ rect[3],
914
+ oP,
915
+ oL,
916
+ sP,
917
+ sL,
918
+ opts.editable ? 1 : 0,
919
+ ...st
920
+ )
921
+ )
922
+ )
923
+ ) === 0;
924
+ }
925
+ /** Create a scrolling list box. `multi` allows selecting several options. */
926
+ addListBox(page, name, rect, options, opts = {}) {
927
+ const st = styleArgs(opts.style);
928
+ return this.g._withStr(
929
+ name,
930
+ (nP, nL) => this.g._withStr(
931
+ options.join("\n"),
932
+ (oP, oL) => this.g._withStr(
933
+ opts.selected ?? "",
934
+ (sP, sL) => this.ex().gp_add_list_box(
935
+ this.h,
936
+ page,
937
+ nP,
938
+ nL,
939
+ rect[0],
940
+ rect[1],
941
+ rect[2],
942
+ rect[3],
943
+ oP,
944
+ oL,
945
+ sP,
946
+ sL,
947
+ opts.multi ? 1 : 0,
948
+ ...st
949
+ )
950
+ )
951
+ )
952
+ ) === 0;
953
+ }
494
954
  };
495
955
  export {
496
956
  GigaPdfDoc,