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

x 与 y 是每帧重算的表达式,不是坐标。 跑马灯、时间戳、自适应位置,全靠这一点;其余参数都是固定值。

📝 本节和前面几节的关系

上一节解决了「文字从哪来」,这一节解决「它放在哪、怎么动」。 把 x、y 讲透,下一节的跑马灯就只剩一行公式。

一、四条位置表达式,按帧算一遍

画面 160×90、水印 54×29,把 t 从 0 算到 2 秒:

表达式 t=0 t=0.5 t=1 t=1.5 t=2


10 10 10 10 10 10 (w-tw)/2 53 53 53 53 53 w-tw-10 96 96 96 96 96 w-mod(t*100,w+tw) 160 110 60 10 -40

前三行每帧都算出同一个数,与 t 无关——它们是「常量表达式」。 第四行每帧都在变,这就是跑马灯的全部秘密。

mod 让 x 涨到顶之后回到起点,w+tw 保证它先完全走出画面再进来。 这两个设计合起来,才是一个「看不出接缝」的循环。

二、参数分五类,按类记

类别 参数 说明


内容 text、textfile 写什么字 位置 x、y 表达式,每帧重算 外观 fontsize、fontcolor 多大、什么颜色 装饰 box、borderw 底衬与描边 时间 enable、alpha 哪几帧出现、多透明

五类里只有「位置」是表达式,其余都是固定值。 这一点很关键:很多人以为 fontsize 也能写 h/30,其实它只接受数字。

想让它随分辨率变,只能在外层先算好、再拼进命令。 这也是为什么真正做项目时,命令几乎都是脚本拼出来的,而不是手敲的。

三、y 的常用写法与一个易错点

想放的位置 y 写什么


顶部留 10 10 垂直居中 (h-th)/2 底部留 10 h-th-10 垂直正中偏上 h/3

th 是文字的包围高度,不是字号。字号写 24,字形实际只有 23 高。 拿字号当高度算,贴底的文字会有一截跑出画面。

这一行差一两个像素不致命,但贴底的场景会露馅: 最后一行字被切掉一半,看上去像分辨率出了问题,其实是 th 用错了。

四、动手 Demo:把表达式按帧算出来

写一个小求值器,把 w、h、tw、th、t 代进去,看四条表达式在五个时刻的值。 再量一下字号 24 的真实字形高度。

from PIL import Image, ImageDraw, ImageFont

W, H = 160, 90
f = ImageFont.truetype(u"C:/Windows/Fonts/msyh.ttc", 24)
bb = ImageDraw.Draw(Image.new(u"RGBA", (8, 8))).textbbox(
    (0, 0), u"水印", font=f)
tw, th = bb[2] - bb[0] + 6, bb[3] - bb[1] + 6
print(u"画面 %dx%d  水印 %dx%d" % (W, H, tw, th))

def ev(expr, t):
    mod = lambda a, b: a - b * int(a / b)
    return eval(expr, {u"__builtins__": {}},
                dict(w=W, h=H, tw=tw, th=th, t=t, mod=mod))

for expr in (u"10", u"(w-tw)/2", u"w-tw-10",
             u"w-mod(t*100,w+tw)"):
    print(u"%-22s %s" % (expr,
          u"  ".join([u"%4d" % ev(expr, t)
                        for t in (0, 0.5, 1.0, 1.5, 2.0)])))

实跑输出:

--------------------------------------------------------------------
6.3.5 drawtext 的主要参数——x 与 y 是表达式,不是坐标
--------------------------------------------------------------------

**① 四条常用位置表达式,按帧算一遍**

**画面 160×90,水印 54×29,把 t 从 0 算到 2 秒:**

  表达式                t=0     t=0.5     t=1.0     t=1.5     t=2.0
  ---------------------------------------------------------------------
  10                    10      10        10        10        10
  (w-tw)/2              53      53        53        53        53
  w-tw-10               96      96        96        96        96
  w-mod(t*100,w+tw)     160     110       60        10        -40

**前三行算出来的都是同一个数,与 t 无关——它们是「常量表达式」。**
**第四行每帧都在变,这就是跑马灯的全部秘密:**
**`mod` 让 x 到顶之后回到起点,`w+tw` 保证它先完全走出画面再进来。**

**② 参数分五类,记的时候按类记**

  类别     参数                       说明
  ----------------------------------------------------------
  内容     text、textfile             写什么字
  位置     x、y                       表达式,每帧重算
  外观     fontsize、fontcolor        多大、什么颜色
  装饰     box、boxcolor、shadowx     底衬与阴影
  时间     enable、alpha              哪几帧出现、多透明

**五类里只有「位置」是表达式,其余都是固定值。**
**这一点很关键:很多人以为 `fontsize` 也能写 `h/30`,其实它只接受数字,**
**想让它随分辨率变,得在外层先算好再拼进命令。**

**③ y 常用表达式**

  想放的位置           y 写什么
  ---------------------------------
  顶部留 10            10
  垂直居中             (h-th)/2
  底部留 10            h-th-10
  画面外从左往右移     h/2

**记住 `th` 是文字的包围高度,不是字号:字号写 24,字形实际只有 23 高,**
**这里量到 29,是因为水印图上下各留了 3 像素空边。**
**拿字号当高度算,贴底的文字会有一截跑出画面。**

eval 加 {u"__builtins__": {}} 是为了禁掉内置函数。 只放 w、h、tw、th、t、mod 六个名字进去,表达式就只能用这几个。

mod 得自己实现:Python 的 % 对负数取模的结果与 FFmpeg 不同, 用 a - b * int(a / b) 才与 drawtext 里的 mod() 行为一致。

这段求值器不是玩具:写跑马灯之前先用它算几帧, 能提前看出文字什么时候出画、循环点在哪,省掉反复转码试错。

30 秒实验

  1. 改速度:把 t*100 改成 t*300,看一圈要多久。
  2. 改方向:写成 mod(t*100,w+tw)-tw,看它是从左往右还是从右往左。
  3. 加停顿:用 if(lt(t,5),w,...) 的写法算一遍,验证前 5 秒确实不动。

常见误区

  1. 把 x、y 当死坐标。它们是表达式,每帧重算。
  2. 以为 fontsize 也能写表达式。只有 x、y 支持。
  3. 拿字号当 th。字号 24 的字形只有 23 高,贴底会切一截。
自测三问(点开对答案) 1. `w-tw-10` 是什么意思? —— 画面宽度减去文字宽度再减 10,也就是把文字贴到右边、留 10 像素边距。 2. 跑马灯为什么用 mod? —— 让 x 到顶后回到起点,形成循环;不用 mod 的话 x 会一直变小。 3. 五类参数里哪一类是表达式? —— 只有位置(x、y)。其余都是固定值。

相关笔记