@reekon-tools/boldr-utils 1.21.1 → 1.23.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.
Files changed (104) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasSkia.js +3 -11
  2. package/dist/annotation/canvas/measurementLineStyle.d.ts +3 -0
  3. package/dist/annotation/canvas/measurementLineStyle.js +14 -0
  4. package/dist/export/pdf/annots.d.ts +23 -0
  5. package/dist/export/pdf/annots.js +135 -0
  6. package/dist/export/pdf/color.d.ts +7 -0
  7. package/dist/export/pdf/color.js +39 -0
  8. package/dist/export/pdf/decrypt.d.ts +21 -0
  9. package/dist/export/pdf/decrypt.js +121 -0
  10. package/dist/export/pdf/emit.d.ts +37 -0
  11. package/dist/export/pdf/emit.js +129 -0
  12. package/dist/export/pdf/fonts.d.ts +37 -0
  13. package/dist/export/pdf/fonts.js +85 -0
  14. package/dist/export/pdf/index.d.ts +7 -0
  15. package/dist/export/pdf/index.js +7 -0
  16. package/dist/export/pdf/layers.d.ts +5 -0
  17. package/dist/export/pdf/layers.js +19 -0
  18. package/dist/export/pdf/marks.d.ts +81 -0
  19. package/dist/export/pdf/marks.js +479 -0
  20. package/dist/export/pdf/pageMapping.d.ts +51 -0
  21. package/dist/export/pdf/pageMapping.js +77 -0
  22. package/dist/export/pdf/renderCanvasToPdf.d.ts +2 -0
  23. package/dist/export/pdf/renderCanvasToPdf.js +288 -0
  24. package/dist/export/pdf/resources.d.ts +35 -0
  25. package/dist/export/pdf/resources.js +95 -0
  26. package/dist/export/pdf/tileContent.d.ts +39 -0
  27. package/dist/export/pdf/tileContent.js +218 -0
  28. package/dist/export/pdf/tiles.d.ts +39 -0
  29. package/dist/export/pdf/tiles.js +392 -0
  30. package/dist/export/pdf/types.d.ts +112 -0
  31. package/dist/export/pdf/types.js +10 -0
  32. package/dist/exports.d.ts +1 -1
  33. package/dist/exports.js +1 -1
  34. package/dist/formulas/calculateFormula.d.ts +1 -1
  35. package/dist/formulas/calculateFormula.js +23 -10
  36. package/dist/formulas/evaluateFormula.d.ts +0 -10
  37. package/dist/formulas/evaluateFormula.js +1 -12
  38. package/dist/formulas/groupFormula.d.ts +43 -0
  39. package/dist/formulas/groupFormula.js +168 -0
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.js +1 -1
  42. package/dist/server.d.ts +4 -1
  43. package/dist/server.js +6 -1
  44. package/package.json +9 -1
  45. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  46. package/dist/canvas/AnnotationCanvas.js +0 -10
  47. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  48. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  49. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  50. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  51. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  52. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  53. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  54. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  55. package/dist/canvas/Tool.d.ts +0 -38
  56. package/dist/canvas/Tool.js +0 -1
  57. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  58. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  59. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  60. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  61. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  62. package/dist/canvas/elements/ShapeElement.js +0 -62
  63. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  64. package/dist/canvas/elements/StrokeElement.js +0 -18
  65. package/dist/canvas/measurementPicker.d.ts +0 -10
  66. package/dist/canvas/measurementPicker.js +0 -1
  67. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  68. package/dist/canvas/measurementStampOverlay.js +0 -1
  69. package/dist/canvas/pointerAdapter.d.ts +0 -3
  70. package/dist/canvas/pointerAdapter.js +0 -19
  71. package/dist/canvas/stampLayout.d.ts +0 -5
  72. package/dist/canvas/stampLayout.js +0 -14
  73. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  74. package/dist/canvas/tools/measurementStampTool.js +0 -37
  75. package/dist/canvas/tools/panTool.d.ts +0 -5
  76. package/dist/canvas/tools/panTool.js +0 -25
  77. package/dist/canvas/tools/penTool.d.ts +0 -13
  78. package/dist/canvas/tools/penTool.js +0 -68
  79. package/dist/canvas/tools/selectTool.d.ts +0 -2
  80. package/dist/canvas/tools/selectTool.js +0 -182
  81. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  82. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  83. package/dist/canvas/viewport.d.ts +0 -16
  84. package/dist/canvas/viewport.js +0 -54
  85. package/dist/data/AnnotationDataContext.d.ts +0 -8
  86. package/dist/data/AnnotationDataContext.js +0 -11
  87. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  88. package/dist/data/AnnotationDataProvider.js +0 -4
  89. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  90. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  91. package/dist/data/canvasPersistence.d.ts +0 -3
  92. package/dist/data/canvasPersistence.js +0 -26
  93. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  94. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  95. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  96. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  97. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  98. package/dist/data/hooks/useAnnotationList.js +0 -26
  99. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  100. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  101. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  102. package/dist/hooks/useParseMeasurement.js +0 -14
  103. package/dist/utils/evaluateFormula.d.ts +0 -20
  104. package/dist/utils/evaluateFormula.js +0 -31
@@ -0,0 +1,51 @@
1
+ import type { AnnotationPdfDocument, AnnotationPdfPageSize } from '../../types/annotation.js';
2
+ export interface DocRect {
3
+ x: number;
4
+ y: number;
5
+ width: number;
6
+ height: number;
7
+ }
8
+ /** A PDF page's box in unrotated user space (pdf-lib's getCropBox shape). */
9
+ export interface PdfBox {
10
+ x: number;
11
+ y: number;
12
+ width: number;
13
+ height: number;
14
+ }
15
+ /** PDF `cm` matrix [a b c d e f]: (x, y) → (a·x + c·y + e, b·x + d·y + f). */
16
+ export type AffineMatrix = [
17
+ a: number,
18
+ b: number,
19
+ c: number,
20
+ d: number,
21
+ e: number,
22
+ f: number
23
+ ];
24
+ export type PageRotation = 0 | 90 | 180 | 270;
25
+ export declare const normalizeRotation: (degrees: number) => PageRotation;
26
+ /** Doc-space rect of every page, index-aligned with `doc.pages`. */
27
+ export declare const pageRectsOf: (doc: AnnotationPdfDocument) => DocRect[];
28
+ /** The displayed (post-rotation) size a CropBox has under `rotation`. */
29
+ export declare const rotatedBoxSize: (box: PdfBox, rotation: PageRotation) => AnnotationPdfPageSize;
30
+ /**
31
+ * The matrix that maps a doc-space point (stack coordinates, y-down) onto the
32
+ * page's PDF user space (y-up, unrotated, CropBox origin). `pageRect` is the
33
+ * page's rect in the stack, `cropBox` its box in user space, `rotation` its
34
+ * normalized `/Rotate`.
35
+ */
36
+ export declare const docToPageMatrix: (pageRect: DocRect, cropBox: PdfBox, rotation: PageRotation) => AffineMatrix;
37
+ /** Apply an affine matrix to a point (for tests and bounds checks). */
38
+ export declare const applyMatrix: (m: AffineMatrix, x: number, y: number) => {
39
+ x: number;
40
+ y: number;
41
+ };
42
+ /** The overlap of two rects, or null when they do not overlap. */
43
+ export declare const intersectRects: (a: DocRect, b: DocRect) => DocRect | null;
44
+ export declare const rectsIntersect: (a: DocRect, b: DocRect) => boolean;
45
+ /** Axis-aligned bounds → DocRect, inflated by `pad` on every side. */
46
+ export declare const boundsToRect: (b: {
47
+ minX: number;
48
+ minY: number;
49
+ maxX: number;
50
+ maxY: number;
51
+ }, pad?: number) => DocRect;
@@ -0,0 +1,77 @@
1
+ // Where each PDF page sits in canvas doc space, and the one affine matrix that
2
+ // carries a doc-space point onto that page's PDF user space.
3
+ //
4
+ // Doc space on a PDF-backed canvas IS PDF points, y-down, with the pages laid
5
+ // out by pdfPageDocRects (the continuous vertical filmstrip). PDF user space is
6
+ // y-up, unrotated, and its origin is wherever the CropBox says. The matrix
7
+ // below folds all three differences — the page's offset in the stack, the
8
+ // y-flip, and `/Rotate` — into a single `cm` so every mark is emitted in its
9
+ // stored doc coordinates unchanged.
10
+ //
11
+ // ROTATION CONVENTION (measured against PDFium in __tests__/pageMapping): a
12
+ // `/Rotate 90` page is displayed rotated 90° clockwise, so the unrotated
13
+ // page's top-left corner lands at the displayed page's top-RIGHT. The stored
14
+ // page sizes (AnnotationPdfDocument.pages) are post-rotation extents, i.e.
15
+ // exactly the displayed W×H the matrix maps from.
16
+ import { pdfPageDocRects } from '../../annotation/canvas/pdfDocument.js';
17
+ export const normalizeRotation = (degrees) => {
18
+ const r = (((Math.round(degrees / 90) * 90) % 360) + 360) % 360;
19
+ return r;
20
+ };
21
+ /** Doc-space rect of every page, index-aligned with `doc.pages`. */
22
+ export const pageRectsOf = (doc) => pdfPageDocRects(doc);
23
+ /** The displayed (post-rotation) size a CropBox has under `rotation`. */
24
+ export const rotatedBoxSize = (box, rotation) => rotation === 90 || rotation === 270
25
+ ? { width: box.height, height: box.width }
26
+ : { width: box.width, height: box.height };
27
+ /**
28
+ * The matrix that maps a doc-space point (stack coordinates, y-down) onto the
29
+ * page's PDF user space (y-up, unrotated, CropBox origin). `pageRect` is the
30
+ * page's rect in the stack, `cropBox` its box in user space, `rotation` its
31
+ * normalized `/Rotate`.
32
+ */
33
+ export const docToPageMatrix = (pageRect, cropBox, rotation) => {
34
+ const { x: rx, y: ry } = pageRect;
35
+ const { x: cx, y: cy, width: cw, height: ch } = cropBox;
36
+ switch (rotation) {
37
+ case 0:
38
+ // X = cx + (x - rx); Y = cy + ch - (y - ry)
39
+ return [1, 0, 0, -1, cx - rx, cy + ch + ry];
40
+ case 90:
41
+ // Displayed top-left is the unrotated bottom-left.
42
+ // X = cx + (y - ry); Y = cy + (x - rx)
43
+ return [0, 1, 1, 0, cx - ry, cy - rx];
44
+ case 180:
45
+ // X = cx + cw - (x - rx); Y = cy + (y - ry)
46
+ return [-1, 0, 0, 1, cx + cw + rx, cy - ry];
47
+ case 270:
48
+ // X = cx + cw - (y - ry); Y = cy + ch - (x - rx)
49
+ return [0, -1, -1, 0, cx + cw + ry, cy + ch + rx];
50
+ }
51
+ };
52
+ /** Apply an affine matrix to a point (for tests and bounds checks). */
53
+ export const applyMatrix = (m, x, y) => ({
54
+ x: m[0] * x + m[2] * y + m[4],
55
+ y: m[1] * x + m[3] * y + m[5],
56
+ });
57
+ /** The overlap of two rects, or null when they do not overlap. */
58
+ export const intersectRects = (a, b) => {
59
+ const x = Math.max(a.x, b.x);
60
+ const y = Math.max(a.y, b.y);
61
+ const right = Math.min(a.x + a.width, b.x + b.width);
62
+ const bottom = Math.min(a.y + a.height, b.y + b.height);
63
+ if (right <= x || bottom <= y)
64
+ return null;
65
+ return { x, y, width: right - x, height: bottom - y };
66
+ };
67
+ export const rectsIntersect = (a, b) => a.x < b.x + b.width &&
68
+ a.x + a.width > b.x &&
69
+ a.y < b.y + b.height &&
70
+ a.y + a.height > b.y;
71
+ /** Axis-aligned bounds → DocRect, inflated by `pad` on every side. */
72
+ export const boundsToRect = (b, pad = 0) => ({
73
+ x: b.minX - pad,
74
+ y: b.minY - pad,
75
+ width: b.maxX - b.minX + 2 * pad,
76
+ height: b.maxY - b.minY + 2 * pad,
77
+ });
@@ -0,0 +1,2 @@
1
+ import { type RenderCanvasToPdfInput, type RenderCanvasToPdfResult } from './types.js';
2
+ export declare const renderCanvasToPdf: (input: RenderCanvasToPdfInput) => Promise<RenderCanvasToPdfResult>;
@@ -0,0 +1,288 @@
1
+ // The canvas → PDF renderer: opens the original document, and appends the
2
+ // canvas's layers, marks and tiles to each page's content stream under one
3
+ // matrix per page. The original page content and its own /Annots are never
4
+ // touched, so the source stays vector, searchable and selectable.
5
+ import { EncryptedPDFError, PDFDocument, } from 'pdf-lib';
6
+ import { backgroundLayersOf } from '../../annotation/canvas/backgroundLayers.js';
7
+ import { fitRectToScreen } from '../../annotation/canvas/viewport.js';
8
+ import { STAMP_TILE_SIZE } from '../../annotation/canvas/stampLayout.js';
9
+ import { clipRectOps, withMatrix } from './emit.js';
10
+ import { PdfFonts } from './fonts.js';
11
+ import { layerOps, layerVisibleRect } from './layers.js';
12
+ import { addMarkAnnotation } from './annots.js';
13
+ import { measurementMark, measurementMarkBounds, measurementMarkOps, shapeBounds, shapeMark, shapeOps, strokeBounds, strokeMark, strokeOps, } from './marks.js';
14
+ import { FormScope, PageScope } from './resources.js';
15
+ import { docToPageMatrix, normalizeRotation, pageRectsOf, rectsIntersect, rotatedBoxSize, } from './pageMapping.js';
16
+ import { designBoxFor, tileFootprint, tileOps } from './tiles.js';
17
+ import { PdfExportError, } from './types.js';
18
+ /** Stored page sizes may differ from the file's boxes by rounding only. */
19
+ const PAGE_SIZE_TOLERANCE_PT = 1.5;
20
+ /**
21
+ * Reference view for the calibration fallback: the doc-space edge a scale-1
22
+ * tile covers when the first page is fitted into a landscape tablet-ish view.
23
+ */
24
+ const FALLBACK_VIEW = { width: 1024, height: 768 };
25
+ const intersectBoxes = (a, b) => {
26
+ const x = Math.max(a.x, b.x);
27
+ const y = Math.max(a.y, b.y);
28
+ const right = Math.min(a.x + a.width, b.x + b.width);
29
+ const top = Math.min(a.y + a.height, b.y + b.height);
30
+ if (right <= x || top <= y)
31
+ return a;
32
+ return { x, y, width: right - x, height: top - y };
33
+ };
34
+ /** PDFium's page box: CropBox ∩ MediaBox, falling back to the MediaBox. */
35
+ const pageBox = (page) => {
36
+ const media = page.getMediaBox();
37
+ const crop = page.node.CropBox() ? page.getCropBox() : media;
38
+ return intersectBoxes(crop, media);
39
+ };
40
+ const embedImage = (doc, img) => img.mime === 'image/png' ? doc.embedPng(img.bytes) : doc.embedJpg(img.bytes);
41
+ const loadSource = async (bytes) => {
42
+ try {
43
+ return await PDFDocument.load(bytes, { updateMetadata: false });
44
+ }
45
+ catch (e) {
46
+ // Both checks: pdf-lib's dual CJS/ESM build can hand back an
47
+ // EncryptedPDFError whose class identity differs from the one imported.
48
+ if (e instanceof EncryptedPDFError ||
49
+ (e instanceof Error && /is encrypted/i.test(e.message))) {
50
+ throw new PdfExportError('encrypted', 'The source PDF is encrypted; strip it with stripPdfEncryption before rendering');
51
+ }
52
+ throw new PdfExportError('corrupt', `The source PDF could not be parsed: ${e instanceof Error ? e.message : String(e)}`);
53
+ }
54
+ };
55
+ export const renderCanvasToPdf = async (input) => {
56
+ const { canvas } = input;
57
+ const pdfDoc = canvas.viewport.pdfDocument;
58
+ if (!pdfDoc) {
59
+ throw new PdfExportError('no-pdf-document', 'This canvas has no PDF document to export');
60
+ }
61
+ const doc = await loadSource(input.sourcePdf);
62
+ const pages = doc.getPages();
63
+ if (pages.length !== pdfDoc.pageCount ||
64
+ pdfDoc.pages.length !== pages.length) {
65
+ throw new PdfExportError('page-count-mismatch', `The canvas records ${pdfDoc.pages.length} pages but the PDF has ${pages.length}`);
66
+ }
67
+ const pageRects = pageRectsOf(pdfDoc);
68
+ const skipped = [];
69
+ // Per-page geometry, validated against what the canvas was laid out from.
70
+ const geometry = pages.map((page, i) => {
71
+ const box = pageBox(page);
72
+ const rotation = normalizeRotation(page.getRotation().angle);
73
+ const size = rotatedBoxSize(box, rotation);
74
+ const stored = pdfDoc.pages[i];
75
+ if (Math.abs(size.width - stored.width) > PAGE_SIZE_TOLERANCE_PT ||
76
+ Math.abs(size.height - stored.height) > PAGE_SIZE_TOLERANCE_PT) {
77
+ throw new PdfExportError('page-geometry-mismatch', `Page ${i + 1} is ${size.width}×${size.height} pt in the PDF but ${stored.width}×${stored.height} pt on the canvas`);
78
+ }
79
+ return { page, box, rotation, rect: pageRects[i] };
80
+ });
81
+ const fonts = await PdfFonts.create(doc, input.primaryFont);
82
+ // Background layers: embed once, draw on every page they overlap.
83
+ const layers = backgroundLayersOf(canvas.viewport);
84
+ const layerImages = new Map();
85
+ for (const layer of layers) {
86
+ if (layer.format === 'svg') {
87
+ skipped.push({
88
+ id: layer.id,
89
+ kind: 'layer',
90
+ reason: 'SVG background layers are not exported yet',
91
+ });
92
+ continue;
93
+ }
94
+ const bytes = input.layerImages[layer.id];
95
+ if (!bytes) {
96
+ skipped.push({
97
+ id: layer.id,
98
+ kind: 'layer',
99
+ reason: 'Layer image bytes were not provided',
100
+ });
101
+ continue;
102
+ }
103
+ try {
104
+ layerImages.set(layer.id, await embedImage(doc, bytes));
105
+ }
106
+ catch (e) {
107
+ skipped.push({
108
+ id: layer.id,
109
+ kind: 'layer',
110
+ reason: `Layer image could not be embedded: ${e instanceof Error ? e.message : String(e)}`,
111
+ });
112
+ }
113
+ }
114
+ // Tiles: resolve content once; embed object thumbnails once.
115
+ const tiles = canvas.placedMeasurements.map((placed) => {
116
+ const content = input.resolveTile(placed) ?? {
117
+ kind: 'empty',
118
+ };
119
+ return { placed, content };
120
+ });
121
+ const thumbnails = new Map();
122
+ for (const { placed, content } of tiles) {
123
+ if (content.kind === 'object' && content.thumbnail) {
124
+ try {
125
+ thumbnails.set(placed.id, await embedImage(doc, content.thumbnail));
126
+ }
127
+ catch (e) {
128
+ skipped.push({
129
+ id: placed.id,
130
+ kind: 'tile',
131
+ reason: `Object thumbnail could not be embedded: ${e instanceof Error ? e.message : String(e)}`,
132
+ });
133
+ }
134
+ }
135
+ }
136
+ const firstRect = pageRects[0];
137
+ const fallbackDocEdge = STAMP_TILE_SIZE /
138
+ Math.max(fitRectToScreen(firstRect, FALLBACK_VIEW.width, FALLBACK_VIEW.height)
139
+ .zoom, 1e-6);
140
+ const hiddenLayerIds = new Set(canvas.layers.filter((l) => l.visible === false).map((l) => l.id));
141
+ const visible = (layerId) => !hiddenLayerIds.has(layerId);
142
+ const mode = input.mode ?? 'annotations';
143
+ let annotationCount = 0;
144
+ /** Content string for a tile's annotation (what Acrobat's comment shows). */
145
+ const tileContents = (content) => {
146
+ switch (content.kind) {
147
+ case 'measurement':
148
+ return [
149
+ content.label,
150
+ `${content.negative ? '-' : ''}${content.value} ${content.unit}`.trim(),
151
+ ]
152
+ .filter(Boolean)
153
+ .join(': ');
154
+ case 'planeDimension':
155
+ return `${content.value} ${content.unit}`.trim();
156
+ case 'header':
157
+ return (content.descriptor.name ??
158
+ content.descriptor.badges.map((b) => b.text).join(' '));
159
+ case 'object':
160
+ return content.name;
161
+ case 'literal':
162
+ return content.value || undefined;
163
+ case 'instructions':
164
+ return content.text || undefined;
165
+ default:
166
+ return undefined;
167
+ }
168
+ };
169
+ for (const { page, box, rotation, rect } of geometry) {
170
+ const matrix = docToPageMatrix(rect, box, rotation);
171
+ const onPage = (b) => b !== null && rectsIntersect(b, rect);
172
+ if (mode === 'flatten') {
173
+ // Everything into the page's content stream, under one matrix + clip.
174
+ const scope = new PageScope(page, doc.context);
175
+ const ctx = { scope, fonts, skipped };
176
+ const ops = [];
177
+ for (const layer of layers) {
178
+ const img = layerImages.get(layer.id);
179
+ if (!img || !onPage(layerVisibleRect(layer)))
180
+ continue;
181
+ ops.push(...layerOps(layer, scope.imageKey(img)));
182
+ }
183
+ for (const s of canvas.strokes) {
184
+ if (!visible(s.layerId) || !onPage(strokeBounds(s)))
185
+ continue;
186
+ ops.push(...strokeOps(s, ctx));
187
+ }
188
+ for (const sh of canvas.shapes) {
189
+ if (!visible(sh.layerId) || !onPage(shapeBounds(sh)))
190
+ continue;
191
+ ops.push(...shapeOps(sh, ctx));
192
+ }
193
+ for (const { placed } of tiles) {
194
+ if (!visible(placed.layerId) || !onPage(measurementMarkBounds(placed)))
195
+ continue;
196
+ ops.push(...measurementMarkOps(placed, ctx));
197
+ }
198
+ for (const { placed, content } of tiles) {
199
+ if (!visible(placed.layerId))
200
+ continue;
201
+ const footprint = tileFootprint(placed, canvas, input.calibration, fallbackDocEdge, designBoxFor(placed, content));
202
+ if (!onPage(footprint))
203
+ continue;
204
+ ops.push(...tileOps({ scope, fonts, thumbnail: thumbnails.get(placed.id) }, content, footprint));
205
+ }
206
+ if (ops.length === 0)
207
+ continue;
208
+ page.pushOperators(...withMatrix(matrix, [
209
+ ...clipRectOps(rect.x, rect.y, rect.width, rect.height),
210
+ ...ops,
211
+ ]));
212
+ continue;
213
+ }
214
+ // Annotation mode: one annotation per element, each with its own
215
+ // appearance stream and resources. Same z-order as the screen.
216
+ const add = (mark, scope, groupWith) => {
217
+ if (!mark)
218
+ return null;
219
+ const ref = addMarkAnnotation({
220
+ doc,
221
+ page,
222
+ mark,
223
+ pageRect: rect,
224
+ matrix,
225
+ scope,
226
+ groupWith,
227
+ });
228
+ if (ref)
229
+ annotationCount++;
230
+ return ref;
231
+ };
232
+ for (const layer of layers) {
233
+ const img = layerImages.get(layer.id);
234
+ const bounds = layerVisibleRect(layer);
235
+ if (!img || !onPage(bounds))
236
+ continue;
237
+ const scope = new FormScope(doc.context);
238
+ add({
239
+ id: layer.id,
240
+ ops: layerOps(layer, scope.imageKey(img)),
241
+ bounds,
242
+ annot: {
243
+ subtype: 'Stamp',
244
+ contents: layer.fileId ? 'Placed file' : 'Background image',
245
+ },
246
+ }, scope);
247
+ }
248
+ for (const s of canvas.strokes) {
249
+ if (!visible(s.layerId) || !onPage(strokeBounds(s)))
250
+ continue;
251
+ const scope = new FormScope(doc.context);
252
+ add(strokeMark(s, { scope, fonts, skipped }), scope);
253
+ }
254
+ for (const sh of canvas.shapes) {
255
+ if (!visible(sh.layerId) || !onPage(shapeBounds(sh)))
256
+ continue;
257
+ const scope = new FormScope(doc.context);
258
+ add(shapeMark(sh, { scope, fonts, skipped }), scope);
259
+ }
260
+ // A measurement's line/rect and its tile are grouped (Acrobat honours
261
+ // /IRT + /RT /Group; other viewers treat them independently).
262
+ const markRefs = new Map();
263
+ for (const { placed } of tiles) {
264
+ if (!visible(placed.layerId) || !onPage(measurementMarkBounds(placed)))
265
+ continue;
266
+ const scope = new FormScope(doc.context);
267
+ const ref = add(measurementMark(placed, { scope, fonts, skipped }), scope);
268
+ if (ref)
269
+ markRefs.set(placed.id, ref);
270
+ }
271
+ for (const { placed, content } of tiles) {
272
+ if (!visible(placed.layerId))
273
+ continue;
274
+ const footprint = tileFootprint(placed, canvas, input.calibration, fallbackDocEdge, designBoxFor(placed, content));
275
+ if (!onPage(footprint))
276
+ continue;
277
+ const scope = new FormScope(doc.context);
278
+ add({
279
+ id: placed.id,
280
+ ops: tileOps({ scope, fonts, thumbnail: thumbnails.get(placed.id) }, content, footprint),
281
+ bounds: footprint,
282
+ annot: { subtype: 'Stamp', contents: tileContents(content) },
283
+ }, scope, markRefs.get(placed.id));
284
+ }
285
+ }
286
+ const pdf = await doc.save({ useObjectStreams: false });
287
+ return { pdf, pageCount: pages.length, annotationCount, skipped };
288
+ };
@@ -0,0 +1,35 @@
1
+ import { PDFContext, PDFDict, PDFFont, PDFImage, PDFName, PDFPage } from 'pdf-lib';
2
+ export interface ResourceScope {
3
+ fontKey(font: PDFFont): PDFName;
4
+ imageKey(image: PDFImage): PDFName;
5
+ /** An ExtGState carrying `alpha` (CA/ca), or undefined at full opacity. */
6
+ alphaState(alpha: number): PDFName | undefined;
7
+ }
8
+ /** Resources on the page itself — content-stream (flatten) output. */
9
+ export declare class PageScope implements ResourceScope {
10
+ private readonly page;
11
+ private readonly context;
12
+ private readonly fonts;
13
+ private readonly images;
14
+ private readonly alphas;
15
+ constructor(page: PDFPage, context: PDFContext);
16
+ fontKey(font: PDFFont): PDFName;
17
+ imageKey(image: PDFImage): PDFName;
18
+ alphaState(alpha: number): PDFName | undefined;
19
+ }
20
+ /** Resources collected for ONE appearance stream (annotation mode). */
21
+ export declare class FormScope implements ResourceScope {
22
+ private readonly context;
23
+ private readonly fontDict;
24
+ private readonly xobjectDict;
25
+ private readonly gsDict;
26
+ private readonly fonts;
27
+ private readonly images;
28
+ private readonly alphas;
29
+ constructor(context: PDFContext);
30
+ fontKey(font: PDFFont): PDFName;
31
+ imageKey(image: PDFImage): PDFName;
32
+ alphaState(alpha: number): PDFName | undefined;
33
+ /** The /Resources dictionary for the appearance stream. */
34
+ resources(): PDFDict;
35
+ }
@@ -0,0 +1,95 @@
1
+ // Where an element's PDF resources (fonts, images, opacity states) get
2
+ // registered. The emitters only ask a scope for a resource NAME; whether that
3
+ // name lands in the page's resource dictionary (flatten mode) or in one
4
+ // annotation appearance stream's own /Resources (annotation mode) is the
5
+ // scope's business.
6
+ import { PDFName, } from 'pdf-lib';
7
+ const roundAlpha = (alpha) => Math.round(Math.min(1, Math.max(0, alpha)) * 1000) / 1000;
8
+ const extGState = (context, alpha) => context.obj({ Type: 'ExtGState', CA: alpha, ca: alpha });
9
+ /** Resources on the page itself — content-stream (flatten) output. */
10
+ export class PageScope {
11
+ constructor(page, context) {
12
+ this.page = page;
13
+ this.context = context;
14
+ this.fonts = new Map();
15
+ this.images = new Map();
16
+ this.alphas = new Map();
17
+ }
18
+ fontKey(font) {
19
+ let key = this.fonts.get(font);
20
+ if (!key) {
21
+ key = this.page.node.newFontDictionary(font.name, font.ref);
22
+ this.fonts.set(font, key);
23
+ }
24
+ return key;
25
+ }
26
+ imageKey(image) {
27
+ let key = this.images.get(image);
28
+ if (!key) {
29
+ key = this.page.node.newXObject('Im', image.ref);
30
+ this.images.set(image, key);
31
+ }
32
+ return key;
33
+ }
34
+ alphaState(alpha) {
35
+ const a = roundAlpha(alpha);
36
+ if (a >= 1)
37
+ return undefined;
38
+ let key = this.alphas.get(a);
39
+ if (!key) {
40
+ key = this.page.node.newExtGState('GSa', extGState(this.context, a));
41
+ this.alphas.set(a, key);
42
+ }
43
+ return key;
44
+ }
45
+ }
46
+ /** Resources collected for ONE appearance stream (annotation mode). */
47
+ export class FormScope {
48
+ constructor(context) {
49
+ this.context = context;
50
+ this.fonts = new Map();
51
+ this.images = new Map();
52
+ this.alphas = new Map();
53
+ this.fontDict = context.obj({});
54
+ this.xobjectDict = context.obj({});
55
+ this.gsDict = context.obj({});
56
+ }
57
+ fontKey(font) {
58
+ let key = this.fonts.get(font);
59
+ if (!key) {
60
+ key = PDFName.of(`F${this.fonts.size + 1}`);
61
+ this.fontDict.set(key, font.ref);
62
+ this.fonts.set(font, key);
63
+ }
64
+ return key;
65
+ }
66
+ imageKey(image) {
67
+ let key = this.images.get(image);
68
+ if (!key) {
69
+ key = PDFName.of(`Im${this.images.size + 1}`);
70
+ this.xobjectDict.set(key, image.ref);
71
+ this.images.set(image, key);
72
+ }
73
+ return key;
74
+ }
75
+ alphaState(alpha) {
76
+ const a = roundAlpha(alpha);
77
+ if (a >= 1)
78
+ return undefined;
79
+ let key = this.alphas.get(a);
80
+ if (!key) {
81
+ key = PDFName.of(`GS${this.alphas.size + 1}`);
82
+ this.gsDict.set(key, extGState(this.context, a));
83
+ this.alphas.set(a, key);
84
+ }
85
+ return key;
86
+ }
87
+ /** The /Resources dictionary for the appearance stream. */
88
+ resources() {
89
+ return this.context.obj({
90
+ Font: this.fontDict,
91
+ XObject: this.xobjectDict,
92
+ ExtGState: this.gsDict,
93
+ });
94
+ }
95
+ }
@@ -0,0 +1,39 @@
1
+ import type { AnnotationCanvasState, PlacedMeasurementRef } from '../../types/annotation.js';
2
+ import { DecimalTolerance, FractionalTolerance, Units, type Formula, type Group, type Measurement, type Section } from '../../types/firestore.js';
3
+ import type { EmbeddedImage, ResolvedTileContent } from './types.js';
4
+ /** Fill for a FORM formula-column tile (the job page's purple). */
5
+ export declare const FORMULA_COLUMN_TILE_FILL = "#7B4783";
6
+ /** Fill for a plane-computed length tile (calibration teal). */
7
+ export declare const PLANE_COMPUTED_TILE_FILL = "#0E7490";
8
+ export interface TileFileInfo {
9
+ name: string;
10
+ type?: string;
11
+ thumbnail?: EmbeddedImage;
12
+ }
13
+ export interface TileResolutionContext {
14
+ canvas: Pick<AnnotationCanvasState, 'planes'>;
15
+ units: Units;
16
+ fractionalTolerance?: FractionalTolerance;
17
+ decimalTolerance?: DecimalTolerance;
18
+ groups: Group[];
19
+ sections: Section[];
20
+ /** Every measurement a tile may reference, by id. */
21
+ measurementsById: Map<string, Measurement>;
22
+ formulas?: Formula[];
23
+ /** Placed object files, by file id. */
24
+ filesById?: Map<string, TileFileInfo>;
25
+ /** The canvas file's own group — the fallback source group for a tile. */
26
+ hostGroupId?: string;
27
+ }
28
+ /**
29
+ * The tile fill, in measurementTileFill's precedence: unsaved scratch grey-blue,
30
+ * completed grey, an explicit colour, formula magenta, else tile blue.
31
+ */
32
+ export declare const measurementTileFillFor: ({ internal, isCompleted, groupFormula, color, }: {
33
+ internal?: boolean;
34
+ isCompleted?: boolean;
35
+ groupFormula?: unknown;
36
+ color?: string;
37
+ }) => string;
38
+ /** What a placed tile shows, from the documents in `ctx`. */
39
+ export declare const resolveTileContent: (placed: PlacedMeasurementRef, ctx: TileResolutionContext) => ResolvedTileContent;