主题
SVG 导出为什么糊了?8 个坑我逐个跑了一遍
SVG 号称无损,可抱怨从来没断过:导出 PNG 糊了、尺寸对不上、字变了、中文变成方块。
这些抱怨有个共同特点——从来没人说是 SVG 的错,都说是「某个软件有问题」。所以我干脆自己当一次那个软件。
我用三套完全独立的 SVG 栅格化器(无头 Chromium、resvg、cairosvg),把 8 个常见坑一个个做成最小复现,每条都量到了具体像素和字节。
结论先给:
- 最常见的三个原因:目标机器没装字体(回退,字宽变了你没察觉)、导出少了 viewBox(内容不跟着缩放)、只导了 1x(屏幕是 2 倍屏)。这三个占我看到的反馈里的大多数。
- 最快定位的办法:把 SVG 拖进浏览器窗口,看渲染出来的比例和设计稿是不是一致;再把根元素的
width/height删掉、只留viewBox看一次——比例不对就是尺寸单位或 viewBox 的锅,比例对但边缘发虚就是 DPR 或字体的锅。 - 一个反直觉的真相: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) |

看第二行——同一个浏览器、同一个文件,只是换成 <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=96 | 800 × 533 |
| resvg 自然尺寸,dpi=72 | 600 × 400 |
| cairosvg 自然尺寸,dpi=96 | 800 × 533 |
| cairosvg 自然尺寸,dpi=72 | 600 × 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 体积 | 灰阶层数 |
|---|---|---|---|
| 1x | 400 × 300 | 8,792 B | 232 |
| 2x | 800 × 600 | 18,698 B(2.13×) | 241 |
| 3x | 1,200 × 900 | 31,230 B(3.55×) | 248 |
怎么量化「糊」?我用 Laplacian 方差(数值越大边缘越锐):
- 1x PNG 被浏览器拉伸到 800×600 → 方差 276.0
- 2x PNG 原生 800×600 → 方差 1210.1
- 2x 比 1x 锐 4.38 倍,体积只多 2.13 倍。

解法:常规网页导 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 SC | 376 px | +4.7% |
resvg,只喂 DejaVuSans.ttf(无中文) | 323 px | 中文变 4 个豆腐块 |

回退字体只差 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 × 12 | 423 B | 56.6 |
| 48 × 48 | 807 B | 2980.4 |
清晰度差 52.7 倍。 SVG 标称矢量,但内嵌位图那一部分永远是这个清晰度——放大多少倍都是插值。
解法:导出前先搜一下文件里有没有 <image> 和 base64;有的话,要么换真正的矢量源,要么别放大。坐标问题则要在导出时确认「内容完全落在 viewBox 内」。
六、渐变、滤镜、混合模式:有的工具直接不认,而且不报错
同一份 SVG,三个工具,结果能差出三种世界:
| 特性 | Chromium | resvg | cairosvg |
|---|---|---|---|
| 线性渐变(两种 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 两种写法都没混合。
两个反直觉的结论:
mix-blend-mode必须写成 CSSstyle属性或<style>块,不能写成 SVG 表现属性。 Chromium 和 resvg 都是这样。- cairosvg 会静默丢弃滤镜和混合模式。 它不报错,只是把阴影和混合效果悄悄抹掉。如果你用 Python 批量把 SVG 转 PNG,图标上的投影会集体消失——而且没有任何日志提示。
解法:交付前拿浏览器打开看一眼(浏览器是目前支持度最好的参考实现),别只信脚本输出。必须用脚本工具的话,先拿一张带滤镜的测试图对一遍。
七、颜色格式:写法越现代,越容易在某处变黑
我测了 11 种颜色写法,白底和黑底各采一次中心像素:
| 写法 | Chromium | resvg | cairosvg |
|---|---|---|---|
#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 |
![]()
但注意有个坑:这是 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 ms | 1.50–1.54 s |
| resvg | 中位 107.9 ms | 0.15–0.17 s |
| cairosvg | 中位 150.7 ms | 0.27 s |
浏览器热渲染其实最快(Skia 很快),但进程启动是硬伤——批量转几千个文件时,resvg 那种单文件独立进程的形态总成本更低。
最后
SVG 不糊。糊的永远是栅格化那一步,而这一步有太多个互不相同的实现。
8 个坑全部是实测,没有一条是从文档里抄的。三个工具的脾气我也摸清了:
- 浏览器(Chromium):支持最全,但
mix-blend-mode必须用 CSSstyle写法,表现属性写法它不认。 - 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 缺字体,排版直接跑版。字体这个坑,是矢量这边和位图那边共通的。
- 本文只做技术排障,不构成对任何软件的推荐或担保