@reekon-tools/boldr-utils 1.23.0 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/annotation/canvas/pdfCanvasPages.d.ts +25 -0
- package/dist/annotation/canvas/pdfCanvasPages.js +255 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +11 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +17 -1
- package/dist/annotation/canvas/viewport.d.ts +10 -0
- package/dist/annotation/canvas/viewport.js +26 -0
- package/dist/annotation/data/AnnotationDataProvider.d.ts +6 -0
- package/dist/annotation/data/InMemoryAnnotationProvider.d.ts +6 -0
- package/dist/annotation/data/InMemoryAnnotationProvider.js +25 -0
- package/dist/annotation/data/hooks/usePdfCanvasDoc.d.ts +29 -0
- package/dist/annotation/data/hooks/usePdfCanvasDoc.js +353 -0
- package/dist/export/pdf/index.d.ts +2 -2
- package/dist/export/pdf/index.js +1 -1
- package/dist/export/pdf/renderCanvasToPdf.d.ts +9 -1
- package/dist/export/pdf/renderCanvasToPdf.js +209 -113
- package/dist/export/pdf/types.d.ts +13 -0
- package/dist/exports.d.ts +4 -1
- package/dist/exports.js +4 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -0
- package/dist/types/firestore.d.ts +6 -0
- package/dist/types/firestore.js +5 -0
- package/dist/types/pdfCanvas.d.ts +38 -0
- package/dist/types/pdfCanvas.js +16 -0
- package/package.json +1 -1
|
@@ -2,8 +2,17 @@
|
|
|
2
2
|
// canvas's layers, marks and tiles to each page's content stream under one
|
|
3
3
|
// matrix per page. The original page content and its own /Annots are never
|
|
4
4
|
// touched, so the source stays vector, searchable and selectable.
|
|
5
|
-
|
|
5
|
+
//
|
|
6
|
+
// Two entry points share one page renderer:
|
|
7
|
+
// - renderCanvasToPdf: a Canvas file carrying a `viewport.pdfDocument`
|
|
8
|
+
// filmstrip — every element lives in one stacked doc space and is drawn on
|
|
9
|
+
// each page its bounds intersect.
|
|
10
|
+
// - renderPdfCanvasToPdf: a PdfCanvas file — one page state per page, each in
|
|
11
|
+
// that page's own doc space, with the user's per-page rotation folded into
|
|
12
|
+
// /Rotate and the page matrix.
|
|
13
|
+
import { EncryptedPDFError, PDFDocument, degrees, } from 'pdf-lib';
|
|
6
14
|
import { backgroundLayersOf } from '../../annotation/canvas/backgroundLayers.js';
|
|
15
|
+
import { effectivePageRotation, pdfPageDisplaySize, } from '../../annotation/canvas/pdfCanvasPages.js';
|
|
7
16
|
import { fitRectToScreen } from '../../annotation/canvas/viewport.js';
|
|
8
17
|
import { STAMP_TILE_SIZE } from '../../annotation/canvas/stampLayout.js';
|
|
9
18
|
import { clipRectOps, withMatrix } from './emit.js';
|
|
@@ -52,153 +61,152 @@ const loadSource = async (bytes) => {
|
|
|
52
61
|
throw new PdfExportError('corrupt', `The source PDF could not be parsed: ${e instanceof Error ? e.message : String(e)}`);
|
|
53
62
|
}
|
|
54
63
|
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (!pdfDoc) {
|
|
59
|
-
throw new PdfExportError('no-pdf-document', 'This canvas has no PDF document to export');
|
|
64
|
+
const assertPageCount = (pages, recorded, stored) => {
|
|
65
|
+
if (pages.length !== recorded || stored !== pages.length) {
|
|
66
|
+
throw new PdfExportError('page-count-mismatch', `The canvas records ${stored} pages but the PDF has ${pages.length}`);
|
|
60
67
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
throw new PdfExportError('page-
|
|
68
|
+
};
|
|
69
|
+
const assertPageSize = (index, actual, stored) => {
|
|
70
|
+
if (Math.abs(actual.width - stored.width) > PAGE_SIZE_TOLERANCE_PT ||
|
|
71
|
+
Math.abs(actual.height - stored.height) > PAGE_SIZE_TOLERANCE_PT) {
|
|
72
|
+
throw new PdfExportError('page-geometry-mismatch', `Page ${index + 1} is ${actual.width}×${actual.height} pt in the PDF but ${stored.width}×${stored.height} pt on the canvas`);
|
|
66
73
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
};
|
|
75
|
+
/** Content string for a tile's annotation (what Acrobat's comment shows). */
|
|
76
|
+
const tileContents = (content) => {
|
|
77
|
+
switch (content.kind) {
|
|
78
|
+
case 'measurement':
|
|
79
|
+
return [
|
|
80
|
+
content.label,
|
|
81
|
+
`${content.negative ? '-' : ''}${content.value} ${content.unit}`.trim(),
|
|
82
|
+
]
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.join(': ');
|
|
85
|
+
case 'planeDimension':
|
|
86
|
+
return `${content.value} ${content.unit}`.trim();
|
|
87
|
+
case 'header':
|
|
88
|
+
return (content.descriptor.name ??
|
|
89
|
+
content.descriptor.badges.map((b) => b.text).join(' '));
|
|
90
|
+
case 'object':
|
|
91
|
+
return content.name;
|
|
92
|
+
case 'literal':
|
|
93
|
+
return content.value || undefined;
|
|
94
|
+
case 'instructions':
|
|
95
|
+
return content.text || undefined;
|
|
96
|
+
default:
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const renderJobs = async (doc, jobs, options, skipped) => {
|
|
101
|
+
const fonts = await PdfFonts.create(doc, options.primaryFont);
|
|
82
102
|
// Background layers: embed once, draw on every page they overlap.
|
|
83
|
-
const layers = backgroundLayersOf(canvas.viewport);
|
|
84
103
|
const layerImages = new Map();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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) {
|
|
104
|
+
const seenLayers = new Set();
|
|
105
|
+
for (const job of jobs) {
|
|
106
|
+
for (const layer of job.layers) {
|
|
107
|
+
if (seenLayers.has(layer.id))
|
|
108
|
+
continue;
|
|
109
|
+
seenLayers.add(layer.id);
|
|
110
|
+
if (layer.format === 'svg') {
|
|
111
|
+
skipped.push({
|
|
112
|
+
id: layer.id,
|
|
113
|
+
kind: 'layer',
|
|
114
|
+
reason: 'SVG background layers are not exported yet',
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const bytes = options.layerImages[layer.id];
|
|
119
|
+
if (!bytes) {
|
|
120
|
+
skipped.push({
|
|
121
|
+
id: layer.id,
|
|
122
|
+
kind: 'layer',
|
|
123
|
+
reason: 'Layer image bytes were not provided',
|
|
124
|
+
});
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
124
127
|
try {
|
|
125
|
-
|
|
128
|
+
layerImages.set(layer.id, await embedImage(doc, bytes));
|
|
126
129
|
}
|
|
127
130
|
catch (e) {
|
|
128
131
|
skipped.push({
|
|
129
|
-
id:
|
|
130
|
-
kind: '
|
|
131
|
-
reason: `
|
|
132
|
+
id: layer.id,
|
|
133
|
+
kind: 'layer',
|
|
134
|
+
reason: `Layer image could not be embedded: ${e instanceof Error ? e.message : String(e)}`,
|
|
132
135
|
});
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
138
|
}
|
|
136
|
-
|
|
139
|
+
// Tiles: resolve content once per placed ref; embed object thumbnails once.
|
|
140
|
+
const tileContent = new Map();
|
|
141
|
+
const thumbnails = new Map();
|
|
142
|
+
for (const job of jobs) {
|
|
143
|
+
for (const placed of job.placed) {
|
|
144
|
+
if (tileContent.has(placed.id))
|
|
145
|
+
continue;
|
|
146
|
+
const content = options.resolveTile(placed) ?? {
|
|
147
|
+
kind: 'empty',
|
|
148
|
+
};
|
|
149
|
+
tileContent.set(placed.id, content);
|
|
150
|
+
if (content.kind === 'object' && content.thumbnail) {
|
|
151
|
+
try {
|
|
152
|
+
thumbnails.set(placed.id, await embedImage(doc, content.thumbnail));
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
skipped.push({
|
|
156
|
+
id: placed.id,
|
|
157
|
+
kind: 'tile',
|
|
158
|
+
reason: `Object thumbnail could not be embedded: ${e instanceof Error ? e.message : String(e)}`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Calibration fallback from the first page's rect (the tile-scale reference
|
|
165
|
+
// on screen is the first page too).
|
|
166
|
+
const firstRect = jobs[0]?.rect ?? { x: 0, y: 0, width: 612, height: 792 };
|
|
137
167
|
const fallbackDocEdge = STAMP_TILE_SIZE /
|
|
138
168
|
Math.max(fitRectToScreen(firstRect, FALLBACK_VIEW.width, FALLBACK_VIEW.height)
|
|
139
169
|
.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
170
|
let annotationCount = 0;
|
|
144
|
-
|
|
145
|
-
|
|
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) {
|
|
171
|
+
for (const job of jobs) {
|
|
172
|
+
const { page, box, rotation, rect } = job;
|
|
170
173
|
const matrix = docToPageMatrix(rect, box, rotation);
|
|
171
174
|
const onPage = (b) => b !== null && rectsIntersect(b, rect);
|
|
172
|
-
|
|
175
|
+
const hiddenLayerIds = new Set(job.layerDefs.filter((l) => l.visible === false).map((l) => l.id));
|
|
176
|
+
const visible = (layerId) => !hiddenLayerIds.has(layerId);
|
|
177
|
+
const footprintFor = (placed, content) => tileFootprint(placed, { tileDocSize: job.tileDocSize }, options.calibration, fallbackDocEdge, designBoxFor(placed, content));
|
|
178
|
+
const contentOf = (placed) => tileContent.get(placed.id) ?? { kind: 'empty' };
|
|
179
|
+
if (options.mode === 'flatten') {
|
|
173
180
|
// Everything into the page's content stream, under one matrix + clip.
|
|
174
181
|
const scope = new PageScope(page, doc.context);
|
|
175
182
|
const ctx = { scope, fonts, skipped };
|
|
176
183
|
const ops = [];
|
|
177
|
-
for (const layer of layers) {
|
|
184
|
+
for (const layer of job.layers) {
|
|
178
185
|
const img = layerImages.get(layer.id);
|
|
179
186
|
if (!img || !onPage(layerVisibleRect(layer)))
|
|
180
187
|
continue;
|
|
181
188
|
ops.push(...layerOps(layer, scope.imageKey(img)));
|
|
182
189
|
}
|
|
183
|
-
for (const s of
|
|
190
|
+
for (const s of job.strokes) {
|
|
184
191
|
if (!visible(s.layerId) || !onPage(strokeBounds(s)))
|
|
185
192
|
continue;
|
|
186
193
|
ops.push(...strokeOps(s, ctx));
|
|
187
194
|
}
|
|
188
|
-
for (const sh of
|
|
195
|
+
for (const sh of job.shapes) {
|
|
189
196
|
if (!visible(sh.layerId) || !onPage(shapeBounds(sh)))
|
|
190
197
|
continue;
|
|
191
198
|
ops.push(...shapeOps(sh, ctx));
|
|
192
199
|
}
|
|
193
|
-
for (const
|
|
200
|
+
for (const placed of job.placed) {
|
|
194
201
|
if (!visible(placed.layerId) || !onPage(measurementMarkBounds(placed)))
|
|
195
202
|
continue;
|
|
196
203
|
ops.push(...measurementMarkOps(placed, ctx));
|
|
197
204
|
}
|
|
198
|
-
for (const
|
|
205
|
+
for (const placed of job.placed) {
|
|
199
206
|
if (!visible(placed.layerId))
|
|
200
207
|
continue;
|
|
201
|
-
const
|
|
208
|
+
const content = contentOf(placed);
|
|
209
|
+
const footprint = footprintFor(placed, content);
|
|
202
210
|
if (!onPage(footprint))
|
|
203
211
|
continue;
|
|
204
212
|
ops.push(...tileOps({ scope, fonts, thumbnail: thumbnails.get(placed.id) }, content, footprint));
|
|
@@ -229,7 +237,7 @@ export const renderCanvasToPdf = async (input) => {
|
|
|
229
237
|
annotationCount++;
|
|
230
238
|
return ref;
|
|
231
239
|
};
|
|
232
|
-
for (const layer of layers) {
|
|
240
|
+
for (const layer of job.layers) {
|
|
233
241
|
const img = layerImages.get(layer.id);
|
|
234
242
|
const bounds = layerVisibleRect(layer);
|
|
235
243
|
if (!img || !onPage(bounds))
|
|
@@ -245,13 +253,13 @@ export const renderCanvasToPdf = async (input) => {
|
|
|
245
253
|
},
|
|
246
254
|
}, scope);
|
|
247
255
|
}
|
|
248
|
-
for (const s of
|
|
256
|
+
for (const s of job.strokes) {
|
|
249
257
|
if (!visible(s.layerId) || !onPage(strokeBounds(s)))
|
|
250
258
|
continue;
|
|
251
259
|
const scope = new FormScope(doc.context);
|
|
252
260
|
add(strokeMark(s, { scope, fonts, skipped }), scope);
|
|
253
261
|
}
|
|
254
|
-
for (const sh of
|
|
262
|
+
for (const sh of job.shapes) {
|
|
255
263
|
if (!visible(sh.layerId) || !onPage(shapeBounds(sh)))
|
|
256
264
|
continue;
|
|
257
265
|
const scope = new FormScope(doc.context);
|
|
@@ -260,7 +268,7 @@ export const renderCanvasToPdf = async (input) => {
|
|
|
260
268
|
// A measurement's line/rect and its tile are grouped (Acrobat honours
|
|
261
269
|
// /IRT + /RT /Group; other viewers treat them independently).
|
|
262
270
|
const markRefs = new Map();
|
|
263
|
-
for (const
|
|
271
|
+
for (const placed of job.placed) {
|
|
264
272
|
if (!visible(placed.layerId) || !onPage(measurementMarkBounds(placed)))
|
|
265
273
|
continue;
|
|
266
274
|
const scope = new FormScope(doc.context);
|
|
@@ -268,10 +276,11 @@ export const renderCanvasToPdf = async (input) => {
|
|
|
268
276
|
if (ref)
|
|
269
277
|
markRefs.set(placed.id, ref);
|
|
270
278
|
}
|
|
271
|
-
for (const
|
|
279
|
+
for (const placed of job.placed) {
|
|
272
280
|
if (!visible(placed.layerId))
|
|
273
281
|
continue;
|
|
274
|
-
const
|
|
282
|
+
const content = contentOf(placed);
|
|
283
|
+
const footprint = footprintFor(placed, content);
|
|
275
284
|
if (!onPage(footprint))
|
|
276
285
|
continue;
|
|
277
286
|
const scope = new FormScope(doc.context);
|
|
@@ -283,6 +292,93 @@ export const renderCanvasToPdf = async (input) => {
|
|
|
283
292
|
}, scope, markRefs.get(placed.id));
|
|
284
293
|
}
|
|
285
294
|
}
|
|
295
|
+
return annotationCount;
|
|
296
|
+
};
|
|
297
|
+
export const renderCanvasToPdf = async (input) => {
|
|
298
|
+
const { canvas } = input;
|
|
299
|
+
const pdfDoc = canvas.viewport.pdfDocument;
|
|
300
|
+
if (!pdfDoc) {
|
|
301
|
+
throw new PdfExportError('no-pdf-document', 'This canvas has no PDF document to export');
|
|
302
|
+
}
|
|
303
|
+
const doc = await loadSource(input.sourcePdf);
|
|
304
|
+
const pages = doc.getPages();
|
|
305
|
+
assertPageCount(pages, pdfDoc.pageCount, pdfDoc.pages.length);
|
|
306
|
+
const pageRects = pageRectsOf(pdfDoc);
|
|
307
|
+
const layers = backgroundLayersOf(canvas.viewport);
|
|
308
|
+
// Per-page geometry, validated against what the canvas was laid out from.
|
|
309
|
+
const jobs = pages.map((page, i) => {
|
|
310
|
+
const box = pageBox(page);
|
|
311
|
+
const rotation = normalizeRotation(page.getRotation().angle);
|
|
312
|
+
assertPageSize(i, rotatedBoxSize(box, rotation), pdfDoc.pages[i]);
|
|
313
|
+
return {
|
|
314
|
+
page,
|
|
315
|
+
box,
|
|
316
|
+
rotation,
|
|
317
|
+
rect: pageRects[i],
|
|
318
|
+
layers,
|
|
319
|
+
layerDefs: canvas.layers,
|
|
320
|
+
strokes: canvas.strokes,
|
|
321
|
+
shapes: canvas.shapes,
|
|
322
|
+
placed: canvas.placedMeasurements,
|
|
323
|
+
tileDocSize: canvas.tileDocSize,
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
const skipped = [];
|
|
327
|
+
const annotationCount = await renderJobs(doc, jobs, {
|
|
328
|
+
resolveTile: input.resolveTile,
|
|
329
|
+
layerImages: input.layerImages,
|
|
330
|
+
calibration: input.calibration,
|
|
331
|
+
primaryFont: input.primaryFont,
|
|
332
|
+
mode: input.mode ?? 'annotations',
|
|
333
|
+
}, skipped);
|
|
334
|
+
const pdf = await doc.save({ useObjectStreams: false });
|
|
335
|
+
return { pdf, pageCount: pages.length, annotationCount, skipped };
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* Export a PdfCanvas file. Each page's state is drawn in that page's own doc
|
|
339
|
+
* space (the DISPLAYED page: rotated extents, origin top-left). A user
|
|
340
|
+
* rotation is written into the page's /Rotate — so the recipient sees the
|
|
341
|
+
* page as the user did — and the page matrix undoes the combined rotation.
|
|
342
|
+
* Pages without a state are still rotated.
|
|
343
|
+
*/
|
|
344
|
+
export const renderPdfCanvasToPdf = async (input) => {
|
|
345
|
+
const { pdfCanvas } = input;
|
|
346
|
+
const doc = await loadSource(input.sourcePdf);
|
|
347
|
+
const pages = doc.getPages();
|
|
348
|
+
assertPageCount(pages, pdfCanvas.document.pageCount, pdfCanvas.document.pages.length);
|
|
349
|
+
const jobs = [];
|
|
350
|
+
pages.forEach((page, i) => {
|
|
351
|
+
const stored = pdfCanvas.document.pages[i];
|
|
352
|
+
const box = pageBox(page);
|
|
353
|
+
const intrinsic = normalizeRotation(page.getRotation().angle);
|
|
354
|
+
// The stored size is the page after its own /Rotate, before the user's.
|
|
355
|
+
assertPageSize(i, rotatedBoxSize(box, intrinsic), stored);
|
|
356
|
+
const effective = effectivePageRotation(intrinsic, stored.rotation);
|
|
357
|
+
if (effective !== intrinsic)
|
|
358
|
+
page.setRotation(degrees(effective));
|
|
359
|
+
const display = pdfPageDisplaySize(stored);
|
|
360
|
+
const state = input.pageStates[i] ?? undefined;
|
|
361
|
+
jobs.push({
|
|
362
|
+
page,
|
|
363
|
+
box,
|
|
364
|
+
rotation: effective,
|
|
365
|
+
rect: { x: 0, y: 0, width: display.width, height: display.height },
|
|
366
|
+
layers: [],
|
|
367
|
+
layerDefs: state?.layers ?? [],
|
|
368
|
+
strokes: state?.strokes ?? [],
|
|
369
|
+
shapes: state?.shapes ?? [],
|
|
370
|
+
placed: state?.placedMeasurements ?? [],
|
|
371
|
+
tileDocSize: pdfCanvas.tileDocSize,
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
const skipped = [];
|
|
375
|
+
const annotationCount = await renderJobs(doc, jobs, {
|
|
376
|
+
resolveTile: input.resolveTile,
|
|
377
|
+
layerImages: {},
|
|
378
|
+
calibration: input.calibration,
|
|
379
|
+
primaryFont: input.primaryFont,
|
|
380
|
+
mode: input.mode ?? 'annotations',
|
|
381
|
+
}, skipped);
|
|
286
382
|
const pdf = await doc.save({ useObjectStreams: false });
|
|
287
383
|
return { pdf, pageCount: pages.length, annotationCount, skipped };
|
|
288
384
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GroupHeaderDescriptor } from '../../annotation/canvas/groupHeaderLayout.js';
|
|
2
2
|
import type { AnnotationCanvasState, PlacedMeasurementRef } from '../../types/annotation.js';
|
|
3
|
+
import type { PdfCanvasData, PdfCanvasPageState } from '../../types/pdfCanvas.js';
|
|
3
4
|
export interface EmbeddedImage {
|
|
4
5
|
bytes: Uint8Array;
|
|
5
6
|
mime: 'image/png' | 'image/jpeg';
|
|
@@ -97,6 +98,18 @@ export interface RenderCanvasToPdfInput {
|
|
|
97
98
|
*/
|
|
98
99
|
mode?: 'annotations' | 'flatten';
|
|
99
100
|
}
|
|
101
|
+
export interface RenderPdfCanvasToPdfInput {
|
|
102
|
+
/** The PDF the file was uploaded from (`pdfCanvas.document.storagePath`). */
|
|
103
|
+
sourcePdf: Uint8Array;
|
|
104
|
+
/** The file-level record (`fileData.pdfCanvas`). */
|
|
105
|
+
pdfCanvas: PdfCanvasData;
|
|
106
|
+
/** Page docs by page index; a page without one is exported un-annotated. */
|
|
107
|
+
pageStates: Partial<Record<number, PdfCanvasPageState>>;
|
|
108
|
+
resolveTile: (ref: PlacedMeasurementRef) => ResolvedTileContent | undefined;
|
|
109
|
+
calibration?: PdfExportCalibration;
|
|
110
|
+
primaryFont: Uint8Array;
|
|
111
|
+
mode?: 'annotations' | 'flatten';
|
|
112
|
+
}
|
|
100
113
|
export interface RenderCanvasToPdfResult {
|
|
101
114
|
pdf: Uint8Array;
|
|
102
115
|
pageCount: number;
|
package/dist/exports.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { useParseMeasurement } from './utils/useParseMeasurement.js';
|
|
|
6
6
|
export * from './types/firestore.js';
|
|
7
7
|
export * from './types/layout.js';
|
|
8
8
|
export * from './types/annotation.js';
|
|
9
|
+
export * from './types/pdfCanvas.js';
|
|
9
10
|
export { getToleranceColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, type ToleranceThreshold, type ToleranceConfig, } from './utils/tolerance.js';
|
|
10
11
|
export { GeneralColors, SecondaryColors, FormColors, TileColors, FormulaColors, YellowColors, RedColors, GreenColors, tailwindColors, } from './theme/colors.js';
|
|
11
12
|
export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, isFormGroupCompleted, } from './utils/groups.js';
|
|
@@ -25,6 +26,7 @@ export { useAnnotationDoc, type UseAnnotationDocResult, } from './annotation/dat
|
|
|
25
26
|
export { useAnnotationList, type UseAnnotationListResult, } from './annotation/data/hooks/useAnnotationList.js';
|
|
26
27
|
export { useAnnotationMutations, type AnnotationMutations, } from './annotation/data/hooks/useAnnotationMutations.js';
|
|
27
28
|
export { useAnnotationCanvasDoc, type SaveStatus, type UseAnnotationCanvasDocOptions, type UseAnnotationCanvasDocResult, } from './annotation/data/hooks/useAnnotationCanvasDoc.js';
|
|
29
|
+
export { usePdfCanvasDoc, type UsePdfCanvasDocOptions, type UsePdfCanvasDocResult, } from './annotation/data/hooks/usePdfCanvasDoc.js';
|
|
28
30
|
export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
|
|
29
31
|
export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
|
|
30
32
|
export type { AnnotationCanvasHandle } from './annotation/canvas/useAnnotationCanvasState.js';
|
|
@@ -35,8 +37,9 @@ export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './anno
|
|
|
35
37
|
export type { CanvasBackgroundSlotArgs, RenderCanvasBackground, } from './annotation/canvas/backgroundSlot.js';
|
|
36
38
|
export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, isFileStamp, type StampTileDims, } from './annotation/canvas/stampLayout.js';
|
|
37
39
|
export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, type GroupHeaderBadge, type GroupHeaderDescriptor, } from './annotation/canvas/groupHeaderLayout.js';
|
|
38
|
-
export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, type ViewportApi, type ViewportState, type ZoomLimits, } from './annotation/canvas/viewport.js';
|
|
40
|
+
export { createViewportApi, fitToScreen, fitRectToScreen, fitDocRectViewport, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, type ViewportApi, type ViewportState, type ZoomLimits, } from './annotation/canvas/viewport.js';
|
|
39
41
|
export { DEFAULT_PDF_PAGE_GAP, DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM, makePdfDocumentId, createPdfDocument, pdfPageDocRects, pdfDocumentExtent, pdfFirstPageDocRect, computeZoomFloor, type PdfDocRect, } from './annotation/canvas/pdfDocument.js';
|
|
42
|
+
export { normalizePdfRotation, effectivePageRotation, pdfPageDisplaySize, pdfCanvasPageDocId, emptyPdfCanvasPageState, pdfCanvasPageDocument, pdfCanvasKnobsOf, pdfCanvasKnobsEqual, pageStateToCanvasState, canvasStateToPageState, rotatePointInPage, rotatePageState, rotatePdfCanvasPage, type PdfPageDisplaySize, type PdfRotationDelta, } from './annotation/canvas/pdfCanvasPages.js';
|
|
40
43
|
export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerIsLocked, isBackgroundLayerLocked, defaultBackgroundLayerLocked, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
|
|
41
44
|
export { fileTileImageSource, expandFileTileToBackground, collapseBackgroundToFileTile, type FileTileImageSource, } from './annotation/canvas/fileTileSwap.js';
|
|
42
45
|
export { createPenTool, type PenToolOptions, } from './annotation/canvas/tools/penTool.js';
|
package/dist/exports.js
CHANGED
|
@@ -10,6 +10,7 @@ export { useParseMeasurement } from './utils/useParseMeasurement.js';
|
|
|
10
10
|
export * from './types/firestore.js';
|
|
11
11
|
export * from './types/layout.js';
|
|
12
12
|
export * from './types/annotation.js';
|
|
13
|
+
export * from './types/pdfCanvas.js';
|
|
13
14
|
export { getToleranceColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, } from './utils/tolerance.js';
|
|
14
15
|
// Shared color palette (canonical source for both apps' brand colors).
|
|
15
16
|
export { GeneralColors, SecondaryColors, FormColors, TileColors, FormulaColors, YellowColors, RedColors, GreenColors, tailwindColors, } from './theme/colors.js';
|
|
@@ -37,12 +38,14 @@ export { useAnnotationDoc, } from './annotation/data/hooks/useAnnotationDoc.js';
|
|
|
37
38
|
export { useAnnotationList, } from './annotation/data/hooks/useAnnotationList.js';
|
|
38
39
|
export { useAnnotationMutations, } from './annotation/data/hooks/useAnnotationMutations.js';
|
|
39
40
|
export { useAnnotationCanvasDoc, } from './annotation/data/hooks/useAnnotationCanvasDoc.js';
|
|
41
|
+
export { usePdfCanvasDoc, } from './annotation/data/hooks/usePdfCanvasDoc.js';
|
|
40
42
|
export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
|
|
41
43
|
export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
|
|
42
44
|
export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, isFileStamp, } from './annotation/canvas/stampLayout.js';
|
|
43
45
|
export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, } from './annotation/canvas/groupHeaderLayout.js';
|
|
44
|
-
export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, } from './annotation/canvas/viewport.js';
|
|
46
|
+
export { createViewportApi, fitToScreen, fitRectToScreen, fitDocRectViewport, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, } from './annotation/canvas/viewport.js';
|
|
45
47
|
export { DEFAULT_PDF_PAGE_GAP, DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM, makePdfDocumentId, createPdfDocument, pdfPageDocRects, pdfDocumentExtent, pdfFirstPageDocRect, computeZoomFloor, } from './annotation/canvas/pdfDocument.js';
|
|
48
|
+
export { normalizePdfRotation, effectivePageRotation, pdfPageDisplaySize, pdfCanvasPageDocId, emptyPdfCanvasPageState, pdfCanvasPageDocument, pdfCanvasKnobsOf, pdfCanvasKnobsEqual, pageStateToCanvasState, canvasStateToPageState, rotatePointInPage, rotatePageState, rotatePdfCanvasPage, } from './annotation/canvas/pdfCanvasPages.js';
|
|
46
49
|
export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerIsLocked, isBackgroundLayerLocked, defaultBackgroundLayerLocked, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
|
|
47
50
|
export { fileTileImageSource, expandFileTileToBackground, collapseBackgroundToFileTile, } from './annotation/canvas/fileTileSwap.js';
|
|
48
51
|
export { createPenTool, } from './annotation/canvas/tools/penTool.js';
|
package/dist/server.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { parseMeasurement } from './utils/parseMeasurement.js';
|
|
|
5
5
|
export * from './types/firestore.js';
|
|
6
6
|
export * from './types/layout.js';
|
|
7
7
|
export * from './types/annotation.js';
|
|
8
|
+
export * from './types/pdfCanvas.js';
|
|
9
|
+
export { normalizePdfRotation, effectivePageRotation, pdfPageDisplaySize, pdfCanvasPageDocId, pageStateToCanvasState, rotatePageState, type PdfPageDisplaySize, type PdfRotationDelta, } from './annotation/canvas/pdfCanvasPages.js';
|
|
8
10
|
export { getToleranceColor, getToleranceSecondaryColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, DEFAULT_TOLERANCE_SECONDARY_COLORS, type ToleranceThreshold, type ToleranceConfig, } from './utils/tolerance.js';
|
|
9
11
|
export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, isFormGroupCompleted, } from './utils/groups.js';
|
|
10
12
|
export { ROLE_RANK, EMPTY_GRANTS, normalizeRole, roleAtLeast, effectiveRole, canView, canEdit, canManage, canManageOrg, isGuest, groupGrants, resolveAccess, type MaybeRole, type GroupedGrants, type ResolvedAccess, } from './utils/permissions.js';
|
package/dist/server.js
CHANGED
|
@@ -10,6 +10,8 @@ export { parseMeasurement } from './utils/parseMeasurement.js';
|
|
|
10
10
|
export * from './types/firestore.js';
|
|
11
11
|
export * from './types/layout.js';
|
|
12
12
|
export * from './types/annotation.js';
|
|
13
|
+
export * from './types/pdfCanvas.js';
|
|
14
|
+
export { normalizePdfRotation, effectivePageRotation, pdfPageDisplaySize, pdfCanvasPageDocId, pageStateToCanvasState, rotatePageState, } from './annotation/canvas/pdfCanvasPages.js';
|
|
13
15
|
export { getToleranceColor, getToleranceSecondaryColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, DEFAULT_TOLERANCE_SECONDARY_COLORS, } from './utils/tolerance.js';
|
|
14
16
|
export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, isFormGroupCompleted, } from './utils/groups.js';
|
|
15
17
|
export { ROLE_RANK, EMPTY_GRANTS, normalizeRole, roleAtLeast, effectiveRole, canView, canEdit, canManage, canManageOrg, isGuest, groupGrants, resolveAccess, } from './utils/permissions.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AnnotationCanvasState } from './annotation.js';
|
|
2
|
+
import type { PdfCanvasData } from './pdfCanvas.js';
|
|
2
3
|
import type { CalculatorUnit } from '../calculator/units.js';
|
|
3
4
|
interface FirestoreDoc {
|
|
4
5
|
id: string;
|
|
@@ -170,6 +171,7 @@ export interface SelectedTemplate extends FirestoreDoc {
|
|
|
170
171
|
export declare enum FileUploadType {
|
|
171
172
|
Annotation = "annotation",
|
|
172
173
|
Canvas = "canvas",
|
|
174
|
+
PdfCanvas = "pdfCanvas",
|
|
173
175
|
Template = "template",
|
|
174
176
|
Background = "background",
|
|
175
177
|
LayoutGroup = "layoutGroup",
|
|
@@ -186,6 +188,7 @@ export interface AnnotationFileData {
|
|
|
186
188
|
clientId: string;
|
|
187
189
|
seq: number;
|
|
188
190
|
};
|
|
191
|
+
pdfCanvas?: PdfCanvasData;
|
|
189
192
|
}
|
|
190
193
|
export interface CalculatorFileData {
|
|
191
194
|
templateId: string;
|
|
@@ -242,6 +245,9 @@ export type FileUpload = FileUploadBase & ({
|
|
|
242
245
|
} | {
|
|
243
246
|
type: FileUploadType.Canvas;
|
|
244
247
|
fileData: AnnotationFileData;
|
|
248
|
+
} | {
|
|
249
|
+
type: FileUploadType.PdfCanvas;
|
|
250
|
+
fileData: AnnotationFileData;
|
|
245
251
|
} | {
|
|
246
252
|
type: FileUploadType.Calculator;
|
|
247
253
|
fileData: CalculatorFileData;
|
package/dist/types/firestore.js
CHANGED
|
@@ -52,6 +52,11 @@ export var FileUploadType;
|
|
|
52
52
|
// v2 Skia photo-annotation canvas. Distinct from the legacy PSPDFKit
|
|
53
53
|
// `Annotation` docs so file-open routing sends it to the canvas editor.
|
|
54
54
|
FileUploadType["Canvas"] = "canvas";
|
|
55
|
+
// A PDF annotated one page at a time with the Skia canvas (see
|
|
56
|
+
// types/pdfCanvas.ts). Its PDF sits at `${folderPath}/file` like a legacy
|
|
57
|
+
// `Annotation` upload, so a client that predates the type still opens it in
|
|
58
|
+
// the Nutrient viewer instead of failing.
|
|
59
|
+
FileUploadType["PdfCanvas"] = "pdfCanvas";
|
|
55
60
|
FileUploadType["Template"] = "template";
|
|
56
61
|
FileUploadType["Background"] = "background";
|
|
57
62
|
FileUploadType["LayoutGroup"] = "layoutGroup";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AnnotationLayer, AnnotationShape, AnnotationStroke, PlacedMeasurementRef, PlaneCalibration } from './annotation.js';
|
|
2
|
+
export type PdfPageRotation = 0 | 90 | 180 | 270;
|
|
3
|
+
export interface PdfCanvasPage {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
rotation: PdfPageRotation;
|
|
7
|
+
}
|
|
8
|
+
export interface PdfCanvasDocument {
|
|
9
|
+
storagePath: string;
|
|
10
|
+
fileName?: string;
|
|
11
|
+
pageCount: number;
|
|
12
|
+
pages: PdfCanvasPage[];
|
|
13
|
+
}
|
|
14
|
+
export interface PdfCanvasRev {
|
|
15
|
+
clientId: string;
|
|
16
|
+
seq: number;
|
|
17
|
+
}
|
|
18
|
+
export interface PdfCanvasData {
|
|
19
|
+
schemaVersion: 1;
|
|
20
|
+
document: PdfCanvasDocument;
|
|
21
|
+
tileScaleFactor?: number;
|
|
22
|
+
tileScaleFactorMobile?: number;
|
|
23
|
+
headerTileScaleFactor?: number;
|
|
24
|
+
headerTileScaleFactorMobile?: number;
|
|
25
|
+
tileDocSize?: number;
|
|
26
|
+
annotatedPages: number[];
|
|
27
|
+
}
|
|
28
|
+
export interface PdfCanvasPageState {
|
|
29
|
+
schemaVersion: 1;
|
|
30
|
+
pageIndex: number;
|
|
31
|
+
layers: AnnotationLayer[];
|
|
32
|
+
strokes: AnnotationStroke[];
|
|
33
|
+
shapes: AnnotationShape[];
|
|
34
|
+
placedMeasurements: PlacedMeasurementRef[];
|
|
35
|
+
planes?: PlaneCalibration[];
|
|
36
|
+
updatedAt: number;
|
|
37
|
+
rev?: PdfCanvasRev;
|
|
38
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Schema of a PDF CANVAS file (FileUploadType.PdfCanvas): a PDF annotated one
|
|
2
|
+
// page at a time with the Skia canvas. Distinct from a Canvas file carrying a
|
|
3
|
+
// `viewport.pdfDocument` filmstrip — there every page shares one doc space;
|
|
4
|
+
// here EVERY PAGE IS ITS OWN CANVAS whose doc space is that page's displayed
|
|
5
|
+
// extent in PDF points (origin top-left, y down), so the canvas engine needs no
|
|
6
|
+
// page concept at all: the editor mounts the current page's state and swaps it
|
|
7
|
+
// on a page turn.
|
|
8
|
+
//
|
|
9
|
+
// Storage split (see pdfCanvasPages.ts for the mapping helpers):
|
|
10
|
+
// - `fileData.pdfCanvas` on the file doc: the small file-level record —
|
|
11
|
+
// which PDF, page geometry + user rotations, shared tile-scale knobs, and
|
|
12
|
+
// which pages have been annotated.
|
|
13
|
+
// - `files/{fileId}/pages/{pageIndex}`: one doc per annotated page holding
|
|
14
|
+
// that page's elements. Sharded so a heavy set never approaches the 1 MiB
|
|
15
|
+
// doc limit, a save rewrites one page, and a listener watches one page.
|
|
16
|
+
export {};
|