Ask what a single note's export should contain, and fix drawings never being in one

Two questions instead of one, because the answers depend on each other:
Markdown holds text only, a PNG holds the drawing only, PDF holds both.
Listing every format flat would have offered combinations that cannot
exist. A note without a drawing skips the first step rather than showing
it mostly greyed out.

While testing that, the drawings turned out never to have made it into
any export at all — not the ZIP, not the PDF. strokeDocOf() *builds* a
stroke document from a bare array, and it was being handed one that was
already a document, so the strokes ended up nested one level too deep and
every length check read undefined. Nothing complained; the export simply
came out without the drawing. It went unnoticed because the notes the
earlier suite exported had none.

Now uses docStrokes(), and the suite draws on a note before exporting it,
checks the PNG's magic bytes, and checks it is 159 KB rather than an empty
white page — a check on the file merely existing would have passed against
the broken version too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-08 17:37:22 +02:00
1 parent 2611d87f8c
commit 8c36c6121b
5 files changed
+284 -57

No files matched your search

+32 -9
View File
@@ -10,7 +10,7 @@
import * as sync from "../syncEngine";
import { parseNoteContent } from "../editor/content";
import { parsingExtensions } from "../editor/extensions.js";
import { CANON_W, CANON_H, paintStrokeDoc, sizeCanvas, strokeDocOf } from "../features/drawing/strokes.js";
import { CANON_W, CANON_H, docStrokes, paintStrokeDoc, sizeCanvas } from "../features/drawing/strokes.js";
import type { NoteContent, NoteMeta, Notebook } from "../types";
import { noteToMarkdown, safeFilename } from "./markdown";
import { printNotes, type PrintableNote } from "./print";
@@ -30,9 +30,10 @@ type Loaded = { meta: NoteMeta; content: NoteContent };
/* ── pieces ── */
/** Renders a stroke document to PNG bytes, so a drawing survives as a picture. */
async function drawingToPng(strokes: unknown): Promise<Uint8Array | null> {
const doc = strokeDocOf(strokes);
if (!doc?.strokes?.length) return null;
async function drawingToPng(doc: unknown): Promise<Uint8Array | null> {
// `doc` is already a stroke document as stored — strokeDocOf() builds one from
// a bare array and would wrap this one a second time, hiding the strokes.
if (docStrokes(doc).length === 0) return null;
const canvas = document.createElement("canvas");
const height = Math.round((DRAWING_EXPORT_W * CANON_H) / CANON_W);
@@ -81,10 +82,31 @@ function nameFor(meta: NoteMeta, taken: Set<string>): string {
/* ── single note ── */
/** What a single-note export should contain. */
export type ExportParts = "text" | "drawing" | "both";
/** Whether this note has a drawing — decides which options the wizard offers. */
export async function noteHasDrawing(meta: NoteMeta): Promise<boolean> {
try {
const { content } = await loadNote(meta);
return docStrokes(content.drawingStrokes).length > 0;
} catch {
return false;
}
}
/** The drawing on its own, as a PNG on white paper. */
export async function exportDrawingAsPng(meta: NoteMeta): Promise<void> {
const { content } = await loadNote(meta);
const png = await drawingToPng(content.drawingStrokes);
if (!png) throw new Error("Diese Notiz enthält keine Zeichnung.");
download(bytesToBlob(png, "image/png"), `${safeFilename(meta.title)}.png`);
}
export async function exportNoteAsMarkdown(meta: NoteMeta): Promise<void> {
const { content } = await loadNote(meta);
const doc = parseNoteContent(content.content, parsingExtensions);
const hasDrawing = !!strokeDocOf(content.drawingStrokes)?.strokes?.length;
const hasDrawing = docStrokes(content.drawingStrokes).length > 0;
const ids = attachmentIds(doc);
const md = noteToMarkdown(doc, {
@@ -133,7 +155,8 @@ const blobToDataUrl = (blob: Blob): Promise<string> =>
export async function exportNotesAsPdf(
metas: NoteMeta[],
title: string,
onProgress?: (p: ExportProgress) => void
onProgress?: (p: ExportProgress) => void,
parts: ExportParts = "both"
): Promise<void> {
const printable: PrintableNote[] = [];
const total = metas.length;
@@ -153,10 +176,10 @@ export async function exportNotesAsPdf(
}
}
const png = await drawingToPng(content.drawingStrokes);
const png = parts === "text" ? null : await drawingToPng(content.drawingStrokes);
const drawing = png ? await blobToDataUrl(bytesToBlob(png, "image/png")) : null;
printable.push({ meta, content, drawing, images });
printable.push({ meta, content, drawing, images, includeText: parts !== "drawing" });
} catch {
/* an unreadable note is left out rather than aborting the whole print */
}
@@ -225,7 +248,7 @@ export async function exportNotesAsZip({
try {
const { content } = await loadNote(meta);
const doc = parseNoteContent(content.content, parsingExtensions);
const hasDrawing = !!strokeDocOf(content.drawingStrokes)?.strokes?.length;
const hasDrawing = docStrokes(content.drawingStrokes).length > 0;
entries.push({
name: `${name}.md`,
+8 -3
View File
@@ -138,21 +138,26 @@ const STYLES = `
export interface PrintableNote {
meta: NoteMeta;
content: NoteContent;
/** Drawing rendered as a data URL, if the note has one. */
/** Drawing rendered as a data URL, if the note has one and it was asked for. */
drawing?: string | null;
/** Attachment id → data URL, for images to survive into the print document. */
images?: Map<string, string>;
/** Omitted for a drawing-only export, where the text would be in the way. */
includeText?: boolean;
}
function articleFor(note: PrintableNote): string {
const doc = parseNoteContent(note.content.content, parsingExtensions);
const resolve = (id: string) => note.images?.get(id) || null;
const when = note.meta.updatedAt ? new Date(note.meta.updatedAt).toLocaleString("de-DE") : "";
const body =
note.includeText === false
? ""
: blocks(parseNoteContent(note.content.content, parsingExtensions).content, resolve);
return `<article>
<h1>${esc(note.meta.title || "Untitled")}</h1>
${when ? `<div class="meta">${esc(when)}</div>` : ""}
${blocks(doc.content, resolve)}
${body}
${note.drawing ? `<div class="drawing"><img src="${note.drawing}" alt="Zeichnung"></div>` : ""}
</article>`;
}