Skip to content

SVG 导出为什么糊了?8 个坑我逐个跑了一遍 ​

SVG 号称无损,可抱怨从来没断过:导出 PNG 糊了、尺寸对不上、字变了、中文变成方块。

这些抱怨有个共同特点——从来没人说是 SVG 的错,都说是「某个软件有问题」。所以我干脆自己当一次那个软件。

我用三套完全独立的 SVG 栅格化器(无头 Chromium、resvg、cairosvg),把 8 个常见坑一个个做成最小复现,每条都量到了具体像素和字节。

结论先给:

  1. 最常见的三个原因:目标机器没装字体(回退,字宽变了你没察觉)、导出少了 viewBox(内容不跟着缩放)、只导了 1x(屏幕是 2 倍屏)。这三个占我看到的反馈里的大多数。
  2. 最快定位的办法:把 SVG 拖进浏览器窗口,看渲染出来的比例和设计稿是不是一致;再把根元素的 width/height 删掉、只留 viewBox 看一次——比例不对就是尺寸单位或 viewBox 的锅,比例对但边缘发虚就是 DPR 或字体的锅。
  3. 一个反直觉的真相:SVG 文件本身几乎不会「糊」。糊的一定是栅格化那一步——或者那个工具压根不支持你文件里的某个特性,而且不报错。

下面按坑展开。每个坑都是:现象 → 根因 → 最小复现 → 解法。

30 秒自查清单 ​

按顺序打勾,命中哪条基本就找到病根了:

  • [ ] SVG 根元素有 viewBox 吗?(width/height 有但 viewBox 没有 = 内容不缩放)
  • [ ] width/height 用的是 px 还是 pt?(pt 会被按 1.333 换算)
  • [ ] 导出 PNG 的像素尺寸 = 逻辑尺寸 × 屏幕 DPR 吗?
  • [ ] 文件里有 <text> 吗?用的字体在目标机器上装了吗?
  • [ ] 文件里有 <filter> 或 mix-blend-mode 吗?导出工具支持吗?
  • [ ] 颜色用了 8 位 hex(#RRGGBBAA)或 hsl() 吗?
  • [ ] 1px 描边的坐标落在整数还是半整数上?
  • [ ] 文件里嵌了 <image>(位图)吗?那它是不是本身就是低分辨率?

一、缺 viewBox:内容不跟着缩放(且在不同挂法下表现不同) ​

现象:SVG 放进网页,尺寸设成 2 倍,图形没变大,只是位置飘了;或者导出时尺寸莫名对不上。

根因:viewBox 定义了「用户坐标系 → 视口」的映射。没有它,用户单位 = CSS 像素,缩放视口不会缩放内容。

最小复现(两个文件只差一个属性):

svg
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100" viewBox="0 0 200 100">
  <rect x="20" y="20" width="40" height="40" fill="#ff0000"/>
</svg>

我把它内联进 HTML,用 CSS 强制 width:400px;height:200px,量红色方块的像素包围盒:

挂法有 viewBox没有 viewBox
内联 + CSS 定尺寸(40,40)–(119,119),放大了 2 倍(20,20)–(59,59),完全没缩放
<img src> 定尺寸(40,40)–(119,119)(40,40)–(119,119)
resvg fitTo:width=400(40,40)–(119,119)(40,40)–(119,119)
cairosvg output_width=400(40,40)–(119,119)(40,40)–(119,119)

同一个 SVG,内联和 img 两种挂法下有没有 viewBox 的缩放差异

看第二行——同一个浏览器、同一个文件,只是换成 <img> 标签,两种写法结果就反过来了。所以「我本地好好的,上线就错位」这类问题,根子常常在挂法,不在文件。

解法:所有 SVG 都带上 viewBox,width/height 只写 px。想控制显示尺寸就交给 CSS,别去改 SVG 属性里的数字。

二、px 和 pt 混用:尺寸凭空差 33% ​

现象:设计稿是 600×400,导出的 PNG 却成了 800×533;或者反过来小了三分之一。

根因:1pt = 1/72 英寸,1 CSS px = 1/96 英寸,1pt = 1.333px。而 Illustrator、InDesign 这类工具默认按 72dpi 工作,浏览器按 96dpi。

最小复现(一个 Illustrator 风格的画板):

svg
<svg xmlns="http://www.w3.org/2000/svg" width="600pt" height="400pt" viewBox="0 0 600 400">
  <rect width="600" height="400" fill="#00aa55"/>
</svg>

同一个文件,五个渲染结果:

工具输出尺寸
Chromium(浏览器,96dpi CSS)800 × 533
resvg 自然尺寸,dpi=96800 × 533
resvg 自然尺寸,dpi=72600 × 400
cairosvg 自然尺寸,dpi=96800 × 533
cairosvg 自然尺寸,dpi=72600 × 400

800 / 600 = 1.3333,正好 33.3%。 换工具就换尺寸,因为「pt 折算成多少 px」取决于工具假定的 dpi。

解法:导出时统一转成 px(Illustrator 导出面板勾「转换为 px」),或者干脆别在 SVG 里写绝对单位——只写 viewBox,尺寸交给 CSS。

三、1x 和 2x:你以为导出了,其实没导 ​

现象:设计稿里的图标放大就毛,PNG 在网页上发虚。

根因:1x PNG 是 1 个像素对 1 个 CSS 像素。屏幕是 2 倍屏时,浏览器要把它拉伸到 2 倍,必然糊。

我把一个 400×300 的 SVG(含 45° 斜线、圆形、中文文字)分别按 DPR 导出:

DPR输出像素PNG 体积灰阶层数
1x400 × 3008,792 B232
2x800 × 60018,698 B(2.13×)241
3x1,200 × 90031,230 B(3.55×)248

怎么量化「糊」?我用 Laplacian 方差(数值越大边缘越锐):

  • 1x PNG 被浏览器拉伸到 800×600 → 方差 276.0
  • 2x PNG 原生 800×600 → 方差 1210.1
  • 2x 比 1x 锐 4.38 倍,体积只多 2.13 倍。

同一个 400×300 的 SVG,导出 1x 和 2x 后的边缘锐度对比

解法:常规网页导 2x,Retina 展示为主就导 3x。体积换清晰度,值。至于「图标要多大」这种设计判断,也可以先在 VectorCraft 在线版 里把稿子搭出来再导,比事后补救省事。

四、字体没转曲:字还在,但已经不是那个字 ​

现象:SVG 里明明写着「矢量图形」,在同事电脑上变成了另一种字体,宽度也变了;更糟的是中文直接变方块。

根因:SVG 的 <text> 只是一句「请用某某字体渲染」。字体文件不会跟着 SVG 走——除非你自己把它转曲(转成 path)。

我拿同一段文字 矢量图形 Vector 123(font-size 40)测墨迹宽度:

条件墨迹宽相对基准
Chromium + Noto Sans CJK SC(本机装了)359 px基准
Chromium + SuperFancyDisplay(本机没装→回退)341 px−5.0%
Chromium + Noto Sans Mono CJK SC376 px+4.7%
resvg,只喂 DejaVuSans.ttf(无中文)323 px中文变 4 个豆腐块

同一个 SVG 在三种字体条件下的渲染结果,中文变成豆腐块

回退字体只差 5%,肉眼看不出来,但如果那段文字是居中或右对齐,排版已经跑版了。

转曲的成本我也量了:同一句话,保留活文字 259 B,转曲后 7,727 B,29.83 倍,多 7,468 字节。转曲版在只有 DejaVu 的机器上宽度 361 px、中文正常——活文字版则是豆腐块。

解法:看用途。对外交付、跨机器传、或者要长期存档 → 转曲;要在网页上排版、要能被搜索和选中 → 保留文字,但要确认目标机器的字体,或者干脆内嵌字体(@font-face + base64,注意体积)。

五、transform 与坐标:内容飞出画布,或者悄悄跑位 ​

三个具体的坑:

① 内容超出 viewBox 被裁掉。 200×200 的画布里画了个 (150,150) 起、100×100 的方块——导出后只剩 50×50,丢了一半。「导出后缺了一块」多是这个原因。

② rotate() 绕的是原点,不是图形中心。 一个 (0,0) 起的 100×20 横条加 transform="rotate(45)",实际 bbox 变成 x 0..69 / y 1..83——整个甩出去了。AI 生成的 SVG 里 transform 满天飞,这条尤其常见。

③ 真正让 AI 生成图「糊」的,是内嵌位图。 很多「SVG」其实是张 PNG 包在 <image> 里。我做了两个文件:一个内嵌 12×12 的 PNG,一个内嵌 48×48 的 PNG,都显示成 48×48:

内嵌位图原始尺寸文件大小Laplacian 方差
12 × 12423 B56.6
48 × 48807 B2980.4

清晰度差 52.7 倍。 SVG 标称矢量,但内嵌位图那一部分永远是这个清晰度——放大多少倍都是插值。

解法:导出前先搜一下文件里有没有 <image> 和 base64;有的话,要么换真正的矢量源,要么别放大。坐标问题则要在导出时确认「内容完全落在 viewBox 内」。

六、渐变、滤镜、混合模式:有的工具直接不认,而且不报错 ​

同一份 SVG,三个工具,结果能差出三种世界:

特性Chromiumresvgcairosvg
线性渐变(两种 gradientUnits)正常正常正常
feGaussianBlur正常正常完全没模糊
feDropShadow正常正常阴影直接消失
mix-blend-mode(CSS style 写法)正常正常没混合
mix-blend-mode(SVG 表现属性写法)没混合没混合没混合

几个具体数字:

  • feGaussianBlur(stdDeviation=4) 在 x=30 处的横向灰度剖面:Chromium 是 139 → 116 → 93 的平滑过渡,resvg 是 139 → 116 → 95,cairosvg 是从 255 直接跳到 0——一条硬边。
  • feDropShadow 造出的暗像素:Chromium 1,486 个、resvg 1,476 个、cairosvg 1,272 个(那只是圆本身,阴影一点没有)。
  • 混合模式:红底 #ff0000 叠灰 #808080 做 multiply,正确结果是 (128,0,0)。Chromium 用 <style> 或 style="..." 写法都出 (128,0,0);写成 SVG 表现属性 mix-blend-mode="multiply" 则出 (128,128,128),等于没混合。cairosvg 两种写法都没混合。

两个反直觉的结论:

  1. mix-blend-mode 必须写成 CSS style 属性或 <style> 块,不能写成 SVG 表现属性。 Chromium 和 resvg 都是这样。
  2. cairosvg 会静默丢弃滤镜和混合模式。 它不报错,只是把阴影和混合效果悄悄抹掉。如果你用 Python 批量把 SVG 转 PNG,图标上的投影会集体消失——而且没有任何日志提示。

解法:交付前拿浏览器打开看一眼(浏览器是目前支持度最好的参考实现),别只信脚本输出。必须用脚本工具的话,先拿一张带滤镜的测试图对一遍。

七、颜色格式:写法越现代,越容易在某处变黑 ​

我测了 11 种颜色写法,白底和黑底各采一次中心像素:

写法Chromiumresvgcairosvg
#ff0000 / #f00 / rgb(255,0,0) / red / currentColor✅✅✅
#ff000080(8 位 hex 带 alpha)✅ (255,127,127)✅ (255,127,127)❌ (255,0,0),alpha 被丢弃
rgba(255,0,0,0.5)✅ (255,127,127)✅ (255,128,128)✅ (255,127,127)
fill-opacity="0.5"✅ (255,128,128)✅ (255,127,127)✅ (255,127,127)
rgb(255 0 0 / 50%)(CSS4 空格语法)✅ (255,128,128)❌ (0,0,0) 变黑❌ 直接抛 ValueError 崩溃
hsl(0,100%,50%) / hsl(120,100%,50%)✅✅❌ (0,0,0) 变黑
hsla(120,100%,50%,0.5)✅ (127,255,127)✅ (128,255,128)❌ (0,0,0) 变黑
oklch(70% 0.15 145)✅ (91,182,97)❌ (0,0,0)❌ (0,0,0)

两个值得记住的点:

  • hsl() 在 cairosvg 里会渲染成纯黑,不是「不支持」而是「给你一个错的颜色」——比报错难查得多。
  • 同一个 50% 半透明,Chromium 和 resvg 的舍入方向还相反:rgba() 下 Chromium 出 127、resvg 出 128;fill-opacity 下 Chromium 出 128、resvg 出 127。差值只有 1/255,肉眼看不见,但做像素级 diff 或截图回归测试时,它会让你误判。

解法:颜色用 #RRGGBB 或 rgb()/rgba()。8 位 hex 带 alpha 很方便,但要先确认目标工具认识它。半透明统一走 fill-opacity 或 opacity,别混着写。

八、0.5px 描边:整数坐标在 1x 下反而更糊 ​

现象:1px 的线看着发灰、发虚。

根因:栅格化只能按整像素画。1px 的线如果正好压在像素边界上,就会被拆到两行,各覆盖一半。

我画了两条一模一样的 1px 黑线(stroke-width:1),只改 y 坐标,取 1x 渲染的灰度剖面:

线的位置实际落点灰度
y=30(整数)跨了 2 行128 / 127 → 糊成 2px
y=60.5(半像素)只占 1 行0 / 255 → 锐利 1px

1px 描边在整数坐标和半像素坐标下的像素级对比

但注意有个坑:这是 1x 的结论。在 2x 下我重测——y=30 的线落在设备行 59、60,两行都是纯 0,也是清晰的;y=60.5 落在设备行 120、121,同样清晰。所以「半像素对齐」是针对 1x 栅格化说的,2x 导出时整数坐标才是对齐的。

非整数坐标的矩形同理:x=10.5 的左边缘被拆成 col 10(灰 127)+ col 11(纯 0),两条边都糊。

解法:不要手工去凑 0.5px。导出时按目标 DPR 对齐坐标,或者——更省事——别栅格化。SVG 直接放网页里,浏览器会按实际 DPR 渲染,矢量在哪个倍率下都是锐的。

什么时候别自己扛 SVG ​

上面 8 个坑里,有几个排查成本不低:viewBox、pt/px、DPR 乘算、滤镜支持度、字体回退……要做批量转换或自动化出图,靠人肉调参数很快失控。

这时候更划算的做法是:把「矢量编辑」本身交给工具,把「排障」留给这篇清单。

我自己的分工是这样的:

  • 要设计、要改稿、要导出一整套尺寸 → 用 VectorCraft 的在线版,前面说的 viewBox、pt/px、DPR 这些坑,在源头上就不容易犯。
  • 已经有 SVG 了,只想转个格式 / 换个尺寸 → 先用本文的清单过一遍,确认问题不在文件本身,再选工具。
  • 要做像素级回归测试 → 别用 cairosvg,它会静默丢滤镜;resvg 或浏览器都更可靠。
  • 要批量出图 → 专门的矢量工具更省事,VectorCraft 在线版 浏览器打开就能用。

顺便说性能。拿 2000 条 path 的 1000×1000 SVG 测了三轮:

工具单次渲染耗时端到端(含进程启动)
Chromium(热浏览器,10 次取中位)68 ms1.50–1.54 s
resvg中位 107.9 ms0.15–0.17 s
cairosvg中位 150.7 ms0.27 s

浏览器热渲染其实最快(Skia 很快),但进程启动是硬伤——批量转几千个文件时,resvg 那种单文件独立进程的形态总成本更低。

最后 ​

SVG 不糊。糊的永远是栅格化那一步,而这一步有太多个互不相同的实现。

8 个坑全部是实测,没有一条是从文档里抄的。三个工具的脾气我也摸清了:

  • 浏览器(Chromium):支持最全,但 mix-blend-mode 必须用 CSS style 写法,表现属性写法它不认。
  • resvg:最接近浏览器的行为,滤镜、混合模式都正常,批量性能也好;但 CSS4 的 rgb(0 0 0 / 50%) 和 oklch() 在它那里会静默变黑。
  • cairosvg:最容易在无声中出错的那个——滤镜没了、混合没了、hsl() 变黑、8 位 hex 的 alpha 没了。而且全程不报错。

所以下次再遇到「SVG 导出后不对」,别急着换设计软件。先打开源文件看三样东西:根元素有没有 viewBox、有没有 <text>、有没有 <filter>。 八成的问题,这三样就能定位。


来源与说明

  • 测试环境:Linux(aarch64),无头 Chromium(--headless,SwiftShader 软渲染)、resvg(@resvg/resvg-js)、cairosvg 2.9.1;系统装了 109 个中文字体,含 Noto Sans CJK SC、Noto Sans Mono CJK SC、AR PL UMing CN 等
  • 文中所有数字均来自实跑命令的输出,图片是真实渲染结果截图,未做修饰
  • 未在本机验证的:Inkscape、librsvg(rsvg-convert)、ImageMagick 的实际行为——本机没装,跨工具对比只覆盖上面三套
  • 字体相关的「目标机器」是用 resvg 限制字体集(只喂 DejaVuSans.ttf)模拟的,不是真换了机器
  • 上文提到的几款工具我都在 13 款免费 Adobe 平替盘点 里写过整体定位;PhotoCraft 的实测稿 里也踩过同一个「字体回退」的坑——PSD 缺字体,排版直接跑版。字体这个坑,是矢量这边和位图那边共通的。
  • 本文只做技术排障,不构成对任何软件的推荐或担保

本站内容仅为个人学习与实验记录,不构成任何投资建议。