diff --git a/crates/engine/src/tests/links.rs b/crates/engine/src/tests/links.rs index c4d9f47c8..a4c222090 100644 --- a/crates/engine/src/tests/links.rs +++ b/crates/engine/src/tests/links.rs @@ -503,23 +503,29 @@ fn a_file_read_for_opening_has_its_links_read_and_its_type_laid_out() { } /// Opening lays out each type object with the plans the objects before it left on the font's -/// face: its bounds and baselines are those of a layout in a font database that holds no plans. +/// face: its bounds and baselines are those of a layout in a font database of its own, which holds +/// no plans. The objects share a script and direction and differ in features and language. Only +/// bundled Latin glyphs are set, which every font database holds. #[test] fn type_read_for_opening_has_the_layout_of_a_font_database_with_no_plans() { let dir = Folder::new("read-for-open-plans"); - let mut s = session(); - for (i, text) in ["AVATAR", "To Wa", "123 ...", "office", "\u{645}\u{631}\u{62d}\u{628}\u{627}"].iter().enumerate() { - s.execute("text.create", &json!({"x": 20, "y": 40 + i * 20, "text": text})).unwrap(); + let style = vectorcraft_doc::CharStyle { size: 100.0, ..Default::default() }; + let manual = vectorcraft_doc::CharStyle { kerning: Some(0.0), ..style.clone() }; + let turkish = vectorcraft_doc::CharStyle { language: Some("tr".into()), ..style.clone() }; + let mut doc = Document::new(800.0, 600.0); + let layer = doc.layers[0].id; + for (text, style) in [("AVAV", &style), ("AVAV", &manual), ("office", &turkish), ("To Wa", &style), ("To Wa", &manual)] { + let t = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(20.0, 120.0), text, style.clone()); + let id = doc.alloc_id(); + doc.insert(Some(layer), usize::MAX, vectorcraft_doc::Node::new(id, NodeKind::Text(Box::new(t)))).unwrap(); } let doc_path = dir.file("type.vectorcraft"); - save(&mut s, &doc_path); - let bytes = std::fs::read(&doc_path).unwrap(); + let bytes = vectorcraft_format::save_file(&doc); let file = cmd::fileio::read_for_open(&doc_path, &bytes, Some(doc_path.clone()), &Value::Null, false, false).unwrap(); - let new_db = vectorcraft_text::FontDb::with_font_dirs(vec![]); let mut seen = 0; file.document().walk(|node| { if let NodeKind::Text(t) = &node.kind { - let fresh = vectorcraft_text::layout(&new_db, t); + let fresh = vectorcraft_text::layout(&vectorcraft_text::FontDb::with_font_dirs(vec![]), t); assert_eq!(t.cached_bounds, Some(fresh.bounds), "{:?}", t.plain_text()); assert_eq!(t.cached_baselines, fresh.baselines(), "{:?}", t.plain_text()); seen += 1; diff --git a/crates/engine/tests/open_type_plans.rs b/crates/engine/tests/open_type_plans.rs new file mode 100644 index 000000000..6ee4c99e6 --- /dev/null +++ b/crates/engine/tests/open_type_plans.rs @@ -0,0 +1,123 @@ +//! A document of type opened with the shaping plans font faces keep, compared with the same file +//! opened while every shaped segment makes a plan of its own: the nodes and the first frame's +//! pixels are the same. Its own test binary: the switch for kept plans applies to the whole process. +// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness). +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use std::sync::{Mutex, MutexGuard, Once}; + +use serde_json::Value; +use vectorcraft_doc::{CharStyle, Document, Node, NodeKind, TextObject, TextRun}; +use vectorcraft_engine::Session; +use vectorcraft_engine::cmd::fileio; +use vectorcraft_geom::{Affine, Point}; +use vectorcraft_render::{RenderOptions, Renderer}; +use vectorcraft_text::FontDb; +use vectorcraft_text::test_fonts::{VARIABLE_FAMILY, keep_shape_plans, variable_font}; + +const WIDTH: u32 = 900; +const HEIGHT: u32 = 1300; + +/// Taken by each test: the switch for kept plans applies to the whole process. +static SWITCH: Mutex<()> = Mutex::new(()); + +fn switch() -> MutexGuard<'static, ()> { + SWITCH.lock().unwrap_or_else(|e| e.into_inner()) +} + +fn style() -> CharStyle { + CharStyle { size: 60.0, ..CharStyle::default() } +} + +/// A saved document of type objects whose shaping differs in script, direction, language, features +/// and variable font instance, the object in the plain style first. +fn file() -> Vec { + static ADD_FONT: Once = Once::new(); + ADD_FONT.call_once(|| { + FontDb::global().add_font(variable_font().unwrap()); + }); + let manual = CharStyle { kerning: Some(0.0), ..style() }; + let variable = |name: &str| CharStyle { font_family: VARIABLE_FAMILY.into(), font_style: name.into(), ..style() }; + let mut mixed = TextObject::point(Point::ZERO, "AVAV", style()); + mixed.runs = vec![TextRun::new("AV", style()), TextRun::new("AV", manual.clone())]; + let mut vertical = TextObject::point(Point::ZERO, "AB\u{65e5}\u{672c}", style()); + vertical.vertical = true; + let objects = [ + TextObject::point(Point::ZERO, "AVATAR office", style()), + TextObject::point(Point::ZERO, "To Wa fi", style()), + TextObject::point(Point::ZERO, "123 ... 456", style()), + TextObject::point(Point::ZERO, "AVAV", manual), + mixed, + TextObject::point(Point::ZERO, "fi office", CharStyle { language: Some("tr".into()), ..style() }), + TextObject::point(Point::ZERO, "AVAV", CharStyle { optical_kerning: true, ..style() }), + TextObject::point(Point::ZERO, "Small Caps", CharStyle { small_caps: Some(70.0), ..style() }), + TextObject::point(Point::ZERO, "\u{645}\u{631}\u{62d}\u{628}\u{627}", style()), + TextObject::point(Point::ZERO, "\u{5e9}\u{5dc}\u{5d5}\u{5dd}", style()), + TextObject::point(Point::ZERO, "lIn lIn", variable("Regular")), + TextObject::point(Point::ZERO, "lIn lIn", variable("Bold")), + vertical, + ]; + let mut doc = Document::new(WIDTH as f64, HEIGHT as f64); + let layer = doc.layers[0].id; + let count = objects.len(); + for (i, mut t) in objects.into_iter().enumerate() { + // Rows down the page; the vertical object, last, stands in the right margin. + t.xf = Affine::translate(if i + 1 == count { (800.0, 60.0) } else { (20.0, 90.0 * (i + 1) as f64) }); + let id = doc.alloc_id(); + doc.insert(Some(layer), usize::MAX, Node::new(id, NodeKind::Text(Box::new(t)))).unwrap(); + } + vectorcraft_format::save_file(&doc) +} + +/// The session `bytes` opened in, as `document.open` reads a file and then opens it. +fn open(bytes: &[u8]) -> Session { + let name = "plans.vectorcraft"; + let mut s = Session::new(); + let read = fileio::read_for_open(name, bytes, Some(name.into()), &Value::Null, false, false).unwrap(); + fileio::open_read(&mut s, read).unwrap(); + s +} + +fn first_frame(doc: &Document) -> Vec { + Renderer::new().render(doc, WIDTH, HEIGHT, Affine::IDENTITY, &RenderOptions::default()).pixels +} + +#[test] +fn an_opened_document_has_the_nodes_of_one_laid_out_with_plans_of_their_own() { + let _switch = switch(); + let bytes = file(); + keep_shape_plans(false); + let own = open(&bytes); + keep_shape_plans(true); + // The second open shapes every object with plans a face kept. + open(&bytes); + let kept = open(&bytes); + let (own, kept) = (own.doc().unwrap(), kept.doc().unwrap()); + let mut laid_out = 0; + kept.doc.walk(|node| { + if let NodeKind::Text(t) = &node.kind { + assert!(t.cached_bounds.is_some(), "{:?}", t.plain_text()); + laid_out += 1; + } + }); + assert_eq!(laid_out, 13); + assert_eq!(kept.doc.layers, own.doc.layers); + assert!(*kept.doc == *own.doc, "the whole document is the same"); +} + +#[test] +fn an_opened_document_draws_the_first_frame_of_one_shaped_with_plans_of_its_own() { + let _switch = switch(); + let bytes = file(); + let opened = open(&bytes); + let doc = &opened.doc().unwrap().doc; + keep_shape_plans(false); + let own = first_frame(doc); + keep_shape_plans(true); + // Lays out each object with plans a face kept: the first frame lays type out again. + first_frame(doc); + let kept = first_frame(doc); + assert_ne!(kept, first_frame(&Document::new(WIDTH as f64, HEIGHT as f64)), "the frame shows the type"); + let differing = kept.iter().zip(&own).filter(|(a, b)| a != b).count(); + assert_eq!((kept.len(), differing), (own.len(), 0), "bytes of the frame that differ"); +} diff --git a/crates/text/src/fontdb/face.rs b/crates/text/src/fontdb/face.rs index bd5090df0..5ab31eeae 100644 --- a/crates/text/src/fontdb/face.rs +++ b/crates/text/src/fontdb/face.rs @@ -244,6 +244,10 @@ impl FontFace { language: Option<&harfrust::Language>, features: &[harfrust::Feature], ) -> Arc { + #[cfg(any(test, feature = "test-fonts"))] + if !crate::test_fonts::keeps_shape_plans() { + return Arc::new(harfrust::ShapePlan::new(shaper, direction, script, language, features)); + } let key = harfrust::ShapePlanKey::new(script, direction).language(language).instance(self.instance.as_ref()).features(features); let kept = |plans: &[Arc]| plans.iter().rev().find(|p| key.matches(p)).cloned(); if let Some(plan) = kept(&self.plans.lock().unwrap_or_else(|e| e.into_inner())) { diff --git a/crates/text/src/test_fonts.rs b/crates/text/src/test_fonts.rs index ebdd08573..69ccbfe70 100644 --- a/crates/text/src/test_fonts.rs +++ b/crates/text/src/test_fonts.rs @@ -1,6 +1,8 @@ //! Fonts built in code for tests, so no font file is committed: a small variable font (#296). //! Compiled for this crate's tests and, with the `test-fonts` feature, for other crates' tests. +use std::sync::atomic::{AtomicBool, Ordering}; + use skrifa::instance::{LocationRef, Size}; use skrifa::raw::TableProvider; use skrifa::raw::tables::glyf::Glyph; @@ -10,6 +12,20 @@ use write_fonts::tables::fvar::{AxisInstanceArrays, Fvar, InstanceRecord, Variat use write_fonts::tables::gvar::{GlyphDelta, GlyphDeltas, GlyphVariations, Gvar, Tent}; use write_fonts::types::{F2Dot14, Fixed, NameId, Tag}; +/// Whether faces use and keep shaping plans ([`keep_shape_plans`]). +static KEEP_SHAPE_PLANS: AtomicBool = AtomicBool::new(true); + +/// Sets whether font faces use and keep shaping plans, for the whole process. When `keep` is false, +/// every shaped segment makes a plan of its own, so a test can compare what kept plans produce with +/// what its own plan produces. Plans a face already holds stay and are used again when it is true. +pub fn keep_shape_plans(keep: bool) { + KEEP_SHAPE_PLANS.store(keep, Ordering::SeqCst); +} + +pub(crate) fn keeps_shape_plans() -> bool { + KEEP_SHAPE_PLANS.load(Ordering::SeqCst) +} + /// The family of [`variable_font`]. pub const VARIABLE_FAMILY: &str = "Varitest Sans";