@reekon-tools/boldr-utils 1.22.0 → 1.24.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/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 +344 -0
- package/dist/export/pdf/annots.d.ts +23 -0
- package/dist/export/pdf/annots.js +135 -0
- package/dist/export/pdf/decrypt.d.ts +21 -0
- package/dist/export/pdf/decrypt.js +121 -0
- package/dist/export/pdf/fonts.d.ts +3 -4
- package/dist/export/pdf/fonts.js +2 -14
- package/dist/export/pdf/index.d.ts +4 -2
- package/dist/export/pdf/index.js +3 -1
- package/dist/export/pdf/marks.d.ts +66 -5
- package/dist/export/pdf/marks.js +150 -7
- package/dist/export/pdf/pageMapping.d.ts +2 -0
- package/dist/export/pdf/pageMapping.js +10 -0
- package/dist/export/pdf/renderCanvasToPdf.d.ts +9 -1
- package/dist/export/pdf/renderCanvasToPdf.js +303 -120
- package/dist/export/pdf/resources.d.ts +35 -0
- package/dist/export/pdf/resources.js +95 -0
- package/dist/export/pdf/tiles.d.ts +5 -9
- package/dist/export/pdf/tiles.js +20 -20
- package/dist/export/pdf/types.d.ts +25 -1
- package/dist/exports.d.ts +3 -0
- package/dist/exports.js +3 -0
- 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 +6 -1
|
@@ -2,16 +2,27 @@
|
|
|
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';
|
|
10
19
|
import { PdfFonts } from './fonts.js';
|
|
11
20
|
import { layerOps, layerVisibleRect } from './layers.js';
|
|
12
|
-
import {
|
|
21
|
+
import { addMarkAnnotation } from './annots.js';
|
|
22
|
+
import { measurementMark, measurementMarkBounds, measurementMarkOps, shapeBounds, shapeMark, shapeOps, strokeBounds, strokeMark, strokeOps, } from './marks.js';
|
|
23
|
+
import { FormScope, PageScope } from './resources.js';
|
|
13
24
|
import { docToPageMatrix, normalizeRotation, pageRectsOf, rectsIntersect, rotatedBoxSize, } from './pageMapping.js';
|
|
14
|
-
import { designBoxFor, tileFootprint, tileOps
|
|
25
|
+
import { designBoxFor, tileFootprint, tileOps } from './tiles.js';
|
|
15
26
|
import { PdfExportError, } from './types.js';
|
|
16
27
|
/** Stored page sizes may differ from the file's boxes by rounding only. */
|
|
17
28
|
const PAGE_SIZE_TOLERANCE_PT = 1.5;
|
|
@@ -41,161 +52,333 @@ const loadSource = async (bytes) => {
|
|
|
41
52
|
return await PDFDocument.load(bytes, { updateMetadata: false });
|
|
42
53
|
}
|
|
43
54
|
catch (e) {
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
// Both checks: pdf-lib's dual CJS/ESM build can hand back an
|
|
56
|
+
// EncryptedPDFError whose class identity differs from the one imported.
|
|
57
|
+
if (e instanceof EncryptedPDFError ||
|
|
58
|
+
(e instanceof Error && /is encrypted/i.test(e.message))) {
|
|
59
|
+
throw new PdfExportError('encrypted', 'The source PDF is encrypted; strip it with stripPdfEncryption before rendering');
|
|
46
60
|
}
|
|
47
61
|
throw new PdfExportError('corrupt', `The source PDF could not be parsed: ${e instanceof Error ? e.message : String(e)}`);
|
|
48
62
|
}
|
|
49
63
|
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!pdfDoc) {
|
|
54
|
-
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}`);
|
|
55
67
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
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`);
|
|
61
73
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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);
|
|
77
102
|
// Background layers: embed once, draw on every page they overlap.
|
|
78
|
-
const layers = backgroundLayersOf(canvas.viewport);
|
|
79
103
|
const layerImages = new Map();
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
id: layer.id,
|
|
104
|
-
kind: 'layer',
|
|
105
|
-
reason: `Layer image could not be embedded: ${e instanceof Error ? e.message : String(e)}`,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// Tiles: resolve content once; embed object thumbnails once.
|
|
110
|
-
const tiles = canvas.placedMeasurements.map((placed) => {
|
|
111
|
-
const content = input.resolveTile(placed) ?? {
|
|
112
|
-
kind: 'empty',
|
|
113
|
-
};
|
|
114
|
-
return { placed, content };
|
|
115
|
-
});
|
|
116
|
-
const thumbnails = new Map();
|
|
117
|
-
for (const { placed, content } of tiles) {
|
|
118
|
-
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
|
+
}
|
|
119
127
|
try {
|
|
120
|
-
|
|
128
|
+
layerImages.set(layer.id, await embedImage(doc, bytes));
|
|
121
129
|
}
|
|
122
130
|
catch (e) {
|
|
123
131
|
skipped.push({
|
|
124
|
-
id:
|
|
125
|
-
kind: '
|
|
126
|
-
reason: `
|
|
132
|
+
id: layer.id,
|
|
133
|
+
kind: 'layer',
|
|
134
|
+
reason: `Layer image could not be embedded: ${e instanceof Error ? e.message : String(e)}`,
|
|
127
135
|
});
|
|
128
136
|
}
|
|
129
137
|
}
|
|
130
138
|
}
|
|
131
|
-
|
|
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 };
|
|
132
167
|
const fallbackDocEdge = STAMP_TILE_SIZE /
|
|
133
168
|
Math.max(fitRectToScreen(firstRect, FALLBACK_VIEW.width, FALLBACK_VIEW.height)
|
|
134
169
|
.zoom, 1e-6);
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
170
|
+
let annotationCount = 0;
|
|
171
|
+
for (const job of jobs) {
|
|
172
|
+
const { page, box, rotation, rect } = job;
|
|
173
|
+
const matrix = docToPageMatrix(rect, box, rotation);
|
|
174
|
+
const onPage = (b) => b !== null && rectsIntersect(b, rect);
|
|
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') {
|
|
180
|
+
// Everything into the page's content stream, under one matrix + clip.
|
|
181
|
+
const scope = new PageScope(page, doc.context);
|
|
182
|
+
const ctx = { scope, fonts, skipped };
|
|
183
|
+
const ops = [];
|
|
184
|
+
for (const layer of job.layers) {
|
|
185
|
+
const img = layerImages.get(layer.id);
|
|
186
|
+
if (!img || !onPage(layerVisibleRect(layer)))
|
|
187
|
+
continue;
|
|
188
|
+
ops.push(...layerOps(layer, scope.imageKey(img)));
|
|
189
|
+
}
|
|
190
|
+
for (const s of job.strokes) {
|
|
191
|
+
if (!visible(s.layerId) || !onPage(strokeBounds(s)))
|
|
192
|
+
continue;
|
|
193
|
+
ops.push(...strokeOps(s, ctx));
|
|
148
194
|
}
|
|
149
|
-
|
|
195
|
+
for (const sh of job.shapes) {
|
|
196
|
+
if (!visible(sh.layerId) || !onPage(shapeBounds(sh)))
|
|
197
|
+
continue;
|
|
198
|
+
ops.push(...shapeOps(sh, ctx));
|
|
199
|
+
}
|
|
200
|
+
for (const placed of job.placed) {
|
|
201
|
+
if (!visible(placed.layerId) || !onPage(measurementMarkBounds(placed)))
|
|
202
|
+
continue;
|
|
203
|
+
ops.push(...measurementMarkOps(placed, ctx));
|
|
204
|
+
}
|
|
205
|
+
for (const placed of job.placed) {
|
|
206
|
+
if (!visible(placed.layerId))
|
|
207
|
+
continue;
|
|
208
|
+
const content = contentOf(placed);
|
|
209
|
+
const footprint = footprintFor(placed, content);
|
|
210
|
+
if (!onPage(footprint))
|
|
211
|
+
continue;
|
|
212
|
+
ops.push(...tileOps({ scope, fonts, thumbnail: thumbnails.get(placed.id) }, content, footprint));
|
|
213
|
+
}
|
|
214
|
+
if (ops.length === 0)
|
|
215
|
+
continue;
|
|
216
|
+
page.pushOperators(...withMatrix(matrix, [
|
|
217
|
+
...clipRectOps(rect.x, rect.y, rect.width, rect.height),
|
|
218
|
+
...ops,
|
|
219
|
+
]));
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// Annotation mode: one annotation per element, each with its own
|
|
223
|
+
// appearance stream and resources. Same z-order as the screen.
|
|
224
|
+
const add = (mark, scope, groupWith) => {
|
|
225
|
+
if (!mark)
|
|
226
|
+
return null;
|
|
227
|
+
const ref = addMarkAnnotation({
|
|
228
|
+
doc,
|
|
229
|
+
page,
|
|
230
|
+
mark,
|
|
231
|
+
pageRect: rect,
|
|
232
|
+
matrix,
|
|
233
|
+
scope,
|
|
234
|
+
groupWith,
|
|
235
|
+
});
|
|
236
|
+
if (ref)
|
|
237
|
+
annotationCount++;
|
|
238
|
+
return ref;
|
|
150
239
|
};
|
|
151
|
-
const
|
|
152
|
-
name: page.node.newXObject('Im', img.ref),
|
|
153
|
-
width: img.width,
|
|
154
|
-
height: img.height,
|
|
155
|
-
});
|
|
156
|
-
const ctx = { page, fonts, alphaState, skipped };
|
|
157
|
-
const onPage = (b) => b !== null && rectsIntersect(b, rect);
|
|
158
|
-
const ops = [];
|
|
159
|
-
// 1. Background image layers, array order.
|
|
160
|
-
for (const layer of layers) {
|
|
240
|
+
for (const layer of job.layers) {
|
|
161
241
|
const img = layerImages.get(layer.id);
|
|
162
|
-
|
|
242
|
+
const bounds = layerVisibleRect(layer);
|
|
243
|
+
if (!img || !onPage(bounds))
|
|
163
244
|
continue;
|
|
164
|
-
|
|
245
|
+
const scope = new FormScope(doc.context);
|
|
246
|
+
add({
|
|
247
|
+
id: layer.id,
|
|
248
|
+
ops: layerOps(layer, scope.imageKey(img)),
|
|
249
|
+
bounds,
|
|
250
|
+
annot: {
|
|
251
|
+
subtype: 'Stamp',
|
|
252
|
+
contents: layer.fileId ? 'Placed file' : 'Background image',
|
|
253
|
+
},
|
|
254
|
+
}, scope);
|
|
165
255
|
}
|
|
166
|
-
|
|
167
|
-
for (const s of canvas.strokes) {
|
|
256
|
+
for (const s of job.strokes) {
|
|
168
257
|
if (!visible(s.layerId) || !onPage(strokeBounds(s)))
|
|
169
258
|
continue;
|
|
170
|
-
|
|
259
|
+
const scope = new FormScope(doc.context);
|
|
260
|
+
add(strokeMark(s, { scope, fonts, skipped }), scope);
|
|
171
261
|
}
|
|
172
|
-
for (const sh of
|
|
262
|
+
for (const sh of job.shapes) {
|
|
173
263
|
if (!visible(sh.layerId) || !onPage(shapeBounds(sh)))
|
|
174
264
|
continue;
|
|
175
|
-
|
|
265
|
+
const scope = new FormScope(doc.context);
|
|
266
|
+
add(shapeMark(sh, { scope, fonts, skipped }), scope);
|
|
176
267
|
}
|
|
177
|
-
|
|
268
|
+
// A measurement's line/rect and its tile are grouped (Acrobat honours
|
|
269
|
+
// /IRT + /RT /Group; other viewers treat them independently).
|
|
270
|
+
const markRefs = new Map();
|
|
271
|
+
for (const placed of job.placed) {
|
|
178
272
|
if (!visible(placed.layerId) || !onPage(measurementMarkBounds(placed)))
|
|
179
273
|
continue;
|
|
180
|
-
|
|
274
|
+
const scope = new FormScope(doc.context);
|
|
275
|
+
const ref = add(measurementMark(placed, { scope, fonts, skipped }), scope);
|
|
276
|
+
if (ref)
|
|
277
|
+
markRefs.set(placed.id, ref);
|
|
181
278
|
}
|
|
182
|
-
|
|
183
|
-
for (const { placed, content } of tiles) {
|
|
279
|
+
for (const placed of job.placed) {
|
|
184
280
|
if (!visible(placed.layerId))
|
|
185
281
|
continue;
|
|
186
|
-
const
|
|
282
|
+
const content = contentOf(placed);
|
|
283
|
+
const footprint = footprintFor(placed, content);
|
|
187
284
|
if (!onPage(footprint))
|
|
188
285
|
continue;
|
|
189
|
-
const
|
|
190
|
-
|
|
286
|
+
const scope = new FormScope(doc.context);
|
|
287
|
+
add({
|
|
288
|
+
id: placed.id,
|
|
289
|
+
ops: tileOps({ scope, fonts, thumbnail: thumbnails.get(placed.id) }, content, footprint),
|
|
290
|
+
bounds: footprint,
|
|
291
|
+
annot: { subtype: 'Stamp', contents: tileContents(content) },
|
|
292
|
+
}, scope, markRefs.get(placed.id));
|
|
191
293
|
}
|
|
192
|
-
if (ops.length === 0)
|
|
193
|
-
continue;
|
|
194
|
-
page.pushOperators(...withMatrix(docToPageMatrix(rect, box, rotation), [
|
|
195
|
-
...clipRectOps(rect.x, rect.y, rect.width, rect.height),
|
|
196
|
-
...ops,
|
|
197
|
-
]));
|
|
198
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);
|
|
199
382
|
const pdf = await doc.save({ useObjectStreams: false });
|
|
200
|
-
return { pdf, pageCount: pages.length, skipped };
|
|
383
|
+
return { pdf, pageCount: pages.length, annotationCount, skipped };
|
|
201
384
|
};
|
|
@@ -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
|
+
}
|
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type PDFImage, type PDFOperator } from 'pdf-lib';
|
|
2
2
|
import type { AnnotationCanvasState, PlacedMeasurementRef } from '../../types/annotation.js';
|
|
3
3
|
import type { PdfFonts } from './fonts.js';
|
|
4
|
+
import type { ResourceScope } from './resources.js';
|
|
4
5
|
import type { AffineMatrix, DocRect } from './pageMapping.js';
|
|
5
6
|
import type { PdfExportCalibration, ResolvedTileContent } from './types.js';
|
|
6
|
-
/** An image XObject already registered on the page, with its pixel size. */
|
|
7
|
-
export interface PageImage {
|
|
8
|
-
name: PDFName;
|
|
9
|
-
width: number;
|
|
10
|
-
height: number;
|
|
11
|
-
}
|
|
12
7
|
export interface TilePaintContext {
|
|
13
|
-
|
|
8
|
+
/** Where fonts, images and opacity states get registered. */
|
|
9
|
+
scope: ResourceScope;
|
|
14
10
|
fonts: PdfFonts;
|
|
15
11
|
/** Object-tile thumbnail, when the content carries one and it embedded. */
|
|
16
|
-
thumbnail?:
|
|
12
|
+
thumbnail?: PDFImage;
|
|
17
13
|
}
|
|
18
14
|
export interface TileFootprint extends DocRect {
|
|
19
15
|
/** Design-space box the painter lays out in. */
|