@segnavia/render 0.2.0 → 0.4.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.
@@ -0,0 +1,266 @@
1
+ import { RenderRefusedError } from "../errors.js";
2
+ import { IDENTITY_MATRIX, applyAffine, invertAffine, multiplyAffine } from "./affine.js";
3
+ import { MAX_DOCUMENT_CONTENT_BYTES, arrayNumbers, decodeContent, rawStreamOf } from "./pdf-objects.js";
4
+ import { evaluateCalculator, parseCalculator, staticOperatorCount } from "./calculator.js";
5
+ import { PDFArray, PDFName, PDFNumber, PDFRef } from "pdf-lib";
6
+ import { deflateSync } from "node:zlib";
7
+ //#region src/press/sample-shading.ts
8
+ const SHADING_SAMPLE_SPACING_PT = .5;
9
+ const MAX_SHADING_SAMPLES = 1048576;
10
+ const MAX_DOCUMENT_SHADING_SAMPLES = 4 * MAX_SHADING_SAMPLES;
11
+ const MAX_DOCUMENT_CALCULATOR_OPERATIONS = 8 * MAX_SHADING_SAMPLES * 52;
12
+ const MAX_SAMPLING_COLOR_CACHE = 4096;
13
+ const SAMPLE_CHANNELS = 4;
14
+ const SAMPLE_BYTES = 1;
15
+ function refuse(found, supported) {
16
+ throw new RenderRefusedError(`The PDF has unsupported ${found}; supported: ${supported}.`);
17
+ }
18
+ function clamp01(value) {
19
+ if (value < 0) return 0;
20
+ if (value > 1) return 1;
21
+ return value;
22
+ }
23
+ function requireFinite(values, found) {
24
+ if (values.some((value) => !Number.isFinite(value))) refuse(found, "finite numbers");
25
+ }
26
+ function shadingNumbers(dictionary, key, count) {
27
+ const expected = count === 4 ? "four finite numbers" : "six finite numbers";
28
+ const array = dictionary.lookupMaybe(PDFName.of(key), PDFArray);
29
+ if (!array || array.size() !== count) refuse(`shading ${key}`, expected);
30
+ const values = Array.from({ length: count }, (_, index) => array.lookupMaybe(index, PDFNumber)?.asNumber());
31
+ if (values.some((value) => value === void 0 || !Number.isFinite(value))) refuse(`shading ${key}`, expected);
32
+ return values;
33
+ }
34
+ function shadingDomainOf(shading) {
35
+ return shadingNumbers(shading, "Domain", 4);
36
+ }
37
+ function axisSamples(span, columnA, columnB) {
38
+ if (![
39
+ span,
40
+ columnA,
41
+ columnB
42
+ ].every(Number.isFinite)) refuse("shading axis length", "a finite length");
43
+ const extent = Math.hypot(columnA, columnB) * Math.abs(span) / SHADING_SAMPLE_SPACING_PT;
44
+ if (!Number.isFinite(extent)) refuse("shading axis length", "a finite length");
45
+ return Math.min(MAX_SHADING_SAMPLES, Math.max(1, Math.ceil(extent)));
46
+ }
47
+ function packedRgb8(rgb) {
48
+ return Math.round(clamp01(rgb[0]) * 255) << 16 | Math.round(clamp01(rgb[1]) * 255) << 8 | Math.round(clamp01(rgb[2]) * 255);
49
+ }
50
+ function unpackRgb8(key) {
51
+ return [
52
+ (key >> 16) / 255,
53
+ (key >> 8 & 255) / 255,
54
+ (key & 255) / 255
55
+ ];
56
+ }
57
+ function createSamplingColorConverter(convert) {
58
+ const cache = /* @__PURE__ */ new Map();
59
+ return {
60
+ size: () => cache.size,
61
+ convert: (rgb) => {
62
+ const key = packedRgb8(rgb);
63
+ const cached = cache.get(key);
64
+ if (cached) return cached;
65
+ const converted = convert(unpackRgb8(key));
66
+ if (cache.size < 4096) cache.set(key, converted);
67
+ return converted;
68
+ }
69
+ };
70
+ }
71
+ function sampleDomainForPainted(domain, matrix, painted) {
72
+ const original = [
73
+ domain[0] ?? 0,
74
+ domain[1] ?? 0,
75
+ domain[2] ?? 0,
76
+ domain[3] ?? 0
77
+ ];
78
+ if (!painted || painted === "unbounded") return original;
79
+ const inverse = invertAffine(matrix);
80
+ if (!inverse) return original;
81
+ const corners = [
82
+ applyAffine(inverse, painted.x0, painted.y0),
83
+ applyAffine(inverse, painted.x1, painted.y0),
84
+ applyAffine(inverse, painted.x0, painted.y1),
85
+ applyAffine(inverse, painted.x1, painted.y1)
86
+ ];
87
+ const xs = corners.map((point) => point.x);
88
+ const ys = corners.map((point) => point.y);
89
+ const x0 = Math.max(original[0] ?? 0, Math.min(...xs));
90
+ const x1 = Math.min(original[1] ?? 0, Math.max(...xs));
91
+ const y0 = Math.max(original[2] ?? 0, Math.min(...ys));
92
+ const y1 = Math.min(original[3] ?? 0, Math.max(...ys));
93
+ if (!(x0 < x1) || !(y0 < y1)) return original;
94
+ return [
95
+ x0,
96
+ x1,
97
+ y0,
98
+ y1
99
+ ];
100
+ }
101
+ function unionSampleDomain(left, right) {
102
+ return [
103
+ Math.min(left[0] ?? 0, right[0] ?? 0),
104
+ Math.max(left[1] ?? 0, right[1] ?? 0),
105
+ Math.min(left[2] ?? 0, right[2] ?? 0),
106
+ Math.max(left[3] ?? 0, right[3] ?? 0)
107
+ ];
108
+ }
109
+ function sampleDomainForUses(domain, uses) {
110
+ const original = [
111
+ domain[0] ?? 0,
112
+ domain[1] ?? 0,
113
+ domain[2] ?? 0,
114
+ domain[3] ?? 0
115
+ ];
116
+ if (uses.length === 0) return original;
117
+ let union;
118
+ for (const use of uses) {
119
+ const next = sampleDomainForPainted(domain, use.matrix, use.painted);
120
+ if (!use.painted || use.painted === "unbounded") return original;
121
+ union = union ? unionSampleDomain(union, next) : next;
122
+ }
123
+ return union ?? original;
124
+ }
125
+ function matrixOf(dictionary) {
126
+ if (!dictionary?.has(PDFName.of("Matrix"))) return IDENTITY_MATRIX.slice();
127
+ return arrayNumbers(dictionary, "Matrix", 6);
128
+ }
129
+ function shadingDeviceMatrix(shading, pattern) {
130
+ const shadingMatrix = shading.has(PDFName.of("Matrix")) ? shadingNumbers(shading, "Matrix", 6) : IDENTITY_MATRIX.slice();
131
+ return pattern ? multiplyAffine(matrixOf(pattern), shadingMatrix) : shadingMatrix;
132
+ }
133
+ function capSampleGrid(n, m) {
134
+ if (n * m <= 1048576) return {
135
+ n,
136
+ m
137
+ };
138
+ const scale = Math.sqrt(MAX_SHADING_SAMPLES / (n * m));
139
+ let nextN = Math.max(1, Math.floor(n * scale));
140
+ let nextM = Math.max(1, Math.floor(m * scale));
141
+ while (nextN * nextM > MAX_SHADING_SAMPLES) if (nextN >= nextM && nextN > 1) nextN--;
142
+ else if (nextM > 1) nextM--;
143
+ else break;
144
+ return {
145
+ n: nextN,
146
+ m: nextM
147
+ };
148
+ }
149
+ function shadingSampleGrid(domain, matrix) {
150
+ const x0 = domain[0] ?? 0;
151
+ const x1 = domain[1] ?? 0;
152
+ const y0 = domain[2] ?? 0;
153
+ const y1 = domain[3] ?? 0;
154
+ requireFinite([
155
+ x0,
156
+ x1,
157
+ y0,
158
+ y1
159
+ ], "shading Domain");
160
+ const columnAx = matrix[0] ?? 1;
161
+ const columnAy = matrix[1] ?? 0;
162
+ const columnBx = matrix[2] ?? 0;
163
+ const columnBy = matrix[3] ?? 1;
164
+ requireFinite([
165
+ columnAx,
166
+ columnAy,
167
+ columnBx,
168
+ columnBy,
169
+ matrix[4] ?? 0,
170
+ matrix[5] ?? 0
171
+ ], "shading Matrix");
172
+ return capSampleGrid(axisSamples(x1 - x0, columnAx, columnAy), axisSamples(y1 - y0, columnBx, columnBy));
173
+ }
174
+ function shadingSampleGridForUses(domain, matrices) {
175
+ if (matrices.length === 0) return shadingSampleGrid(domain, IDENTITY_MATRIX);
176
+ let n = 1;
177
+ let m = 1;
178
+ for (const matrix of matrices) {
179
+ const grid = shadingSampleGrid(domain, matrix);
180
+ n = Math.max(n, grid.n);
181
+ m = Math.max(m, grid.m);
182
+ }
183
+ return capSampleGrid(n, m);
184
+ }
185
+ function domainValue(min, max, index, size) {
186
+ if (size <= 1) return min;
187
+ return min + (max - min) * index / (size - 1);
188
+ }
189
+ function sampleShadingToCmyk(program, domain, size, convert) {
190
+ const x0 = domain[0] ?? 0;
191
+ const x1 = domain[1] ?? 0;
192
+ const y0 = domain[2] ?? 0;
193
+ const y1 = domain[3] ?? 0;
194
+ const samples = new Uint8Array(size.n * size.m * SAMPLE_CHANNELS * SAMPLE_BYTES);
195
+ const toCmyk = createSamplingColorConverter(convert).convert;
196
+ let offset = 0;
197
+ for (let j = 0; j < size.m; j++) {
198
+ const y = domainValue(y0, y1, j, size.m);
199
+ for (let i = 0; i < size.n; i++) {
200
+ const x = domainValue(x0, x1, i, size.n);
201
+ const rgb = evaluateCalculator(program, [x, y]);
202
+ if (rgb.length !== 3) refuse("shading function outputs", "3 numbers");
203
+ const cmyk = toCmyk([
204
+ clamp01(rgb[0] ?? 0),
205
+ clamp01(rgb[1] ?? 0),
206
+ clamp01(rgb[2] ?? 0)
207
+ ]);
208
+ for (const channel of cmyk) {
209
+ samples[offset] = Math.round(clamp01(channel) * 255);
210
+ offset += SAMPLE_BYTES;
211
+ }
212
+ }
213
+ }
214
+ return samples;
215
+ }
216
+ function chargeShadingSamples(budget, samples, operations) {
217
+ const bytes = samples * SAMPLE_CHANNELS * SAMPLE_BYTES;
218
+ if (!Number.isFinite(samples) || samples < 0 || !Number.isFinite(bytes) || bytes < 0) refuse(`document shading samples ${samples}`, "a finite non-negative sample count and byte charge");
219
+ if (!Number.isFinite(operations) || operations < 0) refuse(`document calculator operations ${operations}`, "a finite non-negative operator count");
220
+ const totalSamples = (budget.shadingSamples ?? 0) + samples;
221
+ const totalBytes = budget.decodedBytes + bytes;
222
+ const totalOperations = (budget.calculatorOperations ?? 0) + operations;
223
+ if (!Number.isFinite(totalSamples) || totalSamples > 4194304) refuse(`document shading samples ${totalSamples}`, `at most ${MAX_DOCUMENT_SHADING_SAMPLES} samples`);
224
+ if (!Number.isFinite(totalOperations) || totalOperations > 436207616) refuse(`document calculator operations ${totalOperations}`, `at most ${MAX_DOCUMENT_CALCULATOR_OPERATIONS} operators`);
225
+ if (!Number.isFinite(totalBytes) || totalBytes > 134217728) refuse(`total decoded content length ${totalBytes}`, `at most ${MAX_DOCUMENT_CONTENT_BYTES} bytes`);
226
+ budget.shadingSamples = totalSamples;
227
+ budget.decodedBytes = totalBytes;
228
+ budget.calculatorOperations = totalOperations;
229
+ }
230
+ const resolved = (document, object) => object instanceof PDFRef ? document.context.lookup(object) : object;
231
+ function replaceType1Function(document, shading, pattern, convert, budget, painted, sampleDomain, sampleSize) {
232
+ const existing = shading.get(PDFName.of("Function"));
233
+ const stream = rawStreamOf(resolved(document, existing));
234
+ if (!stream) return refuse("shading FunctionType 4 stream", "a FunctionType 4 stream");
235
+ const shadingDomain = shadingDomainOf(shading);
236
+ const matrix = shadingDeviceMatrix(shading, pattern);
237
+ requireFinite(matrix, "shading Matrix");
238
+ const domain = sampleDomain ? [...sampleDomain] : sampleDomainForPainted(shadingDomain, matrix, painted);
239
+ const size = sampleSize ?? shadingSampleGrid(domain, matrix);
240
+ const program = parseCalculator(new TextDecoder("latin1").decode(decodeContent(stream, budget)));
241
+ const samples = size.n * size.m;
242
+ chargeShadingSamples(budget, samples, samples * staticOperatorCount(program));
243
+ const data = sampleShadingToCmyk(program, domain, size, convert);
244
+ const replacement = document.context.stream(deflateSync(data), {
245
+ FunctionType: 0,
246
+ Domain: domain,
247
+ Range: [
248
+ 0,
249
+ 1,
250
+ 0,
251
+ 1,
252
+ 0,
253
+ 1,
254
+ 0,
255
+ 1
256
+ ],
257
+ Size: [size.n, size.m],
258
+ BitsPerSample: 8,
259
+ Order: 1,
260
+ Filter: "FlateDecode"
261
+ });
262
+ shading.set(PDFName.of("Function"), document.context.register(replacement));
263
+ return size;
264
+ }
265
+ //#endregion
266
+ export { MAX_DOCUMENT_CALCULATOR_OPERATIONS, MAX_DOCUMENT_SHADING_SAMPLES, MAX_SAMPLING_COLOR_CACHE, MAX_SHADING_SAMPLES, SHADING_SAMPLE_SPACING_PT, chargeShadingSamples, createSamplingColorConverter, domainValue, matrixOf, packedRgb8, replaceType1Function, sampleDomainForPainted, sampleDomainForUses, sampleShadingToCmyk, shadingDeviceMatrix, shadingDomainOf, shadingSampleGrid, shadingSampleGridForUses, unionSampleDomain, unpackRgb8 };
@@ -11,6 +11,8 @@ export interface PressReport {
11
11
  fills: number;
12
12
  strokes: number;
13
13
  gradientStops: number;
14
+ /** Function-based shadings rewritten as DeviceCMYK sampled functions. */
15
+ sampledShadings: number;
14
16
  groups: number;
15
17
  }
16
18
  //#endregion
@@ -6,29 +6,38 @@ const key = (name) => PDFName.of(name);
6
6
  const resolved = (state, object) => object instanceof PDFRef ? state.document.context.lookup(object) : object;
7
7
  const dictionary = (state, object) => dictionaryOf(resolved(state, object));
8
8
  const stream = (state, object) => rawStreamOf(resolved(state, object));
9
- function visitContent(state, object, replace) {
9
+ function rememberRef(state, object) {
10
+ if (!(object instanceof PDFRef)) return false;
11
+ const label = object.toString();
12
+ if (state.references.has(label)) return true;
13
+ state.references.add(label);
14
+ return false;
15
+ }
16
+ function visitContent(state, object, replace, enclosing, replay = false, nestResources = true) {
10
17
  if (!object) return;
11
- if (object instanceof PDFRef) {
12
- const label = object.toString();
13
- if (state.references.has(label)) return;
14
- state.references.add(label);
15
- }
18
+ const seenRef = rememberRef(state, object);
16
19
  const found = stream(state, object);
17
- if (!found || state.streams.has(found)) return;
18
- state.streams.add(found);
19
- state.actions?.group(found.dict, found, state.maskSources.has(found));
20
- state.actions?.content(object, found, replace);
21
- visitResources(state, found.dict.lookupMaybe(key("Resources"), PDFDict));
20
+ if (!found) return;
21
+ const seenStream = state.streams.has(found);
22
+ if (!seenStream) state.streams.add(found);
23
+ const seen = seenRef || seenStream;
24
+ if (seen && !replay) return;
25
+ const own = found.dict.lookupMaybe(key("Resources"), PDFDict);
26
+ if (!seen) state.actions?.group(found.dict, found, state.maskSources.has(found));
27
+ state.actions?.content(object, found, replace, own ?? enclosing);
28
+ if (nestResources && !seen) visitResources(state, own);
22
29
  }
23
- function visitSoftMask(state, object) {
24
- const mask = dictionary(state, dictionary(state, object)?.get(key("SMask")));
30
+ function visitSoftMask(state, object, enclosing) {
31
+ const graphicsState = dictionary(state, object);
32
+ const mask = dictionary(state, graphicsState?.get(key("SMask")));
25
33
  const sourceObject = mask?.get(key("G"));
26
34
  const source = stream(state, sourceObject);
27
- if (!source || !sourceObject) return;
35
+ if (!graphicsState || !mask || !source || !sourceObject) return;
36
+ state.actions?.softMask?.(graphicsState, mask, source, source.dict.lookupMaybe(key("Resources"), PDFDict) ?? enclosing);
28
37
  state.maskSources.add(source);
29
- visitContent(state, sourceObject, (replacement) => mask?.set(key("G"), replacement));
38
+ visitContent(state, sourceObject, (replacement) => mask?.set(key("G"), replacement), enclosing);
30
39
  }
31
- function visitAppearances(state, page) {
40
+ function visitAppearances(state, page, enclosing) {
32
41
  const annotations = resolved(state, page.get(key("Annots")));
33
42
  if (!(annotations instanceof PDFArray)) return;
34
43
  for (let index = 0; index < annotations.size(); index++) {
@@ -40,10 +49,10 @@ function visitAppearances(state, page) {
40
49
  ]) {
41
50
  const object = appearance?.get(key(name));
42
51
  if (!object) continue;
43
- if (stream(state, object)) visitContent(state, object, (replacement) => appearance?.set(key(name), replacement));
52
+ if (stream(state, object)) visitContent(state, object, (replacement) => appearance?.set(key(name), replacement), enclosing);
44
53
  else {
45
54
  const states = dictionary(state, object);
46
- for (const [stateName, stateObject] of states?.entries() ?? []) visitContent(state, stateObject, (replacement) => states?.set(stateName, replacement));
55
+ for (const [stateName, stateObject] of states?.entries() ?? []) visitContent(state, stateObject, (replacement) => states?.set(stateName, replacement), enclosing);
47
56
  }
48
57
  }
49
58
  }
@@ -58,8 +67,8 @@ function visitFunction(state, object) {
58
67
  state.actions?.function(fn, decision);
59
68
  for (let index = 0; index < (functions?.size() ?? 0); index++) visitFunction(state, functions?.get(index));
60
69
  }
61
- function visitShading(state, shading) {
62
- if (state.actions?.shading(shading)) visitFunction(state, shading.get(key("Function")));
70
+ function visitShading(state, shading, pattern) {
71
+ if (state.actions?.shading(shading, pattern)) visitFunction(state, shading.get(key("Function")));
63
72
  }
64
73
  function visitShadings(state, resources) {
65
74
  const shadings = resources.lookupMaybe(key("Shading"), PDFDict);
@@ -75,14 +84,14 @@ function visitPatterns(state, resources) {
75
84
  if (!found) continue;
76
85
  if (numberOf(found, "PatternType") === 2) {
77
86
  const shading = dictionary(state, found.get(key("Shading")));
78
- if (shading) visitShading(state, shading);
79
- } else visitContent(state, object, (replacement) => patterns?.set(name, replacement));
87
+ if (shading) visitShading(state, shading, found);
88
+ } else visitContent(state, object, (replacement) => patterns?.set(name, replacement), found.lookupMaybe(key("Resources"), PDFDict) ?? resources);
80
89
  visitResources(state, found.lookupMaybe(key("Resources"), PDFDict));
81
90
  }
82
91
  }
83
92
  function visitGraphicsStates(state, resources) {
84
93
  const graphicsStates = resources.lookupMaybe(key("ExtGState"), PDFDict);
85
- for (const [, object] of graphicsStates?.entries() ?? []) visitSoftMask(state, object);
94
+ for (const [, object] of graphicsStates?.entries() ?? []) visitSoftMask(state, object, resources);
86
95
  }
87
96
  function visitXObjects(state, resources) {
88
97
  const xObjects = resources.lookupMaybe(key("XObject"), PDFDict);
@@ -90,7 +99,7 @@ function visitXObjects(state, resources) {
90
99
  const found = stream(state, object);
91
100
  if (!found) continue;
92
101
  if (nameOf(found.dict.get(key("Subtype"))) === "Image") state.actions?.image(found);
93
- else visitContent(state, object, (replacement) => xObjects?.set(name, replacement));
102
+ else visitContent(state, object, (replacement) => xObjects?.set(name, replacement), found.dict.lookupMaybe(key("Resources"), PDFDict) ?? resources);
94
103
  }
95
104
  }
96
105
  function visitFonts(state, resources) {
@@ -99,18 +108,33 @@ function visitFonts(state, resources) {
99
108
  const font = dictionary(state, object);
100
109
  if (!font || nameOf(font.get(key("Subtype"))) !== "Type3") continue;
101
110
  const charProcs = font.lookupMaybe(key("CharProcs"), PDFDict);
102
- for (const [name, glyph] of charProcs?.entries() ?? []) visitContent(state, glyph, (replacement) => charProcs?.set(name, replacement));
111
+ for (const [name, glyph] of charProcs?.entries() ?? []) visitContent(state, glyph, (replacement) => charProcs?.set(name, replacement), font.lookupMaybe(key("Resources"), PDFDict) ?? resources);
103
112
  visitResources(state, font.lookupMaybe(key("Resources"), PDFDict));
104
113
  }
105
114
  }
106
115
  function visitResources(state, resources) {
107
116
  if (!resources || state.resources.has(resources)) return;
108
117
  state.resources.add(resources);
109
- visitShadings(state, resources);
110
- visitPatterns(state, resources);
111
118
  visitGraphicsStates(state, resources);
112
119
  visitXObjects(state, resources);
113
120
  visitFonts(state, resources);
121
+ visitShadings(state, resources);
122
+ visitPatterns(state, resources);
123
+ }
124
+ function visitPageContents(state, page, contents, pageResources) {
125
+ const replay = Boolean(state.actions?.replayPageContents);
126
+ const nested = [];
127
+ const feed = (object, replace) => {
128
+ visitContent(state, object, replace, pageResources, replay, false);
129
+ const own = stream(state, object)?.dict.lookupMaybe(key("Resources"), PDFDict);
130
+ if (own) nested.push(own);
131
+ };
132
+ if (contents instanceof PDFArray) {
133
+ state.actions?.beginContentsSequence?.();
134
+ for (let index = 0; index < contents.size(); index++) feed(contents.get(index), (replacement) => contents.set(index, replacement));
135
+ state.actions?.endContentsSequence?.();
136
+ } else feed(contents, (replacement) => page.set(key("Contents"), replacement));
137
+ for (const resources of nested) visitResources(state, resources);
114
138
  }
115
139
  function traverse(document, maskSources, actions) {
116
140
  const state = {
@@ -124,11 +148,10 @@ function traverse(document, maskSources, actions) {
124
148
  };
125
149
  for (const page of document.getPages()) {
126
150
  actions?.group(page.node, void 0, false);
127
- const contents = page.node.Contents();
128
- if (contents instanceof PDFArray) for (let index = 0; index < contents.size(); index++) visitContent(state, contents.get(index), (replacement) => contents.set(index, replacement));
129
- else visitContent(state, contents, (replacement) => page.node.set(key("Contents"), replacement));
130
- visitResources(state, page.node.Resources());
131
- visitAppearances(state, page.node);
151
+ const pageResources = page.node.Resources();
152
+ visitPageContents(state, page.node, page.node.Contents(), pageResources);
153
+ visitResources(state, pageResources);
154
+ visitAppearances(state, page.node, pageResources);
132
155
  }
133
156
  }
134
157
  function walkPressGraph(document, actions) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segnavia/render",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,8 +26,8 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@segnavia/format": "0.2.0",
30
- "@segnavia/frame": "0.2.0",
29
+ "@segnavia/format": "0.4.0",
30
+ "@segnavia/frame": "0.4.0",
31
31
  "playwright-core": "1.55.1",
32
32
  "pdf-lib": "^1.17.1"
33
33
  }