//! The WOFF2 container. //! //! WOFF2 defines two transforms for `glyf`/`loca` and permits neither: a //! transformVersion of 3 means "stored as-is", and every browser reads it. //! Taking that route makes the container brotli plus a table directory, which //! is a page of code, against reimplementing a lossy re-encoder that would have //! to reproduce the base's outlines exactly to be safe. //! //! The cost is measured rather than assumed: `quasi-type build` prints the //! woff2 size beside the ttf. Against upstream's own complete woff2 for the //! same face, the null transform costs about 11%: 54.7KB against IBM's 49.2KB //! for Plex Mono Regular, seven added marks included. //! //! The 11.5KB figure in the typography standard is a different thing and should //! not be compared to this one. It is IBM's `split/woff2` Latin-1 subset, which //! is what a web page actually loads. Subsetting is the lever that moves this //! number, and it belongs to whatever serves the face rather than here. use std::io::Write; use crate::Error; /// Known tags, in the order the spec numbers them. A tag in this table costs /// one byte in the directory; anything else costs five. const KNOWN_TAGS: [&[u8; 4]; 63] = [ b"cmap", b"head", b"hhea", b"hmtx", b"maxp", b"name", b"OS/2", b"post", b"cvt ", b"fpgm", b"glyf", b"loca", b"prep", b"CFF ", b"VORG", b"EBDT", b"EBLC", b"gasp", b"hdmx", b"kern", b"LTSH", b"PCLT", b"VDMX", b"vhea", b"vmtx", b"BASE", b"GDEF", b"GPOS", b"GSUB", b"EBSC", b"JSTF", b"MATH", b"CBDT", b"CBLC", b"COLR", b"CPAL", b"SVG ", b"sbix", b"acnt", b"avar", b"bdat", b"bloc", b"bsln", b"cvar", b"fdsc", b"feat", b"fmtx", b"fvar", b"gvar", b"hsty", b"just", b"lcar", b"mort", b"morx", b"opbd", b"prop", b"trak", b"Zapf", b"Silf", b"Glat", b"Gloc", b"Feat", b"Sill", ]; const SIGNATURE: &[u8; 4] = b"wOF2"; pub fn encode(sfnt: &[u8]) -> Result, Error> { if sfnt.len() < 12 { return Err(Error::Woff2("input is not an sfnt".into())); } let flavor = &sfnt[0..4]; let num_tables = u16::from_be_bytes([sfnt[4], sfnt[5]]); let mut directory = Vec::new(); let mut payload = Vec::new(); let mut total_sfnt_size: u32 = 12 + u32::from(num_tables) * 16; for i in 0..num_tables as usize { let entry = 12 + i * 16; let record = sfnt .get(entry..entry + 16) .ok_or_else(|| Error::Woff2("table directory is truncated".into()))?; let tag: [u8; 4] = record[0..4].try_into().unwrap(); let offset = u32::from_be_bytes(record[8..12].try_into().unwrap()) as usize; let length = u32::from_be_bytes(record[12..16].try_into().unwrap()) as usize; let data = sfnt .get(offset..offset + length) .ok_or_else(|| Error::Woff2(format!("table `{}` runs past the file", show(tag))))?; total_sfnt_size += (length as u32).next_multiple_of(4); // Flag byte: the low six bits are the known-tag index, or 63 for a tag // spelled out in full. The top two are the transform version, and 0 is // the null transform for everything except glyf/loca, where the null // transform is 3. let known = KNOWN_TAGS.iter().position(|t| *t == &tag); let transform = if &tag == b"glyf" || &tag == b"loca" { 3u8 } else { 0u8 }; let index = known.map_or(63u8, |i| i as u8); directory.push(index | (transform << 6)); if known.is_none() { directory.extend_from_slice(&tag); } write_uint_base128(&mut directory, length as u32); // A transformed length is written only when the transform is not the // null one, so nothing follows here. payload.extend_from_slice(data); while payload.len() % 4 != 0 { payload.push(0); } } let compressed = brotli_compress(&payload)?; let mut out = Vec::with_capacity(48 + directory.len() + compressed.len()); out.extend_from_slice(SIGNATURE); out.extend_from_slice(flavor); let header_len = 48 + directory.len(); let total: u32 = (header_len + compressed.len()) as u32; out.extend_from_slice(&total.to_be_bytes()); // length out.extend_from_slice(&num_tables.to_be_bytes()); out.extend_from_slice(&0u16.to_be_bytes()); // reserved out.extend_from_slice(&total_sfnt_size.to_be_bytes()); out.extend_from_slice(&(compressed.len() as u32).to_be_bytes()); out.extend_from_slice(&0u16.to_be_bytes()); // majorVersion out.extend_from_slice(&0u16.to_be_bytes()); // minorVersion out.extend_from_slice(&0u32.to_be_bytes()); // metaOffset out.extend_from_slice(&0u32.to_be_bytes()); // metaLength out.extend_from_slice(&0u32.to_be_bytes()); // metaOrigLength out.extend_from_slice(&0u32.to_be_bytes()); // privOffset out.extend_from_slice(&0u32.to_be_bytes()); // privLength debug_assert_eq!(out.len(), 48); out.extend_from_slice(&directory); out.extend_from_slice(&compressed); Ok(out) } /// UIntBase128, the spec's variable-length integer: seven bits per byte, high /// bit set on every byte but the last. fn write_uint_base128(out: &mut Vec, value: u32) { let mut bytes = [0u8; 5]; let mut len = 0; let mut remaining = value; loop { bytes[len] = (remaining & 0x7F) as u8; len += 1; remaining >>= 7; if remaining == 0 { break; } } for i in (0..len).rev() { let last = i == 0; out.push(bytes[i] | if last { 0 } else { 0x80 }); } } fn brotli_compress(data: &[u8]) -> Result, Error> { let mut out = Vec::new(); let params = brotli::enc::BrotliEncoderParams { quality: 11, lgwin: 22, ..Default::default() }; let mut writer = brotli::CompressorWriter::with_params(&mut out, 4096, ¶ms); writer .write_all(data) .map_err(|e| Error::Woff2(format!("brotli: {e}")))?; drop(writer); Ok(out) } fn show(tag: [u8; 4]) -> String { String::from_utf8_lossy(&tag).to_string() } #[cfg(test)] mod tests { use super::*; #[test] fn base128_matches_the_specs_examples() { let mut out = Vec::new(); write_uint_base128(&mut out, 0); assert_eq!(out, vec![0x00]); out.clear(); write_uint_base128(&mut out, 127); assert_eq!(out, vec![0x7F]); out.clear(); write_uint_base128(&mut out, 128); assert_eq!(out, vec![0x81, 0x00]); out.clear(); write_uint_base128(&mut out, 0x4000); assert_eq!(out, vec![0x81, 0x80, 0x00]); } #[test] fn a_truncated_sfnt_is_refused() { assert!(encode(&[0u8; 4]).is_err()); } }