@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/COMPATIBILITY.md +25 -0
- package/LICENSE +21 -0
- package/README.md +179 -0
- package/dist/create-tactical-map-sheet-6DtvTi2G.mjs +1820 -0
- package/dist/create-tactical-map-sheet-DAKVJ3Bx.d.mts +425 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +2 -0
- package/dist/pdf.d.mts +13 -0
- package/dist/pdf.mjs +677 -0
- package/package.json +60 -0
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 };
|