Skip to main content

csp_format/
text.rs

1//! Text layers: `TextLayerString` (UTF-8) and its attributes.
2//!
3//! `TextLayerAttributes` is a little-endian list of records `u32 id, u32
4//! length, bytes` (spec: `docs/spec/06-text.md`). A layer with several text
5//! blocks (balloons) keeps them in `TextLayerStringArray` /
6//! `TextLayerAttributesArray`: `u32le length` + string / attribute list per
7//! block. Records whose ids this module does not decode are kept as bytes,
8//! so the attributes are written back unchanged.
9
10use crate::binary::{Reader, Writer};
11use crate::error::{Result, bail};
12
13/// Record ids whose meaning is known (see the spec for the confidence of each).
14pub mod ids {
15    /// Character runs with a font size (u32 count, then runs).
16    pub const RUN_FONT_SIZE: u32 = 11;
17    /// Font size × 100 in points (u32).
18    pub const FONT_SIZE: u32 = 32;
19    /// Text block bounding box, layer pixels (i32 × 4: x0, y0, x1, y1).
20    pub const BBOX: u32 = 42;
21    /// The balloon the block sits in: 1-based index of the layer's vector
22    /// objects, whose bbox centre the block's bbox is relative to (0 = none,
23    /// the bbox is in layer pixels).
24    pub const BALLOON: u32 = 45;
25    /// Id of the offscreen CSP rendered the text into (u32; 0 = render again).
26    pub const RENDER_OFFSCREEN: u32 = 50;
27    /// Font (u16 ?, u16 length, UTF-8 name, u16 length, PostScript name).
28    pub const FONT: u32 = 57;
29    /// Text block width and height in pixels (i32 × 2).
30    pub const BLOCK_SIZE: u32 = 63;
31    /// Four corners in 1/100 px relative to the anchor (i32 × 8).
32    pub const QUAD: u32 = 64;
33}
34
35/// One attribute record.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Record {
38    /// Record id (see [`ids`]).
39    pub id: u32,
40    /// Record bytes, as stored.
41    pub data: Vec<u8>,
42}
43
44/// A parsed attribute list.
45#[derive(Debug, Clone, PartialEq, Eq, Default)]
46pub struct Attributes {
47    /// The records, in stored order.
48    pub records: Vec<Record>,
49}
50
51/// Font name pair of record 57.
52#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
53pub struct Font {
54    /// First field of the record, not decoded (kept on writing).
55    pub lead: u16,
56    /// Font name as CSP shows it.
57    pub name: String,
58    /// PostScript name of the font.
59    pub postscript_name: String,
60}
61
62impl Attributes {
63    /// Parses a `TextLayerAttributes` blob.
64    pub fn parse(blob: &[u8]) -> Result<Self> {
65        let mut reader = Reader::new(blob);
66        let mut records = Vec::new();
67        while !reader.is_empty() {
68            let id = reader.le_u32()?;
69            let length = reader.le_u32()? as usize;
70            if length > reader.remaining() {
71                bail!("Text attribute {id} says {length} bytes, {} remain", reader.remaining());
72            }
73            records.push(Record { id, data: reader.bytes(length)?.to_vec() });
74        }
75        Ok(Attributes { records })
76    }
77
78    /// The blob, records in the same order (unchanged records give the same bytes).
79    pub fn to_bytes(&self) -> Vec<u8> {
80        let mut writer = Writer::new();
81        for record in &self.records {
82            writer.le_u32(record.id).le_u32(record.data.len() as u32).bytes(&record.data);
83        }
84        writer.into_inner()
85    }
86
87    /// The bytes of the first record with this id.
88    pub fn get(&self, id: u32) -> Option<&[u8]> {
89        self.records.iter().find(|record| record.id == id).map(|record| record.data.as_slice())
90    }
91
92    /// Replaces the first record with this id, or appends one.
93    pub fn set(&mut self, id: u32, data: Vec<u8>) {
94        match self.records.iter_mut().find(|record| record.id == id) {
95            Some(record) => record.data = data,
96            None => self.records.push(Record { id, data }),
97        }
98    }
99
100    fn i32s(&self, id: u32) -> Option<Vec<i32>> {
101        let data = self.get(id)?;
102        Some(data.as_chunks::<4>().0.iter().map(|b| i32::from_le_bytes(*b)).collect())
103    }
104
105    fn set_i32s(&mut self, id: u32, values: &[i32]) {
106        self.set(id, values.iter().flat_map(|value| value.to_le_bytes()).collect());
107    }
108
109    /// Font size in points.
110    pub fn font_size(&self) -> Option<f64> {
111        self.i32s(ids::FONT_SIZE)?.first().map(|value| *value as f64 / 100.0)
112    }
113
114    /// Text block bounding box in layer pixels (record 42: x0, y0, x1, y1).
115    pub fn bbox(&self) -> Option<[i32; 4]> {
116        self.i32s(ids::BBOX)?.try_into().ok()
117    }
118
119    /// Sets record 42.
120    pub fn set_bbox(&mut self, bbox: [i32; 4]) {
121        self.set_i32s(ids::BBOX, &bbox);
122    }
123
124    /// Text block width and height in pixels (record 63).
125    pub fn block_size(&self) -> Option<[i32; 2]> {
126        self.i32s(ids::BLOCK_SIZE)?.try_into().ok()
127    }
128
129    /// Four corners in 1/100 px relative to the anchor (record 64).
130    pub fn quad(&self) -> Option<[i32; 8]> {
131        self.i32s(ids::QUAD)?.try_into().ok()
132    }
133
134    /// The 1-based index of the balloon object the block sits in (record 45).
135    pub fn balloon(&self) -> Option<u32> {
136        self.i32s(ids::BALLOON)?.first().map(|value| *value as u32).filter(|value| *value > 0)
137    }
138
139    /// Id of the offscreen CSP rendered the text into (record 50; 0 = render again).
140    pub fn render_offscreen(&self) -> Option<u32> {
141        self.i32s(ids::RENDER_OFFSCREEN)?.first().map(|value| *value as u32)
142    }
143
144    /// Make CSP render the text again on opening (needed after changing it).
145    pub fn clear_render_offscreen(&mut self) {
146        if self.get(ids::RENDER_OFFSCREEN).is_some() {
147            self.set_i32s(ids::RENDER_OFFSCREEN, &[0]);
148        }
149    }
150
151    /// The font (record 57).
152    pub fn font(&self) -> Option<Font> {
153        let data = self.get(ids::FONT)?;
154        let mut reader = Reader::new(data);
155        let lead = reader.le_u16().ok()?;
156        let length = reader.le_u16().ok()? as usize;
157        let name = String::from_utf8_lossy(reader.bytes(length).ok()?).into_owned();
158        let length = reader.le_u16().ok()? as usize;
159        let postscript_name = String::from_utf8_lossy(reader.bytes(length).ok()?).into_owned();
160        Some(Font { lead, name, postscript_name })
161    }
162
163    /// Move and scale the geometry records (bbox, block size, quad).
164    pub fn transform(&mut self, scale: f64, dx: f64, dy: f64) {
165        if let Some(b) = self.bbox() {
166            self.set_bbox([
167                (b[0] as f64 * scale + dx).floor() as i32,
168                (b[1] as f64 * scale + dy).floor() as i32,
169                (b[2] as f64 * scale + dx).ceil() as i32,
170                (b[3] as f64 * scale + dy).ceil() as i32,
171            ]);
172        }
173        if let Some(size) = self.block_size() {
174            self.set_i32s(ids::BLOCK_SIZE, &size.map(|v| (v as f64 * scale).round() as i32));
175        }
176        if let Some(quad) = self.quad() {
177            self.set_i32s(ids::QUAD, &quad.map(|v| (v as f64 * scale).round() as i32));
178        }
179    }
180}
181
182/// Split a `…Array` column: `u32le length` + bytes, repeated.
183pub fn split_array(blob: &[u8]) -> Result<Vec<&[u8]>> {
184    let mut reader = Reader::new(blob);
185    let mut parts = Vec::new();
186    while !reader.is_empty() {
187        let length = reader.le_u32()? as usize;
188        if length > reader.remaining() {
189            bail!("Array entry of {length} bytes, {} remain", reader.remaining());
190        }
191        parts.push(reader.bytes(length)?);
192    }
193    Ok(parts)
194}
195
196/// Joins parts into a `…Array` column (the inverse of [`split_array`]).
197pub fn join_array<T: AsRef<[u8]>>(parts: &[T]) -> Vec<u8> {
198    let mut writer = Writer::new();
199    for part in parts {
200        writer.le_u32(part.as_ref().len() as u32).bytes(part.as_ref());
201    }
202    writer.into_inner()
203}
204
205/// One text block of a text layer.
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct TextBlock {
208    /// The text (UTF-8, `\n` between lines).
209    pub text: String,
210    /// Its attribute records.
211    pub attributes: Attributes,
212}
213
214/// The text blocks of a layer: the single string and attributes first,
215/// then one block per entry of the arrays. The arrays hold the other
216/// blocks, not a copy of the first: in the corpus the single block always
217/// has its own bounding box and rendered image (V7).
218pub fn read_blocks(row: &crate::database::Row) -> Result<Vec<TextBlock>> {
219    let mut blocks = Vec::new();
220    let (text, attributes) = (row.blob("TextLayerString"), row.blob("TextLayerAttributes"));
221    if text.is_some() || attributes.is_some() {
222        blocks.push(block(text.unwrap_or_default(), attributes)?);
223    }
224    let strings = row.blob("TextLayerStringArray").map(split_array).transpose()?.unwrap_or_default();
225    let attributes = row.blob("TextLayerAttributesArray").map(split_array).transpose()?.unwrap_or_default();
226    for index in 0..strings.len().max(attributes.len()) {
227        blocks.push(block(strings.get(index).copied().unwrap_or_default(), attributes.get(index).copied())?);
228    }
229    Ok(blocks)
230}
231
232fn block(text: &[u8], attributes: Option<&[u8]>) -> Result<TextBlock> {
233    let attributes = match attributes {
234        Some(blob) => Attributes::parse(blob)?,
235        None => Attributes::default(),
236    };
237    Ok(TextBlock { text: String::from_utf8_lossy(text).into_owned(), attributes })
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn attributes_round_trip_and_decode() {
246        let mut attributes = Attributes::default();
247        attributes.set(ids::FONT_SIZE, 600i32.to_le_bytes().to_vec());
248        attributes.set_bbox([1, 2, 30, 40]);
249        attributes.set(99, vec![1, 2, 3]);
250        let bytes = attributes.to_bytes();
251        let parsed = Attributes::parse(&bytes).unwrap();
252        assert_eq!(parsed, attributes);
253        assert_eq!(parsed.font_size(), Some(6.0));
254        assert_eq!(parsed.bbox(), Some([1, 2, 30, 40]));
255        assert!(Attributes::parse(&bytes[..bytes.len() - 1]).is_err());
256        let array = join_array(&[bytes.clone(), vec![]]);
257        assert_eq!(split_array(&array).unwrap(), vec![bytes.as_slice(), &[][..]]);
258    }
259
260    #[test]
261    fn the_arrays_follow_the_single_block() {
262        use crate::database::{Row, SqlValue as Value};
263        let mut first = Attributes::default();
264        first.set_bbox([0, 0, 10, 10]);
265        let mut second = Attributes::default();
266        second.set_bbox([20, 0, 30, 10]);
267        let columns = ["TextLayerString", "TextLayerAttributes", "TextLayerStringArray", "TextLayerAttributesArray"];
268        let row = Row::new(
269            columns.iter().map(|c| c.to_string()).collect(),
270            vec![
271                Value::Blob(b"one".to_vec()),
272                Value::Blob(first.to_bytes()),
273                Value::Blob(join_array(&[b"two".to_vec()])),
274                Value::Blob(join_array(&[second.to_bytes()])),
275            ],
276        );
277        let blocks = read_blocks(&row).unwrap();
278        assert_eq!(blocks.iter().map(|b| b.text.as_str()).collect::<Vec<_>>(), ["one", "two"]);
279        assert_eq!(blocks[1].attributes.bbox(), Some([20, 0, 30, 10]));
280        let single = Row::new(vec!["TextLayerString".into()], vec![Value::Blob(b"only".to_vec())]);
281        assert_eq!(read_blocks(&single).unwrap().len(), 1);
282    }
283}