diff --git a/src/renderer/src/components/Preview/utils.ts b/src/renderer/src/components/Preview/utils.ts index 4fd082ce67..4cb1aaacfa 100644 --- a/src/renderer/src/components/Preview/utils.ts +++ b/src/renderer/src/components/Preview/utils.ts @@ -30,11 +30,6 @@ export function renderSvgInShadowHost(svgContent: string, hostElement: HTMLEleme width: 100%; height: 100%; } - - svg { - width: auto; - height: auto; - } ` // Clear previous content and append new style diff --git a/src/renderer/src/utils/__tests__/image.test.ts b/src/renderer/src/utils/__tests__/image.test.ts index 0893bbabb2..67feb7a640 100644 --- a/src/renderer/src/utils/__tests__/image.test.ts +++ b/src/renderer/src/utils/__tests__/image.test.ts @@ -131,71 +131,84 @@ describe('utils/image', () => { const createSvgElement = (svgString: string): SVGElement => { const div = document.createElement('div') div.innerHTML = svgString - return div.querySelector('svg') as SVGElement + const svgElement = div.querySelector('svg') + if (!svgElement) { + throw new Error(`Test setup error: No element found in string: "${svgString}"`) + } + return 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) + // Mock document.body.appendChild to avoid errors in jsdom + beforeEach(() => { + vi.spyOn(document.body, 'appendChild').mockImplementation(() => ({}) as Node) + vi.spyOn(document.body, 'removeChild').mockImplementation(() => ({}) as Node) }) - it('should not overwrite existing viewBox but still remove width/height', () => { - const svgElement = createSvgElement('') - const result = makeSvgScalable(svgElement) + it('should measure and add viewBox/max-width when viewBox is missing', () => { + const svgElement = createSvgElement('') + // Mock the measurement result on the prototype + const spy = vi + .spyOn(SVGElement.prototype, 'getBoundingClientRect') + .mockReturnValue({ width: 133, height: 106 } as DOMRect) + const result = makeSvgScalable(svgElement) as SVGElement + + expect(spy).toHaveBeenCalled() + expect(result.getAttribute('viewBox')).toBe('0 0 133 106') + expect(result.style.maxWidth).toBe('133px') + expect(result.getAttribute('width')).toBe('100%') + expect(result.hasAttribute('height')).toBe(false) + + spy.mockRestore() // Clean up the prototype spy + }) + + it('should use width attribute for max-width when viewBox is present', () => { + const svgElement = createSvgElement('') + const spy = vi.spyOn(SVGElement.prototype, 'getBoundingClientRect') // Spy to ensure it's NOT called + + const result = makeSvgScalable(svgElement) as SVGElement + + expect(spy).not.toHaveBeenCalled() expect(result.getAttribute('viewBox')).toBe('0 0 50 50') - expect(result.hasAttribute('width')).toBe(false) + expect(result.style.maxWidth).toBe('100pt') + expect(result.getAttribute('width')).toBe('100%') expect(result.hasAttribute('height')).toBe(false) + + spy.mockRestore() }) - it('should not add viewBox for non-numeric width/height but still remove them', () => { - const svgElement = createSvgElement('') - const result = makeSvgScalable(svgElement) + it('should handle measurement failure gracefully', () => { + const svgElement = createSvgElement('') + // Mock a failed measurement + const spy = vi + .spyOn(SVGElement.prototype, 'getBoundingClientRect') + .mockReturnValue({ width: 0, height: 0 } as DOMRect) + + const result = makeSvgScalable(svgElement) as SVGElement expect(result.hasAttribute('viewBox')).toBe(false) - expect(result.hasAttribute('width')).toBe(false) - expect(result.hasAttribute('height')).toBe(false) + expect(result.style.maxWidth).toBe('100pt') // Falls back to width attribute + expect(result.getAttribute('width')).toBe('100%') + + spy.mockRestore() }) - 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) + it('should only set width="100%" if width/height attributes are missing', () => { + const svgElement = createSvgElement('') + const result = makeSvgScalable(svgElement) as SVGElement expect(result.hasAttribute('viewBox')).toBe(false) + expect(result.style.maxWidth).toBe('') + expect(result.getAttribute('width')).toBe('100%') 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 8198eda730..f729ba666b 100644 --- a/src/renderer/src/utils/image.ts +++ b/src/renderer/src/utils/image.ts @@ -272,31 +272,79 @@ export const svgToSvgBlob = (svgElement: SVGElement): Blob => { } /** - * 确保 SVG 元素有 viewBox 并且移除固定的 width/height 属性。 - * 用于“预览”功能,让 SVG 在容器内可缩放。 + * 使用离屏容器测量 DOM 元素的渲染尺寸 + * @param element 要测量的元素 + * @returns 渲染元素的宽度和高度(以像素为单位) + */ +function measureElementSize(element: Element): { width: number; height: number } { + const clone = element.cloneNode(true) as Element + + // 检查元素类型并重置样式 + if (clone instanceof HTMLElement || clone instanceof SVGElement) { + clone.style.width = '' + clone.style.height = '' + clone.style.position = '' + clone.style.visibility = '' + } + + // 创建一个离屏容器 + const container = document.createElement('div') + container.style.position = 'absolute' + container.style.top = '-9999px' + container.style.left = '-9999px' + container.style.visibility = 'hidden' + + container.appendChild(clone) + document.body.appendChild(container) + + // 测量并清理 + const rect = clone.getBoundingClientRect() + document.body.removeChild(container) + + return { width: rect.width, height: rect.height } +} + +/** + * 让 SVG 元素在容器内可缩放,用于“预览”功能。 + * - 补充缺失的 viewBox + * - 补充缺失的 max-width style + * - 把 width 改为 100% + * - 移除 height */ export const makeSvgScalable = (element: Element): Element => { - // Type Guard: Only proceed if the element is actually an SVGElement. + // type guard if (!(element instanceof SVGElement)) { return element } const hasViewBox = element.hasAttribute('viewBox') - const width = element.getAttribute('width') - const height = element.getAttribute('height') + const widthStr = element.getAttribute('width') + const heightStr = 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}`) - element.setAttribute('max-width', `${numericWidth}px`) + let measuredWidth: number | undefined + + // 如果缺少 viewBox 属性,测量元素尺寸来创建 + if (!hasViewBox) { + // 只在 width 和 height 都有值时测量 + if (widthStr && heightStr) { + const renderedSize = measureElementSize(element) + if (renderedSize.width > 0 && renderedSize.height > 0) { + measuredWidth = renderedSize.width + element.setAttribute('viewBox', `0 0 ${renderedSize.width} ${renderedSize.height}`) + } } } - // 移除固定的 width 和 height 属性,让 CSS 控制元素尺寸 - element.removeAttribute('width') + // 设置 max-width + // 优先使用测量得到的宽度值,否则回退到 width 属性值 + if (measuredWidth !== undefined) { + element.style.setProperty('max-width', `${measuredWidth}px`) + } else if (widthStr) { + element.style.setProperty('max-width', widthStr) + } + + // 调整 width 和 height + element.setAttribute('width', '100%') element.removeAttribute('height') return element