Skip to main content

alpha2_layout/
json.rs

1//! Measurements as JSON, from generators as a project holds them: the one
2//! form the wasm binding and the Python package return.
3
4use std::fmt;
5
6use alpha2_text::{TextRasterError, TextRasterizer};
7use alpha2_time::MediaTime;
8
9use crate::{Baselines, CanvasRect, PictureSize};
10
11/// Why a generator could not be measured.
12#[derive(Debug)]
13#[non_exhaustive]
14pub enum MeasureError {
15    /// Not a generator of the kind measured that a project could hold, or
16    /// no picture.
17    Input(String),
18    /// The text host refused: no fonts, say.
19    Text(TextRasterError),
20}
21
22impl fmt::Display for MeasureError {
23    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            Self::Input(message) => formatter.write_str(message),
26            Self::Text(error) => write!(formatter, "text failed: {error}"),
27        }
28    }
29}
30
31impl std::error::Error for MeasureError {}
32
33fn baselines(baselines: Baselines) -> serde_json::Value {
34    serde_json::json!({ "first": baselines.first, "last": baselines.last })
35}
36
37fn rect(rect: CanvasRect) -> serde_json::Value {
38    serde_json::json!({
39        "left": rect.left,
40        "top": rect.top,
41        "right": rect.right,
42        "bottom": rect.bottom,
43    })
44}
45
46/// Where the text generator `generator_json`, as a project holds it, lies on
47/// a `width` by `height` canvas ([`crate::measure_text`]), as JSON
48/// `{ layout, content, ink, lineCount, baselines }`: each a `{ left, top,
49/// right, bottom }` rectangle in fractions of the canvas, `ink` `null` when
50/// the text draws nothing, and `baselines` `{ first, last }` in fractions of
51/// the canvas height.
52///
53/// # Errors
54/// Returns [`MeasureError::Input`] for JSON that is no valid text generator
55/// or an empty canvas, and [`MeasureError::Text`] for the text host's
56/// refusal.
57pub fn measure_text_json(
58    rasterizer: &TextRasterizer,
59    generator_json: &str,
60    width: u32,
61    height: u32,
62) -> Result<String, MeasureError> {
63    let generator: alpha2_model::Generator = serde_json::from_str(generator_json)
64        .map_err(|error| MeasureError::Input(format!("invalid text generator: {error}")))?;
65    if !generator.is_valid() {
66        return Err(MeasureError::Input(
67            "the text generator has a field out of range".into(),
68        ));
69    }
70    let alpha2_model::Generator::Text { text, style } = &generator else {
71        return Err(MeasureError::Input(
72            "only a text generator draws text".into(),
73        ));
74    };
75    let size = PictureSize::new(width, height)
76        .map_err(|error| MeasureError::Input(format!("invalid canvas size: {error}")))?;
77    let bounds = crate::measure_text(rasterizer, text, style, size).map_err(MeasureError::Text)?;
78    Ok(serde_json::json!({
79        "layout": rect(bounds.layout),
80        "content": rect(bounds.content),
81        "ink": bounds.ink.map(rect),
82        "lineCount": bounds.line_count,
83        "baselines": baselines(bounds.baselines),
84    })
85    .to_string())
86}
87
88/// Where each node of the layout generator `generator_json`, as a project
89/// holds it, lies on a `width` by `height` picture at picture time `time`
90/// ([`crate::measure_layout`]), as JSON `{ nodes }`: each node in preorder
91/// as `{ at, border, content, overflow, lineCount, baselines }`, its boxes
92/// in fractions of the picture, `lineCount` `null` for a node without text
93/// and `baselines` (`{ first, last }`, fractions of the picture height)
94/// `null` for a node without text or with empty text. `canvas` is
95/// the project canvas its pixels are pixels of: the picture's size when
96/// `None`.
97///
98/// # Errors
99/// Returns [`MeasureError::Input`] for JSON that is no valid layout
100/// generator on that canvas, or an empty picture or canvas, and
101/// [`MeasureError::Text`] for the text host's refusal.
102pub fn measure_layout_json(
103    rasterizer: &TextRasterizer,
104    generator_json: &str,
105    width: u32,
106    height: u32,
107    time: MediaTime,
108    canvas: Option<(u32, u32)>,
109) -> Result<String, MeasureError> {
110    let generator: alpha2_model::Generator = serde_json::from_str(generator_json)
111        .map_err(|error| MeasureError::Input(format!("invalid layout generator: {error}")))?;
112    let size = PictureSize::new(width, height)
113        .map_err(|error| MeasureError::Input(format!("invalid canvas size: {error}")))?;
114    let (canvas_width, canvas_height) = canvas.unwrap_or((width, height));
115    PictureSize::new(canvas_width, canvas_height)
116        .map_err(|error| MeasureError::Input(format!("invalid project canvas: {error}")))?;
117    if let Err(issue) = generator.validate_on(canvas_width, canvas_height) {
118        return Err(MeasureError::Input(match issue {
119            alpha2_model::GeneratorIssue::Layout(issue) => {
120                format!("the layout generator {} {}", issue.at, issue.problem)
121            }
122            alpha2_model::GeneratorIssue::Shape(issue) => {
123                format!(
124                    "the layout generator's shape {} {}",
125                    issue.at, issue.problem
126                )
127            }
128            alpha2_model::GeneratorIssue::Invalid => {
129                "the layout generator has a field out of range".into()
130            }
131        }));
132    }
133    let alpha2_model::Generator::Layout { root } = &generator else {
134        return Err(MeasureError::Input(
135            "only a layout generator lays out boxes".into(),
136        ));
137    };
138    let bounds = crate::measure_layout(rasterizer, root, time, size, (canvas_width, canvas_height))
139        .map_err(MeasureError::Text)?;
140    let nodes = bounds
141        .nodes
142        .into_iter()
143        .map(|node| {
144            serde_json::json!({
145                "at": node.at,
146                "border": rect(node.border),
147                "content": rect(node.content),
148                "overflow": node.overflow,
149                "lineCount": node.line_count,
150                "baselines": node.baselines.map(baselines),
151            })
152        })
153        .collect::<Vec<_>>();
154    Ok(serde_json::json!({ "nodes": nodes }).to_string())
155}
156
157#[cfg(test)]
158#[path = "json_tests.rs"]
159mod tests;