@jasy/pdf 1.0.0-alpha.2 → 1.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +24 -14
- package/dist/api/args.d.ts +1 -1
- package/dist/api/args.js +2 -5
- package/dist/api/color.d.ts +4 -4
- package/dist/api/color.js +11 -17
- package/dist/api/content.d.ts +8 -8
- package/dist/api/content.js +23 -24
- package/dist/api/descriptor.d.ts +2 -2
- package/dist/api/descriptor.js +75 -31
- package/dist/api/index.d.ts +8 -8
- package/dist/api/index.js +8 -24
- package/dist/api/insets.js +4 -8
- package/dist/api/layout.d.ts +18 -16
- package/dist/api/layout.js +41 -52
- package/dist/api/structure.d.ts +65 -13
- package/dist/api/structure.js +140 -88
- package/dist/api/table.d.ts +5 -5
- package/dist/api/table.js +28 -24
- package/dist/api/text.d.ts +27 -2
- package/dist/api/text.js +45 -27
- package/dist/assets/font-data.d.ts +2 -0
- package/dist/assets/font-data.js +21 -0
- package/dist/assets/font-data.ts +37 -0
- package/dist/common/color.js +1 -5
- package/dist/constants/page-sizes.js +3 -6
- package/dist/constants/pdf-parts.js +1 -4
- package/dist/crypto/security-handler.d.ts +46 -0
- package/dist/crypto/security-handler.js +129 -0
- package/dist/crypto/webcrypto.d.ts +11 -0
- package/dist/crypto/webcrypto.js +62 -0
- package/dist/elements/container-element.d.ts +4 -4
- package/dist/elements/container-element.js +9 -13
- package/dist/elements/image-element.d.ts +18 -2
- package/dist/elements/image-element.js +81 -105
- package/dist/elements/index.d.ts +12 -11
- package/dist/elements/index.js +12 -29
- package/dist/elements/layout/default-text-style-element.d.ts +30 -0
- package/dist/elements/layout/default-text-style-element.js +47 -0
- package/dist/elements/layout/deferred-element.d.ts +3 -3
- package/dist/elements/layout/deferred-element.js +4 -8
- package/dist/elements/layout/expanded-element.d.ts +3 -3
- package/dist/elements/layout/expanded-element.js +10 -14
- package/dist/elements/layout/padding-element.d.ts +3 -3
- package/dist/elements/layout/padding-element.js +9 -14
- package/dist/elements/layout/positioned-element.d.ts +17 -4
- package/dist/elements/layout/positioned-element.js +29 -25
- package/dist/elements/layout/repeating-header-element.d.ts +3 -3
- package/dist/elements/layout/repeating-header-element.js +8 -12
- package/dist/elements/layout/sized-container-element.d.ts +2 -2
- package/dist/elements/layout/sized-container-element.js +6 -11
- package/dist/elements/line-element.d.ts +3 -3
- package/dist/elements/line-element.js +5 -10
- package/dist/elements/page-element.d.ts +8 -6
- package/dist/elements/page-element.js +20 -23
- package/dist/elements/pdf-document-element.d.ts +10 -4
- package/dist/elements/pdf-document-element.js +11 -10
- package/dist/elements/pdf-element.d.ts +12 -3
- package/dist/elements/pdf-element.js +10 -19
- package/dist/elements/rectangle-element.d.ts +5 -5
- package/dist/elements/rectangle-element.js +19 -25
- package/dist/elements/row-element.d.ts +3 -3
- package/dist/elements/row-element.js +7 -11
- package/dist/elements/text-element.d.ts +37 -11
- package/dist/elements/text-element.js +64 -39
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -19
- package/dist/ir/display-list.d.ts +4 -2
- package/dist/ir/display-list.js +1 -2
- package/dist/layout/box-constraints.js +2 -6
- package/dist/layout/fragmentation.d.ts +8 -1
- package/dist/layout/fragmentation.js +22 -10
- package/dist/platform/browser-fs.d.ts +2 -0
- package/dist/platform/browser-fs.js +9 -0
- package/dist/platform/browser-image.d.ts +5 -0
- package/dist/platform/browser-image.js +13 -0
- package/dist/platform/node-fs.d.ts +2 -0
- package/dist/platform/node-fs.js +10 -0
- package/dist/platform/node-image.d.ts +5 -0
- package/dist/platform/node-image.js +9 -0
- package/dist/renderer/container-renderer.d.ts +3 -3
- package/dist/renderer/container-renderer.js +12 -27
- package/dist/renderer/default-text-style-renderer.d.ts +6 -0
- package/dist/renderer/default-text-style-renderer.js +10 -0
- package/dist/renderer/deferred-renderer.d.ts +3 -3
- package/dist/renderer/deferred-renderer.js +8 -23
- package/dist/renderer/expanded-renderer.d.ts +3 -3
- package/dist/renderer/expanded-renderer.js +6 -21
- package/dist/renderer/image-renderer.d.ts +3 -3
- package/dist/renderer/image-renderer.js +77 -75
- package/dist/renderer/index.d.ts +10 -10
- package/dist/renderer/index.js +10 -26
- package/dist/renderer/line-renderer.d.ts +3 -3
- package/dist/renderer/line-renderer.js +13 -28
- package/dist/renderer/padding-renderer.d.ts +3 -3
- package/dist/renderer/padding-renderer.js +6 -21
- package/dist/renderer/page-renderer.d.ts +2 -2
- package/dist/renderer/page-renderer.js +61 -77
- package/dist/renderer/pdf-backend.d.ts +2 -2
- package/dist/renderer/pdf-backend.js +21 -19
- package/dist/renderer/pdf-config.js +4 -7
- package/dist/renderer/pdf-document-class.d.ts +5 -5
- package/dist/renderer/pdf-document-class.js +24 -41
- package/dist/renderer/pdf-document-renderer.d.ts +3 -3
- package/dist/renderer/pdf-document-renderer.js +71 -85
- package/dist/renderer/pdf-renderer.d.ts +2 -2
- package/dist/renderer/pdf-renderer.js +85 -93
- package/dist/renderer/positioned-renderer.d.ts +3 -3
- package/dist/renderer/positioned-renderer.js +8 -23
- package/dist/renderer/rectangle-renderer.d.ts +3 -3
- package/dist/renderer/rectangle-renderer.js +45 -52
- package/dist/renderer/repeating-header-renderer.d.ts +3 -3
- package/dist/renderer/repeating-header-renderer.js +11 -26
- package/dist/renderer/row-renderer.d.ts +3 -3
- package/dist/renderer/row-renderer.js +12 -27
- package/dist/renderer/text-renderer.d.ts +6 -5
- package/dist/renderer/text-renderer.js +33 -42
- package/dist/text/line-breaker.d.ts +8 -5
- package/dist/text/line-breaker.js +67 -16
- package/dist/text/text-style.d.ts +25 -0
- package/dist/text/text-style.js +29 -0
- package/dist/utils/afm-parser.js +3 -13
- package/dist/utils/bytes.d.ts +24 -0
- package/dist/utils/bytes.js +76 -0
- package/dist/utils/flex-layout.d.ts +2 -2
- package/dist/utils/flex-layout.js +15 -20
- package/dist/utils/font-metrics.d.ts +1 -1
- package/dist/utils/font-metrics.js +1 -2
- package/dist/utils/font-path.js +3 -6
- package/dist/utils/image-helper.d.ts +6 -5
- package/dist/utils/image-helper.js +101 -111
- package/dist/utils/md5.d.ts +4 -0
- package/dist/utils/md5.js +79 -0
- package/dist/utils/pdf-object-manager.d.ts +18 -6
- package/dist/utils/pdf-object-manager.js +0 -0
- package/dist/utils/renderer-registry.js +1 -5
- package/dist/utils/ttf-parser.d.ts +2 -2
- package/dist/utils/ttf-parser.js +32 -36
- package/dist/utils/ttf-subsetter.d.ts +1 -1
- package/dist/utils/ttf-subsetter.js +40 -42
- package/dist/utils/utf8-to-windows1252-encoder.js +1 -4
- package/dist/validators/element-validator.d.ts +2 -2
- package/dist/validators/element-validator.js +9 -13
- package/package.json +14 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { PDFObjectManager } from "../utils/pdf-object-manager";
|
|
2
|
-
import { RectangleElement } from "../elements/rectangle-element";
|
|
3
|
-
import { IRNode } from "../ir/display-list";
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import { RectangleElement } from "../elements/rectangle-element.js";
|
|
3
|
+
import { IRNode } from "../ir/display-list.js";
|
|
4
4
|
export declare class RectangleRenderer {
|
|
5
5
|
static render(rectangleElement: RectangleElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
6
6
|
/** One `line` node per present side, along the box edges (top-left coordinates). */
|
|
@@ -1,56 +1,50 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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, overflow, } = 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);
|
|
1
|
+
import { RendererRegistry } from "../utils/renderer-registry.js";
|
|
2
|
+
export class RectangleRenderer {
|
|
3
|
+
static async render(rectangleElement, objectManager) {
|
|
4
|
+
const { x, y, width, height, children, color, backgroundColor, borderWidth, radius, sideBorders, overflow, } = rectangleElement.getProps();
|
|
5
|
+
const h = height;
|
|
6
|
+
const nodes = [];
|
|
7
|
+
if (sideBorders) {
|
|
8
|
+
// Per-side borders: a fill-only box (no stroke), then a line per present side
|
|
9
|
+
// (sharp corners). This is what lets cells draw grid lines.
|
|
10
|
+
if (backgroundColor) {
|
|
11
|
+
nodes.push({ type: "rect", x, y, width, height: h, strokeWidth: 0, fill: backgroundColor });
|
|
35
12
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
13
|
+
nodes.push(...RectangleRenderer.sideLines(x, y, width, h, borderWidth, sideBorders));
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
// The box itself becomes a display-list primitive. A background means a filled box;
|
|
17
|
+
// otherwise it is stroked only. (Unchanged path - byte-identical.)
|
|
18
|
+
const node = {
|
|
19
|
+
type: "rect",
|
|
20
|
+
x,
|
|
21
|
+
y,
|
|
22
|
+
width,
|
|
23
|
+
height: h,
|
|
24
|
+
stroke: color,
|
|
25
|
+
strokeWidth: borderWidth,
|
|
26
|
+
...(backgroundColor ? { fill: backgroundColor } : {}),
|
|
27
|
+
...(radius ? { radius } : {}),
|
|
28
|
+
};
|
|
29
|
+
nodes.push(node);
|
|
30
|
+
}
|
|
31
|
+
// Children follow the box (Rectangle is also a container). With overflow: "hidden" they are
|
|
32
|
+
// wrapped in a clip to the (rounded) box rect, so a Positioned child is cropped at the edge
|
|
33
|
+
// instead of spilling over. Default "visible" emits no clip - byte-identical.
|
|
34
|
+
const clip = overflow === "hidden";
|
|
35
|
+
if (clip) {
|
|
36
|
+
nodes.push({ type: "clip-push", x, y, width, height: h, ...(radius ? { radius } : {}) });
|
|
37
|
+
}
|
|
38
|
+
if (children)
|
|
39
|
+
for (const child of children) {
|
|
40
|
+
const renderer = RendererRegistry.getRenderer(child);
|
|
41
|
+
if (renderer) {
|
|
42
|
+
nodes.push(...(await renderer(child, objectManager)));
|
|
49
43
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
}
|
|
45
|
+
if (clip)
|
|
46
|
+
nodes.push({ type: "clip-pop" });
|
|
47
|
+
return nodes;
|
|
54
48
|
}
|
|
55
49
|
/** One `line` node per present side, along the box edges (top-left coordinates). */
|
|
56
50
|
static sideLines(x, y, width, height, strokeWidth, sides) {
|
|
@@ -66,4 +60,3 @@ class RectangleRenderer {
|
|
|
66
60
|
return lines;
|
|
67
61
|
}
|
|
68
62
|
}
|
|
69
|
-
exports.RectangleRenderer = RectangleRenderer;
|
|
@@ -1,6 +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";
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import { RepeatingHeaderElement } from "../elements/layout/repeating-header-element.js";
|
|
3
|
+
import { IRNode } from "../ir/display-list.js";
|
|
4
4
|
export declare class RepeatingHeaderRenderer {
|
|
5
5
|
static render(element: RepeatingHeaderElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
6
6
|
}
|
|
@@ -1,28 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
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
|
-
});
|
|
1
|
+
import { RendererRegistry } from "../utils/renderer-registry.js";
|
|
2
|
+
export class RepeatingHeaderRenderer {
|
|
3
|
+
static async render(element, objectManager) {
|
|
4
|
+
const { header, body } = element.getProps();
|
|
5
|
+
const nodes = [];
|
|
6
|
+
for (const child of [header, body]) {
|
|
7
|
+
const renderer = RendererRegistry.getRenderer(child);
|
|
8
|
+
if (renderer)
|
|
9
|
+
nodes.push(...(await renderer(child, objectManager)));
|
|
10
|
+
}
|
|
11
|
+
return nodes;
|
|
26
12
|
}
|
|
27
13
|
}
|
|
28
|
-
exports.RepeatingHeaderRenderer = RepeatingHeaderRenderer;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { PDFObjectManager } from "../utils/pdf-object-manager";
|
|
2
|
-
import { RowElement } from "../elements/row-element";
|
|
3
|
-
import { IRNode } from "../ir/display-list";
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import { RowElement } from "../elements/row-element.js";
|
|
3
|
+
import { IRNode } from "../ir/display-list.js";
|
|
4
4
|
export declare class RowRenderer {
|
|
5
5
|
static render(rowElement: RowElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
6
6
|
}
|
|
@@ -1,30 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 ===
|
|
33
|
+
if (textAlignment === HorizontalAlignment.center)
|
|
48
34
|
return (maxWidth - lineWidth) / 2;
|
|
49
|
-
if (textAlignment ===
|
|
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 =
|
|
66
|
-
// yPosition is the top of the text box (top-left); seat line 0's baseline below
|
|
67
|
-
//
|
|
68
|
-
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
"
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|