| 1 | use canvas::layout::TextEngine; |
| 2 | use onestore::document::Format; |
| 3 | use onestore::page::text::Paragraph; |
| 4 | use serde::{Deserialize, Serialize}; |
| 5 | use serde_json::json; |
| 6 | use skrifa::{FontRef, MetadataProvider, raw::TableProvider, string::StringId}; |
| 7 | use std::{env, fs, io, sync::Arc}; |
| 8 | |
| 9 | #[derive(Deserialize)] |
| 10 | #[serde(deny_unknown_fields)] |
| 11 | struct Case { |
| 12 | id: String, |
| 13 | width: f32, |
| 14 | runs: Vec<Run>, |
| 15 | } |
| 16 | |
| 17 | #[derive(Deserialize, Serialize)] |
| 18 | #[serde(deny_unknown_fields)] |
| 19 | struct Run { |
| 20 | text: String, |
| 21 | font: String, |
| 22 | size: f32, |
| 23 | bold: bool, |
| 24 | italic: bool, |
| 25 | } |
| 26 | |
| 27 | fn 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 | } |