From 837039917dff170641cfd39e8c46cf1b6e550081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Herrmann?= Date: Sun, 23 Aug 2026 16:40:48 +0200 Subject: [PATCH] Add PNG export to web editor --- packages/web/src/editor/editor.tsx | 35 ++++++++++++++++++- .../web/src/editor/top-chrome/top-chrome.tsx | 11 +++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/web/src/editor/editor.tsx b/packages/web/src/editor/editor.tsx index 84492c4..d85e021 100644 --- a/packages/web/src/editor/editor.tsx +++ b/packages/web/src/editor/editor.tsx @@ -47,6 +47,19 @@ function captureLabel( return canvas; } +function downloadCanvasAsPng(canvas: HTMLCanvasElement, filename: string): void { + canvas.toBlob((blob) => { + if (!blob) return; + + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + link.click(); + URL.revokeObjectURL(url); + }, "image/png"); +} + function rotateCanvas90CW(src: HTMLCanvasElement): HTMLCanvasElement { const dst = document.createElement("canvas"); dst.width = src.height; @@ -74,6 +87,26 @@ export function Editor() { return () => window.removeEventListener("beforeunload", h); }, []); + const exportPng = useCallback(async (): Promise => { + const stage = stageRef.current; + if (!stage) return; + + const { label, currentLabelName } = useEditorV2Store.getState(); + + // Deselect to avoid selection handles in the exported image. + useEditorV2Store.getState().clearSelection(); + + // Wait a frame for Konva to re-render without selection handles. + await new Promise((r) => requestAnimationFrame(r)); + + const canvas = captureLabel(stage, label.widthPx, label.heightPx); + + const filename = + (currentLabelName || "label").replace(/[^a-zA-Z0-9._-]+/g, "_") + ".png"; + + downloadCanvasAsPng(canvas, filename); + }, []); + const print = useCallback(async (copies: number): Promise => { const printer = getPrinter(); const stage = stageRef.current; @@ -131,7 +164,7 @@ export function Editor() { return (
- +
diff --git a/packages/web/src/editor/top-chrome/top-chrome.tsx b/packages/web/src/editor/top-chrome/top-chrome.tsx index 8e9a9a6..7b1e44c 100644 --- a/packages/web/src/editor/top-chrome/top-chrome.tsx +++ b/packages/web/src/editor/top-chrome/top-chrome.tsx @@ -8,6 +8,7 @@ import { Grid3x3, Ruler, Heart, + Download, } from "lucide-react"; import { useEditorV2Store } from "../../store/editor-store.ts"; import { PrintButton } from "./print-button.tsx"; @@ -18,9 +19,10 @@ import logoSvg from "../../assets/logo.svg"; interface TopChromeProps { onPrint: (copies: number) => Promise; + onExportPng: () => Promise; } -export function TopChrome({ onPrint }: TopChromeProps) { +export function TopChrome({ onPrint, onExportPng }: TopChromeProps) { const zoom = useEditorV2Store((s) => s.zoom); const label = useEditorV2Store((s) => s.label); const gridVisible = useEditorV2Store((s) => s.gridVisible); @@ -81,6 +83,13 @@ export function TopChrome({ onPrint }: TopChromeProps) { +