Skip to main content

alpha2_layout/
json.rs

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