TextEdit/UCD R5 与 Cocoa Text System

更新: TextEdit/UCD 的代码现在可以在 [http://gitorious.org/projects/textedit-ucd/](http://gitorious.org/projects/textedit-ucd/) 找到。

TextEdit/UCD 的第 5 个版本[发布](http://jjgod.org/program/TextEdit-UCD-r5.dmg)了,TextEdit/UCD 开发的目标是尽可能解决所有 TextEdit 固有的中文处理问题,但并不改变 TextEdit 原有的轻量小巧。

在开发这个版本中,我发现了一个 Cocoa Text System 的固有问题:还没有下载的朋友可以先打开自己机器上的 TextEdit,输入一个汉字,一个英文字母,如“中a”,这时汉字会以默认的中文字体显示,英文字母会以 Preferences 中设定的 Plain Text 字体 (如 Monaco) 显示,此时按下 cmd-‘+’ 增大一号字体,然后再输入一个英文字母,就会发现这个新的英文字母居然改用中文字体显示了?

为什么呢?在 TextEdit 中花了一段时间重载各个类,加上 gdb 分析,结果发现,其实在 Make Text Bigger/Make Text Smaller 时,调用 `[NSFontManager modifyFont:]`,这个 `modifyFont:` 方法会将 `-changeFont:` 这个 action 发送到 responder chain 中,而此时的 NSTextView 收到这个 action 以后,居然会将 `[NSFontManager sharedInstance]` 的 `selectedFont` 修改为这个 TextView 的 TextStorage 中第一个字符使用的字体。于是就导致了已选字体的变化。这其实不是我们期望的结果。

然而,由于事实上 NSTextView 的 changeFont 实现可能很复杂,它要逐个分析所有的 run,逐个将这个 run 的字体送到 NSFontManager 中 convertFont 获得新的字体。既然我们无法直接修改这个 changeFont 的代码,就只能用重载的方法,写一个新的 NSTextView 的子类,重载这个函数,在调用基类的 changeFont 之前,先给整个文档的开头加上一个用纯文本字体的新字符,然后在调用完基类 changeFont 之后再把它删除。

这是一个看起来有些 dirty 的方法,不过很有用,也算是“曲线救国”,用欺骗的方式使之有正确的效果。

Chmox 的一些修正

Chmox 是 Mac OS X 上常用的 chm 阅读软件,它最后一个版本是 2006-10-28 发布的 0.4β,这也是它唯一的一个 Universal Binary 的版本,此后作者就再也没有过更新,因为我和许多 Mac 用户一样,为缺少好用的 chm 所困扰,所以便尝试来对此做一些修正。

主要的更新是参照 <a href="http://chmsee achat de viagra france.gro.clinux.org/”>ChmSee 的做法修正了一些编码判断的问题,包括目录的编码和显示内容的编码。欢迎对此有兴趣的朋友将 Chmox 无法打开或者打开错误的 chm 发给我 (当然,文件大小尽可能小),我会尽力修正的。

下面是最近的更新记录 (版本号延续作者原来的定义进行递增):

Chmox 0.4.2

更新:

- 修正某些 chm 文件无法正确载入页面的问题 (faithprice 报告)
- 没有目录时不打开 Drawer
- 提供简单的页内搜索功能

Chmox 0.4.1*

注: 版本号为我按照 http://chmox.sourceforge.net/ 原有版本更新
的,所以这不是官方版本号 (似乎作者本人已经停止维护了)。

更新:

- 更新到 chmlib 0.39
- 修正一些编译警告
- 使用 chm 文件自带的 LCID 信息来判断 CHM 目录的编码,修正在
  解析目录时的一些常见的编码判断错误

下载链接为: Chmox-0.4.2.zip

更新: 刚刚设立了一个 git 代码仓库,请用 git clone git://gitorious.org/chmox/mainline.git 获取最新代码,访问这里了解项目最新开发进度。

mplayer 的字幕播放

[mplayer](http://www.mplayerhq.hu) 是常用的播放软件,其文档对于涉及字幕播放的一些参数不甚详尽,或者留意的人不多,这里对常用选项略作一介绍。

以一个包含了字幕的 mkv 文件为例:

$ mkvinfo ~/Vexille_2077_Nippon_Sakoku.mkv

|+ Segment tracks
| + A track
| + Track number: 1
| + Track UID: 141444798
| + Track type: video
| + Enabled: 1
| + Default flag: 1
| + Forced flag: 0
| + Lacing flag: 0
| + MinCache: 1
| + Timecode scale: 1.000000
| + Max BlockAddition ID: 0
| + Codec ID: V_MPEG4/ISO/AVC
| + Codec decode all: 1
| + CodecPrivate, length 131
| + Default duration: 41.708ms (23.976 fps for a video track)
| + Language: jpn
| + Name:
| + Video track
| + Pixel width: 864
| + Pixel height: 480
| + Interlaced: 0
| + Display width: 864
| + Display height: 480
| + A track
| + Track number: 2
| + Track UID: 3113156519
| + Track type: audio
| + Enabled: 1
| + Default flag: 1
| + Forced flag: 0
| + Lacing flag: 1
| + MinCache: 0
| + Timecode scale: 1.000000
| + Max BlockAddition ID: 0
| + Codec ID: A_AC3
| + Codec decode all: 1
| + Default duration: 32.000ms (31.250 fps for a video track)
| + Language: jpn
| + Name:
| + Audio track
| + Sampling frequency: 48000.000000
| + Channels: 6
| + A track
| + Track number: 3
| + Track UID: 3841949375
| + Track type: subtitles
| + Enabled: 1
| + Default flag: 1
| + Forced flag: 0
| + Lacing flag: 0
| + MinCache: 0
| + Timecode scale: 1.000000
| + Max BlockAddition ID: 0
| + Codec ID: S_TEXT/ASS
| + Codec decode all: 1
| + CodecPrivate, length 903
| + Language: chi
| + Name: GB
| + A track
| + Track number: 4
| + Track UID: 1264721059
| + Track type: subtitles
| + Enabled: 1
| + Default flag: 0
| + Forced flag: 0
| + Lacing flag: 0
| + MinCache: 0
| + Timecode scale: 1.000000
| + Max BlockAddition ID: 0
| + Codec ID: S_TEXT/ASS
| + Codec decode all: 1
| + CodecPrivate, length 891
| + Language: chi
| + Name: Big5
|+ EbmlVoid (size: 1024)
|+ Attachments
| + Attached
| + File name: Font.TTF
| + Mime type: application/x-truetype-font
| + File data, size: 2484512
| + File UID: 577931698
| + File description: CN-GB

这是一个比较典型的情况: 有两个 subtitles track,分别对应简体和繁体的字幕 (从 Name 分别叫 GB 和 Big5 可以看出),字幕的格式都是 [ASS](http://en.wikipedia.org/wiki/SubStation_Alpha),并自带了一个附件 (Attachment),这个附件是一个名为 Font.TTF 的字体文件。*

* 注: 顺带说一句,附带有版权的字体文件是非法的。

mplayer 默认的选项并不能让我们很好地播放这个 mkv 文件,所以需要一些配置。在这里,我首先建议编辑你的 `~/.mplayer/config` 文件,而不是通过 [MPlayer OSX](http://mplayerosx.sourceforge.net) 这样的图形化前端,因为 mplayer 同时支持用配置文件和用命令行选项来选择参数,前者调整起来要方便一些。

对于中文用户,最常见的一个选项是

subcp=gbk

这表示我们显示地指定将所有字幕文件以 GBK 编码处理,对于大部分中文字幕都是适用的。*

* 注: 近来,在 [shooter](http://shooter.cn) (射手网) 上提供的字幕有些也会以 UTF-16 格式出现,如果你用 GBK 编码无法成功看得到字幕,可以尝试换用 `subcp=utf-16` 看看。

* 注 2: mplayer 可以用 [enca](http://packages.debian.org/enca) 库来进行自动编码探测,可惜的是这个库并没有对 CJK 相关语系编码的支持,如果有可能,将 [Universal Charset Detector](http://www.mozilla.org/projects/intl/detectorsrc.html) 用在此处或许更好。

对于 ASS 格式的字幕,mplayer 有两种渲染方式,一种是忽略所有的格式信息,将它作为纯文本显示,另一种则是启用格式,一般我们希望使用后者,所以可以加上选项:

ass=1

对于自带字体的情况,默认 mplayer 并不解析它们,只有在:

embeddedfonts=1

时才会解析,所以也建议加上这个选项。

在播放 [mkv](http://matroska.org) 文件时,mplayer 默认并不会选择字幕文件的 track,所以我们需要手动用 `-sid 0` 或者 `-slang chi` 参数来选择。(你也可以将这类参数写入配置文件,不过在 mkv 不自带字幕时这样的参数不会有效。)

经过上述的调整,mplayer 已经可以很好地播放 ASS 格式字幕和 mkv 媒体文件了,不过还有一种更常见的字幕格式需要调节,那就是 srt。

srt 字幕常以外置文件的形式提供,放在媒体文件同一个目录下,以同样的前缀为名,后缀一般为 .chs.srt, .gb.srt 这样的形式,所以为了让 mplayer 的外部字幕文件查找能够找到这些文件,我们首先就应该加上下面的选项:

sub-fuzziness=1

这个选项的可选值对应:

0 = 不模糊匹配
1 = 匹配任何包含原媒体文件名的字幕文件,但包含原媒体文件名和 slang 所指定语言的优先级更高
2 = 匹配任何字幕文件

所以当同一个目录下有多个语言的字幕时,你可以用

sub-fuzziness=1
slang=chs

这样的配置来优先选择后缀为 .chs.srt 的那个。

对于 srt 字幕文件,一个有用的功能是字体大小的缩放 (因为 srt 本身没有指定字体大小),经过试验,对于一般 DVDRip (resolution 在 600×300 左右),用以下参数较恰当:

subfont-autoscale=2
subfont-text-scale=3.5

对于 720p / 1080p 的 HDRe/Remux,则应该将 `subfont-text-scale` 进一步缩小到 2.5 左右较好。

欢迎补充更多 mplayer 的使用经验。

Cocoa 的 NSString 解码错误处理

在使用 [Safari](http://www.apple.com/safari) 的时候,我们会注意到一个很常见的乱码问题,如下图:

Safari Decode Error

这是在打开 [http://att.newsmth.net/att.php?p.719.214628.536.png]() 这样的图片链接时,Safari 错误的判断了这个图片文件的文件名造成的。而为什么会有这样的错误判断呢?

其实 Safari 使用的是 [Cocoa](http://developer.apple.com/cocoa) 框架 [URL Loading](http://developer.apple.com/documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html) 架构中的 NSURLResponse 类的 [suggestedFilename](http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSURLResponse_Class/Reference/Reference.html#//apple_ref/occ/instm/NSURLResponse/suggestedFilename) 方法实现的。

而这个方法,其实就是解析 HTTP 首部中的 [Content-Disposition](http://www.ietf.org/rfc/rfc2183.txt) 域里的 filename 部分完成的,比如下面这个首部:

$ curl -I http://att.newsmth.net/att.php?p.719.214628.536.png
HTTP/1.1 200 OK
….
Content-Disposition: inline;filename=ͼƬ_6.png
….

显然这是乱码,可奇怪的是,这和我们在上面的图中看到的乱码又不一样,这是为什么呢?

假如将它作为 GBK 来解码就清楚了:

$ curl -I http://att.newsmth.net/att.php?p.719.214628.536.png | iconv -f gbk -t utf-8
HTTP/1.1 200 OK
….
Content-Disposition: inline;filename=图片_6.png
….

哦,原来是 GBK 编码的“图片_6.png”,可是这个文件名怎么会变成开头图片中那种形式的乱码呢?其实写一段 Cocoa 程序就可以发现:

#import

int main()
{
const char *bytes = “图片_6.png”;
NSString *str = [[NSString alloc] initWithCString: bytes
encoding: NSASCIIStringEncoding];
NSLog(@”str: %@”, str);
[str release];

return 0;
}

(用 GBK 编码保存) 这个程序的执行结果就是输出开头那段乱码,原来 NSURLResponse 把 Content-Disposition 中的 filename 当成 ASCII 处理了,怪不得会乱码。

可是也不能怪 NSURLResponse,毕竟服务器没有提供任何编码的信息,而 RFC 2183 中也明确说明,不应该在 filename 中使用任何 ASCII 以外的字符,用了就是后果自负了。

那假如我们要写一个自己的客户端 (或者尝试修正 Safari 的错误行为),该怎么修正已经被按照 ASCII 错误解码的 NSString 呢?

因为 NSString 本身是按照 UTF-16 编码的,所以如果逐个字符地观察这个错误解码后的 NSString:

int max = [str length];

int i;
for (i = 0; i < max; i++) { unichar ch = [str characterAtIndex: i]; printf("%x ", ch); } 我们可以得到: cd bc c6 ac 5f 36 2e 70 6e 67 这样一串输出,`5f 36 2e 70 6e 67` 就是 `_6.png`,比较好认,前面的 `cd bc c6 ac` 是什么呢?一查,原来是“图”和“片”这两个字的 GBK 编码。 这就好理解了:NSString 一开始把一段 GBK 编码的字节流*逐个字节地*按照 8bit-ASCII 处理了,原本 10 个字节对应的是 7 个字符,结果被错误地解码为了 10 个字符,所以我们要把它转换回去,首先是要还原回原来的那段字节流: int max = [str length]; char *nbytes = malloc(max + 1); int i; for (i = 0; i < max; i++) { unichar ch = [str characterAtIndex: i]; nbytes[i] = (char) ch; } nbytes[i] = '\0'; 然后再将这段字节流按照正确的编码 (GB18030) 处理: NSStringEncoding enc = CFStringConvertEncodingToNSStringEncoding( kCFStringEncodingGB_18030_2000); NSLog(@"nstr: %@", [NSString stringWithCString: nbytes encoding: enc]); 结果果然得到了正确的输出: 2008-02-17 06:09:46.408 test[14095:10b] nstr: 图片_6.png 同样的逻辑可以用在很多类似的乱码情形中。完整的代码可以在这里下载: [test-str-encoding.m](http://jjgod.org/code/test-str-encoding.m)

6 Frameworks Apple should include in their OS

To make developers’ lives easier, Apple *really* should make the following 6 frameworks bundled with their OS, it will make application distribution much better.

* [Growl](http://growl.info)
* [Sparkle](http://sparkle.andymatuschak.org)
* [Perian](http://perian.org)
* Universal Charset Detector
* A decent HUD framework (maybe based on [HMBlkAppKit](http://shiira.jp/hmblkappkit/en.html))
* A decent Tab control (maybe based on [PSMTabBarControl](http://www.positivespinmedia.com/dev/PSMTabBarControl.html))