drawtext 在 Linux 上的坑全在两处:字体路径,和表达式里的逗号。 跑马灯的位移是一条 mod 公式,走完 w+tw 个像素就绕回起点。
第六章已经用 drawtext 做过水印和跑马灯, 这一章补两件当时没细讲的事:\, 到底为什么必须转义, 以及 text_w 量的到底是字框还是墨迹。
一、Linux 上那条 drawtext 命令
同一条命令,Linux 上写法是这样(注意三处不一样):
ffmpeg -i in.mp4 \
-vf "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf: \
text='Hello':x=10:y=10:fontsize=28" \
out.mp4
哪一处 Linux 上怎么写 坑在哪
字体路径 /usr/share/fonts/... Linux 没有 C 盘那套路径 路径里的冒号 不用转义 写 C 盘路径必须写成 C\: 文本里的空格 text='…' 不加引号会被断开 表达式里的逗号 要写成 \, 滤镜串自己用逗号分层
字体路径是 Linux 上第一个坑:C:\Windows\Fonts 那套在这儿不存在。
常见字体在 /usr/share/fonts/truetype/ 下按家族分目录,
Debian 系一般有 dejavu/DejaVuSans.ttf,要中文字体得先装 fonts-noto-cjk。
找不到字体时 FFmpeg 报的是 Cannot find a valid font,不会告诉你该装哪个包。
二、表达式里的逗号为什么必须转义
因为滤镜串本身就是用逗号分层的:-vf "hflip,vflip,scale=320:240"。
而 drawtext 的 x 是个表达式,里面常有逗号——
mod(t*100,w+tw) 就有一个。不转义,这一刀就切在表达式中间。
滤镜串 切成几层 切出来是什么
x=w-mod(t100\,w+tw) 1 整条不动 x=w-mod(t100,w+tw) 2 后半截 w+tw) 成了另一个滤镜 hflip,vflip,scale=320:240 3 三个滤镜,本来就要三份
第二行就是没转义的下场。
FFmpeg 会拿 w+tw) 去找一个同名的滤镜,找不到就报错;
报错里指的位置往往跟真正的原因差着十万八千里,
这就是「明明只想改个坐标,却报 No such filter」的来历。
两种解法都行:写 \,,或者把整个表达式用单引号包起来——
x='w-mod(t*100,w+tw)',单引号里的逗号对滤镜解析器不算分隔符。
后者更好读,代价是多一层引号,shell 那边要写对。
三、跑马灯的位移公式
就一条:x = w - mod(t*speed, w+tw)。
w 是屏宽,tw 是文字宽,speed 是每秒走多少像素。
为什么是 w+tw 而不是 w?
因为文字完全走出左边(x 到 -tw)之后才开始下一轮,
整条轨迹的长度是「屏宽 + 文字宽」,除以速度就是走一圈的秒数。
时刻 公式 x 实际左边界 屏上看见几列
0.0 秒 320 -1 0 0.5 秒 270 272 42 1.0 秒 220 222 84 2.0 秒 120 122 173 3.0 秒 20 22 173 5.0 秒 -180 0 20
屏宽 320、文字宽 200、速度 100,走一圈是 520 像素,也就是 5.2 秒。
所以 t=0 和 t=5.2 秒时 x 都是 320——整条字在屏幕右边外面,一列都看不见。
t=5.0 秒那一行有点意外:公式给的 x 是 -180,可屏上还能看见 20 列。
因为文字宽 200,从 -180 起算还剩 20 像素露在屏内。
「看不见」的准确条件是 x <= -tw,不是 x < 0——
这一点直接决定了循环该怎么写才不会出现「闪一下」。
四、text_w 量的是字框,不是墨迹
表里第三列比第二列普遍大 2 像素,除了看得见的那几行。 不是公式错了,是字形左边自带一点留白(bearing)。
getbbox 返回的是「字框」,text_w 也一样——
它们量的是排版用的方框,不是墨迹真正覆盖的范围。
所以「让字刚好贴住右边缘」这种要求,靠 w-tw 是做不到严丝合缝的。
实用结论:留两三个像素余量反而稳。
水印那种要紧贴边缘的场合,宁可写 x=w-tw-4,也别指望 x=w-tw 刚好。
五、硬字幕、软字幕与 Linux 字体
drawtext 画出来的是硬字幕:像素烤进画面,播放器关不掉。
软字幕是另一条流(-c:s mov_text 之类),播放器可以选、可以关。
Linux 上做硬字幕,有两处依赖容易缺:
drawtext 要 fontconfig 找一个字体文件;
subtitles 滤镜要 libass,缺了会在配置滤镜时就报 No such filter。
这两个都不是「参数写错」,是编译时就没带,报错信息也完全不像。
所以第六章在沙箱里绕开了 drawtext,改用 Pillow 画字再 overlay——
那条路不依赖 fontconfig,只要有一个字体文件就能走通,
这也是「本机没有某个滤镜」时最实际的一条退路。
六、动手 Demo:把滤镜串切开看看,再逐时刻验一遍位移
下面这段干两件事:先用二十行代码模拟 FFmpeg 切滤镜串的规则,
证明 \, 到底是干什么用的;再把跑马灯的公式逐时刻算一遍。
def split_chain(s):
"""按「未转义的逗号」切滤镜串——ffmpeg 自己就是这么切的。"""
out, cur, i = [], [], 0
while i < len(s):
if s[i] == "\\" and i + 1 < len(s):
cur.append(s[i:i + 2])
i += 2
continue
if s[i] == ",":
out.append("".join(cur))
cur = []
i += 1
continue
cur.append(s[i])
i += 1
out.append("".join(cur))
return out
for s in ("x=w-mod(t*100\\,w+tw)",
"x=w-mod(t*100,w+tw)",
"hflip,vflip,scale=320:240"):
print("%d 层 %s" % (len(split_chain(s)), split_chain(s)))
W, TW, SPEED = 320, 200, 100
PERIOD = W + TW
print("走一圈 %d 像素 = %.2f 秒" % (PERIOD, PERIOD / float(SPEED)))
for t in (0.0, 0.5, 1.0, 2.0, 3.0, 5.0):
x = int(W - (t * SPEED) % PERIOD)
print("t=%.1f x=%4d 完全看不见吗:%s" %
(t, x, x >= W or x <= -TW))
实跑输出:
--------------------------------------------------------------------
10.3 使用 FFmpeg 实现文字水印及跑马灯
--------------------------------------------------------------------
**① Linux 上的 drawtext,三处跟 Windows 不一样**
ffmpeg -i in.mp4 \
-vf "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf: \
text='Hello':x=10:y=10:fontsize=28" \
out.mp4
哪一处 Linux 上怎么写 坑在哪
--------------------------------------------------------------------
字体路径 /usr/share/fonts/... Linux 没有 C 盘那套路径
路径里的冒号 不用转义 写 C 盘路径必须写成 C\:
文本里的空格 text='…' 不加引号会被断开
表达式里的逗号 要写成 \, 滤镜串自己用逗号分层
**最后一条是这三处里最难缠的:`drawtext` 的 `x` 是个表达式,**
**而表达式里常有逗号(`mod(t*100,w+tw)` 就有),逗号又是滤镜串的分隔符。**
**所以要么写 `\,`,要么把整个表达式用单引号包起来。**
**② 用一段切分代码把这件事证明出来**
滤镜串 切成几层 切出来是什么
------------------------------------------------------------------------
x=w-mod(t*100\,w+tw) 1 x=w-mod(t*100\,w+tw)
x=w-mod(t*100,w+tw) 2 x=w-mod(t*100 | w+tw)
hflip,vflip,scale=320:240 3 hflip | vflip | scale=320:240
**第二行就是没转义的下场:一条 `drawtext` 被硬生生劈成两条,**
**后半截 `w+tw)` 会被当成另一个滤镜名,报「找不到这个滤镜」。**
**而报错里指的位置往往跟真正的原因差着十万八千里。**
**③ 跑马灯的位移公式,逐时刻验一遍**
**文字量出来 200×30 像素,屏宽 320,速度 100 像素每秒。**
**公式是 `x = w - mod(t*speed, w+tw)`,走到 `w+tw` 个像素就绕回起点,**
**也就是 520 像素一个来回,折算 5.20 秒。**
时刻 公式 x 实际左边界 差值 屏上看见几列
------------------------------------------------------------
0.0 秒 320 -1 -321 0
0.5 秒 270 272 +2 42
1.0 秒 220 222 +2 84
2.0 秒 120 122 +2 173
3.0 秒 20 22 +2 173
5.0 秒 -180 0 +180 20
**两点值得盯住:**
**第一,t=0 时公式给的是 x=320,也就是整条字正好在屏幕右边外面,**
**一列都看不见;t=5.20 秒时又回到同一个位置——这就是「走完一圈」。**
**第二,除了看得见的那几行,实际左边界都比公式算的大 2 像素。**
**不是公式错了,是字形左边自带一点留白(bearing),**
**`getbbox` 量的是字框不是墨迹——`text_w` 也一样,它量的是字框。**
**所以想「刚好贴边」就别指望数值严丝合缝,留两像素余量反而稳。**
第一行切出 1 层,第二行切出 2 层——这一刀就是 \, 挡住的那一刀。
第三行切出 3 层是应该的,它本来就是三段的滤镜链。
最后那个布尔值把「看不见」的两个区间都写进去了:
右边还没进来是 x >= w,左边已经出去是 x <= -tw。
只判 x < 0 是不够的——左边刚出屏时右边可能还露着一大截。
30 秒实验
- 把速度改成 200:算一下走一圈要几秒,再想想「太快看不清」该怎么折中。
- 把
mod换成abs:看轨迹会变成什么样,理解mod在这里的唯一作用。 - 在 Linux 上跑一次
fc-list:找出本机到底装了哪些字体,别再瞎猜路径。
常见误区
- 表达式里的逗号不转义。报错是「找不到滤镜」,跟逗号一点关系看不出来。
- 以为循环周期是屏宽。是「屏宽 + 文字宽」,差着一个字宽。
- 以为
x < 0就看不见了。文字还有一半挂着,屏上照样露着。
自测三问(点开对答案)
1. `x=w-mod(t*100,w+tw)` 为什么要写 `\,`? —— 滤镜串用逗号分层,不转义就会把表达式切成两段,后半截被当成另一个滤镜。 2. 跑马灯走一圈是多久? —— (屏宽 + 文字宽)除以速度。320 + 200 再除 100,5.2 秒。 3. `text_w` 和墨迹宽度差在哪? —— `text_w` 是排版字框,墨迹左右各有 bearing 留白,所以框比墨迹宽一两个像素。- ☐ 10.3 完——下一节 10.4 使用 FFmpeg 实现音视频特效