Introducing & Building OpenType Collections (OTCs)

I would like to use this opportunity to introduce two new things.

First, OpenType Collections. TrueType Collections have been around for many years, and are commonplace for OS-bundled fonts. What I am speaking of are ‘sfnt’ Collections that include a ‘CFF ‘ (PostScript charstrings) table rather than a ‘glyf‘ (TrueType charstrings) one. The advantage of an ‘sfnt’ Collection is that fonts that differ in minor ways can be combined into a single resource, which can provide substantial size savings.

Second, brand new AFDKO tools, in the form of two Python scripts, for building, breaking apart, and displaying a synopsis of an OTC’s tables. These scripts were developed by our incredibly talented font tools engineer, Read Roberts, so all thanks should go to him for preparing them.

In terms of the new AFDKO tools, those who cannot wait for the next AFDKO release can immediately download and start experimenting with the two Python scripts, otf2otc.py and otc2otf.py.

The workhorse tool is otf2otc.py, which combines multiple OTFs into a single OTC. By default, tables are shared by the font instances only if they are binary-identical, but this can be overridden by using the “-t” command-line option, which takes a four-character ‘sfnt’ table tag and font index as its argument. The “-o” command-line option is used to specify the name of the OTC, in terms of its filename. What follows on the command line are the OTFs that are to be combined. The order of the specified OTFs is significant if the “-t” command-line option is used, because one must specify the index of the font, which begins at zero (0), among the OTFs that are specified on the command line.

Below are two real-world examples of using this tool to create new OTCs.

First, I will combine Kozuka Gothic Pr6N M (小塚ゴシックPr6N M; Adobe-Japan1-6; 23,058 CIDs) with Kozuka Gothic Pro M (小塚ゴシックPro M; Adobe-Japan1-4, 15,444 CIDs), being sure to override the behavior to specify that the ‘CFF ‘ table of the former font resource is to be used for both font instances (the latter is a pure subset of the former):

% python otf2otc.py -t 'CFF '=0 -o KozGo-Medium.ttc KozGoPr6N-Medium.otf KozGoPro-Medium.otf
Input fonts: ['KozGoPr6N-Medium.otf', 'KozGoPro-Medium.otf']
Done

Second, I will combine the four font resources that make up the Kamono Kana Logo Line family into a single OTC:

% python otf2otc.py -o LogoLineStd.ttc LogoLineStd-Light.otf LogoLineStd-Medium.otf LogoLineStd-Bold.otf LogoLineStd-Ultra.otf
Input fonts: ['LogoLineStd-Light.otf', 'LogoLineStd-Medium.otf', 'LogoLineStd-Bold.otf', 'LogoLineStd-Ultra.otf']
Done

The otc2otf.py tool serves two purposes. One, which is evident by its name, is to break apart a Collection into individual OTFs. This is performed by specifying the “-w” command-line option. The other purpose, which is performed when no command-line option is specified, displays the table checksums, offsets, and lengths for each of the font instances in an OTC. Below is output from the latter function when applied to the two OTCs that I built:

% python otc2otf.py KozGo-Medium.ttc
Input font: KozGo-Medium.ttc
Version: 65536. numFonts: 2.
font 0 offset: 20/0x00000014. KozGoPr6N-Medium
BASE checksum: 0x1B8E18D8, offset: 0x0000020C, length: 0x000000E4
CFF  checksum: 0xFD079A52, offset: 0x000002F0, length: 0x00492C87
GPOS checksum: 0xD5C4A73C, offset: 0x00492F77, length: 0x000077E2
GSUB checksum: 0x38D14556, offset: 0x004A04FD, length: 0x00032FEE
OS/2 checksum: 0x7976A4DD, offset: 0x004F3115, length: 0x00000060
VORG checksum: 0xF3929E18, offset: 0x004F31D5, length: 0x000005B4
cmap checksum: 0xADA99534, offset: 0x004F3A51, length: 0x0003FFF5
head checksum: 0xFE4D2B32, offset: 0x00560F8C, length: 0x00000036
hhea checksum: 0x0A27539C, offset: 0x00560FF8, length: 0x00000024
hmtx checksum: 0x250ADA69, offset: 0x00561040, length: 0x000153BC
maxp checksum: 0x5A125000, offset: 0x00583B16, length: 0x00000006
name checksum: 0x3B0710AF, offset: 0x00583B22, length: 0x0000081A
post checksum: 0xFFB80032, offset: 0x00584B43, length: 0x00000020
vhea checksum: 0x08AC6487, offset: 0x00584B63, length: 0x00000024
vmtx checksum: 0xC22B5D00, offset: 0x00584BAB, length: 0x000158C4
font 1 offset: 272/0x00000110. KozGoPro-Medium
BASE checksum: 0x1B8E18D8, offset: 0x0000020C, length: 0x000000E4
CFF  checksum: 0xFD079A52, offset: 0x000002F0, length: 0x00492C87
GPOS checksum: 0xFD8C8D34, offset: 0x0049A759, length: 0x00005DA4
GSUB checksum: 0xF2DCEFFD, offset: 0x004D34EB, length: 0x0001FC2A
OS/2 checksum: 0x7688A341, offset: 0x004F3175, length: 0x00000060
VORG checksum: 0x7DBD360A, offset: 0x004F3789, length: 0x000002C8
cmap checksum: 0xD184E967, offset: 0x00533A46, length: 0x0002D546
head checksum: 0xFFD060E4, offset: 0x00560FC2, length: 0x00000036
hhea checksum: 0x0BAC3334, offset: 0x0056101C, length: 0x00000024
hmtx checksum: 0x5657396B, offset: 0x005763FC, length: 0x0000D71A
maxp checksum: 0x3C545000, offset: 0x00583B1C, length: 0x00000006
name checksum: 0x5FDE60BF, offset: 0x0058433C, length: 0x00000807
post checksum: 0xFFB80032, offset: 0x00584B43, length: 0x00000020
vhea checksum: 0x0A46467A, offset: 0x00584B87, length: 0x00000024
vmtx checksum: 0xCFEBE768, offset: 0x0059A46F, length: 0x0000E0BA
Done

% python otc2otf.py LogoLineStd.ttc
Input font: LogoLineStd.ttc
Version: 65536. numFonts: 4.
font 0 offset: 28/0x0000001C. LogoLineStd-Light
BASE checksum: 0x1BA818DF, offset: 0x0000040C, length: 0x000000E8
CFF  checksum: 0x1715F501, offset: 0x000005D8, length: 0x00007D10
GPOS checksum: 0x1646F381, offset: 0x00020602, length: 0x000009FE
GSUB checksum: 0x9B0A9A80, offset: 0x00022C14, length: 0x00000926
OS/2 checksum: 0x7529D1A9, offset: 0x0002353A, length: 0x00000060
VORG checksum: 0x0D0B3CF9, offset: 0x000236BA, length: 0x00000050
cmap checksum: 0xA1E5EC81, offset: 0x000237FA, length: 0x00000D88
head checksum: 0xFC3D0459, offset: 0x00024582, length: 0x00000036
hhea checksum: 0x0B4203A3, offset: 0x00024624, length: 0x00000024
hmtx checksum: 0xC1D77DDB, offset: 0x00024690, length: 0x0000039C
maxp checksum: 0x01A15000, offset: 0x00025500, length: 0x00000006
name checksum: 0xE1EDF545, offset: 0x00025506, length: 0x00000797
post checksum: 0xFFB80032, offset: 0x00027350, length: 0x00000020
vhea checksum: 0x09C7136C, offset: 0x00027370, length: 0x00000024
vmtx checksum: 0xC3E2E642, offset: 0x00027394, length: 0x00000632
font 1 offset: 280/0x00000118. LogoLineStd-Medium
BASE checksum: 0x1B8E18D8, offset: 0x000004F4, length: 0x000000E4
CFF  checksum: 0xC61D636F, offset: 0x000082E8, length: 0x000080F7
GPOS checksum: 0xF654DD9C, offset: 0x00021000, length: 0x000009AE
GSUB checksum: 0x9B0A9A80, offset: 0x00022C14, length: 0x00000926
OS/2 checksum: 0x75F1D5A9, offset: 0x0002359A, length: 0x00000060
VORG checksum: 0x0D0B3D27, offset: 0x0002370A, length: 0x00000050
cmap checksum: 0xA1E5EC81, offset: 0x000237FA, length: 0x00000D88
head checksum: 0xFC3D0459, offset: 0x00024582, length: 0x00000036
hhea checksum: 0x0B4203A3, offset: 0x00024624, length: 0x00000024
hmtx checksum: 0xBCAA7581, offset: 0x00024A2C, length: 0x0000039C
maxp checksum: 0x01A15000, offset: 0x00025500, length: 0x00000006
name checksum: 0xDB4FBB0C, offset: 0x00025C9D, length: 0x0000079E
post checksum: 0xFFB80032, offset: 0x00027350, length: 0x00000020
vhea checksum: 0x09C7136C, offset: 0x00027370, length: 0x00000024
vmtx checksum: 0xC2BDD81E, offset: 0x000279C6, length: 0x00000632
font 2 offset: 532/0x00000214. LogoLineStd-Bold
BASE checksum: 0x1B8E18D8, offset: 0x000004F4, length: 0x000000E4
CFF  checksum: 0x31BA2CEC, offset: 0x000103DF, length: 0x000080E4
GPOS checksum: 0xC9E3B98B, offset: 0x000219AE, length: 0x00000922
GSUB checksum: 0x9B0A9A80, offset: 0x00022C14, length: 0x00000926
OS/2 checksum: 0x76B9D7C9, offset: 0x000235FA, length: 0x00000060
VORG checksum: 0x0D0B3D17, offset: 0x0002375A, length: 0x00000050
cmap checksum: 0xA1E5EC81, offset: 0x000237FA, length: 0x00000D88
head checksum: 0xFC43045B, offset: 0x000245B8, length: 0x00000036
hhea checksum: 0x0B4703A2, offset: 0x00024648, length: 0x00000024
hmtx checksum: 0xAEC76614, offset: 0x00024DC8, length: 0x0000039C
maxp checksum: 0x01A15000, offset: 0x00025500, length: 0x00000006
name checksum: 0x959E0945, offset: 0x0002643B, length: 0x00000787
post checksum: 0xFFB80032, offset: 0x00027350, length: 0x00000020
vhea checksum: 0x09C7136C, offset: 0x00027370, length: 0x00000024
vmtx checksum: 0xC14AC4BE, offset: 0x00027FF8, length: 0x00000632
font 3 offset: 784/0x00000310. LogoLineStd-Ultra
BASE checksum: 0x1B8E18D8, offset: 0x000004F4, length: 0x000000E4
CFF  checksum: 0xA164D643, offset: 0x000184C3, length: 0x0000813F
GPOS checksum: 0xD5BEBF8B, offset: 0x000222D0, length: 0x00000944
GSUB checksum: 0x9B0A9A80, offset: 0x00022C14, length: 0x00000926
OS/2 checksum: 0x7781D8C9, offset: 0x0002365A, length: 0x00000060
VORG checksum: 0x0D0A3D12, offset: 0x000237AA, length: 0x00000050
cmap checksum: 0xA1E5EC81, offset: 0x000237FA, length: 0x00000D88
head checksum: 0xFC3D045B, offset: 0x000245EE, length: 0x00000036
hhea checksum: 0x0B4103A2, offset: 0x0002466C, length: 0x00000024
hmtx checksum: 0xAA6F5FBD, offset: 0x00025164, length: 0x0000039C
maxp checksum: 0x01A15000, offset: 0x00025500, length: 0x00000006
name checksum: 0x01BFFDAC, offset: 0x00026BC2, length: 0x0000078E
post checksum: 0xFFB80032, offset: 0x00027350, length: 0x00000020
vhea checksum: 0x09C7136C, offset: 0x00027370, length: 0x00000024
vmtx checksum: 0xC039BB2B, offset: 0x0002862A, length: 0x00000632
Done

Of course, the above output does not explicitly or outwardly specify which tables are shared, and which ones are not, so I wrote a short Perl script, called table-share-check.pl, which filters the output to display this information more clearly (note that the AFDKO spot tool emits similar information, and this filter handles that output equally as well). Below are the same two command lines as above, but filtered through this script:

% python otc2otf.py KozGo-Medium.ttc | perl table-share-check.pl
Shared Tables: BASE, CFF , post
Unshared Tables: GPOS, GSUB, OS/2, VORG, cmap, head, hhea, hmtx, maxp, name, vhea, vmtx

% python otc2otf.py LogoLineStd.ttc | perl table-share-check.pl
Shared Tables: GSUB, cmap, maxp, post, vhea
Unshared Tables: BASE, CFF , GPOS, OS/2, VORG, head, hhea, hmtx, name, vmtx

In terms of size savings, the sizes (in bytes) of the original OTFs and the OTCs that were built from them are shown below:

% ls -l KozGo*
-rw-r--r-- 1 lunde staff 5932329 Jan 27 12:02 KozGo-Medium.ttc
-rw-r--r-- 1 lunde staff 5476748 Jan 27 12:01 KozGoPr6N-Medium.otf
-rw-r--r-- 1 lunde staff 3481300 Jan 27 12:00 KozGoPro-Medium.otf

% ls -l LogoLine*
-rw-r--r-- 1 lunde staff 46408 Jan 27 12:01 LogoLineStd-Bold.otf
-rw-r--r-- 1 lunde staff 45668 Jan 27 12:01 LogoLineStd-Light.otf
-rw-r--r-- 1 lunde staff 46592 Jan 27 12:01 LogoLineStd-Medium.otf
-rw-r--r-- 1 lunde staff 46540 Jan 27 12:01 LogoLineStd-Ultra.otf
-rw-r--r-- 1 lunde staff 167004 Jan 27 12:03 LogoLineStd.ttc

Of course, these tools operate not only on CFF-based fonts, but also on TrueType-based ones, mainly because they are simply manipulating the ‘sfnt’ table structure.

As I alluded to earlier in this article, the next version of AFDKO will include these new scripts, but feel free to download them from this article in order to use them right away. And, in case it is not obvious, if any issues are found, please be sure to report them, such as a comment to this article.

In terms of OSes and applications that support OTCs, I have thus far confirmed OS X (I am running Version 10.9), its TextEdit app, Adobe InDesign (I am using CC, but I think that OTCs are supported from CS6), and Microsoft Excel (running on OS X).

UPDATE: Our friends at Apple just informed me that iOS7 supports OTCs.

Lastly, it seems that “ttc,” not “otc,” must be used as the filename extension for OpenType Collection fonts in order for them to be recognized.

Comments are closed.