@segnavia/render 0.2.0 → 0.3.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.
@@ -232,6 +232,29 @@ function scan(source, convert, inspect, visit, operatorVisit) {
232
232
  }
233
233
  return counts;
234
234
  }
235
+ function visitContentTokens(bytes, visit) {
236
+ visitContentOperations(bytes, ({ operator, operands }) => visit(operator, operands));
237
+ }
238
+ function visitContentOperations(bytes, visit) {
239
+ scan(decoder.decode(bytes), () => [
240
+ 0,
241
+ 0,
242
+ 0,
243
+ 0
244
+ ], false, void 0, (operator, operands) => {
245
+ visit({
246
+ end: operator.end,
247
+ operands: operands.flatMap((operand) => operand.kind === "name" || operand.kind === "number" ? [{
248
+ end: operand.end,
249
+ kind: operand.kind,
250
+ raw: operand.raw,
251
+ start: operand.start
252
+ }] : []),
253
+ operator: operator.raw,
254
+ start: operator.start
255
+ });
256
+ });
257
+ }
235
258
  function inspectContent(bytes) {
236
259
  return scan(decoder.decode(bytes), () => refusal("RGB color operator after conversion", "CMYK operators"), true);
237
260
  }
@@ -266,4 +289,4 @@ function rewriteContent(bytes, convert) {
266
289
  };
267
290
  }
268
291
  //#endregion
269
- export { inspectContent, rewriteContent };
292
+ export { inspectContent, rewriteContent, visitContentOperations, visitContentTokens };
@@ -0,0 +1,275 @@
1
+ import { IDENTITY_MATRIX, multiplyAffine } from "./affine.js";
2
+ import { visitContentTokens } from "./content.js";
3
+ import { numberOf } from "./pdf-objects.js";
4
+ import { clearPath, closePath, curveTo, curveV, curveY, deviceBox, intersectRect, lineTo, moveTo, rectTo } from "./pattern-path.js";
5
+ const cloneGraphics = (graphics) => ({
6
+ clip: graphics.clip ? { ...graphics.clip } : void 0,
7
+ ctm: graphics.ctm.slice(),
8
+ fill: graphics.fill,
9
+ lineJoin: graphics.lineJoin,
10
+ lineWidth: graphics.lineWidth,
11
+ miterLimit: graphics.miterLimit,
12
+ stroke: graphics.stroke
13
+ });
14
+ function numbers(operands, count) {
15
+ if (operands.length !== count || operands.some((operand) => operand.kind !== "number")) return void 0;
16
+ return operands.map((operand) => Number(operand.raw));
17
+ }
18
+ function nameOf(operands) {
19
+ if (operands.length !== 1 || operands[0]?.kind !== "name") return void 0;
20
+ return operands[0].raw.slice(1);
21
+ }
22
+ function unionRect(left, right) {
23
+ return {
24
+ x0: Math.min(left.x0, right.x0),
25
+ y0: Math.min(left.y0, right.y0),
26
+ x1: Math.max(left.x1, right.x1),
27
+ y1: Math.max(left.y1, right.y1)
28
+ };
29
+ }
30
+ function addPaint(bounds, target, paint) {
31
+ if (!target) return;
32
+ const existing = bounds.get(target);
33
+ if (existing === "unbounded" || paint === "unbounded") {
34
+ bounds.set(target, "unbounded");
35
+ return;
36
+ }
37
+ bounds.set(target, existing ? unionRect(existing, paint) : paint);
38
+ }
39
+ function strokeMargin(graphics) {
40
+ const half = graphics.lineWidth / 2;
41
+ if (!Number.isFinite(half) || half < 0) return void 0;
42
+ if (graphics.lineJoin === 1 || graphics.lineJoin === 2) return half;
43
+ if (graphics.lineJoin !== 0 || !Number.isFinite(graphics.miterLimit) || graphics.miterLimit < 1) return void 0;
44
+ return graphics.miterLimit * half;
45
+ }
46
+ function paintFill(state) {
47
+ if (state.pastSaveCap) {
48
+ addPaint(state.bounds, state.current.fill, "unbounded");
49
+ return;
50
+ }
51
+ if (!state.path.box) return;
52
+ const box = deviceBox(state.path.box, state.current.ctm);
53
+ if (box) addPaint(state.bounds, state.current.fill, box);
54
+ }
55
+ function paintStroke(state) {
56
+ if (state.pastSaveCap) {
57
+ addPaint(state.bounds, state.current.stroke, "unbounded");
58
+ return;
59
+ }
60
+ if (!state.path.box) return;
61
+ const margin = strokeMargin(state.current);
62
+ if (margin === void 0) {
63
+ addPaint(state.bounds, state.current.stroke, "unbounded");
64
+ return;
65
+ }
66
+ const grown = {
67
+ x0: state.path.box.x0 - margin,
68
+ y0: state.path.box.y0 - margin,
69
+ x1: state.path.box.x1 + margin,
70
+ y1: state.path.box.y1 + margin
71
+ };
72
+ const box = deviceBox(grown, state.current.ctm);
73
+ if (box) addPaint(state.bounds, state.current.stroke, box);
74
+ }
75
+ function applyMove(state, operands) {
76
+ const values = numbers(operands, 2);
77
+ if (values) moveTo(state.path, values[0] ?? 0, values[1] ?? 0);
78
+ }
79
+ function applyLine(state, operands) {
80
+ const values = numbers(operands, 2);
81
+ if (values) lineTo(state.path, values[0] ?? 0, values[1] ?? 0);
82
+ }
83
+ function applyCurve(state, operands) {
84
+ const values = numbers(operands, 6);
85
+ if (values) curveTo(state.path, values);
86
+ }
87
+ function applyCurveV(state, operands) {
88
+ const values = numbers(operands, 4);
89
+ if (values) curveV(state.path, values);
90
+ }
91
+ function applyCurveY(state, operands) {
92
+ const values = numbers(operands, 4);
93
+ if (values) curveY(state.path, values);
94
+ }
95
+ function applyRect(state, operands) {
96
+ const values = numbers(operands, 4);
97
+ if (values) rectTo(state.path, values[0] ?? 0, values[1] ?? 0, values[2] ?? 0, values[3] ?? 0);
98
+ }
99
+ function applyCm(state, operands) {
100
+ const values = numbers(operands, 6);
101
+ if (!values) {
102
+ addPaint(state.bounds, state.current.fill, "unbounded");
103
+ addPaint(state.bounds, state.current.stroke, "unbounded");
104
+ return;
105
+ }
106
+ state.current.ctm = multiplyAffine(state.current.ctm, values);
107
+ }
108
+ function clearColor(state, fill) {
109
+ if (fill) state.current.fill = void 0;
110
+ else state.current.stroke = void 0;
111
+ }
112
+ function applyColor(state, operator, operands) {
113
+ const fill = operator === operator.toLowerCase();
114
+ const patternName = nameOf(operands);
115
+ if ((operator === "scn" || operator === "SCN") && patternName) {
116
+ const pattern = state.resolve(patternName);
117
+ if (fill) state.current.fill = pattern;
118
+ else state.current.stroke = pattern;
119
+ return;
120
+ }
121
+ if ((operator === "cs" || operator === "CS") && nameOf(operands) === "Pattern") return;
122
+ clearColor(state, fill);
123
+ }
124
+ const colorHandler = (operator) => (state, operands) => applyColor(state, operator, operands);
125
+ function markUnbounded(state) {
126
+ addPaint(state.bounds, state.current.fill, "unbounded");
127
+ addPaint(state.bounds, state.current.stroke, "unbounded");
128
+ }
129
+ function strokeAndClear(state, close) {
130
+ if (close) closePath(state.path);
131
+ paintStroke(state);
132
+ clearPath(state.path);
133
+ }
134
+ function fillAndClear(state) {
135
+ paintFill(state);
136
+ clearPath(state.path);
137
+ }
138
+ function fillStrokeAndClear(state, close) {
139
+ if (close) closePath(state.path);
140
+ paintFill(state);
141
+ paintStroke(state);
142
+ clearPath(state.path);
143
+ }
144
+ function setLineWidth(state, width) {
145
+ if (!Number.isFinite(width) || width < 0) return false;
146
+ state.current.lineWidth = width;
147
+ return true;
148
+ }
149
+ function setLineJoin(state, join) {
150
+ if (join !== 0 && join !== 1 && join !== 2) return false;
151
+ state.current.lineJoin = join;
152
+ return true;
153
+ }
154
+ function setMiterLimit(state, miter) {
155
+ if (!Number.isFinite(miter) || miter < 1) return false;
156
+ state.current.miterLimit = miter;
157
+ return true;
158
+ }
159
+ function applyStrokeNumber(state, operands, assign) {
160
+ const value = numbers(operands, 1)?.[0];
161
+ if (value === void 0 || !assign(state, value)) markUnbounded(state);
162
+ }
163
+ function applyStyleNumber(dict, key, assign) {
164
+ const value = numberOf(dict, key);
165
+ return value === void 0 || assign(value);
166
+ }
167
+ function applyGraphicsState(state, operands) {
168
+ const name = nameOf(operands);
169
+ const dict = name ? state.resolveGraphicsState(name) : void 0;
170
+ if (!dict) {
171
+ markUnbounded(state);
172
+ return;
173
+ }
174
+ if (!(applyStyleNumber(dict, "LW", (width) => setLineWidth(state, width)) && applyStyleNumber(dict, "LJ", (join) => setLineJoin(state, join)) && applyStyleNumber(dict, "ML", (miter) => setMiterLimit(state, miter)))) markUnbounded(state);
175
+ }
176
+ function saveGraphics(state) {
177
+ if (state.pastSaveCap || state.graphics.length >= 64) {
178
+ state.pastSaveCap = true;
179
+ markUnbounded(state);
180
+ return;
181
+ }
182
+ state.graphics.push(cloneGraphics(state.current));
183
+ }
184
+ function applyClip(state) {
185
+ if (!state.path.box) return;
186
+ const box = deviceBox(state.path.box, state.current.ctm);
187
+ if (!box) return;
188
+ state.current.clip = state.current.clip ? intersectRect(state.current.clip, box) : box;
189
+ }
190
+ function applyShading(state, operands) {
191
+ const name = nameOf(operands);
192
+ const shading = name ? state.resolveShading(name) : void 0;
193
+ if (state.pastSaveCap || !state.current.clip) {
194
+ addPaint(state.shadingBounds, shading, "unbounded");
195
+ return;
196
+ }
197
+ addPaint(state.shadingBounds, shading, state.current.clip);
198
+ }
199
+ const handlers = {
200
+ q: saveGraphics,
201
+ Q: (state) => {
202
+ const restored = state.graphics.pop();
203
+ if (restored) state.current = restored;
204
+ },
205
+ cm: applyCm,
206
+ m: applyMove,
207
+ l: applyLine,
208
+ c: applyCurve,
209
+ v: applyCurveV,
210
+ y: applyCurveY,
211
+ h: (state) => closePath(state.path),
212
+ re: applyRect,
213
+ n: (state) => clearPath(state.path),
214
+ W: applyClip,
215
+ "W*": applyClip,
216
+ sh: applyShading,
217
+ Do: markUnbounded,
218
+ S: (state) => strokeAndClear(state, false),
219
+ s: (state) => strokeAndClear(state, true),
220
+ f: fillAndClear,
221
+ F: fillAndClear,
222
+ "f*": fillAndClear,
223
+ B: (state) => fillStrokeAndClear(state, false),
224
+ "B*": (state) => fillStrokeAndClear(state, false),
225
+ b: (state) => fillStrokeAndClear(state, true),
226
+ "b*": (state) => fillStrokeAndClear(state, true),
227
+ w: (state, operands) => applyStrokeNumber(state, operands, setLineWidth),
228
+ j: (state, operands) => applyStrokeNumber(state, operands, setLineJoin),
229
+ M: (state, operands) => applyStrokeNumber(state, operands, setMiterLimit),
230
+ gs: applyGraphicsState,
231
+ cs: colorHandler("cs"),
232
+ CS: colorHandler("CS"),
233
+ sc: colorHandler("sc"),
234
+ SC: colorHandler("SC"),
235
+ scn: colorHandler("scn"),
236
+ SCN: colorHandler("SCN"),
237
+ rg: colorHandler("rg"),
238
+ RG: colorHandler("RG"),
239
+ g: colorHandler("g"),
240
+ G: colorHandler("G"),
241
+ k: colorHandler("k"),
242
+ K: colorHandler("K")
243
+ };
244
+ function emptyGraphics() {
245
+ return {
246
+ ctm: IDENTITY_MATRIX.slice(),
247
+ lineJoin: 0,
248
+ lineWidth: 1,
249
+ miterLimit: 10
250
+ };
251
+ }
252
+ function createPaintedBoundsTracker(bounds, shadingBounds = /* @__PURE__ */ new WeakMap()) {
253
+ const state = {
254
+ bounds,
255
+ current: emptyGraphics(),
256
+ graphics: [],
257
+ pastSaveCap: false,
258
+ path: {},
259
+ resolve: () => void 0,
260
+ resolveGraphicsState: () => void 0,
261
+ resolveShading: () => void 0,
262
+ shadingBounds
263
+ };
264
+ return { feed(bytes, resolve, resolveGraphicsState = () => void 0, resolveShading = () => void 0) {
265
+ state.resolve = resolve;
266
+ state.resolveGraphicsState = resolveGraphicsState;
267
+ state.resolveShading = resolveShading;
268
+ visitContentTokens(bytes, (operator, operands) => handlers[operator]?.(state, operands));
269
+ } };
270
+ }
271
+ function recordPatternPaintedBounds(bytes, resolve, bounds, resolveGraphicsState = () => void 0, resolveShading = () => void 0, shadingBounds = /* @__PURE__ */ new WeakMap()) {
272
+ createPaintedBoundsTracker(bounds, shadingBounds).feed(bytes, resolve, resolveGraphicsState, resolveShading);
273
+ }
274
+ //#endregion
275
+ export { createPaintedBoundsTracker, recordPatternPaintedBounds, unionRect };
@@ -0,0 +1,102 @@
1
+ import { applyAffine } from "./affine.js";
2
+ //#region src/press/pattern-path.ts
3
+ function includePoint(box, x, y) {
4
+ if (!box) return {
5
+ x0: x,
6
+ y0: y,
7
+ x1: x,
8
+ y1: y
9
+ };
10
+ return {
11
+ x0: Math.min(box.x0, x),
12
+ y0: Math.min(box.y0, y),
13
+ x1: Math.max(box.x1, x),
14
+ y1: Math.max(box.y1, y)
15
+ };
16
+ }
17
+ function addPoint(path, x, y) {
18
+ const point = {
19
+ x,
20
+ y
21
+ };
22
+ path.box = includePoint(path.box, x, y);
23
+ path.current = point;
24
+ return point;
25
+ }
26
+ function deviceBox(box, ctm) {
27
+ const corners = [
28
+ applyAffine(ctm, box.x0, box.y0),
29
+ applyAffine(ctm, box.x1, box.y0),
30
+ applyAffine(ctm, box.x0, box.y1),
31
+ applyAffine(ctm, box.x1, box.y1)
32
+ ];
33
+ const xs = corners.map((point) => point.x);
34
+ const ys = corners.map((point) => point.y);
35
+ const x0 = Math.min(...xs);
36
+ const y0 = Math.min(...ys);
37
+ const x1 = Math.max(...xs);
38
+ const y1 = Math.max(...ys);
39
+ if (!Number.isFinite(x0) || !Number.isFinite(y0) || !Number.isFinite(x1) || !Number.isFinite(y1)) return void 0;
40
+ return {
41
+ x0,
42
+ y0,
43
+ x1,
44
+ y1
45
+ };
46
+ }
47
+ function closePath(path) {
48
+ if (path.start) addPoint(path, path.start.x, path.start.y);
49
+ }
50
+ function clearPath(path) {
51
+ path.box = void 0;
52
+ path.current = void 0;
53
+ path.start = void 0;
54
+ }
55
+ function moveTo(path, x, y) {
56
+ path.start = addPoint(path, x, y);
57
+ }
58
+ function lineTo(path, x, y) {
59
+ addPoint(path, x, y);
60
+ }
61
+ function curveTo(path, values) {
62
+ addPoint(path, values[0] ?? 0, values[1] ?? 0);
63
+ addPoint(path, values[2] ?? 0, values[3] ?? 0);
64
+ addPoint(path, values[4] ?? 0, values[5] ?? 0);
65
+ }
66
+ function curveV(path, values) {
67
+ if (!path.current) return;
68
+ addPoint(path, path.current.x, path.current.y);
69
+ addPoint(path, values[0] ?? 0, values[1] ?? 0);
70
+ addPoint(path, values[2] ?? 0, values[3] ?? 0);
71
+ }
72
+ function curveY(path, values) {
73
+ addPoint(path, values[0] ?? 0, values[1] ?? 0);
74
+ addPoint(path, values[2] ?? 0, values[3] ?? 0);
75
+ addPoint(path, values[2] ?? 0, values[3] ?? 0);
76
+ }
77
+ function rectTo(path, x, y, width, height) {
78
+ addPoint(path, x, y);
79
+ path.start = {
80
+ x,
81
+ y
82
+ };
83
+ addPoint(path, x + width, y);
84
+ addPoint(path, x + width, y + height);
85
+ addPoint(path, x, y + height);
86
+ addPoint(path, x, y);
87
+ }
88
+ function intersectRect(left, right) {
89
+ const x0 = Math.max(left.x0, right.x0);
90
+ const y0 = Math.max(left.y0, right.y0);
91
+ const x1 = Math.min(left.x1, right.x1);
92
+ const y1 = Math.min(left.y1, right.y1);
93
+ if (!(x0 < x1) || !(y0 < y1)) return void 0;
94
+ return {
95
+ x0,
96
+ y0,
97
+ x1,
98
+ y1
99
+ };
100
+ }
101
+ //#endregion
102
+ export { addPoint, clearPath, closePath, curveTo, curveV, curveY, deviceBox, includePoint, intersectRect, lineTo, moveTo, rectTo };
@@ -45,7 +45,13 @@ function groupAction(kind, label, maskSource) {
45
45
  if (label === "missing" || label === "DeviceRGB") return decision("convert", supported);
46
46
  return decision("refuse", supported);
47
47
  }
48
- function shadingAction(kind, type) {
48
+ function shadingAction(kind, type, functionType) {
49
+ if (type === 1) {
50
+ const supported = "ShadingType 1 in an RGB space with FunctionType 4, or DeviceCMYK";
51
+ if (kind === "cmyk") return decision("keep", supported);
52
+ if (kind === "rgb" && functionType === 4) return decision("convert", supported);
53
+ return decision("refuse", supported);
54
+ }
49
55
  const supported = "ShadingType 2 or 3 in an RGB or DeviceCMYK color space";
50
56
  if (type !== 2 && type !== 3) return decision("refuse", supported);
51
57
  if (kind === "rgb") return decision("convert", supported);
@@ -1,7 +1,7 @@
1
1
  import { RenderRefusedError } from "../errors.js";
2
2
  import { groupAction, shadingAction } from "./policy.js";
3
3
  import { inspectContent } from "./content.js";
4
- import { decodeContent, nameOf, numberOf, resolvedColorSpace } from "./pdf-objects.js";
4
+ import { decodeContent, dictionaryOf, nameOf, numberOf, resolvedColorSpace } from "./pdf-objects.js";
5
5
  import { walkPressGraph } from "./visitor.js";
6
6
  import { PDFDict, PDFDocument, PDFName, PDFRef } from "pdf-lib";
7
7
  //#region src/press/report.ts
@@ -32,9 +32,18 @@ function inspectShading(state, shading) {
32
32
  if (state.shadings.has(shading)) return false;
33
33
  state.shadings.add(shading);
34
34
  const type = numberOf(shading, "ShadingType");
35
+ const fn = dictionaryOf(resolved(state, shading.get(key("Function"))));
36
+ const functionType = fn ? numberOf(fn, "FunctionType") : void 0;
35
37
  const space = resolvedColorSpace(state.document, resolved(state, shading.get(key("ColorSpace"))));
36
- const policy = shadingAction(space.kind, type);
37
- if (policy.action === "refuse" || policy.action === "convert") refuse(`ShadingType ${type ?? "missing"} color space ${space.label}`, policy.supported);
38
+ const policy = shadingAction(space.kind, type, functionType);
39
+ if (policy.action !== "keep") {
40
+ if (type === 1) refuse(`ShadingType 1 FunctionType ${functionType ?? "missing"} color space ${space.label}`, policy.supported);
41
+ refuse(`ShadingType ${type ?? "missing"} color space ${space.label}`, policy.supported);
42
+ }
43
+ if (type === 1) {
44
+ state.report.sampledShadings++;
45
+ return false;
46
+ }
38
47
  return true;
39
48
  }
40
49
  function inspectGroup(state, owner, maskSource) {
@@ -58,6 +67,7 @@ async function inspectPressPdf(bytes) {
58
67
  fills: 0,
59
68
  strokes: 0,
60
69
  gradientStops: 0,
70
+ sampledShadings: 0,
61
71
  groups: 0
62
72
  },
63
73
  shadings: /* @__PURE__ */ new WeakSet()