怎样快速制作「九宫格」拼图:基于 ImageMagick 和快捷指令的方案
A version of this article appears on Apr. 12, 2023 on SSPAI as a member-only post. Learn more or subscribe
The article is permitted to be self-archived in the version as originally submitted for publication on the author’s personal website under CC BY-NC 4.0 pursuant to § 5.2(b) of the SSPAI Fellowship Contributor Agreement.
引言
「九宫格」是如今大多社交平台展示缩略图的布局,也因此催生出了一种常见的发帖技巧:将一张大图等分为九宫格,利用这种布局产生有视觉冲击力的展示效果。
不过,这种九宫格的制作往往需要第三方软件辅助。如果用 Photoshop 等大型软件显得有些浪费,也超出了日常用户的能力;App Store 上倒是有不少专门应用,但就像所有瞄准大众用户的图片应用一样,也是乱收费和侵犯隐私的重灾区。
为此,本文将介绍如何通过简单免费的自动化工具,快速将图片切成九宫格布局。出于跨平台考虑,优先推荐基于命令行下图片处理的皇冠明珠——ImageMagick 的终端脚本版本;快捷指令版本可以作为 iOS 平台的后备方案。
使用方法
对于终端脚本版本,运行前首先需要准备好 ImageMagick,这可以通过各类包管理工具安装,例如:
# macOS
brew install imagemagick
# Debian/Ubuntu
sudo apt install imagemagick
然后,就可以按照以下语法使用:
./gridify.sh input_file [-background COLOR] [-margin PERCENTAGE]
其中,-background
用来指定背景色,可以是颜色名称(如 white
)、HEX 值(如 #FF0000
)或 RGB 值(如 rgb(255, 0, 0)
)等,不指定则默认用白色。-margin
用来指定四周的额外留白,接受的值是相对于原图长边的百分比,不指定则默认不额外留白。
例如:
# 切分 1.png,白色背景,不额外留白
./gridify.sh 1.png
# 切分 2.png,黑色背景,四周额外留白 10%
./gridify.sh 2.jpg -background black -margin 10
对于快捷指令版本,运行后会依次要求选择输入图片和指定四周的额外留白(同样是指相对于原图长边的百分比)。由于快捷指令的功能有限,这里没有设计选择背景色功能,固定为白色,以避免步骤过于复杂。此外,这个版本没有加入完整的输出步骤,你可以根据使用平台,自行添加输出到相册或 Finder 的步骤。
下面分别介绍这两个版本的制作步骤和原理。
终端脚本的步骤和原理
脚本开头是一些基础杂务,用于读取命令行传入和文件和选项。其中,第 8—23 行的 while
循环用来读取和处理可选参数,具体语法比较琐碎,也与本文无关,这里不多展开。
真正发挥作用的是第 29 行起调用 ImageMagick 的 magick
命令(易读起见,分写成多行):
magick "$file"\
-gravity center\
-background "$background"\
-extent "$extent"\
+gravity\
-crop 3x3@\
grid-%d.jpg
介绍具体的命令和选项作用之前,需要先铺垫一下 ImageMagick 的整体语法。
和另一个以用法晦涩出名的软件 FFmpeg 一样,ImageMagick 确实一直以难用、难理解著称。这既是功能强大、包罗万象的必然代价,也是因为它在三十多年的历史中经历了多次版本更迭,积攒了很多历史包袱和设计失误。
但就目前的大版本 v7 而言,ImageMagick 的用法大致都遵循这样的格式:
command { [settings] input_file [-operator] } ... output_file
换言之,命令按照「操作方式(设置)——操作对象(输入)——操作类型」的格式来描述图片操作。
其中,设置和操作的先后顺序是有影响的;一项设置只影响在它之后的一次输入、输出或操作,并且用完即止。以上面演示的那段脚本为例,伸展操作 -extent
受到在其之前的两个选项影响:-gravity
设置的伸展方向和 -background
设置的背景颜色;但这两个选项与出现在 -extent
之后的裁剪操作 -crop
、以及最后的输出,就没有关系了。
此外,由于 ImageMagick 可以处理多个图片,上述流程可以相互串接。例如,先输入第一张图片做一番处理,然后输入第二张图片做另一番处理,最后将两者以某种方式结合,产生所需效果。
当然,上述规律存在很多例外情况,例如为了兼容旧版,ImageMagick 很多时候也允许颠倒设置和操作的位置,但这更多是脚本维护者需要操心的事情,作为日常用户,我们只需记忆更新和更规范的形式即可。
下面来看看我们所用命令的具体组成结构。
magick
命令
如果看过网上的其他教程,你可能更熟悉用 convert
、morgify
等具体功能命令调用 ImageMagick 的写法。那实际上是多年前的老黄历;从 v7 开始,ImageMagick 就已经将 magick
作为主要命令,旧的独立命令只作为兼容用途保留,实际上是指向 magick
的符号链接。本文采用这种较新(实际上也六年多了)的写法。
-extent
操作及其选项
用于将图片向四周放大,接受 ImageMagick 专门的尺寸(geometry)参数,用于指定输出图片的尺寸。
尺寸参数的形式很多,但最基本的用法就是 widthxheight
,即长宽的像素值。由于我们希望得到一个正好能放进原图的方形,这里需要知道原图较长边的长度,以其作为新图片的边长。
对此,一种容易想到的方法是先读取原图信息,用 bc
等命令计算,然后再作为参数传给 ImageMagick 进行裁剪。但 ImageMagick 其实已经自带了在「原地」计算的能力——FX 表达式。如果善用,可以让脚本非常简洁。
FX 表达式的基本语法为 '%[fx:...]'
,支持直接调用图片自身的属性,以及进行常见算数运算。例如,在第 25 行用到的表达式
%[fx:max(w,h)*(1+$margin/100)]x%[fx:max(w,h)*(1+$margin/100)]
之中,w
和 h
分别是代表输入图片长宽的内置变量,max()
函数取两者中的较大值。
此外,考虑到常见的四周留白需求,这里额外计算了一个可由用户输入的留白 $margin
,单位是相对于长边的百分比。(之所以设计成百分比而不是绝对数值,是考虑到用户一般不知道图片精确尺寸,这时很难直接想出一个合适的留白尺寸;百分比就更为直观。)
例如,对于一张尺寸为 800×600 像素的输入图片,如果不要求额外边距,上述表达式的计算结果就是字符串 800x800
;对于一张尺寸为 1024×768 像素的输入图片,如果要求 25% 的额外边距,上述表达式的计算结果就是字符串 1280x1280
。
将这个字符串作为尺寸参数传给 -extent
操作,就可以将原图伸展到一个正方形。
不过,向什么方向伸展呢?这就是重心 -gravity
选项要做的事情,它决定了原图在伸展之后画布中的摆放位置,其参数是「东西南北中」描述的四面八方(NorthWest
, North
, NorthEast
, West
, Center
, …)。这里使用 center
,也就是让原图向四周伸展,保持居中。
此外,伸展画布会产生留白,其颜色由 -background
选项决定,这也是本文脚本允许用户自己输入的另一项设置。-background
的参数格式和 CSS 差不多,可以是颜色名称(如 red
)、HEX 值(如 #FF0000
)或 RGB 值(如 rgb(255, 0, 0)
)等,这里默认用白色(white
)。
如果不设置 -background
选项,就是不用背景颜色,取决于输出格式是否支持透明通道,所得结果可能是白色(对于 JPG 等)或者透明(对于 PNG 和 GIF 等)。
-crop 操作
这个操作继续处理上一步 -extent
操作传来的方形图片,目的是将其等分为九宫格。
-crop
操作的参数也是尺寸表达式,比较常见的写法是像素坐标(例如 10x20+5-5
指从 (10,20)
像素点向右向上各裁 5 像素)或者百分比(例如 25x25%-10+20
指从纵横 1/4 处向左向下各裁 10 像素)。
但在这里,我们用 @
记号表示裁剪为等分区域;3x3@
就表示横、纵各裁成三等份。如果你有其他裁剪布局需求,可以自行调整这些参数。例如,下图这种布局就可以用参数 3x1@
来实现。
注意第 33 行在执行 -crop
操作之前用 +gravity
选项取消了之前的重心设定,这是因为根据文档,等分裁剪功能在有非默认重心设定下会发生故障。
输出文件名
文件名模版几乎是任何批量处理工具都需要的功能,ImageMagick 也不例外,支持丰富的输出文件名格式。
这里,我们使用 printf 格式,令输出文件统一以 grid-
为前缀,其后接一个序号(%d
),最后的后缀名也兼用于指定输出格式为 JPG。
快捷指令的步骤和原理
首先要做的仍然是准备一些基本信息:读取图片文件,计算其较长边的长度,并询问用户是否需要额外留白,从而确定整个正方形画布的尺寸;并将其除以 3,得到每个宫格的尺寸。
接着,需要将原图放在正方形画布的中央。由于快捷指令并不能像 ImageMagick 那样直接生成图层,我们需要为它提供一个白色背景作为素材。
这里,如果以导入外部图片的形式实现,就过于累赘,因此我们采用一种复杂快捷指令的常用技巧:将素材以 base64 编码的文本形式传入并还原。
上图中,base64 编码内容:
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==
是一个长宽均为 1 像素的纯白色 PNG 格式图片。
其实,如果用 GIF 格式,长度还能再减半,但实测与其他格式图片叠放时会发生问题。至于 JPG 格式,不能压缩到 PNG 这种程度,大概需要 1KB 长度;虽然不差这点存储空间,但毕竟看着有点啰嗦,也不采用。
接着,将这个 1 像素见方的图片拉伸到前面计算所需的背景画布尺寸(因为是纯色,再怎么拉伸也不会模糊),再将原图叠放在其中央即可。
接下来就是裁剪图片。这里使用两个循环的嵌套:外层循环将图片切成纵向的三个竖条,对于每一个竖条,内层循环再将其横向切成三等份。其中,y
和 x
分别是用来标记纵、横向裁切位置的指针,从 0
起记,每循环一次后,递增之前计算的宫格边长。最后输出即可。
很显然,这个任务对于快捷指令已经有点超纲了,从头编写起来的时间成本不低,执行起来效率也不高,会有很明显的等待时间。对于这类需求,本文的建议是,除非是作为一种智力娱乐或者技能练习,没有必要硬着头皮用快捷指令做。