@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.cjs CHANGED
@@ -125,6 +125,10 @@ var GigaPdfEngine = class _GigaPdfEngine {
125
125
  this._free(ptr, b.length);
126
126
  }
127
127
  }
128
+ /** Pass an optional string; an absent/empty value runs `fn(0, 0)` (no alloc). */
129
+ _withOptStr(s, fn) {
130
+ return s ? this._withStr(s, fn) : fn(0, 0);
131
+ }
128
132
  /** Pass a bytes argument; runs `fn(ptr, len)` then frees. */
129
133
  _withBytes(bytes, fn) {
130
134
  const ptr = this._toWasm(bytes);
@@ -206,8 +210,194 @@ var GigaPdfEngine = class _GigaPdfEngine {
206
210
  parseCssFontUrl(css) {
207
211
  return this._withStr(css, (p, l) => this._str((o) => this.ex.gp_parse_css_font_url(p, l, o)));
208
212
  }
213
+ // ── JavaScript engine (no headless browser) ────────────────────────────────
214
+ /**
215
+ * Evaluate a JavaScript snippet with the built-in engine and return the
216
+ * result value as a string (or `Uncaught …` / `SyntaxError: …`).
217
+ */
218
+ evalJs(src) {
219
+ return this._withStr(src, (p, l) => this._str((o) => this.ex.gp_js_eval(p, l, o)));
220
+ }
221
+ /**
222
+ * Run a document's inline `<script>`s and return the resulting HTML. The
223
+ * `htmlRender`/`htmlNeededFonts` paths already do this automatically; use this
224
+ * only when you want the post-script HTML on its own.
225
+ */
226
+ runInlineScripts(html) {
227
+ return this._withStr(
228
+ html,
229
+ (p, l) => this._str((o) => this.ex.gp_run_inline_scripts(p, l, o))
230
+ );
231
+ }
232
+ // ── HTML rendering engine (replaces a headless browser for HTML→PDF) ───────
233
+ /**
234
+ * Phase 1 — the Google fonts the document references. Download each `url`
235
+ * (→ TTF) and pass the bytes back to {@link htmlRender} for an identical render.
236
+ */
237
+ htmlNeededFonts(html) {
238
+ return this._withStr(
239
+ html,
240
+ (p, l) => this._json((o) => this.ex.gp_html_needed_fonts(p, l, o))
241
+ );
242
+ }
243
+ /**
244
+ * Phase 2 — render HTML + CSS to PDF, with the supplied fonts embedded (real
245
+ * Google fonts, real metrics → identical or nearest match). Block, inline and
246
+ * table layout with pagination. Page size and margin are in points
247
+ * (US-Letter portrait, 0.5in margins by default). JavaScript is not executed.
248
+ */
249
+ htmlRender(html, fonts = [], pageWidth = 612, pageHeight = 792, margin = 36) {
250
+ const blob = packHtmlFonts(fonts);
251
+ return this._withStr(
252
+ html,
253
+ (hp, hl) => this._withBytes(
254
+ blob,
255
+ (fp, fl) => this._buffer((o) => this.ex.gp_html_render(hp, hl, fp, fl, pageWidth, pageHeight, margin, o))
256
+ )
257
+ );
258
+ }
259
+ /**
260
+ * Resolve a named paper size — `"A4"`, `"a3-landscape"`, `"letter"`, `"legal"`,
261
+ * `"tabloid"`, `"b5"`, … — to `{ w, h }` in points (portrait unless the name
262
+ * has a `-landscape` suffix). Returns `null` for an unknown name.
263
+ */
264
+ pageSize(name) {
265
+ const outPtr = this.ex.gp_alloc(16);
266
+ try {
267
+ const ok = this._withStr(
268
+ name,
269
+ (p, l) => this.ex.gp_page_size(p, l, outPtr, outPtr + 8)
270
+ );
271
+ if (!ok) return null;
272
+ const dv = this.dv();
273
+ return { w: dv.getFloat64(outPtr, true), h: dv.getFloat64(outPtr + 8, true) };
274
+ } finally {
275
+ this._free(outPtr, 16);
276
+ }
277
+ }
278
+ /**
279
+ * Phase 1 variant that also scans the running `header`/`footer` HTML, so the
280
+ * Google fonts they reference are requested alongside the body's.
281
+ */
282
+ htmlNeededFontsWith(html, header, footer) {
283
+ return this._withStr(
284
+ html,
285
+ (hp, hl) => this._withOptStr(
286
+ header,
287
+ (hdp, hdl) => this._withOptStr(
288
+ footer,
289
+ (ftp, ftl) => this._json((o) => this.ex.gp_html_needed_fonts_ex(hp, hl, hdp, hdl, ftp, ftl, o))
290
+ )
291
+ )
292
+ );
293
+ }
294
+ /**
295
+ * Phase 2 with full page control: named/explicit size, per-side margins, and a
296
+ * running header/footer painted in the page margins. `{{page}}` and `{{pages}}`
297
+ * in the header/footer are replaced with the current / total page number.
298
+ *
299
+ * ```ts
300
+ * const fonts = await fetchFonts(giga.htmlNeededFontsWith(html, header, footer));
301
+ * const pdf = giga.htmlRenderWith(html, fonts, {
302
+ * pageSize: "A4",
303
+ * margin: { top: 72, bottom: 72, left: 54, right: 54 },
304
+ * header: `<div style="text-align:center">My Report</div>`,
305
+ * footer: `<div style="text-align:center">Page {{page}} / {{pages}}</div>`,
306
+ * });
307
+ * ```
308
+ */
309
+ htmlRenderWith(html, fonts = [], options = {}) {
310
+ let pw = options.pageWidth ?? 612;
311
+ let ph = options.pageHeight ?? 792;
312
+ if (options.pageSize) {
313
+ const sz = this.pageSize(options.pageSize);
314
+ if (!sz) throw new Error(`gigapdf: unknown page size "${options.pageSize}"`);
315
+ pw = sz.w;
316
+ ph = sz.h;
317
+ }
318
+ const m = options.margin ?? 36;
319
+ 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 };
320
+ const headerOffset = options.headerOffset ?? 18;
321
+ const footerOffset = options.footerOffset ?? 18;
322
+ const start = options.startPageNumber ?? 1;
323
+ const blob = packHtmlFonts(fonts);
324
+ return this._withStr(
325
+ html,
326
+ (hp, hl) => this._withBytes(
327
+ blob,
328
+ (fp, fl) => this._withOptStr(
329
+ options.header,
330
+ (hdp, hdl) => this._withOptStr(
331
+ options.footer,
332
+ (ftp, ftl) => this._buffer(
333
+ (o) => this.ex.gp_html_render_opts(
334
+ hp,
335
+ hl,
336
+ fp,
337
+ fl,
338
+ pw,
339
+ ph,
340
+ mg.top,
341
+ mg.right,
342
+ mg.bottom,
343
+ mg.left,
344
+ hdp,
345
+ hdl,
346
+ ftp,
347
+ ftl,
348
+ headerOffset,
349
+ footerOffset,
350
+ start,
351
+ o
352
+ )
353
+ )
354
+ )
355
+ )
356
+ )
357
+ );
358
+ }
209
359
  };
360
+ function packHtmlFonts(fonts) {
361
+ let size = 4;
362
+ for (const f of fonts) size += 4 + enc.encode(f.family).length + 2 + 1 + 4 + f.ttf.length;
363
+ const buf = new Uint8Array(size);
364
+ const dv = new DataView(buf.buffer);
365
+ let o = 0;
366
+ dv.setUint32(o, fonts.length, true);
367
+ o += 4;
368
+ for (const f of fonts) {
369
+ const fam = enc.encode(f.family);
370
+ dv.setUint32(o, fam.length, true);
371
+ o += 4;
372
+ buf.set(fam, o);
373
+ o += fam.length;
374
+ dv.setUint16(o, f.weight, true);
375
+ o += 2;
376
+ buf[o] = f.italic ? 1 : 0;
377
+ o += 1;
378
+ dv.setUint32(o, f.ttf.length, true);
379
+ o += 4;
380
+ buf.set(f.ttf, o);
381
+ o += f.ttf.length;
382
+ }
383
+ return buf;
384
+ }
210
385
  var RGB = (rgb) => rgb & 16777215;
386
+ function styleArgs(s = {}) {
387
+ const hasBorder = s.border === null ? 0 : 1;
388
+ const borderRgb = s.border == null ? 0 : s.border;
389
+ const hasBg = s.background == null ? 0 : 1;
390
+ const bgRgb = s.background == null ? 0 : s.background;
391
+ return [
392
+ s.fontSize ?? 0,
393
+ RGB(s.color ?? 0),
394
+ RGB(borderRgb),
395
+ hasBorder,
396
+ RGB(bgRgb),
397
+ hasBg,
398
+ s.borderWidth ?? 1
399
+ ];
400
+ }
211
401
  var GigaPdfDoc = class {
212
402
  constructor(g, h) {
213
403
  this.g = g;
@@ -274,7 +464,7 @@ var GigaPdfDoc = class {
274
464
  * Draw a vector rectangle. Pass an `0xRRGGBB` colour for `stroke`/`fill`, or
275
465
  * `null` to omit that paint. 0 → success.
276
466
  */
277
- addRectangle(page, x, y, w, h, stroke = null, fill = 0, lineWidth = 1) {
467
+ addRectangle(page, x, y, w, h, stroke = null, fill = 0, lineWidth = 1, opacity = 1) {
278
468
  return this.ex().gp_add_rectangle(
279
469
  this.h,
280
470
  page,
@@ -286,9 +476,99 @@ var GigaPdfDoc = class {
286
476
  stroke === null ? 0 : 1,
287
477
  RGB(fill ?? 0),
288
478
  fill === null ? 0 : 1,
289
- lineWidth
479
+ lineWidth,
480
+ opacity
481
+ ) === 0;
482
+ }
483
+ /** Draw a straight line from `(x1,y1)` to `(x2,y2)`. `stroke` is `0xRRGGBB`. */
484
+ drawLine(page, x1, y1, x2, y2, stroke = 0, lineWidth = 1, opacity = 1) {
485
+ return this.ex().gp_draw_line(this.h, page, x1, y1, x2, y2, RGB(stroke), lineWidth, opacity) === 0;
486
+ }
487
+ /**
488
+ * Draw an ellipse (circle when `rx === ry`) centred at `(cx,cy)`. Pass an
489
+ * `0xRRGGBB` colour for `stroke`/`fill`, or `null` to omit that paint.
490
+ */
491
+ addEllipse(page, cx, cy, rx, ry, stroke = null, fill = 0, lineWidth = 1, opacity = 1) {
492
+ return this.ex().gp_add_ellipse(
493
+ this.h,
494
+ page,
495
+ cx,
496
+ cy,
497
+ rx,
498
+ ry,
499
+ RGB(stroke ?? 0),
500
+ stroke === null ? 0 : 1,
501
+ RGB(fill ?? 0),
502
+ fill === null ? 0 : 1,
503
+ lineWidth,
504
+ opacity
505
+ ) === 0;
506
+ }
507
+ /**
508
+ * Draw a polyline/polygon through flat `[x0,y0,x1,y1,…]` points. `close` joins
509
+ * the last vertex back to the first. `0xRRGGBB` colours, or `null` to omit.
510
+ */
511
+ addPolygon(page, points, close = true, stroke = null, fill = 0, lineWidth = 1, opacity = 1) {
512
+ return this.g._withF64(
513
+ points,
514
+ (p, c) => this.ex().gp_add_polygon(
515
+ this.h,
516
+ page,
517
+ p,
518
+ c,
519
+ close ? 1 : 0,
520
+ RGB(stroke ?? 0),
521
+ stroke === null ? 0 : 1,
522
+ RGB(fill ?? 0),
523
+ fill === null ? 0 : 1,
524
+ lineWidth,
525
+ opacity
526
+ )
527
+ ) === 0;
528
+ }
529
+ /**
530
+ * Draw an SVG path (`M`/`L`/`C`/`Q`/`Z`…) anchored so the SVG origin maps to
531
+ * `(ox,oy)` with the Y axis flipped — same convention as `pdf-lib`'s
532
+ * `drawSvgPath`. Covers freeform/polygon/triangle shapes.
533
+ */
534
+ addPath(page, svgPath, ox, oy, stroke = null, fill = 0, lineWidth = 1, opacity = 1) {
535
+ return this.g._withStr(
536
+ svgPath,
537
+ (p, l) => this.ex().gp_add_path(
538
+ this.h,
539
+ page,
540
+ p,
541
+ l,
542
+ ox,
543
+ oy,
544
+ RGB(stroke ?? 0),
545
+ stroke === null ? 0 : 1,
546
+ RGB(fill ?? 0),
547
+ fill === null ? 0 : 1,
548
+ lineWidth,
549
+ opacity
550
+ )
551
+ ) === 0;
552
+ }
553
+ /**
554
+ * Embed a raster image (PNG or JPEG bytes) at `(x,y)` sized `(w,h)` in PDF
555
+ * user space. PNG alpha is honoured; `opacity` (0..1) sets an overall alpha.
556
+ */
557
+ addImage(page, data, x, y, w, h, opacity = 1) {
558
+ return this.g._withBytes(
559
+ data,
560
+ (p, l) => this.ex().gp_add_image(this.h, page, p, l, x, y, w, h, opacity)
290
561
  ) === 0;
291
562
  }
563
+ /**
564
+ * Draw SVG markup on a page as **native vector paths** (crisp at any zoom, not
565
+ * rasterized), fitting its `viewBox` into the box `(x, y, w, h)` in PDF points
566
+ * (origin bottom-left). Supports shapes, `<path>`, groups, transforms and
567
+ * fill/stroke/opacity. Returns `false` if the SVG can't be parsed.
568
+ */
569
+ addSvg(page, svg, x, y, w, h) {
570
+ return this.g._withStr(svg, (p, l) => this.ex().gp_add_svg(this.h, page, p, l, x, y, w, h)) === 0;
571
+ }
292
572
  /** True redaction: delete content intersecting the region (no opaque cover by default). */
293
573
  redact(page, x, y, w, h, coverRgb = 0, hasCover = false) {
294
574
  return this.ex().gp_redact_region(this.h, page, x, y, w, h, RGB(coverRgb), hasCover ? 1 : 0);
@@ -366,6 +646,10 @@ var GigaPdfDoc = class {
366
646
  toPptx() {
367
647
  return this.g._buffer((o) => this.ex().gp_to_pptx(this.h, o));
368
648
  }
649
+ /** Convert to an editable OpenDocument Presentation (`.odp`). */
650
+ toOdp() {
651
+ return this.g._buffer((o) => this.ex().gp_to_odp(this.h, o));
652
+ }
369
653
  toOdt() {
370
654
  return this.g._buffer((o) => this.ex().gp_to_odt(this.h, o));
371
655
  }
@@ -382,13 +666,54 @@ var GigaPdfDoc = class {
382
666
  return this.g._buffer((o) => this.ex().gp_to_pdfa(this.h, o));
383
667
  }
384
668
  // security
385
- saveEncrypted(password, fileId, permissions = -44) {
669
+ /**
670
+ * Serialize the document encrypted with the PDF Standard Security Handler.
671
+ * Defaults to **AES-256 (R6)**. `fileId` is the document `/ID` (any stable
672
+ * hex/string). For AES-256 a **secret 32-byte key** is required — it is taken
673
+ * from `opts.keySeed` or generated with Web Crypto; RC4/AES-128 derive their
674
+ * key from the password and ignore it.
675
+ */
676
+ saveEncrypted(password, fileId, opts = {}) {
677
+ const algo = opts.algorithm ?? "aes256";
678
+ const algoCode = algo === "rc4" ? 0 : algo === "aes128" ? 1 : 2;
679
+ const permissions = opts.permissions ?? -44;
680
+ let key = opts.keySeed ?? new Uint8Array(0);
681
+ if (algoCode === 2 && key.length < 32) {
682
+ const c = globalThis.crypto;
683
+ if (!c?.getRandomValues) {
684
+ throw new Error(
685
+ "AES-256 encryption needs Web Crypto (globalThis.crypto.getRandomValues) or an explicit opts.keySeed"
686
+ );
687
+ }
688
+ const fresh = new Uint8Array(32);
689
+ c.getRandomValues(fresh);
690
+ key = fresh;
691
+ }
386
692
  return this.g._withStr(
387
693
  password,
388
- (pwP, pwL) => this.g._withStr(
389
- fileId,
390
- (idP, idL) => this.g._buffer(
391
- (o) => this.ex().gp_save_encrypted(this.h, pwP, pwL, idP, idL, permissions, o)
694
+ (pwP, pwL) => this.g._withOptStr(
695
+ opts.ownerPassword,
696
+ (oP, oL) => this.g._withStr(
697
+ fileId,
698
+ (idP, idL) => this.g._withBytes(
699
+ key,
700
+ (kP, kL) => this.g._buffer(
701
+ (o) => this.ex().gp_save_encrypted(
702
+ this.h,
703
+ pwP,
704
+ pwL,
705
+ oP,
706
+ oL,
707
+ idP,
708
+ idL,
709
+ kP,
710
+ kL,
711
+ algoCode,
712
+ permissions,
713
+ o
714
+ )
715
+ )
716
+ )
392
717
  )
393
718
  )
394
719
  );
@@ -532,6 +857,141 @@ var GigaPdfDoc = class {
532
857
  (nP, nL) => this.g._withStr(values.join("\n"), (vP, vL) => this.ex().gp_set_choice(this.h, nP, nL, vP, vL))
533
858
  ) === 0;
534
859
  }
860
+ // ── form field creation ──────────────────────────────────────────────────
861
+ /**
862
+ * Create a text field on `page` covering `rect` = `[x0, y0, x1, y1]` (PDF
863
+ * user space). Options: `maxLen` character cap, `multiline`, `password`,
864
+ * and visual `style`.
865
+ */
866
+ addTextField(page, name, rect, value = "", opts = {}) {
867
+ const st = styleArgs(opts.style);
868
+ return this.g._withStr(
869
+ name,
870
+ (nP, nL) => this.g._withStr(
871
+ value,
872
+ (vP, vL) => this.ex().gp_add_text_field(
873
+ this.h,
874
+ page,
875
+ nP,
876
+ nL,
877
+ rect[0],
878
+ rect[1],
879
+ rect[2],
880
+ rect[3],
881
+ vP,
882
+ vL,
883
+ opts.maxLen ?? -1,
884
+ opts.multiline ? 1 : 0,
885
+ opts.password ? 1 : 0,
886
+ ...st
887
+ )
888
+ )
889
+ ) === 0;
890
+ }
891
+ /** Create a checkbox. `export` is the on-state name (default `On`). */
892
+ addCheckbox(page, name, rect, checked = false, opts = {}) {
893
+ const st = styleArgs(opts.style);
894
+ return this.g._withStr(
895
+ name,
896
+ (nP, nL) => this.g._withStr(
897
+ opts.export ?? "On",
898
+ (eP, eL) => this.ex().gp_add_checkbox(
899
+ this.h,
900
+ page,
901
+ nP,
902
+ nL,
903
+ rect[0],
904
+ rect[1],
905
+ rect[2],
906
+ rect[3],
907
+ checked ? 1 : 0,
908
+ eP,
909
+ eL,
910
+ ...st
911
+ )
912
+ )
913
+ ) === 0;
914
+ }
915
+ /**
916
+ * Create a radio-button group: one logical field whose `options` are the
917
+ * individual buttons. `selected` is the initially-chosen export value.
918
+ */
919
+ addRadioGroup(page, name, options, opts = {}) {
920
+ const st = styleArgs(opts.style);
921
+ const exports2 = options.map((o) => o.export).join("\n");
922
+ const rects = options.flatMap((o) => o.rect).join(",");
923
+ return this.g._withStr(
924
+ name,
925
+ (nP, nL) => this.g._withStr(
926
+ exports2,
927
+ (eP, eL) => this.g._withStr(
928
+ rects,
929
+ (rP, rL) => this.g._withStr(
930
+ opts.selected ?? "",
931
+ (sP, sL) => this.ex().gp_add_radio_group(this.h, page, nP, nL, eP, eL, rP, rL, sP, sL, ...st)
932
+ )
933
+ )
934
+ )
935
+ ) === 0;
936
+ }
937
+ /** Create a drop-down combo box. `editable` permits values outside `options`. */
938
+ addComboBox(page, name, rect, options, opts = {}) {
939
+ const st = styleArgs(opts.style);
940
+ return this.g._withStr(
941
+ name,
942
+ (nP, nL) => this.g._withStr(
943
+ options.join("\n"),
944
+ (oP, oL) => this.g._withStr(
945
+ opts.selected ?? "",
946
+ (sP, sL) => this.ex().gp_add_combo_box(
947
+ this.h,
948
+ page,
949
+ nP,
950
+ nL,
951
+ rect[0],
952
+ rect[1],
953
+ rect[2],
954
+ rect[3],
955
+ oP,
956
+ oL,
957
+ sP,
958
+ sL,
959
+ opts.editable ? 1 : 0,
960
+ ...st
961
+ )
962
+ )
963
+ )
964
+ ) === 0;
965
+ }
966
+ /** Create a scrolling list box. `multi` allows selecting several options. */
967
+ addListBox(page, name, rect, options, opts = {}) {
968
+ const st = styleArgs(opts.style);
969
+ return this.g._withStr(
970
+ name,
971
+ (nP, nL) => this.g._withStr(
972
+ options.join("\n"),
973
+ (oP, oL) => this.g._withStr(
974
+ opts.selected ?? "",
975
+ (sP, sL) => this.ex().gp_add_list_box(
976
+ this.h,
977
+ page,
978
+ nP,
979
+ nL,
980
+ rect[0],
981
+ rect[1],
982
+ rect[2],
983
+ rect[3],
984
+ oP,
985
+ oL,
986
+ sP,
987
+ sL,
988
+ opts.multi ? 1 : 0,
989
+ ...st
990
+ )
991
+ )
992
+ )
993
+ ) === 0;
994
+ }
535
995
  };
536
996
  // Annotate the CommonJS export names for ESM import in node:
537
997
  0 && (module.exports = {