1use canvas::layout::TextEngine;
2use onestore::document::Format;
3use onestore::page::text::Paragraph;
4use serde::{Deserialize, Serialize};
5use serde_json::json;
6use skrifa::{FontRef, MetadataProvider, raw::TableProvider, string::StringId};
7use std::{env, fs, io, sync::Arc};
8
9#[derive(Deserialize)]
10#[serde(deny_unknown_fields)]
11struct Case {
12 id: String,
13 width: f32,
14 runs: Vec<Run>,
15}
16
17#[derive(Deserialize, Serialize)]
18#[serde(deny_unknown_fields)]
19struct Run {
20 text: String,
21 font: String,
22 size: f32,
23 bold: bool,
24 italic: bool,
25}
26
27fn main() -> Result<(), Box<dyn std::error::Error>> {
28 let mut args = env::args_os().skip(1);
29 let path = args.next().ok_or("Expected a text-case JSON file")?;
30 let cases: Vec<Case> = serde_json::from_slice(&fs::read(path)?)?;
31 let mut engine = TextEngine::default();
32 while let Some(path) = args.next() {
33 if path == "--substitute-font" {
34 let path = args
35 .next()
36 .ok_or("Provide a font file after --substitute-font.")?;
37 engine.register_substitute(parley::fontique::Blob::new(Arc::new(fs::read(path)?)))?;
38 continue;
39 }
40 let data = parley::fontique::Blob::new(Arc::new(fs::read(path)?));
41 if engine
42 .fonts
43 .collection
44 .register_fonts(data, None)
45 .is_empty()
46 {
47 return Err("The supplied file contains no supported fonts".into());
48 }
49 }
50 let mut results = Vec::new();
51 for case in cases {
52 if !case.width.is_finite() || case.width <= 0.0 || case.runs.is_empty() {
53 return Err(
54 format!("{}: expected positive width and at least one run", case.id).into(),
55 );
56 }
57 let text: String = case.runs.iter().map(|r| r.text.as_str()).collect();
58 let paragraph = Paragraph::from_runs(case.runs.iter().map(|run| {
59 (
60 run.text.clone(),
61 Format {
62 font: Some(run.font.clone()),
63 font_size: Some(run.size),
64 bold: Some(run.bold),
65 italic: Some(run.italic),
66 ..Format::default()
67 },
68 )
69 }));
70 let layout = engine
71 .layout(&paragraph, case.width)
72 .map_err(|error| format!("{}: {error}", case.id))?;
73 let lines: Vec<_> = layout.lines().map(|(line, native)| {
74 let range = native.source.clone();
75 let metrics = line.metrics();
76 let runs: Vec<_> = line.runs().map(|run| {
77 let data = &run.font().font;
78 let font = FontRef::from_index(data.data.as_ref(), data.index)
79 .expect("Parley returned an unreadable font");
80 let os2 = font.os2().ok();
81 json!({
82 "face": font.localized_strings(StringId::POSTSCRIPT_NAME).english_or_first().map(|s| s.to_string()),
83 "version": font.localized_strings(StringId::VERSION_STRING).english_or_first().map(|s| s.to_string()),
84 "size": run.font_size(),
85 "normalized_variation_coords": run.normalized_coords().iter().map(|c| c.to_bits()).collect::<Vec<_>>(),
86 "units_per_em": font.head().ok().map(|h| h.units_per_em()),
87 "win_ascent": os2.as_ref().map(|o| o.us_win_ascent()),
88 "win_descent": os2.as_ref().map(|o| o.us_win_descent()),
89 })
90 }).collect();
91 json!({
92 "start_utf16": text[..range.start].encode_utf16().count(),
93 "end_utf16": text[..range.end].encode_utf16().count(),
94 "text": &text[range],
95 "advance": metrics.advance,
96 "trailing_whitespace": metrics.trailing_whitespace,
97 "baseline": metrics.baseline,
98 "height": metrics.line_height,
99 "windows_baseline": native.baseline,
100 "windows_height": native.height,
101 "runs": runs,
102 })
103 }).collect();
104 results.push(
105 json!({"id": case.id, "width": case.width, "requested_runs": case.runs,
106 "height": lines.iter().map(|l| l["height"].as_f64().unwrap()).sum::<f64>(),
107 "windows_height": layout.height(), "lines": lines}),
108 );
109 }
110 serde_json::to_writer_pretty(
111 io::stdout().lock(),
112 &json!({
113 "engine": "parley", "units": "points", "cases": results,
114 }),
115 )?;
116 Ok(())
117}