@openshift-migration-advisor/shared-components 0.25.0-6a4104ca5ed2 → 0.25.0-883f4c9ae996

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.
@@ -1 +1 @@
1
- {"version":3,"file":"captureChartElement.d.ts","sourceRoot":"","sources":["../../src/report/captureChartElement.ts"],"names":[],"mappings":"AAgCA;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,IAAI,CAqD3E;AAkOD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,iBAAiB,CAAC,CAgC5B"}
1
+ {"version":3,"file":"captureChartElement.d.ts","sourceRoot":"","sources":["../../src/report/captureChartElement.ts"],"names":[],"mappings":"AAgCA;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,IAAI,CAqD3E;AAiPD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,iBAAiB,CAAC,CAkC5B"}
@@ -88,30 +88,56 @@ function applyComputedPadding(source, dest) {
88
88
  dest.style.paddingBottom = computed.paddingBottom;
89
89
  dest.style.paddingLeft = computed.paddingLeft;
90
90
  }
91
- const CARD_BORDER_WIDTH_VAR = "--pf-v6-c-card--BorderWidth";
92
- function applyCardChrome(source, dest) {
91
+ function readCardBorder(source) {
92
+ if (source.style.borderWidth && source.style.borderStyle) {
93
+ return {
94
+ width: source.style.borderWidth,
95
+ style: source.style.borderStyle,
96
+ color: source.style.borderColor,
97
+ };
98
+ }
93
99
  const computed = window.getComputedStyle(source);
94
100
  const before = window.getComputedStyle(source, "::before");
95
- dest.style.borderRadius = computed.borderRadius;
96
- dest.style.backgroundColor = resolvedFill(computed.backgroundColor);
97
- dest.style.overflow = "hidden";
98
- const borderWidth = before.borderTopWidth && before.borderTopWidth !== "0px"
101
+ const width = before.borderTopWidth && before.borderTopWidth !== "0px"
99
102
  ? before.borderTopWidth
100
103
  : computed.borderTopWidth;
101
- const borderStyle = before.borderTopStyle && before.borderTopStyle !== "none"
104
+ const style = before.borderTopStyle && before.borderTopStyle !== "none"
102
105
  ? before.borderTopStyle
103
106
  : computed.borderTopStyle;
104
- const borderColor = before.borderTopColor && before.borderTopColor !== "rgba(0, 0, 0, 0)"
107
+ const color = before.borderTopColor && before.borderTopColor !== "rgba(0, 0, 0, 0)"
105
108
  ? before.borderTopColor
106
109
  : computed.borderTopColor;
107
- if (borderWidth !== "0px" && borderStyle !== "none") {
108
- dest.style.borderWidth = borderWidth;
109
- dest.style.borderStyle = borderStyle;
110
- dest.style.borderColor = borderColor;
110
+ if (width === "0px" || style === "none") {
111
+ return null;
111
112
  }
112
- // PF draws the border on ::before via this token. html2canvas still paints
113
- // that pseudo even if a stylesheet sets content:none, so collapse it here.
114
- dest.style.setProperty(CARD_BORDER_WIDTH_VAR, "0px");
113
+ return { width, style, color };
114
+ }
115
+ function applyCardChrome(source, dest) {
116
+ const computed = window.getComputedStyle(source);
117
+ dest.style.borderRadius = computed.borderRadius;
118
+ dest.style.backgroundColor = resolvedFill(computed.backgroundColor);
119
+ dest.style.overflow = "hidden";
120
+ const border = readCardBorder(source);
121
+ if (border) {
122
+ dest.style.borderWidth = border.width;
123
+ dest.style.borderStyle = border.style;
124
+ dest.style.borderColor = border.color;
125
+ }
126
+ }
127
+ function hideCardBeforePseudo() {
128
+ const style = document.createElement("style");
129
+ style.setAttribute("data-chart-export-hide-before", "");
130
+ style.textContent = `
131
+ [${CHART_EXPORT_CAPTURING_ATTR}] .pf-v6-c-card::before,
132
+ [${CHART_EXPORT_CAPTURING_ATTR}].pf-v6-c-card::before {
133
+ border: none !important;
134
+ box-shadow: none !important;
135
+ }
136
+ `;
137
+ document.head.append(style);
138
+ return () => {
139
+ style.remove();
140
+ };
115
141
  }
116
142
  function copyComputedPadding(node) {
117
143
  const previous = {
@@ -135,8 +161,6 @@ function copyCardChrome(node) {
135
161
  minHeight: node.style.minHeight,
136
162
  height: node.style.height,
137
163
  maxHeight: node.style.maxHeight,
138
- borderWidthVar: node.style.getPropertyValue(CARD_BORDER_WIDTH_VAR),
139
- borderWidthVarPriority: node.style.getPropertyPriority(CARD_BORDER_WIDTH_VAR),
140
164
  };
141
165
  applyCardChrome(node, node);
142
166
  node.style.minHeight = "0px";
@@ -215,12 +239,6 @@ function prepareCaptureLayout(element) {
215
239
  entry.node.style.minHeight = entry.minHeight;
216
240
  entry.node.style.height = entry.height;
217
241
  entry.node.style.maxHeight = entry.maxHeight;
218
- if (entry.borderWidthVar) {
219
- entry.node.style.setProperty(CARD_BORDER_WIDTH_VAR, entry.borderWidthVar, entry.borderWidthVarPriority);
220
- }
221
- else {
222
- entry.node.style.removeProperty(CARD_BORDER_WIDTH_VAR);
223
- }
224
242
  }
225
243
  for (const entry of previousPadding) {
226
244
  entry.node.style.paddingTop = entry.paddingTop;
@@ -234,6 +252,7 @@ export async function captureChartElement(element) {
234
252
  const restoreHidden = revealHiddenChartAncestors(element);
235
253
  const restoreLayout = prepareCaptureLayout(element);
236
254
  element.setAttribute(CHART_EXPORT_CAPTURING_ATTR, "");
255
+ const restoreCardBefore = hideCardBeforePseudo();
237
256
  try {
238
257
  await new Promise((resolve) => {
239
258
  requestAnimationFrame(() => resolve());
@@ -257,6 +276,7 @@ export async function captureChartElement(element) {
257
276
  });
258
277
  }
259
278
  finally {
279
+ restoreCardBefore();
260
280
  element.removeAttribute(CHART_EXPORT_CAPTURING_ATTR);
261
281
  restoreLayout();
262
282
  restoreHidden();
@@ -1 +1 @@
1
- {"version":3,"file":"chartExport.d.ts","sourceRoot":"","sources":["../../src/report/chartExport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAC/D,eAAO,MAAM,wBAAwB,6BAA6B,CAAC;AACnE,eAAO,MAAM,2BAA2B,yBAAyB,CAAC;AAElE,eAAO,MAAM,oBAAoB;;CAEvB,CAAC;AAEX,eAAO,MAAM,sBAAsB;;CAEzB,CAAC;AAEX,eAAO,MAAM,oBAAoB,EAAE,aAAkC,CAAC;AAEtE,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,WAAW,CAAC;IACrB,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC3C,CAAC;AAEF,wBAAgB,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAG7D;AAED,wBAAgB,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC,CASvD;AAED,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,eAAe,EAAE,CAKnB;AAqBD,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,eAAe,EAAE,EACzB,OAAO,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,iBAAiB,CAAC,EAC7D,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,IAAI,GACpD,kBAAkB,EAAE,CAyBtB;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,eAAe,EAAE,GACxB,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAgB,mCAAmC,CACjD,MAAM,EAAE,eAAe,EAAE,GACxB,eAAe,EAAE,CAcnB;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAcrE;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMvD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAU,GAAG,MAAM,CAEpE;AAWD;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,OAAO,EACb,WAAW,CAAC,EAAE,OAAO,GAAG,IAAI,GAC3B,OAAO,CAeT;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAS/D"}
1
+ {"version":3,"file":"chartExport.d.ts","sourceRoot":"","sources":["../../src/report/chartExport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAC/D,eAAO,MAAM,wBAAwB,6BAA6B,CAAC;AACnE,eAAO,MAAM,2BAA2B,yBAAyB,CAAC;AAElE,eAAO,MAAM,oBAAoB;;CAEvB,CAAC;AAEX,eAAO,MAAM,sBAAsB;;CAEzB,CAAC;AAEX,eAAO,MAAM,oBAAoB,EAAE,aAGlC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,WAAW,CAAC;IACrB,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC3C,CAAC;AAEF,wBAAgB,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAG7D;AAED,wBAAgB,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC,CASvD;AAED,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,eAAe,EAAE,CAKnB;AAqBD,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,eAAe,EAAE,EACzB,OAAO,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,iBAAiB,CAAC,EAC7D,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,IAAI,GACpD,kBAAkB,EAAE,CAyBtB;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,eAAe,EAAE,GACxB,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAgB,mCAAmC,CACjD,MAAM,EAAE,eAAe,EAAE,GACxB,eAAe,EAAE,CAcnB;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAcrE;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMvD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAU,GAAG,MAAM,CAEpE;AAWD;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,OAAO,EACb,WAAW,CAAC,EAAE,OAAO,GAAG,IAAI,GAC3B,OAAO,CAeT;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAS/D"}
@@ -7,7 +7,10 @@ export const chartExportHideProps = {
7
7
  export const chartExportScrollProps = {
8
8
  [CHART_EXPORT_SCROLL_ATTR]: "",
9
9
  };
10
- export const chartExportRootStyle = { height: "100%" };
10
+ export const chartExportRootStyle = {
11
+ width: "100%",
12
+ height: "100%",
13
+ };
11
14
  export function releaseCanvas(canvas) {
12
15
  canvas.width = 0;
13
16
  canvas.height = 0;
@@ -1 +1 @@
1
- {"version":3,"file":"htmlExport.d.ts","sourceRoot":"","sources":["../../src/report/htmlExport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,kBAAkB,EAAiB,MAAM,kBAAkB,CAAC;AAE1E,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhD;AAED,wBAAgB,eAAe,CAC7B,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,eAAe,EAAE,EACzB,WAAW,OAAa,GACvB,MAAM,CA0CR;AAED,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,kBAAkB,EAAE,EAC5B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CAiBf"}
1
+ {"version":3,"file":"htmlExport.d.ts","sourceRoot":"","sources":["../../src/report/htmlExport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,kBAAkB,EAAiB,MAAM,kBAAkB,CAAC;AAE1E,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhD;AAED,wBAAgB,eAAe,CAC7B,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,eAAe,EAAE,EACzB,WAAW,OAAa,GACvB,MAAM,CAwCR;AAED,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,kBAAkB,EAAE,EAC5B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CAiBf"}
@@ -32,10 +32,8 @@ export function buildHtmlReport(documentTitle, images, generatedAt = new Date())
32
32
  h2 { font-size: 18px; margin: 32px 0 12px; }
33
33
  .meta { color: #6a6e73; margin-bottom: 24px; }
34
34
  img {
35
- max-width: 100%;
35
+ width: 100%;
36
36
  height: auto;
37
- border: 1px solid #d2d2d2;
38
- border-radius: 8px;
39
37
  }
40
38
  </style>
41
39
  </head>
@@ -1 +1 @@
1
- {"version":3,"file":"pdfExport.d.ts","sourceRoot":"","sources":["../../src/report/pdfExport.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,kBAAkB,EAAiB,MAAM,kBAAkB,CAAC;AAO1E,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,kBAAkB,EAAE,EAC5B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CA+Df"}
1
+ {"version":3,"file":"pdfExport.d.ts","sourceRoot":"","sources":["../../src/report/pdfExport.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,kBAAkB,EAAiB,MAAM,kBAAkB,CAAC;AAQ1E,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,kBAAkB,EAAE,EAC5B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CAuDf"}
@@ -1,7 +1,8 @@
1
1
  import jsPDF from "jspdf";
2
2
  import { releaseCanvas } from "./chartExport.js";
3
- import { sliceCanvas, splitSegmentForPageHeight } from "./pdfPage.js";
3
+ import { fitPdfImageSize, placePdfBlock, sliceCanvas } from "./pdfPage.js";
4
4
  const MARGIN_MM = 10;
5
+ const GAP_MM = 8;
5
6
  /** Flatten captures onto white; jsPDF cannot embed transparent PNG alpha cleanly. */
6
7
  const PAGE_BACKGROUND = "#ffffff";
7
8
  export async function buildPdfFromCharts(charts, documentTitle) {
@@ -10,27 +11,23 @@ export async function buildPdfFromCharts(charts, documentTitle) {
10
11
  const pageHeight = pdf.internal.pageSize.getHeight();
11
12
  const contentWidth = pageWidth - MARGIN_MM * 2;
12
13
  const contentHeight = pageHeight - MARGIN_MM * 2;
14
+ const pageTop = MARGIN_MM;
15
+ const pageBottom = pageHeight - MARGIN_MM;
13
16
  addCoverPage(pdf, documentTitle, charts.map((chart) => chart.title), pageWidth, pageHeight);
17
+ let cursorY = null;
14
18
  for (const chart of charts) {
15
19
  const canvas = await chart.capture();
16
20
  try {
17
21
  if (canvas.width < 1 || canvas.height < 1) {
18
22
  throw new Error("Chart capture produced an empty image");
19
23
  }
20
- pdf.addPage();
21
- const scaleFactor = contentWidth / Math.max(1, canvas.width);
22
- const pageHeightPx = contentHeight / scaleFactor;
23
- const segments = splitSegmentForPageHeight({ top: 0, height: canvas.height }, canvas.height, pageHeightPx);
24
- for (let index = 0; index < segments.length; index++) {
25
- if (index > 0) {
26
- pdf.addPage();
27
- }
28
- const segment = segments[index];
29
- const sliceHeight = Math.max(1, Math.min(segment.height, canvas.height - segment.top));
30
- const pageCanvas = sliceCanvas(canvas, canvas.width, sliceHeight, segment.top, PAGE_BACKGROUND);
31
- addCanvasPage(pdf, pageCanvas, sliceHeight, contentWidth, contentHeight, MARGIN_MM);
32
- releaseCanvas(pageCanvas);
24
+ const size = fitPdfImageSize(canvas.width, canvas.height, contentWidth, contentHeight);
25
+ const placement = placePdfBlock(cursorY, size.heightMm, pageTop, pageBottom);
26
+ if (placement.needsNewPage) {
27
+ pdf.addPage();
33
28
  }
29
+ addChartImage(pdf, canvas, MARGIN_MM + (contentWidth - size.widthMm) / 2, placement.y, size.widthMm, size.heightMm);
30
+ cursorY = placement.y + size.heightMm + GAP_MM;
34
31
  }
35
32
  finally {
36
33
  releaseCanvas(canvas);
@@ -64,16 +61,14 @@ function addCoverPage(pdf, documentTitle, tocItems, pageWidth, pageHeight) {
64
61
  tocY += 7;
65
62
  }
66
63
  }
67
- function addCanvasPage(pdf, canvas, sliceHeightPx, contentWidth, contentHeight, margin) {
68
- if (canvas.width < 1 || canvas.height < 1) {
69
- throw new Error("Chart capture produced an empty image");
64
+ function addChartImage(pdf, canvas, x, y, widthMm, heightMm) {
65
+ const flattened = sliceCanvas(canvas, canvas.width, canvas.height, 0, PAGE_BACKGROUND);
66
+ try {
67
+ pdf.addImage(flattened.toDataURL("image/jpeg", 0.92), "JPEG", x, y, widthMm, heightMm);
68
+ }
69
+ finally {
70
+ releaseCanvas(flattened);
70
71
  }
71
- // JPEG is opaque. jsPDF's PNG path mishandles canvas alpha (black fills / failed export).
72
- const imageData = canvas.toDataURL("image/jpeg", 0.92);
73
- const pageScale = Math.min(contentWidth / canvas.width, contentHeight / sliceHeightPx);
74
- const renderWidthMm = canvas.width * pageScale;
75
- const renderHeightMm = sliceHeightPx * pageScale;
76
- pdf.addImage(imageData, "JPEG", margin + (contentWidth - renderWidthMm) / 2, margin, renderWidthMm, renderHeightMm);
77
72
  }
78
73
  function addPageNumbers(pdf, pageWidth, pageHeight) {
79
74
  const totalPages = pdf.getNumberOfPages();
@@ -3,5 +3,13 @@ export interface PdfExportSegment {
3
3
  height: number;
4
4
  }
5
5
  export declare function splitSegmentForPageHeight(segment: PdfExportSegment, imgHeight: number, pageHeightPx: number): PdfExportSegment[];
6
+ export declare function fitPdfImageSize(pixelWidth: number, pixelHeight: number, maxWidthMm: number, maxHeightMm: number): {
7
+ widthMm: number;
8
+ heightMm: number;
9
+ };
10
+ export declare function placePdfBlock(cursorY: number | null, blockHeightMm: number, pageTopMm: number, pageBottomMm: number): {
11
+ y: number;
12
+ needsNewPage: boolean;
13
+ };
6
14
  export declare function sliceCanvas(sourceCanvas: HTMLCanvasElement, width: number, height: number, offsetY: number, backgroundColor: string): HTMLCanvasElement;
7
15
  //# sourceMappingURL=pdfPage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pdfPage.d.ts","sourceRoot":"","sources":["../../src/report/pdfPage.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,gBAAgB,EACzB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GACnB,gBAAgB,EAAE,CAcpB;AAED,wBAAgB,WAAW,CACzB,YAAY,EAAE,iBAAiB,EAC/B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GACtB,iBAAiB,CAsBnB"}
1
+ {"version":3,"file":"pdfPage.d.ts","sourceRoot":"","sources":["../../src/report/pdfPage.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,gBAAgB,EACzB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GACnB,gBAAgB,EAAE,CAcpB;AAED,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAUvC;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GACnB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,CAKtC;AAED,wBAAgB,WAAW,CACzB,YAAY,EAAE,iBAAiB,EAC/B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GACtB,iBAAiB,CAsBnB"}
@@ -12,6 +12,23 @@ export function splitSegmentForPageHeight(segment, imgHeight, pageHeightPx) {
12
12
  }
13
13
  return slices;
14
14
  }
15
+ export function fitPdfImageSize(pixelWidth, pixelHeight, maxWidthMm, maxHeightMm) {
16
+ const safeWidth = Math.max(1, pixelWidth);
17
+ const safeHeight = Math.max(1, pixelHeight);
18
+ const widthScale = maxWidthMm / safeWidth;
19
+ const heightAtFullWidth = safeHeight * widthScale;
20
+ if (heightAtFullWidth <= maxHeightMm) {
21
+ return { widthMm: maxWidthMm, heightMm: heightAtFullWidth };
22
+ }
23
+ const scale = maxHeightMm / safeHeight;
24
+ return { widthMm: safeWidth * scale, heightMm: maxHeightMm };
25
+ }
26
+ export function placePdfBlock(cursorY, blockHeightMm, pageTopMm, pageBottomMm) {
27
+ if (cursorY === null || cursorY + blockHeightMm > pageBottomMm) {
28
+ return { y: pageTopMm, needsNewPage: true };
29
+ }
30
+ return { y: cursorY, needsNewPage: false };
31
+ }
15
32
  export function sliceCanvas(sourceCanvas, width, height, offsetY, backgroundColor) {
16
33
  const pageCanvas = document.createElement("canvas");
17
34
  pageCanvas.width = width;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openshift-migration-advisor/shared-components",
3
- "version": "0.25.0-6a4104ca5ed2",
3
+ "version": "0.25.0-883f4c9ae996",
4
4
  "description": "Shared React UI components for Migration Advisor apps (report charts, forms, OS support tiers)",
5
5
  "author": "Montse Ortega <mortegag@redhat.com>",
6
6
  "license": "Apache-2.0",
@@ -46,8 +46,8 @@
46
46
  "README.md"
47
47
  ],
48
48
  "dependencies": {
49
- "html2canvas-pro": "^2.4.3",
49
+ "html2canvas-pro": "^2.4.5",
50
50
  "jspdf": "^4.2.1",
51
- "jszip": "^3.10.1"
51
+ "jszip": "^3.10.2"
52
52
  }
53
53
  }
@@ -60,7 +60,7 @@ describe("captureChartElement", () => {
60
60
  expect(panel.style.display).toBe("none");
61
61
  });
62
62
 
63
- it("keeps the card radius and paints a real border for the snapshot", async () => {
63
+ it("paints a real card border and hides the PatternFly ::before during capture", async () => {
64
64
  const root = document.createElement("div");
65
65
  const card = document.createElement("div");
66
66
  card.className = "pf-v6-c-card";
@@ -71,23 +71,20 @@ describe("captureChartElement", () => {
71
71
  document.body.append(root);
72
72
 
73
73
  mockedHtml2Canvas.mockImplementation(async (_element, options) => {
74
- expect(card.style.borderRadius).toBe("16px");
74
+ expect(card.style.borderWidth).toBe("1px");
75
75
  expect(card.style.overflow).toBe("hidden");
76
- expect(card.style.backgroundColor).toBe("rgb(255, 255, 255)");
77
- expect(card.style.getPropertyValue("--pf-v6-c-card--BorderWidth")).toBe(
78
- "0px",
79
- );
76
+ expect(
77
+ document.head.querySelector("[data-chart-export-hide-before]"),
78
+ ).not.toBeNull();
80
79
 
81
80
  const cloneRoot = document.createElement("div");
82
81
  const cloneCard = document.createElement("div");
83
82
  cloneCard.className = "pf-v6-c-card";
84
83
  cloneRoot.append(cloneCard);
85
84
  options?.onclone?.(document, cloneRoot);
86
- expect(cloneCard.style.borderRadius).toBe("16px");
85
+ expect(cloneCard.style.borderWidth).toBe("1px");
86
+ expect(cloneCard.style.borderStyle).toBe("solid");
87
87
  expect(cloneCard.style.overflow).toBe("hidden");
88
- expect(
89
- cloneCard.style.getPropertyValue("--pf-v6-c-card--BorderWidth"),
90
- ).toBe("0px");
91
88
 
92
89
  const canvas = document.createElement("canvas");
93
90
  canvas.width = 1;
@@ -97,8 +94,11 @@ describe("captureChartElement", () => {
97
94
 
98
95
  await captureChartElement(root);
99
96
 
100
- expect(card.style.borderRadius).toBe("16px");
101
- expect(card.style.getPropertyValue("--pf-v6-c-card--BorderWidth")).toBe("");
97
+ expect(card.style.borderWidth).toBe("1px");
98
+ expect(card.style.overflow).toBe("");
99
+ expect(
100
+ document.head.querySelector("[data-chart-export-hide-before]"),
101
+ ).toBeNull();
102
102
  });
103
103
 
104
104
  it("inlines computed card title padding so content stays inset", async () => {
@@ -7,7 +7,11 @@ import {
7
7
  } from "../chartExport.js";
8
8
  import { getChartExportFilename } from "../chartExportFilenames.js";
9
9
  import { buildHtmlReport, escapeHtml } from "../htmlExport.js";
10
- import { splitSegmentForPageHeight } from "../pdfPage.js";
10
+ import {
11
+ fitPdfImageSize,
12
+ placePdfBlock,
13
+ splitSegmentForPageHeight,
14
+ } from "../pdfPage.js";
11
15
 
12
16
  describe("chart export filenames", () => {
13
17
  const date = new Date("2026-07-01T15:30:00");
@@ -161,6 +165,39 @@ describe("splitSegmentForPageHeight", () => {
161
165
  });
162
166
  });
163
167
 
168
+ describe("fitPdfImageSize", () => {
169
+ it("keeps full content width when the chart fits the page", () => {
170
+ expect(fitPdfImageSize(800, 200, 190, 277)).toEqual({
171
+ widthMm: 190,
172
+ heightMm: 47.5,
173
+ });
174
+ });
175
+
176
+ it("scales down a chart taller than the page", () => {
177
+ expect(fitPdfImageSize(1000, 2000, 200, 100)).toEqual({
178
+ widthMm: 50,
179
+ heightMm: 100,
180
+ });
181
+ });
182
+ });
183
+
184
+ describe("placePdfBlock", () => {
185
+ it("stacks charts on the same page while they fit", () => {
186
+ expect(placePdfBlock(null, 50, 10, 287)).toEqual({
187
+ y: 10,
188
+ needsNewPage: true,
189
+ });
190
+ expect(placePdfBlock(68, 50, 10, 287)).toEqual({
191
+ y: 68,
192
+ needsNewPage: false,
193
+ });
194
+ expect(placePdfBlock(250, 50, 10, 287)).toEqual({
195
+ y: 10,
196
+ needsNewPage: true,
197
+ });
198
+ });
199
+ });
200
+
164
201
  describe("buildHtmlReport", () => {
165
202
  it("embeds captured chart images and escapes the title", () => {
166
203
  const html = buildHtmlReport(
@@ -179,6 +216,8 @@ describe("buildHtmlReport", () => {
179
216
  );
180
217
  expect(html).toContain('src="data:image/png;base64,abc"');
181
218
  expect(html).toContain("Infrastructure summary");
219
+ expect(html).not.toContain("border:");
220
+ expect(html).toContain("width: 100%");
182
221
  expect(escapeHtml("<b>")).toBe("&lt;b&gt;");
183
222
  expect(escapeHtml("it's")).toBe("it&#39;s");
184
223
  });
@@ -117,35 +117,65 @@ function applyComputedPadding(source: HTMLElement, dest: HTMLElement): void {
117
117
  dest.style.paddingLeft = computed.paddingLeft;
118
118
  }
119
119
 
120
- const CARD_BORDER_WIDTH_VAR = "--pf-v6-c-card--BorderWidth";
121
-
122
- function applyCardChrome(source: HTMLElement, dest: HTMLElement): void {
120
+ function readCardBorder(source: HTMLElement): {
121
+ width: string;
122
+ style: string;
123
+ color: string;
124
+ } | null {
125
+ if (source.style.borderWidth && source.style.borderStyle) {
126
+ return {
127
+ width: source.style.borderWidth,
128
+ style: source.style.borderStyle,
129
+ color: source.style.borderColor,
130
+ };
131
+ }
123
132
  const computed = window.getComputedStyle(source);
124
133
  const before = window.getComputedStyle(source, "::before");
125
- dest.style.borderRadius = computed.borderRadius;
126
- dest.style.backgroundColor = resolvedFill(computed.backgroundColor);
127
- dest.style.overflow = "hidden";
128
-
129
- const borderWidth =
134
+ const width =
130
135
  before.borderTopWidth && before.borderTopWidth !== "0px"
131
136
  ? before.borderTopWidth
132
137
  : computed.borderTopWidth;
133
- const borderStyle =
138
+ const style =
134
139
  before.borderTopStyle && before.borderTopStyle !== "none"
135
140
  ? before.borderTopStyle
136
141
  : computed.borderTopStyle;
137
- const borderColor =
142
+ const color =
138
143
  before.borderTopColor && before.borderTopColor !== "rgba(0, 0, 0, 0)"
139
144
  ? before.borderTopColor
140
145
  : computed.borderTopColor;
141
- if (borderWidth !== "0px" && borderStyle !== "none") {
142
- dest.style.borderWidth = borderWidth;
143
- dest.style.borderStyle = borderStyle;
144
- dest.style.borderColor = borderColor;
146
+ if (width === "0px" || style === "none") {
147
+ return null;
145
148
  }
146
- // PF draws the border on ::before via this token. html2canvas still paints
147
- // that pseudo even if a stylesheet sets content:none, so collapse it here.
148
- dest.style.setProperty(CARD_BORDER_WIDTH_VAR, "0px");
149
+ return { width, style, color };
150
+ }
151
+
152
+ function applyCardChrome(source: HTMLElement, dest: HTMLElement): void {
153
+ const computed = window.getComputedStyle(source);
154
+ dest.style.borderRadius = computed.borderRadius;
155
+ dest.style.backgroundColor = resolvedFill(computed.backgroundColor);
156
+ dest.style.overflow = "hidden";
157
+ const border = readCardBorder(source);
158
+ if (border) {
159
+ dest.style.borderWidth = border.width;
160
+ dest.style.borderStyle = border.style;
161
+ dest.style.borderColor = border.color;
162
+ }
163
+ }
164
+
165
+ function hideCardBeforePseudo(): () => void {
166
+ const style = document.createElement("style");
167
+ style.setAttribute("data-chart-export-hide-before", "");
168
+ style.textContent = `
169
+ [${CHART_EXPORT_CAPTURING_ATTR}] .pf-v6-c-card::before,
170
+ [${CHART_EXPORT_CAPTURING_ATTR}].pf-v6-c-card::before {
171
+ border: none !important;
172
+ box-shadow: none !important;
173
+ }
174
+ `;
175
+ document.head.append(style);
176
+ return () => {
177
+ style.remove();
178
+ };
149
179
  }
150
180
 
151
181
  function copyComputedPadding(node: HTMLElement): {
@@ -175,8 +205,6 @@ function copyCardChrome(node: HTMLElement): {
175
205
  minHeight: string;
176
206
  height: string;
177
207
  maxHeight: string;
178
- borderWidthVar: string;
179
- borderWidthVarPriority: string;
180
208
  } {
181
209
  const previous = {
182
210
  borderRadius: node.style.borderRadius,
@@ -189,10 +217,6 @@ function copyCardChrome(node: HTMLElement): {
189
217
  minHeight: node.style.minHeight,
190
218
  height: node.style.height,
191
219
  maxHeight: node.style.maxHeight,
192
- borderWidthVar: node.style.getPropertyValue(CARD_BORDER_WIDTH_VAR),
193
- borderWidthVarPriority: node.style.getPropertyPriority(
194
- CARD_BORDER_WIDTH_VAR,
195
- ),
196
220
  };
197
221
  applyCardChrome(node, node);
198
222
  node.style.minHeight = "0px";
@@ -295,15 +319,6 @@ function prepareCaptureLayout(element: HTMLElement): () => void {
295
319
  entry.node.style.minHeight = entry.minHeight;
296
320
  entry.node.style.height = entry.height;
297
321
  entry.node.style.maxHeight = entry.maxHeight;
298
- if (entry.borderWidthVar) {
299
- entry.node.style.setProperty(
300
- CARD_BORDER_WIDTH_VAR,
301
- entry.borderWidthVar,
302
- entry.borderWidthVarPriority,
303
- );
304
- } else {
305
- entry.node.style.removeProperty(CARD_BORDER_WIDTH_VAR);
306
- }
307
322
  }
308
323
  for (const entry of previousPadding) {
309
324
  entry.node.style.paddingTop = entry.paddingTop;
@@ -320,6 +335,7 @@ export async function captureChartElement(
320
335
  const restoreHidden = revealHiddenChartAncestors(element);
321
336
  const restoreLayout = prepareCaptureLayout(element);
322
337
  element.setAttribute(CHART_EXPORT_CAPTURING_ATTR, "");
338
+ const restoreCardBefore = hideCardBeforePseudo();
323
339
 
324
340
  try {
325
341
  await new Promise<void>((resolve) => {
@@ -344,6 +360,7 @@ export async function captureChartElement(
344
360
  },
345
361
  });
346
362
  } finally {
363
+ restoreCardBefore();
347
364
  element.removeAttribute(CHART_EXPORT_CAPTURING_ATTR);
348
365
  restoreLayout();
349
366
  restoreHidden();
@@ -12,7 +12,10 @@ export const chartExportScrollProps = {
12
12
  [CHART_EXPORT_SCROLL_ATTR]: "",
13
13
  } as const;
14
14
 
15
- export const chartExportRootStyle: CSSProperties = { height: "100%" };
15
+ export const chartExportRootStyle: CSSProperties = {
16
+ width: "100%",
17
+ height: "100%",
18
+ };
16
19
 
17
20
  export type ChartExportMeta = {
18
21
  id: string;
@@ -46,10 +46,8 @@ export function buildHtmlReport(
46
46
  h2 { font-size: 18px; margin: 32px 0 12px; }
47
47
  .meta { color: #6a6e73; margin-bottom: 24px; }
48
48
  img {
49
- max-width: 100%;
49
+ width: 100%;
50
50
  height: auto;
51
- border: 1px solid #d2d2d2;
52
- border-radius: 8px;
53
51
  }
54
52
  </style>
55
53
  </head>
@@ -1,8 +1,9 @@
1
1
  import jsPDF from "jspdf";
2
2
  import { type ChartCaptureSource, releaseCanvas } from "./chartExport.js";
3
- import { sliceCanvas, splitSegmentForPageHeight } from "./pdfPage.js";
3
+ import { fitPdfImageSize, placePdfBlock, sliceCanvas } from "./pdfPage.js";
4
4
 
5
5
  const MARGIN_MM = 10;
6
+ const GAP_MM = 8;
6
7
  /** Flatten captures onto white; jsPDF cannot embed transparent PNG alpha cleanly. */
7
8
  const PAGE_BACKGROUND = "#ffffff";
8
9
 
@@ -15,6 +16,8 @@ export async function buildPdfFromCharts(
15
16
  const pageHeight = pdf.internal.pageSize.getHeight();
16
17
  const contentWidth = pageWidth - MARGIN_MM * 2;
17
18
  const contentHeight = pageHeight - MARGIN_MM * 2;
19
+ const pageTop = MARGIN_MM;
20
+ const pageBottom = pageHeight - MARGIN_MM;
18
21
 
19
22
  addCoverPage(
20
23
  pdf,
@@ -24,47 +27,37 @@ export async function buildPdfFromCharts(
24
27
  pageHeight,
25
28
  );
26
29
 
30
+ let cursorY: number | null = null;
27
31
  for (const chart of charts) {
28
32
  const canvas = await chart.capture();
29
33
  try {
30
34
  if (canvas.width < 1 || canvas.height < 1) {
31
35
  throw new Error("Chart capture produced an empty image");
32
36
  }
33
- pdf.addPage();
34
- const scaleFactor = contentWidth / Math.max(1, canvas.width);
35
- const pageHeightPx = contentHeight / scaleFactor;
36
- const segments = splitSegmentForPageHeight(
37
- { top: 0, height: canvas.height },
37
+ const size = fitPdfImageSize(
38
+ canvas.width,
38
39
  canvas.height,
39
- pageHeightPx,
40
+ contentWidth,
41
+ contentHeight,
40
42
  );
41
-
42
- for (let index = 0; index < segments.length; index++) {
43
- if (index > 0) {
44
- pdf.addPage();
45
- }
46
- const segment = segments[index];
47
- const sliceHeight = Math.max(
48
- 1,
49
- Math.min(segment.height, canvas.height - segment.top),
50
- );
51
- const pageCanvas = sliceCanvas(
52
- canvas,
53
- canvas.width,
54
- sliceHeight,
55
- segment.top,
56
- PAGE_BACKGROUND,
57
- );
58
- addCanvasPage(
59
- pdf,
60
- pageCanvas,
61
- sliceHeight,
62
- contentWidth,
63
- contentHeight,
64
- MARGIN_MM,
65
- );
66
- releaseCanvas(pageCanvas);
43
+ const placement = placePdfBlock(
44
+ cursorY,
45
+ size.heightMm,
46
+ pageTop,
47
+ pageBottom,
48
+ );
49
+ if (placement.needsNewPage) {
50
+ pdf.addPage();
67
51
  }
52
+ addChartImage(
53
+ pdf,
54
+ canvas,
55
+ MARGIN_MM + (contentWidth - size.widthMm) / 2,
56
+ placement.y,
57
+ size.widthMm,
58
+ size.heightMm,
59
+ );
60
+ cursorY = placement.y + size.heightMm + GAP_MM;
68
61
  } finally {
69
62
  releaseCanvas(canvas);
70
63
  }
@@ -116,33 +109,33 @@ function addCoverPage(
116
109
  }
117
110
  }
118
111
 
119
- function addCanvasPage(
112
+ function addChartImage(
120
113
  pdf: jsPDF,
121
114
  canvas: HTMLCanvasElement,
122
- sliceHeightPx: number,
123
- contentWidth: number,
124
- contentHeight: number,
125
- margin: number,
115
+ x: number,
116
+ y: number,
117
+ widthMm: number,
118
+ heightMm: number,
126
119
  ): void {
127
- if (canvas.width < 1 || canvas.height < 1) {
128
- throw new Error("Chart capture produced an empty image");
129
- }
130
- // JPEG is opaque. jsPDF's PNG path mishandles canvas alpha (black fills / failed export).
131
- const imageData = canvas.toDataURL("image/jpeg", 0.92);
132
- const pageScale = Math.min(
133
- contentWidth / canvas.width,
134
- contentHeight / sliceHeightPx,
135
- );
136
- const renderWidthMm = canvas.width * pageScale;
137
- const renderHeightMm = sliceHeightPx * pageScale;
138
- pdf.addImage(
139
- imageData,
140
- "JPEG",
141
- margin + (contentWidth - renderWidthMm) / 2,
142
- margin,
143
- renderWidthMm,
144
- renderHeightMm,
120
+ const flattened = sliceCanvas(
121
+ canvas,
122
+ canvas.width,
123
+ canvas.height,
124
+ 0,
125
+ PAGE_BACKGROUND,
145
126
  );
127
+ try {
128
+ pdf.addImage(
129
+ flattened.toDataURL("image/jpeg", 0.92),
130
+ "JPEG",
131
+ x,
132
+ y,
133
+ widthMm,
134
+ heightMm,
135
+ );
136
+ } finally {
137
+ releaseCanvas(flattened);
138
+ }
146
139
  }
147
140
 
148
141
  function addPageNumbers(
@@ -23,6 +23,35 @@ export function splitSegmentForPageHeight(
23
23
  return slices;
24
24
  }
25
25
 
26
+ export function fitPdfImageSize(
27
+ pixelWidth: number,
28
+ pixelHeight: number,
29
+ maxWidthMm: number,
30
+ maxHeightMm: number,
31
+ ): { widthMm: number; heightMm: number } {
32
+ const safeWidth = Math.max(1, pixelWidth);
33
+ const safeHeight = Math.max(1, pixelHeight);
34
+ const widthScale = maxWidthMm / safeWidth;
35
+ const heightAtFullWidth = safeHeight * widthScale;
36
+ if (heightAtFullWidth <= maxHeightMm) {
37
+ return { widthMm: maxWidthMm, heightMm: heightAtFullWidth };
38
+ }
39
+ const scale = maxHeightMm / safeHeight;
40
+ return { widthMm: safeWidth * scale, heightMm: maxHeightMm };
41
+ }
42
+
43
+ export function placePdfBlock(
44
+ cursorY: number | null,
45
+ blockHeightMm: number,
46
+ pageTopMm: number,
47
+ pageBottomMm: number,
48
+ ): { y: number; needsNewPage: boolean } {
49
+ if (cursorY === null || cursorY + blockHeightMm > pageBottomMm) {
50
+ return { y: pageTopMm, needsNewPage: true };
51
+ }
52
+ return { y: cursorY, needsNewPage: false };
53
+ }
54
+
26
55
  export function sliceCanvas(
27
56
  sourceCanvas: HTMLCanvasElement,
28
57
  width: number,