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
4 changes: 4 additions & 0 deletions src/css/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export interface StyleAccumulator {
right?: LengthLike;
bottom?: LengthLike;
left?: LengthLike;
opacity?: number;
}

export interface StyleProperties {
Expand Down Expand Up @@ -210,6 +211,7 @@ export interface StyleProperties {
breakInside: string;
widows: number;
orphans: number;
opacity: number;
}

const defaultStyle = BrowserDefaults.createBaseDefaults() as StyleProperties;
Expand Down Expand Up @@ -296,6 +298,7 @@ export class ComputedStyle implements StyleProperties {
breakInside: string;
widows: number;
orphans: number;
opacity: number;

constructor(init?: Partial<StyleProperties>) {
const data: StyleProperties = {
Expand Down Expand Up @@ -386,6 +389,7 @@ export class ComputedStyle implements StyleProperties {
this.breakInside = data.breakInside;
this.widows = data.widows;
this.orphans = data.orphans;
this.opacity = data.opacity;
this.textAlign = init?.textAlign ?? undefined;
this.verticalAlign = init?.verticalAlign ?? undefined;
this.textDecorationLine = init?.textDecorationLine ?? defaultStyle.textDecorationLine;
Expand Down
8 changes: 5 additions & 3 deletions src/pdf/layout-tree-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,9 +366,11 @@ function convertNode(node: LayoutNode, state: { counter: number }): RenderBox {
// Handle background (colors, gradients, images)
const background = handleBackground(node, borderBox, paddingBox, contentBox);

const position = node.style.position;
const zIndex = typeof node.style.zIndex === "number" ? node.style.zIndex : 0;
const establishesStackingContext =
typeof node.style.zIndex === "number" && node.style.position !== Position.Static;
const opacity = typeof node.style.opacity === "number" ? node.style.opacity : 1;
const isPositioned = position !== Position.Static;
const establishesStackingContext = (isPositioned && typeof node.style.zIndex === "number") || opacity < 1;

return {
id,
Expand All @@ -392,7 +394,7 @@ function convertNode(node: LayoutNode, state: { counter: number }): RenderBox {
left: resolveLength(node.style.borderLeft, Math.max(node.box.contentWidth, 0), { auto: "zero" }),
},
borderRadius,
opacity: 1,
opacity,
overflow: mapOverflow(node.style.overflowX ?? OverflowMode.Visible),
textRuns,
decorations: decorations ?? {},
Expand Down
6 changes: 6 additions & 0 deletions src/pdf/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ export function paginateTree(root: RenderBox, options: PaginationOptions): Layou

const decorations: DecorationCommand[] = []; // Placeholder until decoration pagination is implemented

const pageRoot: RenderBox = {
...root,
children: paintOrder,
};

pages.push({
root: pageRoot,
paintOrder,
floatLayerOrder: [],
flowContentOrder,
Expand Down
166 changes: 41 additions & 125 deletions src/pdf/renderer/page-paint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { pickFooterVariant, pickHeaderVariant, paintHeaderFooter, type HeaderFoo
import { PagePainter, type PainterResult } from "../page-painter.js";
import type { FontRegistry } from "../font/font-registry.js";
import { LayerMode, NodeKind } from "../types.js";
import type { LayoutPageTree, PageSize, Radius, RenderBox, RGBA, Rect, TextPaintOptions } from "../types.js";
import type { LayoutPageTree, PageSize, RenderBox, RGBA, Rect, TextPaintOptions } from "../types.js";
import { paintBoxShadows } from "./paint-box-shadows.js";
import { clampRadiusComponent, shrinkRadius } from "./radius.js";
import { renderSvgBox } from "../svg/render-svg.js";
import { sortBoxesByZIndex } from "./sort-by-z-index.js";
import { paintSingleBackground, paintSingleBorder } from "./paint-utils.js";

export interface PagePaintInput {
readonly pageTree: LayoutPageTree;
Expand All @@ -22,6 +23,38 @@ export interface PagePaintInput {
readonly pageBackground?: RGBA;
}

function flattenBoxTree(root: RenderBox): RenderBox[] {
const result: RenderBox[] = [];
const queue = [root];
while (queue.length > 0) {
const box = queue.shift();
if (box) {
result.push(box);
queue.push(...box.children);
}
}
return result;
}

async function paintBox(painter: PagePainter, box: RenderBox): Promise<void> {
paintBoxShadows(painter, [box], false); // Inset shadows
paintSingleBackground(painter, box);
paintBoxShadows(painter, [box], true); // Outset shadows
paintSingleBorder(painter, box);

if (box.kind === NodeKind.Svg || (box.tagName === "svg" && box.customData?.svg)) {
await renderSvgBox(painter, box);
}

if (box.image) {
painter.drawImage(box.image, box.contentBox);
}

for (const run of box.textRuns) {
await painter.drawTextRun(run);
}
}

export async function paintLayoutPage({
pageTree,
pageNumber,
Expand All @@ -47,13 +80,12 @@ export async function paintLayoutPage({
await paintHeaderFooter(painter, headerVariant, footerVariant, tokens, pageNumber, totalPages, headerFooterTextOptions, true);
}

paintBoxShadows(painter, pageTree.paintOrder, false);
paintBackgrounds(painter, pageTree.paintOrder);
paintBoxShadows(painter, pageTree.paintOrder, true);
paintBorders(painter, pageTree.paintOrder);
await paintSvg(painter, pageTree.flowContentOrder);
paintImages(painter, pageTree.flowContentOrder);
await paintText(painter, pageTree.flowContentOrder);
const allBoxes = flattenBoxTree(pageTree.root);
const sortedBoxes = sortBoxesByZIndex(allBoxes);

for (const box of sortedBoxes) {
await paintBox(painter, box);
}

if (headerFooterLayout.layerMode === LayerMode.Over) {
await paintHeaderFooter(painter, headerVariant, footerVariant, tokens, pageNumber, totalPages, headerFooterTextOptions, false);
Expand All @@ -62,122 +94,6 @@ export async function paintLayoutPage({
return painter.result();
}

async function paintText(painter: PagePainter, boxes: RenderBox[]): Promise<void> {
for (const box of boxes) {
for (const run of box.textRuns) {
await painter.drawTextRun(run);
}
}
}

function paintBackgrounds(painter: PagePainter, boxes: RenderBox[]): void {
for (const box of boxes) {
const background = box.background;
if (!background) {
continue;
}

const paintArea = determineBackgroundPaintArea(box);
if (!paintArea) {
continue;
}

if (background.color) {
painter.fillRoundedRect(paintArea.rect, paintArea.radius, background.color);
}

if (background.gradient) {
painter.fillRoundedRect(paintArea.rect, paintArea.radius, background.gradient as any);
}

if (background.image) {
paintBackgroundImageLayer(painter, background.image, paintArea.rect, paintArea.radius);
}
}
}

function paintBackgroundImageLayer(
painter: PagePainter,
layer: RenderBox["background"]["image"],
clipRect: Rect,
clipRadius: Radius,
): void {
if (!layer) {
return;
}
if (layer.repeat && layer.repeat !== "no-repeat") {
console.warn(`Background repeat mode "${layer.repeat}" is not fully supported yet. Rendering first tile only.`);
}
painter.drawBackgroundImage(layer.image, layer.rect, clipRect, clipRadius);
}

function paintBorders(painter: PagePainter, boxes: RenderBox[]): void {
for (const box of boxes) {
const color = box.borderColor;
if (!color) {
continue;
}
const { border } = box;
if (!hasVisibleBorder(border)) {
continue;
}
const outerRect = box.borderBox;
const innerRect = {
x: outerRect.x + border.left,
y: outerRect.y + border.top,
width: Math.max(outerRect.width - border.left - border.right, 0),
height: Math.max(outerRect.height - border.top - border.bottom, 0),
};
const innerRadius = shrinkRadius(box.borderRadius, border.top, border.right, border.bottom, border.left);
if (innerRect.width <= 0 || innerRect.height <= 0) {
painter.fillRoundedRect(outerRect, box.borderRadius, color);
} else {
painter.fillRoundedRectDifference(outerRect, box.borderRadius, innerRect, innerRadius, color);
}
}
}

function determineBackgroundPaintArea(box: RenderBox): { rect: Rect; radius: Radius } | null {
const rect = box.borderBox ?? box.paddingBox ?? box.contentBox;
if (!rect) {
return null;
}

if (rect === box.borderBox) {
return { rect, radius: box.borderRadius };
}

let radius = shrinkRadius(box.borderRadius, box.border.top, box.border.right, box.border.bottom, box.border.left);
if (rect === box.contentBox) {
radius = shrinkRadius(radius, box.padding.top, box.padding.right, box.padding.bottom, box.padding.left);
}

return { rect, radius };
}

function hasVisibleBorder(border: RenderBox["border"]): boolean {
return border.top > 0 || border.right > 0 || border.bottom > 0 || border.left > 0;
}

function paintImages(painter: PagePainter, boxes: RenderBox[]): void {
for (const box of boxes) {
if (box.image) {
painter.drawImage(box.image, box.contentBox);
}
}
}

async function paintSvg(painter: PagePainter, boxes: RenderBox[]): Promise<void> {
for (const box of boxes) {
if (box.kind === NodeKind.Svg || (box.tagName === "svg" && box.customData?.svg)) {
await renderSvgBox(painter, box);
}
if (box.children.length > 0) {
await paintSvg(painter, box.children);
}
}
}

function paintPageBackground(painter: PagePainter, color: RGBA | undefined, widthPx: number, heightPx: number, offsetY: number): void {
if (!color) {
return;
Expand Down
77 changes: 77 additions & 0 deletions src/pdf/renderer/paint-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { PagePainter } from "../page-painter.js";
import type { RenderBox, Rect, Radius } from "../types.js";
import { shrinkRadius } from "./radius.js";

function determineBackgroundPaintArea(box: RenderBox): { rect: Rect; radius: Radius } | null {
const rect = box.borderBox ?? box.paddingBox ?? box.contentBox;
if (!rect) {
return null;
}

if (rect === box.borderBox) {
return { rect, radius: box.borderRadius };
}

let radius = shrinkRadius(box.borderRadius, box.border.top, box.border.right, box.border.bottom, box.border.left);
if (rect === box.contentBox) {
radius = shrinkRadius(radius, box.padding.top, box.padding.right, box.padding.bottom, box.padding.left);
}

return { rect, radius };
}

export function paintSingleBackground(painter: PagePainter, box: RenderBox): void {
const background = box.background;
if (!background) {
return;
}

const paintArea = determineBackgroundPaintArea(box);
if (!paintArea) {
return;
}

if (background.color) {
painter.fillRoundedRect(paintArea.rect, paintArea.radius, background.color);
}

if (background.gradient) {
painter.fillRoundedRect(paintArea.rect, paintArea.radius, background.gradient as any);
}

if (background.image) {
const layer = background.image;
if (layer.repeat && layer.repeat !== "no-repeat") {
console.warn(`Background repeat mode "${layer.repeat}" is not fully supported yet. Rendering first tile only.`);
}
painter.drawBackgroundImage(layer.image, layer.rect, paintArea.rect, paintArea.radius);
}
}

function hasVisibleBorder(border: RenderBox["border"]): boolean {
return border.top > 0 || border.right > 0 || border.bottom > 0 || border.left > 0;
}

export function paintSingleBorder(painter: PagePainter, box: RenderBox): void {
const color = box.borderColor;
if (!color) {
return;
}
const { border } = box;
if (!hasVisibleBorder(border)) {
return;
}
const outerRect = box.borderBox;
const innerRect = {
x: outerRect.x + border.left,
y: outerRect.y + border.top,
width: Math.max(outerRect.width - border.left - border.right, 0),
height: Math.max(outerRect.height - border.top - border.bottom, 0),
};
const innerRadius = shrinkRadius(box.borderRadius, border.top, border.right, border.bottom, border.left);
if (innerRect.width <= 0 || innerRect.height <= 0) {
painter.fillRoundedRect(outerRect, box.borderRadius, color);
} else {
painter.fillRoundedRectDifference(outerRect, box.borderRadius, innerRect, innerRadius, color);
}
}
Loading