@reekon-tools/boldr-utils 1.21.1 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasSkia.js +3 -11
  2. package/dist/annotation/canvas/measurementLineStyle.d.ts +3 -0
  3. package/dist/annotation/canvas/measurementLineStyle.js +14 -0
  4. package/dist/export/pdf/annots.d.ts +23 -0
  5. package/dist/export/pdf/annots.js +135 -0
  6. package/dist/export/pdf/color.d.ts +7 -0
  7. package/dist/export/pdf/color.js +39 -0
  8. package/dist/export/pdf/decrypt.d.ts +21 -0
  9. package/dist/export/pdf/decrypt.js +121 -0
  10. package/dist/export/pdf/emit.d.ts +37 -0
  11. package/dist/export/pdf/emit.js +129 -0
  12. package/dist/export/pdf/fonts.d.ts +37 -0
  13. package/dist/export/pdf/fonts.js +85 -0
  14. package/dist/export/pdf/index.d.ts +7 -0
  15. package/dist/export/pdf/index.js +7 -0
  16. package/dist/export/pdf/layers.d.ts +5 -0
  17. package/dist/export/pdf/layers.js +19 -0
  18. package/dist/export/pdf/marks.d.ts +81 -0
  19. package/dist/export/pdf/marks.js +479 -0
  20. package/dist/export/pdf/pageMapping.d.ts +51 -0
  21. package/dist/export/pdf/pageMapping.js +77 -0
  22. package/dist/export/pdf/renderCanvasToPdf.d.ts +2 -0
  23. package/dist/export/pdf/renderCanvasToPdf.js +288 -0
  24. package/dist/export/pdf/resources.d.ts +35 -0
  25. package/dist/export/pdf/resources.js +95 -0
  26. package/dist/export/pdf/tileContent.d.ts +39 -0
  27. package/dist/export/pdf/tileContent.js +218 -0
  28. package/dist/export/pdf/tiles.d.ts +39 -0
  29. package/dist/export/pdf/tiles.js +392 -0
  30. package/dist/export/pdf/types.d.ts +112 -0
  31. package/dist/export/pdf/types.js +10 -0
  32. package/dist/exports.d.ts +1 -1
  33. package/dist/exports.js +1 -1
  34. package/dist/formulas/calculateFormula.d.ts +1 -1
  35. package/dist/formulas/calculateFormula.js +23 -10
  36. package/dist/formulas/evaluateFormula.d.ts +0 -10
  37. package/dist/formulas/evaluateFormula.js +1 -12
  38. package/dist/formulas/groupFormula.d.ts +43 -0
  39. package/dist/formulas/groupFormula.js +168 -0
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.js +1 -1
  42. package/dist/server.d.ts +4 -1
  43. package/dist/server.js +6 -1
  44. package/package.json +9 -1
  45. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  46. package/dist/canvas/AnnotationCanvas.js +0 -10
  47. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  48. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  49. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  50. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  51. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  52. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  53. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  54. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  55. package/dist/canvas/Tool.d.ts +0 -38
  56. package/dist/canvas/Tool.js +0 -1
  57. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  58. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  59. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  60. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  61. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  62. package/dist/canvas/elements/ShapeElement.js +0 -62
  63. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  64. package/dist/canvas/elements/StrokeElement.js +0 -18
  65. package/dist/canvas/measurementPicker.d.ts +0 -10
  66. package/dist/canvas/measurementPicker.js +0 -1
  67. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  68. package/dist/canvas/measurementStampOverlay.js +0 -1
  69. package/dist/canvas/pointerAdapter.d.ts +0 -3
  70. package/dist/canvas/pointerAdapter.js +0 -19
  71. package/dist/canvas/stampLayout.d.ts +0 -5
  72. package/dist/canvas/stampLayout.js +0 -14
  73. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  74. package/dist/canvas/tools/measurementStampTool.js +0 -37
  75. package/dist/canvas/tools/panTool.d.ts +0 -5
  76. package/dist/canvas/tools/panTool.js +0 -25
  77. package/dist/canvas/tools/penTool.d.ts +0 -13
  78. package/dist/canvas/tools/penTool.js +0 -68
  79. package/dist/canvas/tools/selectTool.d.ts +0 -2
  80. package/dist/canvas/tools/selectTool.js +0 -182
  81. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  82. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  83. package/dist/canvas/viewport.d.ts +0 -16
  84. package/dist/canvas/viewport.js +0 -54
  85. package/dist/data/AnnotationDataContext.d.ts +0 -8
  86. package/dist/data/AnnotationDataContext.js +0 -11
  87. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  88. package/dist/data/AnnotationDataProvider.js +0 -4
  89. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  90. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  91. package/dist/data/canvasPersistence.d.ts +0 -3
  92. package/dist/data/canvasPersistence.js +0 -26
  93. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  94. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  95. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  96. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  97. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  98. package/dist/data/hooks/useAnnotationList.js +0 -26
  99. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  100. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  101. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  102. package/dist/hooks/useParseMeasurement.js +0 -14
  103. package/dist/utils/evaluateFormula.d.ts +0 -20
  104. package/dist/utils/evaluateFormula.js +0 -31
@@ -10,17 +10,9 @@ import { BackgroundLayerElement } from './elements/BackgroundImageElement.js';
10
10
  import { PlaneElement, PLANE_CHROME_COLOR, PLANE_GRID_COLOR, PLANE_GRID_OPACITY, PLANE_GRID_WIDTH, PLANE_OUTLINE_WIDTH, } from './elements/PlaneElement.js';
11
11
  import { ShapeElement } from './elements/ShapeElement.js';
12
12
  import { StrokeElement } from './elements/StrokeElement.js';
13
- // Default visual constants for the measurement-annotation line (the tile itself
14
- // is a fixed-size overlay; only the line lives in Skia). Width is in doc units,
15
- // so it scales with zoom like the drawn line/arrow shapes. These are fallbacks:
16
- // a placed annotation may override them via `lineColor`/`lineWidth`/`lineCap`.
17
- // The leader line defaults to the medium width preset so it reads clearly
18
- // against the document; the rectangle border stays thinner (it outlines a
19
- // region rather than pointing at one).
20
- const MEASUREMENT_LINE_COLOR = '#0066FF';
21
- const MEASUREMENT_LINE_WIDTH = 16;
22
- const MEASUREMENT_RECT_WIDTH = 2;
23
- // Endpoint handle color — constant selection chrome, independent of the (now
13
+ import { MEASUREMENT_LINE_COLOR, MEASUREMENT_LINE_WIDTH, MEASUREMENT_RECT_WIDTH, } from './measurementLineStyle.js';
14
+ // The measurement-line defaults live in measurementLineStyle.ts (shared with
15
+ // the PDF exporter). Endpoint handle color — constant selection chrome, independent of the (now
24
16
  // editable) line color so the handles stay legible on any line color.
25
17
  const MEASUREMENT_HANDLE_COLOR = '#0066FF';
26
18
  // Selection bounding box for strokes/shapes (measurements get handles instead).
@@ -0,0 +1,3 @@
1
+ export declare const MEASUREMENT_LINE_COLOR = "#0066FF";
2
+ export declare const MEASUREMENT_LINE_WIDTH = 16;
3
+ export declare const MEASUREMENT_RECT_WIDTH = 2;
@@ -0,0 +1,14 @@
1
+ // Default visual constants for the measurement-annotation line and rectangle
2
+ // (the `placement: 'line'` / `'rectangle'` variants of PlacedMeasurementRef).
3
+ // Widths are in doc units, so they scale with zoom like the drawn shapes. A
4
+ // placed annotation may override them via `lineColor` / `lineWidth` /
5
+ // `lineCap`. Skia-free so the PDF exporter (export/pdf) and the Skia scene
6
+ // (AnnotationCanvasSkia) read ONE source — a change here re-styles both the
7
+ // screen and the exported page.
8
+ //
9
+ // The leader line defaults to the medium width preset so it reads clearly
10
+ // against the document; the rectangle border stays thinner (it outlines a
11
+ // region rather than pointing at one).
12
+ export const MEASUREMENT_LINE_COLOR = '#0066FF';
13
+ export const MEASUREMENT_LINE_WIDTH = 16;
14
+ export const MEASUREMENT_RECT_WIDTH = 2;
@@ -0,0 +1,23 @@
1
+ import { PDFDocument, PDFPage, PDFRef } from 'pdf-lib';
2
+ import type { Mark } from './marks.js';
3
+ import { type AffineMatrix, type DocRect } from './pageMapping.js';
4
+ import type { FormScope } from './resources.js';
5
+ export interface AddAnnotationInput {
6
+ doc: PDFDocument;
7
+ page: PDFPage;
8
+ mark: Mark;
9
+ /** The page's doc-space rect; the appearance is clipped to it. */
10
+ pageRect: DocRect;
11
+ matrix: AffineMatrix;
12
+ /** The scope the mark's operators registered their resources in. */
13
+ scope: FormScope;
14
+ /** A sibling annotation this one belongs with (Acrobat groups them). */
15
+ groupWith?: PDFRef;
16
+ }
17
+ /**
18
+ * Wrap a mark as an annotation on `page`. Returns the annotation's ref, or
19
+ * null when the mark lies entirely outside the page.
20
+ */
21
+ export declare const addMarkAnnotation: ({ doc, page, mark, pageRect, matrix, scope, groupWith, }: AddAnnotationInput) => PDFRef | null;
22
+ /** How many annotations a page carries (for tests and the result). */
23
+ export declare const annotationCount: (page: PDFPage) => number;
@@ -0,0 +1,135 @@
1
+ // Annotation mode: every element becomes a PDF annotation object — what
2
+ // Preview, Acrobat and friends show as selectable, movable, deletable
3
+ // markup — carrying the element's exact drawing as its appearance stream
4
+ // plus the standard fields a viewer reads back (colour, width, points).
5
+ //
6
+ // The appearance stream is a Form XObject whose space IS doc space: its
7
+ // /Matrix is the page's doc→user matrix and its /BBox the element's doc
8
+ // bounds clipped to the page, so the operators the flatten path emits are
9
+ // reused byte for byte. /Rect is set to exactly the transformed /BBox, which
10
+ // makes the spec's rect-fitting step (§12.5.5) the identity.
11
+ import { PDFHexString, PDFString, } from 'pdf-lib';
12
+ import { dashIntervals } from '../../annotation/canvas/strokeGeometry.js';
13
+ import { parseHexColor } from './color.js';
14
+ import { applyMatrix, intersectRects, } from './pageMapping.js';
15
+ /** /F flags: Print (bit 3). Not Hidden, not NoView, not Locked. */
16
+ const ANNOT_FLAG_PRINT = 4;
17
+ const rgbArray = (hex) => {
18
+ const { rgb } = parseHexColor(hex);
19
+ return [rgb.red, rgb.green, rgb.blue];
20
+ };
21
+ const round = (n) => Math.round(n * 1000) / 1000;
22
+ const mapPoint = (m, p) => {
23
+ const q = applyMatrix(m, p.x, p.y);
24
+ return [round(q.x), round(q.y)];
25
+ };
26
+ const flatPoints = (m, pts) => pts.flatMap((p) => mapPoint(m, p));
27
+ /** Axis-aligned user-space rect [llx lly urx ury] of a doc rect under `m`. */
28
+ const userRect = (m, r) => {
29
+ const corners = [
30
+ applyMatrix(m, r.x, r.y),
31
+ applyMatrix(m, r.x + r.width, r.y),
32
+ applyMatrix(m, r.x, r.y + r.height),
33
+ applyMatrix(m, r.x + r.width, r.y + r.height),
34
+ ];
35
+ const xs = corners.map((c) => c.x);
36
+ const ys = corners.map((c) => c.y);
37
+ return [
38
+ round(Math.min(...xs)),
39
+ round(Math.min(...ys)),
40
+ round(Math.max(...xs)),
41
+ round(Math.max(...ys)),
42
+ ];
43
+ };
44
+ const lineEnding = (arrow) => (arrow ? 'ClosedArrow' : 'None');
45
+ /** Subtype-specific dictionary entries, in page user space. */
46
+ const metaEntries = (meta, m) => {
47
+ switch (meta.subtype) {
48
+ case 'Ink':
49
+ return {
50
+ C: rgbArray(meta.color),
51
+ BS: { W: meta.width, S: 'S' },
52
+ InkList: meta.paths.map((path) => flatPoints(m, path)),
53
+ };
54
+ case 'Line':
55
+ return {
56
+ C: rgbArray(meta.color),
57
+ IC: rgbArray(meta.color), // arrowhead fill
58
+ BS: borderStyle(meta.width, meta.dash),
59
+ L: [...mapPoint(m, meta.a), ...mapPoint(m, meta.b)],
60
+ LE: [lineEnding(meta.startArrow), lineEnding(meta.endArrow)],
61
+ };
62
+ case 'Square':
63
+ case 'Circle':
64
+ return {
65
+ C: rgbArray(meta.color),
66
+ ...(meta.fill ? { IC: rgbArray(meta.fill) } : {}),
67
+ BS: borderStyle(meta.width, meta.dash),
68
+ };
69
+ case 'Polygon':
70
+ case 'PolyLine':
71
+ return {
72
+ C: rgbArray(meta.color),
73
+ ...(meta.fill ? { IC: rgbArray(meta.fill) } : {}),
74
+ BS: borderStyle(meta.width, meta.dash),
75
+ Vertices: flatPoints(m, meta.vertices),
76
+ ...(meta.subtype === 'PolyLine'
77
+ ? { LE: [lineEnding(meta.startArrow), lineEnding(meta.endArrow)] }
78
+ : {}),
79
+ };
80
+ case 'FreeText': {
81
+ const [r, g, b] = rgbArray(meta.color);
82
+ return {
83
+ DA: PDFString.of(`/Helv ${round(meta.fontSize)} Tf ${r} ${g} ${b} rg`),
84
+ Contents: PDFHexString.fromText(meta.text),
85
+ };
86
+ }
87
+ case 'Stamp':
88
+ return meta.contents
89
+ ? { Contents: PDFHexString.fromText(meta.contents) }
90
+ : {};
91
+ }
92
+ };
93
+ const borderStyle = (width, dash) => dash ? { W: width, S: 'D', D: dashIntervals(width) } : { W: width, S: 'S' };
94
+ const alphaOf = (meta) => ('alpha' in meta ? meta.alpha : 1);
95
+ /**
96
+ * Wrap a mark as an annotation on `page`. Returns the annotation's ref, or
97
+ * null when the mark lies entirely outside the page.
98
+ */
99
+ export const addMarkAnnotation = ({ doc, page, mark, pageRect, matrix, scope, groupWith, }) => {
100
+ const bbox = intersectRects(mark.bounds, pageRect);
101
+ if (!bbox || mark.ops.length === 0)
102
+ return null;
103
+ const { context } = doc;
104
+ const form = context.formXObject(mark.ops, {
105
+ BBox: [
106
+ round(bbox.x),
107
+ round(bbox.y),
108
+ round(bbox.x + bbox.width),
109
+ round(bbox.y + bbox.height),
110
+ ],
111
+ Matrix: matrix.map(round),
112
+ Resources: scope.resources(),
113
+ });
114
+ const formRef = context.register(form);
115
+ const alpha = alphaOf(mark.annot);
116
+ const dict = {
117
+ Type: 'Annot',
118
+ Subtype: mark.annot.subtype,
119
+ Rect: userRect(matrix, bbox),
120
+ F: ANNOT_FLAG_PRINT,
121
+ P: page.ref,
122
+ NM: PDFString.of(mark.id),
123
+ AP: { N: formRef },
124
+ ...(alpha < 1 ? { CA: round(alpha) } : {}),
125
+ ...metaEntries(mark.annot, matrix),
126
+ ...(groupWith ? { IRT: groupWith, RT: 'Group' } : {}),
127
+ };
128
+ // `LiteralObject` is not re-exported by pdf-lib, so the shape is asserted
129
+ // from `obj`'s own parameter type.
130
+ const ref = context.register(context.obj(dict));
131
+ page.node.addAnnot(ref);
132
+ return ref;
133
+ };
134
+ /** How many annotations a page carries (for tests and the result). */
135
+ export const annotationCount = (page) => page.node.Annots()?.size() ?? 0;
@@ -0,0 +1,7 @@
1
+ import { type RGB } from 'pdf-lib';
2
+ export interface ParsedColor {
3
+ rgb: RGB;
4
+ /** 0..1. Absent alpha parses as 1. */
5
+ alpha: number;
6
+ }
7
+ export declare const parseHexColor: (input: string | undefined) => ParsedColor;
@@ -0,0 +1,39 @@
1
+ // Hex colour parsing for the PDF emitter. The canvas stores colours as CSS hex
2
+ // strings (`#RGB`, `#RRGGBB`, `#RRGGBBAA`); pdf-lib wants 0..1 components and
3
+ // PDF carries alpha separately (an ExtGState), so the two are split here.
4
+ import { rgb } from 'pdf-lib';
5
+ const BLACK = { rgb: rgb(0, 0, 0), alpha: 1 };
6
+ const channel = (hex, i, width) => {
7
+ const raw = hex.slice(i, i + width);
8
+ const v = parseInt(width === 1 ? raw + raw : raw, 16);
9
+ return Number.isNaN(v) ? 0 : v / 255;
10
+ };
11
+ export const parseHexColor = (input) => {
12
+ if (!input)
13
+ return BLACK;
14
+ const hex = input.trim().replace(/^#/, '');
15
+ switch (hex.length) {
16
+ case 3:
17
+ return {
18
+ rgb: rgb(channel(hex, 0, 1), channel(hex, 1, 1), channel(hex, 2, 1)),
19
+ alpha: 1,
20
+ };
21
+ case 4:
22
+ return {
23
+ rgb: rgb(channel(hex, 0, 1), channel(hex, 1, 1), channel(hex, 2, 1)),
24
+ alpha: channel(hex, 3, 1),
25
+ };
26
+ case 6:
27
+ return {
28
+ rgb: rgb(channel(hex, 0, 2), channel(hex, 2, 2), channel(hex, 4, 2)),
29
+ alpha: 1,
30
+ };
31
+ case 8:
32
+ return {
33
+ rgb: rgb(channel(hex, 0, 2), channel(hex, 2, 2), channel(hex, 4, 2)),
34
+ alpha: channel(hex, 6, 2),
35
+ };
36
+ default:
37
+ return BLACK;
38
+ }
39
+ };
@@ -0,0 +1,21 @@
1
+ export interface StripPdfEncryptionOptions {
2
+ /** User password, when the document needs one. Never stored. */
3
+ password?: string;
4
+ /** The PDFium wasm bytes; resolved from `@embedpdf/pdfium` when absent. */
5
+ wasmBinary?: ArrayBuffer;
6
+ }
7
+ export interface StripPdfEncryptionResult {
8
+ bytes: Uint8Array;
9
+ /** False when the input had no /Encrypt and was returned untouched. */
10
+ wasEncrypted: boolean;
11
+ }
12
+ /**
13
+ * Open `bytes` with PDFium and return a plaintext copy when the document is
14
+ * encrypted, or the input untouched when it is not.
15
+ *
16
+ * @throws PdfExportError `password-required` when the document needs a user
17
+ * password and none was given; `password-incorrect` when the given one was
18
+ * refused; `unsupported` for a security handler PDFium cannot open;
19
+ * `corrupt` when the bytes are not a PDF or the copy could not be written.
20
+ */
21
+ export declare const stripPdfEncryption: (bytes: Uint8Array, options?: StripPdfEncryptionOptions) => Promise<StripPdfEncryptionResult>;
@@ -0,0 +1,121 @@
1
+ // Strip a PDF's encryption so pdf-lib can append to it.
2
+ //
3
+ // Files saved by annotation tools (Nutrient included) and by many CAD /
4
+ // Bluebeam exports carry an /Encrypt dictionary with an EMPTY user password:
5
+ // every viewer opens them without a prompt, but every stream inside is still
6
+ // RC4 / AES ciphertext keyed off that empty password. PDFium decrypts on the
7
+ // fly, which is why the canvas renders such a file; pdf-lib cannot decrypt at
8
+ // all and refuses to load it. Appending plaintext marks to a document whose
9
+ // trailer says "encrypted" is not an option either — a viewer would run the
10
+ // document key over our plaintext and paint garbage.
11
+ //
12
+ // So the fix is to open with PDFium (the same wasm build the web canvas
13
+ // renders with), drop the encryption, and save a plaintext copy. A document
14
+ // that genuinely needs a user password is reported as such so a host can
15
+ // prompt for it and call again with `password`; nothing here persists it.
16
+ //
17
+ // NODE ONLY. Reached from the `./server` entry, never from the app barrel:
18
+ // it resolves the wasm off disk and loads `@embedpdf/pdfium` lazily (an
19
+ // optional peer), so a consumer that never calls it never pays for it.
20
+ import { readFile } from 'node:fs/promises';
21
+ import { createRequire } from 'node:module';
22
+ import { PdfExportError } from './types.js';
23
+ /** PDFium's FPDF_GetLastError codes. */
24
+ const FPDF_ERR_PASSWORD = 4;
25
+ const FPDF_ERR_SECURITY = 5;
26
+ let modulePromise = null;
27
+ // `HEAPU8` is an Emscripten runtime field the wrapper's typings omit. Read it
28
+ // through a call every time: Emscripten REPLACES the view when memory grows.
29
+ const heapU8 = (m) => m.pdfium.HEAPU8;
30
+ const loadPdfium = (wasmBinary) => {
31
+ if (wasmBinary)
32
+ return initPdfium(wasmBinary);
33
+ if (modulePromise === null) {
34
+ modulePromise = (async () => {
35
+ const require_ = createRequire(import.meta.url);
36
+ const bytes = await readFile(require_.resolve('@embedpdf/pdfium/pdfium.wasm'));
37
+ return initPdfium(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
38
+ })();
39
+ }
40
+ return modulePromise;
41
+ };
42
+ const initPdfium = async (wasmBinary) => {
43
+ const { init } = await import('@embedpdf/pdfium');
44
+ const m = await init({ wasmBinary });
45
+ m.PDFiumExt_Init();
46
+ return m;
47
+ };
48
+ /**
49
+ * Open `bytes` with PDFium and return a plaintext copy when the document is
50
+ * encrypted, or the input untouched when it is not.
51
+ *
52
+ * @throws PdfExportError `password-required` when the document needs a user
53
+ * password and none was given; `password-incorrect` when the given one was
54
+ * refused; `unsupported` for a security handler PDFium cannot open;
55
+ * `corrupt` when the bytes are not a PDF or the copy could not be written.
56
+ */
57
+ export const stripPdfEncryption = async (bytes, options = {}) => {
58
+ const m = await loadPdfium(options.wasmBinary);
59
+ const { malloc, free } = m.pdfium.wasmExports;
60
+ const ptr = malloc(bytes.byteLength);
61
+ heapU8(m).set(bytes, ptr);
62
+ const doc = m.FPDF_LoadMemDocument(ptr, bytes.byteLength, options.password ?? '');
63
+ if (!doc) {
64
+ const err = m.FPDF_GetLastError();
65
+ free(ptr);
66
+ if (err === FPDF_ERR_PASSWORD) {
67
+ throw options.password !== undefined
68
+ ? new PdfExportError('password-incorrect', 'The PDF password is incorrect')
69
+ : new PdfExportError('password-required', 'This PDF is protected by a password');
70
+ }
71
+ if (err === FPDF_ERR_SECURITY) {
72
+ throw new PdfExportError('unsupported', 'This PDF uses a security handler that cannot be opened');
73
+ }
74
+ throw new PdfExportError('corrupt', `PDFium could not open the PDF (error ${err})`);
75
+ }
76
+ try {
77
+ if (!m.EPDF_IsEncrypted(doc))
78
+ return { bytes, wasEncrypted: false };
79
+ if (!m.EPDF_RemoveEncryption(doc)) {
80
+ throw new PdfExportError('corrupt', 'The PDF encryption could not be removed');
81
+ }
82
+ return { bytes: saveCopy(m, doc), wasEncrypted: true };
83
+ }
84
+ finally {
85
+ m.FPDF_CloseDocument(doc);
86
+ free(ptr);
87
+ }
88
+ };
89
+ /**
90
+ * FPDF_SaveAsCopy through an FPDF_FILEWRITE: `{ int version; int (*WriteBlock)
91
+ * (FPDF_FILEWRITE*, const void*, unsigned long); }` — two 32-bit words in
92
+ * wasm32, the callback registered into the function table for the duration.
93
+ */
94
+ const saveCopy = (m, doc) => {
95
+ const { malloc, free } = m.pdfium.wasmExports;
96
+ const chunks = [];
97
+ const writeBlock = m.pdfium.addFunction((_self, data, size) => {
98
+ // Re-read the heap each call: it is replaced when memory grows.
99
+ chunks.push(heapU8(m).slice(data, data + size));
100
+ return 1;
101
+ }, 'iiii');
102
+ const fileWrite = malloc(8);
103
+ try {
104
+ m.pdfium.setValue(fileWrite, 1, 'i32');
105
+ m.pdfium.setValue(fileWrite + 4, writeBlock, 'i32');
106
+ if (!m.FPDF_SaveAsCopy(doc, fileWrite, 0)) {
107
+ throw new PdfExportError('corrupt', 'PDFium could not write the decrypted copy');
108
+ }
109
+ }
110
+ finally {
111
+ free(fileWrite);
112
+ m.pdfium.removeFunction(writeBlock);
113
+ }
114
+ const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));
115
+ let o = 0;
116
+ for (const c of chunks) {
117
+ out.set(c, o);
118
+ o += c.length;
119
+ }
120
+ return out;
121
+ };
@@ -0,0 +1,37 @@
1
+ import { LineCapStyle, type PDFName, type PDFOperator } from 'pdf-lib';
2
+ import type { StrokeCap, Vec2 } from '../../types/annotation.js';
3
+ import type { AffineMatrix } from './pageMapping.js';
4
+ export declare const withMatrix: (m: AffineMatrix, ops: PDFOperator[]) => PDFOperator[];
5
+ /** Flip back to y-up at (x, y): text and images are drawn upright inside it. */
6
+ export declare const uprightAt: (x: number, y: number) => AffineMatrix;
7
+ export declare const lineCapOf: (cap: StrokeCap | undefined) => LineCapStyle;
8
+ export interface StrokeStyle {
9
+ color: string;
10
+ width: number;
11
+ cap?: StrokeCap;
12
+ join?: 'round' | 'miter';
13
+ dash?: boolean;
14
+ /** ExtGState resource name carrying the element's opacity, if any. */
15
+ graphicsState?: PDFName;
16
+ }
17
+ /** Operators that set the stroking state for a Skia-style stroked paint. */
18
+ export declare const strokeStateOps: (s: StrokeStyle) => PDFOperator[];
19
+ export declare const fillStateOps: (color: string, graphicsState?: PDFName) => PDFOperator[];
20
+ /** Path construction for a polyline over `points`; no painting operator. */
21
+ export declare const polylinePathOps: (points: readonly Vec2[], close?: boolean) => PDFOperator[];
22
+ /** Same, over the flat [x, y, x, y, …] array strokes store. */
23
+ export declare const flatPolylinePathOps: (points: readonly number[]) => PDFOperator[];
24
+ export declare const circlePathOps: (cx: number, cy: number, r: number) => PDFOperator[];
25
+ export declare const rectPathOps: (x: number, y: number, w: number, h: number) => PDFOperator[];
26
+ /** Rounded rectangle path in a y-down space (radius clamped to half a side). */
27
+ export declare const roundedRectPathOps: (x: number, y: number, w: number, h: number, radius: number) => PDFOperator[];
28
+ /** Intersect the clip with an axis-aligned rect (no painting). */
29
+ export declare const clipRectOps: (x: number, y: number, w: number, h: number) => PDFOperator[];
30
+ export declare const strokeOp: () => PDFOperator;
31
+ export declare const fillOp: () => PDFOperator;
32
+ /**
33
+ * Draw an image XObject so it occupies the y-down rect (x, y, w, h) with its
34
+ * top row at `y`. PDF maps the unit square's TOP edge to the image's first
35
+ * row, so under a y-down CTM the image must be un-flipped locally.
36
+ */
37
+ export declare const imageOps: (name: PDFName, x: number, y: number, w: number, h: number, graphicsState?: PDFName) => PDFOperator[];
@@ -0,0 +1,129 @@
1
+ // Low-level PDF operator helpers for the exporter. Everything here works in
2
+ // the CURRENT coordinate system (whatever `cm` is in force), so the same
3
+ // helpers draw marks in doc space and tile chrome in a tile's design space.
4
+ import { LineCapStyle, LineJoinStyle, appendBezierCurve, clip, closePath, concatTransformationMatrix, degrees, drawImage, endPath, fill, lineTo, moveTo, popGraphicsState, pushGraphicsState, rectangle, setDashPattern, setFillingColor, setGraphicsState, setLineCap, setLineJoin, setLineWidth, setStrokingColor, stroke, } from 'pdf-lib';
5
+ import { dashIntervals, toSkiaStrokeCap, } from '../../annotation/canvas/strokeGeometry.js';
6
+ import { parseHexColor } from './color.js';
7
+ /** Bézier circle constant. */
8
+ const KAPPA = 0.5522847498307936;
9
+ export const withMatrix = (m, ops) => [
10
+ pushGraphicsState(),
11
+ concatTransformationMatrix(m[0], m[1], m[2], m[3], m[4], m[5]),
12
+ ...ops,
13
+ popGraphicsState(),
14
+ ];
15
+ /** Flip back to y-up at (x, y): text and images are drawn upright inside it. */
16
+ export const uprightAt = (x, y) => [
17
+ 1,
18
+ 0,
19
+ 0,
20
+ -1,
21
+ x,
22
+ y,
23
+ ];
24
+ export const lineCapOf = (cap) => {
25
+ switch (toSkiaStrokeCap(cap)) {
26
+ case 'butt':
27
+ return LineCapStyle.Butt;
28
+ case 'square':
29
+ return LineCapStyle.Projecting;
30
+ default:
31
+ return LineCapStyle.Round;
32
+ }
33
+ };
34
+ /** Operators that set the stroking state for a Skia-style stroked paint. */
35
+ export const strokeStateOps = (s) => {
36
+ const ops = [];
37
+ if (s.graphicsState)
38
+ ops.push(setGraphicsState(s.graphicsState));
39
+ ops.push(setStrokingColor(parseHexColor(s.color).rgb), setLineWidth(s.width), setLineCap(lineCapOf(s.cap)), setLineJoin(s.join === 'round' ? LineJoinStyle.Round : LineJoinStyle.Miter));
40
+ if (s.dash) {
41
+ const [on, off] = dashIntervals(s.width);
42
+ ops.push(setDashPattern([on, off], 0));
43
+ }
44
+ return ops;
45
+ };
46
+ export const fillStateOps = (color, graphicsState) => {
47
+ const ops = [];
48
+ if (graphicsState)
49
+ ops.push(setGraphicsState(graphicsState));
50
+ ops.push(setFillingColor(parseHexColor(color).rgb));
51
+ return ops;
52
+ };
53
+ /** Path construction for a polyline over `points`; no painting operator. */
54
+ export const polylinePathOps = (points, close = false) => {
55
+ if (points.length === 0)
56
+ return [];
57
+ const ops = [moveTo(points[0].x, points[0].y)];
58
+ for (let i = 1; i < points.length; i++) {
59
+ ops.push(lineTo(points[i].x, points[i].y));
60
+ }
61
+ if (close)
62
+ ops.push(closePath());
63
+ return ops;
64
+ };
65
+ /** Same, over the flat [x, y, x, y, …] array strokes store. */
66
+ export const flatPolylinePathOps = (points) => {
67
+ if (points.length < 2)
68
+ return [];
69
+ const ops = [moveTo(points[0], points[1])];
70
+ for (let i = 2; i + 1 < points.length; i += 2) {
71
+ ops.push(lineTo(points[i], points[i + 1]));
72
+ }
73
+ return ops;
74
+ };
75
+ export const circlePathOps = (cx, cy, r) => {
76
+ const k = KAPPA * r;
77
+ return [
78
+ moveTo(cx + r, cy),
79
+ appendBezierCurve(cx + r, cy + k, cx + k, cy + r, cx, cy + r),
80
+ appendBezierCurve(cx - k, cy + r, cx - r, cy + k, cx - r, cy),
81
+ appendBezierCurve(cx - r, cy - k, cx - k, cy - r, cx, cy - r),
82
+ appendBezierCurve(cx + k, cy - r, cx + r, cy - k, cx + r, cy),
83
+ closePath(),
84
+ ];
85
+ };
86
+ export const rectPathOps = (x, y, w, h) => [rectangle(x, y, w, h)];
87
+ /** Rounded rectangle path in a y-down space (radius clamped to half a side). */
88
+ export const roundedRectPathOps = (x, y, w, h, radius) => {
89
+ const r = Math.max(0, Math.min(radius, w / 2, h / 2));
90
+ if (r === 0)
91
+ return rectPathOps(x, y, w, h);
92
+ const k = KAPPA * r;
93
+ return [
94
+ moveTo(x + r, y),
95
+ lineTo(x + w - r, y),
96
+ appendBezierCurve(x + w - r + k, y, x + w, y + r - k, x + w, y + r),
97
+ lineTo(x + w, y + h - r),
98
+ appendBezierCurve(x + w, y + h - r + k, x + w - r + k, y + h, x + w - r, y + h),
99
+ lineTo(x + r, y + h),
100
+ appendBezierCurve(x + r - k, y + h, x, y + h - r + k, x, y + h - r),
101
+ lineTo(x, y + r),
102
+ appendBezierCurve(x, y + r - k, x + r - k, y, x + r, y),
103
+ closePath(),
104
+ ];
105
+ };
106
+ /** Intersect the clip with an axis-aligned rect (no painting). */
107
+ export const clipRectOps = (x, y, w, h) => [rectangle(x, y, w, h), clip(), endPath()];
108
+ export const strokeOp = () => stroke();
109
+ export const fillOp = () => fill();
110
+ /**
111
+ * Draw an image XObject so it occupies the y-down rect (x, y, w, h) with its
112
+ * top row at `y`. PDF maps the unit square's TOP edge to the image's first
113
+ * row, so under a y-down CTM the image must be un-flipped locally.
114
+ */
115
+ export const imageOps = (name, x, y, w, h, graphicsState) => {
116
+ const ops = [pushGraphicsState()];
117
+ if (graphicsState)
118
+ ops.push(setGraphicsState(graphicsState));
119
+ ops.push(concatTransformationMatrix(w, 0, 0, -h, x, y + h), ...drawImage(name, {
120
+ x: 0,
121
+ y: 0,
122
+ width: 1,
123
+ height: 1,
124
+ rotate: degrees(0),
125
+ xSkew: degrees(0),
126
+ ySkew: degrees(0),
127
+ }), popGraphicsState());
128
+ return ops;
129
+ };
@@ -0,0 +1,37 @@
1
+ import { PDFDocument, PDFFont, PDFName, type PDFOperator } from 'pdf-lib';
2
+ import type { ResourceScope } from './resources.js';
3
+ export type TextAlign = 'left' | 'center' | 'right';
4
+ export interface TextOptions {
5
+ align?: TextAlign;
6
+ /** ExtGState name carrying an opacity, if any. */
7
+ graphicsState?: PDFName;
8
+ }
9
+ export declare class PdfFonts {
10
+ private readonly primary;
11
+ private readonly fallback;
12
+ private constructor();
13
+ static create(doc: PDFDocument, primaryBytes: Uint8Array): Promise<PdfFonts>;
14
+ /** Whether the primary face has a glyph for every code point of `text`. */
15
+ private primaryCovers;
16
+ /** Drop the code points the standard fallback cannot encode. */
17
+ private fallbackUsable;
18
+ /**
19
+ * The face to draw `text` with and the text it can actually show. The
20
+ * primary wins when it covers every glyph; otherwise the fallback, minus
21
+ * anything even it cannot encode. `dropped` is true when glyphs were lost.
22
+ */
23
+ pick(text: string): {
24
+ font: PDFFont;
25
+ text: string;
26
+ dropped: boolean;
27
+ };
28
+ width(text: string, size: number): number;
29
+ /**
30
+ * Operators drawing one line of text in a y-DOWN space, with its baseline at
31
+ * `yBaseline` and its left / centre / right edge at `x`. Returns [] when
32
+ * nothing of the text is drawable.
33
+ */
34
+ textOps(scope: ResourceScope, text: string, size: number, color: string, x: number, yBaseline: number, options?: TextOptions): PDFOperator[];
35
+ /** Largest size ≤ `max` at which `text` fits in `maxWidth`. */
36
+ fitSize(text: string, maxWidth: number, max: number, min?: number): number;
37
+ }