diff --git a/src/renderer/src/components/Preview/utils.ts b/src/renderer/src/components/Preview/utils.ts
index db5ba3457b..5b9e23cf96 100644
--- a/src/renderer/src/components/Preview/utils.ts
+++ b/src/renderer/src/components/Preview/utils.ts
@@ -1,3 +1,5 @@
+import { makeSvgScalable } from '@renderer/utils'
+
/**
* Renders an SVG string inside a host element's Shadow DOM to ensure style encapsulation.
* This function handles creating the shadow root, injecting base styles for the host,
@@ -14,13 +16,13 @@ export function renderSvgInShadowHost(svgContent: string, hostElement: HTMLEleme
const shadowRoot = hostElement.shadowRoot || hostElement.attachShadow({ mode: 'open' })
- // Base styles for the host element
+ // Base styles for the host element and the inner SVG
const style = document.createElement('style')
style.textContent = `
:host {
padding: 1em;
background-color: white;
- overflow: auto;
+ overflow: hidden; /* Prevent scrollbars, as scaling is now handled */
border: 0.5px solid var(--color-code-background);
border-radius: 8px;
display: block;
@@ -28,20 +30,24 @@ export function renderSvgInShadowHost(svgContent: string, hostElement: HTMLEleme
width: 100%;
height: 100%;
}
+
svg {
max-width: 100%;
+ max-height: 100%;
+ width: auto;
height: auto;
}
`
- // Clear previous content and append new style and SVG
+ // Clear previous content and append new style
shadowRoot.innerHTML = ''
shadowRoot.appendChild(style)
- // Parse and append the SVG using DOMParser to prevent script execution and check for errors
if (svgContent.trim() === '') {
return
}
+
+ // Parse and append the SVG using DOMParser to prevent script execution and check for errors
const parser = new DOMParser()
const doc = parser.parseFromString(svgContent, 'image/svg+xml')
@@ -53,6 +59,10 @@ export function renderSvgInShadowHost(svgContent: string, hostElement: HTMLEleme
const svgElement = doc.documentElement
if (svgElement && svgElement.nodeName.toLowerCase() === 'svg') {
+ // Standardize the SVG element for proper scaling
+ makeSvgScalable(svgElement)
+
+ // Append the SVG element to the shadow root
shadowRoot.appendChild(svgElement.cloneNode(true))
} else if (svgContent.trim() !== '') {
// Do not throw error for empty content
diff --git a/src/renderer/src/utils/__tests__/image.test.ts b/src/renderer/src/utils/__tests__/image.test.ts
index fc658be50b..0893bbabb2 100644
--- a/src/renderer/src/utils/__tests__/image.test.ts
+++ b/src/renderer/src/utils/__tests__/image.test.ts
@@ -6,7 +6,8 @@ import {
captureScrollableDivAsBlob,
captureScrollableDivAsDataURL,
compressImage,
- convertToBase64
+ convertToBase64,
+ makeSvgScalable
} from '../image'
// mock 依赖
@@ -125,4 +126,76 @@ describe('utils/image', () => {
expect(func).not.toHaveBeenCalled()
})
})
+
+ describe('makeSvgScalable', () => {
+ const createSvgElement = (svgString: string): SVGElement => {
+ const div = document.createElement('div')
+ div.innerHTML = svgString
+ return div.querySelector('svg') as SVGElement
+ }
+
+ it('should add viewBox and remove width/height when viewBox is missing', () => {
+ const svgElement = createSvgElement('')
+ const result = makeSvgScalable(svgElement)
+
+ expect(result.getAttribute('viewBox')).toBe('0 0 800 600')
+ expect(result.hasAttribute('width')).toBe(false)
+ expect(result.hasAttribute('height')).toBe(false)
+ })
+
+ it('should not overwrite existing viewBox but still remove width/height', () => {
+ const svgElement = createSvgElement('')
+ const result = makeSvgScalable(svgElement)
+
+ expect(result.getAttribute('viewBox')).toBe('0 0 50 50')
+ expect(result.hasAttribute('width')).toBe(false)
+ expect(result.hasAttribute('height')).toBe(false)
+ })
+
+ it('should not add viewBox for non-numeric width/height but still remove them', () => {
+ const svgElement = createSvgElement('')
+ const result = makeSvgScalable(svgElement)
+
+ expect(result.hasAttribute('viewBox')).toBe(false)
+ expect(result.hasAttribute('width')).toBe(false)
+ expect(result.hasAttribute('height')).toBe(false)
+ })
+
+ it('should do nothing if width, height, and viewBox are missing', () => {
+ const svgElement = createSvgElement('')
+ const originalOuterHTML = svgElement.outerHTML
+ const result = makeSvgScalable(svgElement)
+
+ // Check that no attributes were added
+ expect(result.hasAttribute('viewBox')).toBe(false)
+ expect(result.hasAttribute('width')).toBe(false)
+ expect(result.hasAttribute('height')).toBe(false)
+ // Check that the content is unchanged
+ expect(result.outerHTML).toBe(originalOuterHTML)
+ })
+
+ it('should not add viewBox if only one dimension is present', () => {
+ const svgElement = createSvgElement('')
+ const result = makeSvgScalable(svgElement)
+
+ expect(result.hasAttribute('viewBox')).toBe(false)
+ expect(result.hasAttribute('height')).toBe(false)
+ })
+
+ it('should return the element unchanged if it is not an SVGElement', () => {
+ const divElement = document.createElement('div')
+ divElement.setAttribute('width', '100')
+ divElement.setAttribute('height', '100')
+
+ const originalOuterHTML = divElement.outerHTML
+ const result = makeSvgScalable(divElement)
+
+ // Check that the element is the same object
+ expect(result).toBe(divElement)
+ // Check that the content is unchanged
+ expect(result.outerHTML).toBe(originalOuterHTML)
+ // Verify no viewBox was added
+ expect(result.hasAttribute('viewBox')).toBe(false)
+ })
+ })
})
diff --git a/src/renderer/src/utils/image.ts b/src/renderer/src/utils/image.ts
index a6ff7db536..a94df580f6 100644
--- a/src/renderer/src/utils/image.ts
+++ b/src/renderer/src/utils/image.ts
@@ -270,3 +270,33 @@ export const svgToSvgBlob = (svgElement: SVGElement): Blob => {
const svgData = new XMLSerializer().serializeToString(svgElement)
return new Blob([svgData], { type: 'image/svg+xml' })
}
+
+/**
+ * 确保 SVG 元素有 viewBox 并且移除固定的 width/height 属性。
+ * 用于“预览”功能,让 SVG 在容器内可缩放。
+ */
+export const makeSvgScalable = (element: Element): Element => {
+ // Type Guard: Only proceed if the element is actually an SVGElement.
+ if (!(element instanceof SVGElement)) {
+ return element
+ }
+
+ const hasViewBox = element.hasAttribute('viewBox')
+ const width = element.getAttribute('width')
+ const height = element.getAttribute('height')
+
+ // 缺少 viewBox 但存在 width 和 height 属性时创建 viewBox
+ if (!hasViewBox && width && height) {
+ const numericWidth = parseFloat(width)
+ const numericHeight = parseFloat(height)
+ if (!isNaN(numericWidth) && !isNaN(numericHeight)) {
+ element.setAttribute('viewBox', `0 0 ${numericWidth} ${numericHeight}`)
+ }
+ }
+
+ // 移除固定的 width 和 height 属性,让 CSS 控制元素尺寸
+ element.removeAttribute('width')
+ element.removeAttribute('height')
+
+ return element
+}