用 AppleScript 来辅助 Pages 排版

Pages 的混合语言排版功能一直为人诟病 (实际上 Pages 的多语言支持也一直很弱,甚至比不上 TextEdit),比如像组合字体 (combined font) 这样在排版软件中很常见的功能也迟迟没有提供,所以经常有人说用 Pages 排版中英文混合的文章是一种折磨。

昨天看到 Apple 版上的一篇文章,便想起来试试 Page ’09 新增的 AppleScript 功能,其实用到的功能在之前的 Pages 版本中应该也有,不过我手头没有更早的版本,所以无法试验了。可以点这里看完整的脚本,其实核心代码非常短:

from appscript import *
pages = app(id="com.apple.iWork.Pages")

for doc in pages.documents():
    words = doc.words()
    i = 0

    for word in words:
        i += 1
        if isch(word[0][0]):
            doc.words[i].font_name.set("SimSun")

这个脚本没有直接用 AppleScript,而是通过第三方提供的 py-appscript 接口来调用 (它的语法比 ScriptingBridge 要友好很多),作用是,扫描所有 Pages 打开的文档,把所有的汉字的字体设为 SimSun,因为仅仅作为一个示例,所以没有加任何参数。

使用方法是,先安装 py-appscript,然后打开 Pages,随便输入一段中文和英文混杂的文字,比如刚才的“在50ml的PBS缓冲中加入15mg的HCl使得最终pH为5”这句,一开始加入时因为默认字体是 Helvetica,所以英文字母和数字都用了 Helvetica,汉字则使用了 Helvetica 的默认 Fallback, 也就是 STXihei,如下图所示。

before

然后运行:

python format-pages.py

这时候切换回 Pages 就能看到效果,如下图所示。

after

其实这个脚本还可以做很多其他的变化,比如加入界面来选择字体、只是转换当前选中的部分等等等等,至少了解一点 AppleScript 的调用方式就可以尝试,这里不赘述了。

最近在写的两个东西

这两天写了两个简单的小程序,主要是满足朋友和个人需求的,还在修改中。

第一个叫 apn 的程序是给 Mac OS X 中 Address Book 中的联系人自动生成姓名发音 (Phonetic Name) 的脚本,所以它的名字就是 Assign Phonetic Name 的缩写。

这是一个 Python 写的程序,直接用到了 pyzh 项目提供的汉字拼音转换代码,通过 Scripting Bridge 提供的 API 来访问 Address Book,这样虽然相比直接调用 Objective-C 的 API 有点慢,但好处在于能充分利用 Python 语言的灵活性。

使用起来很简单,从 github 上把代码抓下来 (你可以 git clone git://github.com/jjgod/apn.git 或者下载一个打包的版本),然后执行

$ python AssignPhoneticName.py

就会自动给你 Address Book 中没有分配过 Phonetic Name 的那些联系人分配一遍。注意因为汉字有多音字,这个程序做不到很智能,你最好在分配之后打开 Address Book 校对一遍。

第二个是给 Cocoa 程序员用的一个 NSView 的子类,叫 PYView,其作用很简单,就是在汉字上方同步的显示拼音,不过目前拼音还得自己提供。

调用的 API 很简单,比如这样:

#include "PYView.h"

NSRect viewRect = NSMakeRect(50, 250, 700, 80);
view = [[PYView alloc] initWithFrame: viewRect
                            fontName: @”FZKai-Z03″
                               color: [NSColor whiteColor]];

NSArray *pinyin1 = [NSArray arrayWithObjects: @"nǐ", @"hǎo", nil];
NSArray *pinyin2 = [NSArray arrayWithObjects: @"zhōng", @"huá",
                    @"rén", @"mín", @"gòng", @"hé", @"guó", nil];


[view appendMarkerItem: [PYMarkerItem itemWithHanzi: @"你好"
                                             pinyin: pinyin1
                                               type: 1]];
[view appendMarkerItem: [PYMarkerItem itemWithHanzi: @"中华人民共和国"
                                             pinyin: pinyin2
                                               type: 1]];

就能得到如图所示的输出结果:

PinyinView

可以从 github 上获取代码:

 git clone git://github.com/jjgod/pinyinview.git

然后参考提供的 PYViewTest 代码来使用。注意接口还在修改中。

用 Python 维护 iTunes Library

Mac OS X 10.5 中,由于 Scripting Bridge 的引入,用 Ruby 或 Python 程序完成原来 AppleScript 才能完成的任务变得非常简单,而因为这两门语言自身的强大,无形中,可以完成的工作也多了不少。比如我们原来可能要用 ID3Mod 这样的软件进行 iTunes Music Library 的歌曲乱码转换,现在写一段不到十行的 Python 脚本就能完成 (当然,界面没有那么方便)。

一个小例子

这里先用 Python 简单的展示一点可以完成的操作:

# 导入必要的模块
from Foundation import *
from ScriptingBridge import *

# 找到 iTunes 这个应用程序
iTunes = SBApplication.applicationWithBundleIdentifier_("com.apple.iTunes")

# 打印出当前正在播放的音乐名称
print iTunes.currentTrack().name()

这段代码在 Leopard 下,既可以保存为 .py 文件,用系统自带的 python 解释器 (/usr/bin/python) 执行,也可以直接在命令行下调用 Python 解释器,查看它的输出,比如我这里是:

$ python
Python 2.5.1 (r251:54863, Oct  5 2007, 21:08:09) 
[GCC 4.0.1 (Apple Inc. build 5465)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from Foundation import *
>>> from ScriptingBridge import *
>>> iTunes = SBApplication.applicationWithBundleIdentifier_("com.apple.iTunes")
>>> print iTunes.currentTrack().name()
Le Festin
>>> 

注意,执行之前最好先在 iTunes 里开始播放一首曲目。

了解更多

见识了 Scripting Bridge 的威力之后,你很自然的想知道这段代码为何可以工作,应该按什么方式来调用它,除了查询当前播放的歌曲名称以外,还有什么其他的接口可以使用。这时候,我们就需要对我们希望用脚本操纵的程序所提供的 Scripting 接口有个清晰的了解。所以可以用 sdef 这个命令获得一个程序的脚本接口定义:

$ sdef /Applications/iTunes.app
xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary><suite name="Standard Suite" code="****" description="Common terms for most applications">
...

出现了一长串的怪异信息,太混乱了,看不懂,怎么办呢?这时应该用 sdp 命令过滤一下,生成一个比较可读的格式,Objective-C 头文件:

$ sdef /Applications/iTunes.app | sdp -fh --basename iTunes
$ ls
iTunes.h
$ more iTunes.h
/*
 * iTunes.h
 */

#import <AppKit/AppKit.h>
#import <ScriptingBridge/ScriptingBridge.h>
...

这里就是一个对 iTunes 提供的脚本编程接口的详尽描述了。从这里我们其实很容易看出,原来 iTunes = SBApplication.applicationWithBundleIdentifier_("com.apple.iTunes") 获得的,是一个 iTunesApplication 对象,以这个对象为出发点,我们可以做到任何 API 允许的事情。下面就是一个例子。

#!/usr/bin/python

from Foundation import *
from ScriptingBridge import *

iTunes = SBApplication.applicationWithBundleIdentifier_("com.apple.iTunes")

for track in iTunes.sources()[0].playlists()[0].tracks():
    print track.name(), track.artist()

简单的观察一下,iTunes 的脚本编程 API 组织是这样的:在 iTunesApplication 下,可以找到多个来源 (iTunesSource),来源有许多种,比如 music library,比如 CD,比如 iPod 等等,而每个来源里,又按照播放列表 (iTunesPlaylist) 来组织,而每个播放列表中,显而易见地有多个曲目 (iTunesTrack),在上面这个例子里,我们要找的第一个来源 (sources()[0]) 的第一个播放列表 (playlists()[0]),正是你的 iTunes Music Library。

值得注意的一点是,在 iTunes.h 中用 SBElementArray * 表示的数据类型,一眼可以看出它是数组,在 Python 中处理起来也很简单,直接当成 list 类型遍历即可。

修改数据

那么,查询我们知道了,该怎么把信息写回去,也就是说,修改原来 Music Library 的内容呢?

先来看看 iTunes.h 中的出现的一段:

@interface iTunesTrack : iTunesItem

- (SBElementArray *) artworks;

@property (copy) NSString *album;
@property (copy) NSString *albumArtist;
@property NSInteger albumRating;
@property (readonly) iTunesERtK albumRatingKind;

这里描述的是一个曲目 (它继承了 iTunesItem 类型,所以也继承了它的 name 等属性),@property 这种写法,是 Objective-C 2.0 中新出现的,也很好理解,(copy) 的意思是你的赋值会被复制一份保存,(readonly) 当然是只读的,而 NSInteger 这种简单的变量也是可以写的。

那么,查询我们知道了,比如这里有个 album 属性,我们就去调用 track 的 album() 方法,但究竟如何赋值呢?还是按照 Cocoa 的 Key-value coding 的老规矩,改成 setAlbum(<参数>),比如:

>>> iTunes.sources()[0].playlists()[0].tracks()[0].name()
u'Le Festin - Performed by Camille'
>>> iTunes.sources()[0].playlists()[0].tracks()[0].setName_(u'Le Festin')

这里把一首原来叫做 Le Festin – Performed by Camille 的歌曲名称修正为 Le Festin,回到 iTunes 里一看,果然改了。

系统自带与 macports

在 Mac OS X 10.4 中,由于系统自带的 Python 是老旧的 2.3 版本,所以对 Python 爱好者来说,都往往不得不自己编译一份或者使用 fink/macports 中提供的 Python 2.4/2.5 版本。而在 Mac OS X 10.5 中,终于把自带的 Python 升级到了最新的 2.5.1 版本,对爱好者来说无疑是省事了,可惜这个自带的版本一个令中文用户郁闷的 bug 是:在提示符模式下无法用输入法输入任何中文字符 (相信其他 CJK 字符也是如此),比如我用输入法打一段:你好 hello 世界 world,等我一确认,Python 解释器只收到了 hello world,中文字符被自动滤掉了!然而,通过 macports 安装的 Python 此功能却是正常的。

这使得我们这里如果要修改中文的歌曲、作家、专辑名等等都变得很不方便,怎么办呢?

系统自带的 Python 的特别之处,其实在于它自带了许多 Apple 添加的模块,比如我们在代码最开始导入的 FoundationScriptingBridge 模块,这些模块有些不是开源的,有些则还没来得及把代码公开,所以我们机器上只安装有编译好的 Python 模块,然而幸好这些模块是 2.5 版本的,只要我们用 macports 也安装一个 2.5 版本,把系统自带模块的路径给加进去,就能找到并使用它们了。让我们还是用开头那个例子做个简单的示范:

#!/opt/local/bin/python2.5

import sys

sys.path.append('/System/Library/Frameworks/Python.framework/Versions/2.5/Extras/lib/python')
sys.path.append('/System/Library/Frameworks/Python.framework/Versions/2.5/Extras/lib/python/PyObjC')

from Foundation import *
from ScriptingBridge import *

iTunes = SBApplication.applicationWithBundleIdentifier_("com.apple.iTunes")

print iTunes.currentTrack().name()

有了上面这些介绍,相信要把这几个工具组合在一起成为一套 iTunes Music Library 的管理利器也不是难事,各位不妨发挥想象力,我就不多说了。