//! Making a drawn mark answer the base's axis. //! //! A static base is cut once per weight, so a mark is drawn once per weight and //! that is the end of it. A variable base is one file covering `wght` 200 to //! 800, and a glyph spliced into it with no variation data holds still across //! the whole range: correct at the default instance, and progressively wrong //! everywhere else. A bold table border would draw at the light stroke weight. //! //! So the pipeline **keeps the axis** rather than instancing it away. That was //! the open question and it is settled here: instancing is cheaper and throws //! away the thing the base was chosen for, and the set's `weight` term already //! describes a relationship the axis makes continuous instead of sampled at two //! points. The recipes are parametric in the base's own measurements, so a //! master is just the same recipe measured at another location — no second //! drawing, no second source of truth. //! //! What that costs is `gvar`, which this module writes. The base's own entries //! are copied as bytes for the same reason `glyf` is spliced rather than //! recompiled: round-tripping a font's variation data to append to it risks //! changing a face nobody asked us to change. use read_fonts::types::GlyphId; use write_fonts::tables::glyf::SimpleGlyph; use write_fonts::tables::gvar::{GlyphDelta, GlyphDeltas, GlyphVariations, Gvar, Tent}; use write_fonts::types::F2Dot14; use crate::Error; /// The four points every glyph carries past its outline: the two side bearings /// and the two vertical ones. `gvar` counts them, and a tuple that varies the /// outline has to account for them even when they do not move. const PHANTOM_POINTS: usize = 4; /// One mark, drawn at every master. pub struct Varied { /// The drawing at the axis default, which is what goes into `glyf`. pub default: SimpleGlyph, /// `(peak, drawing)` for each master that gets deltas. pub masters: Vec<(f32, SimpleGlyph)>, } impl Varied { /// The per-point offsets from the default drawing, one tuple per master. /// /// Returns `None` when nothing moves anywhere, which is a real answer: a /// mark whose recipe ignores the base's weight has no variation data and /// should not be given an empty tuple that says so at length. pub fn deltas(&self, name: &str) -> Result>, Error> { let default = points(&self.default); let mut out = Vec::new(); for (peak, drawing) in &self.masters { let master = points(drawing); if master.len() != default.len() { // The recipes are parametric, so this cannot happen from a // weight change alone; it would take a recipe that adds a // contour past some threshold. Catching it here beats shipping // a font whose glyph interpolates into a different shape. return Err(Error::Draw(format!( "{name} has {} points at the axis default and {} at {peak}, so the \ two cannot interpolate. A mark's point count has to be the same at \ every master.", default.len(), master.len() ))); } let mut deltas: Vec = default .iter() .zip(&master) .map(|(from, to)| GlyphDelta::required(to.0 - from.0, to.1 - from.1)) .collect(); // The phantom points do not move: a mark takes the cell at every // weight, which is what `hmtx` already says. deltas.extend(std::iter::repeat_n( GlyphDelta::optional(0, 0), PHANTOM_POINTS, )); if deltas.iter().all(|d| d.x == 0 && d.y == 0) { continue; } out.push(GlyphDeltas::new( vec![Tent::new(F2Dot14::from_f32(*peak), None)], deltas, )); } Ok((!out.is_empty()).then_some(out)) } } fn points(glyph: &SimpleGlyph) -> Vec<(i16, i16)> { glyph .contours .iter() .flat_map(|contour| contour.iter().map(|point| (point.x, point.y))) .collect() } /// Compile one glyph's variation data into the bytes `gvar` stores for it. /// /// Done a glyph at a time on purpose. `Gvar::new` pulls any peak tuple used by /// more than one glyph into the table's shared-tuple array and leaves an index /// behind, and an index into *our* shared array means nothing inside the base's /// table. One glyph per call cannot share with anything, so what comes back is /// self-contained and safe to splice. pub fn compile(variations: Vec) -> Result, Error> { let gvar = Gvar::new(vec![GlyphVariations::new(GlyphId::new(0), variations)], 1) .map_err(|e| Error::Font(format!("gvar: {e}")))?; let bytes = write_fonts::dump_table(&gvar).map_err(|e| Error::Font(format!("gvar: {e}")))?; let table = Table::read(&bytes)?; let (start, end) = table.entry(0)?; Ok(table.bytes[start..end].to_vec()) } /// Splice per-glyph variation data onto the end of the base's `gvar`. /// /// The base's glyphs keep the bytes they arrived with, exactly as they keep /// their `glyf` entries. Offsets are rewritten in the long format /// unconditionally: it is one code path, and the short format stores offsets /// halved, so a table that outgrows it mid-splice would need rewriting anyway. pub fn splice(base: &[u8], appended: &[Vec], axis_count: u16) -> Result, Error> { let table = Table::read(base)?; if table.axis_count != axis_count { return Err(Error::Font(format!( "the base's `gvar` varies on {} axes and its `fvar` names {axis_count}", table.axis_count ))); } let mut data = table.bytes[table.data_start..table.data_end()].to_vec(); let mut offsets: Vec = (0..=table.glyph_count) .map(|i| table.offset(i)) .collect::>()?; for entry in appended { data.extend_from_slice(entry); // Every entry starts on an even boundary, which the short offset format // requires and the long one is written to match. if data.len() % 2 != 0 { data.push(0); } offsets.push(data.len() as u32); } let glyph_count = table.glyph_count + appended.len() as u32; let shared = &table.bytes[table.shared_start..table.shared_start + table.shared_len]; let header = 20; let offsets_len = (glyph_count as usize + 1) * 4; let shared_offset = header + offsets_len; let data_offset = shared_offset + shared.len(); let mut out = Vec::with_capacity(data_offset + data.len()); out.extend_from_slice(&0x0001_0000u32.to_be_bytes()); out.extend_from_slice(&axis_count.to_be_bytes()); out.extend_from_slice(&table.shared_count.to_be_bytes()); out.extend_from_slice(&(shared_offset as u32).to_be_bytes()); out.extend_from_slice(&u16::try_from(glyph_count).map_err(too_many)?.to_be_bytes()); // Flag 1: the offsets below are `u32` rather than halved `u16`. out.extend_from_slice(&1u16.to_be_bytes()); out.extend_from_slice(&(data_offset as u32).to_be_bytes()); for offset in &offsets { out.extend_from_slice(&offset.to_be_bytes()); } out.extend_from_slice(shared); out.extend_from_slice(&data); Ok(out) } fn too_many(_: std::num::TryFromIntError) -> Error { Error::Font("a face cannot carry more than 65,535 glyphs".into()) } /// Just enough of `gvar` to copy the parts the pipeline does not rewrite. /// /// read-fonts parses this table into deltas, which is the wrong shape here: the /// point is to move the base's bytes across without decoding them. struct Table<'a> { bytes: &'a [u8], axis_count: u16, shared_count: u16, shared_start: usize, shared_len: usize, glyph_count: u32, long_offsets: bool, offsets_start: usize, data_start: usize, } impl<'a> Table<'a> { fn read(bytes: &'a [u8]) -> Result { let short = || Error::Font("the base's `gvar` is truncated".into()); let u16_at = |at: usize| -> Result { bytes .get(at..at + 2) .map(|b| u16::from_be_bytes([b[0], b[1]])) .ok_or_else(short) }; let u32_at = |at: usize| -> Result { bytes .get(at..at + 4) .map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]])) .ok_or_else(short) }; let axis_count = u16_at(4)?; let shared_count = u16_at(6)?; let shared_start = u32_at(8)? as usize; let glyph_count = u32::from(u16_at(12)?); let long_offsets = u16_at(14)? & 1 == 1; let data_start = u32_at(16)? as usize; Ok(Table { bytes, axis_count, shared_count, shared_start, shared_len: shared_count as usize * axis_count as usize * 2, glyph_count, long_offsets, offsets_start: 20, data_start, }) } /// Offsets are stored halved in the short format, which is what makes a /// misread here silently produce a face whose glyphs are someone else's. fn offset(&self, index: u32) -> Result { let short = || Error::Font("the base's `gvar` offset array is truncated".into()); if self.long_offsets { let at = self.offsets_start + index as usize * 4; self.bytes .get(at..at + 4) .map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]])) .ok_or_else(short) } else { let at = self.offsets_start + index as usize * 2; self.bytes .get(at..at + 2) .map(|b| u32::from(u16::from_be_bytes([b[0], b[1]])) * 2) .ok_or_else(short) } } fn data_end(&self) -> usize { self.data_start + self.offset(self.glyph_count).unwrap_or(0) as usize } /// The byte range one glyph's variation data occupies, relative to the /// start of the data array. fn entry(&self, index: u32) -> Result<(usize, usize), Error> { Ok(( self.data_start + self.offset(index)? as usize, self.data_start + self.offset(index + 1)? as usize, )) } #[cfg(test)] fn glyph_data(&self, index: u32) -> Result<&'a [u8], Error> { let (start, end) = self.entry(index)?; self.bytes .get(start..end) .ok_or_else(|| Error::Font("the base's `gvar` is truncated".into())) } } #[cfg(test)] mod tests { use super::*; use read_fonts::tables::glyf::CurvePoint; use write_fonts::tables::glyf::Contour; fn glyph(points: &[(i16, i16)]) -> SimpleGlyph { let contour: Contour = points .iter() .map(|&(x, y)| CurvePoint::on_curve(x, y)) .collect::>() .into(); SimpleGlyph { contours: vec![contour], ..Default::default() } } fn varied(default: &[(i16, i16)], bold: &[(i16, i16)]) -> Varied { Varied { default: glyph(default), masters: vec![(1.0, glyph(bold))], } } #[test] fn a_mark_that_moves_gets_a_tuple_per_master() { let varied = varied( &[(0, 0), (100, 0), (50, 90)], &[(0, 0), (120, 0), (60, 108)], ); let deltas = varied.deltas("uni25B2").unwrap().expect("it moves"); assert_eq!(deltas.len(), 1); } /// A recipe with no weight term draws the same mark everywhere, and an /// empty tuple would only cost bytes to say so. #[test] fn a_mark_that_holds_still_gets_no_variation_data() { let still = [(0, 0), (100, 0), (50, 90)]; assert!(varied(&still, &still).deltas("uni2588").unwrap().is_none()); } #[test] fn a_mark_that_changes_point_count_is_refused() { let varied = varied(&[(0, 0), (100, 0), (50, 90)], &[(0, 0), (120, 0)]); let err = varied.deltas("uni25B2").unwrap_err().to_string(); assert!(err.contains("interpolate"), "{err}"); } /// The deltas are offsets from the default drawing, and the phantom points /// ride along without moving. #[test] fn the_deltas_are_offsets_from_the_default_drawing() { let varied = varied(&[(10, 10)], &[(30, 4)]); let deltas = varied.deltas("m").unwrap().unwrap(); let first = &deltas[0]; assert_eq!(first.deltas.len(), 1 + PHANTOM_POINTS); assert_eq!((first.deltas[0].x, first.deltas[0].y), (20, -6)); assert!(first.deltas[1..].iter().all(|d| d.x == 0 && d.y == 0)); } /// Compiled data has to stand on its own: a peak tuple embedded in the /// entry rather than an index into a shared array the base does not have. #[test] fn compiled_data_embeds_its_peak_rather_than_sharing_it() { let varied = Varied { default: glyph(&[(0, 0), (100, 0), (50, 90)]), masters: vec![ (1.0, glyph(&[(0, 0), (120, 0), (60, 108)])), (-1.0, glyph(&[(0, 0), (90, 0), (45, 81)])), ], }; let entry = compile(varied.deltas("uni25B2").unwrap().unwrap()).unwrap(); let count = u16::from_be_bytes([entry[0], entry[1]]); // Bit 15 of tupleVariationCount is the shared-point-numbers flag; the // low twelve bits are the count. assert_eq!(count & 0x0FFF, 2, "one tuple per master"); // The first tuple header sits past the count and the data offset; its // `tupleIndex` is the second field of that header. let flags = u16::from_be_bytes([entry[6], entry[7]]); assert_eq!(flags & 0x8000, 0x8000, "EMBEDDED_PEAK_TUPLE"); } /// The point of splicing: the base's own entries survive byte for byte. #[test] fn splicing_leaves_every_base_entry_where_it_was() { let base = Gvar::new( (0..3) .map(|gid| { GlyphVariations::new( GlyphId::new(gid), vec![GlyphDeltas::new( vec![Tent::new(F2Dot14::from_f32(1.0), None)], vec![ GlyphDelta::required(gid as i16 + 1, 0), GlyphDelta::required(0, gid as i16 + 2), ], )], ) }) .collect(), 1, ) .unwrap(); let base = write_fonts::dump_table(&base).unwrap(); let mark = compile( varied( &[(0, 0), (100, 0), (50, 90)], &[(0, 0), (120, 0), (60, 108)], ) .deltas("m") .unwrap() .unwrap(), ) .unwrap(); let out = splice(&base, std::slice::from_ref(&mark), 1).unwrap(); let before = Table::read(&base).unwrap(); let after = Table::read(&out).unwrap(); assert_eq!(after.glyph_count, before.glyph_count + 1); assert!(after.long_offsets); for gid in 0..before.glyph_count { assert_eq!( after.glyph_data(gid).unwrap(), before.glyph_data(gid).unwrap(), "glyph {gid}'s variation data changed" ); } assert_eq!(after.glyph_data(before.glyph_count).unwrap(), mark); } #[test] fn a_gvar_whose_axis_count_disagrees_with_fvar_is_refused() { let base = Gvar::new(vec![GlyphVariations::new(GlyphId::new(0), vec![])], 1).unwrap(); let base = write_fonts::dump_table(&base).unwrap(); assert!(splice(&base, &[], 2).is_err()); } }