pagyra-js 0.0.21 → 0.0.23
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/README.md +283 -264
- package/dist/browser/pagyra.min.js +30 -30
- package/dist/browser/pagyra.min.js.map +4 -4
- package/dist/src/css/apply-declarations.js +2 -1
- package/dist/src/css/clip-path-types.d.ts +9 -1
- package/dist/src/css/compute-style/overrides.js +10 -1
- package/dist/src/css/parsers/clip-path-parser.js +51 -0
- package/dist/src/css/parsers/register-parsers.js +21 -0
- package/dist/src/css/properties/visual.d.ts +2 -0
- package/dist/src/css/style.d.ts +5 -0
- package/dist/src/css/style.js +3 -0
- package/dist/src/css/ua-defaults/element-defaults.js +13 -0
- package/dist/src/dom/node.d.ts +2 -0
- package/dist/src/dom/node.js +1 -0
- package/dist/src/fonts/woff2/decoder.d.ts +1 -9
- package/dist/src/fonts/woff2/decoder.js +6 -565
- package/dist/src/fonts/woff2/glyf-reconstructor.d.ts +54 -0
- package/dist/src/fonts/woff2/glyf-reconstructor.js +357 -0
- package/dist/src/fonts/woff2/hmtx-reconstructor.d.ts +5 -0
- package/dist/src/fonts/woff2/hmtx-reconstructor.js +42 -0
- package/dist/src/fonts/woff2/sfnt-builder.d.ts +7 -0
- package/dist/src/fonts/woff2/sfnt-builder.js +55 -0
- package/dist/src/fonts/woff2/utils.d.ts +12 -0
- package/dist/src/fonts/woff2/utils.js +111 -0
- package/dist/src/html-to-pdf/render-finalize.js +5 -1
- package/dist/src/layout/inline/run-placer.js +1 -1
- package/dist/src/layout/strategies/flex/alignment.d.ts +10 -0
- package/dist/src/layout/strategies/flex/alignment.js +91 -0
- package/dist/src/layout/strategies/flex/distributor.d.ts +5 -0
- package/dist/src/layout/strategies/flex/distributor.js +56 -0
- package/dist/src/layout/strategies/flex/line-builder.d.ts +5 -0
- package/dist/src/layout/strategies/flex/line-builder.js +55 -0
- package/dist/src/layout/strategies/flex/types.d.ts +27 -0
- package/dist/src/layout/strategies/flex/types.js +2 -0
- package/dist/src/layout/strategies/flex/utils.d.ts +12 -0
- package/dist/src/layout/strategies/flex/utils.js +113 -0
- package/dist/src/layout/strategies/flex.js +4 -308
- package/dist/src/layout/strategies/grid.js +0 -3
- package/dist/src/layout/strategies/table.js +85 -58
- package/dist/src/layout/utils/text-metrics.js +16 -8
- package/dist/src/pdf/font/embedder.js +3 -3
- package/dist/src/pdf/font/font-subset.js +1 -3
- package/dist/src/pdf/font/to-unicode.js +16 -16
- package/dist/src/pdf/layout-tree-builder.js +15 -9
- package/dist/src/pdf/renderer/box-painter.js +74 -9
- package/dist/src/pdf/renderers/text-renderer.d.ts +4 -2
- package/dist/src/pdf/renderers/text-renderer.js +52 -2
- package/dist/src/pdf/types.d.ts +16 -1
- package/dist/src/pdf/utils/clip-path-resolver.js +28 -12
- package/dist/src/pdf/utils/mask-resolver.d.ts +7 -0
- package/dist/src/pdf/utils/mask-resolver.js +25 -0
- package/dist/src/pdf/utils/node-text-run-factory.d.ts +2 -1
- package/dist/src/pdf/utils/node-text-run-factory.js +5 -26
- package/dist/src/pdf/utils/rounded-rect-to-path.d.ts +7 -0
- package/dist/src/pdf/utils/rounded-rect-to-path.js +86 -0
- package/dist/src/render/offset.d.ts +5 -0
- package/dist/src/render/offset.js +93 -9
- package/dist/src/text/line-breaker.js +31 -0
- package/dist/tests/css/clip-path-parser.spec.js +15 -8
- package/dist/tests/environment/path-resolution.spec.js +2 -1
- package/dist/tests/helpers/ai-layout-diagnostics.js +6 -6
- package/dist/tests/layout/container-query-units.spec.js +0 -7
- package/dist/tests/layout/inline-background-alignment.spec.js +6 -6
- package/dist/tests/layout/table-image-cell.spec.js +95 -0
- package/dist/tests/pdf/alignments.spec.js +12 -12
- package/dist/tests/pdf/clip-path.spec.js +3 -1
- package/dist/tests/pdf/form-text-encoding.spec.js +1 -1
- package/dist/tests/pdf/svg-stroke-dash.spec.js +8 -8
- package/dist/tests/pdf/text-transform-matrix.spec.js +1 -1
- package/dist/tests/pdf/xref-integrity.spec.js +1 -1
- package/dist/tests/verify-subset-multi.spec.js +14 -14
- package/dist/tests/verify-subset.spec.js +12 -12
- package/package.json +89 -71
- package/dist/src/image/js-png-backend.d.ts +0 -7
- package/dist/src/image/js-png-backend.js +0 -9
- package/dist/src/image/png-backend.d.ts +0 -5
- package/dist/src/image/png-wasm-loader.d.ts +0 -5
- package/dist/src/image/png-wasm-loader.js +0 -59
- package/dist/src/image/wasm/png_decoder_wasm.d.ts +0 -8
- package/dist/src/image/wasm/png_decoder_wasm.js +0 -24
- package/dist/src/image/wasm/png_decoder_wasm_bg.js +0 -16
- package/dist/src/image/wasm-png-backend.d.ts +0 -6
- package/dist/src/image/wasm-png-backend.js +0 -17
- package/dist/src/layout/table/cell_layout.d.ts +0 -2
- package/dist/src/layout/table/cell_layout.js +0 -26
- package/dist/tests/image/png-backend.spec.d.ts +0 -1
- package/dist/tests/image/png-backend.spec.js +0 -34
- package/dist/tests/pdf/font-subset-registry-key.spec.d.ts +0 -1
- package/dist/tests/pdf/font-subset-registry-key.spec.js +0 -66
- package/dist/tests/pdf/header-footer.spec.d.ts +0 -1
- package/dist/tests/pdf/header-footer.spec.js +0 -46
- /package/dist/{src/image/png-backend.js → tests/layout/table-image-cell.spec.d.ts} +0 -0
|
@@ -16,7 +16,6 @@ export function createPdfFontSubset(options) {
|
|
|
16
16
|
const { glyphIds, gidMap } = computeSubsetClosure(fontProgram, initialGids);
|
|
17
17
|
const unitsPerEm = fontMetrics.metrics.unitsPerEm;
|
|
18
18
|
const widths = [];
|
|
19
|
-
const usedGidSet = new Set(glyphIds);
|
|
20
19
|
let firstChar;
|
|
21
20
|
let lastChar;
|
|
22
21
|
if (encoding === "sequential") {
|
|
@@ -174,7 +173,6 @@ function subsetFont(fontProgram, glyphIds, encoding) {
|
|
|
174
173
|
}
|
|
175
174
|
const headView = new DataView(head.buffer, head.byteOffset, head.byteLength);
|
|
176
175
|
const locaView = new DataView(loca.buffer, loca.byteOffset, loca.byteLength);
|
|
177
|
-
const glyfView = new DataView(glyf.buffer, glyf.byteOffset, glyf.byteLength);
|
|
178
176
|
const hmtxView = new DataView(hmtx.buffer, hmtx.byteOffset, hmtx.byteLength);
|
|
179
177
|
const hheaView = new DataView(hhea.buffer, hhea.byteOffset, hhea.byteLength);
|
|
180
178
|
const indexToLocFormat = reader.getUint16(headView, 50);
|
|
@@ -352,7 +350,7 @@ function subsetFont(fontProgram, glyphIds, encoding) {
|
|
|
352
350
|
paddedLength += 4 - (data.length % 4);
|
|
353
351
|
offset += paddedLength;
|
|
354
352
|
}
|
|
355
|
-
for (const [
|
|
353
|
+
for (const [, data] of tables) {
|
|
356
354
|
fullTtf.writeBytes(data);
|
|
357
355
|
const padding = (4 - (data.length % 4)) % 4;
|
|
358
356
|
for (let k = 0; k < padding; k++)
|
|
@@ -4,22 +4,22 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export function createToUnicodeCMapText(entries) {
|
|
6
6
|
if (!entries || entries.length === 0) {
|
|
7
|
-
return `/CIDInit /ProcSet findresource begin
|
|
8
|
-
12 dict begin
|
|
9
|
-
begincmap
|
|
10
|
-
/CIDSystemInfo <<
|
|
11
|
-
/Registry (Adobe)
|
|
12
|
-
/Ordering (UCS)
|
|
13
|
-
/Supplement 0
|
|
14
|
-
>> def
|
|
15
|
-
/CMapName /Adobe-Identity-UCS def
|
|
16
|
-
/CMapType 2 def
|
|
17
|
-
1 begincodespacerange
|
|
18
|
-
<0000> <FFFF>
|
|
19
|
-
endcodespacerange
|
|
20
|
-
endcmap
|
|
21
|
-
CMapName currentdict /CMap defineresource pop
|
|
22
|
-
end
|
|
7
|
+
return `/CIDInit /ProcSet findresource begin
|
|
8
|
+
12 dict begin
|
|
9
|
+
begincmap
|
|
10
|
+
/CIDSystemInfo <<
|
|
11
|
+
/Registry (Adobe)
|
|
12
|
+
/Ordering (UCS)
|
|
13
|
+
/Supplement 0
|
|
14
|
+
>> def
|
|
15
|
+
/CMapName /Adobe-Identity-UCS def
|
|
16
|
+
/CMapType 2 def
|
|
17
|
+
1 begincodespacerange
|
|
18
|
+
<0000> <FFFF>
|
|
19
|
+
endcodespacerange
|
|
20
|
+
endcmap
|
|
21
|
+
CMapName currentdict /CMap defineresource pop
|
|
22
|
+
end
|
|
23
23
|
end`;
|
|
24
24
|
}
|
|
25
25
|
const es = entries.slice().sort((a, b) => a.gid - b.gid);
|
|
@@ -12,6 +12,7 @@ import { calculateBoxDimensions } from "./utils/box-dimensions-utils.js";
|
|
|
12
12
|
import { resolveDecorations } from "./utils/text-decoration-utils.js";
|
|
13
13
|
import { resolveBackgroundLayers, resolveTextGradientLayer, } from "./utils/background-layer-resolver.js";
|
|
14
14
|
import { resolveClipPath } from "./utils/clip-path-resolver.js";
|
|
15
|
+
import { resolveMaskGradient } from "./utils/mask-resolver.js";
|
|
15
16
|
import { parseTransform } from "../transform/css-parser.js";
|
|
16
17
|
import { buildNodeTextRuns } from "./utils/node-text-run-factory.js";
|
|
17
18
|
export function buildRenderTree(root, options = {}) {
|
|
@@ -92,7 +93,7 @@ function mapOverflow(mode) {
|
|
|
92
93
|
// ====================
|
|
93
94
|
// MAIN CONVERSION FUNCTION
|
|
94
95
|
// ====================
|
|
95
|
-
function convertNode(node, state, inheritedTextGradient) {
|
|
96
|
+
function convertNode(node, state, inheritedTextGradient, inheritedTextBackground) {
|
|
96
97
|
// Use the original HTML ID if available, otherwise generate a new one
|
|
97
98
|
const originalId = node.customData?.id;
|
|
98
99
|
const id = originalId || `node-${state.counter++}`;
|
|
@@ -104,15 +105,14 @@ function convertNode(node, state, inheritedTextGradient) {
|
|
|
104
105
|
const borderRadius = resolveBorderRadius(node.style, borderBox);
|
|
105
106
|
const transformString = node.style.transform;
|
|
106
107
|
const transform = transformString ? parseTransform(transformString) ?? undefined : undefined;
|
|
108
|
+
const background = resolveBackgroundLayers(node, { borderBox, paddingBox, contentBox });
|
|
109
|
+
const backgroundClip = node.style.backgroundLayers?.some(l => l.clip === "text") ? "text" : undefined;
|
|
110
|
+
const maskGradient = resolveMaskGradient(node, { borderBox, paddingBox, contentBox });
|
|
107
111
|
const ownTextGradient = resolveTextGradientLayer(node, { borderBox, paddingBox, contentBox });
|
|
108
|
-
if (ownTextGradient) {
|
|
109
|
-
log("layout", "debug", "node has background-clip:text gradient", {
|
|
110
|
-
tagName: node.tagName,
|
|
111
|
-
rect: ownTextGradient.rect,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
112
|
const textGradient = ownTextGradient ?? inheritedTextGradient;
|
|
115
|
-
const
|
|
113
|
+
const ownTextBackground = backgroundClip === "text" ? background : undefined;
|
|
114
|
+
const textBackground = ownTextBackground ?? inheritedTextBackground;
|
|
115
|
+
const children = node.children.map((child) => convertNode(child, state, textGradient, textBackground));
|
|
116
116
|
const imageRef = extractImageRef(node);
|
|
117
117
|
const decorations = resolveDecorations(node.style);
|
|
118
118
|
const textRuns = buildNodeTextRuns({
|
|
@@ -132,9 +132,9 @@ function convertNode(node, state, inheritedTextGradient) {
|
|
|
132
132
|
textContent: node.textContent?.slice(0, 40),
|
|
133
133
|
fontFamily: node.style.fontFamily,
|
|
134
134
|
fontSize: node.style.fontSize,
|
|
135
|
+
breakInside: node.style.breakInside,
|
|
135
136
|
contentBox,
|
|
136
137
|
});
|
|
137
|
-
const background = resolveBackgroundLayers(node, { borderBox, paddingBox, contentBox });
|
|
138
138
|
const clipPath = resolveClipPath(node, { borderBox, paddingBox, contentBox });
|
|
139
139
|
const zIndex = typeof node.style.zIndex === "number" ? node.style.zIndex : 0;
|
|
140
140
|
const establishesStackingContext = typeof node.style.zIndex === "number" && node.style.position !== Position.Static ||
|
|
@@ -186,6 +186,8 @@ function convertNode(node, state, inheritedTextGradient) {
|
|
|
186
186
|
borderRadius,
|
|
187
187
|
opacity: node.style.opacity ?? 1,
|
|
188
188
|
overflow: mapOverflow(node.style.overflowX ?? OverflowMode.Visible),
|
|
189
|
+
overflowX: mapOverflow(node.style.overflowX ?? OverflowMode.Visible),
|
|
190
|
+
overflowY: mapOverflow(node.style.overflowY ?? OverflowMode.Visible),
|
|
189
191
|
textRuns,
|
|
190
192
|
decorations: decorations ?? {},
|
|
191
193
|
textShadows: resolveTextShadows(node, fallbackShadowColor),
|
|
@@ -197,8 +199,12 @@ function convertNode(node, state, inheritedTextGradient) {
|
|
|
197
199
|
links: [],
|
|
198
200
|
borderColor: parseColor(node.style.borderColor),
|
|
199
201
|
borderStyle,
|
|
202
|
+
breakInside: node.style.breakInside,
|
|
200
203
|
color: textColor,
|
|
204
|
+
mask: node.style.mask,
|
|
205
|
+
maskGradient,
|
|
201
206
|
background,
|
|
207
|
+
backgroundClip,
|
|
202
208
|
clipPath,
|
|
203
209
|
image: imageRef,
|
|
204
210
|
customData,
|
|
@@ -5,6 +5,7 @@ import { renderSvgBox } from "../svg/render-svg.js";
|
|
|
5
5
|
import { NodeKind } from "../types.js";
|
|
6
6
|
import { computeBackgroundTileRects, rectEquals } from "../utils/background-tiles.js";
|
|
7
7
|
import { computeBorderSideStrokes } from "../utils/border-dashes.js";
|
|
8
|
+
import { roundedRectToPath } from "../utils/rounded-rect-to-path.js";
|
|
8
9
|
import { defaultFormRendererFactory } from "../renderers/form/factory.js";
|
|
9
10
|
import { extractDropShadowLayers, warnUnsupportedFilters, } from "../utils/filter-utils.js";
|
|
10
11
|
export async function paintBoxAtomic(painter, box) {
|
|
@@ -33,12 +34,27 @@ export async function paintBoxAtomic(painter, box) {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
paintBoxShadows(painter, [box], false);
|
|
36
|
-
|
|
37
|
+
// Overflow clipping
|
|
38
|
+
const hasOverflowClip = box.overflow === "hidden" || box.overflow === "clip";
|
|
39
|
+
if (hasOverflowClip) {
|
|
40
|
+
const clipArea = determineOverflowClipArea(box);
|
|
41
|
+
if (clipArea) {
|
|
42
|
+
const clipCommands = roundedRectToPath(clipArea.rect, clipArea.radius);
|
|
43
|
+
painter.beginClipPath(clipCommands);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
let clipCommands = buildClipPathCommands(box.clipPath);
|
|
47
|
+
if (!clipCommands && box.maskGradient && box.maskGradient.gradient.type === "radial") {
|
|
48
|
+
// MVP: Aproximação de máscara radial usando clipping path circular
|
|
49
|
+
clipCommands = buildCircularClipPath(box.maskGradient.rect);
|
|
50
|
+
}
|
|
37
51
|
const hasClip = !!clipCommands;
|
|
38
52
|
if (hasClip && clipCommands) {
|
|
39
53
|
painter.beginClipPath(clipCommands);
|
|
40
54
|
}
|
|
41
|
-
|
|
55
|
+
if (box.backgroundClip !== "text") {
|
|
56
|
+
paintBackground(painter, box);
|
|
57
|
+
}
|
|
42
58
|
paintBorder(painter, box);
|
|
43
59
|
paintBoxShadows(painter, [box], true);
|
|
44
60
|
if (box.kind === NodeKind.Svg || (box.tagName === "svg" && box.customData?.svg)) {
|
|
@@ -54,6 +70,9 @@ export async function paintBoxAtomic(painter, box) {
|
|
|
54
70
|
if (hasClip) {
|
|
55
71
|
painter.endClipPath();
|
|
56
72
|
}
|
|
73
|
+
if (hasOverflowClip) {
|
|
74
|
+
painter.endClipPath();
|
|
75
|
+
}
|
|
57
76
|
if (hasOpacity) {
|
|
58
77
|
painter.endOpacityScope(effectiveOpacity);
|
|
59
78
|
}
|
|
@@ -196,6 +215,32 @@ function determineBackgroundPaintArea(box) {
|
|
|
196
215
|
function hasVisibleBorder(border) {
|
|
197
216
|
return border.top > 0 || border.right > 0 || border.bottom > 0 || border.left > 0;
|
|
198
217
|
}
|
|
218
|
+
function determineOverflowClipArea(box) {
|
|
219
|
+
const rect = box.paddingBox ?? box.contentBox;
|
|
220
|
+
if (!rect) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
let radius = shrinkRadius(box.borderRadius, box.border.top, box.border.right, box.border.bottom, box.border.left);
|
|
224
|
+
if (rect === box.contentBox) {
|
|
225
|
+
radius = shrinkRadius(radius, box.padding.top, box.padding.right, box.padding.bottom, box.padding.left);
|
|
226
|
+
}
|
|
227
|
+
return { rect, radius };
|
|
228
|
+
}
|
|
229
|
+
function buildCircularClipPath(rect) {
|
|
230
|
+
const cx = rect.x + rect.width / 2;
|
|
231
|
+
const cy = rect.y + rect.height / 2;
|
|
232
|
+
const rx = rect.width / 2;
|
|
233
|
+
const ry = rect.height / 2;
|
|
234
|
+
const k = 0.5522847498307936;
|
|
235
|
+
return [
|
|
236
|
+
{ type: "moveTo", x: cx + rx, y: cy },
|
|
237
|
+
{ type: "curveTo", x1: cx + rx, y1: cy + ry * k, x2: cx + rx * k, y2: cy + ry, x: cx, y: cy + ry },
|
|
238
|
+
{ type: "curveTo", x1: cx - rx * k, y1: cy + ry, x2: cx - rx, y2: cy + ry * k, x: cx - rx, y: cy },
|
|
239
|
+
{ type: "curveTo", x1: cx - rx, y1: cy - ry * k, x2: cx - rx * k, y2: cy - ry, x: cx, y: cy - ry },
|
|
240
|
+
{ type: "curveTo", x1: cx + rx * k, y1: cy - ry, x2: cx + rx, y2: cy - ry * k, x: cx + rx, y: cy },
|
|
241
|
+
{ type: "closePath" }
|
|
242
|
+
];
|
|
243
|
+
}
|
|
199
244
|
function zeroRadius() {
|
|
200
245
|
return {
|
|
201
246
|
topLeft: { x: 0, y: 0 },
|
|
@@ -205,16 +250,36 @@ function zeroRadius() {
|
|
|
205
250
|
};
|
|
206
251
|
}
|
|
207
252
|
function buildClipPathCommands(clipPath) {
|
|
208
|
-
if (!clipPath
|
|
253
|
+
if (!clipPath) {
|
|
209
254
|
return null;
|
|
210
255
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
256
|
+
if (clipPath.type === "polygon") {
|
|
257
|
+
if (clipPath.points.length < 3) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
const [first, ...rest] = clipPath.points;
|
|
261
|
+
const commands = [{ type: "moveTo", x: first.x, y: first.y }];
|
|
262
|
+
for (const point of rest) {
|
|
263
|
+
commands.push({ type: "lineTo", x: point.x, y: point.y });
|
|
264
|
+
}
|
|
265
|
+
commands.push({ type: "closePath" });
|
|
266
|
+
return commands;
|
|
215
267
|
}
|
|
216
|
-
|
|
217
|
-
|
|
268
|
+
if (clipPath.type === "ellipse") {
|
|
269
|
+
return buildEllipseClipPath(clipPath.cx, clipPath.cy, clipPath.rx, clipPath.ry);
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
function buildEllipseClipPath(cx, cy, rx, ry) {
|
|
274
|
+
const k = 0.5522847498307936;
|
|
275
|
+
return [
|
|
276
|
+
{ type: "moveTo", x: cx + rx, y: cy },
|
|
277
|
+
{ type: "curveTo", x1: cx + rx, y1: cy + ry * k, x2: cx + rx * k, y2: cy + ry, x: cx, y: cy + ry },
|
|
278
|
+
{ type: "curveTo", x1: cx - rx * k, y1: cy + ry, x2: cx - rx, y2: cy + ry * k, x: cx - rx, y: cy },
|
|
279
|
+
{ type: "curveTo", x1: cx - rx, y1: cy - ry * k, x2: cx - rx * k, y2: cy - ry, x: cx, y: cy - ry },
|
|
280
|
+
{ type: "curveTo", x1: cx + rx * k, y1: cy - ry, x2: cx + rx, y2: cy - ry * k, x: cx + rx, y: cy },
|
|
281
|
+
{ type: "closePath" },
|
|
282
|
+
];
|
|
218
283
|
}
|
|
219
284
|
function paintDropShadows(painter, box, shadows) {
|
|
220
285
|
// Usa o borderBox como base da sombra (aproximação para drop-shadow)
|
|
@@ -13,16 +13,17 @@ export interface TextRendererResult {
|
|
|
13
13
|
export declare class TextRenderer {
|
|
14
14
|
private readonly coordinateTransformer;
|
|
15
15
|
private readonly fontRegistry;
|
|
16
|
+
private readonly imageRenderer?;
|
|
17
|
+
private readonly graphicsStateManager?;
|
|
16
18
|
private readonly commands;
|
|
17
19
|
private readonly fonts;
|
|
18
20
|
private readonly decorationRenderer;
|
|
19
21
|
private readonly shadowRenderer;
|
|
20
|
-
private readonly graphicsStateManager?;
|
|
21
22
|
private readonly fontResolver;
|
|
22
23
|
private readonly gradientService;
|
|
23
24
|
private readonly patterns;
|
|
24
25
|
private transformContext;
|
|
25
|
-
constructor(coordinateTransformer: CoordinateTransformer, fontRegistry: FontRegistry, imageRenderer?: ImageRenderer, graphicsStateManager?: GraphicsStateManager);
|
|
26
|
+
constructor(coordinateTransformer: CoordinateTransformer, fontRegistry: FontRegistry, imageRenderer?: ImageRenderer | undefined, graphicsStateManager?: GraphicsStateManager | undefined);
|
|
26
27
|
drawText(text: string, xPx: number, yPx: number, options?: TextPaintOptions): Promise<void>;
|
|
27
28
|
drawTextRun(run: Run): Promise<void>;
|
|
28
29
|
private drawTextRunWithGlyphs;
|
|
@@ -37,5 +38,6 @@ export declare class TextRenderer {
|
|
|
37
38
|
flushCommands(): string[];
|
|
38
39
|
setTransformContext(rect: Rect): void;
|
|
39
40
|
clearTransformContext(): void;
|
|
41
|
+
private generateBackgroundCommands;
|
|
40
42
|
getResult(): TextRendererResult;
|
|
41
43
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { log } from "../../logging/debug.js";
|
|
2
2
|
import { CoordinateTransformer } from "../utils/coordinate-transformer.js";
|
|
3
|
-
import { formatNumber } from "./text-renderer-utils.js";
|
|
3
|
+
import { formatNumber, fillColorCommand } from "./text-renderer-utils.js";
|
|
4
4
|
import { TextShadowRenderer } from "./text-shadow-renderer.js";
|
|
5
5
|
import { TextDecorationRenderer } from "./text-decoration-renderer.js";
|
|
6
6
|
import { TextFontResolver } from "./text-font-resolver.js";
|
|
@@ -16,11 +16,12 @@ export class TextRenderer {
|
|
|
16
16
|
constructor(coordinateTransformer, fontRegistry, imageRenderer, graphicsStateManager) {
|
|
17
17
|
this.coordinateTransformer = coordinateTransformer;
|
|
18
18
|
this.fontRegistry = fontRegistry;
|
|
19
|
+
this.imageRenderer = imageRenderer;
|
|
20
|
+
this.graphicsStateManager = graphicsStateManager;
|
|
19
21
|
this.commands = [];
|
|
20
22
|
this.fonts = new Map();
|
|
21
23
|
this.patterns = new Map();
|
|
22
24
|
this.transformContext = null;
|
|
23
|
-
this.graphicsStateManager = graphicsStateManager;
|
|
24
25
|
this.fontResolver = new TextFontResolver(fontRegistry);
|
|
25
26
|
this.shadowRenderer = new TextShadowRenderer(coordinateTransformer, fontRegistry, imageRenderer, graphicsStateManager);
|
|
26
27
|
this.decorationRenderer = new TextDecorationRenderer(coordinateTransformer, graphicsStateManager);
|
|
@@ -106,6 +107,27 @@ export class TextRenderer {
|
|
|
106
107
|
const appliedWordSpacing = wordSpacingPx !== 0 && wordSpacingPt !== 0;
|
|
107
108
|
const subsetResource = this.fontRegistry.ensureSubsetForGlyphRun(glyphRun, font);
|
|
108
109
|
this.registerSubsetFont(subsetResource.alias, subsetResource.ref);
|
|
110
|
+
const textBackground = run.textBackground;
|
|
111
|
+
if (textBackground) {
|
|
112
|
+
log("paint", "debug", "text run has background clip: text", {
|
|
113
|
+
text: run.text.slice(0, 32),
|
|
114
|
+
});
|
|
115
|
+
const afterTextCommands = [];
|
|
116
|
+
this.generateBackgroundCommands(textBackground, afterTextCommands);
|
|
117
|
+
if (afterTextCommands.length > 0) {
|
|
118
|
+
const glyphCommands = drawGlyphRun(glyphRun, subsetResource.subset, 0, 0, fontSizePt, color, this.graphicsStateManager, wordSpacingPt, {
|
|
119
|
+
textRenderingMode: 7, // Clip to text
|
|
120
|
+
afterTextCommands,
|
|
121
|
+
tm: textMatrix,
|
|
122
|
+
skipColor: true,
|
|
123
|
+
});
|
|
124
|
+
this.commands.push("q", ...glyphCommands, "Q");
|
|
125
|
+
if (run.decorations) {
|
|
126
|
+
this.commands.push(...this.decorationRenderer.render(run, color));
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
109
131
|
const gradientBackground = run.textGradient;
|
|
110
132
|
if (gradientBackground && gradientBackground.rect && gradientBackground.rect.width > 0 && gradientBackground.rect.height > 0) {
|
|
111
133
|
log("paint", "debug", "text run has background clip gradient", {
|
|
@@ -213,6 +235,34 @@ export class TextRenderer {
|
|
|
213
235
|
clearTransformContext() {
|
|
214
236
|
this.transformContext = null;
|
|
215
237
|
}
|
|
238
|
+
generateBackgroundCommands(background, commands) {
|
|
239
|
+
if (background.color) {
|
|
240
|
+
const color = background.color;
|
|
241
|
+
const rect = background.gradient?.originRect ?? background.image?.originRect ?? { x: -10000, y: -10000, width: 20000, height: 20000 };
|
|
242
|
+
const pdfRect = transformForRect(rect, this.coordinateTransformer, this.transformContext);
|
|
243
|
+
const colorCmd = fillColorCommand(color, this.graphicsStateManager);
|
|
244
|
+
commands.push("q", colorCmd, pdfRect, `0 0 ${formatNumber(this.coordinateTransformer.convertPxToPt(rect.width))} ${formatNumber(this.coordinateTransformer.convertPxToPt(rect.height))} re`, "f", "Q");
|
|
245
|
+
}
|
|
246
|
+
if (background.gradient) {
|
|
247
|
+
const g = background.gradient;
|
|
248
|
+
const shading = g.gradient.type === "radial"
|
|
249
|
+
? this.gradientService.createRadialGradient(g.gradient, g.rect)
|
|
250
|
+
: this.gradientService.createLinearGradient(g.gradient, g.rect);
|
|
251
|
+
const pdfTransform = transformForRect(g.rect, this.coordinateTransformer, this.transformContext);
|
|
252
|
+
commands.push("q", pdfTransform, `0 0 ${formatNumber(this.coordinateTransformer.convertPxToPt(g.rect.width))} ${formatNumber(this.coordinateTransformer.convertPxToPt(g.rect.height))} re`, "W n", `/${shading.shadingName} sh`, "Q");
|
|
253
|
+
}
|
|
254
|
+
if (background.image && this.imageRenderer) {
|
|
255
|
+
const img = background.image;
|
|
256
|
+
const resource = this.imageRenderer.registerResource(img.image);
|
|
257
|
+
// Simplificação: desenha a imagem uma vez. Idealmente deveria suportar tiling (repeat).
|
|
258
|
+
const widthPt = this.coordinateTransformer.convertPxToPt(img.rect.width);
|
|
259
|
+
const heightPt = this.coordinateTransformer.convertPxToPt(img.rect.height);
|
|
260
|
+
const xPt = this.coordinateTransformer.convertPxToPt(img.rect.x);
|
|
261
|
+
const localY = img.rect.y - this.coordinateTransformer.pageOffsetPx;
|
|
262
|
+
const yPt = this.coordinateTransformer.pageHeightPt - this.coordinateTransformer.convertPxToPt(localY + img.rect.height);
|
|
263
|
+
commands.push("q", `${formatNumber(widthPt)} 0 0 ${formatNumber(heightPt)} ${formatNumber(xPt)} ${formatNumber(yPt)} cm`, `/${resource.alias} Do`, "Q");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
216
266
|
getResult() {
|
|
217
267
|
return {
|
|
218
268
|
commands: [...this.commands],
|
package/dist/src/pdf/types.d.ts
CHANGED
|
@@ -50,10 +50,18 @@ export interface Radius {
|
|
|
50
50
|
bottomRight: CornerRadius;
|
|
51
51
|
bottomLeft: CornerRadius;
|
|
52
52
|
}
|
|
53
|
-
export interface
|
|
53
|
+
export interface ClipPathPolygon {
|
|
54
54
|
type: "polygon";
|
|
55
55
|
points: ShapePoint[];
|
|
56
56
|
}
|
|
57
|
+
export interface ClipPathEllipse {
|
|
58
|
+
type: "ellipse";
|
|
59
|
+
cx: number;
|
|
60
|
+
cy: number;
|
|
61
|
+
rx: number;
|
|
62
|
+
ry: number;
|
|
63
|
+
}
|
|
64
|
+
export type ClipPath = ClipPathPolygon | ClipPathEllipse;
|
|
57
65
|
export interface BackgroundImage {
|
|
58
66
|
image: ImageRef;
|
|
59
67
|
rect: Rect;
|
|
@@ -146,6 +154,7 @@ export interface Run {
|
|
|
146
154
|
decorations?: Decorations;
|
|
147
155
|
advanceWidth?: number;
|
|
148
156
|
textGradient?: GradientBackground;
|
|
157
|
+
textBackground?: Background;
|
|
149
158
|
textShadows?: TextShadowLayer[];
|
|
150
159
|
/**
|
|
151
160
|
* Line index in the block (0-based).
|
|
@@ -233,6 +242,8 @@ export interface RenderBox {
|
|
|
233
242
|
background: Background;
|
|
234
243
|
opacity: number;
|
|
235
244
|
overflow: Overflow;
|
|
245
|
+
overflowX: Overflow;
|
|
246
|
+
overflowY: Overflow;
|
|
236
247
|
textRuns: Run[];
|
|
237
248
|
decorations: Decorations;
|
|
238
249
|
textShadows: TextShadowLayer[];
|
|
@@ -255,7 +266,11 @@ export interface RenderBox {
|
|
|
255
266
|
customData?: Record<string, unknown>;
|
|
256
267
|
borderColor?: RGBA;
|
|
257
268
|
borderStyle?: BorderStyles;
|
|
269
|
+
breakInside?: string;
|
|
258
270
|
color?: RGBA;
|
|
271
|
+
mask?: string;
|
|
272
|
+
maskGradient?: GradientBackground;
|
|
273
|
+
backgroundClip?: "border-box" | "padding-box" | "content-box" | "text";
|
|
259
274
|
transform?: TextMatrix;
|
|
260
275
|
/** Parsed CSS filter functions carried from ComputedStyle */
|
|
261
276
|
filter?: FilterFunction[];
|
|
@@ -1,24 +1,40 @@
|
|
|
1
1
|
export function resolveClipPath(node, boxes) {
|
|
2
2
|
const clip = node.style.clipPath;
|
|
3
|
-
if (!clip
|
|
3
|
+
if (!clip) {
|
|
4
4
|
return undefined;
|
|
5
5
|
}
|
|
6
6
|
const referenceRect = selectReferenceRect(clip.referenceBox, boxes);
|
|
7
7
|
if (!referenceRect || referenceRect.width <= 0 || referenceRect.height <= 0) {
|
|
8
8
|
return undefined;
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
if (clip.type === "polygon") {
|
|
11
|
+
if (!clip.points.length) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const points = clip.points.map((point) => {
|
|
15
|
+
const x = resolveClipLength(point.x, referenceRect.width);
|
|
16
|
+
const y = resolveClipLength(point.y, referenceRect.height);
|
|
17
|
+
return {
|
|
18
|
+
x: referenceRect.x + x,
|
|
19
|
+
y: referenceRect.y + y,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
if (points.some((p) => !Number.isFinite(p.x) || !Number.isFinite(p.y))) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
return { type: "polygon", points };
|
|
26
|
+
}
|
|
27
|
+
if (clip.type === "ellipse") {
|
|
28
|
+
const rx = resolveClipLength(clip.rx, referenceRect.width);
|
|
29
|
+
const ry = resolveClipLength(clip.ry, referenceRect.height);
|
|
30
|
+
const cx = referenceRect.x + resolveClipLength(clip.cx, referenceRect.width);
|
|
31
|
+
const cy = referenceRect.y + resolveClipLength(clip.cy, referenceRect.height);
|
|
32
|
+
if (!Number.isFinite(rx) || !Number.isFinite(ry) || !Number.isFinite(cx) || !Number.isFinite(cy)) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return { type: "ellipse", cx, cy, rx, ry };
|
|
20
36
|
}
|
|
21
|
-
return
|
|
37
|
+
return undefined;
|
|
22
38
|
}
|
|
23
39
|
function selectReferenceRect(box, boxes) {
|
|
24
40
|
switch (box) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LayoutNode } from "../../dom/node.js";
|
|
2
|
+
import type { Rect, GradientBackground } from "../types.js";
|
|
3
|
+
export declare function resolveMaskGradient(node: LayoutNode, boxes: {
|
|
4
|
+
borderBox: Rect;
|
|
5
|
+
paddingBox: Rect;
|
|
6
|
+
contentBox: Rect;
|
|
7
|
+
}): GradientBackground | undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { parseRadialGradient, parseLinearGradient } from "../../css/parsers/gradient-parser.js";
|
|
2
|
+
export function resolveMaskGradient(node, boxes) {
|
|
3
|
+
const mask = node.style.mask;
|
|
4
|
+
if (!mask)
|
|
5
|
+
return undefined;
|
|
6
|
+
const radial = parseRadialGradient(mask);
|
|
7
|
+
if (radial) {
|
|
8
|
+
return {
|
|
9
|
+
gradient: radial,
|
|
10
|
+
rect: boxes.borderBox,
|
|
11
|
+
repeat: "no-repeat",
|
|
12
|
+
originRect: boxes.borderBox,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const linear = parseLinearGradient(mask);
|
|
16
|
+
if (linear) {
|
|
17
|
+
return {
|
|
18
|
+
gradient: linear,
|
|
19
|
+
rect: boxes.borderBox,
|
|
20
|
+
repeat: "no-repeat",
|
|
21
|
+
originRect: boxes.borderBox,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LayoutNode } from "../../dom/node.js";
|
|
2
|
-
import type { RenderBox, Rect, RGBA, Run, Decorations, GradientBackground } from "../types.js";
|
|
2
|
+
import type { RenderBox, Rect, RGBA, Run, Decorations, GradientBackground, Background } from "../types.js";
|
|
3
3
|
import type { Matrix } from "../../geometry/matrix.js";
|
|
4
4
|
import type { FontResolver } from "../../fonts/types.js";
|
|
5
5
|
import type { GlyphRun } from "../../layout/text-run.js";
|
|
@@ -15,6 +15,7 @@ export interface NodeTextRunContext {
|
|
|
15
15
|
fallbackColor: RGBA;
|
|
16
16
|
fontResolver?: FontResolver;
|
|
17
17
|
textGradient?: GradientBackground;
|
|
18
|
+
textBackground?: Background;
|
|
18
19
|
}
|
|
19
20
|
export declare function buildNodeTextRuns(context: NodeTextRunContext): Run[];
|
|
20
21
|
/**
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createTextRuns } from "./text-utils.js";
|
|
2
2
|
import { createListMarkerRun } from "./list-utils.js";
|
|
3
|
-
import { multiplyMatrices } from "../../geometry/matrix.js";
|
|
4
3
|
export function buildNodeTextRuns(context) {
|
|
5
|
-
const { node, children,
|
|
4
|
+
const { node, children, contentBox, textColor, decorations, fallbackColor, fontResolver, textGradient, textBackground } = context;
|
|
6
5
|
const textRuns = createTextRuns(node, textColor, decorations);
|
|
7
6
|
// If we have a fontResolver, enhance text runs with GlyphRun data
|
|
8
7
|
if (fontResolver) {
|
|
@@ -19,33 +18,13 @@ export function buildNodeTextRuns(context) {
|
|
|
19
18
|
run.textGradient = textGradient;
|
|
20
19
|
}
|
|
21
20
|
}
|
|
22
|
-
if (
|
|
23
|
-
|
|
21
|
+
if (textBackground) {
|
|
22
|
+
for (const run of textRuns) {
|
|
23
|
+
run.textBackground = textBackground;
|
|
24
|
+
}
|
|
24
25
|
}
|
|
25
26
|
return textRuns;
|
|
26
27
|
}
|
|
27
|
-
function applyTransformToTextRuns(runs, cssMatrix, originBox) {
|
|
28
|
-
if (runs.length === 0) {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
const baseOriginX = Number.isFinite(originBox.x) ? originBox.x : 0;
|
|
32
|
-
const baseOriginY = Number.isFinite(originBox.y) ? originBox.y : 0;
|
|
33
|
-
const originWidth = Number.isFinite(originBox.width) ? originBox.width : 0;
|
|
34
|
-
const originHeight = Number.isFinite(originBox.height) ? originBox.height : 0;
|
|
35
|
-
const originX = baseOriginX + originWidth / 2;
|
|
36
|
-
const originY = baseOriginY + originHeight / 2;
|
|
37
|
-
const toOrigin = translationMatrix(-originX, -originY);
|
|
38
|
-
const fromOrigin = translationMatrix(originX, originY);
|
|
39
|
-
for (const run of runs) {
|
|
40
|
-
const baseMatrix = run.lineMatrix ?? { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
|
|
41
|
-
const localMatrix = multiplyMatrices(toOrigin, baseMatrix);
|
|
42
|
-
const transformedLocal = multiplyMatrices(cssMatrix, localMatrix);
|
|
43
|
-
run.lineMatrix = multiplyMatrices(fromOrigin, transformedLocal);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
function translationMatrix(tx, ty) {
|
|
47
|
-
return { a: 1, b: 0, c: 0, d: 1, e: tx, f: ty };
|
|
48
|
-
}
|
|
49
28
|
/**
|
|
50
29
|
* Enriches text runs with GlyphRun data for TTF-based rendering.
|
|
51
30
|
* For each run, resolves the font, maps text to glyph IDs, and computes positions.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Rect, Radius } from "../types.js";
|
|
2
|
+
import type { PathCommand } from "../renderers/shape-renderer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Converte um retângulo arredondado em uma lista de comandos de caminho (PathCommand).
|
|
5
|
+
* Os comandos estão em pixels da página.
|
|
6
|
+
*/
|
|
7
|
+
export declare function roundedRectToPath(rect: Rect, radius: Radius): PathCommand[];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converte um retângulo arredondado em uma lista de comandos de caminho (PathCommand).
|
|
3
|
+
* Os comandos estão em pixels da página.
|
|
4
|
+
*/
|
|
5
|
+
export function roundedRectToPath(rect, radius) {
|
|
6
|
+
const { x, y, width, height } = rect;
|
|
7
|
+
const tl = radius.topLeft;
|
|
8
|
+
const tr = radius.topRight;
|
|
9
|
+
const br = radius.bottomRight;
|
|
10
|
+
const bl = radius.bottomLeft;
|
|
11
|
+
// Bézier approximation constant for circular arcs
|
|
12
|
+
const k = 0.5522847498307936;
|
|
13
|
+
const commands = [];
|
|
14
|
+
// Início: Top-left após o raio
|
|
15
|
+
commands.push({ type: "moveTo", x: x + tl.x, y: y });
|
|
16
|
+
// Top edge
|
|
17
|
+
commands.push({ type: "lineTo", x: x + width - tr.x, y: y });
|
|
18
|
+
// Top-right corner
|
|
19
|
+
if (tr.x > 0 || tr.y > 0) {
|
|
20
|
+
commands.push({
|
|
21
|
+
type: "curveTo",
|
|
22
|
+
x1: x + width - tr.x + k * tr.x,
|
|
23
|
+
y1: y,
|
|
24
|
+
x2: x + width,
|
|
25
|
+
y2: y + tr.y - k * tr.y,
|
|
26
|
+
x: x + width,
|
|
27
|
+
y: y + tr.y
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
commands.push({ type: "lineTo", x: x + width, y: y });
|
|
32
|
+
}
|
|
33
|
+
// Right edge
|
|
34
|
+
commands.push({ type: "lineTo", x: x + width, y: y + height - br.y });
|
|
35
|
+
// Bottom-right corner
|
|
36
|
+
if (br.x > 0 || br.y > 0) {
|
|
37
|
+
commands.push({
|
|
38
|
+
type: "curveTo",
|
|
39
|
+
x1: x + width,
|
|
40
|
+
y1: y + height - br.y + k * br.y,
|
|
41
|
+
x2: x + width - br.x + k * br.x,
|
|
42
|
+
y2: y + height,
|
|
43
|
+
x: x + width - br.x,
|
|
44
|
+
y: y + height
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
commands.push({ type: "lineTo", x: x + width, y: y + height });
|
|
49
|
+
}
|
|
50
|
+
// Bottom edge
|
|
51
|
+
commands.push({ type: "lineTo", x: x + bl.x, y: y + height });
|
|
52
|
+
// Bottom-left corner
|
|
53
|
+
if (bl.x > 0 || bl.y > 0) {
|
|
54
|
+
commands.push({
|
|
55
|
+
type: "curveTo",
|
|
56
|
+
x1: x + bl.x - k * bl.x,
|
|
57
|
+
y1: y + height,
|
|
58
|
+
x2: x,
|
|
59
|
+
y2: y + height - bl.y + k * bl.y,
|
|
60
|
+
x: x,
|
|
61
|
+
y: y + height - bl.y
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
commands.push({ type: "lineTo", x: x, y: y + height });
|
|
66
|
+
}
|
|
67
|
+
// Left edge
|
|
68
|
+
commands.push({ type: "lineTo", x: x, y: y + tl.y });
|
|
69
|
+
// Top-left corner
|
|
70
|
+
if (tl.x > 0 || tl.y > 0) {
|
|
71
|
+
commands.push({
|
|
72
|
+
type: "curveTo",
|
|
73
|
+
x1: x,
|
|
74
|
+
y1: y + tl.y - k * tl.y,
|
|
75
|
+
x2: x + tl.x - k * tl.x,
|
|
76
|
+
y2: y,
|
|
77
|
+
x: x + tl.x,
|
|
78
|
+
y: y
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
commands.push({ type: "lineTo", x: x, y: y });
|
|
83
|
+
}
|
|
84
|
+
commands.push({ type: "closePath" });
|
|
85
|
+
return commands;
|
|
86
|
+
}
|