From 83ba64c8c8b45fe742d9683857a1e03b4f637db3 Mon Sep 17 00:00:00 2001 From: chouheiwa <849131492@qq.com> Date: Sun, 15 Feb 2026 17:23:26 +0800 Subject: [PATCH 1/4] fix: ensure CJK text visibility in SVG export --- src/lib/components/Actions.svelte | 61 +++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/lib/components/Actions.svelte b/src/lib/components/Actions.svelte index 68a730d3..d45a5328 100644 --- a/src/lib/components/Actions.svelte +++ b/src/lib/components/Actions.svelte @@ -27,9 +27,70 @@ const getFileName = (extension: string) => `mermaid-diagram-${dayjs().format('YYYY-MM-DD-HHmmss')}.${extension}`; + /** + * Fix text visibility issues in exported SVG. + * Handles elements and elements (Mermaid's text containers). + */ + const ensureTextVisibility = (svg: HTMLElement) => { + const textElements = svg.querySelectorAll('text, tspan'); + const foreignObjects = svg.querySelectorAll('foreignObject'); + + // Fix elements with CJK characters + textElements.forEach((el) => { + if (el.getAttribute('data-text-fixed') === 'true') { + return; + } + + const text = el.textContent || ''; + const hasCJK = /[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/.test(text); + + if (hasCJK) { + el.setAttribute('dominant-baseline', 'middle'); + el.setAttribute('data-text-fixed', 'true'); + } + }); + + // Fix elements (Mermaid's primary text containers) + foreignObjects.forEach((foreignObj) => { + const content = foreignObj.textContent || ''; + const hasCJK = /[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/.test(content); + + if (hasCJK) { + // Increase foreignObject height to prevent CJK text clipping + const currentHeight = parseFloat(foreignObj.getAttribute('height') || '24'); + const currentY = parseFloat(foreignObj.getAttribute('y') || '0'); + const newHeight = currentHeight * 1.5; + + // Adjust y coordinate to compensate for height increase, keeping text visually centered + const heightDiff = newHeight - currentHeight; + const newY = currentY - heightDiff / 2; + + foreignObj.setAttribute('height', newHeight.toString()); + foreignObj.setAttribute('y', newY.toString()); + + // Apply vertical centering styles to inner HTML elements + const htmlElements = foreignObj.querySelectorAll('div, span, p'); + htmlElements.forEach((htmlEl) => { + const el = htmlEl as HTMLElement; + el.style.display = 'flex'; + el.style.alignItems = 'center'; + el.style.justifyContent = 'center'; + el.style.height = '100%'; + el.style.lineHeight = '1.2'; + }); + } + }); + }; + const getSvgElement = () => { const svgElement = document.querySelector('#container svg')?.cloneNode(true) as HTMLElement; svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); + + // Ensure CJK text visibility in hand-drawn (rough) mode export + if ($stateStore.rough) { + ensureTextVisibility(svgElement); + } + return svgElement; }; From 49f800bacb33af6572d6463b67d4fdb368fdad7d Mon Sep 17 00:00:00 2001 From: chouheiwa <849131492@qq.com> Date: Mon, 16 Feb 2026 09:18:17 +0800 Subject: [PATCH 2/4] fix: apply foreignObject clipping fix to all languages in rough mode SVG export --- src/lib/components/Actions.svelte | 68 ++++++++++--------------------- 1 file changed, 21 insertions(+), 47 deletions(-) diff --git a/src/lib/components/Actions.svelte b/src/lib/components/Actions.svelte index d45a5328..a44d7508 100644 --- a/src/lib/components/Actions.svelte +++ b/src/lib/components/Actions.svelte @@ -28,57 +28,32 @@ `mermaid-diagram-${dayjs().format('YYYY-MM-DD-HHmmss')}.${extension}`; /** - * Fix text visibility issues in exported SVG. - * Handles elements and elements (Mermaid's text containers). + * Fix text clipping in exported SVG for hand-drawn (rough) mode. + * svg2roughjs copies foreignObject elements but their height is often insufficient, + * causing text bottom edges to be cut off regardless of language. */ - const ensureTextVisibility = (svg: HTMLElement) => { - const textElements = svg.querySelectorAll('text, tspan'); + const fixForeignObjectClipping = (svg: HTMLElement) => { const foreignObjects = svg.querySelectorAll('foreignObject'); - - // Fix elements with CJK characters - textElements.forEach((el) => { - if (el.getAttribute('data-text-fixed') === 'true') { - return; - } - - const text = el.textContent || ''; - const hasCJK = /[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/.test(text); - - if (hasCJK) { - el.setAttribute('dominant-baseline', 'middle'); - el.setAttribute('data-text-fixed', 'true'); - } - }); - - // Fix elements (Mermaid's primary text containers) foreignObjects.forEach((foreignObj) => { - const content = foreignObj.textContent || ''; - const hasCJK = /[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/.test(content); + const currentHeight = parseFloat(foreignObj.getAttribute('height') || '0'); + if (currentHeight <= 0) return; - if (hasCJK) { - // Increase foreignObject height to prevent CJK text clipping - const currentHeight = parseFloat(foreignObj.getAttribute('height') || '24'); - const currentY = parseFloat(foreignObj.getAttribute('y') || '0'); - const newHeight = currentHeight * 1.5; + const currentY = parseFloat(foreignObj.getAttribute('y') || '0'); + const newHeight = currentHeight * 1.5; + const heightDiff = newHeight - currentHeight; - // Adjust y coordinate to compensate for height increase, keeping text visually centered - const heightDiff = newHeight - currentHeight; - const newY = currentY - heightDiff / 2; + foreignObj.setAttribute('height', newHeight.toString()); + foreignObj.setAttribute('y', (currentY - heightDiff / 2).toString()); - foreignObj.setAttribute('height', newHeight.toString()); - foreignObj.setAttribute('y', newY.toString()); - - // Apply vertical centering styles to inner HTML elements - const htmlElements = foreignObj.querySelectorAll('div, span, p'); - htmlElements.forEach((htmlEl) => { - const el = htmlEl as HTMLElement; - el.style.display = 'flex'; - el.style.alignItems = 'center'; - el.style.justifyContent = 'center'; - el.style.height = '100%'; - el.style.lineHeight = '1.2'; - }); - } + // Ensure inner HTML elements are vertically centered within the expanded area + const htmlElements = foreignObj.querySelectorAll('div, span, p'); + htmlElements.forEach((htmlEl) => { + const el = htmlEl as HTMLElement; + el.style.display = 'flex'; + el.style.alignItems = 'center'; + el.style.justifyContent = 'center'; + el.style.height = '100%'; + }); }); }; @@ -86,9 +61,8 @@ const svgElement = document.querySelector('#container svg')?.cloneNode(true) as HTMLElement; svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); - // Ensure CJK text visibility in hand-drawn (rough) mode export if ($stateStore.rough) { - ensureTextVisibility(svgElement); + fixForeignObjectClipping(svgElement); } return svgElement; From 5dd1ec6b6359fdfe76e5845516c6e3c2ecd5612f Mon Sep 17 00:00:00 2001 From: chouheiwa <849131492@qq.com> Date: Mon, 16 Feb 2026 11:12:40 +0800 Subject: [PATCH 3/4] fix: apply foreignObject clipping fix to PNG export path Move fixForeignObjectClipping into getBase64SVG so it covers both SVG and PNG export flows. Previously only getSvgElement (SVG export) applied the fix, while exportImage (PNG export) passed the svg element directly to getBase64SVG, bypassing the fix entirely. --- src/lib/components/Actions.svelte | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/lib/components/Actions.svelte b/src/lib/components/Actions.svelte index a44d7508..2f95c6d4 100644 --- a/src/lib/components/Actions.svelte +++ b/src/lib/components/Actions.svelte @@ -60,11 +60,6 @@ const getSvgElement = () => { const svgElement = document.querySelector('#container svg')?.cloneNode(true) as HTMLElement; svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); - - if ($stateStore.rough) { - fixForeignObjectClipping(svgElement); - } - return svgElement; }; @@ -85,6 +80,10 @@ svg = getSvgElement(); } + if ($stateStore.rough) { + fixForeignObjectClipping(svg); + } + svg.style.backgroundColor = window .getComputedStyle(document.body) .getPropertyValue('--background'); From 5aed4ea7a05c74c430f126ec44fc7b5c400d0652 Mon Sep 17 00:00:00 2001 From: chouheiwa <849131492@qq.com> Date: Mon, 16 Feb 2026 16:19:22 +0800 Subject: [PATCH 4/4] fix: use viewBox dimensions for PNG export canvas size In rough mode, SVG width/height are set to 100%, so getBoundingClientRect returns the container size instead of the actual diagram size. Use viewBox dimensions when available to get the correct aspect ratio for PNG export. --- src/lib/components/Actions.svelte | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/lib/components/Actions.svelte b/src/lib/components/Actions.svelte index 2f95c6d4..983e6618 100644 --- a/src/lib/components/Actions.svelte +++ b/src/lib/components/Actions.svelte @@ -117,18 +117,25 @@ ${svgString}`); const box = svg.getBoundingClientRect(); + // In rough mode, SVG has width/height="100%" so getBoundingClientRect returns + // the container size, not the actual diagram size. Use viewBox dimensions instead. + const svgEl = svg as unknown as SVGSVGElement; + const viewBox = svgEl.viewBox?.baseVal; + const contentWidth = viewBox && viewBox.width > 0 ? viewBox.width : box.width; + const contentHeight = viewBox && viewBox.height > 0 ? viewBox.height : box.height; + if (imageSizeMode === 'width') { - const ratio = box.height / box.width; + const ratio = contentHeight / contentWidth; canvas.width = imageSize; canvas.height = imageSize * ratio; } else if (imageSizeMode === 'height') { - const ratio = box.width / box.height; + const ratio = contentWidth / contentHeight; canvas.width = imageSize * ratio; canvas.height = imageSize; } else { const multiplier = 2; - canvas.width = box.width * multiplier; - canvas.height = box.height * multiplier; + canvas.width = contentWidth * multiplier; + canvas.height = contentHeight * multiplier; } const context = canvas.getContext('2d');