Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/css/clip-path-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ export interface ClipPathPolygon {
referenceBox?: ClipPathReferenceBox;
}

export type ClipPath = ClipPathPolygon;
export interface ClipPathPath {
type: "path";
commands: string;
referenceBox?: ClipPathReferenceBox;
}

export type ClipPath = ClipPathPolygon | ClipPathPath;
22 changes: 22 additions & 0 deletions src/css/parsers/clip-path-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,31 @@ function parseClipPathValue(value: string): ClipPath | undefined {
if (polygon) {
return polygon;
}

const path = parsePath(normalized);
if (path) {
return path;
}

return undefined;
}

function parsePath(input: string): ClipPath | undefined {
const match = /^path\s*\(\s*['"]?([^'"]+)['"]?\s*\)$/i.exec(input);
if (!match) {
return undefined;
}
const commands = match[1].trim();
if (!commands) {
return undefined;
}
return {
type: "path",
commands,
referenceBox: "border-box",
};
}

function parsePolygon(input: string): ClipPathPolygon | undefined {
const match = /^polygon\s*\((.+)\)$/i.exec(input);
if (!match) {
Expand Down
102 changes: 77 additions & 25 deletions src/layout/strategies/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,36 +120,41 @@ export class TableLayoutStrategy implements LayoutStrategy {
const tableBorderLeft = numericBorder(node.style.borderLeft);

// Collapse table border with outer cells
const processedTop = new Set<LayoutNode>();
const processedBottom = new Set<LayoutNode>();
const processedLeft = new Set<LayoutNode>();
const processedRight = new Set<LayoutNode>();

for (let c = 0; c < numCols; c++) {
// Top edge cells collapse with table top border
const topCell = grid[0][c];
if (topCell && this.isOriginCell(topCell, 0, c)) {
if (topCell && !processedTop.has(topCell)) {
const cellTop = numericBorder(topCell.style.borderTop);
const shared = Math.max(cellTop, tableBorderTop);
topCell.style.borderTop = shared;
topCell.style.borderTop = Math.max(cellTop, tableBorderTop);
processedTop.add(topCell);
}
// Bottom edge cells collapse with table bottom border
const bottomCell = grid[numRows - 1][c];
if (bottomCell && this.isOriginCell(bottomCell, numRows - 1, c)) {
if (bottomCell && !processedBottom.has(bottomCell) && this.isRowBoundary(bottomCell, numRows - 1)) {
const cellBottom = numericBorder(bottomCell.style.borderBottom);
const shared = Math.max(cellBottom, tableBorderBottom);
bottomCell.style.borderBottom = shared;
bottomCell.style.borderBottom = Math.max(cellBottom, tableBorderBottom);
processedBottom.add(bottomCell);
}
}
for (let r = 0; r < numRows; r++) {
// Left edge cells collapse with table left border
const leftCell = grid[r][0];
if (leftCell && this.isOriginCell(leftCell, r, 0)) {
if (leftCell && !processedLeft.has(leftCell)) {
const cellLeft = numericBorder(leftCell.style.borderLeft);
const shared = Math.max(cellLeft, tableBorderLeft);
leftCell.style.borderLeft = shared;
leftCell.style.borderLeft = Math.max(cellLeft, tableBorderLeft);
processedLeft.add(leftCell);
}
// Right edge cells collapse with table right border
const rightCell = grid[r][numCols - 1];
if (rightCell && this.isOriginCell(rightCell, r, numCols - 1)) {
if (rightCell && !processedRight.has(rightCell) && this.isColumnBoundary(rightCell, numCols - 1)) {
const cellRight = numericBorder(rightCell.style.borderRight);
const shared = Math.max(cellRight, tableBorderRight);
rightCell.style.borderRight = shared;
rightCell.style.borderRight = Math.max(cellRight, tableBorderRight);
processedRight.add(rightCell);
}
}

Expand All @@ -165,16 +170,25 @@ export class TableLayoutStrategy implements LayoutStrategy {
const upper = grid[r][c];
const lower = grid[r + 1][c];
if (!upper || !lower) continue;
if (upper === lower) continue;
if (upper === lower) {
// Se as células são as mesmas (rowspan), não há borda compartilhada para processar AQUI
// mas precisamos garantir que a próxima linha saiba que esta célula continua.
continue;
}
if (!this.isRowBoundary(upper, r)) continue;
const upperBottom = numericBorder(upper.style.borderBottom);
const lowerTop = numericBorder(lower.style.borderTop);
const shared = Math.max(upperBottom, lowerTop);

if (shared > 0) {
// "Winner" takes the color. If widths are equal, prefer the upper one (top-down bias)
if (upperBottom >= lowerTop && upper.style.borderColor) {
lower.style.borderColor = upper.style.borderColor;
}
}

lower.style.borderTop = shared;
upper.style.borderBottom = 0;
if (lower.style.borderColor === undefined && upper.style.borderColor !== undefined) {
lower.style.borderColor = upper.style.borderColor;
}
}
}
// Resolve horizontal shared borders between adjacent columns
Expand All @@ -188,11 +202,16 @@ export class TableLayoutStrategy implements LayoutStrategy {
const leftRight = numericBorder(left.style.borderRight);
const rightLeft = numericBorder(right.style.borderLeft);
const shared = Math.max(leftRight, rightLeft);

if (shared > 0) {
// "Winner" takes the color. If widths are equal, prefer the left one (left-to-right bias)
if (leftRight >= rightLeft && left.style.borderColor) {
right.style.borderColor = left.style.borderColor;
}
}

right.style.borderLeft = shared;
left.style.borderRight = 0;
if (right.style.borderColor === undefined && left.style.borderColor !== undefined) {
right.style.borderColor = left.style.borderColor;
}
}
}
}
Expand Down Expand Up @@ -470,11 +489,15 @@ export class TableLayoutStrategy implements LayoutStrategy {

const minContentWidths = new Array(numCols).fill(0);

// First pass: resolve widths for cells with colspan = 1
for (let r = 0; r < grid.length; r++) {
for (let c = 0; c < numCols; c++) {
const cell = grid[r][c];
if (!cell || !this.isOriginCell(cell, r, c)) continue;

const colSpan = Math.min(this.cellColSpan(cell), numCols - c);
if (colSpan !== 1) continue;

let maxIntrinsicWidth = 0;
if (cell.intrinsicInlineSize) {
maxIntrinsicWidth = cell.intrinsicInlineSize;
Expand All @@ -487,14 +510,43 @@ export class TableLayoutStrategy implements LayoutStrategy {

const horizontalExtras = horizontalNonContent(cell, tableWidth);
const cellMinWidth = maxIntrinsicWidth + horizontalExtras;
minContentWidths[c] = Math.max(minContentWidths[c], cellMinWidth);
}
}

// Second pass: resolve widths for cells with colspan > 1
for (let r = 0; r < grid.length; r++) {
for (let c = 0; c < numCols; c++) {
const cell = grid[r][c];
if (!cell || !this.isOriginCell(cell, r, c)) continue;

const colSpan = Math.min(this.cellColSpan(cell), numCols - c);
if (colSpan <= 1) continue;

if (colSpan === 1) {
minContentWidths[c] = Math.max(minContentWidths[c], cellMinWidth);
} else {
const share = cellMinWidth / colSpan;
for (let offset = 0; offset < colSpan; offset++) {
minContentWidths[c + offset] = Math.max(minContentWidths[c + offset], share);
let maxIntrinsicWidth = 0;
if (cell.intrinsicInlineSize) {
maxIntrinsicWidth = cell.intrinsicInlineSize;
}
cell.walk((node) => {
if (node.intrinsicInlineSize !== undefined) {
maxIntrinsicWidth = Math.max(maxIntrinsicWidth, node.intrinsicInlineSize);
}
});

const horizontalExtras = horizontalNonContent(cell, tableWidth);
const cellMinWidth = maxIntrinsicWidth + horizontalExtras;

// Check if current spanned columns already satisfy the min width
let currentSpanWidth = 0;
for (let i = 0; i < colSpan; i++) {
currentSpanWidth += minContentWidths[c + i];
}

if (cellMinWidth > currentSpanWidth) {
const extra = cellMinWidth - currentSpanWidth;
const share = extra / colSpan;
for (let i = 0; i < colSpan; i++) {
minContentWidths[c + i] += share;
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/pdf/layout-tree-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
resolveTextGradientLayer,
} from "./utils/background-layer-resolver.js";
import { resolveClipPath } from "./utils/clip-path-resolver.js";
import { resolveMaskGradient } from "./utils/mask-resolver.js";
import { resolveMaskGradients } from "./utils/mask-resolver.js";
import { parseTransform } from "../transform/css-parser.js";
import { buildNodeTextRuns } from "./utils/node-text-run-factory.js";
import type { FontResolver } from "../fonts/types.js";
Expand Down Expand Up @@ -148,7 +148,7 @@ function convertNode(

const background = resolveBackgroundLayers(node, { borderBox, paddingBox, contentBox });
const backgroundClip = node.style.backgroundLayers?.some(l => l.clip === "text") ? "text" : undefined;
const maskGradient = resolveMaskGradient(node, { borderBox, paddingBox, contentBox });
const maskLayers = resolveMaskGradients(node, { borderBox, paddingBox, contentBox });

const ownTextGradient = resolveTextGradientLayer(node, { borderBox, paddingBox, contentBox });
const textGradient = ownTextGradient ?? inheritedTextGradient;
Expand Down Expand Up @@ -256,7 +256,8 @@ function convertNode(
breakInside: node.style.breakInside,
color: textColor,
mask: node.style.mask,
maskGradient,
maskGradient: maskLayers.length > 0 ? maskLayers[0] : undefined,
maskLayers,
background,
backgroundClip,
clipPath,
Expand Down
65 changes: 60 additions & 5 deletions src/pdf/renderer/box-painter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,17 @@ export async function paintBoxAtomic(painter: PagePainter, box: RenderBox): Prom
}

let clipCommands = buildClipPathCommands(box.clipPath);
if (!clipCommands && box.maskGradient && box.maskGradient.gradient.type === "radial") {
// MVP: Aproximação de máscara radial usando clipping path circular
clipCommands = buildCircularClipPath(box.maskGradient.rect);
if (!clipCommands && box.maskLayers && box.maskLayers.length > 0) {
const masks = box.maskLayers.filter(l => l.gradient.type === "radial");
if (masks.length > 0) {
clipCommands = [];
for (const m of masks) {
clipCommands.push(...buildCircularClipPath(m.rect));
}
}
}

const hasClip = !!clipCommands;
const hasClip = !!clipCommands && clipCommands.length > 0;
if (hasClip && clipCommands) {
painter.beginClipPath(clipCommands);
}
Expand Down Expand Up @@ -320,7 +325,57 @@ function buildClipPathCommands(clipPath: RenderBox["clipPath"]): PathCommand[] |
}

function paintDropShadows(painter: PagePainter, box: RenderBox, shadows: import("../types.js").ShadowLayer[]): void {
// Usa o borderBox como base da sombra (aproximação para drop-shadow)
// Para drop-shadow em caminhos complexos (clip-path), precisamos de uma abordagem baseada em path
const clipCommands = buildClipPathCommands(box.clipPath);

if (clipCommands && clipCommands.length > 0) {
for (const shadow of shadows) {
if (shadow.color.a <= 0) continue;

const blur = Math.max(0, shadow.blur);
const steps = blur > 0 ? Math.max(3, Math.ceil(blur / 1.5)) : 1;
const baseAlpha = shadow.color.a;

// Pintamos várias vezes com offsets variados para simular blur (distribuição radial)
for (let i = 0; i < steps; i++) {
const fraction = steps > 1 ? i / (steps - 1) : 0;
const angle = (i / steps) * Math.PI * 2;
const blurRadius = (blur * 0.5) * fraction;
const dx = shadow.offsetX + Math.cos(angle) * blurRadius;
const dy = shadow.offsetY + Math.sin(angle) * blurRadius;

// Distribuição de opacidade: dividimos a opacidade total pelos steps
// mas damos um pouco mais de peso para as camadas internas.
const weight = (steps - i) / ((steps * (steps + 1)) / 2);
const opacity = baseAlpha * weight * (steps / 1.5);

const offsetCommands = clipCommands.map(cmd => {
if (cmd.type === "moveTo" || cmd.type === "lineTo") {
return { ...cmd, x: cmd.x + dx, y: cmd.y + dy };
}
if (cmd.type === "curveTo") {
return {
...cmd,
x1: cmd.x1 + dx,
y1: cmd.y1 + dy,
x2: cmd.x2 + dx,
y2: cmd.y2 + dy,
x: cmd.x + dx,
y: cmd.y + dy
};
}
return cmd;
});

painter.beginOpacityScope(Math.min(1, opacity));
painter.fillPath(offsetCommands, shadow.color);
painter.endOpacityScope(Math.min(1, opacity));
}
}
return;
}

// Usa o borderBox como base da sombra (aproximação para drop-shadow retangular)
const virtualBox: Partial<RenderBox> = {
borderBox: box.borderBox,
borderRadius: box.borderRadius,
Expand Down
1 change: 1 addition & 0 deletions src/pdf/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ export interface RenderBox {
color?: RGBA;
mask?: string;
maskGradient?: GradientBackground;
maskLayers?: GradientBackground[];
backgroundClip?: "border-box" | "padding-box" | "content-box" | "text";
transform?: TextMatrix;

Expand Down
Loading