@segnavia/render 0.8.0 → 0.9.1
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/deck-pdf.js +58 -5
- package/dist/overflow.js +100 -25
- package/package.json +4 -4
package/dist/deck-pdf.js
CHANGED
|
@@ -4,8 +4,9 @@ import { pdfPixelBox } from "./pdf-geometry.js";
|
|
|
4
4
|
import { composeImposedPdf, drawClippedPage, embedOnePage } from "./pdf-compose.js";
|
|
5
5
|
import { Imposition, fitPage, impose, pagePlacementLayout } from "@segnavia/format";
|
|
6
6
|
import { newNonce } from "@segnavia/frame/bridge";
|
|
7
|
-
import { htmlSlideDocument, notesLayoutGeometry, printableDeck } from "@segnavia/frame/print-document";
|
|
7
|
+
import { htmlSlideDocument, notesLayoutGeometry, paginateNotes, printableDeck } from "@segnavia/frame/print-document";
|
|
8
8
|
import { PDFDocument } from "pdf-lib";
|
|
9
|
+
import { fitImage, imageLayerStyles } from "@segnavia/frame/image-fit";
|
|
9
10
|
//#region src/deck-pdf.ts
|
|
10
11
|
function nameSlideError(error, id) {
|
|
11
12
|
if (error instanceof Error) {
|
|
@@ -14,6 +15,47 @@ function nameSlideError(error, id) {
|
|
|
14
15
|
}
|
|
15
16
|
throw new Error(`Slide "${id}" could not be rendered: ${String(error)}`);
|
|
16
17
|
}
|
|
18
|
+
function firstNotesPages(pageOwners, slideCount) {
|
|
19
|
+
return Array.from({ length: slideCount }, (_, slide) => pageOwners.indexOf(slide));
|
|
20
|
+
}
|
|
21
|
+
async function placeRenderedImages(tab) {
|
|
22
|
+
const layers = tab.locator("[data-segnavia-image-layer]");
|
|
23
|
+
const styles = (await layers.evaluateAll((elements) => elements.map((element) => {
|
|
24
|
+
const layer = element;
|
|
25
|
+
const image = layer.querySelector("img");
|
|
26
|
+
return {
|
|
27
|
+
slide: {
|
|
28
|
+
width: Number(layer.dataset.slideWidth),
|
|
29
|
+
height: Number(layer.dataset.slideHeight)
|
|
30
|
+
},
|
|
31
|
+
image: {
|
|
32
|
+
width: image?.naturalWidth ?? 0,
|
|
33
|
+
height: image?.naturalHeight ?? 0
|
|
34
|
+
},
|
|
35
|
+
fit: layer.dataset.fit,
|
|
36
|
+
frame: layer.dataset.frame === "true"
|
|
37
|
+
};
|
|
38
|
+
}))).map((fact) => {
|
|
39
|
+
if (fact.image.width <= 0 || fact.image.height <= 0) return null;
|
|
40
|
+
const placement = fitImage(fact.slide, fact.image, fact.fit, void 0, fact.frame);
|
|
41
|
+
return imageLayerStyles(fact.image, placement, fact.slide);
|
|
42
|
+
});
|
|
43
|
+
await layers.evaluateAll((elements, values) => {
|
|
44
|
+
elements.forEach((element, index) => {
|
|
45
|
+
const styles = values[index];
|
|
46
|
+
if (!styles) return;
|
|
47
|
+
const layer = element;
|
|
48
|
+
const image = layer.querySelector("img");
|
|
49
|
+
if (!image) return;
|
|
50
|
+
Object.assign(layer.style, styles.layer);
|
|
51
|
+
Object.assign(image.style, styles.image);
|
|
52
|
+
});
|
|
53
|
+
}, styles);
|
|
54
|
+
}
|
|
55
|
+
async function paginateRenderedNotes(tab, maxSheets) {
|
|
56
|
+
if (maxSheets === void 0) return tab.evaluate(paginateNotes, null);
|
|
57
|
+
return tab.evaluate(`(${paginateNotes.toString()})(null,${JSON.stringify(maxSheets)})`);
|
|
58
|
+
}
|
|
17
59
|
async function regularDeckPdf(deck, opts, services) {
|
|
18
60
|
const htmlSlides = deck.slides.flatMap((slide, index) => slide.content.kind === "html" ? [{
|
|
19
61
|
index,
|
|
@@ -23,6 +65,7 @@ async function regularDeckPdf(deck, opts, services) {
|
|
|
23
65
|
const css = `${services.fontFaces(opts)}${opts.css ?? ""}`;
|
|
24
66
|
const slideCss = containFixedPageOverflow(css);
|
|
25
67
|
const notesGeometry = opts.layout === "notes" ? notesLayoutGeometry(deck.size, opts.notesPaper) : void 0;
|
|
68
|
+
let pageOwners = [];
|
|
26
69
|
const [printable, rendered] = await Promise.all([services.renderPdf({
|
|
27
70
|
viewport: notesGeometry ? {
|
|
28
71
|
width: Math.round(notesGeometry.sheet.width),
|
|
@@ -35,7 +78,14 @@ async function regularDeckPdf(deck, opts, services) {
|
|
|
35
78
|
omitHtmlSlideFrames: true
|
|
36
79
|
}),
|
|
37
80
|
boundMs: opts.boundMs,
|
|
38
|
-
beforeOutput:
|
|
81
|
+
beforeOutput: async (tab) => {
|
|
82
|
+
await placeRenderedImages(tab);
|
|
83
|
+
if (notesGeometry) {
|
|
84
|
+
if ((await paginateRenderedNotes(tab, opts.limits?.maxSheets)).exceeded) throw new RenderRefusedError(`The PDF needs more than ${opts.limits?.maxSheets} sheets; the limit is ${opts.limits?.maxSheets}.`);
|
|
85
|
+
pageOwners = await tab.locator(".sheet").evaluateAll((sheets) => sheets.map((sheet) => Number(sheet.dataset.slideIndex)));
|
|
86
|
+
}
|
|
87
|
+
await opts.beforeOutput?.(tab);
|
|
88
|
+
}
|
|
39
89
|
}, {
|
|
40
90
|
preferCSSPageSize: true,
|
|
41
91
|
printBackground: true
|
|
@@ -77,8 +127,10 @@ async function regularDeckPdf(deck, opts, services) {
|
|
|
77
127
|
height: deck.size.height
|
|
78
128
|
};
|
|
79
129
|
const geometry = pdfPixelBox(slideBox, notesGeometry?.sheet.height ?? deck.size.height);
|
|
130
|
+
const firstPages = notesGeometry ? firstNotesPages(pageOwners, deck.slides.length) : [];
|
|
80
131
|
for (const item of rendered) {
|
|
81
|
-
const
|
|
132
|
+
const pageIndex = notesGeometry ? firstPages[item.index] ?? -1 : item.index;
|
|
133
|
+
const sheet = pages[pageIndex < 0 ? item.index : pageIndex];
|
|
82
134
|
if (!sheet) throw new Error(`The print document has no sheet for slide "${deck.slides[item.index]?.id ?? item.index}".`);
|
|
83
135
|
const embedded = await embedOnePage(output, item.bytes, `The rendered slide "${deck.slides[item.index]?.id ?? item.index}"`);
|
|
84
136
|
drawClippedPage(sheet, embedded, geometry, geometry);
|
|
@@ -109,7 +161,8 @@ async function imposedDeckPdf(deck, imposition, opts, services) {
|
|
|
109
161
|
viewport: deck.size,
|
|
110
162
|
scripts: slide.content.kind === "html" ? deck.scripts === "sandboxed" : true,
|
|
111
163
|
html,
|
|
112
|
-
boundMs: opts.boundMs
|
|
164
|
+
boundMs: opts.boundMs,
|
|
165
|
+
beforeOutput: slide.content.kind === "image" ? placeRenderedImages : void 0
|
|
113
166
|
}, {
|
|
114
167
|
width: `${deck.size.width}px`,
|
|
115
168
|
height: `${deck.size.height}px`,
|
|
@@ -151,4 +204,4 @@ async function renderDeckPdf(deck, opts, services) {
|
|
|
151
204
|
return imposedDeckPdf(deck, Imposition.parse(opts.imposition), opts, services);
|
|
152
205
|
}
|
|
153
206
|
//#endregion
|
|
154
|
-
export { renderDeckPdf };
|
|
207
|
+
export { firstNotesPages, renderDeckPdf };
|
package/dist/overflow.js
CHANGED
|
@@ -33,15 +33,33 @@ function solveScale(pageWidth, measurements) {
|
|
|
33
33
|
}
|
|
34
34
|
return { scale: pageWidth / (measured.get(high) !== void 0 && measured.get(high) >= pageWidth ? high : lastSatisfied) };
|
|
35
35
|
}
|
|
36
|
+
/** Returns the visible horizontal part of a box after ancestor clipping. */
|
|
37
|
+
function clippedHorizontalBox(box, clips) {
|
|
38
|
+
const visible = clips.reduce((current, clip) => ({
|
|
39
|
+
left: Math.max(current.left, clip.left),
|
|
40
|
+
right: Math.min(current.right, clip.right)
|
|
41
|
+
}), box);
|
|
42
|
+
return visible.right > visible.left ? visible : void 0;
|
|
43
|
+
}
|
|
44
|
+
function horizontalOverflow(box, clips, pageWidth, rtl) {
|
|
45
|
+
const visible = clippedHorizontalBox(box, clips);
|
|
46
|
+
return {
|
|
47
|
+
clippedStart: Math.max(0, rtl ? box.right - pageWidth : -box.left),
|
|
48
|
+
clippedEnd: Math.max(0, rtl ? -box.left : box.right - pageWidth),
|
|
49
|
+
rightOverflow: visible ? Math.max(0, visible.right - pageWidth) : 0
|
|
50
|
+
};
|
|
51
|
+
}
|
|
36
52
|
async function measuredOverflow(tab, pageWidth) {
|
|
37
|
-
|
|
53
|
+
const measured = await tab.evaluate(() => {
|
|
38
54
|
const root = document.documentElement;
|
|
39
55
|
const extent = Math.max(root.scrollWidth, document.body?.scrollWidth ?? 0);
|
|
40
56
|
const endOverflow = Math.max(0, extent - root.clientWidth);
|
|
41
57
|
const rtl = getComputedStyle(root).direction === "rtl";
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
const clipsContent = (style) => {
|
|
59
|
+
const contain = style.contain.split(/\s+/);
|
|
60
|
+
return style.overflowX !== "visible" || contain.includes("paint") || contain.includes("strict") || contain.includes("content") || style.clipPath !== "none" || style.clip !== "auto";
|
|
61
|
+
};
|
|
62
|
+
const extentClipped = [root, document.body].some((element) => element && clipsContent(getComputedStyle(element)));
|
|
45
63
|
const locator = (element) => {
|
|
46
64
|
if (element.id) return `#${CSS.escape(element.id)}`;
|
|
47
65
|
const parts = [];
|
|
@@ -53,29 +71,86 @@ async function measuredOverflow(tab, pageWidth) {
|
|
|
53
71
|
}
|
|
54
72
|
return parts.join(" > ");
|
|
55
73
|
};
|
|
56
|
-
|
|
74
|
+
const clipBox = (element, style) => {
|
|
57
75
|
const box = element.getBoundingClientRect();
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
const scale = element instanceof HTMLElement && element.offsetWidth > 0 ? box.width / element.offsetWidth : 1;
|
|
77
|
+
let left = box.left + element.clientLeft * scale;
|
|
78
|
+
let right = left + element.clientWidth * scale;
|
|
79
|
+
if (style.overflowX === "clip") {
|
|
80
|
+
const values = style.overflowClipMargin.split(/\s+/);
|
|
81
|
+
const reference = values.find((value) => value === "content-box" || value === "border-box" || value === "padding-box");
|
|
82
|
+
if (reference === "border-box") {
|
|
83
|
+
left = box.left;
|
|
84
|
+
right = box.right;
|
|
85
|
+
} else if (reference === "content-box") {
|
|
86
|
+
left += Number.parseFloat(style.paddingLeft) * scale;
|
|
87
|
+
right -= Number.parseFloat(style.paddingRight) * scale;
|
|
88
|
+
}
|
|
89
|
+
const margin = values.map(Number.parseFloat).find(Number.isFinite) ?? 0;
|
|
90
|
+
left -= margin * scale;
|
|
91
|
+
right += margin * scale;
|
|
92
|
+
}
|
|
93
|
+
const legacy = style.clip.match(/^rect\((?:([^, ]+)[, ]+)(?:([^, ]+)[, ]+)(?:([^, ]+)[, ]+)([^, )]+)\)$/);
|
|
94
|
+
if (legacy) {
|
|
95
|
+
const length = (value, fallback) => value && value !== "auto" ? Number.parseFloat(value) * scale : fallback;
|
|
96
|
+
left = box.left + length(legacy[4], 0);
|
|
97
|
+
right = box.left + length(legacy[2], box.width);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
left,
|
|
101
|
+
right
|
|
102
|
+
};
|
|
103
|
+
};
|
|
68
104
|
return {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
105
|
+
extent,
|
|
106
|
+
endOverflow,
|
|
107
|
+
extentClipped,
|
|
108
|
+
rtl,
|
|
109
|
+
elements: [...document.querySelectorAll("*")].map((element) => {
|
|
110
|
+
const box = element.getBoundingClientRect();
|
|
111
|
+
const clips = [];
|
|
112
|
+
let ancestor = element.parentElement;
|
|
113
|
+
while (ancestor) {
|
|
114
|
+
const style = getComputedStyle(ancestor);
|
|
115
|
+
if (clipsContent(style)) clips.push(clipBox(ancestor, style));
|
|
116
|
+
ancestor = ancestor.parentElement;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
locator: locator(element),
|
|
120
|
+
box: {
|
|
121
|
+
left: box.left,
|
|
122
|
+
right: box.right
|
|
123
|
+
},
|
|
124
|
+
clips
|
|
125
|
+
};
|
|
126
|
+
})
|
|
77
127
|
};
|
|
78
|
-
}
|
|
128
|
+
});
|
|
129
|
+
const contributions = [];
|
|
130
|
+
let clippedStart = 0;
|
|
131
|
+
let clippedEnd = measured.endOverflow;
|
|
132
|
+
let rightOverflow = measured.rtl || measured.extentClipped ? 0 : measured.endOverflow;
|
|
133
|
+
for (const element of measured.elements) {
|
|
134
|
+
const overflow = horizontalOverflow(element.box, element.clips, pageWidth, measured.rtl);
|
|
135
|
+
clippedStart = Math.max(clippedStart, overflow.clippedStart);
|
|
136
|
+
clippedEnd = Math.max(clippedEnd, overflow.clippedEnd);
|
|
137
|
+
rightOverflow = Math.max(rightOverflow, overflow.rightOverflow);
|
|
138
|
+
if ((overflow.clippedStart > 0 || overflow.clippedEnd > 0) && contributions.length < 20) contributions.push({
|
|
139
|
+
locator: element.locator,
|
|
140
|
+
clippedStart: overflow.clippedStart,
|
|
141
|
+
clippedEnd: overflow.clippedEnd
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
overflow: {
|
|
146
|
+
pageWidth,
|
|
147
|
+
extent: measured.extent,
|
|
148
|
+
clippedStart,
|
|
149
|
+
clippedEnd,
|
|
150
|
+
elements: contributions
|
|
151
|
+
},
|
|
152
|
+
rightOverflow
|
|
153
|
+
};
|
|
79
154
|
}
|
|
80
155
|
function overflowEntry(measured, scale, part, inputIndex) {
|
|
81
156
|
return {
|
|
@@ -120,4 +195,4 @@ async function printWithOverflow(tab, pageWidth, part, pdf, inputIndex) {
|
|
|
120
195
|
};
|
|
121
196
|
}
|
|
122
197
|
//#endregion
|
|
123
|
-
export { CHROMIUM_MAX_SHRINK, measureOverflow, printWithOverflow, solveScale };
|
|
198
|
+
export { CHROMIUM_MAX_SHRINK, clippedHorizontalBox, horizontalOverflow, measureOverflow, printWithOverflow, solveScale };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segnavia/render",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@segnavia/format": "0.
|
|
30
|
-
"@segnavia/frame": "0.
|
|
31
|
-
"@segnavia/html": "0.
|
|
29
|
+
"@segnavia/format": "0.9.1",
|
|
30
|
+
"@segnavia/frame": "0.9.1",
|
|
31
|
+
"@segnavia/html": "0.9.1",
|
|
32
32
|
"playwright-core": "1.55.1",
|
|
33
33
|
"pdf-lib": "^1.17.1"
|
|
34
34
|
}
|