diff --git a/Cargo.lock b/Cargo.lock index 3c1597c..8d31ae8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,10 +2,32 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "font-test-data" +version = "0.1.0" +source = "git+https://github.com/googlefonts/fontations?rev=10c27ef7bba1549fa37a3f41cd4870b2a24b1073#10c27ef7bba1549fa37a3f41cd4870b2a24b1073" + +[[package]] +name = "font-types" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd7f3ea17572640b606b35df42cfb6ecdf003704b062580e59918692190b73d" + +[[package]] +name = "read-fonts" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7555e052e772f964a1c99f1434f6a2c3a47a5f8e4292236921f121a7753cb2b5" +dependencies = [ + "font-types", +] + [[package]] name = "swash" version = "0.1.9" dependencies = [ + "font-test-data", + "read-fonts", "yazi", "zeno", ] diff --git a/Cargo.toml b/Cargo.toml index d2cadb1..6ae8231 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.9" authors = ["Chad Brokaw "] edition = "2018" description = "Font introspection, complex text shaping and glyph rendering." -license = "MIT OR Apache-2.0" +license = "MIT/Apache-2.0" keywords = ["font", "shape", "glyph", "text"] categories = ["graphics", "text-processing"] repository = "https://github.com/dfrg/swash" @@ -20,3 +20,8 @@ render = ["scale", "zeno/eval"] [dependencies] yazi = { version = "0.1.6", optional = true } zeno = { version = "0.2.2", optional = true, default_features = false } +read-fonts = "0.15.2" + +[dev-dependencies] +font-test-data= { git = "https://github.com/googlefonts/fontations", rev = "10c27ef7bba1549fa37a3f41cd4870b2a24b1073" } +read-fonts = { version = "0.15.2", features = ["scaler_test"] } diff --git a/src/scale/cff/cff.rs b/src/scale/cff/cff.rs deleted file mode 100644 index dff2ca5..0000000 --- a/src/scale/cff/cff.rs +++ /dev/null @@ -1,1862 +0,0 @@ -//! Adobe compact font format. - -use super::hint::{Hinter, HinterState}; -use super::internal::*; -use crate::{FontRef, GlyphId}; - -use super::TRACE; - -use core::ops::Range; - -pub const CFF_: RawTag = raw_tag(b"CFF "); -pub const CFF2: RawTag = raw_tag(b"CFF2"); - -/// Compact font format (CFF or CFF2). -#[derive(Copy, Clone)] -pub struct Cff<'a> { - data: &'a [u8], - proxy: CffProxy, -} - -impl<'a> Cff<'a> { - /// Returns the glyph with the specified glyph id. - pub fn get(&self, glyph_id: GlyphId) -> Option { - Glyph::new(self.data, &self.proxy, glyph_id) - } -} - -/// Proxy for rematerializing a CFF table. -#[derive(Copy, Clone)] -pub struct CffProxy { - cff: u32, - gsubrs: Index, - subrs: Index, - char_strings: Index, - fd_array: Index, - fd_select: u32, - private: (u32, u32), - vstore: u32, - vsindex: u16, - font_matrix: Option, - is_cff2: bool, - is_simple: bool, - units_per_em: u16, -} - -impl CffProxy { - pub fn from_font<'a>(font: &FontRef<'a>) -> Option { - let mut offset = font.table_offset(CFF2); - if offset == 0 { - offset = font.table_offset(CFF_); - } - if offset == 0 { - return None; - } - let units_per_em = font.head().map(|head| head.units_per_em()).unwrap_or(1000); - - Self::parse(font.data, offset, units_per_em) - } - - pub fn materialize<'a>(&self, font: &FontRef<'a>) -> Cff<'a> { - let data = font.data.get(self.cff as usize..).unwrap_or(&[]); - Cff { data, proxy: *self } - } - - fn parse(data: &[u8], cff: u32, units_per_em: u16) -> Option { - let data = data.get(cff as usize..)?; - let mut c = Stream::new(data); - let major = c.read::()?; - let _minor = c.read::()?; - let is_cff2 = if major == 2 { - true - } else if major != 1 { - return None; - } else { - false - }; - let header_size = c.read::()? as usize; - let top_dict_range; - let gsubrs = if is_cff2 { - if header_size < 5 { - return None; - } - let top_dict_len = c.read::()? as usize; - c.set_offset(header_size)?; - let start = c.offset(); - top_dict_range = start..start + top_dict_len; - c.skip(top_dict_len); - Index::new2(data, c.offset() as u32)? - } else { - if header_size < 4 { - return None; - } - c.set_offset(header_size)?; - c.skip(Index::new(data, c.offset() as u32)?.len(data)? as usize)?; - let top_idx = Index::new(data, c.offset() as u32)?; - if top_idx.count(data) != 1 { - return None; - } - c.skip(top_idx.len(data)?)?; - top_dict_range = top_idx.get_range(data, 0)?; - c.skip(Index::new(data, c.offset() as u32)?.len(data)? as usize)?; - Index::new(data, c.offset() as u32)? - }; - let mut loader = Loader { - char_strings: 0, - subrs: 0, - private: 0..0, - fd_array: 0, - fd_select: 0, - font_matrix: None, - vstore: 0, - vsindex: 0, - ok: true, - units_per_em, - }; - parse_dict(data, top_dict_range, None, &mut loader)?; - if !loader.ok { - return None; - } - let mut is_simple = !is_cff2; - let mut fd_array = Index::empty(); - let mut private = (0u32, 0u32); - let mut vstore = 0; - if is_cff2 { - if loader.vstore != 0 { - vstore = loader.vstore as u32 + 2; - } - fd_array = Index::new2(data, loader.fd_array as u32)?; - if fd_array.count(data) == 0 { - is_simple = true; - } else if fd_array.count(data) == 1 { - is_simple = true; - let font_dict_range = fd_array.get_range(data, 0)?; - parse_dict(data, font_dict_range, None, &mut loader)?; - private = (loader.private.start as u32, loader.private.end as u32); - if private.0 != 0 { - let blend = if vstore != 0 { - Some(BlendData::new(data, vstore, &[])) - } else { - None - }; - parse_dict(data, loader.private.clone(), blend, &mut loader)?; - } - } else if loader.fd_select == 0 { - return None; - } - } else { - private = (loader.private.start as u32, loader.private.end as u32); - if loader.fd_array != 0 { - fd_array = Index::new(data, loader.fd_array as u32)?; - is_simple = false; - } - } - if private.0 != 0 { - parse_dict(data, loader.private.clone(), None, &mut loader)?; - } - if !loader.ok || loader.char_strings == 0 { - return None; - } - let char_strings = if is_cff2 { - Index::new2(data, loader.char_strings as u32)? - } else { - Index::new(data, loader.char_strings as u32)? - }; - let subrs = if loader.subrs != 0 { - if is_cff2 { - Index::new2(data, loader.subrs as u32)? - } else { - Index::new(data, loader.subrs as u32)? - } - } else { - Index::empty() - }; - Some(Self { - cff, - gsubrs, - subrs, - char_strings, - fd_array, - fd_select: loader.fd_select as u32, - font_matrix: loader.font_matrix, - private, - vstore, - vsindex: loader.vsindex, - is_cff2, - is_simple, - units_per_em: loader.units_per_em, - }) - } -} - -/// Compact font format glyph outline. -#[derive(Copy, Clone)] -pub struct Glyph<'a> { - data: &'a [u8], - proxy: CffProxy, - fd: u16, - private: (u32, u32), - vsindex: u16, - subrs: Index, - range: (u32, u32), -} - -impl<'a> Glyph<'a> { - /// Returns a char string for the specified CFF table and glyph id. - fn new(data: &'a [u8], proxy: &CffProxy, glyph_id: GlyphId) -> Option { - let range = proxy.char_strings.get_range(data, glyph_id as u32)?; - let mut fd = 0u16; - let mut private = proxy.private; - let mut vsindex = proxy.vsindex; - let mut subrs = proxy.subrs; - if !proxy.is_simple { - fd = parse_fd_select(data, proxy.fd_select, glyph_id)? as u16; - let mut loader = Loader { - char_strings: 0, - subrs: 0, - private: 0..0, - fd_array: 0, - fd_select: 0, - font_matrix: None, - vstore: 0, - vsindex, - ok: true, - units_per_em: proxy.units_per_em, - }; - let fd_range = proxy.fd_array.get_range(data, fd as u32)?; - let blend = if proxy.vstore != 0 { - Some(BlendData::new(data, proxy.vstore, &[])) - } else { - None - }; - parse_dict(data, fd_range, blend, &mut loader)?; - private = (loader.private.start as u32, loader.private.end as u32); - parse_dict(data, loader.private.clone(), blend, &mut loader)?; - vsindex = loader.vsindex; - if loader.subrs != 0 { - subrs = if proxy.is_cff2 { - Index::new2(data, loader.subrs as u32)? - } else { - Index::new(data, loader.subrs as u32)? - }; - } - } - Some(Self { - data, - proxy: *proxy, - fd, - private, - vsindex, - subrs, - range: (range.start as u32, range.end as u32), - }) - } - - /// Returns the index of the associated font dictionary. - pub fn subfont_index(&self) -> u16 { - self.fd - } - - /// Computes the bounding box of the glyph. - pub fn _bounds(&self, scale: f32, coords: &[i16]) -> [f32; 4] { - let mut sink = PathBounds { - some: false, - xmin: core::f32::MAX, - xmax: core::f32::MIN, - ymin: core::f32::MAX, - ymax: core::f32::MIN, - }; - if let Some(xform) = self.proxy.font_matrix { - let xform = Transform::combine(&xform, &Transform::scale(scale)); - let mut sink = TransformSink { - xform, - inner: &mut sink, - }; - self.parse(coords, &mut sink); - } else if scale != 1. { - let mut sink = ScaleSink { - scale, - inner: &mut sink, - }; - self.parse(coords, &mut sink); - } else { - self.parse(coords, &mut sink); - } - if sink.some { - [sink.xmin, sink.xmax, sink.ymin, sink.ymax] - } else { - [0.; 4] - } - } - - /// Evaluates the glyph, directing the result to the specified sink. - pub fn path( - &self, - scale: f32, - coords: &[i16], - hinting: Option<&HinterState>, - sink: &mut impl GlyphSink, - ) -> bool { - if let Some(state) = hinting { - let scale = state.scale(); - if let Some(mut xform) = self.proxy.font_matrix { - xform.x *= scale; - xform.y *= scale; - let mut sink = TransformSink { xform, inner: sink }; - let mut hinter = Hinter::new(state, &mut sink); - self.parse(coords, &mut hinter) - } else { - let mut hinter = Hinter::new(state, sink); - self.parse(coords, &mut hinter) - } - } else if let Some(xform) = self.proxy.font_matrix { - let xform = Transform::combine(&xform, &Transform::scale(scale)); - let mut sink = TransformSink { xform, inner: sink }; - self.parse(coords, &mut sink) - } else { - let mut sink = ScaleSink { scale, inner: sink }; - self.parse(coords, &mut sink) - } - } - - /// Attempts to parse the private dictionary associated with the char string, - /// directing the results to the supplied sink. - pub(super) fn eval_private_dict(&self, coords: &[i16], sink: &mut impl DictionarySink) -> bool { - if self.private.0 == 0 { - return true; - } - let blend = if self.proxy.vstore != 0 { - Some(BlendData::new(self.data, self.proxy.vstore, coords)) - } else { - None - }; - let range = self.private.0 as usize..self.private.1 as usize; - parse_dict(self.data, range, blend, sink).is_some() - } - - pub(super) fn parse(&self, coords: &[i16], sink: &mut impl GlyphSink) -> bool { - if self.range.0 == self.range.1 { - return true; - } - let mut state = ParseState { - open: false, - have_width: false, - stem_count: 0, - vsindex: self.vsindex, - }; - let blend = if self.proxy.vstore != 0 && !coords.is_empty() { - Some(BlendData::new(self.data, self.proxy.vstore, coords)) - } else { - None - }; - let mut blend_state = BlendState::new(); - let range = self.range.0 as usize..self.range.1 as usize; - let mut stack = Stack::new(); - self.parse_imp( - &mut state, - blend, - &mut blend_state, - &mut stack, - range, - 0, - 0., - 0., - sink, - ) - .is_some() - } - - fn parse_imp( - &self, - s: &mut ParseState, - blend: Option, - blend_state: &mut BlendState, - stack: &mut Stack, - range: Range, - depth: u32, - mut x: f32, - mut y: f32, - b: &mut impl GlyphSink, - ) -> Option<(f32, f32)> { - if depth > 10 { - return None; - } - use opcodes::*; - let mut c = Stream::with_range(self.data, range)?; - while c.remaining() != 0 { - let mut op = c.read::()? as u16; - if op == ESCAPE { - let b1 = c.read::()?; - op = b1 as u16 | 12 << 8; - } - if TRACE && op < 38 { - //println!("{}", opcodes::NAMES[op as usize]); - } - match op { - 0 | 2 | 9 | 13 | 17 => { - return None; - } - VSINDEX => { - if !self.proxy.is_cff2 { - return None; - } - if stack.is_empty() { - return None; - } - s.vsindex = stack.pop() as u16; - } - BLEND => { - if !self.proxy.is_cff2 { - return None; - } - let mut total = stack.len(); - if total < 1 { - return None; - } - total -= 1; - let count = stack.pop() as usize; - if count > total { - return None; - } - if let Some(ref blend) = blend { - total = blend_state.apply( - blend, - s.vsindex, - &mut stack.elements[0..total], - count, - )?; - } - stack.top -= total - count; - } - SHORTINT => { - stack.push(c.read::()? as f32)?; - } - 32..=246 => { - stack.push(op as f32 - 139.)?; - } - 247..=250 => { - let b1 = c.read::()? as f32; - stack.push((op as f32 - 247.) * 256. + b1 + 108.)?; - } - 251..=254 => { - let b1 = c.read::()? as f32; - stack.push(-((op as f32 - 251.) * 256. + b1) - 108.)?; - } - 255 => { - stack.push(c.read::()? as f32 / 65536.)?; - } - RETURN => { - break; - } - ENDCHAR => { - if !stack.is_empty() && !s.have_width { - s.have_width = true; - stack.clear(); - } - if s.open { - s.open = false; - b.close(); - } - break; - } - HSTEM | VSTEM | HSTEMHM | VSTEMHM => { - let mut i = 0; - let len = if stack.is_odd() && !s.have_width { - s.have_width = true; - i = 1; - stack.len() - 1 - } else { - stack.len() - }; - let horz = op == HSTEM || op == HSTEMHM; - let mut u = 0.; - while i < stack.len() { - u += stack.get(i); - let w = stack.get(i + 1); - let v = u + w; - if horz { - b.hstem(u, v); - } else { - b.vstem(u, v); - } - u = v; - i += 2; - } - s.stem_count += len / 2; - stack.clear(); - } - HINTMASK | CNTRMASK => { - let mut i = 0; - let len = if stack.is_odd() && !s.have_width { - s.have_width = true; - i = 1; - stack.len() - 1 - } else { - stack.len() - }; - let mut u = 0.; - while i < stack.len() { - u += stack.get(i); - let w = stack.get(i + 1); - let v = u + w; - b.vstem(u, v); - u = v; - i += 2; - } - s.stem_count += len / 2; - let count = (s.stem_count + 7) / 8; - let mask = c.read_bytes(count)?; - if op == HINTMASK { - b.hint_mask(mask); - } else { - b.counter_mask(mask); - } - stack.clear(); - } - RMOVETO => { - let mut i = 0; - if stack.len() == 3 && !s.have_width { - s.have_width = true; - i = 1; - } else if stack.len() < 2 { - return None; - } - if !s.open { - s.open = true; - } else { - b.close(); - } - x += stack.get(i); - y += stack.get(i + 1); - b.move_to(x, y); - stack.clear(); - } - HMOVETO | VMOVETO => { - let mut i = 0; - if stack.len() == 2 && !s.have_width { - s.have_width = true; - i = 1; - } else if stack.is_empty() { - return None; - } - if !s.open { - s.open = true; - } else { - b.close(); - } - if op == HMOVETO { - x += stack.get(i); - } else { - y += stack.get(i); - } - b.move_to(x, y); - stack.clear(); - } - RLINETO => { - if stack.is_odd() { - return None; - } - let mut i = 0; - while i < stack.len() { - x += stack.get(i); - y += stack.get(i + 1); - b.line_to(x, y); - i += 2; - } - stack.clear(); - } - HLINETO => { - let mut i = 0; - while i < stack.len() { - x += stack.get(i); - i += 1; - b.line_to(x, y); - if i == stack.len() { - break; - } - y += stack.get(i); - i += 1; - b.line_to(x, y); - } - stack.clear(); - } - VLINETO => { - let mut i = 0; - while i < stack.len() { - y += stack.get(i); - i += 1; - b.line_to(x, y); - if i == stack.len() { - break; - } - x += stack.get(i); - i += 1; - b.line_to(x, y); - } - stack.clear(); - } - RRCURVETO => { - if stack.len() % 6 != 0 { - return None; - } - let mut i = 0; - while i < stack.len() { - let x1 = x + stack.get(i); - let y1 = y + stack.get(i + 1); - let x2 = x1 + stack.get(i + 2); - let y2 = y1 + stack.get(i + 3); - x = x2 + stack.get(i + 4); - y = y2 + stack.get(i + 5); - b.curve_to(x1, y1, x2, y2, x, y); - i += 6; - } - stack.clear(); - } - RCURVELINE => { - if stack.len() < 8 || (stack.len() - 2) % 6 != 0 { - return None; - } - let mut i = 0; - while i < stack.len() - 2 { - let x1 = x + stack.get(i); - let y1 = y + stack.get(i + 1); - let x2 = x1 + stack.get(i + 2); - let y2 = y1 + stack.get(i + 3); - x = x2 + stack.get(i + 4); - y = y2 + stack.get(i + 5); - b.curve_to(x1, y1, x2, y2, x, y); - i += 6; - } - x += stack.get(i); - y += stack.get(i + 1); - b.line_to(x, y); - stack.clear(); - } - RLINECURVE => { - if stack.len() < 8 || (stack.len() - 6) & 1 != 0 { - return None; - } - let mut i = 0; - while i < stack.len() - 6 { - x += stack.get(i); - y += stack.get(i + 1); - b.line_to(x, y); - i += 2; - } - let x1 = x + stack.get(i); - let y1 = y + stack.get(i + 1); - let x2 = x1 + stack.get(i + 2); - let y2 = y1 + stack.get(i + 3); - x = x2 + stack.get(i + 4); - y = y2 + stack.get(i + 5); - b.curve_to(x1, y1, x2, y2, x, y); - stack.clear(); - } - VVCURVETO => { - let mut i = 0; - if stack.is_odd() { - x += stack.get(0); - i += 1; - } - if (stack.len() - i) % 4 != 0 { - return None; - } - while i < stack.len() { - let x1 = x; - let y1 = y + stack.get(i); - let x2 = x1 + stack.get(i + 1); - let y2 = y1 + stack.get(i + 2); - x = x2; - y = y2 + stack.get(i + 3); - b.curve_to(x1, y1, x2, y2, x, y); - i += 4; - } - stack.clear(); - } - HHCURVETO => { - let mut i = 0; - if stack.is_odd() { - y += stack.get(0); - i += 1; - } - if (stack.len() - i) % 4 != 0 { - return None; - } - while i < stack.len() { - let x1 = x + stack.get(i); - let y1 = y; - let x2 = x1 + stack.get(i + 1); - let y2 = y1 + stack.get(i + 2); - x = x2 + stack.get(i + 3); - y = y2; - b.curve_to(x1, y1, x2, y2, x, y); - i += 4; - } - stack.clear(); - } - VHCURVETO => { - if stack.len() < 4 { - return None; - } - stack.reverse(); - while !stack.is_empty() { - if stack.len() < 4 { - return None; - } - let x1 = x; - let y1 = y + stack.pop(); - let x2 = x1 + stack.pop(); - let y2 = y1 + stack.pop(); - x = x2 + stack.pop(); - y = y2 + if stack.len() == 1 { stack.pop() } else { 0.0 }; - b.curve_to(x1, y1, x2, y2, x, y); - if stack.is_empty() { - break; - } - if stack.len() < 4 { - return None; - } - let x1 = x + stack.pop(); - let y1 = y; - let x2 = x1 + stack.pop(); - let y2 = y1 + stack.pop(); - y = y2 + stack.pop(); - x = x2 + if stack.len() == 1 { stack.pop() } else { 0.0 }; - b.curve_to(x1, y1, x2, y2, x, y); - } - debug_assert!(stack.is_empty()); - } - HVCURVETO => { - if stack.len() < 4 { - return None; - } - stack.reverse(); - while !stack.is_empty() { - if stack.len() < 4 { - return None; - } - let x1 = x + stack.pop(); - let y1 = y; - let x2 = x1 + stack.pop(); - let y2 = y1 + stack.pop(); - y = y2 + stack.pop(); - x = x2 + if stack.len() == 1 { stack.pop() } else { 0.0 }; - b.curve_to(x1, y1, x2, y2, x, y); - if stack.is_empty() { - break; - } - if stack.len() < 4 { - return None; - } - let x1 = x; - let y1 = y + stack.pop(); - let x2 = x1 + stack.pop(); - let y2 = y1 + stack.pop(); - x = x2 + stack.pop(); - y = y2 + if stack.len() == 1 { stack.pop() } else { 0.0 }; - b.curve_to(x1, y1, x2, y2, x, y); - } - debug_assert!(stack.is_empty()); - } - HFLEX => { - if stack.len() != 7 { - return None; - } - let dx1 = x + stack.get(0); - let dy1 = y; - let dx2 = dx1 + stack.get(1); - let dy2 = dy1 + stack.get(2); - let dx3 = dx2 + stack.get(3); - let dy3 = dy2; - let dx4 = dx3 + stack.get(4); - let dy4 = dy2; - let dx5 = dx4 + stack.get(5); - let dy5 = y; - x = dx5 + stack.get(6); - b.curve_to(dx1, dy1, dx2, dy2, dx3, dy3); - b.curve_to(dx4, dy4, dx5, dy5, x, y); - stack.clear(); - } - FLEX => { - if stack.len() != 13 { - return None; - } - let dx1 = x + stack.get(0); - let dy1 = y + stack.get(1); - let dx2 = dx1 + stack.get(2); - let dy2 = dy1 + stack.get(3); - let dx3 = dx2 + stack.get(4); - let dy3 = dy2 + stack.get(5); - let dx4 = dx3 + stack.get(6); - let dy4 = dy3 + stack.get(7); - let dx5 = dx4 + stack.get(8); - let dy5 = dy4 + stack.get(9); - x = dx5 + stack.get(10); - y = dy5 + stack.get(11); - b.curve_to(dx1, dy1, dx2, dy2, dx3, dy3); - b.curve_to(dx4, dy4, dx5, dy5, x, y); - stack.clear(); - } - HFLEX1 => { - if stack.len() != 9 { - return None; - } - let dx1 = x + stack.get(0); - let dy1 = y + stack.get(1); - let dx2 = dx1 + stack.get(2); - let dy2 = dy1 + stack.get(3); - let dx3 = dx2 + stack.get(4); - let dy3 = dy2; - let dx4 = dx3 + stack.get(5); - let dy4 = dy2; - let dx5 = dx4 + stack.get(6); - let dy5 = dy4 + stack.get(7); - x = dx5 + stack.get(8); - b.curve_to(dx1, dy1, dx2, dy2, dx3, dy3); - b.curve_to(dx4, dy4, dx5, dy5, x, y); - stack.clear(); - } - FLEX1 => { - if stack.len() != 11 { - return None; - } - let dx1 = x + stack.get(0); - let dy1 = y + stack.get(1); - let dx2 = dx1 + stack.get(2); - let dy2 = dy1 + stack.get(3); - let dx3 = dx2 + stack.get(4); - let dy3 = dy2 + stack.get(5); - let dx4 = dx3 + stack.get(6); - let dy4 = dy3 + stack.get(7); - let dx5 = dx4 + stack.get(8); - let dy5 = dy4 + stack.get(9); - fn abs(x: f32) -> f32 { - if x < 0. { - -x - } else { - x - } - } - if abs(dx5 - x) > abs(dy5 - y) { - x = dx5 + stack.get(10); - } else { - y = dy5 + stack.get(10); - } - b.curve_to(dx1, dy1, dx2, dy2, dx3, dy3); - b.curve_to(dx4, dy4, dx5, dy5, x, y); - stack.clear(); - } - CALLSUBR | CALLGSUBR => { - if stack.is_empty() { - return None; - } - let data = self.data; - let count = if op == CALLSUBR { - self.subrs.count(data) - } else { - self.proxy.gsubrs.count(data) - }; - let bias = if count < 1240 { - 107 - } else if count < 33900 { - 1131 - } else { - 32768 - }; - let index = stack.pop() as i32 + bias; - let range = if op == CALLSUBR { - self.subrs.get_range(data, index as u32)? - } else { - self.proxy.gsubrs.get_range(data, index as u32)? - }; - let pos = - self.parse_imp(s, blend, blend_state, stack, range, depth + 1, x, y, b)?; - x = pos.0; - y = pos.1; - } - _ => { - return None; - } - } - } - Some((x, y)) - } -} - -#[derive(Default)] -struct PathBounds { - some: bool, - xmin: f32, - xmax: f32, - ymin: f32, - ymax: f32, -} - -impl PathBounds { - fn insert(&mut self, x: f32, y: f32) { - if x < self.xmin { - self.xmin = x; - } - if x > self.xmax { - self.xmax = x; - } - if y < self.ymin { - self.ymin = y; - } - if y > self.ymax { - self.ymax = y; - } - } -} - -impl GlyphSink for PathBounds { - fn move_to(&mut self, x: f32, y: f32) { - self.some = true; - self.insert(x, y); - } - - fn line_to(&mut self, x: f32, y: f32) { - self.some = true; - self.insert(x, y); - } - - fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) { - self.some = true; - self.insert(cx1, cy1); - self.insert(cx2, cy2); - self.insert(x, y); - } - - fn close(&mut self) {} -} - -#[derive(Copy, Clone)] -struct Transform { - pub xx: f32, - pub xy: f32, - pub yx: f32, - pub yy: f32, - pub x: f32, - pub y: f32, -} - -impl Transform { - fn scale(scale: f32) -> Self { - Self { - xx: scale, - xy: 0., - yx: 0., - yy: scale, - x: 0., - y: 0., - } - } - - #[inline(always)] - fn transform_point(&self, x: f32, y: f32) -> (f32, f32) { - ( - (x * self.xx + y * self.yx) + self.x, - (x * self.xy + y * self.yy) + self.y, - ) - } - - #[allow(clippy::suspicious_operation_groupings)] - fn combine(a: &Transform, b: &Transform) -> Self { - let xx = a.xx * b.xx + a.yx * b.xy; - let yx = a.xx * b.yx + a.yx * b.yy; - let xy = a.xy * b.xx + a.yy * b.xy; - let yy = a.xy * b.yx + a.yy * b.yy; - let x = a.x * b.xx + a.y * b.xy + b.x; - let y = a.x * b.yx + a.y * b.yy + b.y; - Self { - xx, - xy, - yx, - yy, - x, - y, - } - } -} - -struct TransformSink<'a, S> { - xform: Transform, - inner: &'a mut S, -} - -impl<'a, S: GlyphSink> GlyphSink for TransformSink<'a, S> { - fn move_to(&mut self, x: f32, y: f32) { - let (x, y) = self.xform.transform_point(x, y); - self.inner.move_to(x, y); - } - - fn line_to(&mut self, x: f32, y: f32) { - let (x, y) = self.xform.transform_point(x, y); - self.inner.line_to(x, y); - } - - fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) { - let (cx1, cy1) = self.xform.transform_point(cx1, cy1); - let (cx2, cy2) = self.xform.transform_point(cx2, cy2); - let (x, y) = self.xform.transform_point(x, y); - self.inner.curve_to(cx1, cy1, cx2, cy2, x, y); - } - - fn close(&mut self) { - self.inner.close(); - } -} - -struct ScaleSink<'a, S> { - scale: f32, - inner: &'a mut S, -} - -impl<'a, S: GlyphSink> GlyphSink for ScaleSink<'a, S> { - fn move_to(&mut self, x: f32, y: f32) { - let s = self.scale; - self.inner.move_to(x * s, y * s); - } - - fn line_to(&mut self, x: f32, y: f32) { - let s = self.scale; - self.inner.line_to(x * s, y * s); - } - - fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) { - let s = self.scale; - self.inner - .curve_to(cx1 * s, cy1 * s, cx2 * s, cy2 * s, x * s, y * s); - } - - fn close(&mut self) { - self.inner.close(); - } -} - -/// Trait for receiving the path commands of a glyph. -#[allow(unused_variables)] -pub trait GlyphSink { - fn hstem(&mut self, y: f32, dy: f32) {} - fn vstem(&mut self, x: f32, dx: f32) {} - fn hint_mask(&mut self, mask: &[u8]) {} - fn counter_mask(&mut self, mask: &[u8]) {} - fn move_to(&mut self, x: f32, y: f32); - fn line_to(&mut self, x: f32, y: f32); - fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32); - fn close(&mut self); -} - -/// A trait that acts as a callback during dictionary parsing. -#[allow(unused_variables)] -pub trait DictionarySink { - /// Specifies the char string type. Only type 2 is supported. - fn char_string_type(&mut self, ty: u32) {} - - /// Specifies the offset to the char string index. - fn char_strings(&mut self, offset: usize) {} - - /// Specifies the offset and length of the private dictionary. - fn private_dictionary(&mut self, offset: usize, len: usize) {} - - /// Specifies the hinting alignment zones. - fn blue_values(&mut self, values: &[f32]) {} - - /// Specifies the hinting alignment zones for the font family. - fn family_blues(&mut self, values: &[f32]) {} - - /// Specifies other hinting alignment zones. - fn other_blues(&mut self, values: &[f32]) {} - - /// Specifies other hinting alignment zones for the font family. - fn family_other_blues(&mut self, values: &[f32]) {} - - /// Specifies a value for overshoot suppression. - fn blue_scale(&mut self, scale: f32) {} - - /// Specifies a value for small overshoot supression. - fn blue_shift(&mut self, shift: f32) {} - - /// Specifies an alignment zone extension. - fn blue_fuzz(&mut self, fuzz: f32) {} - - /// Specifies the offset to the local subroutine index. - fn subroutines(&mut self, offset: usize) {} - - /// Specifies the offset to the font dictionary index. - fn fd_array(&mut self, offset: usize) {} - - /// Specifies the offset to the font dictionary selector table. - fn fd_select(&mut self, offset: usize) {} - - fn font_matrix(&mut self, values: &[f32]) {} - - fn language_group(&mut self, group: u32) {} - - /// Specifies the offset to the variation store. - fn variation_store(&mut self, offset: usize) {} - - /// Specifies the outer index in the variation store. - fn variation_store_index(&mut self, index: u16) {} -} - -struct Loader { - char_strings: usize, - subrs: usize, - private: Range, - fd_select: usize, - fd_array: usize, - font_matrix: Option, - vstore: usize, - vsindex: u16, - ok: bool, - units_per_em: u16, -} - -impl DictionarySink for Loader { - fn char_string_type(&mut self, ty: u32) { - if ty != 2 { - self.ok = false; - } - } - - fn char_strings(&mut self, offset: usize) { - self.char_strings = offset; - } - - fn private_dictionary(&mut self, offset: usize, len: usize) { - self.private = offset..offset + len; - } - - fn subroutines(&mut self, offset: usize) { - self.subrs = self.private.start + offset; - } - - fn fd_select(&mut self, offset: usize) { - self.fd_select = offset; - } - - fn fd_array(&mut self, offset: usize) { - self.fd_array = offset; - } - - fn font_matrix(&mut self, values: &[f32]) { - if values.len() == 6 { - self.font_matrix = Some(Transform { - xx: values[0] * self.units_per_em as f32, - xy: values[1] * self.units_per_em as f32, - yx: values[2] * self.units_per_em as f32, - yy: values[3] * self.units_per_em as f32, - x: values[4], - y: values[5], - }); - } - } - - fn variation_store(&mut self, offset: usize) { - self.vstore = offset; - } - - fn variation_store_index(&mut self, index: u16) { - self.vsindex = index; - } -} - -struct IndexMetadata { - count: u32, - offsets_offset: u32, - data_offset: u32, - offset_size: u32, -} - -impl IndexMetadata { - pub fn unpack(data: &[u8], key: u32) -> Option { - let is_cff2 = key & 1 != 0; - let offset = key >> 1; - let b = Bytes::with_offset(data, offset as usize)?; - if is_cff2 { - let count = b.read::(0)?; - if count == 0 { - Some(Self { - count: 0, - offsets_offset: 4, - data_offset: 0, - offset_size: 0, - }) - } else { - let offset_size = b.read::(4)? as u32; - let offsets_offset = offset + 5; - let data_offset = 5 + offset_size * (count + 1); - Some(Self { - count, - offsets_offset, - data_offset, - offset_size, - }) - } - } else { - let count = b.read::(0)? as u32; - if count == 0 { - Some(Self { - count: 0, - offsets_offset: 2, - data_offset: 0, - offset_size: 0, - }) - } else { - let offset_size = b.read::(2)? as u32; - let offsets_offset = offset + 3; - let data_offset = 3 + offset_size * (count + 1); - Some(Self { - count, - offsets_offset, - data_offset, - offset_size, - }) - } - } - } -} - -#[derive(Copy, Clone, Default)] -struct Index { - key: u32, -} - -impl Index { - fn empty() -> Self { - Self { key: 0 } - } - - fn new(data: &[u8], offset: u32) -> Option { - let key = offset << 1; - IndexMetadata::unpack(data, key)?; - Some(Self { key }) - } - - fn new2(data: &[u8], offset: u32) -> Option { - let key = (offset << 1) | 1; - IndexMetadata::unpack(data, key)?; - Some(Self { key }) - } - - #[inline(always)] - fn get_offset(&self, data: &[u8], meta: &IndexMetadata, index: u32) -> Option { - let offsize = meta.offset_size; - let base = (meta.offsets_offset + offsize * index) as usize; - let b = Bytes::new(data); - match offsize { - 1 => Some(b.read::(base)? as usize), - 2 => Some(b.read::(base)? as usize), - 3 => Some(b.read::(base)?.0 as usize), - 4 => Some(b.read::(base)? as usize), - _ => None, - } - } - - fn count(&self, data: &[u8]) -> u32 { - let is_cff2 = self.key & 1 != 0; - if is_cff2 { - Bytes::new(data).read_or_default::((self.key >> 1) as usize) - } else { - Bytes::new(data).read_or_default::((self.key >> 1) as usize) as u32 - } - } - - fn len(&self, data: &[u8]) -> Option { - let meta = IndexMetadata::unpack(data, self.key)?; - if meta.count == 0 { - return Some(meta.offsets_offset as usize); - } - let last = self.get_offset(data, &meta, meta.count)?; - Some(meta.data_offset as usize + last - 1) - } - - fn get_range(&self, data: &[u8], index: u32) -> Option> { - let meta = IndexMetadata::unpack(data, self.key)?; - if index >= meta.count { - return None; - } - let base = ((self.key >> 1) + meta.data_offset) as usize; - let offset1 = self.get_offset(data, &meta, index)?; - let offset2 = self.get_offset(data, &meta, index + 1)?; - let start = base + offset1 - 1; - let end = base + offset2 - 1; - if end < start || end > data.len() { - return None; - } - Some(start..end) - } -} - -#[derive(Copy, Clone)] -struct BlendData<'a> { - store: Bytes<'a>, - coords: &'a [i16], -} - -impl<'a> BlendData<'a> { - fn new(data: &'a [u8], store: u32, coords: &'a [i16]) -> Self { - let store = Bytes::with_offset(data, store as usize).unwrap_or_else(|| Bytes::new(&[])); - Self { store, coords } - } -} - -#[derive(Copy, Clone)] -struct BlendState { - scalars: [f32; 32], - region_count: usize, - index: u16, - present: bool, -} - -impl BlendState { - fn new() -> Self { - Self { - scalars: [0f32; 32], - region_count: 0, - index: 0, - present: false, - } - } - - fn apply( - &mut self, - data: &BlendData, - index: u16, - values: &mut [f32], - count: usize, - ) -> Option { - let len = values.len(); - if count >= len { - return None; - } - if !self.present || index != self.index { - let (ok, region_count) = Self::compute_scalars(data, index, &mut self.scalars)?; - self.region_count = region_count; - self.index = index; - self.present = true; - if !ok { - for i in 0..region_count { - self.scalars[i] = 0.; - } - } - } - let region_count = self.region_count; - let region_value_count = region_count * count; - let total_count = region_value_count + count; - if total_count > len { - return None; - } - let base = len - total_count; - let mut region_idx = base + count; - for i in 0..count { - let mut delta = 0.; - for j in 0..region_count { - delta += self.scalars[j] * values[region_idx + j]; - } - region_idx += region_count; - values[base + i] += delta; - } - Some(total_count) - } - - #[allow(clippy::needless_range_loop)] - fn compute_scalars(data: &BlendData, outer: u16, scalars: &mut [f32]) -> Option<(bool, usize)> { - let b = &data.store; - let vary_coords = data.coords; - let coords_len = vary_coords.len(); - let store = 0; - if outer >= b.read::(store + 6)? { - return None; - } - let region_base = store + b.read::(store + 2)? as usize; - let axis_count = b.read::(region_base)? as usize; - let region_record_size = axis_count * 6; - let region_count = b.read::(region_base + 2)? as usize; - let data_base = store + b.read::(store + 8 + outer as usize * 4)? as usize; - let region_index_base = data_base + 6; - let region_index_count = b.read::(data_base + 4)? as usize; - if region_index_count > scalars.len() { - return Some((false, region_index_count)); - } - for i in 0..region_index_count { - let region_index = b.read::(region_index_base + i * 2)? as usize; - if region_index >= region_count { - return None; - } - let region_offset = region_base + 4 + region_index * region_record_size; - let mut scalar = 1.; - for axis in 0..axis_count { - fn f2dot14_to_f32(x: i16) -> f32 { - ((x as i32) << 2) as f32 / 65536. - } - let region_axis_base = region_offset + axis * 6; - let start = f2dot14_to_f32(b.read::(region_axis_base)?); - let peak = f2dot14_to_f32(b.read::(region_axis_base + 2)?); - let end = f2dot14_to_f32(b.read::(region_axis_base + 4)?); - let coord = if axis >= coords_len { - 0. - } else { - f2dot14_to_f32(vary_coords[axis]) - }; - if (start > peak || peak > end) - || (start < 0. && end > 0. && peak != 0.) - || peak == 0. - { - continue; - } else if coord < start || coord > end { - scalar = 0.; - break; - } else if coord == peak { - continue; - } else if coord < peak { - scalar *= (coord - start) / (peak - start) - } else { - scalar *= (end - coord) / (end - peak) - }; - } - scalars[i] = scalar; - } - Some((true, region_index_count)) - } -} - -struct ParseState { - open: bool, - have_width: bool, - stem_count: usize, - vsindex: u16, -} - -const MAX_STACK: usize = 513; - -#[derive(Copy, Clone)] -struct Stack { - elements: [f32; MAX_STACK], - top: usize, -} - -impl Stack { - fn new() -> Self { - Self { - elements: [0.; MAX_STACK], - top: 0, - } - } - - fn push(&mut self, value: f32) -> Option<()> { - if self.top == MAX_STACK { - return None; - } - self.elements[self.top] = value; - self.top += 1; - Some(()) - } - - fn pop(&mut self) -> f32 { - self.top -= 1; - self.elements[self.top] - } - - fn len(&self) -> usize { - self.top - } - - fn is_empty(&self) -> bool { - self.top == 0 - } - - fn is_odd(&self) -> bool { - self.top & 1 == 1 - } - - fn get(&self, index: usize) -> f32 { - self.elements[index] - } - - fn reverse(&mut self) { - self.elements.split_at_mut(self.top).0.reverse(); - } - - fn clear(&mut self) { - self.top = 0; - } -} - -fn parse_dict( - data: &[u8], - range: Range, - blend: Option, - sink: &mut Sink, -) -> Option<()> { - use opcodes::*; - if range.is_empty() { - return Some(()); - } - let mut s = Stream::with_range(data, range)?; - let mut operands = [0f32; MAX_STACK]; - let mut blend_state = BlendState::new(); - let mut vsindex = 0u16; - loop { - let (mut op, mut n) = parse_dict_entry(&mut s, &mut operands)?; - if op == DICT_BLEND { - if n == 0 { - return None; - } - let count = operands[n - 1] as usize; - if let Some(ref blend) = blend { - blend_state.apply(blend, vsindex, &mut operands[0..n - 1], count); - n = count; - } else { - n = count; - } - let (op2, n2) = parse_dict_entry(&mut s, &mut operands)?; - if n2 != 0 { - return None; - } - op = op2; - } - let ops = &mut operands[0..n]; - match op { - 0xFFFF => break, - CHAR_STRING_TYPE => { - if n != 1 { - return None; - } - sink.char_string_type(ops[0] as u32); - } - CHAR_STRINGS => { - if n != 1 { - return None; - } - sink.char_strings(ops[0] as usize); - } - PRIVATE => { - if n != 2 { - return None; - } - sink.private_dictionary(ops[1] as usize, ops[0] as usize); - } - FD_ARRAY => { - if n != 1 { - return None; - } - sink.fd_array(ops[0] as usize); - } - FD_SELECT => { - if n != 1 { - return None; - } - sink.fd_select(ops[0] as usize); - } - FONT_MATRIX => { - if n == 6 { - sink.font_matrix(ops); - } - } - LANGUAGE_GROUP => { - sink.language_group(ops[0] as u32); - } - BLUE_VALUES => { - delta_vector(ops); - sink.blue_values(ops); - } - OTHER_BLUES => { - delta_vector(ops); - sink.other_blues(ops); - } - FAMILY_BLUES => { - delta_vector(ops); - sink.family_blues(ops); - } - FAMILY_OTHER_BLUES => { - delta_vector(ops); - sink.family_other_blues(ops); - } - BLUE_SCALE => { - if n != 1 { - return None; - } - sink.blue_scale(ops[0]); - } - BLUE_SHIFT => { - if n != 1 { - return None; - } - sink.blue_shift(ops[0]); - } - BLUE_FUZZ => { - if n != 1 { - return None; - } - sink.blue_fuzz(ops[0]); - } - DICT_VSINDEX => { - if n != 1 { - return None; - } - vsindex = ops[0] as u16; - } - DICT_BLEND => {} - VSTORE => { - if n != 1 { - return None; - } - sink.variation_store(ops[0] as usize); - } - STD_HW => {} - STD_VW => {} - STEM_SNAP_H => {} - STEM_SNAP_V => {} - SUBRS => { - if n != 1 { - return None; - } - sink.subroutines(ops[0] as usize); - } - _ => {} - } - } - Some(()) -} - -fn delta_vector(ops: &mut [f32]) { - if ops.is_empty() { - return; - } - let mut s = ops[0]; - for v in ops.iter_mut().skip(1) { - s += *v; - *v = s; - } -} - -fn parse_dict_entry(s: &mut Stream, operands: &mut [f32]) -> Option<(u16, usize)> { - let max_operands = operands.len(); - let mut n = 0; - loop { - if s.remaining() == 0 { - return Some((0xFFFF, 0)); - } - let b0 = s.read::()?; - if b0 <= 24 { - let mut b1 = 0u8; - if b0 == 12 { - b1 = s.read()?; - } - return Some(((b0 as u16) << 8 | b1 as u16, n)); - } - match b0 { - 28 | 29 | 32..=254 => { - if n == max_operands { - return None; - } - operands[n] = parse_integer(s, b0 as i32)?; - n += 1; - } - 30 => { - if n == max_operands { - return None; - } - operands[n] = parse_real(s)?; - n += 1; - } - _ => { - return None; - } - } - } -} - -#[inline(always)] -fn parse_integer(s: &mut Stream, b0: i32) -> Option { - match b0 { - 28 => Some(s.read::()? as f32), - 29 => Some(s.read::()? as f32), - 32..=246 => Some((b0 - 139) as f32), - 247..=250 => Some(((b0 - 247) * 256 + s.read::()? as i32 + 108) as f32), - 251..=254 => Some((-(b0 - 251) * 256 - s.read::()? as i32 - 108) as f32), - _ => None, - } -} - -fn parse_real(s: &mut Stream) -> Option { - const MAX_LEN: usize = 64; - let mut buf = [0u8; MAX_LEN]; - let mut n = 0; - let mut done = false; - let mut missing_exp = true; - loop { - let b = s.read::()?; - let n0 = (b >> 4) & 0xF; - let n1 = b & 0xF; - for j in 0..2 { - if n + 1 == MAX_LEN { - return None; - } - let nibble = if j == 0 { n0 } else { n1 }; - match nibble { - 0x0..=0x9 => { - buf[n] = b'0' + nibble; - n += 1; - missing_exp = false; - } - 0xA => { - buf[n] = b'.'; - n += 1; - } - 0xB => { - buf[n] = b'E'; - n += 1; - missing_exp = true; - } - 0xC => { - buf[n] = b'E'; - buf[n + 1] = b'-'; - n += 2; - missing_exp = true; - } - 0xE => { - buf[n] = b'-'; - n += 1; - } - 0xF => { - done = true; - break; - } - _ => { - return None; - } - } - } - if done { - break; - } - } - if missing_exp && n < MAX_LEN { - buf[n] = b'0'; - n += 1; - } - core::str::from_utf8(&buf[0..n]).map_or(None, |b| b.parse::().ok()) -} - -fn parse_fd_select(data: &[u8], offset: u32, glyph_id: u16) -> Option { - if offset == 0 { - return Some(0); - } - let d = Bytes::new(data); - let base = offset as usize; - let fmt = d.read::(base)?; - if fmt == 0 { - return Some(d.read::(base + 1 + glyph_id as usize)? as usize); - } else if fmt == 3 { - let nranges = d.read::(base + 1)? as usize; - let mut l = 0; - let mut h = nranges; - while l < h { - let i = (l + h) / 2; - let rec = base + 3 + i * 3; - let first = d.read::(rec)?; - if glyph_id < first { - h = i; - } else if glyph_id >= d.read::(rec + 3)? { - l = i + 1; - } else { - return Some(d.read::(rec + 2)? as usize); - } - } - } else if fmt == 4 { - let nranges = d.read::(base + 1)? as usize; - let mut l = 0; - let mut h = nranges; - while l < h { - let i = (l + h) / 2; - let rec = base + 5 + i * 6; - let first = d.read::(rec)?; - if (glyph_id as u32) < first { - h = i; - } else if glyph_id as u32 > d.read::(rec + 6)? { - l = i + 1; - } else { - return Some(d.read::(rec + 4)? as usize); - } - } - } - None -} - -#[allow(dead_code)] -mod opcodes { - // top - pub const CHAR_STRING_TYPE: u16 = 12 << 8 | 6; - pub const CHAR_STRINGS: u16 = 17 << 8; - pub const PRIVATE: u16 = 18 << 8; - pub const FD_ARRAY: u16 = 12 << 8 | 36; - pub const FD_SELECT: u16 = 12 << 8 | 37; - pub const FONT_MATRIX: u16 = 12 << 8 | 7; - - // private - pub const BLUE_VALUES: u16 = 6 << 8; - pub const OTHER_BLUES: u16 = 7 << 8; - pub const FAMILY_BLUES: u16 = 8 << 8; - pub const FAMILY_OTHER_BLUES: u16 = 9 << 8; - pub const BLUE_SCALE: u16 = 12 << 8 | 9; - pub const BLUE_SHIFT: u16 = 12 << 8 | 10; - pub const BLUE_FUZZ: u16 = 12 << 8 | 11; - pub const STD_HW: u16 = 10 << 8; - pub const STD_VW: u16 = 11 << 8; - pub const STEM_SNAP_H: u16 = 12 << 8 | 12; - pub const STEM_SNAP_V: u16 = 12 << 8 | 13; - pub const LANGUAGE_GROUP: u16 = 12 << 8 | 17; - pub const SUBRS: u16 = 19 << 8; - pub const DEFAULT_WIDTH_X: u16 = 20 << 8; - pub const NOMINAL_WIDTH_X: u16 = 21 << 8; - pub const DICT_VSINDEX: u16 = 22 << 8; - pub const DICT_BLEND: u16 = 23 << 8; - pub const VSTORE: u16 = 24 << 8; - - // char strings - pub const HSTEM: u16 = 1; - pub const VSTEM: u16 = 3; - pub const VMOVETO: u16 = 4; - pub const RLINETO: u16 = 5; - pub const HLINETO: u16 = 6; - pub const VLINETO: u16 = 7; - pub const RRCURVETO: u16 = 8; - pub const CALLSUBR: u16 = 10; - pub const RETURN: u16 = 11; - pub const ESCAPE: u16 = 12; - pub const ENDCHAR: u16 = 14; - pub const VSINDEX: u16 = 15; - pub const BLEND: u16 = 16; - pub const HSTEMHM: u16 = 18; - pub const HINTMASK: u16 = 19; - pub const CNTRMASK: u16 = 20; - pub const RMOVETO: u16 = 21; - pub const HMOVETO: u16 = 22; - pub const VSTEMHM: u16 = 23; - pub const RCURVELINE: u16 = 24; - pub const RLINECURVE: u16 = 25; - pub const VVCURVETO: u16 = 26; - pub const HHCURVETO: u16 = 27; - pub const SHORTINT: u16 = 28; - pub const CALLGSUBR: u16 = 29; - pub const VHCURVETO: u16 = 30; - pub const HVCURVETO: u16 = 31; - - // 2 bytes - pub const HFLEX: u16 = 34 | 12 << 8; - pub const FLEX: u16 = 35 | 12 << 8; - pub const HFLEX1: u16 = 36 | 12 << 8; - pub const FLEX1: u16 = 37 | 12 << 8; - - #[rustfmt::skip] - pub const NAMES: [&'static str; 38] = [ - "", "hstem", "", "vstem", "vmoveto", "rlineto", "hlineto", "vlineto", "rrcurveto", "", - "callsubr", "return", "escape", "", "endchar", "vsindex", "blend", "", "hstemhm", "hintmask", - "cntrmask", "rmoveto", "hmoveto", "vstemhm", "rcurveline", "rlinecurve", "vvcurveto", - "hhcurveto", "shortint", "callgsubr", "vhcurveto", "hvcurveto", "", "", "hflex", "flex", - "hflex1", "flex1", - ]; -} diff --git a/src/scale/cff/hint.rs b/src/scale/cff/hint.rs index 5c555e7..84aeae9 100644 --- a/src/scale/cff/hint.rs +++ b/src/scale/cff/hint.rs @@ -1,396 +1,343 @@ -use super::cff::{DictionarySink, Glyph, GlyphSink}; -use super::internal::fixed::{muldiv, Fixed}; +//! CFF hinting. -use super::TRACE; +use read_fonts::{ + tables::postscript::{charstring::CommandSink, dict::Blues}, + types::Fixed, +}; -/// Hinting state for a compact font format glyph. +// "Default values for OS/2 typoAscender/Descender.." +// See +const ICF_TOP: Fixed = Fixed::from_i32(880); +const ICF_BOTTOM: Fixed = Fixed::from_i32(-120); + +// +const MAX_BLUES: usize = 7; +const MAX_OTHER_BLUES: usize = 5; +const MAX_BLUE_ZONES: usize = MAX_BLUES + MAX_OTHER_BLUES; + +// +const MAX_HINTS: usize = 96; + +// One bit per stem hint +// +const HINT_MASK_SIZE: usize = (MAX_HINTS + 7) / 8; + +// Constant for hint adjustment and em box hint placement. +// +const MIN_COUNTER: Fixed = Fixed::from_bits(0x8000); + +// +const EPSILON: Fixed = Fixed::from_bits(1); + +/// Parameters used to generate the stem and counter zones for the hinting +/// algorithm. +#[derive(Clone)] +pub(crate) struct HintParams { + pub blues: Blues, + pub family_blues: Blues, + pub other_blues: Blues, + pub family_other_blues: Blues, + pub blue_scale: Fixed, + pub blue_shift: Fixed, + pub blue_fuzz: Fixed, + pub language_group: i32, +} + +impl Default for HintParams { + fn default() -> Self { + Self { + blues: Blues::default(), + other_blues: Blues::default(), + family_blues: Blues::default(), + family_other_blues: Blues::default(), + // See + blue_scale: Fixed::from_f64(0.039625), + blue_shift: Fixed::from_i32(7), + blue_fuzz: Fixed::ONE, + language_group: 0, + } + } +} + +/// See +#[derive(Copy, Clone, PartialEq, Default, Debug)] +struct BlueZone { + is_bottom: bool, + cs_bottom_edge: Fixed, + cs_top_edge: Fixed, + cs_flat_edge: Fixed, + ds_flat_edge: Fixed, +} + +/// Hinting state for a PostScript subfont. /// /// Note that hinter states depend on the scale, subfont index and /// variation coordinates of a glyph. They can be retained and reused /// if those values remain the same. -#[derive(Copy, Clone)] -pub struct HinterState { +#[derive(Copy, Clone, PartialEq, Default)] +pub(crate) struct HintState { scale: Fixed, - fscale: f32, - blues: BlueValues, - other_blues: BlueValues, - family_blues: BlueValues, - family_other_blues: BlueValues, blue_scale: Fixed, blue_shift: Fixed, blue_fuzz: Fixed, - language_group: u8, + language_group: i32, supress_overshoot: bool, - em_box_hints: bool, + do_em_box_hints: bool, boost: Fixed, + darken_y: Fixed, zones: [BlueZone; MAX_BLUE_ZONES], - zone_count: u16, + zone_count: usize, } -impl HinterState { - /// Creates a new hinting state for the specified glyph, scale and - /// variation coordinates. - pub fn new(glyph: &Glyph, scale: f32, coords: &[i16]) -> Self { +impl HintState { + pub fn new(params: &HintParams, scale: Fixed) -> Self { let mut state = Self { - scale: Fixed::from_f32(scale), - fscale: scale, - blues: BlueValues::new(), - other_blues: BlueValues::new(), - family_blues: BlueValues::new(), - family_other_blues: BlueValues::new(), - blue_scale: DEFAULT_BLUE_SCALE, - blue_shift: DEFAULT_BLUE_SHIFT, - blue_fuzz: DEFAULT_BLUE_FUZZ, - language_group: 0, + scale, + blue_scale: params.blue_scale, + blue_shift: params.blue_shift, + blue_fuzz: params.blue_fuzz, + language_group: params.language_group, supress_overshoot: false, - em_box_hints: false, - boost: Fixed(0), + do_em_box_hints: false, + boost: Fixed::ZERO, + darken_y: Fixed::ZERO, zones: [BlueZone::default(); MAX_BLUE_ZONES], zone_count: 0, }; - glyph.eval_private_dict(coords, &mut state); - state.initialize_zones(); + state.build_zones(params); state } - /// Returns the current scaling factor of the hinter state. - pub fn scale(&self) -> f32 { - self.fscale + fn zones(&self) -> &[BlueZone] { + &self.zones[..self.zone_count] } - fn initialize_zones(&mut self) { - self.em_box_hints = false; - if self.language_group == 1 { - if self.blues.len == 2 { - let v = self.blues.values(); - if v[0].0 < ICF_BOTTOM - && v[0].1 < ICF_BOTTOM - && v[1].0 > ICF_TOP - && v[1].1 > ICF_TOP + /// Initialize zones from the set of blues values. + /// + /// See + fn build_zones(&mut self, params: &HintParams) { + self.do_em_box_hints = false; + // + match (self.language_group, params.blues.values().len()) { + (1, 2) => { + let blues = params.blues.values(); + if blues[0].0 < ICF_BOTTOM + && blues[0].1 < ICF_BOTTOM + && blues[1].0 > ICF_TOP + && blues[1].1 > ICF_TOP { - self.em_box_hints = true; + // FreeType generates synthetic hints here. We'll do it + // later when building the hint map. + self.do_em_box_hints = true; + return; } - } else if self.blues.len == 0 { - self.em_box_hints = true; } - } - if self.em_box_hints { - return; + (1, 0) => { + self.do_em_box_hints = true; + return; + } + _ => {} } let mut zones = [BlueZone::default(); MAX_BLUE_ZONES]; - let darken_y = Fixed(0); - let mut max_height = Fixed(0); - let mut z = 0usize; - for blue in self.blues.values() { - let bottom = blue.0; - let top = blue.1; - let height = top - bottom; - if height.0 < 0 { + let mut max_zone_height = Fixed::ZERO; + let mut zone_ix = 0usize; + // Copy blues and other blues to a combined array of top and bottom zones. + for blue in params.blues.values().iter().take(MAX_BLUES) { + // FreeType loads blues as integers and then expands to 16.16 + // at initialization. We load them as 16.16 so floor them here + // to ensure we match. + // + let bottom = blue.0.floor(); + let top = blue.1.floor(); + let zone_height = top - bottom; + if zone_height < Fixed::ZERO { + // Reject zones with negative height continue; } - if height > max_height { - max_height = height; - } - let zone = &mut zones[z]; - zone.bottom = bottom; - zone.top = top; - if z == 0 { + max_zone_height = max_zone_height.max(zone_height); + let zone = &mut zones[zone_ix]; + zone.cs_bottom_edge = bottom; + zone.cs_top_edge = top; + if zone_ix == 0 { + // First blue value is bottom zone zone.is_bottom = true; - zone.flat = top; + zone.cs_flat_edge = top; } else { - zone.top += Fixed(2) * darken_y; - zone.bottom += Fixed(2) * darken_y; + // Remaining blue values are top zones zone.is_bottom = false; - zone.flat = zone.bottom; + // Adjust both edges of top zone upward by twice darkening amount + zone.cs_top_edge += twice(self.darken_y); + zone.cs_bottom_edge += twice(self.darken_y); + zone.cs_flat_edge = zone.cs_bottom_edge; } - z += 1; + zone_ix += 1; } - let units_per_pixel = Fixed::ONE / self.scale; - for blue in self.other_blues.values() { - let bottom = blue.0; - let top = blue.1; - let height = top - bottom; - if height.0 < 0 { + for blue in params.other_blues.values().iter().take(MAX_OTHER_BLUES) { + let bottom = blue.0.floor(); + let top = blue.1.floor(); + let zone_height = top - bottom; + if zone_height < Fixed::ZERO { + // Reject zones with negative height continue; } - if height > max_height { - max_height = height; - } - let zone = &mut zones[z]; - zone.bottom = bottom; - zone.top = top; + max_zone_height = max_zone_height.max(zone_height); + let zone = &mut zones[zone_ix]; + // All "other" blues are bottom zone zone.is_bottom = true; - zone.flat = top; - z += 1; + zone.cs_bottom_edge = bottom; + zone.cs_top_edge = top; + zone.cs_flat_edge = top; + zone_ix += 1; } - for zone in &mut zones[..z] { - let flat = zone.flat; + // Adjust for family blues + let units_per_pixel = Fixed::ONE / self.scale; + for zone in &mut zones[..zone_ix] { + let flat = zone.cs_flat_edge; let mut min_diff = Fixed::MAX; if zone.is_bottom { - for blue in self.family_other_blues.values() { + // In a bottom zone, the top edge is the flat edge. + // Search family other blues for bottom zones. Look for the + // closest edge that is within the one pixel threshold. + for blue in params.family_other_blues.values() { let family_flat = blue.1; let diff = (flat - family_flat).abs(); if diff < min_diff && diff < units_per_pixel { - zone.flat = family_flat; + zone.cs_flat_edge = family_flat; min_diff = diff; - if diff.0 == 0 { + if diff == Fixed::ZERO { break; } } } - if self.family_blues.len > 0 { - let family_flat = self.family_blues.values[0].1; + // Check the first member of family blues, which is a bottom + // zone + if !params.family_blues.values().is_empty() { + let family_flat = params.family_blues.values()[0].1; let diff = (flat - family_flat).abs(); if diff < min_diff && diff < units_per_pixel { - zone.flat = family_flat; + zone.cs_flat_edge = family_flat; } } } else { - for blue in self.family_blues.values().iter().skip(1) { - let family_flat = blue.0 + Fixed(2) * darken_y; + // In a top zone, the bottom edge is the flat edge. + // Search family blues for top zones, skipping the first, which + // is a bottom zone. Look for closest family edge that is + // within the one pixel threshold. + for blue in params.family_blues.values().iter().skip(1) { + let family_flat = blue.0 + twice(self.darken_y); let diff = (flat - family_flat).abs(); if diff < min_diff && diff < units_per_pixel { - zone.flat = family_flat; + zone.cs_flat_edge = family_flat; min_diff = diff; - if diff.0 == 0 { + if diff == Fixed::ZERO { break; } } } } } - if max_height.0 > 0 && self.blue_scale > (Fixed::ONE / max_height) { - self.blue_scale = Fixed::ONE / max_height; + if max_zone_height > Fixed::ZERO && self.blue_scale > (Fixed::ONE / max_zone_height) { + // Clamp at maximum scale + self.blue_scale = Fixed::ONE / max_zone_height; } + // Suppress overshoot and boost blue zones at small sizes if self.scale < self.blue_scale { self.supress_overshoot = true; - self.boost = Fixed::from_f32(0.6) - - Fixed(muldiv( - Fixed::from_f32(0.6).0, - self.scale.0, - self.blue_scale.0, - )); - if self.boost.0 > 0x7FFF { - self.boost.0 = 0x7FFF; - } + self.boost = + Fixed::from_f64(0.6) - Fixed::from_f64(0.6).mul_div(self.scale, self.blue_scale); + // boost must remain less than 0.5, or baseline could go negative + self.boost = self.boost.min(Fixed::from_bits(0x7FFF)); } - if darken_y.0 != 0 { - self.boost.0 = 0; + if self.darken_y != Fixed::ZERO { + self.boost = Fixed::ZERO; } + // Set device space alignment for each zone; apply boost amount before + // rounding flat edge let scale = self.scale; let boost = self.boost; - for zone in &mut zones[..z] { + for zone in &mut zones[..zone_ix] { let boost = if zone.is_bottom { -boost } else { boost }; - zone.ds_flat = (zone.flat * scale + boost).round(); + zone.ds_flat_edge = (zone.cs_flat_edge * scale + boost).round(); } self.zones = zones; - self.zone_count = z as u16; + self.zone_count = zone_ix; } - fn capture(&self, bottom: &mut Hint, top: &mut Hint) -> bool { + /// Check whether a hint is captured by one of the blue zones. + /// + /// See + fn capture(&self, bottom_edge: &mut Hint, top_edge: &mut Hint) -> bool { let fuzz = self.blue_fuzz; let mut captured = false; - let mut adjustment = Fixed(0); - for zone in &self.zones[..self.zone_count as usize] { + let mut adjustment = Fixed::ZERO; + for zone in self.zones() { if zone.is_bottom - && bottom.is_bottom() - && (zone.bottom - fuzz) <= bottom.coord - && bottom.coord <= (zone.top + fuzz) + && bottom_edge.is_bottom() + && (zone.cs_bottom_edge - fuzz) <= bottom_edge.cs_coord + && bottom_edge.cs_coord <= (zone.cs_top_edge + fuzz) { + // Bottom edge captured by bottom zone. adjustment = if self.supress_overshoot { - zone.ds_flat - } else if zone.top - bottom.coord >= self.blue_shift { - bottom.ds_coord.round().min(zone.ds_flat - Fixed::ONE) + zone.ds_flat_edge + } else if zone.cs_top_edge - bottom_edge.cs_coord >= self.blue_shift { + // Guarantee minimum of 1 pixel overshoot + bottom_edge + .ds_coord + .round() + .min(zone.ds_flat_edge - Fixed::ONE) } else { - bottom.ds_coord.round() - } - bottom.ds_coord; + bottom_edge.ds_coord.round() + }; + adjustment -= bottom_edge.ds_coord; captured = true; break; } if !zone.is_bottom - && top.is_top() - && (zone.bottom - fuzz) <= top.coord - && top.coord <= (zone.top + fuzz) + && top_edge.is_top() + && (zone.cs_bottom_edge - fuzz) <= top_edge.cs_coord + && top_edge.cs_coord <= (zone.cs_top_edge + fuzz) { + // Top edge captured by top zone. adjustment = if self.supress_overshoot { - zone.ds_flat - } else if top.coord - zone.bottom >= self.blue_shift { - top.ds_coord.round().max(zone.ds_flat + Fixed::ONE) + zone.ds_flat_edge + } else if top_edge.cs_coord - zone.cs_bottom_edge >= self.blue_shift { + // Guarantee minimum of 1 pixel overshoot + top_edge + .ds_coord + .round() + .max(zone.ds_flat_edge + Fixed::ONE) } else { - top.ds_coord.round() - } - top.ds_coord; + top_edge.ds_coord.round() + }; + adjustment -= top_edge.ds_coord; captured = true; break; } } if captured { - if bottom.is_valid() { - bottom.ds_coord += adjustment; - bottom.lock(); + // Move both edges and mark them as "locked" + if bottom_edge.is_valid() { + bottom_edge.ds_coord += adjustment; + bottom_edge.lock(); } - if top.is_valid() { - top.ds_coord += adjustment; - top.lock(); + if top_edge.is_valid() { + top_edge.ds_coord += adjustment; + top_edge.lock(); } } captured } - - // pub(super) fn dump_blues(&self) { - // for (i, zone) in (&self.zones[..self.zone_count as usize]).iter().enumerate() { - // println!( - // "[{}] {} t: {} b: {} f: {} ds: {}", - // i, - // if zone.is_bottom { "B" } else { "T" }, - // zone.top.0, - // zone.bottom.0, - // zone.flat.0, - // zone.ds_flat.0, - // ); - // } - // } -} - -pub struct Hinter<'a, 'b, Sink> { - state: &'a HinterState, - sink: &'b mut Sink, - stem_hints: [StemHint; MAX_STEM_HINTS], - stem_count: u8, - mask: HintMask, - initial_map: HintMap, - map: HintMap, } -impl<'a, 'b, Sink: GlyphSink> Hinter<'a, 'b, Sink> { - pub fn new(state: &'a HinterState, sink: &'b mut Sink) -> Self { - Self { - state, - sink, - stem_hints: [StemHint::default(); MAX_STEM_HINTS], - stem_count: 0, - mask: HintMask::all(), - initial_map: HintMap::new(), - map: HintMap::new(), - } - } - - fn hint(&mut self, coord: f32) -> f32 { - if !self.map.valid { - self.build_hint_map(Some(self.mask), Fixed(0)); - } - let f = (coord * 65536.) as i32; - let mapped = self.map.map(self.state.scale, Fixed(f)); - ((mapped.0 >> 10) as f32) / 64. - } - - #[inline(always)] - fn scale(&self, coord: f32) -> f32 { - ((Fixed::from_f32(coord) * self.state.scale).0 >> 10) as f32 / 64. - } - - fn add_stem(&mut self, min: Fixed, max: Fixed) { - let index = self.stem_count as usize; - if index >= MAX_STEM_HINTS || self.map.valid { - return; - } - let stem = &mut self.stem_hints[index]; - stem.min = min; - stem.max = max; - stem.used = false; - stem.ds_min = Fixed(0); - stem.ds_max = Fixed(0); - self.stem_count = index as u8 + 1; - } - - fn build_hint_map(&mut self, mask: Option, origin: Fixed) { - self.map.build( - &self.state, - mask, - Some(&mut self.initial_map), - &mut self.stem_hints[..self.stem_count as usize], - origin, - false, - ); - } -} - -impl<'a, 'b, Sink: GlyphSink> GlyphSink for Hinter<'a, 'b, Sink> { - fn hstem(&mut self, y: f32, dy: f32) { - let y = (y * 65536.) as i32; - let dy = (dy * 65536.) as i32; - self.add_stem(Fixed(y), Fixed(dy)); - } - - fn hint_mask(&mut self, mask: &[u8]) { - let mut hint_mask = HintMask::new(); - hint_mask.set_mask(mask); - if TRACE { - //println!("Got hintmask: {:?}", mask); - } - if hint_mask != self.mask { - self.mask = hint_mask; - self.map.valid = false; - } - } - - fn counter_mask(&mut self, mask: &[u8]) { - let mut hint_mask = HintMask::new(); - hint_mask.set_mask(mask); - let mut map = HintMap::new(); - map.build( - &self.state, - Some(hint_mask), - Some(&mut self.initial_map), - &mut self.stem_hints[..self.stem_count as usize], - Fixed(0), - false, - ); - } - - fn move_to(&mut self, x: f32, y: f32) { - let x = self.scale(x); - let y = self.hint(y); - self.sink.move_to(x, y); - } - - fn line_to(&mut self, x: f32, y: f32) { - let x = self.scale(x); - let y = self.hint(y); - self.sink.line_to(x, y); - } - - fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) { - let cx1 = self.scale(cx1); - let cy1 = self.hint(cy1); - let cx2 = self.scale(cx2); - let cy2 = self.hint(cy2); - let x = self.scale(x); - let y = self.hint(y); - self.sink.curve_to(cx1, cy1, cx2, cy2, x, y); - } - - fn close(&mut self) {} -} - -const ICF_TOP: Fixed = Fixed::from_i32(880); -const ICF_BOTTOM: Fixed = Fixed::from_i32(-120); - -const MAX_BLUE_VALUES: usize = 7; -const MAX_BLUE_ZONES: usize = 12; -const MAX_STEM_HINTS: usize = 48; -const MAX_HINTS: usize = MAX_STEM_HINTS * 2; - -const HINT_MASK_SIZE: usize = (MAX_STEM_HINTS + 7) / 4; - -const DEFAULT_BLUE_SCALE: Fixed = Fixed((0.039625 * 65536. + 0.5) as i32); -const DEFAULT_BLUE_SHIFT: Fixed = Fixed::from_i32(7); -const DEFAULT_BLUE_FUZZ: Fixed = Fixed::ONE; - +/// #[derive(Copy, Clone, Default)] struct StemHint { - used: bool, + /// If true, device space position is valid + is_used: bool, + // Character space position min: Fixed, max: Fixed, + // Device space position after first use ds_min: Fixed, ds_max: Fixed, } @@ -403,11 +350,13 @@ const PAIR_TOP: u8 = 0x8; const LOCKED: u8 = 0x10; const SYNTHETIC: u8 = 0x20; -#[derive(Copy, Clone, Default)] +/// +#[derive(Copy, Clone, PartialEq, Default, Debug)] struct Hint { flags: u8, + /// Index in original stem hint array (if not synthetic) index: u8, - coord: Fixed, + cs_coord: Fixed, ds_coord: Fixed, scale: Fixed, } @@ -445,6 +394,9 @@ impl Hint { self.flags |= LOCKED } + /// Hint initialization from an incoming stem hint. + /// + /// See fn setup( &mut self, stem: &StemHint, @@ -454,43 +406,58 @@ impl Hint { darken_y: Fixed, is_bottom: bool, ) { + // "Ghost hints" are used to align a single edge rather than a + // stem-- think the top and bottom edges of an uppercase + // sans-serif I. + // These are encoded internally with stem hints of width -21 + // and -20 for bottom and top hints, respectively. + const GHOST_BOTTOM_WIDTH: Fixed = Fixed::from_i32(-21); + const GHOST_TOP_WIDTH: Fixed = Fixed::from_i32(-20); let width = stem.max - stem.min; - if width == Fixed::from_i32(-21) { + if width == GHOST_BOTTOM_WIDTH { if is_bottom { - self.coord = stem.max; + self.cs_coord = stem.max; self.flags = GHOST_BOTTOM; } else { self.flags = 0; } - } else if width == Fixed::from_i32(-20) { + } else if width == GHOST_TOP_WIDTH { if !is_bottom { - self.coord = stem.min; + self.cs_coord = stem.min; self.flags = GHOST_TOP; } else { self.flags = 0; } - } else if width < Fixed(0) { + } else if width < Fixed::ZERO { + // If width < 0, this is an inverted pair. We follow FreeType and + // swap the coordinates if is_bottom { - self.coord = stem.max; + self.cs_coord = stem.max; self.flags = PAIR_BOTTOM; } else { - self.coord = stem.min; + self.cs_coord = stem.min; self.flags = PAIR_TOP; } - } else if is_bottom { - self.coord = stem.min; - self.flags = PAIR_BOTTOM; } else { - self.coord = stem.max; - self.flags = PAIR_TOP; + // This is a normal pair + if is_bottom { + self.cs_coord = stem.min; + self.flags = PAIR_BOTTOM; + } else { + self.cs_coord = stem.max; + self.flags = PAIR_TOP; + } } if self.is_top() { - self.coord += Fixed::from_i32(2) * darken_y; + // For top hints, adjust character space position up by twice the + // darkening amount + self.cs_coord += twice(darken_y); } - self.coord += origin; + self.cs_coord += origin; self.scale = scale; self.index = index; - if self.flags != 0 && stem.used { + // If original stem hint was used, copy the position + if self.flags != 0 && stem.is_used { if self.is_top() { self.ds_coord = stem.ds_max; } else { @@ -498,188 +465,211 @@ impl Hint { } self.lock(); } else { - self.ds_coord = self.coord * scale; + self.ds_coord = self.cs_coord * scale; } } } +/// Collection of adjusted hint edges. +/// +/// #[derive(Copy, Clone)] struct HintMap { - hints: [Hint; MAX_HINTS], + edges: [Hint; MAX_HINTS], len: usize, - valid: bool, + is_valid: bool, + scale: Fixed, } impl HintMap { - fn new() -> Self { + fn new(scale: Fixed) -> Self { Self { - hints: [Hint::default(); MAX_HINTS], + edges: [Hint::default(); MAX_HINTS], len: 0, - valid: false, + is_valid: false, + scale, } } fn clear(&mut self) { self.len = 0; - self.valid = false; + self.is_valid = false; } - fn map(&self, scale: Fixed, coord: Fixed) -> Fixed { + /// Transform character space coordinate to device space. + /// + /// Based on + fn transform(&self, coord: Fixed) -> Fixed { if self.len == 0 { - return coord * scale; + return coord * self.scale; } let limit = self.len - 1; let mut i = 0; - while i < limit && coord >= self.hints[i + 1].coord { + while i < limit && coord >= self.edges[i + 1].cs_coord { i += 1; } - while i > 0 && coord < self.hints[i].coord { + while i > 0 && coord < self.edges[i].cs_coord { i -= 1; } - let hint = &self.hints[i]; - if i == 0 && coord < self.hints[0].coord { - ((coord - self.hints[0].coord) * scale) + self.hints[0].ds_coord + let first_edge = &self.edges[0]; + if i == 0 && coord < first_edge.cs_coord { + // Special case for points below first edge: use uniform scale + ((coord - first_edge.cs_coord) * self.scale) + first_edge.ds_coord } else { - ((coord - hint.coord) * hint.scale) + hint.ds_coord + // Use highest edge where cs_coord >= edge.cs_coord + let edge = &self.edges[i]; + ((coord - edge.cs_coord) * edge.scale) + edge.ds_coord } } - fn insert(&mut self, bottom: &Hint, top: &Hint, scale: Fixed, initial: Option<&HintMap>) { - let (is_pair, mut first) = if !bottom.is_valid() { + /// Insert hint edges into map, sorted by character space coordinate. + /// + /// Based on + fn insert(&mut self, bottom: &Hint, top: &Hint, initial: Option<&HintMap>) { + let (is_pair, mut first_edge) = if !bottom.is_valid() { + // Bottom is invalid: insert only top edge (false, *top) } else if !top.is_valid() { + // Top is invalid: insert only bottom edge (false, *bottom) } else { + // We have a valid pair! (true, *bottom) }; - let mut second = *top; - if is_pair && top.coord < bottom.coord { + let mut second_edge = *top; + if is_pair && top.cs_coord < bottom.cs_coord { + // Paired edges must be in proper order. FT just ignores the hint. return; } - let count = if is_pair { 2 } else { 1 }; - if self.len + count > MAX_HINTS { + let edge_count = if is_pair { 2 } else { 1 }; + if self.len + edge_count > MAX_HINTS { + // Won't fit. Again, ignore. return; } - if TRACE { - // println!( - // " Got hint at {} ({})", - // first.coord.to_f32(), - // first.ds_coord.to_f32() - // ); - // if is_pair { - // println!( - // " Got hint at {} ({})", - // second.coord.to_f32(), - // second.ds_coord.to_f32() - // ); - // } - } - let mut insertion_index = 0; - while insertion_index < self.len { - if self.hints[insertion_index].coord >= first.coord { + // Find insertion index that keeps the edge list sorted + let mut insert_ix = 0; + while insert_ix < self.len { + if self.edges[insert_ix].cs_coord >= first_edge.cs_coord { break; } - insertion_index += 1; + insert_ix += 1; } - if insertion_index < self.len { - let current = &self.hints[insertion_index]; - if (current.coord == first.coord) - || (is_pair && current.coord <= second.coord) + // Discard hints that overlap in character space + if insert_ix < self.len { + let current = &self.edges[insert_ix]; + // Existing edge is the same + if (current.cs_coord == first_edge.cs_coord) + // Pair straddles the next edge + || (is_pair && current.cs_coord <= second_edge.cs_coord) + // Inserting between paired edges || current.is_pair_top() { return; } } - if !first.is_locked() { - if let Some(ref initial) = initial { + // Recompute device space locations using initial hint map + if !first_edge.is_locked() { + if let Some(initial) = initial { if is_pair { - let mid = initial.map(scale, (second.coord + first.coord) / 2); - let half = (second.coord - first.coord) / 2 * scale; - first.ds_coord = mid - half; - second.ds_coord = mid + half; + // Preserve stem width: position center of stem with + // initial hint map and two edges with nominal scale + let mid = initial.transform( + first_edge.cs_coord + half(second_edge.cs_coord - first_edge.cs_coord), + ); + let half_width = half(second_edge.cs_coord - first_edge.cs_coord) * self.scale; + first_edge.ds_coord = mid - half_width; + second_edge.ds_coord = mid + half_width; } else { - first.ds_coord = initial.map(scale, first.coord); + first_edge.ds_coord = initial.transform(first_edge.cs_coord); } } } - if insertion_index > 0 && first.ds_coord < self.hints[insertion_index - 1].ds_coord { + // Now discard hints that overlap in device space: + if insert_ix > 0 && first_edge.ds_coord < self.edges[insert_ix - 1].ds_coord { + // Inserting after an existing edge return; } - if insertion_index < self.len - && ((is_pair && second.ds_coord > self.hints[insertion_index].ds_coord) - || first.ds_coord > self.hints[insertion_index].ds_coord) + if insert_ix < self.len + && ((is_pair && second_edge.ds_coord > self.edges[insert_ix].ds_coord) + || first_edge.ds_coord > self.edges[insert_ix].ds_coord) { + // Inserting before an existing edge return; } - if insertion_index != self.len { + // If we're inserting in the middle, make room in the edge array + if insert_ix != self.len { let mut src_index = self.len - 1; - let mut dst_index = self.len + count - 1; + let mut dst_index = self.len + edge_count - 1; loop { - self.hints[dst_index] = self.hints[src_index]; - if src_index == insertion_index { + self.edges[dst_index] = self.edges[src_index]; + if src_index == insert_ix { break; } src_index -= 1; dst_index -= 1; } } - self.hints[insertion_index] = first; + self.edges[insert_ix] = first_edge; if is_pair { - self.hints[insertion_index + 1] = second; - } - if TRACE { - // println!( - // " Inserting hint at {} ({})", - // first.coord.to_f32(), - // first.ds_coord.to_f32() - // ); - // if is_pair { - // println!( - // " Inserting hint at {} ({})", - // second.coord.to_f32(), - // second.ds_coord.to_f32() - // ); - // } + self.edges[insert_ix + 1] = second_edge; } - self.len += count; + self.len += edge_count; } + /// Adjust hint pairs so that one of the two edges is on a pixel boundary. + /// + /// Based on fn adjust(&mut self) { - let mut saved = [(0usize, Fixed(0)); MAX_HINTS]; + let mut saved = [(0usize, Fixed::ZERO); MAX_HINTS]; let mut saved_count = 0usize; let mut i = 0; + // From FT with adjustments for variable names: + // "First pass is bottom-up (font hint order) without look-ahead. + // Locked edges are already adjusted. + // Unlocked edges begin with ds_coord from `initial_map'. + // Save edges that are not optimally adjusted in `saved' array, + // and process them in second pass." let limit = self.len; while i < limit { - let is_pair = self.hints[i].is_pair(); + let is_pair = self.edges[i].is_pair(); let j = if is_pair { i + 1 } else { i }; - if !self.hints[i].is_locked() { - let frac_down = self.hints[i].ds_coord.fract(); - let frac_up = self.hints[j].ds_coord.fract(); - let down_move_down = Fixed(0) - frac_down; - let up_move_down = Fixed(0) - frac_up; - let down_move_up = if frac_down == Fixed(0) { - Fixed(0) + if !self.edges[i].is_locked() { + // We can adjust hint edges that are not locked + let frac_down = self.edges[i].ds_coord.fract(); + let frac_up = self.edges[j].ds_coord.fract(); + // There are four possibilities. We compute them all. + // (moves down are negative) + let down_move_down = Fixed::ZERO - frac_down; + let up_move_down = Fixed::ZERO - frac_up; + let down_move_up = if frac_down == Fixed::ZERO { + Fixed::ZERO } else { Fixed::ONE - frac_down }; - let up_move_up = if frac_up == Fixed(0) { - Fixed(0) + let up_move_up = if frac_up == Fixed::ZERO { + Fixed::ZERO } else { Fixed::ONE - frac_up }; - let move_up = Fixed(down_move_up.0.min(up_move_up.0)); - let move_down = Fixed(down_move_down.0.max(up_move_down.0)); - const MIN_COUNTER: Fixed = Fixed(0x8000); + // Smallest move up + let move_up = down_move_up.min(up_move_up); + // Smallest move down + let move_down = down_move_down.max(up_move_down); let mut save_edge = false; let adjustment; + // Check for room to move up: + // 1. We're at the top of the array, or + // 2. The next edge is at or above the proposed move up if j >= self.len - 1 - || self.hints[j + 1].ds_coord - >= (self.hints[j].ds_coord + move_up + MIN_COUNTER) + || self.edges[j + 1].ds_coord + >= (self.edges[j].ds_coord + move_up + MIN_COUNTER) { + // Also check for room to move down... if i == 0 - || self.hints[i - 1].ds_coord - <= (self.hints[i].ds_coord + move_down - MIN_COUNTER) + || self.edges[i - 1].ds_coord + <= (self.edges[i].ds_coord + move_down - MIN_COUNTER) { + // .. and move the smallest distance adjustment = if -move_down < move_up { move_down } else { @@ -689,311 +679,801 @@ impl HintMap { adjustment = move_up; } } else if i == 0 - || self.hints[i - 1].ds_coord - <= (self.hints[i].ds_coord + move_down - MIN_COUNTER) + || self.edges[i - 1].ds_coord + <= (self.edges[i].ds_coord + move_down - MIN_COUNTER) { + // We can move down adjustment = move_down; + // True if the move is not optimum save_edge = move_up < -move_down; } else { - adjustment = Fixed(0); + // We can't move either way without overlapping + adjustment = Fixed::ZERO; save_edge = true; } - if save_edge && j < self.len - 1 && !self.hints[j + 1].is_locked() { + // Capture non-optimal adjustments and save them for a second + // pass. This is only possible if the edge above is unlocked + // and can be moved. + if save_edge && j < self.len - 1 && !self.edges[j + 1].is_locked() { + // (index, desired adjustment) saved[saved_count] = (j, move_up - adjustment); saved_count += 1; } - self.hints[i].ds_coord += adjustment; + // Apply the adjustment + self.edges[i].ds_coord += adjustment; if is_pair { - self.hints[j].ds_coord += adjustment; + self.edges[j].ds_coord += adjustment; } } - if i > 0 && self.hints[i].coord != self.hints[i - 1].coord { - let a = self.hints[i]; - let b = self.hints[i - 1]; - self.hints[i - 1].scale = (a.ds_coord - b.ds_coord) / (a.coord - b.coord); + // Compute the new edge scale + if i > 0 && self.edges[i].cs_coord != self.edges[i - 1].cs_coord { + let a = self.edges[i]; + let b = self.edges[i - 1]; + self.edges[i - 1].scale = (a.ds_coord - b.ds_coord) / (a.cs_coord - b.cs_coord); } if is_pair { - if self.hints[j].coord != self.hints[j - 1].coord { - let a = self.hints[j]; - let b = self.hints[j - 1]; - self.hints[j - 1].scale = (a.ds_coord - b.ds_coord) / (a.coord - b.coord); + if self.edges[j].cs_coord != self.edges[j - 1].cs_coord { + let a = self.edges[j]; + let b = self.edges[j - 1]; + self.edges[j - 1].scale = (a.ds_coord - b.ds_coord) / (a.cs_coord - b.cs_coord); } i += 1; } i += 1; } - for i in (0..saved_count).rev() { - let (j, adjustment) = saved[i]; - if self.hints[j + 1].ds_coord - >= (self.hints[j].ds_coord + adjustment + Fixed::from_f32(0.5)) - { - self.hints[j].ds_coord += adjustment; - if self.hints[j].is_pair() { - self.hints[j - 1].ds_coord += adjustment; + // Second pass tries to move non-optimal edges up if the first + // pass created room + for (j, adjustment) in saved[..saved_count].iter().copied().rev() { + if self.edges[j + 1].ds_coord >= (self.edges[j].ds_coord + adjustment + MIN_COUNTER) { + self.edges[j].ds_coord += adjustment; + if self.edges[j].is_pair() { + self.edges[j - 1].ds_coord += adjustment; } } } } + /// Builds a hintmap from hints and mask. + /// + /// If `initial_map` is invalid, this recurses one level to initialize + /// it. If `is_initial` is true, simply build the initial map. + /// + /// Based on fn build( &mut self, - state: &HinterState, + state: &HintState, mask: Option, mut initial_map: Option<&mut HintMap>, stems: &mut [StemHint], origin: Fixed, - initial: bool, + is_initial: bool, ) { let scale = state.scale; - let darken_y = Fixed(0); - if !initial { - if let Some(ref mut initial_map) = initial_map { - if !initial_map.valid { - initial_map.build(state, None, None, stems, origin, true); + let darken_y = Fixed::ZERO; + if !is_initial { + if let Some(initial_map) = &mut initial_map { + if !initial_map.is_valid { + // Note: recursive call here to build the initial map if it + // is provided and invalid + initial_map.build(state, Some(HintMask::all()), None, stems, origin, true); } } } let initial_map = initial_map.map(|x| x as &HintMap); self.clear(); + // If the mask is missing or invalid, assume all hints are active let mut mask = mask.unwrap_or_else(HintMask::all); - if !mask.valid { + if !mask.is_valid { mask = HintMask::all(); } - if state.em_box_hints { + if state.do_em_box_hints { + // FreeType generates these during blues initialization. Do + // it here just to avoid carrying the extra state in the + // already large HintState struct. + // let mut bottom = Hint::default(); - bottom.coord = ICF_BOTTOM - Fixed(1); - bottom.ds_coord = (bottom.coord * scale).round() - Fixed::from_f32(0.5); + bottom.cs_coord = ICF_BOTTOM - EPSILON; + bottom.ds_coord = (bottom.cs_coord * scale).round() - MIN_COUNTER; bottom.scale = scale; bottom.flags = GHOST_BOTTOM | LOCKED | SYNTHETIC; let mut top = Hint::default(); - top.coord = ICF_TOP + Fixed(1); - top.ds_coord = (top.coord * scale).round() + Fixed::from_f32(0.5); + top.cs_coord = ICF_TOP + EPSILON + twice(state.darken_y); + top.ds_coord = (top.cs_coord * scale).round() + MIN_COUNTER; top.scale = scale; top.flags = GHOST_TOP | LOCKED | SYNTHETIC; let invalid = Hint::default(); - self.insert(&bottom, &invalid, scale, initial_map); - self.insert(&invalid, &top, scale, initial_map); + self.insert(&bottom, &invalid, initial_map); + self.insert(&invalid, &top, initial_map); } let mut tmp_mask = mask; + // FreeType iterates over the hint mask with some fancy bit logic. We + // do the simpler thing and loop over the stems. + // for (i, stem) in stems.iter().enumerate() { if !tmp_mask.get(i) { continue; } + let hint_ix = i as u8; let mut bottom = Hint::default(); let mut top = Hint::default(); - bottom.setup(stem, i as u8, origin, scale, darken_y, true); - top.setup(stem, i as u8, origin, scale, darken_y, false); + bottom.setup(stem, hint_ix, origin, scale, darken_y, true); + top.setup(stem, hint_ix, origin, scale, darken_y, false); + // Insert hints that are locked or captured by a blue zone if bottom.is_locked() || top.is_locked() || state.capture(&mut bottom, &mut top) { - if initial { - self.insert(&bottom, &top, scale, None); + if is_initial { + self.insert(&bottom, &top, None); } else { - self.insert(&bottom, &top, scale, initial_map); + self.insert(&bottom, &top, initial_map); } + // Avoid processing this hint in the second pass tmp_mask.clear(i); } } - if initial { - if self.len == 0 || self.hints[0].coord.0 > 0 || self.hints[self.len - 1].coord.0 < 0 { + if is_initial { + // Heuristic: insert a point at (0, 0) if it's not covered by a + // mapping. Ensures a lock at baseline for glyphs missing a + // baseline hint. + if self.len == 0 + || self.edges[0].cs_coord > Fixed::ZERO + || self.edges[self.len - 1].cs_coord < Fixed::ZERO + { let edge = Hint { flags: GHOST_BOTTOM | LOCKED | SYNTHETIC, scale, ..Default::default() }; let invalid = Hint::default(); - self.insert(&edge, &invalid, scale, None); + self.insert(&edge, &invalid, None); } } else { + // Insert hints that were skipped in the first pass for (i, stem) in stems.iter().enumerate() { if !tmp_mask.get(i) { continue; } + let hint_ix = i as u8; let mut bottom = Hint::default(); let mut top = Hint::default(); - bottom.setup(stem, i as u8, origin, scale, darken_y, true); - top.setup(stem, i as u8, origin, scale, darken_y, false); - self.insert(&bottom, &top, scale, initial_map); + bottom.setup(stem, hint_ix, origin, scale, darken_y, true); + top.setup(stem, hint_ix, origin, scale, darken_y, false); + self.insert(&bottom, &top, initial_map); } } - self.dump(); + // Adjust edges that are not locked to blue zones self.adjust(); - self.dump(); - if !initial { - for i in 0..self.len { - let hint = &self.hints[i]; - if hint.is_synthetic() { + if !is_initial { + // Save position of edges that were used by the hint map. + for edge in &self.edges[..self.len] { + if edge.is_synthetic() { continue; } - let stem = &mut stems[hint.index as usize]; - if hint.is_top() { - stem.ds_max = hint.ds_coord; + let stem = &mut stems[edge.index as usize]; + if edge.is_top() { + stem.ds_max = edge.ds_coord; } else { - stem.ds_min = hint.ds_coord; + stem.ds_min = edge.ds_coord; } - stem.used = true; + stem.is_used = true; } - self.valid = true; - } - self.valid = true; - } - - fn dump(&self) { - // if !TRACE { - // return; - // } - // for i in 0..self.len { - // let hint = self.hints[i]; - // println!( - // "[{}] {} {} {} {}{}{}{}{}{}", - // hint.index, - // hint.coord.to_f32(), - // hint.ds_coord.to_f32() / hint.scale.to_f32(), - // hint.scale.to_f32() * 65536., - // if hint.is_pair() { "p" } else { "" }, - // if hint.flags & (GHOST_BOTTOM | GHOST_TOP) != 0 { - // "g" - // } else { - // "" - // }, - // if hint.is_top() { "t" } else { "" }, - // if hint.is_bottom() { "b" } else { "" }, - // if hint.is_locked() { "L" } else { "" }, - // if hint.is_synthetic() { "S" } else { "" }, - // ); - // } - // println!("-------------------------------"); + } + self.is_valid = true; } } -#[derive(Copy, Clone, PartialEq)] -pub struct HintMask { +/// Bitmask that specifies which hints are currently active. +/// +/// "Each bit of the mask, starting with the most-significant bit of +/// the first byte, represents the corresponding hint zone in the +/// order in which the hints were declared at the beginning of +/// the charstring." +/// +/// See +/// Also +#[derive(Copy, Clone, PartialEq, Default)] +struct HintMask { mask: [u8; HINT_MASK_SIZE], - valid: bool, + is_valid: bool, } impl HintMask { - pub fn new() -> Self { - Self { - mask: [0u8; HINT_MASK_SIZE], - valid: false, + fn new(bytes: &[u8]) -> Option { + let len = bytes.len(); + if len > HINT_MASK_SIZE { + return None; } + let mut mask = Self::default(); + mask.mask[..len].copy_from_slice(&bytes[..len]); + mask.is_valid = true; + Some(mask) } - pub fn all() -> Self { + fn all() -> Self { Self { mask: [0xFF; HINT_MASK_SIZE], - valid: true, + is_valid: true, } } - fn clear_all(&mut self) { - self.mask = [0u8; HINT_MASK_SIZE]; - self.valid = true; + fn clear(&mut self, bit: usize) { + self.mask[bit >> 3] &= !msb_mask(bit); } - pub fn set_mask(&mut self, mask: &[u8]) { - self.clear_all(); - if mask.len() > HINT_MASK_SIZE { - return; - } - for (i, b) in mask.iter().enumerate() { - self.mask[i] = *b; + fn get(&self, bit: usize) -> bool { + self.mask[bit >> 3] & msb_mask(bit) != 0 + } +} + +/// Returns a bit mask for the selected bit with the +/// most significant bit at index 0. +fn msb_mask(bit: usize) -> u8 { + 1 << (7 - (bit & 0x7)) +} + +pub(super) struct HintingSink<'a, S> { + state: &'a HintState, + sink: &'a mut S, + stem_hints: [StemHint; MAX_HINTS], + stem_count: u8, + mask: HintMask, + initial_map: HintMap, + map: HintMap, + /// Most recent move_to in character space. + start_point: Option<[Fixed; 2]>, + /// Most recent line_to. First two elements are coords in character + /// space and the last two are in device space. + pending_line: Option<[Fixed; 4]>, +} + +impl<'a, S: CommandSink> HintingSink<'a, S> { + pub fn new(state: &'a HintState, sink: &'a mut S) -> Self { + let scale = state.scale; + Self { + state, + sink, + stem_hints: [StemHint::default(); MAX_HINTS], + stem_count: 0, + mask: HintMask::all(), + initial_map: HintMap::new(scale), + map: HintMap::new(scale), + start_point: None, + pending_line: None, } - self.valid = true; } - #[inline] - #[allow(dead_code)] - pub fn set(&mut self, bit: usize) { - self.mask[bit >> 3] |= 1 << (7 - (bit & 0x7)); + pub fn finish(&mut self) { + self.maybe_close_subpath(); } - #[inline] - pub fn clear(&mut self, bit: usize) { - self.mask[bit >> 3] &= !(1 << (7 - (bit & 0x7))); + fn maybe_close_subpath(&mut self) { + // This requires some explanation. The hint mask can be modified + // during charstring evaluation which changes the set of hints that + // are applied. FreeType ensures that the closing line for any subpath + // is transformed with the same hint map as the starting point for the + // subpath. This is done by stashing a copy of the hint map that is + // active when a new subpath is started. Unlike FreeType, we make use + // of close elements, so we can cheat a bit here and avoid the + // extra hintmap. If we're closing an open subpath and have a pending + // line and the line is not equal to the start point in character + // space, then we emit the saved device space coordinates for the + // line. If the coordinates do match in character space, we omit + // that line. The unconditional close command ensures that the + // start and end points coincide. + // Note: this doesn't apply to subpaths that end in cubics. + match (self.start_point.take(), self.pending_line.take()) { + (Some(start), Some([cs_x, cs_y, ds_x, ds_y])) => { + if start != [cs_x, cs_y] { + self.sink.line_to(ds_x, ds_y); + } + self.sink.close(); + } + (Some(_), _) => self.sink.close(), + _ => {} + } } - #[inline] - pub fn get(&self, bit: usize) -> bool { - self.mask[bit >> 3] & (1 << (7 - (bit & 0x7))) != 0 + fn flush_pending_line(&mut self) { + if let Some([_, _, x, y]) = self.pending_line.take() { + self.sink.line_to(x, y); + } } - // pub fn dump(&self) { - // for i in 0..MAX_STEM_HINTS { - // print!("{}", self.get(i) as u8); - // } - // println!(); - // for b in &self.mask { - // print!("{:#8b}", *b) - // } - // println!(); - // } -} + fn hint(&mut self, coord: Fixed) -> Fixed { + if !self.map.is_valid { + self.build_hint_map(Some(self.mask), Fixed::ZERO); + } + trunc(self.map.transform(coord)) + } -#[derive(Copy, Clone, Default)] -struct BlueZone { - is_bottom: bool, - bottom: Fixed, - top: Fixed, - flat: Fixed, - ds_flat: Fixed, -} + fn scale(&self, coord: Fixed) -> Fixed { + trunc(coord * self.state.scale) + } -#[derive(Copy, Clone, Default)] -struct BlueValues { - values: [(Fixed, Fixed); MAX_BLUE_VALUES], - len: u32, + fn add_stem(&mut self, min: Fixed, max: Fixed) { + let index = self.stem_count as usize; + if index >= MAX_HINTS || self.map.is_valid { + return; + } + let stem = &mut self.stem_hints[index]; + stem.min = min; + stem.max = max; + stem.is_used = false; + stem.ds_min = Fixed::ZERO; + stem.ds_max = Fixed::ZERO; + self.stem_count = index as u8 + 1; + } + + fn build_hint_map(&mut self, mask: Option, origin: Fixed) { + self.map.build( + self.state, + mask, + Some(&mut self.initial_map), + &mut self.stem_hints[..self.stem_count as usize], + origin, + false, + ); + } } -impl BlueValues { - fn new() -> Self { - Self::default() +impl<'a, S: CommandSink> CommandSink for HintingSink<'a, S> { + fn hstem(&mut self, min: Fixed, max: Fixed) { + self.add_stem(min, max); } - fn set(&mut self, values: &[f32]) { - let len = values.len().min(MAX_BLUE_VALUES * 2); - for i in 0..len / 2 { - let a = values[i * 2]; - let b = values[i * 2 + 1]; - self.values[i] = (Fixed::from_i32(a as i32), Fixed::from_i32(b as i32)); + fn hint_mask(&mut self, mask: &[u8]) { + // For invalid hint masks, FreeType assumes all hints are active. + // See + let mask = HintMask::new(mask).unwrap_or_else(HintMask::all); + if mask != self.mask { + self.mask = mask; + self.map.is_valid = false; } - self.len = len as u32 / 2; } - fn values(&self) -> &[(Fixed, Fixed)] { - &self.values[..self.len as usize] + fn counter_mask(&mut self, mask: &[u8]) { + // For counter masks, we build a temporary hint map "just to + // place and lock those stems participating in the counter + // mask." Building the map modifies the stem hint array as a + // side effect. + // See + let mask = HintMask::new(mask).unwrap_or_else(HintMask::all); + let mut map = HintMap::new(self.state.scale); + map.build( + self.state, + Some(mask), + Some(&mut self.initial_map), + &mut self.stem_hints[..self.stem_count as usize], + Fixed::ZERO, + false, + ); + } + + fn move_to(&mut self, x: Fixed, y: Fixed) { + self.maybe_close_subpath(); + self.start_point = Some([x, y]); + let x = self.scale(x); + let y = self.hint(y); + self.sink.move_to(x, y); + } + + fn line_to(&mut self, x: Fixed, y: Fixed) { + self.flush_pending_line(); + let ds_x = self.scale(x); + let ds_y = self.hint(y); + self.pending_line = Some([x, y, ds_x, ds_y]); + } + + fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { + self.flush_pending_line(); + let cx1 = self.scale(cx1); + let cy1 = self.hint(cy1); + let cx2 = self.scale(cx2); + let cy2 = self.hint(cy2); + let x = self.scale(x); + let y = self.hint(y); + self.sink.curve_to(cx1, cy1, cx2, cy2, x, y); + } + + fn close(&mut self) { + // We emit close commands based on the sequence of moves. + // See `maybe_close_subpath` } } -impl DictionarySink for HinterState { - fn blue_values(&mut self, values: &[f32]) { - self.blues.set(values); +/// FreeType converts from 16.16 to 26.6 by truncation. We keep our +/// values in 16.16 so simply zero the low 10 bits to match the +/// precision when converting to f32. +fn trunc(value: Fixed) -> Fixed { + Fixed::from_bits(value.to_bits() & !0x3FF) +} + +fn half(value: Fixed) -> Fixed { + Fixed::from_bits(value.to_bits() / 2) +} + +fn twice(value: Fixed) -> Fixed { + Fixed::from_bits(value.to_bits().wrapping_mul(2)) +} + +#[cfg(test)] +mod tests { + use read_fonts::{tables::postscript::charstring::CommandSink, types::F2Dot14, FontRef}; + + use super::{ + BlueZone, Blues, Fixed, Hint, HintMap, HintMask, HintParams, HintState, HintingSink, + StemHint, GHOST_BOTTOM, GHOST_TOP, HINT_MASK_SIZE, LOCKED, PAIR_BOTTOM, PAIR_TOP, + }; + + fn make_hint_state() -> HintState { + fn make_blues(values: &[f64]) -> Blues { + Blues::new(values.iter().copied().map(Fixed::from_f64)) + } + // + // + // + // + // + let params = HintParams { + blues: make_blues(&[ + -15.0, 0.0, 536.0, 547.0, 571.0, 582.0, 714.0, 726.0, 760.0, 772.0, + ]), + other_blues: make_blues(&[-255.0, -240.0]), + blue_scale: Fixed::from_f64(0.05), + blue_shift: Fixed::from_i32(7), + blue_fuzz: Fixed::ZERO, + ..Default::default() + }; + HintState::new(¶ms, Fixed::ONE / Fixed::from_i32(64)) } - fn family_blues(&mut self, values: &[f32]) { - self.family_blues.set(values); + #[test] + fn scaled_blue_zones() { + let state = make_hint_state(); + assert!(!state.do_em_box_hints); + assert_eq!(state.zone_count, 6); + assert_eq!(state.boost, Fixed::from_bits(27035)); + assert!(state.supress_overshoot); + // FreeType generates the following zones: + let expected_zones = &[ + // csBottomEdge -983040 int + // csTopEdge 0 int + // csFlatEdge 0 int + // dsFlatEdge 0 int + // bottomZone 1 '\x1' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(-983040), + is_bottom: true, + ..Default::default() + }, + // csBottomEdge 35127296 int + // csTopEdge 35848192 int + // csFlatEdge 35127296 int + // dsFlatEdge 589824 int + // bottomZone 0 '\0' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(35127296), + cs_top_edge: Fixed::from_bits(35848192), + cs_flat_edge: Fixed::from_bits(35127296), + ds_flat_edge: Fixed::from_bits(589824), + is_bottom: false, + }, + // csBottomEdge 37421056 int + // csTopEdge 38141952 int + // csFlatEdge 37421056 int + // dsFlatEdge 589824 int + // bottomZone 0 '\0' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(37421056), + cs_top_edge: Fixed::from_bits(38141952), + cs_flat_edge: Fixed::from_bits(37421056), + ds_flat_edge: Fixed::from_bits(589824), + is_bottom: false, + }, + // csBottomEdge 46792704 int + // csTopEdge 47579136 int + // csFlatEdge 46792704 int + // dsFlatEdge 786432 int + // bottomZone 0 '\0' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(46792704), + cs_top_edge: Fixed::from_bits(47579136), + cs_flat_edge: Fixed::from_bits(46792704), + ds_flat_edge: Fixed::from_bits(786432), + is_bottom: false, + }, + // csBottomEdge 49807360 int + // csTopEdge 50593792 int + // csFlatEdge 49807360 int + // dsFlatEdge 786432 int + // bottomZone 0 '\0' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(49807360), + cs_top_edge: Fixed::from_bits(50593792), + cs_flat_edge: Fixed::from_bits(49807360), + ds_flat_edge: Fixed::from_bits(786432), + is_bottom: false, + }, + // csBottomEdge -16711680 int + // csTopEdge -15728640 int + // csFlatEdge -15728640 int + // dsFlatEdge -262144 int + // bottomZone 1 '\x1' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(-16711680), + cs_top_edge: Fixed::from_bits(-15728640), + cs_flat_edge: Fixed::from_bits(-15728640), + ds_flat_edge: Fixed::from_bits(-262144), + is_bottom: true, + }, + ]; + assert_eq!(state.zones(), expected_zones); } - fn other_blues(&mut self, values: &[f32]) { - self.other_blues.set(values); + #[test] + fn blue_zone_capture() { + let state = make_hint_state(); + let bottom_edge = Hint { + flags: PAIR_BOTTOM, + ds_coord: Fixed::from_f64(2.3), + ..Default::default() + }; + let top_edge = Hint { + flags: PAIR_TOP, + // This value chosen to fit within the first "top" blue zone + cs_coord: Fixed::from_bits(35127297), + ds_coord: Fixed::from_f64(2.3), + ..Default::default() + }; + // Capture both + { + let (mut bottom_edge, mut top_edge) = (bottom_edge, top_edge); + assert!(state.capture(&mut bottom_edge, &mut top_edge)); + assert!(bottom_edge.is_locked()); + assert!(top_edge.is_locked()); + } + // Capture none + { + // Used to guarantee the edges are below all blue zones and will + // not be captured + let min_cs_coord = Fixed::MIN; + let mut bottom_edge = Hint { + cs_coord: min_cs_coord, + ..bottom_edge + }; + let mut top_edge = Hint { + cs_coord: min_cs_coord, + ..top_edge + }; + assert!(!state.capture(&mut bottom_edge, &mut top_edge)); + assert!(!bottom_edge.is_locked()); + assert!(!top_edge.is_locked()); + } + // Capture bottom, ignore invalid top + { + let mut bottom_edge = bottom_edge; + let mut top_edge = Hint { + // Empty flags == invalid hint + flags: 0, + ..top_edge + }; + assert!(state.capture(&mut bottom_edge, &mut top_edge)); + assert!(bottom_edge.is_locked()); + assert!(!top_edge.is_locked()); + } + // Capture top, ignore invalid bottom + { + let mut bottom_edge = Hint { + // Empty flags == invalid hint + flags: 0, + ..bottom_edge + }; + let mut top_edge = top_edge; + assert!(state.capture(&mut bottom_edge, &mut top_edge)); + assert!(!bottom_edge.is_locked()); + assert!(top_edge.is_locked()); + } } - fn family_other_blues(&mut self, values: &[f32]) { - self.family_other_blues.set(values); + #[test] + fn hint_mask_ops() { + const MAX_BITS: usize = HINT_MASK_SIZE * 8; + let all_bits = HintMask::all(); + for i in 0..MAX_BITS { + assert!(all_bits.get(i)); + } + let odd_bits = HintMask::new(&[0b01010101; HINT_MASK_SIZE]).unwrap(); + for i in 0..MAX_BITS { + assert_eq!(i & 1 != 0, odd_bits.get(i)); + } + let mut cleared_bits = odd_bits; + for i in 0..MAX_BITS { + if i & 1 != 0 { + cleared_bits.clear(i); + } + } + assert_eq!(cleared_bits.mask, HintMask::default().mask); } - fn blue_scale(&mut self, scale: f32) { - self.blue_scale = Fixed::from_f32(scale); + #[test] + fn hint_mapping() { + let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap(); + let cff_font = super::super::outlines::Outlines::new(&font).unwrap(); + let state = cff_font + .subfont(0, 8.0, &[F2Dot14::from_f32(-1.0); 2]) + .unwrap() + .hint_state; + let mut initial_map = HintMap::new(state.scale); + let mut map = HintMap::new(state.scale); + // Stem hints from Cantarell-VF.otf glyph id 2 + let mut stems = [ + StemHint { + min: Fixed::from_bits(1376256), + max: Fixed::ZERO, + ..Default::default() + }, + StemHint { + min: Fixed::from_bits(16318464), + max: Fixed::from_bits(17563648), + ..Default::default() + }, + StemHint { + min: Fixed::from_bits(45481984), + max: Fixed::from_bits(44171264), + ..Default::default() + }, + ]; + map.build( + &state, + Some(HintMask::all()), + Some(&mut initial_map), + &mut stems, + Fixed::ZERO, + false, + ); + // FT generates the following hint map: + // + // index csCoord dsCoord scale flags + // 0 0.00 0.00 526 gbL + // 1 249.00 250.14 524 pb + // 1 268.00 238.22 592 pt + // 2 694.00 750.41 524 gtL + let expected_edges = [ + Hint { + index: 0, + cs_coord: Fixed::from_f64(0.0), + ds_coord: Fixed::from_f64(0.0), + scale: Fixed::from_bits(526), + flags: GHOST_BOTTOM | LOCKED, + }, + Hint { + index: 1, + cs_coord: Fixed::from_bits(16318464), + ds_coord: Fixed::from_bits(131072), + scale: Fixed::from_bits(524), + flags: PAIR_BOTTOM, + }, + Hint { + index: 1, + cs_coord: Fixed::from_bits(17563648), + ds_coord: Fixed::from_bits(141028), + scale: Fixed::from_bits(592), + flags: PAIR_TOP, + }, + Hint { + index: 2, + cs_coord: Fixed::from_bits(45481984), + ds_coord: Fixed::from_bits(393216), + scale: Fixed::from_bits(524), + flags: GHOST_TOP | LOCKED, + }, + ]; + assert_eq!(expected_edges, &map.edges[..map.len]); + // And FT generates the following mappings + let mappings = [ + // (coord in font units, expected hinted coord in device space) in 16.16 + (0, 0), // 0 -> 0 + (44302336, 382564), // 676 -> 5.828125 + (45481984, 393216), // 694 -> 6 + (16318464, 131072), // 249 -> 2 + (17563648, 141028), // 268 -> 2.140625 + (49676288, 426752), // 758 -> 6.5 + (56754176, 483344), // 866 -> 7.375 + (57868288, 492252), // 883 -> 7.5 + (50069504, 429896), // 764 -> 6.546875 + ]; + for (coord, expected) in mappings { + assert_eq!( + map.transform(Fixed::from_bits(coord)), + Fixed::from_bits(expected) + ); + } } - fn blue_shift(&mut self, shift: f32) { - self.blue_shift = Fixed::from_f32(shift); + /// HintingSink is mostly pass-through. This test captures the logic + /// around omission of pending lines that match subpath start. + /// See HintingSink::maybe_close_subpath for details. + #[test] + fn hinting_sink_omits_closing_line_that_matches_start() { + let state = HintState { + scale: Fixed::ONE, + ..Default::default() + }; + let mut path = Path::default(); + let mut sink = HintingSink::new(&state, &mut path); + let move1_2 = [Fixed::from_f64(1.0), Fixed::from_f64(2.0)]; + let line2_3 = [Fixed::from_f64(2.0), Fixed::from_f64(3.0)]; + let line1_2 = [Fixed::from_f64(1.0), Fixed::from_f64(2.0)]; + let line3_4 = [Fixed::from_f64(3.0), Fixed::from_f64(4.0)]; + let curve = [ + Fixed::from_f64(3.0), + Fixed::from_f64(4.0), + Fixed::from_f64(5.0), + Fixed::from_f64(6.0), + Fixed::from_f64(1.0), + Fixed::from_f64(2.0), + ]; + // First subpath, closing line matches start + sink.move_to(move1_2[0], move1_2[1]); + sink.line_to(line2_3[0], line2_3[1]); + sink.line_to(line1_2[0], line1_2[1]); + // Second subpath, closing line does not match start + sink.move_to(move1_2[0], move1_2[1]); + sink.line_to(line2_3[0], line2_3[1]); + sink.line_to(line3_4[0], line3_4[1]); + // Third subpath, ends with cubic. Still emits a close command + // even though end point matches start. + sink.move_to(move1_2[0], move1_2[1]); + sink.line_to(line2_3[0], line2_3[1]); + sink.curve_to(curve[0], curve[1], curve[2], curve[3], curve[4], curve[5]); + sink.finish(); + // Subpaths always end with a close command. If a final line coincides + // with the start of a subpath, it is omitted. + assert_eq!( + &path.0, + &[ + // First subpath + MoveTo(move1_2), + LineTo(line2_3), + // line1_2 is omitted + Close, + // Second subpath + MoveTo(move1_2), + LineTo(line2_3), + LineTo(line3_4), + Close, + // Third subpath + MoveTo(move1_2), + LineTo(line2_3), + CurveTo(curve), + Close, + ] + ); } - fn blue_fuzz(&mut self, fuzz: f32) { - self.blue_fuzz = Fixed::from_f32(fuzz); + #[derive(Copy, Clone, PartialEq, Debug)] + enum Command { + MoveTo([Fixed; 2]), + LineTo([Fixed; 2]), + CurveTo([Fixed; 6]), + Close, } - fn language_group(&mut self, group: u32) { - self.language_group = group as u8; + use Command::*; + + #[derive(Default)] + struct Path(Vec); + + impl CommandSink for Path { + fn move_to(&mut self, x: Fixed, y: Fixed) { + self.0.push(MoveTo([x, y])); + } + fn line_to(&mut self, x: Fixed, y: Fixed) { + self.0.push(LineTo([x, y])); + } + fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) { + self.0.push(CurveTo([cx0, cy0, cx1, cy1, x, y])); + } + fn close(&mut self) { + self.0.push(Close); + } } } diff --git a/src/scale/cff/mod.rs b/src/scale/cff/mod.rs index 63d17ae..b2f6ae8 100644 --- a/src/scale/cff/mod.rs +++ b/src/scale/cff/mod.rs @@ -1,26 +1,18 @@ -/*! -PostScript outlines. - -*/ - -#[allow(clippy::module_inception)] -mod cff; mod hint; +mod outlines; -pub use cff::{Cff, CffProxy, Glyph, GlyphSink}; -pub use hint::HinterState; +pub(crate) use outlines::Outlines; -use super::{internal, TRACE}; +use super::Outline; +use read_fonts::types::{F2Dot14, GlyphId}; -use crate::font::FontRef; - -pub struct Scaler { +pub struct SubfontCache { entries: Vec, max_entries: usize, epoch: u64, } -impl Scaler { +impl SubfontCache { pub fn new(max_entries: usize) -> Self { Self { entries: Vec::new(), @@ -31,82 +23,70 @@ impl Scaler { pub fn scale( &mut self, - font: &FontRef, + outlines: &outlines::Outlines, id: u64, + glyph_id: u16, + size: f32, coords: &[i16], - proxy: &CffProxy, - scale: f32, hint: bool, - glyph_id: u16, - sink: &mut impl GlyphSink, + outline: &mut Outline, ) -> Option<()> { - let cff = proxy.materialize(font); - let glyph = cff.get(glyph_id)?; - if hint { - let dict = glyph.subfont_index(); - let state = self.entry(id, dict, coords, scale, &glyph); - if glyph.path(scale, coords, Some(state), sink) { - Some(()) - } else { - None - } - } else if glyph.path(scale, coords, None, sink) { - Some(()) - } else { - None - } - } - - fn entry( - &mut self, - id: u64, - dict: u16, - coords: &[i16], - scale: f32, - glyph: &Glyph, - ) -> &HinterState { let epoch = self.epoch; - let (found, index) = self.find_entry(id, dict, coords, scale); - if found { - let entry = &mut self.entries[index]; + let gid = GlyphId::new(glyph_id); + let subfont_index = outlines.subfont_index(gid); + let (found, entry_index) = self.find_entry(id, subfont_index, coords, size); + let (subfont, coords) = if found { + let entry = &mut self.entries[entry_index]; entry.epoch = epoch; - &entry.state + (&entry.subfont, &entry.coords) } else { self.epoch += 1; - let state = HinterState::new(glyph, scale, coords); - if index == self.entries.len() { + let epoch = self.epoch; + if entry_index == self.entries.len() { + let coords: Vec = coords.iter().map(|x| F2Dot14::from_bits(*x)).collect(); + let subfont = outlines.subfont(subfont_index, size, &coords).ok()?; self.entries.push(Entry { - epoch, id, - dict, - state, - coords: Vec::from(coords), - scale, + epoch, + subfont, + subfont_index, + size, + coords, }); - &self.entries[index].state + let entry = &self.entries[entry_index]; + (&entry.subfont, &entry.coords) } else { - let entry = &mut self.entries[index]; + let entry = &mut self.entries[entry_index]; + entry.id = u64::MAX; entry.epoch = epoch; - entry.id = id; - entry.dict = dict; - entry.state = state; entry.coords.clear(); - entry.coords.extend_from_slice(coords); - entry.scale = scale; - &entry.state + entry + .coords + .extend(coords.iter().map(|x| F2Dot14::from_bits(*x))); + entry.subfont = outlines.subfont(subfont_index, size, &entry.coords).ok()?; + entry.id = id; + entry.subfont_index = subfont_index; + entry.size = size; + (&entry.subfont, &entry.coords) } - } + }; + outlines + .draw(subfont, gid, coords, hint, &mut OutlineBuilder(outline)) + .ok()?; + Some(()) } - fn find_entry(&self, id: u64, dict: u16, coords: &[i16], scale: f32) -> (bool, usize) { + fn find_entry(&self, id: u64, index: u32, coords: &[i16], size: f32) -> (bool, usize) { let mut lowest_epoch = self.epoch; let mut lowest_index = 0; - let vary = !coords.is_empty(); for (i, entry) in self.entries.iter().enumerate() { if entry.id == id - && entry.dict == dict - && entry.scale == scale - && (!vary || (coords == &entry.coords[..])) + && entry.subfont_index == index + && entry.size == size + && coords + .iter() + .map(|x| F2Dot14::from_bits(*x)) + .eq(entry.coords.iter().copied()) { return (true, i); } @@ -125,8 +105,33 @@ impl Scaler { struct Entry { epoch: u64, id: u64, - dict: u16, - state: HinterState, - coords: Vec, - scale: f32, + subfont: outlines::Subfont, + subfont_index: u32, + size: f32, + coords: Vec, +} + +struct OutlineBuilder<'a>(&'a mut Outline); + +impl read_fonts::types::Pen for OutlineBuilder<'_> { + fn move_to(&mut self, x: f32, y: f32) { + self.0.move_to((x, y).into()); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.0.line_to((x, y).into()); + } + + fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { + self.0.quad_to((cx0, cy0).into(), (x, y).into()); + } + + fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { + self.0 + .curve_to((cx0, cy0).into(), (cx1, cy1).into(), (x, y).into()); + } + + fn close(&mut self) { + self.0.close(); + } } diff --git a/src/scale/cff/outlines.rs b/src/scale/cff/outlines.rs new file mode 100644 index 0000000..5c0df21 --- /dev/null +++ b/src/scale/cff/outlines.rs @@ -0,0 +1,709 @@ +//! Support for scaling CFF outlines. + +use std::ops::Range; + +use read_fonts::{ + tables::{ + cff::Cff, + cff2::Cff2, + postscript::{ + charstring::{self, CommandSink}, + dict, BlendState, Error, FdSelect, Index, + }, + variations::ItemVariationStore, + }, + types::{F2Dot14, Fixed, GlyphId, Pen}, + FontData, FontRead, TableProvider, +}; + +use super::hint::{HintParams, HintState, HintingSink}; + +/// Type for loading, scaling and hinting outlines in CFF/CFF2 tables. +/// +/// The skrifa crate provides a higher level interface for this that handles +/// caching and abstracting over the different outline formats. Consider using +/// that if detailed control over resources is not required. +/// +/// # Subfonts +/// +/// CFF tables can contain multiple logical "subfonts" which determine the +/// state required for processing some subset of glyphs. This state is +/// accessed using the [`FDArray and FDSelect`](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=28) +/// operators to select an appropriate subfont for any given glyph identifier. +/// This process is exposed on this type with the +/// [`subfont_index`](Self::subfont_index) method to retrieve the subfont +/// index for the requested glyph followed by using the +/// [`subfont`](Self::subfont) method to create an appropriately configured +/// subfont for that glyph. +#[derive(Clone)] +pub(crate) struct Outlines<'a> { + version: Version<'a>, + top_dict: TopDict<'a>, + units_per_em: u16, +} + +impl<'a> Outlines<'a> { + /// Creates a new scaler for the given font. + /// + /// This will choose an underyling CFF2 or CFF table from the font, in that + /// order. + pub fn new(font: &impl TableProvider<'a>) -> Result { + let units_per_em = font.head()?.units_per_em(); + if let Ok(cff2) = font.cff2() { + Self::from_cff2(cff2, units_per_em) + } else { + // "The Name INDEX in the CFF data must contain only one entry; + // that is, there must be only one font in the CFF FontSet" + // So we always pass 0 for Top DICT index when reading from an + // OpenType font. + // + Self::from_cff(font.cff()?, 0, units_per_em) + } + } + + pub fn from_cff( + cff1: Cff<'a>, + top_dict_index: usize, + units_per_em: u16, + ) -> Result { + let top_dict_data = cff1.top_dicts().get(top_dict_index)?; + let top_dict = TopDict::new(cff1.offset_data().as_bytes(), top_dict_data, false)?; + Ok(Self { + version: Version::Version1(cff1), + top_dict, + units_per_em, + }) + } + + pub fn from_cff2(cff2: Cff2<'a>, units_per_em: u16) -> Result { + let table_data = cff2.offset_data().as_bytes(); + let top_dict = TopDict::new(table_data, cff2.top_dict_data(), true)?; + Ok(Self { + version: Version::Version2(cff2), + top_dict, + units_per_em, + }) + } + + pub fn is_cff2(&self) -> bool { + matches!(self.version, Version::Version2(_)) + } + + /// Returns the number of available glyphs. + pub fn glyph_count(&self) -> usize { + self.top_dict + .charstrings + .as_ref() + .map(|cs| cs.count() as usize) + .unwrap_or_default() + } + + /// Returns the number of available subfonts. + pub fn subfont_count(&self) -> u32 { + self.top_dict + .font_dicts + .as_ref() + .map(|font_dicts| font_dicts.count()) + // All CFF fonts have at least one logical subfont. + .unwrap_or(1) + } + + /// Returns the subfont (or Font DICT) index for the given glyph + /// identifier. + pub fn subfont_index(&self, glyph_id: GlyphId) -> u32 { + // For CFF tables, an FDSelect index will be present for CID-keyed + // fonts. Otherwise, the Top DICT will contain an entry for the + // "global" Private DICT. + // See + // + // CFF2 tables always contain a Font DICT and an FDSelect is only + // present if the size of the DICT is greater than 1. + // See + // + // In both cases, we return a subfont index of 0 when FDSelect is missing. + self.top_dict + .fd_select + .as_ref() + .and_then(|select| select.font_index(glyph_id)) + .unwrap_or(0) as u32 + } + + /// Creates a new subfont for the given index, size, normalized + /// variation coordinates and hinting state. + /// + /// The index of a subfont for a particular glyph can be retrieved with + /// the [`subfont_index`](Self::subfont_index) method. + pub fn subfont(&self, index: u32, size: f32, coords: &[F2Dot14]) -> Result { + let private_dict_range = self.private_dict_range(index)?; + let private_dict_data = self.offset_data().read_array(private_dict_range.clone())?; + let mut hint_params = HintParams::default(); + let mut subrs_offset = None; + let mut store_index = 0; + let blend_state = self + .top_dict + .var_store + .clone() + .map(|store| BlendState::new(store, coords, store_index)) + .transpose()?; + for entry in dict::entries(private_dict_data, blend_state) { + use dict::Entry::*; + match entry? { + BlueValues(values) => hint_params.blues = values, + FamilyBlues(values) => hint_params.family_blues = values, + OtherBlues(values) => hint_params.other_blues = values, + FamilyOtherBlues(values) => hint_params.family_blues = values, + BlueScale(value) => hint_params.blue_scale = value, + BlueShift(value) => hint_params.blue_shift = value, + BlueFuzz(value) => hint_params.blue_fuzz = value, + LanguageGroup(group) => hint_params.language_group = group, + // Subrs offset is relative to the private DICT + SubrsOffset(offset) => subrs_offset = Some(private_dict_range.start + offset), + VariationStoreIndex(index) => store_index = index, + _ => {} + } + } + let scale = if size <= 0.0 { + Fixed::ONE + } else { + // Note: we do an intermediate scale to 26.6 to ensure we + // match FreeType + Fixed::from_bits((size * 64.) as i32) / Fixed::from_bits(self.units_per_em as i32) + }; + // When hinting, use a modified scale factor + // + let hint_scale = Fixed::from_bits((scale.to_bits() + 32) / 64); + let hint_state = HintState::new(&hint_params, hint_scale); + Ok(Subfont { + is_cff2: self.is_cff2(), + scale, + subrs_offset, + hint_state, + store_index, + }) + } + + /// Loads and scales an outline for the given subfont instance, glyph + /// identifier and normalized variation coordinates. + /// + /// Before calling this method, use [`subfont_index`](Self::subfont_index) + /// to retrieve the subfont index for the desired glyph and then + /// [`subfont`](Self::subfont) to create an instance of the subfont for a + /// particular size and location in variation space. + /// Creating subfont instances is not free, so this process is exposed in + /// discrete steps to allow for caching. + /// + /// The result is emitted to the specified pen. + pub fn draw( + &self, + subfont: &Subfont, + glyph_id: GlyphId, + coords: &[F2Dot14], + hint: bool, + pen: &mut impl Pen, + ) -> Result<(), Error> { + let charstring_data = self + .top_dict + .charstrings + .as_ref() + .ok_or(Error::MissingCharstrings)? + .get(glyph_id.to_u16() as usize)?; + let subrs = subfont.subrs(self)?; + let blend_state = subfont.blend_state(self, coords)?; + let mut pen_sink = charstring::PenSink::new(pen); + let mut simplifying_adapter = NopFilteringSink::new(&mut pen_sink); + if hint { + let mut hinting_adapter = + HintingSink::new(&subfont.hint_state, &mut simplifying_adapter); + charstring::evaluate( + charstring_data, + self.global_subrs(), + subrs, + blend_state, + &mut hinting_adapter, + )?; + hinting_adapter.finish(); + } else { + let mut scaling_adapter = + ScalingSink26Dot6::new(&mut simplifying_adapter, subfont.scale); + charstring::evaluate( + charstring_data, + self.global_subrs(), + subrs, + blend_state, + &mut scaling_adapter, + )?; + } + simplifying_adapter.finish(); + Ok(()) + } + + fn offset_data(&self) -> FontData<'a> { + match &self.version { + Version::Version1(cff1) => cff1.offset_data(), + Version::Version2(cff2) => cff2.offset_data(), + } + } + + fn global_subrs(&self) -> Index<'a> { + match &self.version { + Version::Version1(cff1) => cff1.global_subrs().into(), + Version::Version2(cff2) => cff2.global_subrs().into(), + } + } + + fn private_dict_range(&self, subfont_index: u32) -> Result, Error> { + if let Some(font_dicts) = &self.top_dict.font_dicts { + // If we have a font dict array, extract the private dict range + // from the font dict at the given index. + let font_dict_data = font_dicts.get(subfont_index as usize)?; + let mut range = None; + for entry in dict::entries(font_dict_data, None) { + if let dict::Entry::PrivateDictRange(r) = entry? { + range = Some(r); + break; + } + } + range + } else { + // Last chance, use the private dict range from the top dict if + // available. + self.top_dict.private_dict_range.clone() + } + .ok_or(Error::MissingPrivateDict) + } +} + +#[derive(Clone)] +enum Version<'a> { + /// + Version1(Cff<'a>), + /// + Version2(Cff2<'a>), +} + +/// Specifies local subroutines and hinting parameters for some subset of +/// glyphs in a CFF or CFF2 table. +/// +/// This type is designed to be cacheable to avoid re-evaluating the private +/// dict every time a charstring is processed. +/// +/// For variable fonts, this is dependent on a location in variation space. +#[derive(Clone)] +pub(crate) struct Subfont { + is_cff2: bool, + scale: Fixed, + subrs_offset: Option, + pub(crate) hint_state: HintState, + store_index: u16, +} + +impl Subfont { + /// Returns the local subroutine index. + pub fn subrs<'a>(&self, scaler: &Outlines<'a>) -> Result>, Error> { + if let Some(subrs_offset) = self.subrs_offset { + let offset_data = scaler.offset_data().as_bytes(); + let index_data = offset_data.get(subrs_offset..).unwrap_or_default(); + Ok(Some(Index::new(index_data, self.is_cff2)?)) + } else { + Ok(None) + } + } + + /// Creates a new blend state for the given normalized variation + /// coordinates. + pub fn blend_state<'a>( + &self, + scaler: &Outlines<'a>, + coords: &'a [F2Dot14], + ) -> Result>, Error> { + if let Some(var_store) = scaler.top_dict.var_store.clone() { + Ok(Some(BlendState::new(var_store, coords, self.store_index)?)) + } else { + Ok(None) + } + } +} + +/// Entries that we parse from the Top DICT that are required to support +/// charstring evaluation. +#[derive(Clone, Default)] +struct TopDict<'a> { + charstrings: Option>, + font_dicts: Option>, + fd_select: Option>, + private_dict_range: Option>, + var_store: Option>, +} + +impl<'a> TopDict<'a> { + fn new(table_data: &'a [u8], top_dict_data: &'a [u8], is_cff2: bool) -> Result { + let mut items = TopDict::default(); + for entry in dict::entries(top_dict_data, None) { + match entry? { + dict::Entry::CharstringsOffset(offset) => { + items.charstrings = Some(Index::new( + table_data.get(offset..).unwrap_or_default(), + is_cff2, + )?); + } + dict::Entry::FdArrayOffset(offset) => { + items.font_dicts = Some(Index::new( + table_data.get(offset..).unwrap_or_default(), + is_cff2, + )?); + } + dict::Entry::FdSelectOffset(offset) => { + items.fd_select = Some(FdSelect::read(FontData::new( + table_data.get(offset..).unwrap_or_default(), + ))?); + } + dict::Entry::PrivateDictRange(range) => { + items.private_dict_range = Some(range); + } + dict::Entry::VariationStoreOffset(offset) if is_cff2 => { + items.var_store = Some(ItemVariationStore::read(FontData::new( + // IVS is preceded by a 2 byte length + table_data.get(offset + 2..).unwrap_or_default(), + ))?); + } + _ => {} + } + } + Ok(items) + } +} + +/// Command sink adapter that applies a scaling factor. +/// +/// This assumes a 26.6 scaling factor packed into a Fixed and thus, +/// this is not public and exists only to match FreeType's exact +/// scaling process. +struct ScalingSink26Dot6<'a, S> { + inner: &'a mut S, + scale: Fixed, +} + +impl<'a, S> ScalingSink26Dot6<'a, S> { + fn new(sink: &'a mut S, scale: Fixed) -> Self { + Self { scale, inner: sink } + } + + fn scale(&self, coord: Fixed) -> Fixed { + // The following dance is necessary to exactly match FreeType's + // application of scaling factors. This seems to be the result + // of merging the contributed Adobe code while not breaking the + // FreeType public API. + // + // The first two steps apply to both scaled and unscaled outlines: + // + // 1. Multiply by 1/64 + // + let a = coord * Fixed::from_bits(0x0400); + // 2. Truncate the bottom 10 bits. Combined with the division by 64, + // converts to font units. + // + let b = Fixed::from_bits(a.to_bits() >> 10); + if self.scale != Fixed::ONE { + // Scaled case: + // 3. Multiply by the original scale factor (to 26.6) + // + let c = b * self.scale; + // 4. Convert from 26.6 to 16.16 + Fixed::from_bits(c.to_bits() << 10) + } else { + // Unscaled case: + // 3. Convert from integer to 16.16 + Fixed::from_bits(b.to_bits() << 16) + } + } +} + +impl<'a, S: CommandSink> CommandSink for ScalingSink26Dot6<'a, S> { + fn hstem(&mut self, y: Fixed, dy: Fixed) { + self.inner.hstem(y, dy); + } + + fn vstem(&mut self, x: Fixed, dx: Fixed) { + self.inner.vstem(x, dx); + } + + fn hint_mask(&mut self, mask: &[u8]) { + self.inner.hint_mask(mask); + } + + fn counter_mask(&mut self, mask: &[u8]) { + self.inner.counter_mask(mask); + } + + fn move_to(&mut self, x: Fixed, y: Fixed) { + self.inner.move_to(self.scale(x), self.scale(y)); + } + + fn line_to(&mut self, x: Fixed, y: Fixed) { + self.inner.line_to(self.scale(x), self.scale(y)); + } + + fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { + self.inner.curve_to( + self.scale(cx1), + self.scale(cy1), + self.scale(cx2), + self.scale(cy2), + self.scale(x), + self.scale(y), + ); + } + + fn close(&mut self) { + self.inner.close(); + } +} + +/// Command sink adapter that supresses degenerate move and line commands. +/// +/// FreeType avoids emitting empty contours and zero length lines to prevent +/// artifacts when stem darkening is enabled. We don't support stem darkening +/// because it's not enabled by any of our clients but we remove the degenerate +/// elements regardless to match the output. +/// +/// See +struct NopFilteringSink<'a, S> { + start: Option<(Fixed, Fixed)>, + last: Option<(Fixed, Fixed)>, + pending_move: Option<(Fixed, Fixed)>, + inner: &'a mut S, +} + +impl<'a, S> NopFilteringSink<'a, S> +where + S: CommandSink, +{ + fn new(inner: &'a mut S) -> Self { + Self { + start: None, + last: None, + pending_move: None, + inner, + } + } + + fn flush_pending_move(&mut self) { + if let Some((x, y)) = self.pending_move.take() { + if let Some((last_x, last_y)) = self.start { + if self.last != self.start { + self.inner.line_to(last_x, last_y); + } + } + self.start = Some((x, y)); + self.last = None; + self.inner.move_to(x, y); + } + } + + pub fn finish(&mut self) { + match self.start { + Some((x, y)) if self.last != self.start => { + self.inner.line_to(x, y); + } + _ => {} + } + } +} + +impl<'a, S> CommandSink for NopFilteringSink<'a, S> +where + S: CommandSink, +{ + fn hstem(&mut self, y: Fixed, dy: Fixed) { + self.inner.hstem(y, dy); + } + + fn vstem(&mut self, x: Fixed, dx: Fixed) { + self.inner.vstem(x, dx); + } + + fn hint_mask(&mut self, mask: &[u8]) { + self.inner.hint_mask(mask); + } + + fn counter_mask(&mut self, mask: &[u8]) { + self.inner.counter_mask(mask); + } + + fn move_to(&mut self, x: Fixed, y: Fixed) { + self.pending_move = Some((x, y)); + } + + fn line_to(&mut self, x: Fixed, y: Fixed) { + if self.pending_move == Some((x, y)) { + return; + } + self.flush_pending_move(); + if self.last == Some((x, y)) || (self.last.is_none() && self.start == Some((x, y))) { + return; + } + self.inner.line_to(x, y); + self.last = Some((x, y)); + } + + fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { + self.flush_pending_move(); + self.last = Some((x, y)); + self.inner.curve_to(cx1, cy1, cx2, cy2, x, y); + } + + fn close(&mut self) { + if self.pending_move.is_none() { + self.inner.close(); + self.start = None; + self.last = None; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use read_fonts::FontRef; + + #[test] + fn unscaled_scaling_sink_produces_integers() { + let nothing = &mut (); + let sink = ScalingSink26Dot6::new(nothing, Fixed::ONE); + for coord in [50.0, 50.1, 50.125, 50.5, 50.9] { + assert_eq!(sink.scale(Fixed::from_f64(coord)).to_f32(), 50.0); + } + } + + #[test] + fn scaled_scaling_sink() { + let ppem = 20.0; + let upem = 1000.0; + // match FreeType scaling with intermediate conversion to 26.6 + let scale = Fixed::from_bits((ppem * 64.) as i32) / Fixed::from_bits(upem as i32); + let nothing = &mut (); + let sink = ScalingSink26Dot6::new(nothing, scale); + let inputs = [ + // input coord, expected scaled output + (0.0, 0.0), + (8.0, 0.15625), + (16.0, 0.3125), + (32.0, 0.640625), + (72.0, 1.4375), + (128.0, 2.5625), + ]; + for (coord, expected) in inputs { + assert_eq!( + sink.scale(Fixed::from_f64(coord)).to_f32(), + expected, + "scaling coord {coord}" + ); + } + } + + #[test] + fn read_cff_static() { + let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap(); + let cff = Outlines::new(&font).unwrap(); + assert!(!cff.is_cff2()); + assert!(cff.top_dict.var_store.is_none()); + assert!(cff.top_dict.font_dicts.is_none()); + assert!(cff.top_dict.private_dict_range.is_some()); + assert!(cff.top_dict.fd_select.is_none()); + assert_eq!(cff.subfont_count(), 1); + assert_eq!(cff.subfont_index(GlyphId::new(1)), 0); + assert_eq!(cff.global_subrs().count(), 17); + } + + #[test] + fn read_cff2_static() { + let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap(); + let cff = Outlines::new(&font).unwrap(); + assert!(cff.is_cff2()); + assert!(cff.top_dict.var_store.is_some()); + assert!(cff.top_dict.font_dicts.is_some()); + assert!(cff.top_dict.private_dict_range.is_none()); + assert!(cff.top_dict.fd_select.is_none()); + assert_eq!(cff.subfont_count(), 1); + assert_eq!(cff.subfont_index(GlyphId::new(1)), 0); + assert_eq!(cff.global_subrs().count(), 0); + } + + #[test] + fn read_example_cff2_table() { + let cff = Outlines::from_cff2( + Cff2::read(FontData::new(font_test_data::cff2::EXAMPLE)).unwrap(), + 1000, + ) + .unwrap(); + assert!(cff.is_cff2()); + assert!(cff.top_dict.var_store.is_some()); + assert!(cff.top_dict.font_dicts.is_some()); + assert!(cff.top_dict.private_dict_range.is_none()); + assert!(cff.top_dict.fd_select.is_none()); + assert_eq!(cff.subfont_count(), 1); + assert_eq!(cff.subfont_index(GlyphId::new(1)), 0); + assert_eq!(cff.global_subrs().count(), 0); + } + + #[test] + fn cff2_variable_outlines_match_freetype() { + compare_glyphs( + font_test_data::CANTARELL_VF_TRIMMED, + font_test_data::CANTARELL_VF_TRIMMED_GLYPHS, + ); + } + + #[test] + fn cff_static_outlines_match_freetype() { + compare_glyphs( + font_test_data::NOTO_SERIF_DISPLAY_TRIMMED, + font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS, + ); + } + + /// For the given font data and extracted outlines, parse the extracted + /// outline data into a set of expected values and compare these with the + /// results generated by the scaler. + /// + /// This will compare all outlines at various sizes and (for variable + /// fonts), locations in variation space. + fn compare_glyphs(font_data: &[u8], expected_outlines: &str) { + let font = FontRef::new(font_data).unwrap(); + let outlines = read_fonts::scaler_test::parse_glyph_outlines(expected_outlines); + let scaler = super::Outlines::new(&font).unwrap(); + let mut path = read_fonts::scaler_test::Path::default(); + for expected_outline in &outlines { + if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() { + continue; + } + path.elements.clear(); + let subfont = scaler + .subfont( + scaler.subfont_index(expected_outline.glyph_id), + expected_outline.size, + &expected_outline.coords, + ) + .unwrap(); + scaler + .draw( + &subfont, + expected_outline.glyph_id, + &expected_outline.coords, + false, + &mut path, + ) + .unwrap(); + if path.elements != expected_outline.path { + panic!( + "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}", + expected_outline.glyph_id, + expected_outline.size, + expected_outline.coords, + &path.elements, + &expected_outline.path + ); + } + } + } +} diff --git a/src/scale/mod.rs b/src/scale/mod.rs index 4e05f89..721c265 100644 --- a/src/scale/mod.rs +++ b/src/scale/mod.rs @@ -286,7 +286,7 @@ pub struct ScaleContext { struct State { glyf_scaler: glyf::Scaler, - cff_scaler: cff::Scaler, + cff_cache: cff::SubfontCache, scratch0: Vec, scratch1: Vec, outline: Outline, @@ -308,7 +308,7 @@ impl ScaleContext { fonts: FontCache::new(max_entries), state: State { glyf_scaler: glyf::Scaler::new(max_entries), - cff_scaler: cff::Scaler::new(max_entries), + cff_cache: cff::SubfontCache::new(max_entries), scratch0: Vec::new(), scratch1: Vec::new(), outline: Outline::new(), @@ -417,10 +417,30 @@ impl<'a> ScalerBuilder<'a> { } else { 1. }; + // Handle read-fonts conversion for CFF + let cff = if matches!(&self.proxy.outlines, OutlinesProxy::Cff) { + let font = if self.font.offset == 0 { + read_fonts::FontRef::new(self.font.data).ok() + } else { + // TODO: make this faster + let index = crate::FontDataRef::new(self.font.data).and_then(|font_data| { + font_data + .fonts() + .position(|font| font.offset == self.font.offset) + }); + index.and_then(|index| { + read_fonts::FontRef::from_index(self.font.data, index as u32).ok() + }) + }; + font.and_then(|font| cff::Outlines::new(&font).ok()) + } else { + None + }; Scaler { state: self.state, font: self.font, proxy: self.proxy, + cff, id: self.id, coords: &self.coords[..], size: self.size, @@ -438,6 +458,7 @@ pub struct Scaler<'a> { state: &'a mut State, font: FontRef<'a>, proxy: &'a ScalerProxy, + cff: Option>, id: u64, coords: &'a [i16], size: f32, @@ -517,22 +538,20 @@ impl<'a> Scaler<'a> { _ => &mut self.state.outline, }; match &self.proxy.outlines { - OutlinesProxy::None => false, - OutlinesProxy::Cff(proxy) => { + OutlinesProxy::Cff if self.cff.is_some() => { + let cff_scaler = self.cff.as_ref().unwrap(); outline.begin_layer(color_index); - let mut builder = CffBuilder { outline }; if self .state - .cff_scaler + .cff_cache .scale( - &self.font, + cff_scaler, self.id, + glyph_id, + self.size, &self.coords, - proxy, - self.scale, self.hint, - glyph_id, - &mut builder, + outline, ) .is_some() { @@ -573,6 +592,7 @@ impl<'a> Scaler<'a> { false } } + _ => false, } } @@ -1086,26 +1106,3 @@ fn fill_outline( } Some(()) } - -struct CffBuilder<'a> { - outline: &'a mut Outline, -} - -impl cff::GlyphSink for CffBuilder<'_> { - fn move_to(&mut self, x: f32, y: f32) { - self.outline.move_to(Point::new(x, y)); - } - - fn line_to(&mut self, x: f32, y: f32) { - self.outline.line_to(Point::new(x, y)); - } - - fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) { - self.outline - .curve_to(Point::new(cx1, cy1), Point::new(cx2, cy2), Point::new(x, y)); - } - - fn close(&mut self) { - self.outline.close(); - } -} diff --git a/src/scale/proxy.rs b/src/scale/proxy.rs index 83aa5e2..c17d8ad 100644 --- a/src/scale/proxy.rs +++ b/src/scale/proxy.rs @@ -1,6 +1,7 @@ +use crate::internal::raw_tag; + use super::{ super::{metrics::MetricsProxy, strike::BitmapStrikesProxy, FontRef}, - cff::CffProxy, color::ColorProxy, glyf::GlyfProxy, }; @@ -8,7 +9,7 @@ use super::{ #[derive(Copy, Clone)] pub enum OutlinesProxy { None, - Cff(CffProxy), + Cff, Glyf(GlyfProxy), } @@ -25,8 +26,8 @@ impl ScalerProxy { pub fn from_font(font: &FontRef) -> Self { let outlines = if let Some(glyf) = GlyfProxy::from_font(font) { OutlinesProxy::Glyf(glyf) - } else if let Some(cff) = CffProxy::from_font(font) { - OutlinesProxy::Cff(cff) + } else if font.table(raw_tag(b"CFF ")).is_some() || font.table(raw_tag(b"CFF2")).is_some() { + OutlinesProxy::Cff } else { OutlinesProxy::None };