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:
1 parent
2611d87f8c
commit
8c36c6121b
5 files changed
+284
-57
No files matched your search
@@ -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`,
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user