diff --git a/web-common/src/features/canvas/CanvasDashboardWrapper.svelte b/web-common/src/features/canvas/CanvasDashboardWrapper.svelte
index cab84a40e072..e3538fed692f 100644
--- a/web-common/src/features/canvas/CanvasDashboardWrapper.svelte
+++ b/web-common/src/features/canvas/CanvasDashboardWrapper.svelte
@@ -66,11 +66,12 @@
class="pointer-events-none absolute"
style="left: -99999px; top: 0;"
>
-
+
+
{/if}
diff --git a/web-common/src/features/exports/pdf/assemble.ts b/web-common/src/features/exports/pdf/assemble.ts
index 9f692e23f3c6..6ef80848a03e 100644
--- a/web-common/src/features/exports/pdf/assemble.ts
+++ b/web-common/src/features/exports/pdf/assemble.ts
@@ -97,7 +97,10 @@ function drawTitle(
doc.setFontSize(TITLE_FONT_SIZE_PT);
setTextColor(doc, TITLE_COLOR);
- const maxWidthPt = result.pageWidthPt - 2 * result.marginPt;
+ // Content narrower than the page is centred (see paginate), and the title
+ // follows it rather than sitting at the page margin.
+ const contentLeftPt = result.marginPt + result.contentOffsetPt;
+ const maxWidthPt = result.pageWidthPt - 2 * contentLeftPt;
let title = meta.title;
if (doc.getTextWidth(title) > maxWidthPt) {
while (title.length > 1 && doc.getTextWidth(`${title}…`) > maxWidthPt) {
@@ -107,7 +110,7 @@ function drawTitle(
}
// Baseline near the bottom of the reserved band, leaving a gap before content.
- doc.text(title, result.marginPt, result.marginPt + TITLE_FONT_SIZE_PT);
+ doc.text(title, contentLeftPt, result.marginPt + TITLE_FONT_SIZE_PT);
doc.setFont("helvetica", "normal");
}
@@ -116,6 +119,7 @@ function drawFooter(
result: PaginationResult,
meta: AssembleMeta,
): void {
+ const contentLeftPt = result.marginPt + result.contentOffsetPt;
const yPt = result.pageHeightPt - 10;
const generatedText = `Generated ${meta.generatedAt}`;
const linkPrefix = "Open the live dashboard: ";
@@ -123,11 +127,11 @@ function drawFooter(
doc.setFontSize(8);
setTextColor(doc, FOOTER_TEXT_COLOR);
- doc.text(generatedText, result.marginPt, yPt);
+ doc.text(generatedText, contentLeftPt, yPt);
const linkXPt =
result.pageWidthPt -
- result.marginPt -
+ contentLeftPt -
doc.getTextWidth(`${linkPrefix}${linkText}`);
doc.text(linkPrefix, linkXPt, yPt);
setTextColor(doc, FOOTER_LINK_COLOR);
diff --git a/web-common/src/features/exports/pdf/capture.spec.ts b/web-common/src/features/exports/pdf/capture.spec.ts
index 47b74d5da4f0..40a9c4922840 100644
--- a/web-common/src/features/exports/pdf/capture.spec.ts
+++ b/web-common/src/features/exports/pdf/capture.spec.ts
@@ -1,6 +1,99 @@
// @vitest-environment jsdom
-import { describe, expect, it } from "vitest";
-import { captureTargetsIn, inlineSvgStyles, rowIndexFor } from "./capture";
+import { getFontEmbedCSS, toJpeg } from "html-to-image";
+import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
+import {
+ captureCanvasBlocks,
+ captureTargetsIn,
+ inlineSvgStyles,
+ rasterizeNode,
+ rowIndexFor,
+} from "./capture";
+
+vi.mock("html-to-image", () => ({
+ toJpeg: vi.fn(() => Promise.resolve("data:image/jpeg;base64,")),
+ getFontEmbedCSS: vi.fn(() => Promise.resolve("")),
+}));
+
+describe("rasterizeNode", () => {
+ beforeEach(() => vi.mocked(toJpeg).mockClear());
+
+ function cardWith(inner: string): HTMLElement {
+ const card = document.createElement("div");
+ card.innerHTML = inner;
+ return card;
+ }
+
+ // WebKit hands back a blank raster the first time it captures a