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.
Files changed (92) hide show
  1. package/README.md +283 -264
  2. package/dist/browser/pagyra.min.js +30 -30
  3. package/dist/browser/pagyra.min.js.map +4 -4
  4. package/dist/src/css/apply-declarations.js +2 -1
  5. package/dist/src/css/clip-path-types.d.ts +9 -1
  6. package/dist/src/css/compute-style/overrides.js +10 -1
  7. package/dist/src/css/parsers/clip-path-parser.js +51 -0
  8. package/dist/src/css/parsers/register-parsers.js +21 -0
  9. package/dist/src/css/properties/visual.d.ts +2 -0
  10. package/dist/src/css/style.d.ts +5 -0
  11. package/dist/src/css/style.js +3 -0
  12. package/dist/src/css/ua-defaults/element-defaults.js +13 -0
  13. package/dist/src/dom/node.d.ts +2 -0
  14. package/dist/src/dom/node.js +1 -0
  15. package/dist/src/fonts/woff2/decoder.d.ts +1 -9
  16. package/dist/src/fonts/woff2/decoder.js +6 -565
  17. package/dist/src/fonts/woff2/glyf-reconstructor.d.ts +54 -0
  18. package/dist/src/fonts/woff2/glyf-reconstructor.js +357 -0
  19. package/dist/src/fonts/woff2/hmtx-reconstructor.d.ts +5 -0
  20. package/dist/src/fonts/woff2/hmtx-reconstructor.js +42 -0
  21. package/dist/src/fonts/woff2/sfnt-builder.d.ts +7 -0
  22. package/dist/src/fonts/woff2/sfnt-builder.js +55 -0
  23. package/dist/src/fonts/woff2/utils.d.ts +12 -0
  24. package/dist/src/fonts/woff2/utils.js +111 -0
  25. package/dist/src/html-to-pdf/render-finalize.js +5 -1
  26. package/dist/src/layout/inline/run-placer.js +1 -1
  27. package/dist/src/layout/strategies/flex/alignment.d.ts +10 -0
  28. package/dist/src/layout/strategies/flex/alignment.js +91 -0
  29. package/dist/src/layout/strategies/flex/distributor.d.ts +5 -0
  30. package/dist/src/layout/strategies/flex/distributor.js +56 -0
  31. package/dist/src/layout/strategies/flex/line-builder.d.ts +5 -0
  32. package/dist/src/layout/strategies/flex/line-builder.js +55 -0
  33. package/dist/src/layout/strategies/flex/types.d.ts +27 -0
  34. package/dist/src/layout/strategies/flex/types.js +2 -0
  35. package/dist/src/layout/strategies/flex/utils.d.ts +12 -0
  36. package/dist/src/layout/strategies/flex/utils.js +113 -0
  37. package/dist/src/layout/strategies/flex.js +4 -308
  38. package/dist/src/layout/strategies/grid.js +0 -3
  39. package/dist/src/layout/strategies/table.js +85 -58
  40. package/dist/src/layout/utils/text-metrics.js +16 -8
  41. package/dist/src/pdf/font/embedder.js +3 -3
  42. package/dist/src/pdf/font/font-subset.js +1 -3
  43. package/dist/src/pdf/font/to-unicode.js +16 -16
  44. package/dist/src/pdf/layout-tree-builder.js +15 -9
  45. package/dist/src/pdf/renderer/box-painter.js +74 -9
  46. package/dist/src/pdf/renderers/text-renderer.d.ts +4 -2
  47. package/dist/src/pdf/renderers/text-renderer.js +52 -2
  48. package/dist/src/pdf/types.d.ts +16 -1
  49. package/dist/src/pdf/utils/clip-path-resolver.js +28 -12
  50. package/dist/src/pdf/utils/mask-resolver.d.ts +7 -0
  51. package/dist/src/pdf/utils/mask-resolver.js +25 -0
  52. package/dist/src/pdf/utils/node-text-run-factory.d.ts +2 -1
  53. package/dist/src/pdf/utils/node-text-run-factory.js +5 -26
  54. package/dist/src/pdf/utils/rounded-rect-to-path.d.ts +7 -0
  55. package/dist/src/pdf/utils/rounded-rect-to-path.js +86 -0
  56. package/dist/src/render/offset.d.ts +5 -0
  57. package/dist/src/render/offset.js +93 -9
  58. package/dist/src/text/line-breaker.js +31 -0
  59. package/dist/tests/css/clip-path-parser.spec.js +15 -8
  60. package/dist/tests/environment/path-resolution.spec.js +2 -1
  61. package/dist/tests/helpers/ai-layout-diagnostics.js +6 -6
  62. package/dist/tests/layout/container-query-units.spec.js +0 -7
  63. package/dist/tests/layout/inline-background-alignment.spec.js +6 -6
  64. package/dist/tests/layout/table-image-cell.spec.js +95 -0
  65. package/dist/tests/pdf/alignments.spec.js +12 -12
  66. package/dist/tests/pdf/clip-path.spec.js +3 -1
  67. package/dist/tests/pdf/form-text-encoding.spec.js +1 -1
  68. package/dist/tests/pdf/svg-stroke-dash.spec.js +8 -8
  69. package/dist/tests/pdf/text-transform-matrix.spec.js +1 -1
  70. package/dist/tests/pdf/xref-integrity.spec.js +1 -1
  71. package/dist/tests/verify-subset-multi.spec.js +14 -14
  72. package/dist/tests/verify-subset.spec.js +12 -12
  73. package/package.json +89 -71
  74. package/dist/src/image/js-png-backend.d.ts +0 -7
  75. package/dist/src/image/js-png-backend.js +0 -9
  76. package/dist/src/image/png-backend.d.ts +0 -5
  77. package/dist/src/image/png-wasm-loader.d.ts +0 -5
  78. package/dist/src/image/png-wasm-loader.js +0 -59
  79. package/dist/src/image/wasm/png_decoder_wasm.d.ts +0 -8
  80. package/dist/src/image/wasm/png_decoder_wasm.js +0 -24
  81. package/dist/src/image/wasm/png_decoder_wasm_bg.js +0 -16
  82. package/dist/src/image/wasm-png-backend.d.ts +0 -6
  83. package/dist/src/image/wasm-png-backend.js +0 -17
  84. package/dist/src/layout/table/cell_layout.d.ts +0 -2
  85. package/dist/src/layout/table/cell_layout.js +0 -26
  86. package/dist/tests/image/png-backend.spec.d.ts +0 -1
  87. package/dist/tests/image/png-backend.spec.js +0 -34
  88. package/dist/tests/pdf/font-subset-registry-key.spec.d.ts +0 -1
  89. package/dist/tests/pdf/font-subset-registry-key.spec.js +0 -66
  90. package/dist/tests/pdf/header-footer.spec.d.ts +0 -1
  91. package/dist/tests/pdf/header-footer.spec.js +0 -46
  92. /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 [_, data] of tables) {
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 children = node.children.map((child) => convertNode(child, state, textGradient));
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
- const clipCommands = buildClipPathCommands(box.clipPath);
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
- paintBackground(painter, box);
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 || clipPath.type !== "polygon" || clipPath.points.length < 3) {
253
+ if (!clipPath) {
209
254
  return null;
210
255
  }
211
- const [first, ...rest] = clipPath.points;
212
- const commands = [{ type: "moveTo", x: first.x, y: first.y }];
213
- for (const point of rest) {
214
- commands.push({ type: "lineTo", x: point.x, y: point.y });
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
- commands.push({ type: "closePath" });
217
- return commands;
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],
@@ -50,10 +50,18 @@ export interface Radius {
50
50
  bottomRight: CornerRadius;
51
51
  bottomLeft: CornerRadius;
52
52
  }
53
- export interface ClipPath {
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 || clip.type !== "polygon" || !clip.points.length) {
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
- const points = clip.points.map((point) => {
11
- const x = resolveClipLength(point.x, referenceRect.width);
12
- const y = resolveClipLength(point.y, referenceRect.height);
13
- return {
14
- x: referenceRect.x + x,
15
- y: referenceRect.y + y,
16
- };
17
- });
18
- if (points.some((p) => !Number.isFinite(p.x) || !Number.isFinite(p.y))) {
19
- return undefined;
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 { type: "polygon", points };
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, borderBox, contentBox, textColor, decorations, transform, fallbackColor, fontResolver, textGradient } = context;
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 (transform && textRuns.length > 0) {
23
- // applyTransformToTextRuns(textRuns, transform, borderBox);
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
+ }