From cd07bc0e683fa0dec79097af464c99732104a1f4 Mon Sep 17 00:00:00 2001 From: tianma-if Date: Thu, 8 Oct 2026 17:05:43 +0800 Subject: [PATCH] Recover Android note image rendering failures --- .../src/components/LocalTiptapEditor.tsx | 43 +++++++++++++++++-- .../src/screens/WorkspaceMemoDetail.tsx | 4 ++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/apps/mobile/src/components/LocalTiptapEditor.tsx b/apps/mobile/src/components/LocalTiptapEditor.tsx index 7afb41e4..c479a440 100644 --- a/apps/mobile/src/components/LocalTiptapEditor.tsx +++ b/apps/mobile/src/components/LocalTiptapEditor.tsx @@ -1253,6 +1253,7 @@ function LocalTiptapEditorImpl(props: LocalTiptapEditorProps) { const reportProgress = (stage: "prepare" | "render" | "transfer") => { void Promise.resolve(notify({ type: "progress", stage })).catch(() => {}); }; + const missingImagePlaceholder = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="; void (async () => { reportProgress("prepare"); @@ -1319,7 +1320,9 @@ function LocalTiptapEditorImpl(props: LocalTiptapEditorProps) { const failedImages = exportedImages.filter((image) => !image.complete || image.naturalWidth === 0).length; const totalHeight = Math.max(1, Math.ceil(documentRoot.getBoundingClientRect().height)); const renderPlan = planMobileNoteImageRender(targetWidth, totalHeight); + let embedFailedImages = 0; let captureRoot = documentRoot; + let captureWrapper: HTMLDivElement | null = null; if (renderPlan.sourceScale < 1) { const wrapper = document.createElement("div"); wrapper.style.cssText = `position:relative;width:${renderPlan.sourceWidth}px;height:${renderPlan.sourceHeight}px;overflow:hidden;`; @@ -1328,18 +1331,50 @@ function LocalTiptapEditorImpl(props: LocalTiptapEditorProps) { documentRoot.style.transform = `scale(${renderPlan.sourceScale})`; documentRoot.style.transformOrigin = "top left"; captureRoot = wrapper; + captureWrapper = wrapper; } const backgroundColor = NOTE_IMAGE_BACKGROUND_COLORS[resolvedTheme] || themeCfg.canvasBg; reportProgress("render"); - const canvas = await toCanvas(captureRoot, { + const renderCanvas = (root: HTMLElement, width: number, height: number) => toCanvas(root, { backgroundColor, cacheBust: false, - height: renderPlan.sourceHeight, + height, + imagePlaceholder: missingImagePlaceholder, + onImageErrorHandler: (event) => { + embedFailedImages += 1; + if (event instanceof Event && event.target instanceof HTMLImageElement) { + event.target.src = missingImagePlaceholder; + } + }, pixelRatio: renderPlan.pixelRatio, skipFonts: true, - width: renderPlan.sourceWidth, + width, }); + let canvas: HTMLCanvasElement; + try { + canvas = await renderCanvas(captureRoot, renderPlan.sourceWidth, renderPlan.sourceHeight); + } catch { + // Android WebView can reject a large SVG image without an Error message. + // Try a smaller source once while keeping the entire card visible. + const retryScale = renderPlan.sourceScale * 0.75; + const retryWidth = Math.max(1, Math.floor(targetWidth * retryScale)); + const retryHeight = Math.max(1, Math.floor(totalHeight * retryScale)); + if (retryScale < 0.5) throw new Error("NOTE_IMAGE_RENDER_FAILED"); + if (!captureWrapper) { + captureWrapper = document.createElement("div"); + documentRoot.replaceWith(captureWrapper); + captureWrapper.appendChild(documentRoot); + } + captureWrapper.style.cssText = `position:relative;width:${retryWidth}px;height:${retryHeight}px;overflow:hidden;`; + documentRoot.style.transform = `scale(${retryScale})`; + documentRoot.style.transformOrigin = "top left"; + try { + canvas = await renderCanvas(captureWrapper, retryWidth, retryHeight); + } catch { + throw new Error("NOTE_IMAGE_RENDER_FAILED"); + } + } const blob = await new Promise((resolve, reject) => { canvas.toBlob( (result) => result ? resolve(result) : reject(new Error("Image renderer returned an empty file")), @@ -1365,7 +1400,7 @@ function LocalTiptapEditorImpl(props: LocalTiptapEditorProps) { width: canvas.width, height: canvas.height, totalImages: exportedImages.length, - failedImages, + failedImages: Math.min(exportedImages.length, failedImages + embedFailedImages), }); } catch (error) { await notify({ type: "error", message: error instanceof Error ? error.message : "Image export failed" }); diff --git a/apps/mobile/src/screens/WorkspaceMemoDetail.tsx b/apps/mobile/src/screens/WorkspaceMemoDetail.tsx index 72ad54bb..9e4cc112 100644 --- a/apps/mobile/src/screens/WorkspaceMemoDetail.tsx +++ b/apps/mobile/src/screens/WorkspaceMemoDetail.tsx @@ -826,6 +826,10 @@ export const MemoDetailModal = ({ ? (resolvedLocale !== "zh-CN" ? "This note is too long for one readable image. Choose a smaller font size or split the note." : "这篇笔记太长,无法生成清晰的单张图片。请调小导出字号,或拆分笔记后重试。") + : message === "NOTE_IMAGE_RENDER_FAILED" + ? (resolvedLocale !== "zh-CN" + ? "This image could not be rendered on this device. Try a smaller font size or a shorter note." + : "这台设备无法渲染这张图片。请调小导出字号,或缩短笔记后重试。") : message; Alert.alert( resolvedLocale !== "zh-CN" ? "Image export failed" : "导出笔记图片失败",