Skip to main content

max / quasi-type

6.6 KB · 176 lines History Blame Raw
1 //! The WOFF2 container.
2 //!
3 //! WOFF2 defines two transforms for `glyf`/`loca` and permits neither: a
4 //! transformVersion of 3 means "stored as-is", and every browser reads it.
5 //! Taking that route makes the container brotli plus a table directory, which
6 //! is a page of code, against reimplementing a lossy re-encoder that would have
7 //! to reproduce the base's outlines exactly to be safe.
8 //!
9 //! The cost is measured rather than assumed: `quasi-type build` prints the
10 //! woff2 size beside the ttf. Against upstream's own complete woff2 for the
11 //! same face, the null transform costs about 11%: 54.7KB against IBM's 49.2KB
12 //! for Plex Mono Regular, seven added marks included.
13 //!
14 //! The 11.5KB figure in the typography standard is a different thing and should
15 //! not be compared to this one. It is IBM's `split/woff2` Latin-1 subset, which
16 //! is what a web page actually loads. Subsetting is the lever that moves this
17 //! number, and it belongs to whatever serves the face rather than here.
18
19 use std::io::Write;
20
21 use crate::Error;
22
23 /// Known tags, in the order the spec numbers them. A tag in this table costs
24 /// one byte in the directory; anything else costs five.
25 const KNOWN_TAGS: [&[u8; 4]; 63] = [
26 b"cmap", b"head", b"hhea", b"hmtx", b"maxp", b"name", b"OS/2", b"post", b"cvt ", b"fpgm",
27 b"glyf", b"loca", b"prep", b"CFF ", b"VORG", b"EBDT", b"EBLC", b"gasp", b"hdmx", b"kern",
28 b"LTSH", b"PCLT", b"VDMX", b"vhea", b"vmtx", b"BASE", b"GDEF", b"GPOS", b"GSUB", b"EBSC",
29 b"JSTF", b"MATH", b"CBDT", b"CBLC", b"COLR", b"CPAL", b"SVG ", b"sbix", b"acnt", b"avar",
30 b"bdat", b"bloc", b"bsln", b"cvar", b"fdsc", b"feat", b"fmtx", b"fvar", b"gvar", b"hsty",
31 b"just", b"lcar", b"mort", b"morx", b"opbd", b"prop", b"trak", b"Zapf", b"Silf", b"Glat",
32 b"Gloc", b"Feat", b"Sill",
33 ];
34
35 const SIGNATURE: &[u8; 4] = b"wOF2";
36
37 pub fn encode(sfnt: &[u8]) -> Result<Vec<u8>, Error> {
38 if sfnt.len() < 12 {
39 return Err(Error::Woff2("input is not an sfnt".into()));
40 }
41 let flavor = &sfnt[0..4];
42 let num_tables = u16::from_be_bytes([sfnt[4], sfnt[5]]);
43
44 let mut directory = Vec::new();
45 let mut payload = Vec::new();
46 let mut total_sfnt_size: u32 = 12 + u32::from(num_tables) * 16;
47
48 for i in 0..num_tables as usize {
49 let entry = 12 + i * 16;
50 let record = sfnt
51 .get(entry..entry + 16)
52 .ok_or_else(|| Error::Woff2("table directory is truncated".into()))?;
53 let tag: [u8; 4] = record[0..4].try_into().unwrap();
54 let offset = u32::from_be_bytes(record[8..12].try_into().unwrap()) as usize;
55 let length = u32::from_be_bytes(record[12..16].try_into().unwrap()) as usize;
56 let data = sfnt
57 .get(offset..offset + length)
58 .ok_or_else(|| Error::Woff2(format!("table `{}` runs past the file", show(tag))))?;
59
60 total_sfnt_size += (length as u32).next_multiple_of(4);
61
62 // Flag byte: the low six bits are the known-tag index, or 63 for a tag
63 // spelled out in full. The top two are the transform version, and 0 is
64 // the null transform for everything except glyf/loca, where the null
65 // transform is 3.
66 let known = KNOWN_TAGS.iter().position(|t| *t == &tag);
67 let transform = if &tag == b"glyf" || &tag == b"loca" {
68 3u8
69 } else {
70 0u8
71 };
72 let index = known.map_or(63u8, |i| i as u8);
73 directory.push(index | (transform << 6));
74 if known.is_none() {
75 directory.extend_from_slice(&tag);
76 }
77 write_uint_base128(&mut directory, length as u32);
78 // A transformed length is written only when the transform is not the
79 // null one, so nothing follows here.
80
81 payload.extend_from_slice(data);
82 while payload.len() % 4 != 0 {
83 payload.push(0);
84 }
85 }
86
87 let compressed = brotli_compress(&payload)?;
88
89 let mut out = Vec::with_capacity(48 + directory.len() + compressed.len());
90 out.extend_from_slice(SIGNATURE);
91 out.extend_from_slice(flavor);
92 let header_len = 48 + directory.len();
93 let total: u32 = (header_len + compressed.len()) as u32;
94 out.extend_from_slice(&total.to_be_bytes()); // length
95 out.extend_from_slice(&num_tables.to_be_bytes());
96 out.extend_from_slice(&0u16.to_be_bytes()); // reserved
97 out.extend_from_slice(&total_sfnt_size.to_be_bytes());
98 out.extend_from_slice(&(compressed.len() as u32).to_be_bytes());
99 out.extend_from_slice(&0u16.to_be_bytes()); // majorVersion
100 out.extend_from_slice(&0u16.to_be_bytes()); // minorVersion
101 out.extend_from_slice(&0u32.to_be_bytes()); // metaOffset
102 out.extend_from_slice(&0u32.to_be_bytes()); // metaLength
103 out.extend_from_slice(&0u32.to_be_bytes()); // metaOrigLength
104 out.extend_from_slice(&0u32.to_be_bytes()); // privOffset
105 out.extend_from_slice(&0u32.to_be_bytes()); // privLength
106 debug_assert_eq!(out.len(), 48);
107 out.extend_from_slice(&directory);
108 out.extend_from_slice(&compressed);
109 Ok(out)
110 }
111
112 /// UIntBase128, the spec's variable-length integer: seven bits per byte, high
113 /// bit set on every byte but the last.
114 fn write_uint_base128(out: &mut Vec<u8>, value: u32) {
115 let mut bytes = [0u8; 5];
116 let mut len = 0;
117 let mut remaining = value;
118 loop {
119 bytes[len] = (remaining & 0x7F) as u8;
120 len += 1;
121 remaining >>= 7;
122 if remaining == 0 {
123 break;
124 }
125 }
126 for i in (0..len).rev() {
127 let last = i == 0;
128 out.push(bytes[i] | if last { 0 } else { 0x80 });
129 }
130 }
131
132 fn brotli_compress(data: &[u8]) -> Result<Vec<u8>, Error> {
133 let mut out = Vec::new();
134 let params = brotli::enc::BrotliEncoderParams {
135 quality: 11,
136 lgwin: 22,
137 ..Default::default()
138 };
139 let mut writer = brotli::CompressorWriter::with_params(&mut out, 4096, &params);
140 writer
141 .write_all(data)
142 .map_err(|e| Error::Woff2(format!("brotli: {e}")))?;
143 drop(writer);
144 Ok(out)
145 }
146
147 fn show(tag: [u8; 4]) -> String {
148 String::from_utf8_lossy(&tag).to_string()
149 }
150
151 #[cfg(test)]
152 mod tests {
153 use super::*;
154
155 #[test]
156 fn base128_matches_the_specs_examples() {
157 let mut out = Vec::new();
158 write_uint_base128(&mut out, 0);
159 assert_eq!(out, vec![0x00]);
160 out.clear();
161 write_uint_base128(&mut out, 127);
162 assert_eq!(out, vec![0x7F]);
163 out.clear();
164 write_uint_base128(&mut out, 128);
165 assert_eq!(out, vec![0x81, 0x00]);
166 out.clear();
167 write_uint_base128(&mut out, 0x4000);
168 assert_eq!(out, vec![0x81, 0x80, 0x00]);
169 }
170
171 #[test]
172 fn a_truncated_sfnt_is_refused() {
173 assert!(encode(&[0u8; 4]).is_err());
174 }
175 }
176