How I met this API

这两天在看 hammerspoon,先照葫芦画瓢实现了 window 控制功能,然后想着网上找找看有没有什么有意思的脚本,就翻到了这个 Hammerspoon replacement for Caffeine,作者不满于 Caffeine 官方 app 的图标在 Retina 屏幕上的效果,于是用 hammerspoon 实现了一个图标切换功能。如果再配合上 hs.application API,实现点击开关 app 功能,就完美了。

但是这个脚本里的两段看似乱码的东西很有意思:

ampOnIcon = [[ASCII:
.....1a..........AC..........E
..............................
......4.......................
1..........aA..........CE.....
e.2......4.3...........h......
..............................
..............................
.......................h......
e.2......6.3..........t..q....
5..........c..........s.......
......6..................q....
......................s..t....
.....5c.......................
]]

ampOffIcon = [[ASCII:
.....1a.....x....AC.y.......zE
..............................
......4.......................
1..........aA..........CE.....
e.2......4.3...........h......
..............................
..............................
.......................h......
e.2......6.3..........t..q....
5..........c..........s.......
......6..................q....
......................s..t....
...x.5c....y.......z..........
]]

完整的脚本中,这两个变量被 setIcon 调用,最终作为图标显示到菜单栏上。代码如下 ⬇️

function setCaffeineDisplay(state)
    if state then
        caffeine:setIcon(ampOnIcon)
    else
        caffeine:setIcon(ampOffIcon)
    end
end

桥豆麻袋!虽然我没有写过很多 macOS app,但是不要骗我。Apple 为了保证 macOS 应用图标的显示效果,在 Xcode 中设置了一套完整的尺寸参照,需要开发者上传所有尺寸的 icon,用过的都知道。

Xcode - App Icon 栏.png

而这段乱码又是什么?

WTF is this shit.jpeg

带着疑问的小脑袋开始搜索,然而搜索结果属实不尽人意:

ASCII icon 搜索.png

Lua ASCII icon 搜索.png 完全没有有用的信息,只能换一个思路了,去看文档!

很快就翻到了 hs.menubar:setIconsetIcon 这个函数接受两个参数,我们只用到了第一个 imageData:

imageData - This can one of the following:
    - An hs.image object
    - A string containing a path to an image file
    - A string beginning with ASCII: which signifies that the rest of the string is interpreted as a special form of ASCII diagram, which will be rendered to an image and used as the icon. See the notes below for information about the special format of ASCII diagram.
    - nil, indicating that the current image is to be removed

第三种解释:一个 ASCII 开头的字符串,表示一个 ASCII diagram。虽然后面注明了 “See the notes below for information about the special format of ASCII diagram“,但实际上文档里并没有找到这一块儿。

于是继续搜,ASCII diagram、a special form of ASCII diagram、ASCII draw icon......各种关键词都搜一遍,还是没找到什么有用的信息。就在要放弃的时候,突然想到:为什么要始终从 Hammerspoon 侧找?Hammerspoon 也只是通过 Lua 调用 macOS 系统命令,从 macOS 侧试一下呢?

于是继续搜,NSStatusItem ASCII icon,果然!第二条就是 hammerspoon 的源代码:

NSStatusItem ASCII icon 搜索.png

于是点进去看,结果竟然真的找到了 “See the notes below for information about the special format of ASCII diagram“ 里提到的 notes:

///  * To use the ASCII diagram image support, see http://cocoamine.net/blog/2015/03/20/replacing-photoshop-with-nsstring/ and be sure to preface your ASCII diagram with the special string `ASCII:

循着链接继续往下搜,跳到 Replacing Photoshop With NSString,原来是作者为了减少使用 code 画图标带来的体积开销,自定义了一套规则,写了一个绘图库 ASCIImage,开源在了 Github 上。而 Hammerspoon 则是借助这个库,实现了使用 ASCII diagram 绘制图标的功能。

规则

ASCIImage 库设立的规则很简单:

  1. 图片以一个字符串数组来定义,每一个字符串代表一行
  2. 字符串数组的每一个元素在去除空格后,应该长度相等
  3. 1-9,A-Z,a-z 中除了 o(小写) 以外的字符构成形状,其余字符可以作为填充,但是在计算形状时会被忽略。

为了绘制多种图形,ASCIImage 还设置了一些形状规则:

  1. 连续的字符会被绘制成多边形
  2. 单个字符会被绘制成点
  3. 当一个字符被使用正好两次时,这两个点会组成一条线
  4. 当一个字符被使用多次时,这些点会被绘制成一个椭圆

完整的规则见:ASCIImage Bezier Paths

根据上面的规则,再来看上面的乱码就很简单了:

ampOnIcon = [[ASCII:
.....1a..........AC..........E
..............................
......4.......................
1..........aA..........CE.....
e.2......4.3...........h......
..............................
..............................
.......................h......
e.2......6.3..........t..q....
5..........c..........s.......
......6..................q....
......................s..t....
.....5c.......................
]]

按照步骤:

1-6、A、C、E、a、c、e、h、q、s、t 均被使用了两次,所以这是一个完全由线组成的图,呈现出的效果是:

amphetamine icon.png

当我们想画一个六边形的时候,也很简(jian)单(lou):

hexagon = [[ASCII:
. . . . 8 1 . 1 2 . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. 8 . . . . . . . . . 2 .
. 7 . . . . . . . . . 3 .
. . . . . . . . . . . . .
. 7 . . . . . . . . . 3 .
. 6 . . . . . . . . . 4 .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . 6 5 . 5 4 . . . .
]]

总结

不得不佩服,这个创意真的是太棒了。虽然在前端看来好像没什么用,反而加大开发者负担,但是对于通过 app store 分发的原生应用来说,体积是非常敏感的一个指标,而通常占用一个 app 体积最大份额的往往就是图片,有了这个 ASCIImage 库之后,许多简单的图片就可以被转换成字符串,大大减少占用体积。而对于开发者来说,则又多了一项课外摸鱼的技能 😏。

参考

  1. Replacing photoshop with nsstring
  2. Hammerspoon – macOS automation with Lua
  3. https://codegolf.stackexchange.com/questions/128104/draw-hyperneutrinos-benzene-hegaxon-icon-in-ascii
  4. https://github.com/heptal/dotfiles/blob/master/roles/hammerspoon/files/asciicons.lua
  5. https://github.com/qqnluaq/asciicon/blob/master/ascii-icon.js
  6. https://github.com/olizilla/asciicon/blob/master/index.js