@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.
@@ -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
- import { EncryptedPDFError, PDFDocument, } from 'pdf-lib';
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
- 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');
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
- 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}`);
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
- 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);
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
- 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) {
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
- thumbnails.set(placed.id, await embedImage(doc, content.thumbnail));
128
+ layerImages.set(layer.id, await embedImage(doc, bytes));
126
129
  }
127
130
  catch (e) {
128
131
  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
+ 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
- const firstRect = pageRects[0];
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
- /** 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) {
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
- if (mode === 'flatten') {
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 canvas.strokes) {
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 canvas.shapes) {
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 { placed } of tiles) {
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 { placed, content } of tiles) {
205
+ for (const placed of job.placed) {
199
206
  if (!visible(placed.layerId))
200
207
  continue;
201
- const footprint = tileFootprint(placed, canvas, input.calibration, fallbackDocEdge, designBoxFor(placed, content));
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 canvas.strokes) {
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 canvas.shapes) {
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 { placed } of tiles) {
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 { placed, content } of tiles) {
279
+ for (const placed of job.placed) {
272
280
  if (!visible(placed.layerId))
273
281
  continue;
274
- const footprint = tileFootprint(placed, canvas, input.calibration, fallbackDocEdge, designBoxFor(placed, content));
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;
@@ -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 {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.23.0",
3
+ "version": "1.25.0",
4
4
  "description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
5
5
  "author": "REEKON Tools",
6
6
  "license": "MIT",