@jasy/pdf 1.0.0-alpha.2 → 1.0.0-alpha.3

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 (139) hide show
  1. package/README.md +3 -3
  2. package/dist/api/args.d.ts +1 -1
  3. package/dist/api/args.js +2 -5
  4. package/dist/api/color.d.ts +4 -4
  5. package/dist/api/color.js +11 -17
  6. package/dist/api/content.d.ts +8 -8
  7. package/dist/api/content.js +23 -24
  8. package/dist/api/descriptor.d.ts +2 -2
  9. package/dist/api/descriptor.js +75 -31
  10. package/dist/api/index.d.ts +8 -8
  11. package/dist/api/index.js +8 -24
  12. package/dist/api/insets.js +4 -8
  13. package/dist/api/layout.d.ts +18 -16
  14. package/dist/api/layout.js +41 -52
  15. package/dist/api/structure.d.ts +60 -13
  16. package/dist/api/structure.js +132 -88
  17. package/dist/api/table.d.ts +5 -5
  18. package/dist/api/table.js +28 -24
  19. package/dist/api/text.d.ts +27 -2
  20. package/dist/api/text.js +45 -27
  21. package/dist/assets/font-data.d.ts +2 -0
  22. package/dist/assets/font-data.js +6 -0
  23. package/dist/assets/font-data.ts +7 -0
  24. package/dist/common/color.js +1 -5
  25. package/dist/constants/page-sizes.js +3 -6
  26. package/dist/constants/pdf-parts.js +1 -4
  27. package/dist/elements/container-element.d.ts +4 -4
  28. package/dist/elements/container-element.js +9 -13
  29. package/dist/elements/image-element.d.ts +18 -2
  30. package/dist/elements/image-element.js +81 -105
  31. package/dist/elements/index.d.ts +12 -11
  32. package/dist/elements/index.js +12 -29
  33. package/dist/elements/layout/default-text-style-element.d.ts +30 -0
  34. package/dist/elements/layout/default-text-style-element.js +47 -0
  35. package/dist/elements/layout/deferred-element.d.ts +3 -3
  36. package/dist/elements/layout/deferred-element.js +4 -8
  37. package/dist/elements/layout/expanded-element.d.ts +3 -3
  38. package/dist/elements/layout/expanded-element.js +10 -14
  39. package/dist/elements/layout/padding-element.d.ts +3 -3
  40. package/dist/elements/layout/padding-element.js +9 -14
  41. package/dist/elements/layout/positioned-element.d.ts +17 -4
  42. package/dist/elements/layout/positioned-element.js +29 -25
  43. package/dist/elements/layout/repeating-header-element.d.ts +3 -3
  44. package/dist/elements/layout/repeating-header-element.js +8 -12
  45. package/dist/elements/layout/sized-container-element.d.ts +2 -2
  46. package/dist/elements/layout/sized-container-element.js +6 -11
  47. package/dist/elements/line-element.d.ts +3 -3
  48. package/dist/elements/line-element.js +5 -10
  49. package/dist/elements/page-element.d.ts +8 -6
  50. package/dist/elements/page-element.js +20 -23
  51. package/dist/elements/pdf-document-element.d.ts +10 -4
  52. package/dist/elements/pdf-document-element.js +11 -10
  53. package/dist/elements/pdf-element.d.ts +12 -3
  54. package/dist/elements/pdf-element.js +10 -19
  55. package/dist/elements/rectangle-element.d.ts +5 -5
  56. package/dist/elements/rectangle-element.js +19 -25
  57. package/dist/elements/row-element.d.ts +3 -3
  58. package/dist/elements/row-element.js +7 -11
  59. package/dist/elements/text-element.d.ts +37 -11
  60. package/dist/elements/text-element.js +64 -39
  61. package/dist/index.d.ts +3 -3
  62. package/dist/index.js +3 -19
  63. package/dist/ir/display-list.d.ts +4 -2
  64. package/dist/ir/display-list.js +1 -2
  65. package/dist/layout/box-constraints.js +2 -6
  66. package/dist/layout/fragmentation.d.ts +8 -1
  67. package/dist/layout/fragmentation.js +22 -10
  68. package/dist/platform/browser-fs.d.ts +2 -0
  69. package/dist/platform/browser-fs.js +9 -0
  70. package/dist/platform/browser-image.d.ts +5 -0
  71. package/dist/platform/browser-image.js +13 -0
  72. package/dist/platform/node-fs.d.ts +2 -0
  73. package/dist/platform/node-fs.js +10 -0
  74. package/dist/platform/node-image.d.ts +5 -0
  75. package/dist/platform/node-image.js +9 -0
  76. package/dist/renderer/container-renderer.d.ts +3 -3
  77. package/dist/renderer/container-renderer.js +12 -27
  78. package/dist/renderer/default-text-style-renderer.d.ts +6 -0
  79. package/dist/renderer/default-text-style-renderer.js +10 -0
  80. package/dist/renderer/deferred-renderer.d.ts +3 -3
  81. package/dist/renderer/deferred-renderer.js +8 -23
  82. package/dist/renderer/expanded-renderer.d.ts +3 -3
  83. package/dist/renderer/expanded-renderer.js +6 -21
  84. package/dist/renderer/image-renderer.d.ts +3 -3
  85. package/dist/renderer/image-renderer.js +77 -75
  86. package/dist/renderer/index.d.ts +10 -10
  87. package/dist/renderer/index.js +10 -26
  88. package/dist/renderer/line-renderer.d.ts +3 -3
  89. package/dist/renderer/line-renderer.js +13 -28
  90. package/dist/renderer/padding-renderer.d.ts +3 -3
  91. package/dist/renderer/padding-renderer.js +6 -21
  92. package/dist/renderer/page-renderer.d.ts +2 -2
  93. package/dist/renderer/page-renderer.js +61 -77
  94. package/dist/renderer/pdf-backend.d.ts +2 -2
  95. package/dist/renderer/pdf-backend.js +21 -19
  96. package/dist/renderer/pdf-config.js +4 -7
  97. package/dist/renderer/pdf-document-class.d.ts +5 -5
  98. package/dist/renderer/pdf-document-class.js +24 -41
  99. package/dist/renderer/pdf-document-renderer.d.ts +3 -3
  100. package/dist/renderer/pdf-document-renderer.js +71 -85
  101. package/dist/renderer/pdf-renderer.d.ts +2 -2
  102. package/dist/renderer/pdf-renderer.js +83 -93
  103. package/dist/renderer/positioned-renderer.d.ts +3 -3
  104. package/dist/renderer/positioned-renderer.js +8 -23
  105. package/dist/renderer/rectangle-renderer.d.ts +3 -3
  106. package/dist/renderer/rectangle-renderer.js +45 -52
  107. package/dist/renderer/repeating-header-renderer.d.ts +3 -3
  108. package/dist/renderer/repeating-header-renderer.js +11 -26
  109. package/dist/renderer/row-renderer.d.ts +3 -3
  110. package/dist/renderer/row-renderer.js +12 -27
  111. package/dist/renderer/text-renderer.d.ts +6 -5
  112. package/dist/renderer/text-renderer.js +33 -42
  113. package/dist/text/line-breaker.d.ts +8 -5
  114. package/dist/text/line-breaker.js +67 -16
  115. package/dist/text/text-style.d.ts +25 -0
  116. package/dist/text/text-style.js +29 -0
  117. package/dist/utils/afm-parser.js +3 -13
  118. package/dist/utils/bytes.d.ts +24 -0
  119. package/dist/utils/bytes.js +76 -0
  120. package/dist/utils/flex-layout.d.ts +2 -2
  121. package/dist/utils/flex-layout.js +15 -20
  122. package/dist/utils/font-metrics.d.ts +1 -1
  123. package/dist/utils/font-metrics.js +1 -2
  124. package/dist/utils/font-path.js +3 -6
  125. package/dist/utils/image-helper.d.ts +6 -5
  126. package/dist/utils/image-helper.js +101 -111
  127. package/dist/utils/md5.d.ts +4 -0
  128. package/dist/utils/md5.js +80 -0
  129. package/dist/utils/pdf-object-manager.d.ts +10 -6
  130. package/dist/utils/pdf-object-manager.js +89 -94
  131. package/dist/utils/renderer-registry.js +1 -5
  132. package/dist/utils/ttf-parser.d.ts +2 -2
  133. package/dist/utils/ttf-parser.js +32 -36
  134. package/dist/utils/ttf-subsetter.d.ts +1 -1
  135. package/dist/utils/ttf-subsetter.js +40 -42
  136. package/dist/utils/utf8-to-windows1252-encoder.js +1 -4
  137. package/dist/validators/element-validator.d.ts +2 -2
  138. package/dist/validators/element-validator.js +9 -13
  139. package/package.json +14 -2
@@ -1,30 +1,15 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.RowRenderer = void 0;
13
- const renderer_registry_1 = require("../utils/renderer-registry");
14
- class RowRenderer {
15
- static render(rowElement, objectManager) {
16
- return __awaiter(this, void 0, void 0, function* () {
17
- const { children } = rowElement.getProps();
18
- const nodes = [];
19
- if (children)
20
- for (const child of children) {
21
- const renderer = renderer_registry_1.RendererRegistry.getRenderer(child);
22
- if (renderer) {
23
- nodes.push(...(yield renderer(child, objectManager)));
24
- }
1
+ import { RendererRegistry } from "../utils/renderer-registry.js";
2
+ export class RowRenderer {
3
+ static async render(rowElement, objectManager) {
4
+ const { children } = rowElement.getProps();
5
+ const nodes = [];
6
+ if (children)
7
+ for (const child of children) {
8
+ const renderer = RendererRegistry.getRenderer(child);
9
+ if (renderer) {
10
+ nodes.push(...(await renderer(child, objectManager)));
25
11
  }
26
- return nodes;
27
- });
12
+ }
13
+ return nodes;
28
14
  }
29
15
  }
30
- exports.RowRenderer = RowRenderer;
@@ -1,9 +1,10 @@
1
- import { TextElement, TextSegment } from "../elements/text-element";
2
- import { FontStyle, PDFObjectManager } from "../utils/pdf-object-manager";
3
- import type { FontMetrics } from "../utils/font-metrics";
4
- import { IRNode } from "../ir/display-list";
1
+ import { TextElement, TextSegment } from "../elements/text-element.js";
2
+ import { FontStyle, PDFObjectManager } from "../utils/pdf-object-manager.js";
3
+ import type { FontMetrics } from "../utils/font-metrics.js";
4
+ import { IRNode } from "../ir/display-list.js";
5
+ import { TextOverflow } from "../text/line-breaker.js";
5
6
  export declare class TextRenderer {
6
- static calculateTextHeight(content: string | TextSegment[], fontSize: number, fontFamily: string, fontStyle: FontStyle, objectManager: FontMetrics, maxWidth: number): number;
7
+ static calculateTextHeight(content: string | TextSegment[], fontSize: number, fontFamily: string, fontStyle: FontStyle, objectManager: FontMetrics, maxWidth: number, maxLines?: number, overflow?: TextOverflow, lineHeight?: number): number;
7
8
  static render(textElement: TextElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
8
9
  private static _buildRuns;
9
10
  }
@@ -1,52 +1,38 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.TextRenderer = void 0;
13
- const pdf_element_1 = require("../elements/pdf-element");
14
- const line_breaker_1 = require("../text/line-breaker");
1
+ import { HorizontalAlignment } from "../elements/pdf-element.js";
2
+ import { wrapStringIntoLines, breakSegmentsIntoLines, } from "../text/line-breaker.js";
15
3
  // Distance from the top of a line down to its baseline, as a fraction of the font
16
4
  // size. ~0.683 is the standard-14 ascent ratio used to seat the first baseline.
17
5
  const BASELINE_RATIO = 683 / 1000;
18
- class TextRenderer {
6
+ export class TextRenderer {
19
7
  // Measuring only needs metrics, not the full object manager. (The render pass below
20
8
  // still receives the manager because it also registers fonts/images.)
21
- static calculateTextHeight(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth) {
22
- // Plain string: one line height per wrapped line.
9
+ static calculateTextHeight(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth, maxLines, overflow, lineHeight = 1) {
10
+ // Plain string: one line box (fontSize * lineHeight) per wrapped line.
23
11
  if (typeof content === "string") {
24
- const lines = (0, line_breaker_1.wrapStringIntoLines)(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager);
25
- return lines.length * fontSize;
12
+ const lines = wrapStringIntoLines(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager, maxLines, overflow);
13
+ return lines.length * fontSize * lineHeight;
26
14
  }
27
- // Segments: each line contributes its own (tallest-on-line) leading.
28
- const lines = (0, line_breaker_1.breakSegmentsIntoLines)(content, { fontFamily, fontSize, fontStyle }, maxWidth, objectManager);
29
- return lines.reduce((total, line) => total + line.maxFontSize, 0);
15
+ // Segments: each line contributes its own (tallest-on-line) leading, scaled by lineHeight.
16
+ const lines = breakSegmentsIntoLines(content, { fontFamily, fontSize, fontStyle }, maxWidth, objectManager, maxLines, overflow);
17
+ return lines.reduce((total, line) => total + line.maxFontSize * lineHeight, 0);
30
18
  }
31
- static render(textElement, objectManager) {
32
- return __awaiter(this, void 0, void 0, function* () {
33
- const { x, y, width, fontSize, color, content, fontFamily, fontStyle, textAlignment } = textElement.getProps();
34
- // Component -> display list. Wrapping and positioning stay here; the backend
35
- // turns each run into BT/Tf/Td/Tj/ET. The wrapping algorithm is unchanged from
36
- // the original renderer - unifying it into the engine is Phase 3.
37
- return TextRenderer._buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, width !== null && width !== void 0 ? width : Number.NaN, textAlignment, color, x, y);
38
- });
19
+ static async render(textElement, objectManager) {
20
+ const { x, y, width, fontSize, color, content, fontFamily, fontStyle, textAlignment, maxLines, overflow, lineHeight, } = textElement.getProps();
21
+ // Component -> display list. Wrapping and positioning stay here; the backend
22
+ // turns each run into BT/Tf/Td/Tj/ET. The wrapping algorithm is unchanged from
23
+ // the original renderer - unifying it into the engine is Phase 3.
24
+ return TextRenderer._buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, width ?? Number.NaN, textAlignment, color, x, y, maxLines, overflow, lineHeight);
39
25
  }
40
26
  // Lay the content out into absolutely-positioned text runs. Glyph positions match
41
27
  // the previous hand-written operators exactly (verified by pixel-identical render);
42
28
  // only the output form changed from PDF strings to `TextRun`s.
43
- static _buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth, textAlignment, color, initialX, yPosition) {
29
+ static _buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth, textAlignment, color, initialX, yPosition, maxLines, overflow, lineHeight = 1) {
44
30
  const runs = [];
45
31
  // Horizontal offset of a line of the given width under the current alignment.
46
32
  const alignmentOffset = (lineWidth) => {
47
- if (textAlignment === pdf_element_1.HorizontalAlignment.center)
33
+ if (textAlignment === HorizontalAlignment.center)
48
34
  return (maxWidth - lineWidth) / 2;
49
- if (textAlignment === pdf_element_1.HorizontalAlignment.right)
35
+ if (textAlignment === HorizontalAlignment.right)
50
36
  return maxWidth - lineWidth;
51
37
  return 0;
52
38
  };
@@ -62,16 +48,19 @@ class TextRenderer {
62
48
  };
63
49
  // --- Plain string: one run per wrapped line. ---
64
50
  if (typeof content === "string") {
65
- const lines = (0, line_breaker_1.wrapStringIntoLines)(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager);
66
- // yPosition is the top of the text box (top-left); seat line 0's baseline below
67
- // it, then step DOWN per line. The seam flips the whole thing to PDF space.
68
- const baseline = yPosition + fontSize * BASELINE_RATIO;
51
+ const lines = wrapStringIntoLines(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager, maxLines, overflow);
52
+ // yPosition is the top of the text box (top-left); seat line 0's baseline below it, then step
53
+ // DOWN by one line box (fontSize * lineHeight) per line. The lineHeight EXTRA leading is split
54
+ // half above / half below (CSS/Flutter "half-leading"), so the text sits centered in its line
55
+ // box instead of clinging to the top. At lineHeight 1 the half-leading is 0 -> byte-identical.
56
+ const halfLeading = (fontSize * (lineHeight - 1)) / 2;
57
+ const baseline = yPosition + halfLeading + fontSize * BASELINE_RATIO;
69
58
  lines.forEach((line, index) => {
70
59
  const lineWidth = objectManager.getStringWidth(line, fontFamily, fontSize, fontStyle);
71
60
  runs.push({
72
61
  type: "text",
73
62
  x: initialX + alignmentOffset(lineWidth),
74
- y: baseline + fontSize * index,
63
+ y: baseline + fontSize * lineHeight * index,
75
64
  text: line,
76
65
  fontFamily,
77
66
  fontStyle,
@@ -115,11 +104,13 @@ class TextRenderer {
115
104
  overallMaxFont = size;
116
105
  }
117
106
  let lineY = yPosition + overallMaxFont * BASELINE_RATIO;
118
- for (const line of (0, line_breaker_1.breakSegmentsIntoLines)(content, { fontFamily, fontSize, fontStyle }, maxWidth, objectManager)) {
119
- pushLine(line, lineY);
120
- lineY += line.maxFontSize;
107
+ for (const line of breakSegmentsIntoLines(content, { fontFamily, fontSize, fontStyle }, maxWidth, objectManager, maxLines, overflow)) {
108
+ // Half-leading: shift this line's baseline down by half its own extra leading, so the line
109
+ // sits centered in its box (CSS/Flutter). At lineHeight 1 the shift is 0 -> byte-identical.
110
+ const halfLeading = (line.maxFontSize * (lineHeight - 1)) / 2;
111
+ pushLine(line, lineY + halfLeading);
112
+ lineY += line.maxFontSize * lineHeight;
121
113
  }
122
114
  return runs;
123
115
  }
124
116
  }
125
- exports.TextRenderer = TextRenderer;
@@ -1,12 +1,15 @@
1
- import type { FontStyle } from "../utils/pdf-object-manager";
2
- import type { FontMetrics } from "../utils/font-metrics";
3
- import type { TextSegment } from "../elements/text-element";
1
+ import type { FontStyle } from "../utils/pdf-object-manager.js";
2
+ import type { FontMetrics } from "../utils/font-metrics.js";
3
+ import type { TextSegment } from "../elements/text-element.js";
4
4
  /** Default font for segments that don't override it. */
5
5
  export interface SegmentDefaults {
6
6
  fontFamily: string;
7
7
  fontSize: number;
8
8
  fontStyle: FontStyle;
9
9
  }
10
+ /** What happens to text beyond `maxLines`: `"clip"` drops it, `"ellipsis"` ends the last kept line
11
+ * with an ellipsis. Mirrors Flutter's `TextOverflow`. */
12
+ export type TextOverflow = "clip" | "ellipsis";
10
13
  /** One laid-out line of segments. `maxFontSize` is the tallest font ON THIS LINE - its
11
14
  * leading - matching how real engines (and this lib's plain-string path) space lines. */
12
15
  export interface SegmentLine {
@@ -22,14 +25,14 @@ export interface SegmentLine {
22
25
  * rendering call this, so they can never disagree. Depends only on `FontMetrics`,
23
26
  * not the PDF byte writer - the future fragmentation pass can reuse it.
24
27
  */
25
- export declare function wrapStringIntoLines(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle, maxWidth: number, metrics: FontMetrics): string[];
28
+ export declare function wrapStringIntoLines(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle, maxWidth: number, metrics: FontMetrics, maxLines?: number, overflow?: TextOverflow): string[];
26
29
  /**
27
30
  * Break styled segments into lines that fit within `maxWidth`. Same greedy
28
31
  * word-splitting as the string breaker, but each line records the tallest font on
29
32
  * THAT line as its leading (per-line, not a paragraph-global maximum). Single source
30
33
  * of truth: both height measurement and rendering call this.
31
34
  */
32
- export declare function breakSegmentsIntoLines(segments: TextSegment[], defaults: SegmentDefaults, maxWidth: number, metrics: FontMetrics): SegmentLine[];
35
+ export declare function breakSegmentsIntoLines(segments: TextSegment[], defaults: SegmentDefaults, maxWidth: number, metrics: FontMetrics, maxLines?: number, overflow?: TextOverflow): SegmentLine[];
33
36
  /**
34
37
  * Inverse of `breakSegmentsIntoLines`: flatten broken lines back into a `TextSegment[]`
35
38
  * that re-wraps to exactly those lines. The wrap consumed the space at each line break,
@@ -1,8 +1,6 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.wrapStringIntoLines = wrapStringIntoLines;
4
- exports.breakSegmentsIntoLines = breakSegmentsIntoLines;
5
- exports.segmentLinesToSegments = segmentLinesToSegments;
1
+ /** Three ASCII dots, NOT the "…" glyph (U+2026): plain dots encode in every font - standard-14
2
+ * (WinAnsi) and any embedded TTF - whereas U+2026 needs a glyph the font may not carry. */
3
+ const ELLIPSIS = "...";
6
4
  /**
7
5
  * Break a plain string into lines that each fit within `maxWidth`, splitting on
8
6
  * spaces (greedy: a word stays on the current line unless it would overflow).
@@ -11,7 +9,7 @@ exports.segmentLinesToSegments = segmentLinesToSegments;
11
9
  * rendering call this, so they can never disagree. Depends only on `FontMetrics`,
12
10
  * not the PDF byte writer - the future fragmentation pass can reuse it.
13
11
  */
14
- function wrapStringIntoLines(text, fontFamily, fontSize, fontStyle, maxWidth, metrics) {
12
+ export function wrapStringIntoLines(text, fontFamily, fontSize, fontStyle, maxWidth, metrics, maxLines, overflow) {
15
13
  let currentLine = "";
16
14
  let currentWidth = 0;
17
15
  const lines = [];
@@ -19,7 +17,10 @@ function wrapStringIntoLines(text, fontFamily, fontSize, fontStyle, maxWidth, me
19
17
  words.forEach((word, index) => {
20
18
  const wordWidth = metrics.getStringWidth(word, fontFamily, fontSize, fontStyle);
21
19
  const spaceWidth = metrics.getCharWidth(" ", fontSize, undefined, fontFamily, fontStyle);
22
- if (currentWidth + wordWidth > maxWidth) {
20
+ // Break before a word that won't fit - but only once the line has content. A single word wider
21
+ // than maxWidth must sit on its (empty) line and overflow, not push a phantom empty line before
22
+ // it (which would over-count the height by a line and shift the text down at render).
23
+ if (currentWidth + wordWidth > maxWidth && currentLine !== "") {
23
24
  lines.push(currentLine.trim());
24
25
  currentLine = word;
25
26
  currentWidth = wordWidth;
@@ -31,7 +32,15 @@ function wrapStringIntoLines(text, fontFamily, fontSize, fontStyle, maxWidth, me
31
32
  });
32
33
  if (currentLine)
33
34
  lines.push(currentLine.trim());
34
- return lines;
35
+ // Open-end by default; cap only when maxLines is set (the others get undefined → untouched).
36
+ if (maxLines == null || lines.length <= maxLines)
37
+ return lines;
38
+ const kept = lines.slice(0, maxLines);
39
+ if (overflow === "ellipsis") {
40
+ const last = kept.length - 1;
41
+ kept[last] = ellipsize(kept[last], fontFamily, fontSize, fontStyle, maxWidth, metrics);
42
+ }
43
+ return kept;
35
44
  }
36
45
  /**
37
46
  * Break styled segments into lines that fit within `maxWidth`. Same greedy
@@ -39,7 +48,7 @@ function wrapStringIntoLines(text, fontFamily, fontSize, fontStyle, maxWidth, me
39
48
  * THAT line as its leading (per-line, not a paragraph-global maximum). Single source
40
49
  * of truth: both height measurement and rendering call this.
41
50
  */
42
- function breakSegmentsIntoLines(segments, defaults, maxWidth, metrics) {
51
+ export function breakSegmentsIntoLines(segments, defaults, maxWidth, metrics, maxLines, overflow) {
43
52
  const lines = [];
44
53
  let width = 0;
45
54
  let maxFontSize = 0; // per line: starts at 0, grows to the tallest font on the line
@@ -54,13 +63,15 @@ function breakSegmentsIntoLines(segments, defaults, maxWidth, metrics) {
54
63
  // Start this segment's piece empty; its content is filled word-by-word below. (Not
55
64
  // the original content - otherwise a segment whose FIRST word overflows would carry
56
65
  // its whole text into the line that just closed.)
57
- lineSegments.push(Object.assign(Object.assign({}, segment), { fontFamily: family, content: "" }));
66
+ lineSegments.push({ ...segment, fontFamily: family, content: "" });
58
67
  combined = "";
59
68
  if (maxFontSize < size)
60
69
  maxFontSize = size;
61
70
  words.forEach((word, wordIndex) => {
62
71
  const wordWidth = metrics.getStringWidth(word, family, size, style);
63
- if (width + wordWidth > maxWidth) {
72
+ // Same guard as the string path: don't open a phantom empty line for an over-wide first word -
73
+ // place it (overflowing) on the current empty line instead.
74
+ if (width + wordWidth > maxWidth && width > 0) {
64
75
  lines.push({ segments: lineSegments, width, maxFontSize });
65
76
  // Start the next line; its leading resets to the wrapping segment's size.
66
77
  width = 0;
@@ -68,13 +79,13 @@ function breakSegmentsIntoLines(segments, defaults, maxWidth, metrics) {
68
79
  lineSegments = [];
69
80
  combined = word;
70
81
  width += wordWidth + spaceWidth;
71
- lineSegments.push(Object.assign(Object.assign({}, segment), { content: combined }));
82
+ lineSegments.push({ ...segment, content: combined });
72
83
  }
73
84
  else {
74
85
  combined += wordIndex === 0 ? word : " " + word;
75
86
  width += wordWidth + spaceWidth;
76
87
  if (lineSegments.length === 0) {
77
- lineSegments.push(Object.assign(Object.assign({}, segment), { fontFamily: family, content: combined }));
88
+ lineSegments.push({ ...segment, fontFamily: family, content: combined });
78
89
  }
79
90
  lineSegments[lineSegments.length - 1].content = combined;
80
91
  }
@@ -83,7 +94,13 @@ function breakSegmentsIntoLines(segments, defaults, maxWidth, metrics) {
83
94
  if (lineSegments.length > 0) {
84
95
  lines.push({ segments: lineSegments, width, maxFontSize });
85
96
  }
86
- return lines;
97
+ // Open-end by default; cap only when maxLines is set.
98
+ if (maxLines == null || lines.length <= maxLines)
99
+ return lines;
100
+ const kept = lines.slice(0, maxLines);
101
+ if (overflow === "ellipsis")
102
+ ellipsizeSegmentLine(kept[kept.length - 1], defaults, maxWidth, metrics);
103
+ return kept;
87
104
  }
88
105
  /**
89
106
  * Inverse of `breakSegmentsIntoLines`: flatten broken lines back into a `TextSegment[]`
@@ -92,10 +109,10 @@ function breakSegmentsIntoLines(segments, defaults, maxWidth, metrics) {
92
109
  * last word of a line and the first of the next would fuse ("a b" + "c d" -> "a bc d").
93
110
  * Used by text fragmentation to rebuild the fitted/remainder halves of a split paragraph.
94
111
  */
95
- function segmentLinesToSegments(lines) {
112
+ export function segmentLinesToSegments(lines) {
96
113
  const result = [];
97
114
  lines.forEach((line, lineIndex) => {
98
- line.segments.forEach((segment) => result.push(Object.assign({}, segment)));
115
+ line.segments.forEach((segment) => result.push({ ...segment }));
99
116
  if (lineIndex < lines.length - 1) {
100
117
  const last = result[result.length - 1];
101
118
  if (last && !last.content.endsWith(" "))
@@ -104,3 +121,37 @@ function segmentLinesToSegments(lines) {
104
121
  });
105
122
  return result;
106
123
  }
124
+ /** Appends "…" to a single line, dropping trailing words (then characters) until the line plus the
125
+ * ellipsis fits `maxWidth`. Falls back to a bare "…" if not even one character fits. */
126
+ function ellipsize(line, fontFamily, fontSize, fontStyle, maxWidth, metrics) {
127
+ const fits = (s) => metrics.getStringWidth(s + ELLIPSIS, fontFamily, fontSize, fontStyle) <= maxWidth;
128
+ if (fits(line))
129
+ return line + ELLIPSIS;
130
+ const words = line.split(" ");
131
+ while (words.length > 1) {
132
+ words.pop();
133
+ if (fits(words.join(" ")))
134
+ return words.join(" ") + ELLIPSIS;
135
+ }
136
+ let single = words[0] ?? "";
137
+ while (single.length > 1) {
138
+ single = single.slice(0, -1);
139
+ if (fits(single))
140
+ return single + ELLIPSIS;
141
+ }
142
+ return ELLIPSIS;
143
+ }
144
+ /** Ellipsizes the LAST segment of a truncated segment line in place (within the width left by the
145
+ * segments before it) and recomputes the line width. */
146
+ function ellipsizeSegmentLine(line, defaults, maxWidth, metrics) {
147
+ const segs = line.segments;
148
+ if (segs.length === 0)
149
+ return;
150
+ const widthOf = (seg) => metrics.getStringWidth(seg.content, seg.fontFamily || defaults.fontFamily, seg.fontSize || defaults.fontSize, seg.fontStyle || defaults.fontStyle);
151
+ let prefix = 0;
152
+ for (let i = 0; i < segs.length - 1; i++)
153
+ prefix += widthOf(segs[i]);
154
+ const last = segs[segs.length - 1];
155
+ last.content = ellipsize(last.content, last.fontFamily || defaults.fontFamily, last.fontSize || defaults.fontSize, last.fontStyle || defaults.fontStyle, maxWidth - prefix, metrics);
156
+ line.width = prefix + widthOf(last);
157
+ }
@@ -0,0 +1,25 @@
1
+ import { Color } from "../common/color.js";
2
+ import { FontStyle } from "../utils/pdf-object-manager.js";
3
+ import { HorizontalAlignment } from "../elements/pdf-element.js";
4
+ /**
5
+ * The inheritable text properties - the same set CSS and Flutter cascade. A `Text` resolves each of
6
+ * its own (possibly unset) properties against the nearest cascaded style: explicit > inherited >
7
+ * built-in default. Box/layout properties (padding, border, width, ...) are deliberately NOT here -
8
+ * they never inherit, exactly as in CSS.
9
+ */
10
+ export interface ResolvedTextStyle {
11
+ fontSize: number;
12
+ fontFamily: string;
13
+ fontStyle: FontStyle;
14
+ color: Color;
15
+ textAlignment: HorizontalAlignment;
16
+ lineHeight: number;
17
+ }
18
+ /**
19
+ * The root of the cascade: what a `Text` falls back to when neither it nor any ancestor sets a
20
+ * property. These MUST match the historical `Text` defaults so a document that sets nothing renders
21
+ * byte-for-byte as before.
22
+ */
23
+ export declare const DEFAULT_TEXT_STYLE: ResolvedTextStyle;
24
+ /** Layers a partial override onto a complete style; an unset (undefined) field keeps the base. */
25
+ export declare function mergeTextStyle(base: ResolvedTextStyle, override?: Partial<ResolvedTextStyle>): ResolvedTextStyle;
@@ -0,0 +1,29 @@
1
+ import { Color } from "../common/color.js";
2
+ import { FontStyle } from "../utils/pdf-object-manager.js";
3
+ import { HorizontalAlignment } from "../elements/pdf-element.js";
4
+ /**
5
+ * The root of the cascade: what a `Text` falls back to when neither it nor any ancestor sets a
6
+ * property. These MUST match the historical `Text` defaults so a document that sets nothing renders
7
+ * byte-for-byte as before.
8
+ */
9
+ export const DEFAULT_TEXT_STYLE = {
10
+ fontSize: 12,
11
+ fontFamily: "Helvetica",
12
+ fontStyle: FontStyle.Normal,
13
+ color: new Color(0, 0, 0),
14
+ textAlignment: HorizontalAlignment.left,
15
+ lineHeight: 1,
16
+ };
17
+ /** Layers a partial override onto a complete style; an unset (undefined) field keeps the base. */
18
+ export function mergeTextStyle(base, override) {
19
+ if (!override)
20
+ return base;
21
+ return {
22
+ fontSize: override.fontSize ?? base.fontSize,
23
+ fontFamily: override.fontFamily ?? base.fontFamily,
24
+ fontStyle: override.fontStyle ?? base.fontStyle,
25
+ color: override.color ?? base.color,
26
+ textAlignment: override.textAlignment ?? base.textAlignment,
27
+ lineHeight: override.lineHeight ?? base.lineHeight,
28
+ };
29
+ }
@@ -1,12 +1,5 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.AFMParser = void 0;
7
- const fs_1 = __importDefault(require("fs"));
8
- const path_1 = __importDefault(require("path"));
9
- class AFMParser {
1
+ import { AGL } from "../assets/font-data.js";
2
+ export class AFMParser {
10
3
  constructor(afmData) {
11
4
  this.advanceWidths = {};
12
5
  this.kerningPairs = {};
@@ -15,9 +8,7 @@ class AFMParser {
15
8
  this.loadGlyphList();
16
9
  }
17
10
  loadGlyphList() {
18
- const afmFilePath = path_1.default.resolve(__dirname, "../", "assets/agl.txt");
19
- const fileContent = fs_1.default.readFileSync(afmFilePath, "utf-8");
20
- const lines = fileContent.split("\n");
11
+ const lines = AGL.split("\n");
21
12
  for (const line of lines) {
22
13
  const parts = line.trim().split(";");
23
14
  if (parts.length >= 2) {
@@ -88,4 +79,3 @@ class AFMParser {
88
79
  return this.kerningPairs[pair] || 0;
89
80
  }
90
81
  }
91
- exports.AFMParser = AFMParser;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A `Uint8Array` → a latin1 (ISO-8859-1) string: each byte 0x00-0xFF maps 1:1 to the same code point.
3
+ * This matches `Buffer.toString("latin1")` exactly. NOTE: do NOT use `TextDecoder("latin1")` here - per
4
+ * the encoding spec that label is really windows-1252, which mangles 0x80-0x9F; we need a true 1:1 pass
5
+ * so arbitrary binary (e.g. a compressed stream) rides through unchanged. The PDF body is assembled as
6
+ * such a binary string and the final encoder passes 0x00-0xFF through.
7
+ */
8
+ export declare function latin1FromBytes(bytes: Uint8Array): string;
9
+ /**
10
+ * A latin1 (ISO-8859-1) string → a `Uint8Array`: each char's low byte. Matches `Buffer.from(str,
11
+ * "latin1")` / `"binary"`. Use for the binary strings the engine assembles char-by-char (CMaps, object
12
+ * bodies) before they become a stream.
13
+ */
14
+ export declare function bytesFromLatin1(str: string): Uint8Array;
15
+ export declare function u8(b: Uint8Array, o: number): number;
16
+ export declare function u16(b: Uint8Array, o: number): number;
17
+ export declare function u32(b: Uint8Array, o: number): number;
18
+ export declare function i16(b: Uint8Array, o: number): number;
19
+ export declare function wu16(b: Uint8Array, value: number, offset: number): void;
20
+ export declare function wu32(b: Uint8Array, value: number, offset: number): void;
21
+ export declare function wi16(b: Uint8Array, value: number, offset: number): void;
22
+ export declare function writeLatin1(b: Uint8Array, str: string, offset: number): void;
23
+ /** Concatenate Uint8Arrays into one (mirrors Buffer.concat). */
24
+ export declare function concatBytes(parts: Uint8Array[]): Uint8Array;
@@ -0,0 +1,76 @@
1
+ // Byte helpers for an isomorphic engine (Node + browser). Kept tiny + dependency-free; as the engine
2
+ // moves off Node-only APIs, the Node `Buffer` conveniences are replaced by these.
3
+ /**
4
+ * A `Uint8Array` → a latin1 (ISO-8859-1) string: each byte 0x00-0xFF maps 1:1 to the same code point.
5
+ * This matches `Buffer.toString("latin1")` exactly. NOTE: do NOT use `TextDecoder("latin1")` here - per
6
+ * the encoding spec that label is really windows-1252, which mangles 0x80-0x9F; we need a true 1:1 pass
7
+ * so arbitrary binary (e.g. a compressed stream) rides through unchanged. The PDF body is assembled as
8
+ * such a binary string and the final encoder passes 0x00-0xFF through.
9
+ */
10
+ export function latin1FromBytes(bytes) {
11
+ let out = "";
12
+ const CHUNK = 0x8000; // chunk the apply() to stay under the argument-count limit on big streams
13
+ for (let i = 0; i < bytes.length; i += CHUNK) {
14
+ out += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
15
+ }
16
+ return out;
17
+ }
18
+ /**
19
+ * A latin1 (ISO-8859-1) string → a `Uint8Array`: each char's low byte. Matches `Buffer.from(str,
20
+ * "latin1")` / `"binary"`. Use for the binary strings the engine assembles char-by-char (CMaps, object
21
+ * bodies) before they become a stream.
22
+ */
23
+ export function bytesFromLatin1(str) {
24
+ const u8a = new Uint8Array(str.length);
25
+ for (let i = 0; i < str.length; i++)
26
+ u8a[i] = str.charCodeAt(i) & 0xff;
27
+ return u8a;
28
+ }
29
+ // Big-endian integer reads from a Uint8Array (TrueType + PDF are big-endian), mirroring
30
+ // Buffer.readUInt8 / readUInt16BE / readUInt32BE / readInt16BE without needing a Buffer.
31
+ export function u8(b, o) {
32
+ return b[o];
33
+ }
34
+ export function u16(b, o) {
35
+ return (b[o] << 8) | b[o + 1];
36
+ }
37
+ export function u32(b, o) {
38
+ // Multiply the top byte (a left shift by 24 would go negative in 32-bit signed math).
39
+ return b[o] * 0x1000000 + ((b[o + 1] << 16) | (b[o + 2] << 8) | b[o + 3]);
40
+ }
41
+ export function i16(b, o) {
42
+ const v = (b[o] << 8) | b[o + 1];
43
+ return v & 0x8000 ? v - 0x10000 : v;
44
+ }
45
+ // Big-endian integer + latin1-string writes into a Uint8Array, mirroring Buffer.writeUInt16BE /
46
+ // writeUInt32BE / writeInt16BE / write(str, off, len, "latin1"). Arg order matches Buffer: (value, offset).
47
+ export function wu16(b, value, offset) {
48
+ b[offset] = (value >>> 8) & 0xff;
49
+ b[offset + 1] = value & 0xff;
50
+ }
51
+ export function wu32(b, value, offset) {
52
+ b[offset] = (value >>> 24) & 0xff;
53
+ b[offset + 1] = (value >>> 16) & 0xff;
54
+ b[offset + 2] = (value >>> 8) & 0xff;
55
+ b[offset + 3] = value & 0xff;
56
+ }
57
+ export function wi16(b, value, offset) {
58
+ wu16(b, value & 0xffff, offset);
59
+ }
60
+ export function writeLatin1(b, str, offset) {
61
+ for (let i = 0; i < str.length; i++)
62
+ b[offset + i] = str.charCodeAt(i) & 0xff;
63
+ }
64
+ /** Concatenate Uint8Arrays into one (mirrors Buffer.concat). */
65
+ export function concatBytes(parts) {
66
+ let len = 0;
67
+ for (const p of parts)
68
+ len += p.length;
69
+ const out = new Uint8Array(len);
70
+ let off = 0;
71
+ for (const p of parts) {
72
+ out.set(p, off);
73
+ off += p.length;
74
+ }
75
+ return out;
76
+ }
@@ -1,5 +1,5 @@
1
- import { PDFElement, LayoutContext } from "../elements/pdf-element";
2
- import { BoxConstraints, Offset, Size } from "../layout/box-constraints";
1
+ import { PDFElement, LayoutContext } from "../elements/pdf-element.js";
2
+ import { BoxConstraints, Offset, Size } from "../layout/box-constraints.js";
3
3
  /** Distribution of the children ALONG the stacking (main) axis when there is leftover
4
4
  * space and no flex child to absorb it. */
5
5
  export type MainAlign = "start" | "center" | "end" | "between" | "around";