@reekon-tools/boldr-utils 1.21.1 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasSkia.js +3 -11
  2. package/dist/annotation/canvas/measurementLineStyle.d.ts +3 -0
  3. package/dist/annotation/canvas/measurementLineStyle.js +14 -0
  4. package/dist/export/pdf/annots.d.ts +23 -0
  5. package/dist/export/pdf/annots.js +135 -0
  6. package/dist/export/pdf/color.d.ts +7 -0
  7. package/dist/export/pdf/color.js +39 -0
  8. package/dist/export/pdf/decrypt.d.ts +21 -0
  9. package/dist/export/pdf/decrypt.js +121 -0
  10. package/dist/export/pdf/emit.d.ts +37 -0
  11. package/dist/export/pdf/emit.js +129 -0
  12. package/dist/export/pdf/fonts.d.ts +37 -0
  13. package/dist/export/pdf/fonts.js +85 -0
  14. package/dist/export/pdf/index.d.ts +7 -0
  15. package/dist/export/pdf/index.js +7 -0
  16. package/dist/export/pdf/layers.d.ts +5 -0
  17. package/dist/export/pdf/layers.js +19 -0
  18. package/dist/export/pdf/marks.d.ts +81 -0
  19. package/dist/export/pdf/marks.js +479 -0
  20. package/dist/export/pdf/pageMapping.d.ts +51 -0
  21. package/dist/export/pdf/pageMapping.js +77 -0
  22. package/dist/export/pdf/renderCanvasToPdf.d.ts +2 -0
  23. package/dist/export/pdf/renderCanvasToPdf.js +288 -0
  24. package/dist/export/pdf/resources.d.ts +35 -0
  25. package/dist/export/pdf/resources.js +95 -0
  26. package/dist/export/pdf/tileContent.d.ts +39 -0
  27. package/dist/export/pdf/tileContent.js +218 -0
  28. package/dist/export/pdf/tiles.d.ts +39 -0
  29. package/dist/export/pdf/tiles.js +392 -0
  30. package/dist/export/pdf/types.d.ts +112 -0
  31. package/dist/export/pdf/types.js +10 -0
  32. package/dist/exports.d.ts +1 -1
  33. package/dist/exports.js +1 -1
  34. package/dist/formulas/calculateFormula.d.ts +1 -1
  35. package/dist/formulas/calculateFormula.js +23 -10
  36. package/dist/formulas/evaluateFormula.d.ts +0 -10
  37. package/dist/formulas/evaluateFormula.js +1 -12
  38. package/dist/formulas/groupFormula.d.ts +43 -0
  39. package/dist/formulas/groupFormula.js +168 -0
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.js +1 -1
  42. package/dist/server.d.ts +4 -1
  43. package/dist/server.js +6 -1
  44. package/package.json +9 -1
  45. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  46. package/dist/canvas/AnnotationCanvas.js +0 -10
  47. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  48. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  49. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  50. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  51. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  52. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  53. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  54. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  55. package/dist/canvas/Tool.d.ts +0 -38
  56. package/dist/canvas/Tool.js +0 -1
  57. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  58. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  59. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  60. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  61. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  62. package/dist/canvas/elements/ShapeElement.js +0 -62
  63. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  64. package/dist/canvas/elements/StrokeElement.js +0 -18
  65. package/dist/canvas/measurementPicker.d.ts +0 -10
  66. package/dist/canvas/measurementPicker.js +0 -1
  67. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  68. package/dist/canvas/measurementStampOverlay.js +0 -1
  69. package/dist/canvas/pointerAdapter.d.ts +0 -3
  70. package/dist/canvas/pointerAdapter.js +0 -19
  71. package/dist/canvas/stampLayout.d.ts +0 -5
  72. package/dist/canvas/stampLayout.js +0 -14
  73. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  74. package/dist/canvas/tools/measurementStampTool.js +0 -37
  75. package/dist/canvas/tools/panTool.d.ts +0 -5
  76. package/dist/canvas/tools/panTool.js +0 -25
  77. package/dist/canvas/tools/penTool.d.ts +0 -13
  78. package/dist/canvas/tools/penTool.js +0 -68
  79. package/dist/canvas/tools/selectTool.d.ts +0 -2
  80. package/dist/canvas/tools/selectTool.js +0 -182
  81. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  82. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  83. package/dist/canvas/viewport.d.ts +0 -16
  84. package/dist/canvas/viewport.js +0 -54
  85. package/dist/data/AnnotationDataContext.d.ts +0 -8
  86. package/dist/data/AnnotationDataContext.js +0 -11
  87. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  88. package/dist/data/AnnotationDataProvider.js +0 -4
  89. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  90. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  91. package/dist/data/canvasPersistence.d.ts +0 -3
  92. package/dist/data/canvasPersistence.js +0 -26
  93. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  94. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  95. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  96. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  97. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  98. package/dist/data/hooks/useAnnotationList.js +0 -26
  99. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  100. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  101. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  102. package/dist/hooks/useParseMeasurement.js +0 -14
  103. package/dist/utils/evaluateFormula.d.ts +0 -20
  104. package/dist/utils/evaluateFormula.js +0 -31
@@ -0,0 +1,85 @@
1
+ // Text for the exporter: one embedded primary face (the apps' tile font) plus
2
+ // a standard-14 fallback for the glyphs it lacks. Metropolis Bold, for one,
3
+ // has no `°` — and an angle tile is nothing without it.
4
+ import { StandardFonts, degrees, drawText, } from 'pdf-lib';
5
+ import fontkit from '@pdf-lib/fontkit';
6
+ import { parseHexColor } from './color.js';
7
+ import { uprightAt, withMatrix } from './emit.js';
8
+ const codePointsOf = (text) => Array.from(text, (ch) => ch.codePointAt(0));
9
+ export class PdfFonts {
10
+ constructor(primary, fallback) {
11
+ this.primary = primary;
12
+ this.fallback = fallback;
13
+ }
14
+ static async create(doc, primaryBytes) {
15
+ doc.registerFontkit(fontkit);
16
+ // NOT subset: fontkit's CFF subsetter throws on this face ("value" argument out of
17
+ // bounds in CFFSubset.encode), and the whole file is ~24 KB anyway.
18
+ const primary = await doc.embedFont(primaryBytes, { subset: false });
19
+ const fallback = await doc.embedStandardFont(StandardFonts.HelveticaBold);
20
+ return new PdfFonts(primary, fallback);
21
+ }
22
+ /** Whether the primary face has a glyph for every code point of `text`. */
23
+ primaryCovers(text) {
24
+ const fk = this.primary.embedder?.font;
25
+ if (!fk?.hasGlyphForCodePoint)
26
+ return true;
27
+ return codePointsOf(text).every((cp) => cp === 0x20 || fk.hasGlyphForCodePoint(cp));
28
+ }
29
+ /** Drop the code points the standard fallback cannot encode. */
30
+ fallbackUsable(text) {
31
+ const enc = this.fallback.embedder?.encoding;
32
+ if (!enc?.canEncodeUnicodeCodePoint)
33
+ return text;
34
+ return Array.from(text)
35
+ .filter((ch) => enc.canEncodeUnicodeCodePoint(ch.codePointAt(0)))
36
+ .join('');
37
+ }
38
+ /**
39
+ * The face to draw `text` with and the text it can actually show. The
40
+ * primary wins when it covers every glyph; otherwise the fallback, minus
41
+ * anything even it cannot encode. `dropped` is true when glyphs were lost.
42
+ */
43
+ pick(text) {
44
+ if (this.primaryCovers(text)) {
45
+ return { font: this.primary, text, dropped: false };
46
+ }
47
+ const usable = this.fallbackUsable(text);
48
+ return { font: this.fallback, text: usable, dropped: usable !== text };
49
+ }
50
+ width(text, size) {
51
+ const p = this.pick(text);
52
+ return p.text ? p.font.widthOfTextAtSize(p.text, size) : 0;
53
+ }
54
+ /**
55
+ * Operators drawing one line of text in a y-DOWN space, with its baseline at
56
+ * `yBaseline` and its left / centre / right edge at `x`. Returns [] when
57
+ * nothing of the text is drawable.
58
+ */
59
+ textOps(scope, text, size, color, x, yBaseline, options = {}) {
60
+ const picked = this.pick(text);
61
+ if (!picked.text || size <= 0)
62
+ return [];
63
+ const w = picked.font.widthOfTextAtSize(picked.text, size);
64
+ const align = options.align ?? 'left';
65
+ const left = align === 'center' ? x - w / 2 : align === 'right' ? x - w : x;
66
+ return withMatrix(uprightAt(left, yBaseline), drawText(picked.font.encodeText(picked.text), {
67
+ color: parseHexColor(color).rgb,
68
+ font: scope.fontKey(picked.font),
69
+ size,
70
+ rotate: degrees(0),
71
+ xSkew: degrees(0),
72
+ ySkew: degrees(0),
73
+ x: 0,
74
+ y: 0,
75
+ graphicsState: options.graphicsState,
76
+ }));
77
+ }
78
+ /** Largest size ≤ `max` at which `text` fits in `maxWidth`. */
79
+ fitSize(text, maxWidth, max, min = 4) {
80
+ const atOne = this.width(text, 1);
81
+ if (atOne <= 0)
82
+ return max;
83
+ return Math.max(min, Math.min(max, maxWidth / atOne));
84
+ }
85
+ }
@@ -0,0 +1,7 @@
1
+ export { renderCanvasToPdf } from './renderCanvasToPdf.js';
2
+ export { stripPdfEncryption, type StripPdfEncryptionOptions, type StripPdfEncryptionResult, } from './decrypt.js';
3
+ export { docToPageMatrix, pageRectsOf, normalizeRotation, rotatedBoxSize, applyMatrix, type AffineMatrix, type PageRotation, type PdfBox, } from './pageMapping.js';
4
+ export { resolveTileContent, measurementTileFillFor, FORMULA_COLUMN_TILE_FILL, PLANE_COMPUTED_TILE_FILL, type TileFileInfo, type TileResolutionContext, } from './tileContent.js';
5
+ export { tileFootprint, type TileFootprint } from './tiles.js';
6
+ export { annotationCount } from './annots.js';
7
+ export { PdfExportError, type EmbeddedImage, type PdfExportCalibration, type RenderCanvasToPdfInput, type RenderCanvasToPdfResult, type ResolvedTileContent, type MeasurementTileContent, type LiteralTileContent, type EmptyTileContent, type HeaderTileContent, type ObjectTileContent, type PlaneDimensionTileContent, type InstructionsTileContent, type SkippedItem, } from './types.js';
@@ -0,0 +1,7 @@
1
+ export { renderCanvasToPdf } from './renderCanvasToPdf.js';
2
+ export { stripPdfEncryption, } from './decrypt.js';
3
+ export { docToPageMatrix, pageRectsOf, normalizeRotation, rotatedBoxSize, applyMatrix, } from './pageMapping.js';
4
+ export { resolveTileContent, measurementTileFillFor, FORMULA_COLUMN_TILE_FILL, PLANE_COMPUTED_TILE_FILL, } from './tileContent.js';
5
+ export { tileFootprint } from './tiles.js';
6
+ export { annotationCount } from './annots.js';
7
+ export { PdfExportError, } from './types.js';
@@ -0,0 +1,5 @@
1
+ import { type PDFName, type PDFOperator } from 'pdf-lib';
2
+ import type { AnnotationBackgroundLayer } from '../../types/annotation.js';
3
+ import type { DocRect } from './pageMapping.js';
4
+ export declare const layerVisibleRect: (layer: AnnotationBackgroundLayer) => DocRect;
5
+ export declare const layerOps: (layer: AnnotationBackgroundLayer, imageName: PDFName) => PDFOperator[];
@@ -0,0 +1,19 @@
1
+ // Background image layers baked into the page: the full image placed at its
2
+ // doc rect, clipped to its crop window. Draw order is array order, under every
3
+ // mark — exactly the z-order AnnotationCanvasSkia paints.
4
+ import { popGraphicsState, pushGraphicsState, } from 'pdf-lib';
5
+ import { backgroundLayerDocRect, backgroundLayerFullDocRect, } from '../../annotation/canvas/backgroundLayers.js';
6
+ import { clipRectOps, imageOps } from './emit.js';
7
+ export const layerVisibleRect = (layer) => backgroundLayerDocRect(layer);
8
+ export const layerOps = (layer, imageName) => {
9
+ const crop = backgroundLayerDocRect(layer);
10
+ const full = backgroundLayerFullDocRect(layer);
11
+ if (crop.width <= 0 || crop.height <= 0)
12
+ return [];
13
+ return [
14
+ pushGraphicsState(),
15
+ ...clipRectOps(crop.x, crop.y, crop.width, crop.height),
16
+ ...imageOps(imageName, full.x, full.y, full.width, full.height),
17
+ popGraphicsState(),
18
+ ];
19
+ };
@@ -0,0 +1,81 @@
1
+ import { type PDFOperator } from 'pdf-lib';
2
+ import type { AnnotationShape, AnnotationStroke, PlacedMeasurementRef, Vec2 } from '../../types/annotation.js';
3
+ import { dashIntervals } from '../../annotation/canvas/strokeGeometry.js';
4
+ import type { PdfFonts } from './fonts.js';
5
+ import { type DocRect } from './pageMapping.js';
6
+ import type { ResourceScope } from './resources.js';
7
+ import type { SkippedItem } from './types.js';
8
+ export interface MarkContext {
9
+ /** Where fonts, images and opacity states get registered. */
10
+ scope: ResourceScope;
11
+ fonts: PdfFonts;
12
+ skipped: SkippedItem[];
13
+ }
14
+ /**
15
+ * What an element IS, for the annotation dictionary that wraps its
16
+ * appearance stream in annotation mode: the standard fields a viewer reads
17
+ * back (and regenerates from after an edit). Points and rects are in DOC
18
+ * space; the annotation builder maps them onto the page.
19
+ */
20
+ export type AnnotMeta = {
21
+ subtype: 'Ink';
22
+ color: string;
23
+ width: number;
24
+ alpha: number;
25
+ paths: Vec2[][];
26
+ } | {
27
+ subtype: 'Line';
28
+ color: string;
29
+ width: number;
30
+ alpha: number;
31
+ dash: boolean;
32
+ a: Vec2;
33
+ b: Vec2;
34
+ startArrow: boolean;
35
+ endArrow: boolean;
36
+ } | {
37
+ subtype: 'Square' | 'Circle';
38
+ color: string;
39
+ fill?: string;
40
+ width: number;
41
+ alpha: number;
42
+ dash: boolean;
43
+ rect: DocRect;
44
+ } | {
45
+ subtype: 'Polygon' | 'PolyLine';
46
+ color: string;
47
+ fill?: string;
48
+ width: number;
49
+ alpha: number;
50
+ dash: boolean;
51
+ vertices: Vec2[];
52
+ startArrow: boolean;
53
+ endArrow: boolean;
54
+ } | {
55
+ subtype: 'FreeText';
56
+ color: string;
57
+ fontSize: number;
58
+ text: string;
59
+ alpha: number;
60
+ } | {
61
+ subtype: 'Stamp';
62
+ contents?: string;
63
+ };
64
+ /** One drawable element: its operators, its doc-space bounds, its identity. */
65
+ export interface Mark {
66
+ id: string;
67
+ ops: PDFOperator[];
68
+ bounds: DocRect;
69
+ annot: AnnotMeta;
70
+ }
71
+ export declare const strokeBounds: (s: AnnotationStroke) => DocRect | null;
72
+ export declare const strokeOps: (s: AnnotationStroke, ctx: MarkContext) => PDFOperator[];
73
+ export declare const shapeBounds: (shape: AnnotationShape) => DocRect | null;
74
+ export declare const shapeOps: (shape: AnnotationShape, ctx: MarkContext) => PDFOperator[];
75
+ export declare const measurementMarkBounds: (placed: PlacedMeasurementRef) => DocRect | null;
76
+ export declare const measurementMarkOps: (placed: PlacedMeasurementRef, ctx: MarkContext) => PDFOperator[];
77
+ export { dashIntervals };
78
+ export declare const strokeMark: (s: AnnotationStroke, ctx: MarkContext) => Mark | null;
79
+ export declare const shapeMark: (shape: AnnotationShape, ctx: MarkContext) => Mark | null;
80
+ /** The measurement annotation's line / rectangle (not its tile). */
81
+ export declare const measurementMark: (placed: PlacedMeasurementRef, ctx: MarkContext) => Mark | null;
@@ -0,0 +1,479 @@
1
+ // Vector marks: strokes, shapes and the measurement annotation's line /
2
+ // rectangle, emitted as PDF path operators in DOC space. Mirrors what
3
+ // StrokeElement, ShapeElement and AnnotationCanvasSkia's measurement pass draw,
4
+ // reusing the same geometry helpers so caps, dashes and arrowheads agree.
5
+ import { fill, popGraphicsState, pushGraphicsState, rectangle, stroke, } from 'pdf-lib';
6
+ import { arrowheadTriangle, dashIntervals, toSkiaStrokeCap, } from '../../annotation/canvas/strokeGeometry.js';
7
+ import { visualShapeBounds } from '../../annotation/canvas/shapeGeometry.js';
8
+ import { DEFAULT_TEXT_FONT_SIZE, TEXT_BASELINE_FACTOR, TEXT_LINE_HEIGHT_FACTOR, textShapeBounds, } from '../../annotation/canvas/textGeometry.js';
9
+ import { normalizeRect, placementOf, } from '../../annotation/canvas/measurementGeometry.js';
10
+ import { MEASUREMENT_LINE_COLOR, MEASUREMENT_LINE_WIDTH, MEASUREMENT_RECT_WIDTH, } from '../../annotation/canvas/measurementLineStyle.js';
11
+ import { parseHexColor } from './color.js';
12
+ import { circlePathOps, fillStateOps, flatPolylinePathOps, polylinePathOps, rectPathOps, strokeStateOps, } from './emit.js';
13
+ import { boundsToRect } from './pageMapping.js';
14
+ // Readable glyph colour over a highlight band — mirror of
15
+ // ShapeElement.highlightTextColor (which lives behind a Skia import).
16
+ const highlightTextColor = (hex) => {
17
+ const h = hex.replace('#', '');
18
+ if (h.length < 6)
19
+ return '#1A1A1A';
20
+ const r = parseInt(h.slice(0, 2), 16);
21
+ const g = parseInt(h.slice(2, 4), 16);
22
+ const b = parseInt(h.slice(4, 6), 16);
23
+ const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
24
+ return lum > 0.6 ? '#1A1A1A' : '#FFFFFF';
25
+ };
26
+ const arrowheadOps = (tip, from, width, color, gs) => {
27
+ if (!tip || !from || (tip.x === from.x && tip.y === from.y))
28
+ return [];
29
+ const tri = arrowheadTriangle(tip, from, width);
30
+ return [
31
+ pushGraphicsState(),
32
+ ...fillStateOps(color, gs),
33
+ ...polylinePathOps(tri, true),
34
+ fill(),
35
+ popGraphicsState(),
36
+ ];
37
+ };
38
+ /* ------------------------------------------------------------------ *
39
+ * Strokes
40
+ * ------------------------------------------------------------------ */
41
+ export const strokeBounds = (s) => {
42
+ if (s.points.length < 2)
43
+ return null;
44
+ let minX = Infinity;
45
+ let minY = Infinity;
46
+ let maxX = -Infinity;
47
+ let maxY = -Infinity;
48
+ for (let i = 0; i + 1 < s.points.length; i += 2) {
49
+ const x = s.points[i];
50
+ const y = s.points[i + 1];
51
+ if (x < minX)
52
+ minX = x;
53
+ if (y < minY)
54
+ minY = y;
55
+ if (x > maxX)
56
+ maxX = x;
57
+ if (y > maxY)
58
+ maxY = y;
59
+ }
60
+ // Round caps and arrowheads reach past the points by ~2× the width.
61
+ return boundsToRect({ minX, minY, maxX, maxY }, Math.max(s.width * 2, 6));
62
+ };
63
+ export const strokeOps = (s, ctx) => {
64
+ const n = s.points.length;
65
+ if (n < 2)
66
+ return [];
67
+ const opacity = s.tool === 'highlighter' ? 0.3 : 1;
68
+ const alpha = opacity * parseHexColor(s.color).alpha;
69
+ const gs = ctx.scope.alphaState(alpha);
70
+ const isDot = n === 4 && s.points[0] === s.points[2] && s.points[1] === s.points[3];
71
+ if (isDot) {
72
+ // Skia paints a zero-length round-capped contour as a dot; PDF only does
73
+ // so for round caps, and not reliably across viewers — draw it explicitly.
74
+ const x = s.points[0];
75
+ const y = s.points[1];
76
+ const r = s.width / 2;
77
+ const cap = toSkiaStrokeCap(s.cap);
78
+ if (cap === 'butt')
79
+ return [];
80
+ return [
81
+ pushGraphicsState(),
82
+ ...fillStateOps(s.color, gs),
83
+ ...(cap === 'square'
84
+ ? rectPathOps(x - r, y - r, s.width, s.width)
85
+ : circlePathOps(x, y, r)),
86
+ fill(),
87
+ popGraphicsState(),
88
+ ];
89
+ }
90
+ const ops = [
91
+ pushGraphicsState(),
92
+ ...strokeStateOps({
93
+ color: s.color,
94
+ width: s.width,
95
+ cap: s.cap,
96
+ join: 'round',
97
+ dash: !!s.dash,
98
+ graphicsState: gs,
99
+ }),
100
+ ...flatPolylinePathOps(s.points),
101
+ stroke(),
102
+ popGraphicsState(),
103
+ ];
104
+ if (n >= 4) {
105
+ const first = { x: s.points[0], y: s.points[1] };
106
+ const second = { x: s.points[2], y: s.points[3] };
107
+ const last = { x: s.points[n - 2], y: s.points[n - 1] };
108
+ const penult = { x: s.points[n - 4], y: s.points[n - 3] };
109
+ if (s.startCap === 'arrow') {
110
+ ops.push(...arrowheadOps(first, second, s.width, s.color, gs));
111
+ }
112
+ if (s.cap === 'arrow') {
113
+ ops.push(...arrowheadOps(last, penult, s.width, s.color, gs));
114
+ }
115
+ }
116
+ return ops;
117
+ };
118
+ /* ------------------------------------------------------------------ *
119
+ * Shapes
120
+ * ------------------------------------------------------------------ */
121
+ export const shapeBounds = (shape) => {
122
+ if (shape.kind === 'text') {
123
+ const b = textShapeBounds(shape);
124
+ return b ? boundsToRect(b, 4) : null;
125
+ }
126
+ const b = visualShapeBounds(shape);
127
+ if (!b)
128
+ return null;
129
+ const w = shape.style.strokeWidth ?? 2;
130
+ return boundsToRect(b, Math.max(w * 2, 6));
131
+ };
132
+ const textShapeOps = (shape, ctx) => {
133
+ const origin = shape.geometry.points[0];
134
+ if (!origin || !shape.text)
135
+ return [];
136
+ const fontSize = shape.style.fontSize ?? DEFAULT_TEXT_FONT_SIZE;
137
+ const colorStr = shape.style.stroke ?? '#000000';
138
+ const decoration = shape.style.textDecoration;
139
+ const glyphColor = decoration === 'highlight' ? highlightTextColor(colorStr) : colorStr;
140
+ const gs = ctx.scope.alphaState(parseHexColor(colorStr).alpha);
141
+ const lineHeight = TEXT_LINE_HEIGHT_FACTOR * fontSize;
142
+ const ops = [];
143
+ let lost = false;
144
+ shape.text.split('\n').forEach((line, i) => {
145
+ const top = origin.y + i * lineHeight;
146
+ const baseline = top + TEXT_BASELINE_FACTOR * fontSize;
147
+ const picked = ctx.fonts.pick(line);
148
+ if (picked.dropped)
149
+ lost = true;
150
+ const width = picked.text ? ctx.fonts.width(picked.text, fontSize) : 0;
151
+ if (decoration === 'highlight' && width > 0) {
152
+ ops.push(pushGraphicsState(), ...fillStateOps(colorStr, gs), rectangle(origin.x, top, width, lineHeight), fill(), popGraphicsState());
153
+ }
154
+ ops.push(...ctx.fonts.textOps(ctx.scope, line, fontSize, glyphColor, origin.x, baseline, {
155
+ graphicsState: gs,
156
+ }));
157
+ if ((decoration === 'underline' || decoration === 'squiggle') &&
158
+ width > 0) {
159
+ // Skia positions the underline just under the baseline at ~1/14 em; the
160
+ // wavy variant has no PDF operator and is drawn solid.
161
+ const thickness = fontSize / 14;
162
+ ops.push(pushGraphicsState(), ...fillStateOps(colorStr, gs), rectangle(origin.x, baseline + fontSize * 0.08, width, thickness), fill(), popGraphicsState());
163
+ }
164
+ });
165
+ if (lost) {
166
+ ctx.skipped.push({
167
+ id: shape.id,
168
+ kind: 'text',
169
+ reason: 'Some characters have no glyph in the export fonts and were dropped',
170
+ });
171
+ }
172
+ return ops;
173
+ };
174
+ export const shapeOps = (shape, ctx) => {
175
+ const { kind, geometry, style } = shape;
176
+ if (kind === 'text')
177
+ return textShapeOps(shape, ctx);
178
+ const strokeColor = style.stroke ?? '#000000';
179
+ const strokeWidth = style.strokeWidth ?? 2;
180
+ const gs = ctx.scope.alphaState(parseHexColor(strokeColor).alpha);
181
+ const closed = geometry.closed !== false;
182
+ const pts = geometry.points;
183
+ const strokeWith = (pathOps, join, cap) => [
184
+ pushGraphicsState(),
185
+ ...strokeStateOps({
186
+ color: strokeColor,
187
+ width: strokeWidth,
188
+ cap,
189
+ join,
190
+ dash: !!style.dash,
191
+ graphicsState: gs,
192
+ }),
193
+ ...pathOps,
194
+ stroke(),
195
+ popGraphicsState(),
196
+ ];
197
+ const fillWith = (pathOps) => style.fill
198
+ ? [
199
+ pushGraphicsState(),
200
+ ...fillStateOps(style.fill, ctx.scope.alphaState(parseHexColor(style.fill).alpha)),
201
+ ...pathOps,
202
+ fill(),
203
+ popGraphicsState(),
204
+ ]
205
+ : [];
206
+ switch (kind) {
207
+ case 'rect': {
208
+ const [a, b] = pts;
209
+ if (!a || !b)
210
+ return [];
211
+ const x = Math.min(a.x, b.x);
212
+ const y = Math.min(a.y, b.y);
213
+ const w = Math.abs(b.x - a.x);
214
+ const h = Math.abs(b.y - a.y);
215
+ return [
216
+ ...fillWith(rectPathOps(x, y, w, h)),
217
+ ...strokeWith(rectPathOps(x, y, w, h), 'miter'),
218
+ ];
219
+ }
220
+ case 'ellipse': {
221
+ // Rendered as a CIRCLE: centre = midpoint, r = half the larger extent.
222
+ const [a, b] = pts;
223
+ if (!a || !b)
224
+ return [];
225
+ const cx = (a.x + b.x) / 2;
226
+ const cy = (a.y + b.y) / 2;
227
+ const r = Math.max(Math.abs(b.x - a.x), Math.abs(b.y - a.y)) / 2;
228
+ return [
229
+ ...fillWith(circlePathOps(cx, cy, r)),
230
+ ...strokeWith(circlePathOps(cx, cy, r), 'miter'),
231
+ ];
232
+ }
233
+ case 'line':
234
+ case 'arrow': {
235
+ const [a, b] = pts;
236
+ if (!a || !b)
237
+ return [];
238
+ const ops = strokeWith(polylinePathOps([a, b]), 'miter', style.cap);
239
+ if (style.startCap === 'arrow')
240
+ ops.push(...arrowheadOps(a, b, strokeWidth, strokeColor, gs));
241
+ if (kind === 'arrow' || style.cap === 'arrow') {
242
+ ops.push(...arrowheadOps(b, a, strokeWidth, strokeColor, gs));
243
+ }
244
+ return ops;
245
+ }
246
+ case 'polygon': {
247
+ if (pts.length < 2)
248
+ return [];
249
+ const ops = [];
250
+ if (closed)
251
+ ops.push(...fillWith(polylinePathOps(pts, true)));
252
+ ops.push(...strokeWith(polylinePathOps(pts, closed), 'round', style.cap));
253
+ if (!closed) {
254
+ const n = pts.length;
255
+ if (style.startCap === 'arrow')
256
+ ops.push(...arrowheadOps(pts[0], pts[1], strokeWidth, strokeColor, gs));
257
+ if (style.cap === 'arrow')
258
+ ops.push(...arrowheadOps(pts[n - 1], pts[n - 2], strokeWidth, strokeColor, gs));
259
+ }
260
+ return ops;
261
+ }
262
+ default:
263
+ return [];
264
+ }
265
+ };
266
+ /* ------------------------------------------------------------------ *
267
+ * Measurement annotation line / rectangle (the tile itself is in tiles.ts)
268
+ * ------------------------------------------------------------------ */
269
+ export const measurementMarkBounds = (placed) => {
270
+ const placement = placementOf(placed);
271
+ if (placement === 'rectangle' && placed.rect) {
272
+ const n = normalizeRect(placed.rect);
273
+ const w = placed.lineWidth ?? MEASUREMENT_RECT_WIDTH;
274
+ return boundsToRect(n, w);
275
+ }
276
+ if (placement === 'line' && placed.line) {
277
+ const { a, b } = placed.line;
278
+ const w = placed.lineWidth ?? MEASUREMENT_LINE_WIDTH;
279
+ return boundsToRect({
280
+ minX: Math.min(a.x, b.x),
281
+ minY: Math.min(a.y, b.y),
282
+ maxX: Math.max(a.x, b.x),
283
+ maxY: Math.max(a.y, b.y),
284
+ }, Math.max(w * 2, 6));
285
+ }
286
+ return null;
287
+ };
288
+ export const measurementMarkOps = (placed, ctx) => {
289
+ const placement = placementOf(placed);
290
+ if (placement === 'rectangle' && placed.rect) {
291
+ const n = normalizeRect(placed.rect);
292
+ const color = placed.lineColor ?? MEASUREMENT_LINE_COLOR;
293
+ const width = placed.lineWidth ?? MEASUREMENT_RECT_WIDTH;
294
+ return [
295
+ pushGraphicsState(),
296
+ ...strokeStateOps({
297
+ color,
298
+ width,
299
+ join: 'miter',
300
+ dash: !!placed.lineDash,
301
+ graphicsState: ctx.scope.alphaState(parseHexColor(color).alpha),
302
+ }),
303
+ ...rectPathOps(n.minX, n.minY, n.maxX - n.minX, n.maxY - n.minY),
304
+ stroke(),
305
+ popGraphicsState(),
306
+ ];
307
+ }
308
+ if (placement === 'line' && placed.line) {
309
+ const { a, b } = placed.line;
310
+ const color = placed.lineColor ?? MEASUREMENT_LINE_COLOR;
311
+ const width = placed.lineWidth ?? MEASUREMENT_LINE_WIDTH;
312
+ const gs = ctx.scope.alphaState(parseHexColor(color).alpha);
313
+ const ops = [
314
+ pushGraphicsState(),
315
+ ...strokeStateOps({
316
+ color,
317
+ width,
318
+ cap: placed.lineCap,
319
+ join: 'miter',
320
+ dash: !!placed.lineDash,
321
+ graphicsState: gs,
322
+ }),
323
+ ...polylinePathOps([a, b]),
324
+ stroke(),
325
+ popGraphicsState(),
326
+ ];
327
+ if (placed.lineStartCap === 'arrow')
328
+ ops.push(...arrowheadOps(a, b, width, color, gs));
329
+ if (placed.lineCap === 'arrow')
330
+ ops.push(...arrowheadOps(b, a, width, color, gs));
331
+ return ops;
332
+ }
333
+ return [];
334
+ };
335
+ // Re-exported so the renderer can size dash tests without a second import.
336
+ export { dashIntervals };
337
+ /* ------------------------------------------------------------------ *
338
+ * Marks: ops + bounds + annotation metadata, per element
339
+ * ------------------------------------------------------------------ */
340
+ const strokePaths = (s) => {
341
+ const pts = [];
342
+ for (let i = 0; i + 1 < s.points.length; i += 2) {
343
+ pts.push({ x: s.points[i], y: s.points[i + 1] });
344
+ }
345
+ return pts;
346
+ };
347
+ export const strokeMark = (s, ctx) => {
348
+ const bounds = strokeBounds(s);
349
+ if (!bounds)
350
+ return null;
351
+ const opacity = s.tool === 'highlighter' ? 0.3 : 1;
352
+ return {
353
+ id: s.id,
354
+ ops: strokeOps(s, ctx),
355
+ bounds,
356
+ annot: {
357
+ subtype: 'Ink',
358
+ color: s.color,
359
+ width: s.width,
360
+ alpha: opacity * parseHexColor(s.color).alpha,
361
+ paths: [strokePaths(s)],
362
+ },
363
+ };
364
+ };
365
+ export const shapeMark = (shape, ctx) => {
366
+ const bounds = shapeBounds(shape);
367
+ if (!bounds)
368
+ return null;
369
+ const ops = shapeOps(shape, ctx);
370
+ const { kind, geometry, style } = shape;
371
+ const color = style.stroke ?? '#000000';
372
+ const width = style.strokeWidth ?? 2;
373
+ const alpha = parseHexColor(color).alpha;
374
+ const dash = !!style.dash;
375
+ const pts = geometry.points;
376
+ let annot;
377
+ switch (kind) {
378
+ case 'rect':
379
+ case 'ellipse': {
380
+ const b = visualShapeBounds(shape);
381
+ annot = {
382
+ subtype: kind === 'rect' ? 'Square' : 'Circle',
383
+ color,
384
+ fill: style.fill,
385
+ width,
386
+ alpha,
387
+ dash,
388
+ rect: boundsToRect(b),
389
+ };
390
+ break;
391
+ }
392
+ case 'line':
393
+ case 'arrow':
394
+ annot = {
395
+ subtype: 'Line',
396
+ color,
397
+ width,
398
+ alpha,
399
+ dash,
400
+ a: pts[0],
401
+ b: pts[1],
402
+ startArrow: style.startCap === 'arrow',
403
+ endArrow: kind === 'arrow' || style.cap === 'arrow',
404
+ };
405
+ break;
406
+ case 'polygon': {
407
+ const closed = geometry.closed !== false;
408
+ annot = {
409
+ subtype: closed ? 'Polygon' : 'PolyLine',
410
+ color,
411
+ fill: closed ? style.fill : undefined,
412
+ width,
413
+ alpha,
414
+ dash,
415
+ vertices: pts,
416
+ startArrow: !closed && style.startCap === 'arrow',
417
+ endArrow: !closed && style.cap === 'arrow',
418
+ };
419
+ break;
420
+ }
421
+ case 'text':
422
+ annot = {
423
+ subtype: 'FreeText',
424
+ color,
425
+ fontSize: style.fontSize ?? DEFAULT_TEXT_FONT_SIZE,
426
+ text: shape.text ?? '',
427
+ alpha,
428
+ };
429
+ break;
430
+ default:
431
+ return null;
432
+ }
433
+ return { id: shape.id, ops, bounds, annot };
434
+ };
435
+ /** The measurement annotation's line / rectangle (not its tile). */
436
+ export const measurementMark = (placed, ctx) => {
437
+ const bounds = measurementMarkBounds(placed);
438
+ if (!bounds)
439
+ return null;
440
+ const ops = measurementMarkOps(placed, ctx);
441
+ const color = placed.lineColor ?? MEASUREMENT_LINE_COLOR;
442
+ const alpha = parseHexColor(color).alpha;
443
+ const dash = !!placed.lineDash;
444
+ if (placementOf(placed) === 'rectangle' && placed.rect) {
445
+ const n = normalizeRect(placed.rect);
446
+ return {
447
+ id: `${placed.id}:rect`,
448
+ ops,
449
+ bounds,
450
+ annot: {
451
+ subtype: 'Square',
452
+ color,
453
+ width: placed.lineWidth ?? MEASUREMENT_RECT_WIDTH,
454
+ alpha,
455
+ dash,
456
+ rect: boundsToRect(n),
457
+ },
458
+ };
459
+ }
460
+ if (placementOf(placed) === 'line' && placed.line) {
461
+ return {
462
+ id: `${placed.id}:line`,
463
+ ops,
464
+ bounds,
465
+ annot: {
466
+ subtype: 'Line',
467
+ color,
468
+ width: placed.lineWidth ?? MEASUREMENT_LINE_WIDTH,
469
+ alpha,
470
+ dash,
471
+ a: placed.line.a,
472
+ b: placed.line.b,
473
+ startArrow: placed.lineStartCap === 'arrow',
474
+ endArrow: placed.lineCap === 'arrow',
475
+ },
476
+ };
477
+ }
478
+ return null;
479
+ };