From ab2e263a2ccf8b9f8300c02f2fab079a68d009e7 Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Tue, 29 Sep 2026 11:06:20 +0200 Subject: [PATCH] fix(pdf): draw azurill's timeline line under forme Forme gives an absolute box no height from its top and bottom offsets, so the bar that react-pdf stretched down each section's items vanished. The converter now draws such a bar as the left border of a box around the other children, which is as tall as they are and is painted on every page the section reaches. --- packages/pdf/src/forme/to-forme.tsx | 68 ++++++++++++++++++- .../date-layout/all-templates.json | 4 +- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/packages/pdf/src/forme/to-forme.tsx b/packages/pdf/src/forme/to-forme.tsx index 52b743852..7e0caa7f2 100644 --- a/packages/pdf/src/forme/to-forme.tsx +++ b/packages/pdf/src/forme/to-forme.tsx @@ -351,6 +351,70 @@ function unreverseRow(spread: { style: FormeStyle; children: ReactNode[] }): { }; } +// What lays a container's children out, as opposed to what sizes, spaces and paints the container. +const CONTAINER_KEYS = new Set([ + "flexDirection", + "flexWrap", + "justifyContent", + "alignItems", + "alignContent", + "rowGap", + "columnGap", + "gap", +]); + +/** + * Forme 0.25 gives an absolute box no height from its `top` and `bottom`, where Yoga stretched it between them + * (Azurill's timeline line). A plain bar down the whole content box becomes the left border of a box around the other + * children instead: that box is as tall as they are, and its border is drawn on every page it reaches. A box that + * merely stretches along a row keeps its whole height on the page where the row breaks. + */ +function barAsBorder(spread: { style: FormeStyle; children: ReactNode[] }): { + style: FormeStyle; + children: ReactNode[]; +} { + const isBar = (child: ReactNode): child is ReactElement<{ style: FormeStyle }> => { + if (!isValidElement<{ style?: FormeStyle }>(child)) return false; + const { position, height, top, bottom, left, width, backgroundColor } = child.props.style ?? {}; + return ( + position === "absolute" && + height === undefined && + top === 0 && + bottom === 0 && + typeof left === "number" && + typeof width === "number" && + width > 0 && + typeof backgroundColor === "string" + ); + }; + const bar = spread.children.find(isBar); + if (!bar) return spread; + const { left, width, backgroundColor } = bar.props.style as { left: number; width: number; backgroundColor: string }; + + const outer: Record = {}; + const inner: Record = { position: "relative", flexGrow: 1 }; + for (const [property, value] of Object.entries(spread.style)) + (CONTAINER_KEYS.has(property) ? inner : outer)[property] = value; + // Forme strokes a border centred on the box's edge, so the edge sits half the bar's width in. + const edge = left + width / 2; + inner.marginLeft = -(edge + width); + + const content = createElement( + FormeView, + { key: "content", style: inner as FormeStyle }, + ...spread.children.filter((child) => child !== bar), + ); + const bordered = createElement( + FormeView, + { + key: "bar", + style: { flexGrow: 1, marginLeft: edge, borderLeftWidth: width, borderLeftColor: backgroundColor }, + }, + content, + ); + return { style: outer as FormeStyle, children: [bordered] }; +} + function convertChildren(children: HostNode[], context: Context): ReactNode[] { return children.map((child, index) => convertNode(child, context, index)); } @@ -378,8 +442,8 @@ function convertNode(node: HostNode, parentContext: Context, key: number): React const converted = convertStyle(props.style, context, node); const { style } = converted; if (converted.hidden) return null; - const spread = unreverseRow( - spreadRowGap(style, convertChildren(node.children, childContext(context, converted)), context), + const spread = barAsBorder( + unreverseRow(spreadRowGap(style, convertChildren(node.children, childContext(context, converted)), context)), ); let children = spread.children; const viewStyle = flowStyle(props, spread.style); diff --git a/packages/pdf/test-artifacts/date-layout/all-templates.json b/packages/pdf/test-artifacts/date-layout/all-templates.json index 0b582d695..b72a3eb24 100644 --- a/packages/pdf/test-artifacts/date-layout/all-templates.json +++ b/packages/pdf/test-artifacts/date-layout/all-templates.json @@ -4,8 +4,8 @@ "textItemCount": 87, "missingMarkers": [], "rasterSha256": [ - "1dde504c195a01a4195ea35631bb7fbe7fb0d5322fca096c0fb5c4c06fe987ea", - "9acc4ff81096bd83c04058e00e905963aa4d21b7683e0c8c757924f5c394f61d" + "689ba999c173466faab5f60ad03c081e5e7a4def9a7acbfa88583652030c42a9", + "e8b5f42ff68f9527c2ce103bddbf5453d8e0220d68c975384e1e93f56f01a764" ], "coordinates": { "EXP_LONG": [