From 5e9b8c94231ff8d46725042c695a90f279fc9392 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Wed, 22 Feb 2017 14:19:27 -0800 Subject: [PATCH] Implement basic support for WOFF 1.0 --- Cargo.toml | 1 + examples/benchmark.rs | 3 +- examples/dump-outlines.rs | 3 +- examples/generate-atlas.rs | 3 +- examples/lorem-ipsum.rs | 3 +- src/containers/dfont.rs | 2 + src/containers/mod.rs | 1 + src/containers/otf.rs | 81 ++++++++++++++++---------- src/containers/ttc.rs | 10 ++-- src/containers/woff.rs | 114 +++++++++++++++++++++++++++++++++++++ src/font.rs | 22 +++++-- src/lib.rs | 1 + src/tests/buffers.rs | 3 +- 13 files changed, 201 insertions(+), 46 deletions(-) create mode 100644 src/containers/woff.rs diff --git a/Cargo.toml b/Cargo.toml index a236f48e..e815acba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ authors = ["Patrick Walton "] bitflags = "0.7" byteorder = "1" euclid = "0.10" +flate2 = "0.2" gl = "0.6" memmap = "0.5" time = "0.1" diff --git a/examples/benchmark.rs b/examples/benchmark.rs index fe4fd933..032f8231 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -66,6 +66,7 @@ fn main() { let shelf_height = point_size * 2; let file = Mmap::open_path(env::args().nth(1).unwrap(), Protection::Read).unwrap(); + let mut buffer = vec![]; let mut results = vec![]; let start = time::precise_time_ns(); @@ -75,7 +76,7 @@ fn main() { loop { glyph_count = 0; unsafe { - let font = Font::new(file.as_slice()).unwrap(); + let font = Font::new(file.as_slice(), &mut buffer).unwrap(); let codepoint_ranges = [CodepointRange::new(' ' as u32, '~' as u32)]; let glyph_mapping = font.glyph_mapping_for_codepoint_ranges(&codepoint_ranges) diff --git a/examples/dump-outlines.rs b/examples/dump-outlines.rs index 56e961f6..b09082fc 100644 --- a/examples/dump-outlines.rs +++ b/examples/dump-outlines.rs @@ -13,8 +13,9 @@ use std::env; fn main() { let file = Mmap::open_path(env::args().nth(1).unwrap(), Protection::Read).unwrap(); + let mut buffer = vec![]; unsafe { - let font = Font::new(file.as_slice()).unwrap(); + let font = Font::new(file.as_slice(), &mut buffer).unwrap(); let codepoint_ranges = [CodepointRange::new('!' as u32, '~' as u32)]; let glyph_mapping = font.glyph_mapping_for_codepoint_ranges(&codepoint_ranges).unwrap(); for (glyph_index, (_, glyph_id)) in glyph_mapping.iter().enumerate() { diff --git a/examples/generate-atlas.rs b/examples/generate-atlas.rs index a3067169..cd96ad9b 100644 --- a/examples/generate-atlas.rs +++ b/examples/generate-atlas.rs @@ -72,6 +72,7 @@ fn main() { let rasterizer = Rasterizer::new(&instance, device, queue, rasterizer_options).unwrap(); let file = Mmap::open_path(matches.value_of("FONT-FILE").unwrap(), Protection::Read).unwrap(); + let mut buffer = vec![]; let point_size = match matches.value_of("POINT-SIZE") { Some(point_size) => point_size.parse().unwrap(), @@ -85,7 +86,7 @@ fn main() { let (outlines, atlas); unsafe { - let font = Font::from_collection_index(file.as_slice(), font_index).unwrap(); + let font = Font::from_collection_index(file.as_slice(), font_index, &mut buffer).unwrap(); let codepoint_ranges = [CodepointRange::new(' ' as u32, '~' as u32)]; let glyph_mapping = font.glyph_mapping_for_codepoint_ranges(&codepoint_ranges).unwrap(); diff --git a/examples/lorem-ipsum.rs b/examples/lorem-ipsum.rs index caa8d0be..7cf25723 100644 --- a/examples/lorem-ipsum.rs +++ b/examples/lorem-ipsum.rs @@ -102,8 +102,9 @@ fn main() { }; let file = Mmap::open_path(matches.value_of("FONT-FILE").unwrap(), Protection::Read).unwrap(); + let mut buffer = vec![]; let font = unsafe { - Font::from_collection_index(file.as_slice(), font_index).unwrap() + Font::from_collection_index(file.as_slice(), font_index, &mut buffer).unwrap() }; let units_per_em = font.units_per_em() as f32; diff --git a/src/containers/dfont.rs b/src/containers/dfont.rs index 2dfb5105..3fbba7f5 100644 --- a/src/containers/dfont.rs +++ b/src/containers/dfont.rs @@ -18,6 +18,8 @@ use font::Font; use std::mem; use util::Jump; +pub const MAGIC_NUMBER: u32 = 0x0100; + const SFNT: u32 = ((b's' as u32) << 24) | ((b'f' as u32) << 16) | ((b'n' as u32) << 8) | diff --git a/src/containers/mod.rs b/src/containers/mod.rs index da0ebd47..547b88c7 100644 --- a/src/containers/mod.rs +++ b/src/containers/mod.rs @@ -13,4 +13,5 @@ pub mod dfont; pub mod otf; pub mod ttc; +pub mod woff; diff --git a/src/containers/otf.rs b/src/containers/otf.rs index 7ae98168..8ebb509c 100644 --- a/src/containers/otf.rs +++ b/src/containers/otf.rs @@ -32,6 +32,31 @@ const OTTO: u32 = ((b'O' as u32) << 24) | ((b'T' as u32) << 8) | (b'O' as u32); +pub const KNOWN_TABLE_COUNT: usize = 9; + +pub static KNOWN_TABLES: [u32; KNOWN_TABLE_COUNT] = [ + cff::TAG, + os_2::TAG, + cmap::TAG, + glyf::TAG, + head::TAG, + hhea::TAG, + hmtx::TAG, + kern::TAG, + loca::TAG, +]; + +// This must agree with the above. +const TABLE_INDEX_CFF: usize = 0; +const TABLE_INDEX_OS_2: usize = 1; +const TABLE_INDEX_CMAP: usize = 2; +const TABLE_INDEX_GLYF: usize = 3; +const TABLE_INDEX_HEAD: usize = 4; +const TABLE_INDEX_HHEA: usize = 5; +const TABLE_INDEX_HMTX: usize = 6; +const TABLE_INDEX_KERN: usize = 7; +const TABLE_INDEX_LOCA: usize = 8; + pub static SFNT_VERSIONS: [u32; 3] = [ 0x10000, ((b't' as u32) << 24) | ((b'r' as u32) << 16) | ((b'u' as u32) << 8) | (b'e' as u32), @@ -65,32 +90,18 @@ impl<'a> Font<'a> { let num_tables = try!(reader.read_u16::().map_err(FontError::eof)); try!(reader.jump(mem::size_of::() * 3).map_err(FontError::eof)); - let (mut cff_table, mut cmap_table) = (None, None); - let (mut glyf_table, mut head_table) = (None, None); - let (mut hhea_table, mut hmtx_table) = (None, None); - let (mut kern_table, mut loca_table) = (None, None); - let mut os_2_table = None; - + let mut tables = [None; KNOWN_TABLE_COUNT]; for _ in 0..num_tables { let table_id = try!(reader.read_u32::().map_err(FontError::eof)); - - // Skip over the checksum. - try!(reader.read_u32::().map_err(FontError::eof)); - + let _checksum = try!(reader.read_u32::().map_err(FontError::eof)); let offset = try!(reader.read_u32::().map_err(FontError::eof)) as usize; let length = try!(reader.read_u32::().map_err(FontError::eof)) as usize; - let mut slot = match table_id { - cff::TAG => &mut cff_table, - cmap::TAG => &mut cmap_table, - glyf::TAG => &mut glyf_table, - head::TAG => &mut head_table, - hhea::TAG => &mut hhea_table, - hmtx::TAG => &mut hmtx_table, - kern::TAG => &mut kern_table, - loca::TAG => &mut loca_table, - os_2::TAG => &mut os_2_table, - _ => continue, + // Find the table ID in our list of known IDs, which must be sorted. + debug_assert!(KNOWN_TABLES.windows(2).all(|w| w[0] < w[1])); + let slot = match KNOWN_TABLES.binary_search(&table_id) { + Err(_) => continue, + Ok(table_index) => &mut tables[table_index], }; // Make sure there isn't more than one copy of the table. @@ -103,27 +114,37 @@ impl<'a> Font<'a> { }) } - let cff_table = match cff_table { + Font::from_table_list(bytes, &tables) + } + + #[doc(hidden)] + pub fn from_table_list<'b>(bytes: &'b [u8], + tables: &[Option>; KNOWN_TABLE_COUNT]) + -> Result, FontError> { + let cff_table = match tables[TABLE_INDEX_CFF] { None => None, Some(cff_table) => Some(try!(CffTable::new(cff_table))), }; - let loca_table = match loca_table { + let loca_table = match tables[TABLE_INDEX_LOCA] { None => None, Some(loca_table) => Some(try!(LocaTable::new(loca_table))), }; + // For brevity below… + let missing = FontError::RequiredTableMissing; + let tables = FontTables { - cmap: CmapTable::new(try!(cmap_table.ok_or(FontError::RequiredTableMissing))), - head: try!(HeadTable::new(try!(head_table.ok_or(FontError::RequiredTableMissing)))), - hhea: try!(HheaTable::new(try!(hhea_table.ok_or(FontError::RequiredTableMissing)))), - hmtx: HmtxTable::new(try!(hmtx_table.ok_or(FontError::RequiredTableMissing))), - os_2: try!(Os2Table::new(try!(os_2_table.ok_or(FontError::RequiredTableMissing)))), + cmap: CmapTable::new(try!(tables[TABLE_INDEX_CMAP].ok_or(missing))), + head: try!(HeadTable::new(try!(tables[TABLE_INDEX_HEAD].ok_or(missing)))), + hhea: try!(HheaTable::new(try!(tables[TABLE_INDEX_HHEA].ok_or(missing)))), + hmtx: HmtxTable::new(try!(tables[TABLE_INDEX_HMTX].ok_or(missing))), + os_2: try!(Os2Table::new(try!(tables[TABLE_INDEX_OS_2].ok_or(missing)))), cff: cff_table, - glyf: glyf_table.map(GlyfTable::new), + glyf: tables[TABLE_INDEX_GLYF].map(GlyfTable::new), loca: loca_table, - kern: kern_table.and_then(|table| KernTable::new(table).ok()), + kern: tables[TABLE_INDEX_KERN].and_then(|table| KernTable::new(table).ok()), }; Ok(Font::from_tables(bytes, tables)) diff --git a/src/containers/ttc.rs b/src/containers/ttc.rs index fb1d9b63..820a4ef4 100644 --- a/src/containers/ttc.rs +++ b/src/containers/ttc.rs @@ -18,10 +18,10 @@ use font::Font; use std::mem; use util::Jump; -pub const TTCF: u32 = ((b't' as u32) << 24) | - ((b't' as u32) << 16) | - ((b'c' as u32) << 8) | - (b'f' as u32); +pub const MAGIC_NUMBER: u32 = ((b't' as u32) << 24) | + ((b't' as u32) << 16) | + ((b'c' as u32) << 8) | + (b'f' as u32); impl<'a> Font<'a> { /// Creates a new font from a single font within a byte buffer containing the contents of a @@ -29,7 +29,7 @@ impl<'a> Font<'a> { pub fn from_ttc_index<'b>(bytes: &'b [u8], index: u32) -> Result, FontError> { let mut reader = bytes; let magic_number = try!(reader.read_u32::().map_err(FontError::eof)); - if magic_number != TTCF { + if magic_number != MAGIC_NUMBER { return Err(FontError::UnknownFormat) } diff --git a/src/containers/woff.rs b/src/containers/woff.rs new file mode 100644 index 00000000..b2dc6134 --- /dev/null +++ b/src/containers/woff.rs @@ -0,0 +1,114 @@ +// Copyright 2017 The Servo Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Web Open Font Format 1.0 (`.woff`) files. +//! +//! See the specification: https://www.w3.org/TR/WOFF/ +//! +//! TODO(pcwalton): WOFF 2.0. + +use byteorder::{BigEndian, ReadBytesExt}; +use containers::otf::{KNOWN_TABLES, KNOWN_TABLE_COUNT, SFNT_VERSIONS}; +use error::FontError; +use flate2::FlateReadExt; +use font::{Font, FontTable}; +use std::io::Read; +use std::iter; +use std::mem; +use util::Jump; + +pub const MAGIC_NUMBER: u32 = ((b'w' as u32) << 24) | + ((b'O' as u32) << 16) | + ((b'F' as u32) << 8) | + (b'F' as u32); + +impl<'a> Font<'a> { + /// Creates a new font from a buffer containing data in the WOFF format. + /// + /// The given buffer will be used to decompress data. + /// + /// Decompresses eagerly. + pub fn from_woff<'b>(bytes: &'b [u8], buffer: &'b mut Vec) -> Result, FontError> { + let mut reader = bytes; + + // Check magic number. + let magic_number = try!(reader.read_u32::().map_err(FontError::eof)); + if magic_number != MAGIC_NUMBER { + return Err(FontError::UnknownFormat) + } + + // Check the flavor. + let flavor = try!(reader.read_u32::().map_err(FontError::eof)); + if !SFNT_VERSIONS.contains(&flavor) { + return Err(FontError::UnknownFormat) + } + + // Get the number of tables. + let _length = try!(reader.read_u32::().map_err(FontError::eof)); + let num_tables = try!(reader.read_u16::().map_err(FontError::eof)); + let _reserved = try!(reader.read_u16::().map_err(FontError::eof)); + + // Allocate size for uncompressed tables. + let total_sfnt_size = try!(reader.read_u32::().map_err(FontError::eof)); + try!(reader.jump(mem::size_of::() * 6).map_err(FontError::eof)); + let buffer_start = buffer.len(); + buffer.extend(iter::repeat(0).take(total_sfnt_size as usize)); + let mut buffer = &mut buffer[buffer_start..]; + + // Decompress and load tables as necessary. + let mut tables = [None; KNOWN_TABLE_COUNT]; + for _ in 0..num_tables { + let tag = try!(reader.read_u32::().map_err(FontError::eof)); + let offset = try!(reader.read_u32::().map_err(FontError::eof)); + let comp_length = try!(reader.read_u32::().map_err(FontError::eof)); + let orig_length = try!(reader.read_u32::().map_err(FontError::eof)); + let _orig_checksum = try!(reader.read_u32::().map_err(FontError::eof)); + + // Find the table ID in our list of known IDs, which must be sorted. + debug_assert!(KNOWN_TABLES.windows(2).all(|w| w[0] < w[1])); + let slot = match KNOWN_TABLES.binary_search(&tag) { + Err(_) => continue, + Ok(table_index) => &mut tables[table_index], + }; + + // Make sure there isn't more than one copy of the table. + if slot.is_some() { + return Err(FontError::Failed) + } + + // Allocate space in the buffer. + let comp_end = offset as usize + comp_length as usize; + let mut temp = buffer; // borrow check black magic + let (mut dest, mut rest) = temp.split_at_mut(orig_length as usize); + buffer = rest; + + // Decompress or copy as applicable. + // + // FIXME(pcwalton): Errors here may be zlib errors, not EOFs. + if comp_length != orig_length { + let mut table_reader = bytes; + try!(table_reader.jump(offset as usize).map_err(FontError::eof)); + let mut table_reader = table_reader.zlib_decode(); + try!(table_reader.read_exact(dest).map_err(FontError::eof)); + } else if comp_end <= bytes.len() { + dest.clone_from_slice(&bytes[offset as usize..comp_end]) + } else { + return Err(FontError::UnexpectedEof) + } + + *slot = Some(FontTable { + bytes: dest, + }) + } + + Font::from_table_list(bytes, &tables) + } +} + diff --git a/src/font.rs b/src/font.rs index 39aa1a84..41fbf97b 100644 --- a/src/font.rs +++ b/src/font.rs @@ -12,8 +12,10 @@ use byteorder::{BigEndian, ReadBytesExt}; use charmap::{CodepointRange, GlyphMapping}; +use containers::dfont; use containers::otf::{FontTables, SFNT_VERSIONS}; -use containers::ttc::TTCF; +use containers::ttc; +use containers::woff; use error::FontError; use euclid::Point2D; use outline::GlyphBounds; @@ -49,9 +51,12 @@ impl<'a> Font<'a> { /// If this is a `.ttc` or `.dfont` collection, this returns the first font within it. If you /// want to read another one, use the `Font::from_collection_index` API. /// + /// The supplied `buffer` is an arbitrary vector that may or may not be used as a temporary + /// storage space. Typically you will want to just pass an empty vector here. + /// /// Returns the font on success or an error on failure. - pub fn new<'b>(bytes: &'b [u8]) -> Result, FontError> { - Font::from_collection_index(bytes, 0) + pub fn new<'b>(bytes: &'b [u8], buffer: &'b mut Vec) -> Result, FontError> { + Font::from_collection_index(bytes, 0, buffer) } /// Creates a new font from a single font within a byte buffer containing the contents of a @@ -59,15 +64,20 @@ impl<'a> Font<'a> { /// /// If this is a `.ttc` or `.dfont` collection, this returns the appropriate font within it. /// + /// The supplied `buffer` is an arbitrary vector that may or may not be used as a temporary + /// storage space. Typically you will want to just pass an empty vector here. + /// /// Returns the font on success or an error on failure. - pub fn from_collection_index<'b>(bytes: &'b [u8], index: u32) -> Result, FontError> { + pub fn from_collection_index<'b>(bytes: &'b [u8], index: u32, buffer: &'b mut Vec) + -> Result, FontError> { // Check the magic number. let mut reader = bytes; let magic_number = try!(reader.read_u32::().map_err(FontError::eof)); match magic_number { - TTCF => Font::from_ttc_index(bytes, index), + ttc::MAGIC_NUMBER => Font::from_ttc_index(bytes, index), + woff::MAGIC_NUMBER => Font::from_woff(bytes, buffer), + dfont::MAGIC_NUMBER => Font::from_dfont_index(bytes, index), magic_number if SFNT_VERSIONS.contains(&magic_number) => Font::from_otf(bytes, 0), - 0x0100 => Font::from_dfont_index(bytes, index), _ => Err(FontError::UnknownFormat), } } diff --git a/src/lib.rs b/src/lib.rs index 0ea0aff1..e6cde5b2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,6 +78,7 @@ extern crate bitflags; extern crate byteorder; extern crate compute_shader; extern crate euclid; +extern crate flate2; extern crate gl; #[cfg(test)] extern crate memmap; diff --git a/src/tests/buffers.rs b/src/tests/buffers.rs index 4e651f71..8ecfc6c4 100644 --- a/src/tests/buffers.rs +++ b/src/tests/buffers.rs @@ -12,8 +12,9 @@ static TEST_FONT_PATH: &'static str = "resources/tests/nimbus-sans/NimbusSanL-Re #[bench] fn bench_add_glyphs(bencher: &mut Bencher) { let file = Mmap::open_path(TEST_FONT_PATH, Protection::Read).expect("Couldn't open test font"); + let mut buffer = vec![]; unsafe { - let font = Font::new(file.as_slice()).unwrap(); + let font = Font::new(file.as_slice(), &mut buffer).unwrap(); let codepoint_ranges = [CodepointRange::new('!' as u32, '~' as u32)]; let glyph_mapping = font.glyph_mapping_for_codepoint_ranges(&codepoint_ranges) .expect("Couldn't find glyph ranges");