🎬 第六章 FFmpeg命令行实现图片水印及文字跑马灯
📄 一句话概括

「中文显示成方块」背后有三个完全不同的毛病:缺字形、编码错、路径错。 现象不一样,查法也不一样,混着查会绕很久。

📝 本节和前面几节的关系

上一节已经摸到第一种(缺字形),这一节把三种凑齐,给出定位顺序。 学会这套,遇到乱码就不用再靠猜。

一、成因一:字体里没有这个字形

字体 非透明像素 看到的样子


arial 104 两个空心方框 msyh 389 两个正常汉字

同样的字、同样的字号,msyh 是 arial 的 3.7 倍。 这一类乱码的特征是「方块大小一致、排列整齐」——它画的是缺字符号,不是字。

注意它不报错、不警告,画面上就是一片整齐的方框。 新手很容易以为是「编码坏了」,其实是字体压根没这个字形。

二、成因二:编码不对,字被解成了别的字

环节 结果 长度


原文 水印 2 UTF-8 字节 e6b0b4e58db0 6 按 GBK 解 姘村嵃 3 按 latin1 解 æ´å° 6

关键看长度:2 个汉字只有 6 个字节,按 GBK 解出来变成 3 个字, 按 latin1 解出来变成 6 个字符——长度一变,就是解错了。

这一类乱码的特征是「全是怪字,但每个都长得不一样」。 与豆腐块的「整齐划一」正好相反,光看现象就能先分到哪一类。

三、成因三:路径写错,悄悄回落到默认字体

写法 会发生什么


C:\Windows\Fonts\msyh.ttc 冒号被当分隔符,路径只剩 C C\:/Windows/Fonts/msyh.ttc 正确 'C\:/Windows/Fonts/msyh.ttc' 命令行里带空格才需要

这种错最阴:它不报错,只是换成一个没有汉字的默认字体, 于是你看到的现象与「字体没字形」一模一样。

查半天查不到编码上去,原因就是它把你引到了错误的第一类。 所以定位顺序很重要:先看方块长什么样,再决定往哪儿查。

四、三步定位法

现象 先查什么


方块大小一样、排列整齐 字体里有没有这个字形 全是怪字、长度不对 编码,字节数与字符数比值 现象同第一类但字体明明对 fontfile 路径转义对不对

三行按顺序查,一分钟内能定案。 难的是很多人直接从第二行开始,把字体问题当成了编码问题。

一条通用经验:凡是从命令行传进去的中文, 先假设编码是对的,先怀疑「字体」与「路径」——这两类在实际项目里出现得更多。

五、动手 Demo:三种成因各造一次

先用 arial 与 msyh 各渲染两个汉字,量像素差。 再把同一个字符串按三种编码解读,看长度怎么变。

import numpy as np
from PIL import Image, ImageDraw, ImageFont

def px(text, fn):
    f = ImageFont.truetype(u"C:/Windows/Fonts/" + fn, 24)
    im = Image.new(u"RGBA", (80, 40), (0, 0, 0, 0))
    ImageDraw.Draw(im).text((0, 0), text, font=f,
                              fill=(255, 255, 255, 255))
    return int((np.array(im)[:, :, 3] > 0).sum())

pa, pb = px(u"中文", u"arial.ttf"), px(u"中文", u"msyh.ttc")
print(u"arial %d  msyh %d  倍数 %.1f" % (pa, pb, pb / float(pa)))

s = u"水印"
b = s.encode(u"utf-8")
print(u"原文 %s 长度 %d" % (s, len(s)))
print(u"UTF-8 %s 长度 %d" % (b.hex(), len(b)))
print(u"GBK   %s 长度 %d" % (b.decode(u"gbk", u"replace"),
                               len(b.decode(u"gbk", u"replace"))))
print(u"latin1 %s 长度 %d" % (b.decode(u"latin1"),
                                len(b.decode(u"latin1"))))

实跑输出:

--------------------------------------------------------------------
6.6.2 解决中文乱码问题——三种成因,三种查法
--------------------------------------------------------------------

**「中文显示成方块」这句话背后其实有三个完全不同的毛病。**

**① 成因一:字体里没有这个字形**

  字体      非透明像素     看到的样子
  -----------------------------------------
  arial     104            两个空心方框
  msyh      389            两个正常汉字

**同样的字、同样的字号,msyh 是 arial 的 3.7 倍。**
**这一类乱码的特征是「方块大小一致、排列整齐」——**
**因为它画的是缺字符号,不是字。**

**② 成因二:编码不对,字被解成了别的字**

  环节             结果             长度
  ------------------------------------------
  原文             水印             2
  UTF-8 字节       e6b0b4e58db0     6
  按 GBK 解        姘村嵃           3
  按 latin1 解     水印           6

**关键看长度:2 个汉字只有 6 个字节,按 GBK 解出来变成 3 个字,**
**按 latin1 解出来变成 6 个字符——长度一变,就是解错了。**
**这一类乱码的特征是「全是怪字,但每个都长得不一样」。**

**③ 成因三:fontfile 路径写错,回落到默认字体**

  写法                             会发生什么
  -----------------------------------------------------------------
  C:\Windows\Fonts\msyh.ttc        冒号被当参数分隔,路径只剩 C
  C\:/Windows/Fonts/msyh.ttc       正确
  'C\:/Windows/Fonts/msyh.ttc'     命令行里带空格时必须这样套

**这种错最阴:它不报错,只是悄悄换成一个没有汉字的默认字体,**
**于是你看到的现象与「字体没字形」一模一样,查半天查不到编码上去。**

**④ 三步定位法**

  现象                       先查什么
  -------------------------------------------------------
  方块大小一样、排列整齐     字体里有没有这个字形
  全是怪字、长度不对         编码,字节数与字符数比值
  用的是默认字体             fontfile 路径转义对不对

**三行按顺序查,一分钟内能定案——**
**难的是很多人直接从第二行开始查,把字体问题当成了编码问题。**

固定 80×40 的画布,是为了不让画布尺寸干扰像素计数。 换了画布,同一个字的像素数不变,但如果画布太小,字会被裁掉。

decode(..., u"replace") 里的 replace 让解不出的字节变成问号, 而不是抛异常——排查乱码时,先看到东西比看到异常有用。

长度对比是最快的判据:字节数 6、GBK 解出 3 个字、latin1 解出 6 个字符, 三个数一对,就知道中间被谁动过手脚。

30 秒实验

  1. 造第三种:把 fontfile 写成 C:\Windows\...(不转义),看它怎么回落。
  2. 换个编码:把文件存成 GBK 再读,看字节数怎么变。
  3. 写个检查:给一段文字,写函数判断它「更像 UTF-8 还是 GBK」。

常见误区

  1. 看到方块就去查编码。方块多数是缺字形,不是编码错。
  2. 以为 fontfile 写错会报错。它悄悄换成默认字体,什么都不说。
  3. 只比字符数不比字节数。汉字 3 字节、字母 1 字节,不比字节看不出问题。
自测三问(点开对答案) 1. 豆腐块与编码错怎么区分? —— 豆腐块大小一致、排列整齐;编码错是各种怪字,长度也不对。 2. fontfile 写错为什么不报错? —— 它会回落到编译期默认字体,行为上是「成功」的,只是字不对。 3. 最快的编码判据是什么? —— 比字节数与字符数的比值:UTF-8 明显大于 1,GBK 小于 2。

相关笔记