怎样快速制作「九宫格」拼图:基于 ImageMagick 和快捷指令的方案

2023-04-12

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.

引言

「九宫格」是如今大多社交平台展示缩略图的布局,也因此催生出了一种常见的发帖技巧:将一张大图等分为九宫格,利用这种布局产生有视觉冲击力的展示效果。

来源:Envato Elements

来源:Envato Elements

不过,这种九宫格的制作往往需要第三方软件辅助。如果用 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 命令

如果看过网上的其他教程,你可能更熟悉用 convertmorgify 等具体功能命令调用 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)]

之中,wh 分别是代表输入图片长宽的内置变量,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@ 来实现。

来源:Instagram @andrewinla

来源:Instagram @andrewinla

注意第 33 行在执行 -crop 操作之前用 +gravity 选项取消了之前的重心设定,这是因为根据文档,等分裁剪功能在有非默认重心设定下会发生故障。

输出文件名

文件名模版几乎是任何批量处理工具都需要的功能,ImageMagick 也不例外,支持丰富的输出文件名格式

这里,我们使用 printf 格式,令输出文件统一以 grid- 为前缀,其后接一个序号(%d),最后的后缀名也兼用于指定输出格式为 JPG。

快捷指令的步骤和原理

首先要做的仍然是准备一些基本信息:读取图片文件,计算其较长边的长度,并询问用户是否需要额外留白,从而确定整个正方形画布的尺寸;并将其除以 3,得到每个宫格的尺寸。

接着,需要将原图放在正方形画布的中央。由于快捷指令并不能像 ImageMagick 那样直接生成图层,我们需要为它提供一个白色背景作为素材。

这里,如果以导入外部图片的形式实现,就过于累赘,因此我们采用一种复杂快捷指令的常用技巧:将素材以 base64 编码的文本形式传入并还原。

上图中,base64 编码内容:

iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==

是一个长宽均为 1 像素的纯白色 PNG 格式图片。

其实,如果用 GIF 格式,长度还能再减半,但实测与其他格式图片叠放时会发生问题。至于 JPG 格式,不能压缩到 PNG 这种程度,大概需要 1KB 长度;虽然不差这点存储空间,但毕竟看着有点啰嗦,也不采用。

接着,将这个 1 像素见方的图片拉伸到前面计算所需的背景画布尺寸(因为是纯色,再怎么拉伸也不会模糊),再将原图叠放在其中央即可。

接下来就是裁剪图片。这里使用两个循环的嵌套:外层循环将图片切成纵向的三个竖条,对于每一个竖条,内层循环再将其横向切成三等份。其中,yx 分别是用来标记纵、横向裁切位置的指针,从 0 起记,每循环一次后,递增之前计算的宫格边长。最后输出即可。

很显然,这个任务对于快捷指令已经有点超纲了,从头编写起来的时间成本不低,执行起来效率也不高,会有很明显的等待时间。对于这类需求,本文的建议是,除非是作为一种智力娱乐或者技能练习,没有必要硬着头皮用快捷指令做。