@qrcommunication/gigapdf-lib 0.1.0 → 0.7.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.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @giga-pdf/wasm-engine — TypeScript SDK for the zero-dependency Rust→WASM PDF
3
- * engine (gigapdf-engine). Wraps the flat `extern "C"` `gp_*` ABI behind a typed,
2
+ * @qrcommunication/gigapdf-lib — TypeScript SDK for the zero-dependency Rust→WASM
3
+ * PDF engine (gigapdf-lib). Wraps the flat `extern "C"` `gp_*` ABI behind a typed,
4
4
  * ergonomic class. No third-party runtime deps; the `.wasm` is self-contained.
5
5
  *
6
6
  * Usage:
@@ -42,6 +42,8 @@ declare class GigaPdfEngine {
42
42
  _json<T = unknown>(call: (outLenPtr: number) => number): T;
43
43
  /** Pass a string argument; runs `fn(ptr, len)` then frees. */
44
44
  _withStr<T>(s: string, fn: (ptr: number, len: number) => T): T;
45
+ /** Pass an optional string; an absent/empty value runs `fn(0, 0)` (no alloc). */
46
+ _withOptStr<T>(s: string | undefined, fn: (ptr: number, len: number) => T): T;
45
47
  /** Pass a bytes argument; runs `fn(ptr, len)` then frees. */
46
48
  _withBytes<T>(bytes: Uint8Array, fn: (ptr: number, len: number) => T): T;
47
49
  /** Pass a `u32[]` argument (e.g. page numbers); runs `fn(ptr, count)` then frees. */
@@ -63,6 +65,102 @@ declare class GigaPdfEngine {
63
65
  fontRequestUrl(family: string, weight?: number, italic?: boolean): string;
64
66
  /** Extract the trusted gstatic font URL from a Google Fonts CSS2 response. */
65
67
  parseCssFontUrl(css: string): string;
68
+ /**
69
+ * Evaluate a JavaScript snippet with the built-in engine and return the
70
+ * result value as a string (or `Uncaught …` / `SyntaxError: …`).
71
+ */
72
+ evalJs(src: string): string;
73
+ /**
74
+ * Run a document's inline `<script>`s and return the resulting HTML. The
75
+ * `htmlRender`/`htmlNeededFonts` paths already do this automatically; use this
76
+ * only when you want the post-script HTML on its own.
77
+ */
78
+ runInlineScripts(html: string): string;
79
+ /**
80
+ * Phase 1 — the Google fonts the document references. Download each `url`
81
+ * (→ TTF) and pass the bytes back to {@link htmlRender} for an identical render.
82
+ */
83
+ htmlNeededFonts(html: string): HtmlFontRequest[];
84
+ /**
85
+ * Phase 2 — render HTML + CSS to PDF, with the supplied fonts embedded (real
86
+ * Google fonts, real metrics → identical or nearest match). Block, inline and
87
+ * table layout with pagination. Page size and margin are in points
88
+ * (US-Letter portrait, 0.5in margins by default). JavaScript is not executed.
89
+ */
90
+ htmlRender(html: string, fonts?: HtmlFont[], pageWidth?: number, pageHeight?: number, margin?: number): Uint8Array;
91
+ /**
92
+ * Resolve a named paper size — `"A4"`, `"a3-landscape"`, `"letter"`, `"legal"`,
93
+ * `"tabloid"`, `"b5"`, … — to `{ w, h }` in points (portrait unless the name
94
+ * has a `-landscape` suffix). Returns `null` for an unknown name.
95
+ */
96
+ pageSize(name: string): {
97
+ w: number;
98
+ h: number;
99
+ } | null;
100
+ /**
101
+ * Phase 1 variant that also scans the running `header`/`footer` HTML, so the
102
+ * Google fonts they reference are requested alongside the body's.
103
+ */
104
+ htmlNeededFontsWith(html: string, header?: string, footer?: string): HtmlFontRequest[];
105
+ /**
106
+ * Phase 2 with full page control: named/explicit size, per-side margins, and a
107
+ * running header/footer painted in the page margins. `{{page}}` and `{{pages}}`
108
+ * in the header/footer are replaced with the current / total page number.
109
+ *
110
+ * ```ts
111
+ * const fonts = await fetchFonts(giga.htmlNeededFontsWith(html, header, footer));
112
+ * const pdf = giga.htmlRenderWith(html, fonts, {
113
+ * pageSize: "A4",
114
+ * margin: { top: 72, bottom: 72, left: 54, right: 54 },
115
+ * header: `<div style="text-align:center">My Report</div>`,
116
+ * footer: `<div style="text-align:center">Page {{page}} / {{pages}}</div>`,
117
+ * });
118
+ * ```
119
+ */
120
+ htmlRenderWith(html: string, fonts?: HtmlFont[], options?: HtmlRenderOptions): Uint8Array;
121
+ }
122
+ /** A Google font the HTML engine needs (resolved from the catalogue). */
123
+ interface HtmlFontRequest {
124
+ family: string;
125
+ weight: number;
126
+ italic: boolean;
127
+ /** Google Fonts CSS URL — the host fetches it to obtain the TTF. */
128
+ url: string;
129
+ }
130
+ /** A downloaded font handed to {@link GigaPdfEngine.htmlRender}. */
131
+ interface HtmlFont {
132
+ family: string;
133
+ weight: number;
134
+ italic: boolean;
135
+ ttf: Uint8Array;
136
+ }
137
+ /** Per-side page margins in points; omitted sides default to 36pt. */
138
+ interface HtmlMargins {
139
+ top?: number;
140
+ right?: number;
141
+ bottom?: number;
142
+ left?: number;
143
+ }
144
+ /** Page setup for {@link GigaPdfEngine.htmlRenderWith}. */
145
+ interface HtmlRenderOptions {
146
+ /** Named paper size (`"A4"`, `"a3-landscape"`, `"letter"`, …) — wins over width/height. */
147
+ pageSize?: string;
148
+ /** Explicit page width in points (default 612 = US Letter). Ignored if `pageSize` is set. */
149
+ pageWidth?: number;
150
+ /** Explicit page height in points (default 792). Ignored if `pageSize` is set. */
151
+ pageHeight?: number;
152
+ /** Uniform margin (points) or per-side margins. Default 36pt (0.5in). */
153
+ margin?: number | HtmlMargins;
154
+ /** Running header HTML painted in the top margin (`{{page}}` / `{{pages}}` tokens). */
155
+ header?: string;
156
+ /** Running footer HTML painted in the bottom margin (same tokens). */
157
+ footer?: string;
158
+ /** Distance from the top edge to the header block, in points (default 18). */
159
+ headerOffset?: number;
160
+ /** Distance from the bottom edge to the footer block, in points (default 18). */
161
+ footerOffset?: number;
162
+ /** Number assigned to the first page for `{{page}}` (default 1). */
163
+ startPageNumber?: number;
66
164
  }
67
165
  interface FontInfo {
68
166
  family: string;
@@ -152,6 +250,26 @@ interface PageInfo {
152
250
  height: number;
153
251
  rotation: number;
154
252
  }
253
+ /** Visual styling for a newly-created form field. */
254
+ interface FieldStyle {
255
+ /** Text size in points; `0` (default) auto-sizes to the field box. */
256
+ fontSize?: number;
257
+ /** Text / mark colour `0xRRGGBB` (default black). */
258
+ color?: number;
259
+ /** Border colour `0xRRGGBB`, or `null` for no border (default black). */
260
+ border?: number | null;
261
+ /** Background fill `0xRRGGBB`, or `null` for transparent (default none). */
262
+ background?: number | null;
263
+ /** Border width in points (default `1`). */
264
+ borderWidth?: number;
265
+ }
266
+ /** One option of a radio group: its export value and on-page rectangle. */
267
+ interface RadioOption {
268
+ /** The export value stored in the field when this button is selected. */
269
+ export: string;
270
+ /** `[x0, y0, x1, y1]` in PDF user space. */
271
+ rect: [number, number, number, number];
272
+ }
155
273
  /** A live document handle. Call {@link close} when done. */
156
274
  declare class GigaPdfDoc {
157
275
  private readonly g;
@@ -179,7 +297,37 @@ declare class GigaPdfDoc {
179
297
  * Draw a vector rectangle. Pass an `0xRRGGBB` colour for `stroke`/`fill`, or
180
298
  * `null` to omit that paint. 0 → success.
181
299
  */
182
- addRectangle(page: number, x: number, y: number, w: number, h: number, stroke?: number | null, fill?: number | null, lineWidth?: number): boolean;
300
+ addRectangle(page: number, x: number, y: number, w: number, h: number, stroke?: number | null, fill?: number | null, lineWidth?: number, opacity?: number): boolean;
301
+ /** Draw a straight line from `(x1,y1)` to `(x2,y2)`. `stroke` is `0xRRGGBB`. */
302
+ drawLine(page: number, x1: number, y1: number, x2: number, y2: number, stroke?: number, lineWidth?: number, opacity?: number): boolean;
303
+ /**
304
+ * Draw an ellipse (circle when `rx === ry`) centred at `(cx,cy)`. Pass an
305
+ * `0xRRGGBB` colour for `stroke`/`fill`, or `null` to omit that paint.
306
+ */
307
+ addEllipse(page: number, cx: number, cy: number, rx: number, ry: number, stroke?: number | null, fill?: number | null, lineWidth?: number, opacity?: number): boolean;
308
+ /**
309
+ * Draw a polyline/polygon through flat `[x0,y0,x1,y1,…]` points. `close` joins
310
+ * the last vertex back to the first. `0xRRGGBB` colours, or `null` to omit.
311
+ */
312
+ addPolygon(page: number, points: number[], close?: boolean, stroke?: number | null, fill?: number | null, lineWidth?: number, opacity?: number): boolean;
313
+ /**
314
+ * Draw an SVG path (`M`/`L`/`C`/`Q`/`Z`…) anchored so the SVG origin maps to
315
+ * `(ox,oy)` with the Y axis flipped — same convention as `pdf-lib`'s
316
+ * `drawSvgPath`. Covers freeform/polygon/triangle shapes.
317
+ */
318
+ addPath(page: number, svgPath: string, ox: number, oy: number, stroke?: number | null, fill?: number | null, lineWidth?: number, opacity?: number): boolean;
319
+ /**
320
+ * Embed a raster image (PNG or JPEG bytes) at `(x,y)` sized `(w,h)` in PDF
321
+ * user space. PNG alpha is honoured; `opacity` (0..1) sets an overall alpha.
322
+ */
323
+ addImage(page: number, data: Uint8Array, x: number, y: number, w: number, h: number, opacity?: number): boolean;
324
+ /**
325
+ * Draw SVG markup on a page as **native vector paths** (crisp at any zoom, not
326
+ * rasterized), fitting its `viewBox` into the box `(x, y, w, h)` in PDF points
327
+ * (origin bottom-left). Supports shapes, `<path>`, groups, transforms and
328
+ * fill/stroke/opacity. Returns `false` if the SVG can't be parsed.
329
+ */
330
+ addSvg(page: number, svg: string, x: number, y: number, w: number, h: number): boolean;
183
331
  /** True redaction: delete content intersecting the region (no opaque cover by default). */
184
332
  redact(page: number, x: number, y: number, w: number, h: number, coverRgb?: number, hasCover?: boolean): number;
185
333
  rotatePage(page: number, degrees: number): boolean;
@@ -204,6 +352,8 @@ declare class GigaPdfDoc {
204
352
  toHtml(): string;
205
353
  toDocx(): Uint8Array;
206
354
  toPptx(): Uint8Array;
355
+ /** Convert to an editable OpenDocument Presentation (`.odp`). */
356
+ toOdp(): Uint8Array;
207
357
  toOdt(): Uint8Array;
208
358
  toXlsx(): Uint8Array;
209
359
  toOds(): Uint8Array;
@@ -245,6 +395,42 @@ declare class GigaPdfDoc {
245
395
  setRadio(name: string, value: string): boolean;
246
396
  /** Set a choice field's selection (multi-select list boxes accept several values). */
247
397
  setChoice(name: string, values: string[]): boolean;
398
+ /**
399
+ * Create a text field on `page` covering `rect` = `[x0, y0, x1, y1]` (PDF
400
+ * user space). Options: `maxLen` character cap, `multiline`, `password`,
401
+ * and visual `style`.
402
+ */
403
+ addTextField(page: number, name: string, rect: [number, number, number, number], value?: string, opts?: {
404
+ maxLen?: number;
405
+ multiline?: boolean;
406
+ password?: boolean;
407
+ style?: FieldStyle;
408
+ }): boolean;
409
+ /** Create a checkbox. `export` is the on-state name (default `On`). */
410
+ addCheckbox(page: number, name: string, rect: [number, number, number, number], checked?: boolean, opts?: {
411
+ export?: string;
412
+ style?: FieldStyle;
413
+ }): boolean;
414
+ /**
415
+ * Create a radio-button group: one logical field whose `options` are the
416
+ * individual buttons. `selected` is the initially-chosen export value.
417
+ */
418
+ addRadioGroup(page: number, name: string, options: RadioOption[], opts?: {
419
+ selected?: string;
420
+ style?: FieldStyle;
421
+ }): boolean;
422
+ /** Create a drop-down combo box. `editable` permits values outside `options`. */
423
+ addComboBox(page: number, name: string, rect: [number, number, number, number], options: string[], opts?: {
424
+ selected?: string;
425
+ editable?: boolean;
426
+ style?: FieldStyle;
427
+ }): boolean;
428
+ /** Create a scrolling list box. `multi` allows selecting several options. */
429
+ addListBox(page: number, name: string, rect: [number, number, number, number], options: string[], opts?: {
430
+ selected?: string;
431
+ multi?: boolean;
432
+ style?: FieldStyle;
433
+ }): boolean;
248
434
  }
249
435
 
250
- export { type AnnotationInfo, type Box, type Element, type FieldInfo, type FieldKind, type FontInfo, GigaPdfDoc, GigaPdfEngine, type LayerInfo, type LinkInfo, type OcrWord, type OutlineEntry, type PageInfo, type SearchHit, type TextLine, type TextRunInfo };
436
+ export { type AnnotationInfo, type Box, type Element, type FieldInfo, type FieldKind, type FieldStyle, type FontInfo, GigaPdfDoc, GigaPdfEngine, type HtmlFont, type HtmlFontRequest, type HtmlMargins, type HtmlRenderOptions, type LayerInfo, type LinkInfo, type OcrWord, type OutlineEntry, type PageInfo, type RadioOption, type SearchHit, type TextLine, type TextRunInfo };
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
249
464
  ) === 0;
250
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)
520
+ ) === 0;
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
  }
@@ -491,6 +775,141 @@ var GigaPdfDoc = class {
491
775
  (nP, nL) => this.g._withStr(values.join("\n"), (vP, vL) => this.ex().gp_set_choice(this.h, nP, nL, vP, vL))
492
776
  ) === 0;
493
777
  }
778
+ // ── form field creation ──────────────────────────────────────────────────
779
+ /**
780
+ * Create a text field on `page` covering `rect` = `[x0, y0, x1, y1]` (PDF
781
+ * user space). Options: `maxLen` character cap, `multiline`, `password`,
782
+ * and visual `style`.
783
+ */
784
+ addTextField(page, name, rect, value = "", opts = {}) {
785
+ const st = styleArgs(opts.style);
786
+ return this.g._withStr(
787
+ name,
788
+ (nP, nL) => this.g._withStr(
789
+ value,
790
+ (vP, vL) => this.ex().gp_add_text_field(
791
+ this.h,
792
+ page,
793
+ nP,
794
+ nL,
795
+ rect[0],
796
+ rect[1],
797
+ rect[2],
798
+ rect[3],
799
+ vP,
800
+ vL,
801
+ opts.maxLen ?? -1,
802
+ opts.multiline ? 1 : 0,
803
+ opts.password ? 1 : 0,
804
+ ...st
805
+ )
806
+ )
807
+ ) === 0;
808
+ }
809
+ /** Create a checkbox. `export` is the on-state name (default `On`). */
810
+ addCheckbox(page, name, rect, checked = false, opts = {}) {
811
+ const st = styleArgs(opts.style);
812
+ return this.g._withStr(
813
+ name,
814
+ (nP, nL) => this.g._withStr(
815
+ opts.export ?? "On",
816
+ (eP, eL) => this.ex().gp_add_checkbox(
817
+ this.h,
818
+ page,
819
+ nP,
820
+ nL,
821
+ rect[0],
822
+ rect[1],
823
+ rect[2],
824
+ rect[3],
825
+ checked ? 1 : 0,
826
+ eP,
827
+ eL,
828
+ ...st
829
+ )
830
+ )
831
+ ) === 0;
832
+ }
833
+ /**
834
+ * Create a radio-button group: one logical field whose `options` are the
835
+ * individual buttons. `selected` is the initially-chosen export value.
836
+ */
837
+ addRadioGroup(page, name, options, opts = {}) {
838
+ const st = styleArgs(opts.style);
839
+ const exports = options.map((o) => o.export).join("\n");
840
+ const rects = options.flatMap((o) => o.rect).join(",");
841
+ return this.g._withStr(
842
+ name,
843
+ (nP, nL) => this.g._withStr(
844
+ exports,
845
+ (eP, eL) => this.g._withStr(
846
+ rects,
847
+ (rP, rL) => this.g._withStr(
848
+ opts.selected ?? "",
849
+ (sP, sL) => this.ex().gp_add_radio_group(this.h, page, nP, nL, eP, eL, rP, rL, sP, sL, ...st)
850
+ )
851
+ )
852
+ )
853
+ ) === 0;
854
+ }
855
+ /** Create a drop-down combo box. `editable` permits values outside `options`. */
856
+ addComboBox(page, name, rect, options, opts = {}) {
857
+ const st = styleArgs(opts.style);
858
+ return this.g._withStr(
859
+ name,
860
+ (nP, nL) => this.g._withStr(
861
+ options.join("\n"),
862
+ (oP, oL) => this.g._withStr(
863
+ opts.selected ?? "",
864
+ (sP, sL) => this.ex().gp_add_combo_box(
865
+ this.h,
866
+ page,
867
+ nP,
868
+ nL,
869
+ rect[0],
870
+ rect[1],
871
+ rect[2],
872
+ rect[3],
873
+ oP,
874
+ oL,
875
+ sP,
876
+ sL,
877
+ opts.editable ? 1 : 0,
878
+ ...st
879
+ )
880
+ )
881
+ )
882
+ ) === 0;
883
+ }
884
+ /** Create a scrolling list box. `multi` allows selecting several options. */
885
+ addListBox(page, name, rect, options, opts = {}) {
886
+ const st = styleArgs(opts.style);
887
+ return this.g._withStr(
888
+ name,
889
+ (nP, nL) => this.g._withStr(
890
+ options.join("\n"),
891
+ (oP, oL) => this.g._withStr(
892
+ opts.selected ?? "",
893
+ (sP, sL) => this.ex().gp_add_list_box(
894
+ this.h,
895
+ page,
896
+ nP,
897
+ nL,
898
+ rect[0],
899
+ rect[1],
900
+ rect[2],
901
+ rect[3],
902
+ oP,
903
+ oL,
904
+ sP,
905
+ sL,
906
+ opts.multi ? 1 : 0,
907
+ ...st
908
+ )
909
+ )
910
+ )
911
+ ) === 0;
912
+ }
494
913
  };
495
914
  export {
496
915
  GigaPdfDoc,