@orbat-mapper/tactical-map-sheet 0.1.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/pdf.mjs ADDED
@@ -0,0 +1,677 @@
1
+ import { a as POINTS_PER_MILLIMETRE, i as MILLIMETRES_PER_CSS_PIXEL, n as createTacticalMapSheetDocument, o as openSansCentralBaselineOffset, r as formatDecimal } from "./create-tactical-map-sheet-6DtvTi2G.mjs";
2
+ import { jsPDF } from "jspdf";
3
+ //#region src/pdf-transparency.ts
4
+ const FORM_DICTIONARY_MARKER = new TextEncoder().encode("/Subtype /Form\n/BBox");
5
+ const TRANSPARENCY_GROUP_DICTIONARY = new TextEncoder().encode("/Subtype /Form\n/Group <</S /Transparency /I true /K false>>\n/BBox");
6
+ function byteSequenceAt(bytes, sequence, offset) {
7
+ if (offset + sequence.length > bytes.length) return false;
8
+ for (let index = 0; index < sequence.length; index += 1) if (bytes[offset + index] !== sequence[index]) return false;
9
+ return true;
10
+ }
11
+ /**
12
+ * Mark jsPDF Form XObjects as isolated transparency groups and repair the
13
+ * classic cross-reference table after inserting the additional dictionaries.
14
+ */
15
+ function insertTransparencyGroupDictionaries(bytes) {
16
+ const offsets = [];
17
+ for (let offset = 0; offset <= bytes.length - FORM_DICTIONARY_MARKER.length; offset += 1) if (byteSequenceAt(bytes, FORM_DICTIONARY_MARKER, offset)) {
18
+ offsets.push(offset);
19
+ offset += FORM_DICTIONARY_MARKER.length - 1;
20
+ }
21
+ if (offsets.length === 0) return bytes;
22
+ const addedBytes = offsets.length * (TRANSPARENCY_GROUP_DICTIONARY.length - FORM_DICTIONARY_MARKER.length);
23
+ const expanded = new Uint8Array(bytes.length + addedBytes);
24
+ let sourceOffset = 0;
25
+ let targetOffset = 0;
26
+ for (const offset of offsets) {
27
+ expanded.set(bytes.subarray(sourceOffset, offset), targetOffset);
28
+ targetOffset += offset - sourceOffset;
29
+ expanded.set(TRANSPARENCY_GROUP_DICTIONARY, targetOffset);
30
+ targetOffset += TRANSPARENCY_GROUP_DICTIONARY.length;
31
+ sourceOffset = offset + FORM_DICTIONARY_MARKER.length;
32
+ }
33
+ expanded.set(bytes.subarray(sourceOffset), targetOffset);
34
+ const text = new TextDecoder("latin1").decode(expanded);
35
+ const xrefOffset = text.lastIndexOf("\nxref\n") + 1;
36
+ const trailer = text.slice(xrefOffset).match(/trailer\n([\s\S]*?)\nstartxref\n/);
37
+ const size = trailer?.[1]?.match(/\/Size (\d+)/)?.[1];
38
+ if (xrefOffset <= 0 || !trailer?.[1] || !size) throw new Error("Unable to rebuild the deterministic PDF cross-reference table");
39
+ const objectOffsets = /* @__PURE__ */ new Map();
40
+ for (const match of text.matchAll(/(?:^|\n)(\d+) 0 obj\n/g)) objectOffsets.set(Number(match[1]), match.index + (match[0].startsWith("\n") ? 1 : 0));
41
+ const objectCount = Number(size);
42
+ const entries = ["0000000000 65535 f "];
43
+ for (let objectNumber = 1; objectNumber < objectCount; objectNumber += 1) {
44
+ const offset = objectOffsets.get(objectNumber);
45
+ if (offset === void 0) throw new Error(`Missing PDF object ${objectNumber}`);
46
+ entries.push(`${String(offset).padStart(10, "0")} 00000 n `);
47
+ }
48
+ const rebuiltXref = new TextEncoder().encode(`xref\n0 ${objectCount}\n${entries.join("\n")}\ntrailer\n${trailer[1]}\nstartxref\n${xrefOffset}\n%%EOF`);
49
+ const result = new Uint8Array(xrefOffset + rebuiltXref.length);
50
+ result.set(expanded.subarray(0, xrefOffset));
51
+ result.set(rebuiltXref, xrefOffset);
52
+ return result;
53
+ }
54
+ //#endregion
55
+ //#region src/pdf.ts
56
+ const DEFAULT_VECTOR_PAINT = {
57
+ strokeWidth: 1,
58
+ lineCap: "butt",
59
+ lineJoin: "miter",
60
+ dash: [],
61
+ opacity: 1,
62
+ fillOpacity: 1,
63
+ strokeOpacity: 1,
64
+ fontSize: 16,
65
+ textAnchor: "start"
66
+ };
67
+ const FIXED_CREATION_DATE = "D:20000101000000+00'00'";
68
+ const FIXED_FILE_ID = "00000000000000000000000000000001";
69
+ /** jsPDF accepts opaque CSS hex colors but not alpha-bearing CSS colors. */
70
+ function pdfColor(input) {
71
+ const color = input.trim();
72
+ const hex = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(color);
73
+ if (hex) {
74
+ if (hex[1].length === 3 || hex[1].length === 6) return {
75
+ color,
76
+ alpha: 1
77
+ };
78
+ const digits = hex[1].length <= 4 ? hex[1].replace(/./g, (value) => value + value) : hex[1];
79
+ return {
80
+ color: `#${digits.slice(0, 6)}`,
81
+ alpha: digits.length === 8 ? parseInt(digits.slice(6, 8), 16) / 255 : 1
82
+ };
83
+ }
84
+ const rgb = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/i.exec(color);
85
+ if (!rgb) return {
86
+ color,
87
+ alpha: 1
88
+ };
89
+ if (rgb[4] === void 0) return {
90
+ color,
91
+ alpha: 1
92
+ };
93
+ const channels = rgb.slice(1, 4).map(Number);
94
+ if (channels.some((channel) => !Number.isFinite(channel))) return {
95
+ color,
96
+ alpha: 1
97
+ };
98
+ return {
99
+ color: `#${channels.map((channel) => Math.round(channel).toString(16).padStart(2, "0")).join("")}`,
100
+ alpha: rgb[4] === void 0 ? 1 : Number(rgb[4])
101
+ };
102
+ }
103
+ function numberAttribute(attributes, name, fallback = 0) {
104
+ const value = attributes[name];
105
+ return typeof value === "number" ? value : fallback;
106
+ }
107
+ function stringAttribute(attributes, name) {
108
+ const value = attributes[name];
109
+ return typeof value === "string" ? value : void 0;
110
+ }
111
+ function arrayAttribute(attributes, name) {
112
+ const value = attributes[name];
113
+ return Array.isArray(value) ? value : void 0;
114
+ }
115
+ function inheritedPaint(parent, node) {
116
+ const attributes = node.attributes;
117
+ const fill = stringAttribute(attributes, "fill") ?? parent.fill;
118
+ const stroke = stringAttribute(attributes, "stroke") ?? parent.stroke;
119
+ return {
120
+ fill: fill === "none" ? void 0 : fill,
121
+ stroke: stroke === "none" ? void 0 : stroke,
122
+ strokeWidth: numberAttribute(attributes, "stroke-width", parent.strokeWidth),
123
+ lineCap: stringAttribute(attributes, "stroke-linecap") ?? parent.lineCap,
124
+ lineJoin: stringAttribute(attributes, "stroke-linejoin") ?? parent.lineJoin,
125
+ dash: arrayAttribute(attributes, "stroke-dasharray") ?? parent.dash,
126
+ opacity: parent.opacity,
127
+ fillOpacity: numberAttribute(attributes, "fill-opacity", parent.fillOpacity),
128
+ strokeOpacity: numberAttribute(attributes, "stroke-opacity", parent.strokeOpacity),
129
+ fontSize: numberAttribute(attributes, "font-size", parent.fontSize),
130
+ textAnchor: stringAttribute(attributes, "text-anchor") ?? parent.textAnchor
131
+ };
132
+ }
133
+ function applyStroke(pdf, paint) {
134
+ if (!paint.stroke) return;
135
+ pdf.setDrawColor(paint.stroke);
136
+ pdf.setLineWidth(paint.strokeWidth);
137
+ pdf.setLineCap(paint.lineCap);
138
+ pdf.setLineJoin(paint.lineJoin);
139
+ pdf.setLineDashPattern([...paint.dash], 0);
140
+ }
141
+ function paintPath(pdf, paint, patternKey) {
142
+ const fill = paint.fill ? pdfColor(paint.fill) : void 0;
143
+ const stroke = paint.stroke ? pdfColor(paint.stroke) : void 0;
144
+ if (fill) pdf.setFillColor(fill.color);
145
+ applyStroke(pdf, {
146
+ ...paint,
147
+ stroke: stroke?.color
148
+ });
149
+ pdf.setGState(pdf.GState({
150
+ opacity: paint.opacity * paint.fillOpacity * (fill?.alpha ?? 1),
151
+ "stroke-opacity": paint.opacity * paint.strokeOpacity * (stroke?.alpha ?? 1)
152
+ }));
153
+ const pattern = patternKey ? { key: patternKey } : void 0;
154
+ if ((paint.fill || pattern) && paint.stroke) pdf.fillStroke(pattern);
155
+ else if (paint.fill || pattern) pdf.fill(pattern);
156
+ else if (paint.stroke) pdf.stroke();
157
+ else pdf.discardPath();
158
+ }
159
+ function quadraticToCubic(startX, startY, controlX, controlY, endX, endY) {
160
+ return [
161
+ startX + 2 / 3 * (controlX - startX),
162
+ startY + 2 / 3 * (controlY - startY),
163
+ endX + 2 / 3 * (controlX - endX),
164
+ endY + 2 / 3 * (controlY - endY),
165
+ endX,
166
+ endY
167
+ ];
168
+ }
169
+ function vectorAngle(ux, uy, vx, vy) {
170
+ const length = Math.hypot(ux, uy) * Math.hypot(vx, vy);
171
+ if (length === 0) return 0;
172
+ const angle = Math.acos(Math.max(-1, Math.min(1, (ux * vx + uy * vy) / length)));
173
+ return ux * vy - uy * vx < 0 ? -angle : angle;
174
+ }
175
+ function arcToCubics(startX, startY, values) {
176
+ let radiusX = Math.abs(values[0]);
177
+ let radiusY = Math.abs(values[1]);
178
+ const rotationDegrees = values[2];
179
+ const largeArc = values[3];
180
+ const sweep = values[4];
181
+ const endX = values[5];
182
+ const endY = values[6];
183
+ if (radiusX === 0 || radiusY === 0 || startX === endX && startY === endY) return {
184
+ operations: [{
185
+ op: "l",
186
+ c: [endX, endY]
187
+ }],
188
+ endX,
189
+ endY
190
+ };
191
+ const rotation = rotationDegrees * Math.PI / 180;
192
+ const cos = Math.cos(rotation);
193
+ const sin = Math.sin(rotation);
194
+ const halfDx = (startX - endX) / 2;
195
+ const halfDy = (startY - endY) / 2;
196
+ const transformedX = cos * halfDx + sin * halfDy;
197
+ const transformedY = -sin * halfDx + cos * halfDy;
198
+ const radiiScale = transformedX * transformedX / (radiusX * radiusX) + transformedY * transformedY / (radiusY * radiusY);
199
+ if (radiiScale > 1) {
200
+ const factor = Math.sqrt(radiiScale);
201
+ radiusX *= factor;
202
+ radiusY *= factor;
203
+ }
204
+ const numerator = Math.max(0, radiusX * radiusX * radiusY * radiusY - radiusX * radiusX * transformedY * transformedY - radiusY * radiusY * transformedX * transformedX);
205
+ const denominator = radiusX * radiusX * transformedY * transformedY + radiusY * radiusY * transformedX * transformedX;
206
+ const coefficient = denominator === 0 ? 0 : (Boolean(largeArc) === Boolean(sweep) ? -1 : 1) * Math.sqrt(numerator / denominator);
207
+ const centerPrimeX = coefficient * radiusX * transformedY / radiusY;
208
+ const centerPrimeY = -coefficient * radiusY * transformedX / radiusX;
209
+ const centerX = cos * centerPrimeX - sin * centerPrimeY + (startX + endX) / 2;
210
+ const centerY = sin * centerPrimeX + cos * centerPrimeY + (startY + endY) / 2;
211
+ const ux = (transformedX - centerPrimeX) / radiusX;
212
+ const uy = (transformedY - centerPrimeY) / radiusY;
213
+ const vx = (-transformedX - centerPrimeX) / radiusX;
214
+ const vy = (-transformedY - centerPrimeY) / radiusY;
215
+ const startAngle = vectorAngle(1, 0, ux, uy);
216
+ let deltaAngle = vectorAngle(ux, uy, vx, vy);
217
+ if (!sweep && deltaAngle > 0) deltaAngle -= Math.PI * 2;
218
+ if (sweep && deltaAngle < 0) deltaAngle += Math.PI * 2;
219
+ const segments = Math.ceil(Math.abs(deltaAngle) / (Math.PI / 2));
220
+ const segmentAngle = deltaAngle / segments;
221
+ const operations = [];
222
+ const point = (angle) => [centerX + radiusX * Math.cos(angle) * cos - radiusY * Math.sin(angle) * sin, centerY + radiusX * Math.cos(angle) * sin + radiusY * Math.sin(angle) * cos];
223
+ for (let index = 0; index < segments; index += 1) {
224
+ const from = startAngle + index * segmentAngle;
225
+ const to = from + segmentAngle;
226
+ const alpha = 4 / 3 * Math.tan((to - from) / 4);
227
+ const [toX, toY] = point(to);
228
+ const derivative = (angle) => [-radiusX * Math.sin(angle) * cos - radiusY * Math.cos(angle) * sin, -radiusX * Math.sin(angle) * sin + radiusY * Math.cos(angle) * cos];
229
+ const [fromDx, fromDy] = derivative(from);
230
+ const [toDx, toDy] = derivative(to);
231
+ const [fromX, fromY] = point(from);
232
+ operations.push({
233
+ op: "c",
234
+ c: [
235
+ fromX + alpha * fromDx,
236
+ fromY + alpha * fromDy,
237
+ toX - alpha * toDx,
238
+ toY - alpha * toDy,
239
+ toX,
240
+ toY
241
+ ]
242
+ });
243
+ }
244
+ return {
245
+ operations,
246
+ endX,
247
+ endY
248
+ };
249
+ }
250
+ function svgPathOperations(operations) {
251
+ const result = [];
252
+ let x = 0;
253
+ let y = 0;
254
+ let subpathX = 0;
255
+ let subpathY = 0;
256
+ let cubicControlX = 0;
257
+ let cubicControlY = 0;
258
+ let quadraticControlX = 0;
259
+ let quadraticControlY = 0;
260
+ let previous = "";
261
+ for (const operation of operations) {
262
+ const values = [...operation.values];
263
+ const absoluteX = (value) => operation.relative ? x + value : value;
264
+ const absoluteY = (value) => operation.relative ? y + value : value;
265
+ if (operation.command === "Z") {
266
+ result.push({
267
+ op: "h",
268
+ c: []
269
+ });
270
+ x = subpathX;
271
+ y = subpathY;
272
+ } else if (operation.command === "M" || operation.command === "L") {
273
+ x = absoluteX(values[0]);
274
+ y = absoluteY(values[1]);
275
+ result.push({
276
+ op: operation.command === "M" ? "m" : "l",
277
+ c: [x, y]
278
+ });
279
+ if (operation.command === "M") [subpathX, subpathY] = [x, y];
280
+ } else if (operation.command === "H") {
281
+ x = absoluteX(values[0]);
282
+ result.push({
283
+ op: "l",
284
+ c: [x, y]
285
+ });
286
+ } else if (operation.command === "V") {
287
+ y = absoluteY(values[0]);
288
+ result.push({
289
+ op: "l",
290
+ c: [x, y]
291
+ });
292
+ } else if (operation.command === "C") {
293
+ const control1X = absoluteX(values[0]);
294
+ const control1Y = absoluteY(values[1]);
295
+ cubicControlX = absoluteX(values[2]);
296
+ cubicControlY = absoluteY(values[3]);
297
+ x = absoluteX(values[4]);
298
+ y = absoluteY(values[5]);
299
+ result.push({
300
+ op: "c",
301
+ c: [
302
+ control1X,
303
+ control1Y,
304
+ cubicControlX,
305
+ cubicControlY,
306
+ x,
307
+ y
308
+ ]
309
+ });
310
+ } else if (operation.command === "S") {
311
+ const control1X = previous === "C" || previous === "S" ? 2 * x - cubicControlX : x;
312
+ const control1Y = previous === "C" || previous === "S" ? 2 * y - cubicControlY : y;
313
+ cubicControlX = absoluteX(values[0]);
314
+ cubicControlY = absoluteY(values[1]);
315
+ x = absoluteX(values[2]);
316
+ y = absoluteY(values[3]);
317
+ result.push({
318
+ op: "c",
319
+ c: [
320
+ control1X,
321
+ control1Y,
322
+ cubicControlX,
323
+ cubicControlY,
324
+ x,
325
+ y
326
+ ]
327
+ });
328
+ } else if (operation.command === "Q" || operation.command === "T") {
329
+ const controlX = operation.command === "T" && (previous === "Q" || previous === "T") ? 2 * x - quadraticControlX : operation.command === "Q" ? absoluteX(values[0]) : x;
330
+ const controlY = operation.command === "T" && (previous === "Q" || previous === "T") ? 2 * y - quadraticControlY : operation.command === "Q" ? absoluteY(values[1]) : y;
331
+ const endIndex = operation.command === "Q" ? 2 : 0;
332
+ const endX = absoluteX(values[endIndex]);
333
+ const endY = absoluteY(values[endIndex + 1]);
334
+ result.push({
335
+ op: "c",
336
+ c: quadraticToCubic(x, y, controlX, controlY, endX, endY)
337
+ });
338
+ [quadraticControlX, quadraticControlY, x, y] = [
339
+ controlX,
340
+ controlY,
341
+ endX,
342
+ endY
343
+ ];
344
+ } else if (operation.command === "A") {
345
+ const arcValues = [
346
+ values[0],
347
+ values[1],
348
+ values[2],
349
+ values[3],
350
+ values[4],
351
+ absoluteX(values[5]),
352
+ absoluteY(values[6])
353
+ ];
354
+ const arc = arcToCubics(x, y, arcValues);
355
+ result.push(...arc.operations);
356
+ [x, y] = [arc.endX, arc.endY];
357
+ }
358
+ previous = operation.command;
359
+ }
360
+ return result;
361
+ }
362
+ function transformMatrix(pdf, transform) {
363
+ const values = transform.values;
364
+ if (transform.type === "matrix") return pdf.Matrix(...values);
365
+ if (transform.type === "translate") return pdf.Matrix(1, 0, 0, 1, values[0], values[1] ?? 0);
366
+ if (transform.type === "scale") return pdf.Matrix(values[0], 0, 0, values[1] ?? values[0], 0, 0);
367
+ const radians = values[0] * Math.PI / 180;
368
+ const rotation = pdf.Matrix(Math.cos(radians), Math.sin(radians), -Math.sin(radians), Math.cos(radians), 0, 0);
369
+ if (values.length === 1) return rotation;
370
+ const [cx, cy] = values.slice(1);
371
+ return pdf.Matrix(1, 0, 0, 1, cx, cy).multiply(rotation).multiply(pdf.Matrix(1, 0, 0, 1, -cx, -cy));
372
+ }
373
+ function renderOpacityGroup(pdf, context, opacity, render) {
374
+ if (opacity === 0) return;
375
+ if (opacity === 1) return render();
376
+ const key = `opacity-${context.formIds.next++}`;
377
+ const [x, y, width, height] = context.bounds;
378
+ pdf.beginFormObject(x, y, width, height, pdf.Matrix(1, 0, 0, 1, 0, 0));
379
+ render();
380
+ pdf.endFormObject(key);
381
+ pdf.saveGraphicsState();
382
+ pdf.setGState(pdf.GState({
383
+ opacity,
384
+ "stroke-opacity": opacity
385
+ }));
386
+ pdf.doFormObject(key, pdf.Matrix(1, 0, 0, 1, 0, 0));
387
+ pdf.restoreGraphicsState();
388
+ }
389
+ function renderVectorNode(pdf, node, parentPaint, context, ignoreNodeOpacity = false) {
390
+ const nodeOpacity = numberAttribute(node.attributes, "opacity", 1);
391
+ if (!ignoreNodeOpacity && nodeOpacity !== 1) {
392
+ renderOpacityGroup(pdf, context, nodeOpacity, () => renderVectorNode(pdf, node, parentPaint, context, true));
393
+ return;
394
+ }
395
+ const paint = inheritedPaint(parentPaint, node);
396
+ pdf.saveGraphicsState();
397
+ if (node.kind === "group") {
398
+ for (const transform of node.transforms) pdf.setCurrentTransformationMatrix(transformMatrix(pdf, transform));
399
+ for (const child of node.children) renderVectorNode(pdf, child, paint, context);
400
+ pdf.restoreGraphicsState();
401
+ return;
402
+ }
403
+ if (node.kind === "text") {
404
+ const x = numberAttribute(node.attributes, "x");
405
+ const y = numberAttribute(node.attributes, "y");
406
+ pdf.setFont("Open Sans", "normal", 400);
407
+ pdf.setFontSize(paint.fontSize * POINTS_PER_MILLIMETRE);
408
+ const renderedWidth = pdf.getTextWidth(node.text);
409
+ const renderedLeft = paint.textAnchor === "middle" ? -renderedWidth / 2 : paint.textAnchor === "end" ? -renderedWidth : 0;
410
+ const fill = pdfColor(paint.fill ?? "#000000");
411
+ pdf.setTextColor(fill.color);
412
+ pdf.setGState(pdf.GState({ opacity: paint.opacity * paint.fillOpacity * fill.alpha }));
413
+ pdf.text(node.text, x + renderedLeft, y, {
414
+ align: "left",
415
+ baseline: "alphabetic"
416
+ });
417
+ pdf.restoreGraphicsState();
418
+ return;
419
+ }
420
+ if (node.kind === "path") pdf.path(svgPathOperations(node.operations));
421
+ else if (node.kind === "circle") pdf.circle(numberAttribute(node.attributes, "cx"), numberAttribute(node.attributes, "cy"), numberAttribute(node.attributes, "r"), null);
422
+ else if (node.kind === "ellipse") pdf.ellipse(numberAttribute(node.attributes, "cx"), numberAttribute(node.attributes, "cy"), numberAttribute(node.attributes, "rx"), numberAttribute(node.attributes, "ry"), null);
423
+ else if (node.kind === "rect") {
424
+ const x = numberAttribute(node.attributes, "x");
425
+ const y = numberAttribute(node.attributes, "y");
426
+ const width = numberAttribute(node.attributes, "width");
427
+ const height = numberAttribute(node.attributes, "height");
428
+ const rawRadiusX = numberAttribute(node.attributes, "rx", numberAttribute(node.attributes, "ry"));
429
+ const rawRadiusY = numberAttribute(node.attributes, "ry", rawRadiusX);
430
+ const radiusX = Math.min(rawRadiusX, width / 2);
431
+ const radiusY = Math.min(rawRadiusY, height / 2);
432
+ if (radiusX > 0 || radiusY > 0) pdf.roundedRect(x, y, width, height, radiusX, radiusY, null);
433
+ else pdf.rect(x, y, width, height, null);
434
+ } else if (node.kind === "line") pdf.path([{
435
+ op: "m",
436
+ c: [numberAttribute(node.attributes, "x1"), numberAttribute(node.attributes, "y1")]
437
+ }, {
438
+ op: "l",
439
+ c: [numberAttribute(node.attributes, "x2"), numberAttribute(node.attributes, "y2")]
440
+ }]);
441
+ else if (node.kind === "polyline" || node.kind === "polygon") {
442
+ const operations = [];
443
+ for (let index = 0; index < node.points.length; index += 2) operations.push({
444
+ op: index === 0 ? "m" : "l",
445
+ c: [node.points[index], node.points[index + 1]]
446
+ });
447
+ if (node.kind === "polygon") operations.push({
448
+ op: "h",
449
+ c: []
450
+ });
451
+ pdf.path(operations);
452
+ }
453
+ paintPath(pdf, paint);
454
+ pdf.restoreGraphicsState();
455
+ }
456
+ function sceneTextFont(text) {
457
+ if (text.fontStyle === "italic") return {
458
+ style: "italic",
459
+ weight: 400,
460
+ resourceStyle: "Italic"
461
+ };
462
+ if (text.fontWeight === 300) return {
463
+ style: "normal",
464
+ weight: 300,
465
+ resourceStyle: "Light"
466
+ };
467
+ return {
468
+ style: "normal",
469
+ weight: 400,
470
+ resourceStyle: "Regular"
471
+ };
472
+ }
473
+ function renderText(pdf, text) {
474
+ const backgroundLeft = text.anchor === "start" ? 0 : text.anchor === "end" ? -text.widthMm : -text.widthMm / 2;
475
+ pdf.saveGraphicsState();
476
+ const radians = text.rotationDegrees * Math.PI / 180;
477
+ pdf.setCurrentTransformationMatrix(pdf.Matrix(1, 0, 0, 1, text.position.xMm, text.position.yMm));
478
+ pdf.setCurrentTransformationMatrix(pdf.Matrix(Math.cos(radians), Math.sin(radians), -Math.sin(radians), Math.cos(radians), 0, 0));
479
+ pdf.setCurrentTransformationMatrix(pdf.Matrix(1, 0, 0, 1, -text.position.xMm, -text.position.yMm));
480
+ if (text.background) {
481
+ pdf.setFillColor("#ffffff");
482
+ pdf.rect(text.position.xMm + backgroundLeft, text.position.yMm - text.heightMm / 2, text.widthMm, text.heightMm, "F");
483
+ }
484
+ const font = sceneTextFont(text);
485
+ pdf.setFont("Open Sans", font.style, font.weight);
486
+ pdf.setFontSize(text.fontSizeMm * POINTS_PER_MILLIMETRE);
487
+ const renderedWidthMm = pdf.getTextWidth(text.text);
488
+ const renderedLeft = text.anchor === "start" ? 0 : text.anchor === "end" ? -renderedWidthMm : -renderedWidthMm / 2;
489
+ const baselineOffsetMm = openSansCentralBaselineOffset(font.resourceStyle, text.fontSizeMm / MILLIMETRES_PER_CSS_PIXEL) * MILLIMETRES_PER_CSS_PIXEL;
490
+ const fill = pdfColor(text.color);
491
+ const stroke = text.haloColor ? pdfColor(text.haloColor) : void 0;
492
+ pdf.setTextColor(fill.color);
493
+ if (text.haloColor) {
494
+ pdf.setDrawColor(stroke.color);
495
+ pdf.setLineWidth(text.haloWidthMm ?? 0);
496
+ }
497
+ if (fill.alpha !== 1 || (stroke?.alpha ?? 1) !== 1) pdf.setGState(pdf.GState({
498
+ opacity: fill.alpha,
499
+ "stroke-opacity": stroke?.alpha ?? fill.alpha
500
+ }));
501
+ pdf.text(text.text, text.position.xMm + renderedLeft, text.position.yMm + baselineOffsetMm, {
502
+ align: "left",
503
+ baseline: "alphabetic",
504
+ ...text.haloColor ? { renderingMode: "fillThenStroke" } : {}
505
+ });
506
+ pdf.restoreGraphicsState();
507
+ }
508
+ function renderElement(pdf, element, formIds) {
509
+ if (element.kind === "text") return renderText(pdf, element);
510
+ if (element.kind === "vector-symbol") {
511
+ pdf.saveGraphicsState();
512
+ pdf.setCurrentTransformationMatrix(pdf.Matrix(1, 0, 0, 1, element.position.xMm, element.position.yMm));
513
+ const angle = element.rotationDegrees * Math.PI / 180;
514
+ pdf.setCurrentTransformationMatrix(pdf.Matrix(Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), 0, 0));
515
+ pdf.setCurrentTransformationMatrix(pdf.Matrix(element.scaleMmPerUnit, 0, 0, element.scaleMmPerUnit, 0, 0));
516
+ pdf.setCurrentTransformationMatrix(pdf.Matrix(1, 0, 0, 1, -element.anchor.x, -element.anchor.y));
517
+ const [viewX, viewY, viewWidth, viewHeight] = element.viewBox;
518
+ const viewportScale = Math.min(element.width / viewWidth, element.height / viewHeight);
519
+ const offsetX = (element.width - viewWidth * viewportScale) / 2 - viewX * viewportScale;
520
+ const offsetY = (element.height - viewHeight * viewportScale) / 2 - viewY * viewportScale;
521
+ pdf.setCurrentTransformationMatrix(pdf.Matrix(viewportScale, 0, 0, viewportScale, offsetX, offsetY));
522
+ const paint = {
523
+ ...DEFAULT_VECTOR_PAINT,
524
+ fill: "#000000"
525
+ };
526
+ const context = {
527
+ bounds: element.viewBox,
528
+ formIds
529
+ };
530
+ renderOpacityGroup(pdf, context, element.opacity, () => {
531
+ for (const node of element.nodes) renderVectorNode(pdf, node, paint, context);
532
+ });
533
+ pdf.restoreGraphicsState();
534
+ return;
535
+ }
536
+ const [first, ...rest] = element.points;
537
+ if (!first) throw new Error("Cannot serialize an empty path");
538
+ pdf.path([
539
+ {
540
+ op: "m",
541
+ c: [first.xMm, first.yMm]
542
+ },
543
+ ...rest.map((point) => ({
544
+ op: "l",
545
+ c: [point.xMm, point.yMm]
546
+ })),
547
+ ...element.closed ? [{
548
+ op: "h",
549
+ c: []
550
+ }] : []
551
+ ]);
552
+ paintPath(pdf, {
553
+ ...DEFAULT_VECTOR_PAINT,
554
+ fill: element.fillColor,
555
+ stroke: element.strokeColor,
556
+ strokeWidth: element.strokeWidthMm,
557
+ lineCap: element.lineCap,
558
+ lineJoin: element.lineJoin,
559
+ dash: element.strokeDashMm
560
+ }, element.fillPatternId);
561
+ }
562
+ function patternPaint(paint) {
563
+ return {
564
+ ...DEFAULT_VECTOR_PAINT,
565
+ fill: paint.fillColor,
566
+ stroke: paint.stroke?.color,
567
+ strokeWidth: paint.stroke?.widthMm ?? DEFAULT_VECTOR_PAINT.strokeWidth,
568
+ lineCap: paint.stroke?.lineCap ?? DEFAULT_VECTOR_PAINT.lineCap,
569
+ dash: paint.stroke?.dashMm ?? []
570
+ };
571
+ }
572
+ function renderPatternCommand(pdf, command) {
573
+ if (command.type === "circle") pdf.circle(command.center.xMm, command.center.yMm, command.radiusMm, null);
574
+ else {
575
+ const operations = command.operations.map((operation) => {
576
+ if (operation.type === "close") return {
577
+ op: "h",
578
+ c: []
579
+ };
580
+ if (operation.type === "cubic") return {
581
+ op: "c",
582
+ c: [
583
+ operation.control1.xMm,
584
+ operation.control1.yMm,
585
+ operation.control2.xMm,
586
+ operation.control2.yMm,
587
+ operation.to.xMm,
588
+ operation.to.yMm
589
+ ]
590
+ };
591
+ return {
592
+ op: operation.type === "move" ? "m" : "l",
593
+ c: [operation.to.xMm, operation.to.yMm]
594
+ };
595
+ });
596
+ pdf.path(operations);
597
+ }
598
+ paintPath(pdf, patternPaint(command.paint));
599
+ }
600
+ function registerPattern(pdf, pattern, pageHeightMm) {
601
+ const identity = pattern.identity;
602
+ const matrix = pdf.Matrix(POINTS_PER_MILLIMETRE, 0, 0, -POINTS_PER_MILLIMETRE, identity.phaseXMm * POINTS_PER_MILLIMETRE, (pageHeightMm - identity.phaseYMm) * POINTS_PER_MILLIMETRE);
603
+ const tiling = pdf.TilingPattern([
604
+ 0,
605
+ 0,
606
+ identity.tileWidthMm,
607
+ identity.tileHeightMm
608
+ ], identity.tileWidthMm, identity.tileHeightMm, void 0, matrix);
609
+ pdf.beginTilingPattern(tiling);
610
+ for (const command of pattern.commands) renderPatternCommand(pdf, command);
611
+ pdf.endTilingPattern(identity.id, tiling);
612
+ }
613
+ function registerFont(pdf, font) {
614
+ const fileName = `OpenSans-${font.style}.ttf`;
615
+ const style = font.style === "Italic" ? "italic" : "normal";
616
+ const weight = font.style === "Light" ? 300 : 400;
617
+ pdf.addFileToVFS(fileName, font.base64);
618
+ pdf.addFont(fileName, "Open Sans", style, weight, "Identity-H");
619
+ }
620
+ function canonicalizeScene(scene) {
621
+ return JSON.parse(JSON.stringify(scene, (_key, value) => typeof value === "number" ? Number(formatDecimal(value)) : value));
622
+ }
623
+ function renderLayers(pdf, layers, formIds) {
624
+ for (const layer of layers) for (const graphic of layer.graphics) for (const element of graphic.elements) renderElement(pdf, element, formIds);
625
+ }
626
+ function serializePdf(inputScene) {
627
+ const scene = canonicalizeScene(inputScene);
628
+ const pdf = new jsPDF({
629
+ orientation: scene.page.widthMm >= scene.page.heightMm ? "landscape" : "portrait",
630
+ unit: "mm",
631
+ format: [scene.page.widthMm, scene.page.heightMm],
632
+ compress: false,
633
+ precision: 6,
634
+ putOnlyUsedFonts: true
635
+ });
636
+ pdf.setCreationDate(FIXED_CREATION_DATE);
637
+ pdf.setFileId(FIXED_FILE_ID);
638
+ pdf.setProperties({ creator: "@orbat-mapper/tactical-map-sheet" });
639
+ const formIds = { next: 0 };
640
+ for (const font of scene.fonts) registerFont(pdf, font);
641
+ pdf.advancedAPI((advanced) => {
642
+ for (const pattern of scene.patterns) registerPattern(advanced, pattern, scene.page.heightMm);
643
+ if (scene.backgroundColor) {
644
+ const background = pdfColor(scene.backgroundColor);
645
+ if (background.alpha === 1) {
646
+ advanced.setFillColor(background.color);
647
+ advanced.rect(0, 0, scene.page.widthMm, scene.page.heightMm, "F");
648
+ } else {
649
+ advanced.saveGraphicsState();
650
+ advanced.setFillColor(background.color);
651
+ advanced.setGState(advanced.GState({ opacity: background.alpha }));
652
+ advanced.rect(0, 0, scene.page.widthMm, scene.page.heightMm, "F");
653
+ advanced.restoreGraphicsState();
654
+ }
655
+ }
656
+ advanced.saveGraphicsState();
657
+ advanced.rect(scene.mapFrame.xMm, scene.mapFrame.yMm, scene.mapFrame.widthMm, scene.mapFrame.heightMm, null);
658
+ advanced.clip();
659
+ advanced.discardPath();
660
+ renderLayers(advanced, scene.layers, formIds);
661
+ advanced.restoreGraphicsState();
662
+ renderLayers(advanced, scene.marginalia ?? [], formIds);
663
+ });
664
+ return insertTransparencyGroupDictionaries(new Uint8Array(pdf.output("arraybuffer")));
665
+ }
666
+ function renderMapSheetPdf(request) {
667
+ const { scene, result } = createTacticalMapSheetDocument(request);
668
+ return {
669
+ pdf: serializePdf(scene),
670
+ footprint: result.footprint,
671
+ warnings: result.warnings,
672
+ metadata: result.metadata,
673
+ resources: result.resources
674
+ };
675
+ }
676
+ //#endregion
677
+ export { renderMapSheetPdf };