@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.
- package/dist/report/captureChartElement.d.ts.map +1 -1
- package/dist/report/captureChartElement.js +43 -23
- package/dist/report/chartExport.d.ts.map +1 -1
- package/dist/report/chartExport.js +4 -1
- package/dist/report/htmlExport.d.ts.map +1 -1
- package/dist/report/htmlExport.js +1 -3
- package/dist/report/pdfExport.d.ts.map +1 -1
- package/dist/report/pdfExport.js +18 -23
- package/dist/report/pdfPage.d.ts +8 -0
- package/dist/report/pdfPage.d.ts.map +1 -1
- package/dist/report/pdfPage.js +17 -0
- package/package.json +3 -3
- package/src/report/__tests__/captureChartElement.test.ts +12 -12
- package/src/report/__tests__/chartExportPackagers.test.ts +40 -1
- package/src/report/captureChartElement.ts +49 -32
- package/src/report/chartExport.ts +4 -1
- package/src/report/htmlExport.ts +1 -3
- package/src/report/pdfExport.ts +49 -56
- package/src/report/pdfPage.ts +29 -0
|
@@ -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;
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
104
|
+
const style = before.borderTopStyle && before.borderTopStyle !== "none"
|
|
102
105
|
? before.borderTopStyle
|
|
103
106
|
: computed.borderTopStyle;
|
|
104
|
-
const
|
|
107
|
+
const color = before.borderTopColor && before.borderTopColor !== "rgba(0, 0, 0, 0)"
|
|
105
108
|
? before.borderTopColor
|
|
106
109
|
: computed.borderTopColor;
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
dest.style.borderStyle = borderStyle;
|
|
110
|
-
dest.style.borderColor = borderColor;
|
|
110
|
+
if (width === "0px" || style === "none") {
|
|
111
|
+
return null;
|
|
111
112
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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,
|
|
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 = {
|
|
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,
|
|
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
|
-
|
|
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;
|
|
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"}
|
package/dist/report/pdfExport.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import jsPDF from "jspdf";
|
|
2
2
|
import { releaseCanvas } from "./chartExport.js";
|
|
3
|
-
import {
|
|
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
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
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();
|
package/dist/report/pdfPage.d.ts
CHANGED
|
@@ -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"}
|
package/dist/report/pdfPage.js
CHANGED
|
@@ -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-
|
|
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.
|
|
49
|
+
"html2canvas-pro": "^2.4.5",
|
|
50
50
|
"jspdf": "^4.2.1",
|
|
51
|
-
"jszip": "^3.10.
|
|
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("
|
|
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.
|
|
74
|
+
expect(card.style.borderWidth).toBe("1px");
|
|
75
75
|
expect(card.style.overflow).toBe("hidden");
|
|
76
|
-
expect(
|
|
77
|
-
|
|
78
|
-
|
|
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.
|
|
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.
|
|
101
|
-
expect(card.style.
|
|
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 {
|
|
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("<b>");
|
|
183
222
|
expect(escapeHtml("it's")).toBe("it'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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
|
138
|
+
const style =
|
|
134
139
|
before.borderTopStyle && before.borderTopStyle !== "none"
|
|
135
140
|
? before.borderTopStyle
|
|
136
141
|
: computed.borderTopStyle;
|
|
137
|
-
const
|
|
142
|
+
const color =
|
|
138
143
|
before.borderTopColor && before.borderTopColor !== "rgba(0, 0, 0, 0)"
|
|
139
144
|
? before.borderTopColor
|
|
140
145
|
: computed.borderTopColor;
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
dest.style.borderStyle = borderStyle;
|
|
144
|
-
dest.style.borderColor = borderColor;
|
|
146
|
+
if (width === "0px" || style === "none") {
|
|
147
|
+
return null;
|
|
145
148
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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 = {
|
|
15
|
+
export const chartExportRootStyle: CSSProperties = {
|
|
16
|
+
width: "100%",
|
|
17
|
+
height: "100%",
|
|
18
|
+
};
|
|
16
19
|
|
|
17
20
|
export type ChartExportMeta = {
|
|
18
21
|
id: string;
|
package/src/report/htmlExport.ts
CHANGED
|
@@ -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
|
-
|
|
49
|
+
width: 100%;
|
|
50
50
|
height: auto;
|
|
51
|
-
border: 1px solid #d2d2d2;
|
|
52
|
-
border-radius: 8px;
|
|
53
51
|
}
|
|
54
52
|
</style>
|
|
55
53
|
</head>
|
package/src/report/pdfExport.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import jsPDF from "jspdf";
|
|
2
2
|
import { type ChartCaptureSource, releaseCanvas } from "./chartExport.js";
|
|
3
|
-
import {
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
+
contentWidth,
|
|
41
|
+
contentHeight,
|
|
40
42
|
);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
112
|
+
function addChartImage(
|
|
120
113
|
pdf: jsPDF,
|
|
121
114
|
canvas: HTMLCanvasElement,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
x: number,
|
|
116
|
+
y: number,
|
|
117
|
+
widthMm: number,
|
|
118
|
+
heightMm: number,
|
|
126
119
|
): void {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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(
|
package/src/report/pdfPage.ts
CHANGED
|
@@ -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,
|