@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/editor.js ADDED
@@ -0,0 +1,1126 @@
1
+ // src/viewer.ts
2
+ var CSS = `
3
+ .gpv{position:relative;display:flex;flex-direction:column;height:100%;width:100%;background:var(--gpv-bg,#525659);color:#eee;font:13px/1.4 system-ui,sans-serif;overflow:hidden}
4
+ .gpv-bar{display:flex;align-items:center;gap:6px;padding:6px 10px;background:#323639;border-bottom:1px solid #000;flex:0 0 auto;user-select:none}
5
+ .gpv-bar button{background:#4a4f52;color:#eee;border:0;border-radius:4px;padding:4px 9px;cursor:pointer;font:inherit}
6
+ .gpv-bar button:hover{background:#5a6063}
7
+ .gpv-bar .gpv-sp{flex:1}
8
+ .gpv-bar input{width:46px;background:#222;color:#eee;border:1px solid #000;border-radius:4px;padding:3px;text-align:center;font:inherit}
9
+ .gpv-bar select{background:#222;color:#eee;border:1px solid #000;border-radius:4px;padding:3px;font:inherit;cursor:pointer}
10
+ .gpv-bar .gpv-zoom{min-width:44px;text-align:center;color:#ddd;font-variant-numeric:tabular-nums}
11
+ .gpv-body{flex:1;display:flex;min-height:0}
12
+ .gpv-thumbs{flex:0 0 132px;overflow-y:auto;background:#2a2d2f;padding:8px;display:flex;flex-direction:column;gap:8px}
13
+ .gpv-thumbs img{width:100%;display:block;border:2px solid transparent;border-radius:2px;cursor:pointer;background:#fff}
14
+ .gpv-thumbs .gpv-on img{border-color:#3b82f6}
15
+ .gpv-thumbs span{display:block;text-align:center;color:#aaa;font-size:11px}
16
+ .gpv-pages{flex:1;overflow:auto;padding:18px;display:flex;flex-direction:column;align-items:center;gap:18px;scroll-behavior:smooth}
17
+ .gpv-page{position:relative;box-shadow:0 1px 6px rgba(0,0,0,.6);background:#fff}
18
+ .gpv-page img{display:block;width:100%;height:auto}
19
+ .gpv:fullscreen{--gpv-bg:#000}
20
+ .gpv.gpv-present .gpv-thumbs,.gpv.gpv-present .gpv-bar{display:none}
21
+ .gpv.gpv-present .gpv-pages{padding:0;justify-content:center;overflow:hidden}
22
+ .gpv.gpv-present .gpv-page{box-shadow:none;max-height:100vh}
23
+ .gpv.gpv-present .gpv-page img{width:auto;max-width:100vw;max-height:100vh}
24
+ `;
25
+ var styleInjected = false;
26
+ function injectStyle(doc) {
27
+ if (styleInjected) return;
28
+ const el = doc.createElement("style");
29
+ el.textContent = CSS;
30
+ doc.head.appendChild(el);
31
+ styleInjected = true;
32
+ }
33
+ function pngSize(b) {
34
+ if (b.length < 24) return { w: 1, h: 1 };
35
+ const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
36
+ return { w: dv.getUint32(16) || 1, h: dv.getUint32(20) || 1 };
37
+ }
38
+ var GigaPdfViewer = class {
39
+ constructor(engine, container, options = {}) {
40
+ this.engine = engine;
41
+ this.options = options;
42
+ this.cssScale = options.scale ?? 1;
43
+ this.renderScale = options.renderScale ?? 2;
44
+ this.root = container;
45
+ injectStyle(container.ownerDocument);
46
+ this.buildChrome();
47
+ this.onKey = (e) => this.handleKey(e);
48
+ this.onFsChange = () => this.syncPresentClass();
49
+ container.ownerDocument.addEventListener("keydown", this.onKey);
50
+ container.ownerDocument.addEventListener("fullscreenchange", this.onFsChange);
51
+ }
52
+ engine;
53
+ options;
54
+ // `protected` members are the surface the editor subclass builds on.
55
+ doc = null;
56
+ count = 0;
57
+ current = 1;
58
+ cssScale;
59
+ renderScale;
60
+ /** Active auto-fit mode; re-applied on resize and page change. */
61
+ fitMode = "none";
62
+ presenting = false;
63
+ resizeObs = null;
64
+ // Per-page caches (1-indexed; slot 0 unused).
65
+ urls = [];
66
+ sizes = [];
67
+ // DOM nodes.
68
+ root;
69
+ bar = null;
70
+ body;
71
+ thumbs;
72
+ pages;
73
+ pageInput = null;
74
+ zoomReadout = null;
75
+ zoomSelect = null;
76
+ pageEls = [];
77
+ onKey;
78
+ onFsChange;
79
+ /** Number of pages currently open. */
80
+ get pageCount() {
81
+ return this.count;
82
+ }
83
+ /** The page currently in view (1-based). */
84
+ get currentPage() {
85
+ return this.current;
86
+ }
87
+ /** Orientation of `page` (after open). */
88
+ orientation(page) {
89
+ const s = this.sizes[page];
90
+ return s && s.w > s.h ? "landscape" : "portrait";
91
+ }
92
+ /** The current document as PDF bytes (including any applied edits). */
93
+ save() {
94
+ if (!this.doc) return new Uint8Array(0);
95
+ return this.doc.save();
96
+ }
97
+ /** Open a document; Office/HTML are converted to PDF in-engine. Returns the page count. */
98
+ async open(src) {
99
+ const pdf = this.toPdf(src);
100
+ this.close();
101
+ this.doc = this.engine.open(pdf);
102
+ this.count = this.doc.pageCount();
103
+ this.urls = new Array(this.count + 1).fill(null);
104
+ this.sizes = new Array(this.count + 1).fill(null);
105
+ this.pageEls = new Array(this.count + 1).fill(null);
106
+ this.current = 1;
107
+ this.renderAllPages();
108
+ if (this.options.thumbnails !== false) this.buildThumbs();
109
+ this.goTo(1);
110
+ return this.count;
111
+ }
112
+ toPdf(src) {
113
+ switch (src.kind) {
114
+ case "pdf":
115
+ return src.bytes;
116
+ case "office":
117
+ return this.engine.officeToPdf(src.bytes);
118
+ case "html":
119
+ return src.fonts?.length ? this.engine.htmlRender(src.html, src.fonts) : this.engine.htmlToPdf(src.html);
120
+ case "auto":
121
+ return this.detectAndConvert(src.bytes);
122
+ }
123
+ }
124
+ detectAndConvert(b) {
125
+ const is = (...sig) => sig.every((v, i) => b[i] === v);
126
+ if (is(37, 80, 68, 70)) return b;
127
+ if (is(80, 75)) return this.engine.officeToPdf(b);
128
+ if (is(208, 207, 17, 224)) return this.engine.officeToPdf(b);
129
+ return this.engine.htmlToPdf(new TextDecoder().decode(b));
130
+ }
131
+ // ── rendering ────────────────────────────────────────────────────────────────
132
+ renderPageRaster(page) {
133
+ const cachedUrl = this.urls[page];
134
+ const cachedSize = this.sizes[page];
135
+ if (cachedUrl && cachedSize) return { url: cachedUrl, ...cachedSize };
136
+ const png = this.doc.renderPage(page, this.renderScale);
137
+ const size = pngSize(png);
138
+ const buf = new ArrayBuffer(png.byteLength);
139
+ new Uint8Array(buf).set(png);
140
+ const url = URL.createObjectURL(new Blob([buf], { type: "image/png" }));
141
+ this.urls[page] = url;
142
+ this.sizes[page] = size;
143
+ return { url, ...size };
144
+ }
145
+ /** The page's width / height in PDF points (raster pixels ÷ render scale). */
146
+ pageWidthPt(page) {
147
+ const s = this.sizes[page];
148
+ return s ? s.w / this.renderScale : 0;
149
+ }
150
+ pageHeightPt(page) {
151
+ const s = this.sizes[page];
152
+ return s ? s.h / this.renderScale : 0;
153
+ }
154
+ /** Re-raster a page after its content changed (drops the cached image). */
155
+ reRenderPage(page) {
156
+ const old = this.urls[page];
157
+ if (old) URL.revokeObjectURL(old);
158
+ this.urls[page] = null;
159
+ this.sizes[page] = null;
160
+ const { url, w } = this.renderPageRaster(page);
161
+ const box = this.pageEls[page];
162
+ if (!box) return;
163
+ box.style.width = `${w / this.renderScale * this.cssScale}px`;
164
+ const img = box.querySelector("img");
165
+ if (img) img.src = url;
166
+ }
167
+ /** Hook for subclasses (the editor) to attach per-page overlays. */
168
+ afterRender() {
169
+ }
170
+ renderAllPages() {
171
+ this.pages.replaceChildren();
172
+ for (let p = 1; p <= this.count; p++) {
173
+ const { url, w, h } = this.renderPageRaster(p);
174
+ const box = this.root.ownerDocument.createElement("div");
175
+ box.className = "gpv-page";
176
+ box.dataset.page = String(p);
177
+ box.style.width = `${w / this.renderScale * this.cssScale}px`;
178
+ const img = this.root.ownerDocument.createElement("img");
179
+ img.src = url;
180
+ img.alt = `Page ${p}`;
181
+ img.loading = "lazy";
182
+ box.appendChild(img);
183
+ this.pages.appendChild(box);
184
+ this.pageEls[p] = box;
185
+ }
186
+ this.afterRender();
187
+ }
188
+ buildThumbs() {
189
+ this.thumbs.replaceChildren();
190
+ for (let p = 1; p <= this.count; p++) {
191
+ const url = this.urls[p];
192
+ const item = this.root.ownerDocument.createElement("div");
193
+ item.dataset.page = String(p);
194
+ const img = this.root.ownerDocument.createElement("img");
195
+ if (url) img.src = url;
196
+ img.addEventListener("click", () => this.goTo(p));
197
+ const label = this.root.ownerDocument.createElement("span");
198
+ label.textContent = String(p);
199
+ item.append(img, label);
200
+ this.thumbs.appendChild(item);
201
+ }
202
+ this.highlightThumb();
203
+ }
204
+ highlightThumb() {
205
+ for (const child of Array.from(this.thumbs.children)) {
206
+ child.classList.toggle("gpv-on", child.getAttribute("data-page") === String(this.current));
207
+ }
208
+ }
209
+ // ── navigation ───────────────────────────────────────────────────────────────
210
+ /** Scroll/jump to `page` (clamped to range). */
211
+ goTo(page) {
212
+ if (this.count === 0) return;
213
+ this.current = Math.min(Math.max(1, Math.round(page)), this.count);
214
+ if (this.presenting) {
215
+ for (let p = 1; p <= this.count; p++) {
216
+ const el = this.pageEls[p];
217
+ if (el) el.style.display = p === this.current ? "" : "none";
218
+ }
219
+ } else {
220
+ this.pageEls[this.current]?.scrollIntoView({ block: "start" });
221
+ }
222
+ if (this.pageInput) this.pageInput.value = String(this.current);
223
+ if (this.fitMode !== "none") this.applyFitMode();
224
+ this.highlightThumb();
225
+ }
226
+ next() {
227
+ this.goTo(this.current + 1);
228
+ }
229
+ prev() {
230
+ this.goTo(this.current - 1);
231
+ }
232
+ // ── zoom ─────────────────────────────────────────────────────────────────────
233
+ /** Current CSS zoom multiplier (1 = 100%). */
234
+ get zoom() {
235
+ return this.cssScale;
236
+ }
237
+ /** Resize the page boxes to the current `cssScale` (no re-raster). */
238
+ applyScale(scale) {
239
+ this.cssScale = Math.min(Math.max(0.08, scale), 8);
240
+ for (let p = 1; p <= this.count; p++) {
241
+ const box = this.pageEls[p];
242
+ const size = this.sizes[p];
243
+ if (box && size) box.style.width = `${size.w / this.renderScale * this.cssScale}px`;
244
+ }
245
+ this.updateZoomReadout();
246
+ this.onZoomChange();
247
+ }
248
+ /** Set an explicit zoom multiplier (cancels any auto-fit mode). */
249
+ setZoom(scale) {
250
+ this.fitMode = "none";
251
+ this.applyScale(scale);
252
+ }
253
+ /** Set zoom as a percentage (e.g. `125`). */
254
+ setZoomPercent(pct) {
255
+ this.setZoom(pct / 100);
256
+ }
257
+ /** Reset to 100 % (actual size). */
258
+ actualSize() {
259
+ this.setZoom(1);
260
+ }
261
+ zoomIn() {
262
+ this.setZoom(this.cssScale * 1.2);
263
+ }
264
+ zoomOut() {
265
+ this.setZoom(this.cssScale / 1.2);
266
+ }
267
+ /** Fit the current page's **width** to the viewport (sticks across resizes). */
268
+ fitWidth() {
269
+ this.fitMode = "width";
270
+ this.applyFitMode();
271
+ }
272
+ /** Fit the **whole** current page (width *and* height) to the viewport. */
273
+ fitPage() {
274
+ this.fitMode = "page";
275
+ this.applyFitMode();
276
+ }
277
+ /** Recompute the zoom for the active fit mode against the current page. */
278
+ applyFitMode() {
279
+ if (this.fitMode === "none" || this.presenting || this.count === 0) return;
280
+ const size = this.sizes[this.current];
281
+ if (!size) return;
282
+ const wPt = size.w / this.renderScale;
283
+ const hPt = size.h / this.renderScale;
284
+ if (this.fitMode === "width") {
285
+ this.applyScale((this.pages.clientWidth - 36) / wPt || 1);
286
+ } else {
287
+ const availW = this.pages.clientWidth - 36;
288
+ const availH = this.pages.clientHeight - 36;
289
+ this.applyScale(Math.min(availW / wPt, availH / hPt) || 1);
290
+ }
291
+ }
292
+ /** Hook for subclasses (the editor) to react to zoom changes. */
293
+ onZoomChange() {
294
+ }
295
+ updateZoomReadout() {
296
+ if (this.zoomReadout) this.zoomReadout.textContent = `${Math.round(this.cssScale * 100)}%`;
297
+ if (this.zoomSelect) {
298
+ const v = this.fitMode === "width" ? "width" : this.fitMode === "page" ? "page" : String(this.cssScale);
299
+ const has = Array.from(this.zoomSelect.options).some((o) => o.value === v);
300
+ this.zoomSelect.value = has ? v : "";
301
+ }
302
+ }
303
+ // ── presentation (fullscreen slideshow) ───────────────────────────────────────
304
+ /** Enter fullscreen single-page presentation mode. */
305
+ present() {
306
+ this.presenting = true;
307
+ this.root.classList.add("gpv-present");
308
+ this.goTo(this.current);
309
+ void this.root.requestFullscreen?.();
310
+ }
311
+ /** Leave presentation mode. */
312
+ exitPresent() {
313
+ this.presenting = false;
314
+ this.root.classList.remove("gpv-present");
315
+ if (this.root.ownerDocument.fullscreenElement) void this.root.ownerDocument.exitFullscreen?.();
316
+ for (let p = 1; p <= this.count; p++) {
317
+ const el = this.pageEls[p];
318
+ if (el) el.style.display = "";
319
+ }
320
+ this.goTo(this.current);
321
+ }
322
+ syncPresentClass() {
323
+ if (this.presenting && !this.root.ownerDocument.fullscreenElement) this.exitPresent();
324
+ }
325
+ // ── keyboard ─────────────────────────────────────────────────────────────────
326
+ handleKey(e) {
327
+ if (!this.isActive()) return;
328
+ switch (e.key) {
329
+ case "ArrowRight":
330
+ case "PageDown":
331
+ case " ":
332
+ this.next();
333
+ break;
334
+ case "ArrowLeft":
335
+ case "PageUp":
336
+ this.prev();
337
+ break;
338
+ case "Home":
339
+ this.goTo(1);
340
+ break;
341
+ case "End":
342
+ this.goTo(this.count);
343
+ break;
344
+ case "+":
345
+ case "=":
346
+ this.zoomIn();
347
+ break;
348
+ case "-":
349
+ this.zoomOut();
350
+ break;
351
+ case "0":
352
+ this.actualSize();
353
+ break;
354
+ case "f":
355
+ case "F":
356
+ this.presenting ? this.exitPresent() : this.present();
357
+ break;
358
+ case "Escape":
359
+ if (this.presenting) this.exitPresent();
360
+ break;
361
+ default:
362
+ return;
363
+ }
364
+ e.preventDefault();
365
+ }
366
+ isActive() {
367
+ return this.presenting || this.root.contains(this.root.ownerDocument.activeElement) || this.root.matches(":hover");
368
+ }
369
+ // ── chrome ───────────────────────────────────────────────────────────────────
370
+ buildChrome() {
371
+ const d = this.root.ownerDocument;
372
+ this.root.classList.add("gpv");
373
+ if (this.options.background) this.root.style.setProperty("--gpv-bg", this.options.background);
374
+ this.root.replaceChildren();
375
+ if (this.options.toolbar !== false) {
376
+ const bar = d.createElement("div");
377
+ bar.className = "gpv-bar";
378
+ const btn = (label, fn, title) => {
379
+ const b = d.createElement("button");
380
+ b.textContent = label;
381
+ if (title) b.title = title;
382
+ b.addEventListener("click", fn);
383
+ bar.appendChild(b);
384
+ return b;
385
+ };
386
+ btn("\u2039", () => this.prev(), "Previous page");
387
+ btn("\u203A", () => this.next(), "Next page");
388
+ const input = d.createElement("input");
389
+ input.value = "1";
390
+ input.addEventListener("change", () => this.goTo(Number(input.value) || 1));
391
+ bar.appendChild(input);
392
+ this.pageInput = input;
393
+ btn("\u2212", () => this.zoomOut(), "Zoom out");
394
+ const zr = d.createElement("span");
395
+ zr.className = "gpv-zoom";
396
+ zr.textContent = "100%";
397
+ bar.appendChild(zr);
398
+ this.zoomReadout = zr;
399
+ btn("+", () => this.zoomIn(), "Zoom in");
400
+ const zsel = d.createElement("select");
401
+ zsel.title = "Zoom level";
402
+ const zopts = [
403
+ ["width", "Fit width"],
404
+ ["page", "Fit page"],
405
+ ["0.5", "50%"],
406
+ ["0.75", "75%"],
407
+ ["1", "100%"],
408
+ ["1.25", "125%"],
409
+ ["1.5", "150%"],
410
+ ["2", "200%"],
411
+ ["4", "400%"]
412
+ ];
413
+ for (const [val, label] of zopts) {
414
+ const o = d.createElement("option");
415
+ o.value = val;
416
+ o.textContent = label;
417
+ zsel.appendChild(o);
418
+ }
419
+ zsel.value = "1";
420
+ zsel.addEventListener("change", () => {
421
+ const v = zsel.value;
422
+ if (v === "width") this.fitWidth();
423
+ else if (v === "page") this.fitPage();
424
+ else if (v) this.setZoom(Number(v));
425
+ });
426
+ bar.appendChild(zsel);
427
+ this.zoomSelect = zsel;
428
+ const sp = d.createElement("div");
429
+ sp.className = "gpv-sp";
430
+ bar.appendChild(sp);
431
+ btn("\u26F6 Present", () => this.present(), "Fullscreen presentation (F)");
432
+ this.root.appendChild(bar);
433
+ this.bar = bar;
434
+ }
435
+ this.body = d.createElement("div");
436
+ this.body.className = "gpv-body";
437
+ this.thumbs = d.createElement("div");
438
+ this.thumbs.className = "gpv-thumbs";
439
+ if (this.options.thumbnails === false) this.thumbs.style.display = "none";
440
+ this.pages = d.createElement("div");
441
+ this.pages.className = "gpv-pages";
442
+ this.pages.addEventListener("scroll", () => this.trackScroll());
443
+ this.body.append(this.thumbs, this.pages);
444
+ this.root.appendChild(this.body);
445
+ if (typeof ResizeObserver !== "undefined") {
446
+ this.resizeObs = new ResizeObserver(() => this.applyFitMode());
447
+ this.resizeObs.observe(this.pages);
448
+ }
449
+ this.pages.addEventListener(
450
+ "wheel",
451
+ (e) => {
452
+ if (!e.ctrlKey && !e.metaKey) return;
453
+ e.preventDefault();
454
+ this.setZoom(this.cssScale * (e.deltaY < 0 ? 1.1 : 1 / 1.1));
455
+ },
456
+ { passive: false }
457
+ );
458
+ }
459
+ trackScroll() {
460
+ if (this.presenting || this.count === 0) return;
461
+ const mid = this.pages.scrollTop + this.pages.clientHeight / 2;
462
+ let best = this.current;
463
+ for (let p = 1; p <= this.count; p++) {
464
+ const el = this.pageEls[p];
465
+ if (el && el.offsetTop <= mid) best = p;
466
+ }
467
+ if (best !== this.current) {
468
+ this.current = best;
469
+ if (this.pageInput) this.pageInput.value = String(best);
470
+ if (this.fitMode !== "none") this.applyFitMode();
471
+ this.highlightThumb();
472
+ }
473
+ }
474
+ // ── lifecycle ────────────────────────────────────────────────────────────────
475
+ /** Close the current document and free its rendered pages. */
476
+ close() {
477
+ for (const u of this.urls) if (u) URL.revokeObjectURL(u);
478
+ this.urls = [];
479
+ this.sizes = [];
480
+ this.pageEls = [];
481
+ this.doc?.close();
482
+ this.doc = null;
483
+ this.count = 0;
484
+ this.pages?.replaceChildren();
485
+ this.thumbs?.replaceChildren();
486
+ }
487
+ /** Destroy the viewer: close the document and detach listeners. */
488
+ destroy() {
489
+ this.close();
490
+ this.resizeObs?.disconnect();
491
+ this.resizeObs = null;
492
+ this.root.ownerDocument.removeEventListener("keydown", this.onKey);
493
+ this.root.ownerDocument.removeEventListener("fullscreenchange", this.onFsChange);
494
+ this.root.classList.remove("gpv", "gpv-present");
495
+ this.root.replaceChildren();
496
+ }
497
+ };
498
+
499
+ // src/editor.ts
500
+ var SVG_NS = "http://www.w3.org/2000/svg";
501
+ var PT_PER_MM = 72 / 25.4;
502
+ var hex = (n) => "#" + (n & 16777215).toString(16).padStart(6, "0");
503
+ var GigaPdfEditor = class extends GigaPdfViewer {
504
+ tool = "select";
505
+ style = { color: 1710618, fill: null, lineWidth: 1.5, fontSize: 16, opacity: 1 };
506
+ els = [];
507
+ overlays = [];
508
+ idSeq = 1;
509
+ selected = null;
510
+ fontObj = null;
511
+ editorOpts;
512
+ palette = null;
513
+ // Rulers & margins. Margins are stored in **PDF points**, shared across pages.
514
+ margins = { top: 56.7, right: 56.7, bottom: 56.7, left: 56.7 };
515
+ // ≈20 mm
516
+ guides = [];
517
+ showGuides = true;
518
+ marginInputs = {
519
+ top: null,
520
+ right: null,
521
+ bottom: null,
522
+ left: null
523
+ };
524
+ constructor(engine, container, opts = {}) {
525
+ super(engine, container, opts);
526
+ this.editorOpts = opts;
527
+ this.buildPalette();
528
+ }
529
+ // ── public API ─────────────────────────────────────────────────────────────
530
+ setTool(tool) {
531
+ this.tool = tool;
532
+ for (const b of this.palette?.querySelectorAll("button[data-tool]") ?? []) {
533
+ b.classList.toggle("gpe-on", b.getAttribute("data-tool") === tool);
534
+ }
535
+ if (this.selected && tool !== "select") {
536
+ this.selected = null;
537
+ this.redrawAll();
538
+ }
539
+ }
540
+ setStyle(patch) {
541
+ this.style = { ...this.style, ...patch };
542
+ if (this.selected) {
543
+ this.selected.s = { ...this.selected.s, ...patch };
544
+ this.redraw(this.selected.page);
545
+ }
546
+ }
547
+ /** Edits drawn but not yet baked into the PDF. */
548
+ get pendingEdits() {
549
+ return this.els.length;
550
+ }
551
+ /** Delete the currently selected element. */
552
+ removeSelected() {
553
+ if (!this.selected) return;
554
+ const page = this.selected.page;
555
+ this.els = this.els.filter((e) => e !== this.selected);
556
+ this.selected = null;
557
+ this.redraw(page);
558
+ }
559
+ /** Discard all un-applied edits. */
560
+ clearEdits() {
561
+ this.els = [];
562
+ this.selected = null;
563
+ this.redrawAll();
564
+ }
565
+ /** Bake every pending edit into the PDF, then re-render the affected pages. */
566
+ applyEdits() {
567
+ if (!this.doc || this.els.length === 0) return 0;
568
+ const pages = /* @__PURE__ */ new Set();
569
+ let applied = 0;
570
+ for (const e of this.els) {
571
+ if (this.bake(e)) {
572
+ pages.add(e.page);
573
+ applied++;
574
+ }
575
+ }
576
+ this.els = [];
577
+ this.selected = null;
578
+ for (const p of pages) this.reRenderPage(p);
579
+ this.redrawAll();
580
+ return applied;
581
+ }
582
+ // ── apply one element to the PDF (flip Y: top-left points → PDF bottom-left) ──
583
+ bake(e) {
584
+ const doc = this.doc;
585
+ const H = this.pageHeightPt(e.page);
586
+ const nx = Math.min(e.x, e.x + e.w);
587
+ const ny = Math.min(e.y, e.y + e.h);
588
+ const nw = Math.abs(e.w);
589
+ const nh = Math.abs(e.h);
590
+ const { color, fill, lineWidth, opacity } = e.s;
591
+ switch (e.kind) {
592
+ case "rect":
593
+ return doc.addRectangle(e.page, nx, H - (ny + nh), nw, nh, color, fill, lineWidth, opacity);
594
+ case "ellipse":
595
+ return doc.addEllipse(e.page, nx + nw / 2, H - (ny + nh / 2), nw / 2, nh / 2, color, fill, lineWidth, opacity);
596
+ case "line":
597
+ return doc.addPolygon(e.page, [e.x, H - e.y, e.x + e.w, H - (e.y + e.h)], false, color, null, lineWidth, opacity);
598
+ case "ink": {
599
+ const flat = [];
600
+ const pts = e.pts ?? [];
601
+ for (let i = 0; i + 1 < pts.length; i += 2) {
602
+ flat.push(pts[i], H - pts[i + 1]);
603
+ }
604
+ return flat.length >= 4 ? doc.addPolygon(e.page, flat, false, color, null, lineWidth, opacity) : false;
605
+ }
606
+ case "text": {
607
+ const id = this.ensureFont();
608
+ if (id === null || !e.text) return false;
609
+ return doc.addText(e.page, e.x, H - e.y, e.s.fontSize, e.text, id, color);
610
+ }
611
+ case "image":
612
+ return e.data ? doc.addImage(e.page, e.data, nx, H - (ny + nh), nw, nh, opacity) : false;
613
+ case "highlight":
614
+ return doc.addRectangle(e.page, nx, H - (ny + nh), nw, nh, null, color, 0, 0.4);
615
+ case "redact":
616
+ return doc.redact(e.page, nx, H - (ny + nh), nw, nh, 0, false) >= 0;
617
+ default:
618
+ return false;
619
+ }
620
+ }
621
+ ensureFont() {
622
+ if (this.fontObj !== null) return this.fontObj;
623
+ const f = this.editorOpts.defaultFont;
624
+ if (!f || !this.doc) return null;
625
+ this.fontObj = this.doc.embedFont(f.family, f.ttf);
626
+ return this.fontObj;
627
+ }
628
+ // ── overlays (rebuilt after each (re)render) ─────────────────────────────────
629
+ afterRender() {
630
+ this.overlays = new Array(this.count + 1).fill(null);
631
+ this.guides = new Array(this.count + 1).fill(null);
632
+ const d = this.root.ownerDocument;
633
+ for (let p = 1; p <= this.count; p++) {
634
+ const box = this.pageEls[p];
635
+ if (!box) continue;
636
+ box.style.position = "relative";
637
+ const svg = d.createElementNS(SVG_NS, "svg");
638
+ svg.setAttribute("viewBox", `0 0 ${this.pageWidthPt(p)} ${this.pageHeightPt(p)}`);
639
+ svg.setAttribute("preserveAspectRatio", "none");
640
+ svg.style.cssText = "position:absolute;inset:0;width:100%;height:100%;touch-action:none";
641
+ svg.dataset.page = String(p);
642
+ box.appendChild(svg);
643
+ this.overlays[p] = svg;
644
+ this.bindOverlay(svg, p);
645
+ const g = d.createElementNS(SVG_NS, "svg");
646
+ g.setAttribute("viewBox", `0 0 ${this.pageWidthPt(p)} ${this.pageHeightPt(p)}`);
647
+ g.setAttribute("preserveAspectRatio", "none");
648
+ g.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible";
649
+ g.dataset.page = String(p);
650
+ box.appendChild(g);
651
+ this.guides[p] = g;
652
+ }
653
+ this.redrawAll();
654
+ this.redrawGuidesAll();
655
+ }
656
+ /** Keep ruler ticks / handles a constant on-screen size as the zoom changes. */
657
+ onZoomChange() {
658
+ this.redrawGuidesAll();
659
+ }
660
+ // ── rulers & margins (public API) ────────────────────────────────────────────
661
+ /** Set page margins (default unit: millimetres). */
662
+ setMargins(m, unit = "mm") {
663
+ const k = unit === "mm" ? PT_PER_MM : 1;
664
+ for (const side of ["top", "right", "bottom", "left"]) {
665
+ const v = m[side];
666
+ if (typeof v === "number" && isFinite(v)) this.margins[side] = Math.max(0, v * k);
667
+ }
668
+ this.syncMarginInputs();
669
+ this.redrawGuidesAll();
670
+ }
671
+ /** Current margins in the requested unit (millimetres by default). */
672
+ getMargins(unit = "mm") {
673
+ const k = unit === "mm" ? PT_PER_MM : 1;
674
+ return {
675
+ top: this.margins.top / k,
676
+ right: this.margins.right / k,
677
+ bottom: this.margins.bottom / k,
678
+ left: this.margins.left / k
679
+ };
680
+ }
681
+ /** Show or hide the rulers and margin guides. */
682
+ showRulers(on) {
683
+ this.showGuides = on;
684
+ this.redrawGuidesAll();
685
+ }
686
+ syncMarginInputs() {
687
+ for (const side of ["top", "right", "bottom", "left"]) {
688
+ const inp = this.marginInputs[side];
689
+ if (inp) inp.value = String(Math.round(this.margins[side] / PT_PER_MM));
690
+ }
691
+ }
692
+ // ── ruler / margin drawing ───────────────────────────────────────────────────
693
+ redrawGuidesAll() {
694
+ for (let p = 1; p <= this.count; p++) this.redrawGuides(p);
695
+ }
696
+ redrawGuides(page) {
697
+ const g = this.guides[page];
698
+ if (!g) return;
699
+ g.replaceChildren();
700
+ if (!this.showGuides) return;
701
+ const W = this.pageWidthPt(page);
702
+ const H = this.pageHeightPt(page);
703
+ if (!W || !H) return;
704
+ const d = this.root.ownerDocument;
705
+ const z = this.zoom || 1;
706
+ const band = 14 / z;
707
+ const fs = 8 / z;
708
+ const line = (x1, y1, x2, y2, stroke, dash) => {
709
+ const l = d.createElementNS(SVG_NS, "line");
710
+ l.setAttribute("x1", String(x1));
711
+ l.setAttribute("y1", String(y1));
712
+ l.setAttribute("x2", String(x2));
713
+ l.setAttribute("y2", String(y2));
714
+ l.setAttribute("stroke", stroke);
715
+ l.setAttribute("stroke-width", "1");
716
+ l.setAttribute("vector-effect", "non-scaling-stroke");
717
+ if (dash) l.setAttribute("stroke-dasharray", dash);
718
+ g.appendChild(l);
719
+ };
720
+ const label = (x, y, text) => {
721
+ const t = d.createElementNS(SVG_NS, "text");
722
+ t.setAttribute("x", String(x));
723
+ t.setAttribute("y", String(y));
724
+ t.setAttribute("font-size", String(fs));
725
+ t.setAttribute("fill", "#c9ced2");
726
+ t.setAttribute("font-family", "system-ui,sans-serif");
727
+ t.textContent = text;
728
+ g.appendChild(t);
729
+ };
730
+ const bandRect = (w, h) => {
731
+ const r = d.createElementNS(SVG_NS, "rect");
732
+ r.setAttribute("x", "0");
733
+ r.setAttribute("y", "0");
734
+ r.setAttribute("width", String(w));
735
+ r.setAttribute("height", String(h));
736
+ r.setAttribute("fill", "rgba(38,42,45,0.86)");
737
+ g.appendChild(r);
738
+ };
739
+ bandRect(W, band);
740
+ bandRect(band, H);
741
+ const step = PT_PER_MM * 5;
742
+ for (let x = 0, m = 0; x <= W + 0.5; x += step, m += 5) {
743
+ const major = m % 10 === 0;
744
+ line(x, 0, x, major ? band : band * 0.55, "#7a8085");
745
+ if (major && m > 0) label(x + 1 / z, band - 1.5 / z, String(m));
746
+ }
747
+ for (let y = 0, m = 0; y <= H + 0.5; y += step, m += 5) {
748
+ const major = m % 10 === 0;
749
+ line(0, y, major ? band : band * 0.55, y, "#7a8085");
750
+ if (major && m > 0) label(1 / z, y - 1 / z, String(m));
751
+ }
752
+ const ml = this.margins.left;
753
+ const mr = W - this.margins.right;
754
+ const mt = this.margins.top;
755
+ const mb = H - this.margins.bottom;
756
+ line(ml, 0, ml, H, "#3b82f6", "5 3");
757
+ line(mr, 0, mr, H, "#3b82f6", "5 3");
758
+ line(0, mt, W, mt, "#3b82f6", "5 3");
759
+ line(0, mb, W, mb, "#3b82f6", "5 3");
760
+ const hs = 11 / z;
761
+ this.marginHandle(g, page, "left", ml, band / 2, hs, "ew-resize", W, H);
762
+ this.marginHandle(g, page, "right", mr, band / 2, hs, "ew-resize", W, H);
763
+ this.marginHandle(g, page, "top", band / 2, mt, hs, "ns-resize", W, H);
764
+ this.marginHandle(g, page, "bottom", band / 2, mb, hs, "ns-resize", W, H);
765
+ }
766
+ marginHandle(g, page, side, cx, cy, size, cursor, W, H) {
767
+ const d = this.root.ownerDocument;
768
+ const r = d.createElementNS(SVG_NS, "rect");
769
+ r.setAttribute("x", String(cx - size / 2));
770
+ r.setAttribute("y", String(cy - size / 2));
771
+ r.setAttribute("width", String(size));
772
+ r.setAttribute("height", String(size));
773
+ r.setAttribute("rx", String(size * 0.25));
774
+ r.setAttribute("fill", "#3b82f6");
775
+ r.setAttribute("stroke", "#fff");
776
+ r.setAttribute("stroke-width", "1");
777
+ r.setAttribute("vector-effect", "non-scaling-stroke");
778
+ r.style.cssText = `pointer-events:all;cursor:${cursor}`;
779
+ r.addEventListener("pointerdown", (e) => {
780
+ e.stopPropagation();
781
+ e.preventDefault();
782
+ try {
783
+ g.setPointerCapture(e.pointerId);
784
+ } catch {
785
+ }
786
+ const move = (ev) => {
787
+ const pp = this.toPagePt(g, ev.clientX, ev.clientY);
788
+ const val = side === "left" ? pp.x : side === "right" ? W - pp.x : side === "top" ? pp.y : H - pp.y;
789
+ this.dragMargin(page, side, val);
790
+ };
791
+ const up = (ev) => {
792
+ try {
793
+ g.releasePointerCapture(ev.pointerId);
794
+ } catch {
795
+ }
796
+ g.removeEventListener("pointermove", move);
797
+ g.removeEventListener("pointerup", up);
798
+ g.removeEventListener("pointercancel", up);
799
+ };
800
+ g.addEventListener("pointermove", move);
801
+ g.addEventListener("pointerup", up);
802
+ g.addEventListener("pointercancel", up);
803
+ });
804
+ g.appendChild(r);
805
+ }
806
+ dragMargin(page, side, val) {
807
+ const W = this.pageWidthPt(page);
808
+ const H = this.pageHeightPt(page);
809
+ const lim = side === "left" || side === "right" ? W : H;
810
+ const opp = side === "left" ? this.margins.right : side === "right" ? this.margins.left : side === "top" ? this.margins.bottom : this.margins.top;
811
+ this.margins[side] = Math.max(0, Math.min(val, lim - opp - 8));
812
+ this.syncMarginInputs();
813
+ this.redrawGuidesAll();
814
+ }
815
+ toPagePt(svg, clientX, clientY) {
816
+ const m = svg.getScreenCTM();
817
+ if (!m) return { x: 0, y: 0 };
818
+ const pt = new DOMPoint(clientX, clientY).matrixTransform(m.inverse());
819
+ return { x: pt.x, y: pt.y };
820
+ }
821
+ bindOverlay(svg, page) {
822
+ let draft = null;
823
+ let origin = null;
824
+ let moving = null;
825
+ svg.addEventListener("pointerdown", (e) => {
826
+ svg.setPointerCapture(e.pointerId);
827
+ const p = this.toPagePt(svg, e.clientX, e.clientY);
828
+ if (this.tool === "select") {
829
+ const hit = this.hitTest(page, p);
830
+ this.selected = hit;
831
+ if (hit) moving = { el: hit, dx: p.x - hit.x, dy: p.y - hit.y };
832
+ this.redraw(page);
833
+ return;
834
+ }
835
+ if (this.tool === "text") {
836
+ this.placeText(page, p);
837
+ return;
838
+ }
839
+ if (this.tool === "image") {
840
+ void this.placeImage(page, p);
841
+ return;
842
+ }
843
+ origin = p;
844
+ draft = {
845
+ id: this.idSeq++,
846
+ page,
847
+ kind: this.tool,
848
+ x: p.x,
849
+ y: p.y,
850
+ w: 0,
851
+ h: 0,
852
+ s: { ...this.style },
853
+ ...this.tool === "ink" ? { pts: [p.x, p.y] } : {}
854
+ };
855
+ this.els.push(draft);
856
+ });
857
+ svg.addEventListener("pointermove", (e) => {
858
+ const p = this.toPagePt(svg, e.clientX, e.clientY);
859
+ if (moving) {
860
+ moving.el.x = p.x - moving.dx;
861
+ moving.el.y = p.y - moving.dy;
862
+ this.redraw(page);
863
+ return;
864
+ }
865
+ if (!draft || !origin) return;
866
+ if (draft.kind === "ink") {
867
+ draft.pts.push(p.x, p.y);
868
+ } else {
869
+ draft.x = origin.x;
870
+ draft.y = origin.y;
871
+ draft.w = p.x - origin.x;
872
+ draft.h = p.y - origin.y;
873
+ }
874
+ this.redraw(page);
875
+ });
876
+ const end = () => {
877
+ if (draft && !this.isMeaningful(draft)) {
878
+ this.els = this.els.filter((x) => x !== draft);
879
+ this.redraw(page);
880
+ } else if (draft) {
881
+ this.selected = draft;
882
+ this.redraw(page);
883
+ }
884
+ draft = null;
885
+ origin = null;
886
+ moving = null;
887
+ };
888
+ svg.addEventListener("pointerup", end);
889
+ svg.addEventListener("pointercancel", end);
890
+ }
891
+ isMeaningful(e) {
892
+ if (e.kind === "ink") return (e.pts?.length ?? 0) >= 4;
893
+ return Math.abs(e.w) > 1 || Math.abs(e.h) > 1;
894
+ }
895
+ hitTest(page, p) {
896
+ for (let i = this.els.length - 1; i >= 0; i--) {
897
+ const e = this.els[i];
898
+ if (e.page !== page) continue;
899
+ const nx = Math.min(e.x, e.x + e.w);
900
+ const ny = Math.min(e.y, e.y + e.h);
901
+ const nw = Math.abs(e.w) || 12;
902
+ const nh = Math.abs(e.h) || 12;
903
+ if (p.x >= nx - 4 && p.x <= nx + nw + 4 && p.y >= ny - 4 && p.y <= ny + nh + 4) return e;
904
+ }
905
+ return null;
906
+ }
907
+ placeText(page, p) {
908
+ const text = this.root.ownerDocument.defaultView?.prompt("Text:") ?? "";
909
+ if (!text.trim()) return;
910
+ this.els.push({ id: this.idSeq++, page, kind: "text", x: p.x, y: p.y + this.style.fontSize, w: 0, h: 0, text, s: { ...this.style } });
911
+ this.redraw(page);
912
+ }
913
+ async placeImage(page, p) {
914
+ const d = this.root.ownerDocument;
915
+ const input = d.createElement("input");
916
+ input.type = "file";
917
+ input.accept = "image/png,image/jpeg";
918
+ input.addEventListener("change", async () => {
919
+ const file = input.files?.[0];
920
+ if (!file) return;
921
+ const data = new Uint8Array(await file.arrayBuffer());
922
+ const url = URL.createObjectURL(file);
923
+ this.els.push({ id: this.idSeq++, page, kind: "image", x: p.x, y: p.y, w: 120, h: 120, data, imgUrl: url, s: { ...this.style } });
924
+ this.redraw(page);
925
+ });
926
+ input.click();
927
+ }
928
+ // ── overlay drawing ──────────────────────────────────────────────────────────
929
+ redrawAll() {
930
+ for (let p = 1; p <= this.count; p++) this.redraw(p);
931
+ }
932
+ redraw(page) {
933
+ const svg = this.overlays[page];
934
+ if (!svg) return;
935
+ svg.replaceChildren();
936
+ const d = this.root.ownerDocument;
937
+ for (const e of this.els) {
938
+ if (e.page !== page) continue;
939
+ const node = this.elNode(d, e);
940
+ if (node) svg.appendChild(node);
941
+ }
942
+ if (this.selected && this.selected.page === page) {
943
+ const nx = Math.min(this.selected.x, this.selected.x + this.selected.w);
944
+ const ny = Math.min(this.selected.y, this.selected.y + this.selected.h);
945
+ const sel = d.createElementNS(SVG_NS, "rect");
946
+ sel.setAttribute("x", String(nx - 3));
947
+ sel.setAttribute("y", String(ny - 3));
948
+ sel.setAttribute("width", String((Math.abs(this.selected.w) || 12) + 6));
949
+ sel.setAttribute("height", String((Math.abs(this.selected.h) || 12) + 6));
950
+ sel.setAttribute("fill", "none");
951
+ sel.setAttribute("stroke", "#3b82f6");
952
+ sel.setAttribute("stroke-dasharray", "4 3");
953
+ sel.setAttribute("stroke-width", "1");
954
+ svg.appendChild(sel);
955
+ }
956
+ }
957
+ elNode(d, e) {
958
+ const stroke = hex(e.s.color);
959
+ const fill = e.s.fill === null ? "none" : hex(e.s.fill);
960
+ const nx = Math.min(e.x, e.x + e.w);
961
+ const ny = Math.min(e.y, e.y + e.h);
962
+ const nw = Math.abs(e.w);
963
+ const nh = Math.abs(e.h);
964
+ switch (e.kind) {
965
+ case "rect":
966
+ case "highlight":
967
+ case "redact": {
968
+ const r = d.createElementNS(SVG_NS, "rect");
969
+ r.setAttribute("x", String(nx));
970
+ r.setAttribute("y", String(ny));
971
+ r.setAttribute("width", String(nw));
972
+ r.setAttribute("height", String(nh));
973
+ if (e.kind === "rect") {
974
+ r.setAttribute("fill", fill);
975
+ r.setAttribute("stroke", stroke);
976
+ r.setAttribute("stroke-width", String(e.s.lineWidth));
977
+ r.setAttribute("opacity", String(e.s.opacity));
978
+ } else if (e.kind === "highlight") {
979
+ r.setAttribute("fill", stroke);
980
+ r.setAttribute("opacity", "0.4");
981
+ } else {
982
+ r.setAttribute("fill", "#000");
983
+ r.setAttribute("stroke", "#f00");
984
+ r.setAttribute("stroke-dasharray", "3 2");
985
+ r.setAttribute("stroke-width", "0.5");
986
+ }
987
+ return r;
988
+ }
989
+ case "ellipse": {
990
+ const el = d.createElementNS(SVG_NS, "ellipse");
991
+ el.setAttribute("cx", String(nx + nw / 2));
992
+ el.setAttribute("cy", String(ny + nh / 2));
993
+ el.setAttribute("rx", String(nw / 2));
994
+ el.setAttribute("ry", String(nh / 2));
995
+ el.setAttribute("fill", fill);
996
+ el.setAttribute("stroke", stroke);
997
+ el.setAttribute("stroke-width", String(e.s.lineWidth));
998
+ el.setAttribute("opacity", String(e.s.opacity));
999
+ return el;
1000
+ }
1001
+ case "line": {
1002
+ const ln = d.createElementNS(SVG_NS, "line");
1003
+ ln.setAttribute("x1", String(e.x));
1004
+ ln.setAttribute("y1", String(e.y));
1005
+ ln.setAttribute("x2", String(e.x + e.w));
1006
+ ln.setAttribute("y2", String(e.y + e.h));
1007
+ ln.setAttribute("stroke", stroke);
1008
+ ln.setAttribute("stroke-width", String(e.s.lineWidth));
1009
+ return ln;
1010
+ }
1011
+ case "ink": {
1012
+ const pl = d.createElementNS(SVG_NS, "polyline");
1013
+ const pts = e.pts ?? [];
1014
+ let s = "";
1015
+ for (let i = 0; i + 1 < pts.length; i += 2) s += `${pts[i]},${pts[i + 1]} `;
1016
+ pl.setAttribute("points", s.trim());
1017
+ pl.setAttribute("fill", "none");
1018
+ pl.setAttribute("stroke", stroke);
1019
+ pl.setAttribute("stroke-width", String(e.s.lineWidth));
1020
+ pl.setAttribute("stroke-linejoin", "round");
1021
+ pl.setAttribute("stroke-linecap", "round");
1022
+ return pl;
1023
+ }
1024
+ case "text": {
1025
+ const t = d.createElementNS(SVG_NS, "text");
1026
+ t.setAttribute("x", String(e.x));
1027
+ t.setAttribute("y", String(e.y));
1028
+ t.setAttribute("font-size", String(e.s.fontSize));
1029
+ t.setAttribute("fill", stroke);
1030
+ t.setAttribute("font-family", "sans-serif");
1031
+ t.textContent = e.text ?? "";
1032
+ return t;
1033
+ }
1034
+ case "image": {
1035
+ const im = d.createElementNS(SVG_NS, "image");
1036
+ im.setAttribute("x", String(nx));
1037
+ im.setAttribute("y", String(ny));
1038
+ im.setAttribute("width", String(nw || 120));
1039
+ im.setAttribute("height", String(nh || 120));
1040
+ if (e.imgUrl) im.setAttribute("href", e.imgUrl);
1041
+ return im;
1042
+ }
1043
+ default:
1044
+ return null;
1045
+ }
1046
+ }
1047
+ // ── tool palette ─────────────────────────────────────────────────────────────
1048
+ buildPalette() {
1049
+ const d = this.root.ownerDocument;
1050
+ const bar = d.createElement("div");
1051
+ bar.className = "gpe-bar";
1052
+ bar.style.cssText = "display:flex;flex-wrap:wrap;gap:4px;align-items:center;padding:6px 10px;background:#3a3f42;border-bottom:1px solid #000;color:#eee;font:13px system-ui";
1053
+ const tools = [
1054
+ ["select", "\u25B8"],
1055
+ ["text", "T"],
1056
+ ["rect", "\u25AD"],
1057
+ ["ellipse", "\u25EF"],
1058
+ ["line", "\u2571"],
1059
+ ["ink", "\u270E"],
1060
+ ["image", "\u{1F5BC}"],
1061
+ ["highlight", "\u25A5"],
1062
+ ["redact", "\u2588"]
1063
+ ];
1064
+ for (const [t, label] of tools) {
1065
+ const b = d.createElement("button");
1066
+ b.textContent = label;
1067
+ b.title = t;
1068
+ b.dataset.tool = t;
1069
+ b.style.cssText = "background:#4a4f52;color:#eee;border:0;border-radius:4px;padding:4px 8px;cursor:pointer";
1070
+ b.addEventListener("click", () => this.setTool(t));
1071
+ bar.appendChild(b);
1072
+ }
1073
+ const color = d.createElement("input");
1074
+ color.type = "color";
1075
+ color.value = hex(this.style.color);
1076
+ color.title = "Colour";
1077
+ color.addEventListener("input", () => this.setStyle({ color: parseInt(color.value.slice(1), 16) }));
1078
+ bar.appendChild(color);
1079
+ const mg = d.createElement("span");
1080
+ mg.style.cssText = "display:flex;align-items:center;gap:3px;margin-left:8px";
1081
+ const mtog = d.createElement("input");
1082
+ mtog.type = "checkbox";
1083
+ mtog.checked = this.showGuides;
1084
+ mtog.title = "Show rulers & margins";
1085
+ mtog.addEventListener("change", () => this.showRulers(mtog.checked));
1086
+ const mlbl = d.createElement("span");
1087
+ mlbl.textContent = "Marges mm";
1088
+ mlbl.style.color = "#bbb";
1089
+ mg.append(mtog, mlbl);
1090
+ for (const side of ["top", "right", "bottom", "left"]) {
1091
+ const inp = d.createElement("input");
1092
+ inp.type = "number";
1093
+ inp.min = "0";
1094
+ inp.value = String(Math.round(this.margins[side] / PT_PER_MM));
1095
+ inp.title = side;
1096
+ inp.style.cssText = "width:44px;background:#222;color:#eee;border:1px solid #000;border-radius:4px;padding:3px;text-align:center";
1097
+ inp.addEventListener("change", () => this.setMargins({ [side]: Number(inp.value) || 0 }, "mm"));
1098
+ this.marginInputs[side] = inp;
1099
+ mg.appendChild(inp);
1100
+ }
1101
+ bar.appendChild(mg);
1102
+ const sp = d.createElement("span");
1103
+ sp.style.flex = "1";
1104
+ bar.appendChild(sp);
1105
+ const apply = d.createElement("button");
1106
+ apply.textContent = "Apply";
1107
+ apply.style.cssText = "background:#2563eb;color:#fff;border:0;border-radius:4px;padding:4px 12px;cursor:pointer";
1108
+ apply.addEventListener("click", () => this.applyEdits());
1109
+ bar.appendChild(apply);
1110
+ const del = d.createElement("button");
1111
+ del.textContent = "Delete";
1112
+ del.style.cssText = "background:#4a4f52;color:#eee;border:0;border-radius:4px;padding:4px 10px;cursor:pointer";
1113
+ del.addEventListener("click", () => this.removeSelected());
1114
+ bar.appendChild(del);
1115
+ const style = d.createElement("style");
1116
+ style.textContent = ".gpe-bar button.gpe-on{outline:2px solid #3b82f6}";
1117
+ d.head.appendChild(style);
1118
+ this.root.insertBefore(bar, this.root.children[1] ?? null);
1119
+ this.palette = bar;
1120
+ this.setTool("select");
1121
+ }
1122
+ };
1123
+ export {
1124
+ GigaPdfEditor
1125
+ };
1126
+ //# sourceMappingURL=editor.js.map