@jasy/pdf 1.0.0-alpha.1

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 +171 -0
  2. package/dist/api/args.d.ts +10 -0
  3. package/dist/api/args.js +13 -0
  4. package/dist/api/color.d.ts +24 -0
  5. package/dist/api/color.js +215 -0
  6. package/dist/api/content.d.ts +36 -0
  7. package/dist/api/content.js +50 -0
  8. package/dist/api/descriptor.d.ts +25 -0
  9. package/dist/api/descriptor.js +71 -0
  10. package/dist/api/index.d.ts +8 -0
  11. package/dist/api/index.js +27 -0
  12. package/dist/api/insets.d.ts +16 -0
  13. package/dist/api/insets.js +21 -0
  14. package/dist/api/layout.d.ts +72 -0
  15. package/dist/api/layout.js +99 -0
  16. package/dist/api/structure.d.ts +80 -0
  17. package/dist/api/structure.js +125 -0
  18. package/dist/api/table.d.ts +37 -0
  19. package/dist/api/table.js +87 -0
  20. package/dist/api/text.d.ts +28 -0
  21. package/dist/api/text.js +61 -0
  22. package/dist/assets/Courier-Bold.afm +342 -0
  23. package/dist/assets/Courier-BoldOblique.afm +342 -0
  24. package/dist/assets/Courier-Oblique.afm +342 -0
  25. package/dist/assets/Courier.afm +342 -0
  26. package/dist/assets/Helvetica-Bold.afm +2827 -0
  27. package/dist/assets/Helvetica-BoldOblique.afm +2827 -0
  28. package/dist/assets/Helvetica-Oblique.afm +3051 -0
  29. package/dist/assets/Helvetica.afm +3051 -0
  30. package/dist/assets/Symbol.afm +213 -0
  31. package/dist/assets/Times-Bold.afm +2588 -0
  32. package/dist/assets/Times-BoldItalic.afm +2384 -0
  33. package/dist/assets/Times-Italic.afm +2667 -0
  34. package/dist/assets/Times-Roman.afm +2419 -0
  35. package/dist/assets/ZapfDingbats.afm +225 -0
  36. package/dist/assets/agl.txt +695 -0
  37. package/dist/common/color.d.ts +37 -0
  38. package/dist/common/color.js +62 -0
  39. package/dist/constants/page-sizes.d.ts +44 -0
  40. package/dist/constants/page-sizes.js +92 -0
  41. package/dist/constants/pdf-parts.d.ts +5 -0
  42. package/dist/constants/pdf-parts.js +8 -0
  43. package/dist/elements/container-element.d.ts +35 -0
  44. package/dist/elements/container-element.js +91 -0
  45. package/dist/elements/image-element.d.ts +56 -0
  46. package/dist/elements/image-element.js +151 -0
  47. package/dist/elements/index.d.ts +10 -0
  48. package/dist/elements/index.js +28 -0
  49. package/dist/elements/layout/deferred-element.d.ts +19 -0
  50. package/dist/elements/layout/deferred-element.js +33 -0
  51. package/dist/elements/layout/expanded-element.d.ts +30 -0
  52. package/dist/elements/layout/expanded-element.js +68 -0
  53. package/dist/elements/layout/padding-element.d.ts +29 -0
  54. package/dist/elements/layout/padding-element.js +76 -0
  55. package/dist/elements/layout/repeating-header-element.d.ts +29 -0
  56. package/dist/elements/layout/repeating-header-element.js +60 -0
  57. package/dist/elements/layout/sized-container-element.d.ts +14 -0
  58. package/dist/elements/layout/sized-container-element.js +37 -0
  59. package/dist/elements/line-element.d.ts +31 -0
  60. package/dist/elements/line-element.js +44 -0
  61. package/dist/elements/page-element.d.ts +58 -0
  62. package/dist/elements/page-element.js +90 -0
  63. package/dist/elements/pdf-document-element.d.ts +13 -0
  64. package/dist/elements/pdf-document-element.js +22 -0
  65. package/dist/elements/pdf-element.d.ts +67 -0
  66. package/dist/elements/pdf-element.js +55 -0
  67. package/dist/elements/rectangle-element.d.ts +55 -0
  68. package/dist/elements/rectangle-element.js +120 -0
  69. package/dist/elements/row-element.d.ts +42 -0
  70. package/dist/elements/row-element.js +54 -0
  71. package/dist/elements/text-element.d.ts +59 -0
  72. package/dist/elements/text-element.js +137 -0
  73. package/dist/index.d.ts +3 -0
  74. package/dist/index.js +21 -0
  75. package/dist/ir/display-list.d.ts +74 -0
  76. package/dist/ir/display-list.js +2 -0
  77. package/dist/layout/box-constraints.d.ts +56 -0
  78. package/dist/layout/box-constraints.js +73 -0
  79. package/dist/layout/fragmentation.d.ts +42 -0
  80. package/dist/layout/fragmentation.js +61 -0
  81. package/dist/renderer/container-renderer.d.ts +6 -0
  82. package/dist/renderer/container-renderer.js +30 -0
  83. package/dist/renderer/deferred-renderer.d.ts +6 -0
  84. package/dist/renderer/deferred-renderer.js +25 -0
  85. package/dist/renderer/expanded-renderer.d.ts +6 -0
  86. package/dist/renderer/expanded-renderer.js +23 -0
  87. package/dist/renderer/image-renderer.d.ts +6 -0
  88. package/dist/renderer/image-renderer.js +85 -0
  89. package/dist/renderer/index.d.ts +10 -0
  90. package/dist/renderer/index.js +26 -0
  91. package/dist/renderer/line-renderer.d.ts +6 -0
  92. package/dist/renderer/line-renderer.js +30 -0
  93. package/dist/renderer/padding-renderer.d.ts +6 -0
  94. package/dist/renderer/padding-renderer.js +23 -0
  95. package/dist/renderer/page-renderer.d.ts +5 -0
  96. package/dist/renderer/page-renderer.js +81 -0
  97. package/dist/renderer/pdf-backend.d.ts +45 -0
  98. package/dist/renderer/pdf-backend.js +184 -0
  99. package/dist/renderer/pdf-config.d.ts +8 -0
  100. package/dist/renderer/pdf-config.js +17 -0
  101. package/dist/renderer/pdf-document-class.d.ts +42 -0
  102. package/dist/renderer/pdf-document-class.js +118 -0
  103. package/dist/renderer/pdf-document-renderer.d.ts +20 -0
  104. package/dist/renderer/pdf-document-renderer.js +104 -0
  105. package/dist/renderer/pdf-renderer.d.ts +5 -0
  106. package/dist/renderer/pdf-renderer.js +92 -0
  107. package/dist/renderer/rectangle-renderer.d.ts +8 -0
  108. package/dist/renderer/rectangle-renderer.js +61 -0
  109. package/dist/renderer/repeating-header-renderer.d.ts +6 -0
  110. package/dist/renderer/repeating-header-renderer.js +28 -0
  111. package/dist/renderer/row-renderer.d.ts +6 -0
  112. package/dist/renderer/row-renderer.js +30 -0
  113. package/dist/renderer/text-renderer.d.ts +9 -0
  114. package/dist/renderer/text-renderer.js +125 -0
  115. package/dist/text/line-breaker.d.ts +40 -0
  116. package/dist/text/line-breaker.js +106 -0
  117. package/dist/utils/afm-parser.d.ts +12 -0
  118. package/dist/utils/afm-parser.js +91 -0
  119. package/dist/utils/flex-layout.d.ts +53 -0
  120. package/dist/utils/flex-layout.js +119 -0
  121. package/dist/utils/font-metrics.d.ts +12 -0
  122. package/dist/utils/font-metrics.js +2 -0
  123. package/dist/utils/font-path.d.ts +1 -0
  124. package/dist/utils/font-path.js +19 -0
  125. package/dist/utils/image-helper.d.ts +43 -0
  126. package/dist/utils/image-helper.js +206 -0
  127. package/dist/utils/pdf-object-manager.d.ts +97 -0
  128. package/dist/utils/pdf-object-manager.js +645 -0
  129. package/dist/utils/renderer-registry.d.ts +6 -0
  130. package/dist/utils/renderer-registry.js +19 -0
  131. package/dist/utils/ttf-parser.d.ts +29 -0
  132. package/dist/utils/ttf-parser.js +191 -0
  133. package/dist/utils/ttf-subsetter.d.ts +1 -0
  134. package/dist/utils/ttf-subsetter.js +161 -0
  135. package/dist/utils/utf8-to-windows1252-encoder.d.ts +2 -0
  136. package/dist/utils/utf8-to-windows1252-encoder.js +55 -0
  137. package/dist/validators/element-validator.d.ts +8 -0
  138. package/dist/validators/element-validator.js +61 -0
  139. package/package.json +50 -0
@@ -0,0 +1,92 @@
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.PDFRenderer = void 0;
13
+ const pdf_document_renderer_1 = require("./pdf-document-renderer");
14
+ const renderer_registry_1 = require("../utils/renderer-registry");
15
+ const elements_1 = require("../elements");
16
+ const text_renderer_1 = require("./text-renderer");
17
+ const container_element_1 = require("../elements/container-element");
18
+ const rectangle_element_1 = require("../elements/rectangle-element");
19
+ const row_element_1 = require("../elements/row-element");
20
+ const container_renderer_1 = require("./container-renderer");
21
+ const rectangle_renderer_1 = require("./rectangle-renderer");
22
+ const row_renderer_1 = require("./row-renderer");
23
+ const expanded_renderer_1 = require("./expanded-renderer");
24
+ const padding_renderer_1 = require("./padding-renderer");
25
+ const image_renderer_1 = require("./image-renderer");
26
+ const line_renderer_1 = require("./line-renderer");
27
+ const repeating_header_element_1 = require("../elements/layout/repeating-header-element");
28
+ const repeating_header_renderer_1 = require("./repeating-header-renderer");
29
+ const deferred_element_1 = require("../elements/layout/deferred-element");
30
+ const deferred_renderer_1 = require("./deferred-renderer");
31
+ const box_constraints_1 = require("../layout/box-constraints");
32
+ class PDFRenderer {
33
+ static render(document, objectManager) {
34
+ return __awaiter(this, void 0, void 0, function* () {
35
+ // Register all Renderer
36
+ renderer_registry_1.RendererRegistry.register(elements_1.TextElement, text_renderer_1.TextRenderer.render);
37
+ renderer_registry_1.RendererRegistry.register(container_element_1.ContainerElement, container_renderer_1.ContainerRenderer.render);
38
+ renderer_registry_1.RendererRegistry.register(row_element_1.RowElement, row_renderer_1.RowRenderer.render);
39
+ renderer_registry_1.RendererRegistry.register(rectangle_element_1.RectangleElement, rectangle_renderer_1.RectangleRenderer.render);
40
+ renderer_registry_1.RendererRegistry.register(elements_1.ExpandedElement, expanded_renderer_1.ExpandedRenderer.render);
41
+ renderer_registry_1.RendererRegistry.register(elements_1.PaddingElement, padding_renderer_1.PaddingRenderer.render);
42
+ renderer_registry_1.RendererRegistry.register(elements_1.ImageElement, image_renderer_1.ImageRenderer.render);
43
+ renderer_registry_1.RendererRegistry.register(elements_1.LineElement, line_renderer_1.LineRenderer.render);
44
+ renderer_registry_1.RendererRegistry.register(repeating_header_element_1.RepeatingHeaderElement, repeating_header_renderer_1.RepeatingHeaderRenderer.render);
45
+ renderer_registry_1.RendererRegistry.register(deferred_element_1.DeferredElement, deferred_renderer_1.DeferredRenderer.render);
46
+ let pdfContent = "";
47
+ // Header: version line + the PDF/A binary marker (the object manager owns it so its length
48
+ // matches the xref offset calculation).
49
+ pdfContent += objectManager.getHeader();
50
+ // Layout pass: thread the context explicitly. The seed page config is the document
51
+ // default; each PageElement overrides it for its own subtree.
52
+ const ctx = {
53
+ metrics: objectManager,
54
+ pageConfig: objectManager.getPDFConfig(),
55
+ };
56
+ document.calculateLayout(new box_constraints_1.BoxConstraints(), { x: 0, y: 0 }, ctx);
57
+ // Render pages and contents (the driver paginates overflowing pages).
58
+ yield pdf_document_renderer_1.PDFDocumentRenderer.render(document, objectManager, ctx);
59
+ // Add the catalog. XMP metadata (/Metadata) and embedded files (/AF + /Names/EmbeddedFiles)
60
+ // are added only when present; with neither, the catalog is byte-identical to before.
61
+ const catalogParts = [`/Type /Catalog /Pages ${objectManager.getParentObjectNumber()} 0 R`];
62
+ const xmp = objectManager.getXmpMetadata();
63
+ if (xmp) {
64
+ const metadataObject = objectManager.addObject(`<< /Type /Metadata /Subtype /XML /Length ${xmp.length} >>\nstream\n${xmp}\nendstream`);
65
+ catalogParts.push(`/Metadata ${metadataObject} 0 R`);
66
+ }
67
+ const outputIntent = objectManager.getOutputIntent();
68
+ if (outputIntent) {
69
+ catalogParts.push(`/OutputIntents [${outputIntent} 0 R]`);
70
+ }
71
+ const attachments = objectManager.getAttachments();
72
+ if (attachments.length > 0) {
73
+ const names = attachments.map((a) => `(${a.name}) ${a.filespec} 0 R`).join(" ");
74
+ const af = attachments.map((a) => `${a.filespec} 0 R`).join(" ");
75
+ catalogParts.push(`/AF [${af}]`, `/Names << /EmbeddedFiles << /Names [${names}] >> >>`);
76
+ }
77
+ const catalogObject = `<< ${catalogParts.join(" ")} >>`;
78
+ objectManager.addObject(catalogObject);
79
+ // Now that the render pass has revealed which glyphs each embedded font uses, fill the reserved
80
+ // font objects with the subset font program (must happen before the objects are serialized).
81
+ objectManager.finalizeCustomFonts();
82
+ // Add rendered objects
83
+ pdfContent += objectManager.getRenderedObjects();
84
+ // Add XRef table and trailer
85
+ const startxref = pdfContent.length;
86
+ pdfContent += objectManager.getXRefTable();
87
+ pdfContent += objectManager.getTrailerAndXRef(startxref);
88
+ return pdfContent;
89
+ });
90
+ }
91
+ }
92
+ exports.PDFRenderer = PDFRenderer;
@@ -0,0 +1,8 @@
1
+ import { PDFObjectManager } from "../utils/pdf-object-manager";
2
+ import { RectangleElement } from "../elements/rectangle-element";
3
+ import { IRNode } from "../ir/display-list";
4
+ export declare class RectangleRenderer {
5
+ static render(rectangleElement: RectangleElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
6
+ /** One `line` node per present side, along the box edges (top-left coordinates). */
7
+ private static sideLines;
8
+ }
@@ -0,0 +1,61 @@
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.RectangleRenderer = void 0;
13
+ const renderer_registry_1 = require("../utils/renderer-registry");
14
+ class RectangleRenderer {
15
+ static render(rectangleElement, objectManager) {
16
+ return __awaiter(this, void 0, void 0, function* () {
17
+ const { x, y, width, height, children, color, backgroundColor, borderWidth, radius, sideBorders, } = rectangleElement.getProps();
18
+ const h = height;
19
+ const nodes = [];
20
+ if (sideBorders) {
21
+ // Per-side borders: a fill-only box (no stroke), then a line per present side
22
+ // (sharp corners). This is what lets cells draw grid lines.
23
+ if (backgroundColor) {
24
+ nodes.push({ type: "rect", x, y, width, height: h, strokeWidth: 0, fill: backgroundColor });
25
+ }
26
+ nodes.push(...RectangleRenderer.sideLines(x, y, width, h, borderWidth, sideBorders));
27
+ }
28
+ else {
29
+ // The box itself becomes a display-list primitive. A background means a filled box;
30
+ // otherwise it is stroked only. (Unchanged path - byte-identical.)
31
+ const node = Object.assign(Object.assign({ type: "rect", x,
32
+ y,
33
+ width, height: h, stroke: color, strokeWidth: borderWidth }, (backgroundColor ? { fill: backgroundColor } : {})), (radius ? { radius } : {}));
34
+ nodes.push(node);
35
+ }
36
+ // Children follow the box (Rectangle is also a container).
37
+ if (children)
38
+ for (const child of children) {
39
+ const renderer = renderer_registry_1.RendererRegistry.getRenderer(child);
40
+ if (renderer) {
41
+ nodes.push(...(yield renderer(child, objectManager)));
42
+ }
43
+ }
44
+ return nodes;
45
+ });
46
+ }
47
+ /** One `line` node per present side, along the box edges (top-left coordinates). */
48
+ static sideLines(x, y, width, height, strokeWidth, sides) {
49
+ const lines = [];
50
+ const add = (x1, y1, x2, y2, stroke) => {
51
+ if (stroke)
52
+ lines.push({ type: "line", x1, y1, x2, y2, stroke, strokeWidth });
53
+ };
54
+ add(x, y, x + width, y, sides.top); // top
55
+ add(x, y + height, x + width, y + height, sides.bottom); // bottom
56
+ add(x, y, x, y + height, sides.left); // left
57
+ add(x + width, y, x + width, y + height, sides.right); // right
58
+ return lines;
59
+ }
60
+ }
61
+ exports.RectangleRenderer = RectangleRenderer;
@@ -0,0 +1,6 @@
1
+ import { PDFObjectManager } from "../utils/pdf-object-manager";
2
+ import { RepeatingHeaderElement } from "../elements/layout/repeating-header-element";
3
+ import { IRNode } from "../ir/display-list";
4
+ export declare class RepeatingHeaderRenderer {
5
+ static render(element: RepeatingHeaderElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
6
+ }
@@ -0,0 +1,28 @@
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.RepeatingHeaderRenderer = void 0;
13
+ const renderer_registry_1 = require("../utils/renderer-registry");
14
+ class RepeatingHeaderRenderer {
15
+ static render(element, objectManager) {
16
+ return __awaiter(this, void 0, void 0, function* () {
17
+ const { header, body } = element.getProps();
18
+ const nodes = [];
19
+ for (const child of [header, body]) {
20
+ const renderer = renderer_registry_1.RendererRegistry.getRenderer(child);
21
+ if (renderer)
22
+ nodes.push(...(yield renderer(child, objectManager)));
23
+ }
24
+ return nodes;
25
+ });
26
+ }
27
+ }
28
+ exports.RepeatingHeaderRenderer = RepeatingHeaderRenderer;
@@ -0,0 +1,6 @@
1
+ import { PDFObjectManager } from "../utils/pdf-object-manager";
2
+ import { RowElement } from "../elements/row-element";
3
+ import { IRNode } from "../ir/display-list";
4
+ export declare class RowRenderer {
5
+ static render(rowElement: RowElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
6
+ }
@@ -0,0 +1,30 @@
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
+ }
25
+ }
26
+ return nodes;
27
+ });
28
+ }
29
+ }
30
+ exports.RowRenderer = RowRenderer;
@@ -0,0 +1,9 @@
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";
5
+ export declare class TextRenderer {
6
+ static calculateTextHeight(content: string | TextSegment[], fontSize: number, fontFamily: string, fontStyle: FontStyle, objectManager: FontMetrics, maxWidth: number): number;
7
+ static render(textElement: TextElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
8
+ private static _buildRuns;
9
+ }
@@ -0,0 +1,125 @@
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");
15
+ // Distance from the top of a line down to its baseline, as a fraction of the font
16
+ // size. ~0.683 is the standard-14 ascent ratio used to seat the first baseline.
17
+ const BASELINE_RATIO = 683 / 1000;
18
+ class TextRenderer {
19
+ // Measuring only needs metrics, not the full object manager. (The render pass below
20
+ // 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.
23
+ if (typeof content === "string") {
24
+ const lines = (0, line_breaker_1.wrapStringIntoLines)(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager);
25
+ return lines.length * fontSize;
26
+ }
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);
30
+ }
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
+ });
39
+ }
40
+ // Lay the content out into absolutely-positioned text runs. Glyph positions match
41
+ // the previous hand-written operators exactly (verified by pixel-identical render);
42
+ // only the output form changed from PDF strings to `TextRun`s.
43
+ static _buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth, textAlignment, color, initialX, yPosition) {
44
+ const runs = [];
45
+ // Horizontal offset of a line of the given width under the current alignment.
46
+ const alignmentOffset = (lineWidth) => {
47
+ if (textAlignment === pdf_element_1.HorizontalAlignment.center)
48
+ return (maxWidth - lineWidth) / 2;
49
+ if (textAlignment === pdf_element_1.HorizontalAlignment.right)
50
+ return maxWidth - lineWidth;
51
+ return 0;
52
+ };
53
+ // Advance width WITHOUT kerning. This is how Tj moves the text cursor, so
54
+ // segments flowing after each other land here. (Standard-14 fonts carry no
55
+ // /Widths array; the viewer advances by the AFM widths - same source as below.)
56
+ const advanceNoKerning = (text, family, size, style) => {
57
+ let width = 0;
58
+ for (const ch of text) {
59
+ width += objectManager.getCharWidth(ch, size, undefined, family, style);
60
+ }
61
+ return width;
62
+ };
63
+ // --- Plain string: one run per wrapped line. ---
64
+ 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;
69
+ lines.forEach((line, index) => {
70
+ const lineWidth = objectManager.getStringWidth(line, fontFamily, fontSize, fontStyle);
71
+ runs.push({
72
+ type: "text",
73
+ x: initialX + alignmentOffset(lineWidth),
74
+ y: baseline + fontSize * index,
75
+ text: line,
76
+ fontFamily,
77
+ fontStyle,
78
+ fontSize,
79
+ color,
80
+ });
81
+ });
82
+ return runs;
83
+ }
84
+ // --- Segments: break into lines (shared breaker), then emit one run per segment.
85
+ // Segment 0 starts at the aligned line origin; each following segment is offset by
86
+ // the previous segment's kerning-free advance. Each line drops by its OWN leading
87
+ // (tallest font on that line), so mixed-font lines space correctly and the drawn
88
+ // height matches the measured height.
89
+ const pushLine = (line, lineY) => {
90
+ let x = initialX + alignmentOffset(line.width);
91
+ line.segments.forEach((segment) => {
92
+ const family = segment.fontFamily || fontFamily;
93
+ const size = segment.fontSize || fontSize;
94
+ const style = segment.fontStyle || fontStyle;
95
+ runs.push({
96
+ type: "text",
97
+ x,
98
+ y: lineY,
99
+ text: segment.content,
100
+ fontFamily: family,
101
+ fontStyle: style,
102
+ fontSize: size,
103
+ color: segment.fontColor || color,
104
+ });
105
+ x += advanceNoKerning(segment.content, family, size, style);
106
+ });
107
+ };
108
+ // The overall tallest font seats the first baseline; each line then steps DOWN by
109
+ // its own leading. yPosition is the top of the text box (top-left); the seam flips
110
+ // the whole thing to PDF space.
111
+ let overallMaxFont = fontSize;
112
+ for (const segment of content) {
113
+ const size = segment.fontSize || fontSize;
114
+ if (size > overallMaxFont)
115
+ overallMaxFont = size;
116
+ }
117
+ 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;
121
+ }
122
+ return runs;
123
+ }
124
+ }
125
+ exports.TextRenderer = TextRenderer;
@@ -0,0 +1,40 @@
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";
4
+ /** Default font for segments that don't override it. */
5
+ export interface SegmentDefaults {
6
+ fontFamily: string;
7
+ fontSize: number;
8
+ fontStyle: FontStyle;
9
+ }
10
+ /** One laid-out line of segments. `maxFontSize` is the tallest font ON THIS LINE - its
11
+ * leading - matching how real engines (and this lib's plain-string path) space lines. */
12
+ export interface SegmentLine {
13
+ segments: TextSegment[];
14
+ width: number;
15
+ maxFontSize: number;
16
+ }
17
+ /**
18
+ * Break a plain string into lines that each fit within `maxWidth`, splitting on
19
+ * spaces (greedy: a word stays on the current line unless it would overflow).
20
+ *
21
+ * Single source of truth for plain-string wrapping: both height measurement and
22
+ * rendering call this, so they can never disagree. Depends only on `FontMetrics`,
23
+ * not the PDF byte writer - the future fragmentation pass can reuse it.
24
+ */
25
+ export declare function wrapStringIntoLines(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle, maxWidth: number, metrics: FontMetrics): string[];
26
+ /**
27
+ * Break styled segments into lines that fit within `maxWidth`. Same greedy
28
+ * word-splitting as the string breaker, but each line records the tallest font on
29
+ * THAT line as its leading (per-line, not a paragraph-global maximum). Single source
30
+ * of truth: both height measurement and rendering call this.
31
+ */
32
+ export declare function breakSegmentsIntoLines(segments: TextSegment[], defaults: SegmentDefaults, maxWidth: number, metrics: FontMetrics): SegmentLine[];
33
+ /**
34
+ * Inverse of `breakSegmentsIntoLines`: flatten broken lines back into a `TextSegment[]`
35
+ * that re-wraps to exactly those lines. The wrap consumed the space at each line break,
36
+ * so re-insert one between lines (unless the piece already ends with one) - otherwise the
37
+ * last word of a line and the first of the next would fuse ("a b" + "c d" -> "a bc d").
38
+ * Used by text fragmentation to rebuild the fitted/remainder halves of a split paragraph.
39
+ */
40
+ export declare function segmentLinesToSegments(lines: SegmentLine[]): TextSegment[];
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wrapStringIntoLines = wrapStringIntoLines;
4
+ exports.breakSegmentsIntoLines = breakSegmentsIntoLines;
5
+ exports.segmentLinesToSegments = segmentLinesToSegments;
6
+ /**
7
+ * Break a plain string into lines that each fit within `maxWidth`, splitting on
8
+ * spaces (greedy: a word stays on the current line unless it would overflow).
9
+ *
10
+ * Single source of truth for plain-string wrapping: both height measurement and
11
+ * rendering call this, so they can never disagree. Depends only on `FontMetrics`,
12
+ * not the PDF byte writer - the future fragmentation pass can reuse it.
13
+ */
14
+ function wrapStringIntoLines(text, fontFamily, fontSize, fontStyle, maxWidth, metrics) {
15
+ let currentLine = "";
16
+ let currentWidth = 0;
17
+ const lines = [];
18
+ const words = text.split(" ");
19
+ words.forEach((word, index) => {
20
+ const wordWidth = metrics.getStringWidth(word, fontFamily, fontSize, fontStyle);
21
+ const spaceWidth = metrics.getCharWidth(" ", fontSize, undefined, fontFamily, fontStyle);
22
+ if (currentWidth + wordWidth > maxWidth) {
23
+ lines.push(currentLine.trim());
24
+ currentLine = word;
25
+ currentWidth = wordWidth;
26
+ }
27
+ else {
28
+ currentLine += index === 0 ? word : " " + word;
29
+ currentWidth += wordWidth + spaceWidth;
30
+ }
31
+ });
32
+ if (currentLine)
33
+ lines.push(currentLine.trim());
34
+ return lines;
35
+ }
36
+ /**
37
+ * Break styled segments into lines that fit within `maxWidth`. Same greedy
38
+ * word-splitting as the string breaker, but each line records the tallest font on
39
+ * THAT line as its leading (per-line, not a paragraph-global maximum). Single source
40
+ * of truth: both height measurement and rendering call this.
41
+ */
42
+ function breakSegmentsIntoLines(segments, defaults, maxWidth, metrics) {
43
+ const lines = [];
44
+ let width = 0;
45
+ let maxFontSize = 0; // per line: starts at 0, grows to the tallest font on the line
46
+ let lineSegments = [];
47
+ let combined = "";
48
+ segments.forEach((segment) => {
49
+ const family = segment.fontFamily || defaults.fontFamily;
50
+ const size = segment.fontSize || defaults.fontSize;
51
+ const style = segment.fontStyle || defaults.fontStyle;
52
+ const spaceWidth = metrics.getCharWidth(" ", size, undefined, family, style);
53
+ const words = segment.content.split(" ");
54
+ // Start this segment's piece empty; its content is filled word-by-word below. (Not
55
+ // the original content - otherwise a segment whose FIRST word overflows would carry
56
+ // its whole text into the line that just closed.)
57
+ lineSegments.push(Object.assign(Object.assign({}, segment), { fontFamily: family, content: "" }));
58
+ combined = "";
59
+ if (maxFontSize < size)
60
+ maxFontSize = size;
61
+ words.forEach((word, wordIndex) => {
62
+ const wordWidth = metrics.getStringWidth(word, family, size, style);
63
+ if (width + wordWidth > maxWidth) {
64
+ lines.push({ segments: lineSegments, width, maxFontSize });
65
+ // Start the next line; its leading resets to the wrapping segment's size.
66
+ width = 0;
67
+ maxFontSize = size;
68
+ lineSegments = [];
69
+ combined = word;
70
+ width += wordWidth + spaceWidth;
71
+ lineSegments.push(Object.assign(Object.assign({}, segment), { content: combined }));
72
+ }
73
+ else {
74
+ combined += wordIndex === 0 ? word : " " + word;
75
+ width += wordWidth + spaceWidth;
76
+ if (lineSegments.length === 0) {
77
+ lineSegments.push(Object.assign(Object.assign({}, segment), { fontFamily: family, content: combined }));
78
+ }
79
+ lineSegments[lineSegments.length - 1].content = combined;
80
+ }
81
+ });
82
+ });
83
+ if (lineSegments.length > 0) {
84
+ lines.push({ segments: lineSegments, width, maxFontSize });
85
+ }
86
+ return lines;
87
+ }
88
+ /**
89
+ * Inverse of `breakSegmentsIntoLines`: flatten broken lines back into a `TextSegment[]`
90
+ * that re-wraps to exactly those lines. The wrap consumed the space at each line break,
91
+ * so re-insert one between lines (unless the piece already ends with one) - otherwise the
92
+ * last word of a line and the first of the next would fuse ("a b" + "c d" -> "a bc d").
93
+ * Used by text fragmentation to rebuild the fitted/remainder halves of a split paragraph.
94
+ */
95
+ function segmentLinesToSegments(lines) {
96
+ const result = [];
97
+ lines.forEach((line, lineIndex) => {
98
+ line.segments.forEach((segment) => result.push(Object.assign({}, segment)));
99
+ if (lineIndex < lines.length - 1) {
100
+ const last = result[result.length - 1];
101
+ if (last && !last.content.endsWith(" "))
102
+ last.content += " ";
103
+ }
104
+ });
105
+ return result;
106
+ }
@@ -0,0 +1,12 @@
1
+ export declare class AFMParser {
2
+ private advanceWidths;
3
+ private kerningPairs;
4
+ private glyphMap;
5
+ constructor(afmData: string);
6
+ private loadGlyphList;
7
+ private getGlyphName;
8
+ private parseAFMData;
9
+ private parseCharMetrics;
10
+ getAdvanceWidth(charName: string): number;
11
+ getKerning(firstChar: string, secondChar: string): number;
12
+ }
@@ -0,0 +1,91 @@
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 {
10
+ constructor(afmData) {
11
+ this.advanceWidths = {};
12
+ this.kerningPairs = {};
13
+ this.glyphMap = {};
14
+ this.parseAFMData(afmData);
15
+ this.loadGlyphList();
16
+ }
17
+ 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");
21
+ for (const line of lines) {
22
+ const parts = line.trim().split(";");
23
+ if (parts.length >= 2) {
24
+ const unicodeHex = parts[0];
25
+ const glyphName = parts[1];
26
+ // Converts the Unicode hex code into the corresponding character
27
+ const unicodeChar = String.fromCharCode(parseInt(unicodeHex, 16));
28
+ // Adds the character as the key and the glyph name as the value in the list
29
+ this.glyphMap[unicodeChar] = glyphName;
30
+ }
31
+ }
32
+ }
33
+ getGlyphName(char) {
34
+ return this.glyphMap[char] || char;
35
+ }
36
+ parseAFMData(afmData) {
37
+ const lines = afmData.split("\n");
38
+ let inCharMetrics = false;
39
+ for (let line of lines) {
40
+ line = line.trim();
41
+ if (line.startsWith("StartCharMetrics")) {
42
+ inCharMetrics = true;
43
+ continue;
44
+ }
45
+ if (line.startsWith("EndCharMetrics")) {
46
+ inCharMetrics = false;
47
+ continue;
48
+ }
49
+ if (inCharMetrics) {
50
+ const charData = this.parseCharMetrics(line);
51
+ if (charData) {
52
+ const { charName, wx } = charData;
53
+ this.advanceWidths[charName] = wx;
54
+ }
55
+ }
56
+ if (line.startsWith("KPX")) {
57
+ const parts = line.split(/\s+/);
58
+ const firstChar = parts[1];
59
+ const secondChar = parts[2];
60
+ const kerning = parseFloat(parts[3]);
61
+ const pair = `${firstChar}-${secondChar}`;
62
+ this.kerningPairs[pair] = kerning;
63
+ }
64
+ }
65
+ }
66
+ parseCharMetrics(line) {
67
+ const parts = line.split(";").map((part) => part.trim());
68
+ let charName = "";
69
+ let wx = 0;
70
+ for (const part of parts) {
71
+ if (part.startsWith("N ")) {
72
+ charName = part.split(" ")[1];
73
+ }
74
+ if (part.startsWith("WX ")) {
75
+ wx = parseFloat(part.split(" ")[1]);
76
+ }
77
+ }
78
+ if (charName && wx) {
79
+ return { charName, wx };
80
+ }
81
+ return null;
82
+ }
83
+ getAdvanceWidth(charName) {
84
+ return this.advanceWidths[this.getGlyphName(charName)] || 0;
85
+ }
86
+ getKerning(firstChar, secondChar) {
87
+ const pair = `${firstChar}-${secondChar}`;
88
+ return this.kerningPairs[pair] || 0;
89
+ }
90
+ }
91
+ exports.AFMParser = AFMParser;