mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 00:17:42 +00:00
Type: tests compare opened documents with and without kept shaping plans
A new test binary opens a document of 13 type objects with the plans a font face keeps and again while every shaped segment makes its own plan. The document, its cached bounds and baselines included, and the first frame's pixels must be the same. `test_fonts::keep_shape_plans` switches the plans off; it exists only in tests and with the `test-fonts` feature. The engine's open test now builds objects of one script and direction that differ in kerning and language, and lays each one out in a font database of its own, so a plan used for the wrong features fails its comparison. It no longer sets Arabic, which could fall back to a system font in one database and not the other.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<u8> {
|
||||
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<u8> {
|
||||
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");
|
||||
}
|
||||
@@ -244,6 +244,10 @@ impl FontFace {
|
||||
language: Option<&harfrust::Language>,
|
||||
features: &[harfrust::Feature],
|
||||
) -> Arc<harfrust::ShapePlan> {
|
||||
#[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<harfrust::ShapePlan>]| plans.iter().rev().find(|p| key.matches(p)).cloned();
|
||||
if let Some(plan) = kept(&self.plans.lock().unwrap_or_else(|e| e.into_inner())) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user