Skip to main content

csp_format/
vector.rs

1//! Vector data (`VectorObjectList.VectorData`, an external chunk).
2//!
3//! Vector layers store no pixels; CSP draws them from this data when it
4//! opens the file. Big endian (spec: `docs/spec/05-vector.md`):
5//!
6//! ```text
7//! u32 0, u32 length of the objects
8//! object × n:
9//!   header (header_size bytes)
10//!     +0  u32 header_size (76, 88, 92, 96) +4  u32 72
11//!     +8  u32 point_size (88, 104)       +12 u32 88
12//!     +16 u32 point_count                +20 u32 flags
13//!     +24 i32 bbox x0, y0, x1, y1 (layer pixels)
14//!     +40 u32 main colour R, G, B        +52 u32 sub colour R, G, B
15//!     +64 f64 opacity (1.0)
16//!     +72 …  brush style ids (u32 × 1–3), the brush size (f64), u32
17//!   point × point_count (point_size bytes)
18//!     +0 f64 x  +8 f64 y  +16 i32 bbox x0, y0, x1, y1  +32 u32 flags
19//!     +36 … f32 values (pressure, width ratio, …), +88 f64 x, y (104-byte points)
20//! ```
21//!
22//! Coordinates are layer pixels; add `LayerOffsetX/Y` for the canvas. The
23//! parser keeps every byte, so unknown fields are written back unchanged.
24
25use crate::binary::{Reader, Writer, be_f64_at, be_u32_at};
26use crate::error::{Result, bail};
27
28/// Header flag (+20) of a closed shape.
29pub const FLAG_CLOSED: u32 = 0x1000;
30
31/// One stroke or shape. `header` and every point keep their raw bytes.
32#[derive(Debug, Clone, PartialEq)]
33pub struct VectorObject {
34    /// The header bytes (its size is the first `u32`).
35    pub header: Vec<u8>,
36    /// Bytes per point (from the header).
37    pub point_size: usize,
38    /// Each point's bytes.
39    pub points: Vec<Vec<u8>>,
40}
41
42/// Typed view of one point.
43#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
44pub struct Point {
45    /// X in layer pixels.
46    pub x: f64,
47    /// Y in layer pixels.
48    pub y: f64,
49    /// Bounding box of the point's stamp in layer pixels (x0, y0, x1, y1).
50    pub bbox: [i32; 4],
51    /// Point flags (`u32` at +32).
52    pub flags: u32,
53    /// The f32 values after the flags (meaning partly unknown, see the spec).
54    pub values: [f32; 13],
55}
56
57fn put(data: &mut [u8], offset: usize, bytes: &[u8]) {
58    if let Some(target) = data.get_mut(offset..offset + bytes.len()) {
59        target.copy_from_slice(bytes);
60    }
61}
62
63impl VectorObject {
64    /// Header size in bytes (88, 92 or 96 in CSP files; 24 for some old objects).
65    pub fn header_size(&self) -> usize {
66        self.header.len()
67    }
68
69    /// Header flags (`u32` at +20).
70    pub fn flags(&self) -> u32 {
71        be_u32_at(&self.header, 20)
72    }
73
74    /// The shape is closed ([`FLAG_CLOSED`]).
75    pub fn is_closed(&self) -> bool {
76        self.flags() & FLAG_CLOSED != 0
77    }
78
79    /// Bounding box in layer pixels (x0, y0, x1, y1).
80    pub fn bbox(&self) -> [i32; 4] {
81        [0, 4, 8, 12].map(|i| be_u32_at(&self.header, 24 + i) as i32)
82    }
83
84    /// Sets the bounding box.
85    pub fn set_bbox(&mut self, bbox: [i32; 4]) {
86        for (i, value) in bbox.iter().enumerate() {
87            put(&mut self.header, 24 + 4 * i, &value.to_be_bytes());
88        }
89    }
90
91    /// Main colour as 8-bit RGB (each channel is stored as 32 bits whose
92    /// top byte is the 8-bit value).
93    pub fn main_color(&self) -> [u8; 3] {
94        [0, 4, 8].map(|i| (be_u32_at(&self.header, 40 + i) >> 24) as u8)
95    }
96
97    /// Sub colour as 8-bit RGB.
98    pub fn sub_color(&self) -> [u8; 3] {
99        [0, 4, 8].map(|i| (be_u32_at(&self.header, 52 + i) >> 24) as u8)
100    }
101
102    /// Opacity 0.0–1.0 (`f64` at +64).
103    pub fn opacity(&self) -> f64 {
104        be_f64_at(&self.header, 64)
105    }
106
107    /// Offset of the brush size (f64) in the header: after the brush style
108    /// ids (one, two or three `u32` from +72) and before a last `u32`.
109    fn brush_size_offset(&self) -> Option<usize> {
110        match self.header.len() {
111            size @ (88 | 92 | 96) => Some(size - 12),
112            _ => None,
113        }
114    }
115
116    /// Brush size in pixels (None for header layouts without one).
117    pub fn brush_size(&self) -> Option<f64> {
118        self.brush_size_offset().map(|offset| be_f64_at(&self.header, offset))
119    }
120
121    /// Sets the brush size, for header layouts that have one.
122    pub fn set_brush_size(&mut self, size: f64) {
123        if let Some(offset) = self.brush_size_offset() {
124            put(&mut self.header, offset, &size.to_be_bytes());
125        }
126    }
127
128    /// Brush style ids (`BrushStyle.MainId`) the header refers to.
129    pub fn brush_styles(&self) -> Vec<u32> {
130        let end = self.brush_size_offset().unwrap_or(76).min(self.header.len());
131        (72..end).step_by(4).map(|offset| be_u32_at(&self.header, offset)).collect()
132    }
133
134    /// The first settings word of a header-96 object (balloons and frames),
135    /// which names the kind of shape: 0x130 a balloon, 0x111 or 0x311 a
136    /// balloon's tail, 0x410 a frame (values seen in the corpus).
137    pub fn shape_kind(&self) -> Option<u32> {
138        (self.header.len() == 96).then(|| be_u32_at(&self.header, 72))
139    }
140
141    /// A balloon's tail (drawn with the line of the balloon before it).
142    pub fn is_balloon_tail(&self) -> bool {
143        self.shape_kind().is_some_and(|kind| kind & 0xff == 0x11)
144    }
145
146    /// The typed view of point `index`.
147    pub fn point(&self, index: usize) -> Option<Point> {
148        let raw = self.points.get(index)?;
149        let mut values = [0f32; 13];
150        for (i, value) in values.iter_mut().enumerate() {
151            *value = f32::from_bits(be_u32_at(raw, 36 + 4 * i));
152        }
153        Some(Point {
154            x: be_f64_at(raw, 0),
155            y: be_f64_at(raw, 8),
156            bbox: [16, 20, 24, 28].map(|offset| be_u32_at(raw, offset) as i32),
157            flags: be_u32_at(raw, 32),
158            values,
159        })
160    }
161
162    /// (x, y) of every point, in layer pixels.
163    pub fn coordinates(&self) -> Vec<(f64, f64)> {
164        self.points.iter().map(|raw| (be_f64_at(raw, 0), be_f64_at(raw, 8))).collect()
165    }
166
167    /// Move and scale every coordinate (`p * scale + offset`), with the
168    /// bounding boxes and brush size following.
169    pub fn transform(&mut self, scale: f64, dx: f64, dy: f64) {
170        let map = |bbox: [i32; 4]| {
171            [
172                (bbox[0] as f64 * scale + dx).floor() as i32,
173                (bbox[1] as f64 * scale + dy).floor() as i32,
174                (bbox[2] as f64 * scale + dx).ceil() as i32,
175                (bbox[3] as f64 * scale + dy).ceil() as i32,
176            ]
177        };
178        self.set_bbox(map(self.bbox()));
179        if let Some(size) = self.brush_size() {
180            self.set_brush_size(size * scale);
181        }
182        for raw in &mut self.points {
183            let x = be_f64_at(raw, 0) * scale + dx;
184            let y = be_f64_at(raw, 8) * scale + dy;
185            put(raw, 0, &x.to_be_bytes());
186            put(raw, 8, &y.to_be_bytes());
187            let bbox = map([16, 20, 24, 28].map(|offset| be_u32_at(raw, offset) as i32));
188            for (i, value) in bbox.iter().enumerate() {
189                put(raw, 16 + 4 * i, &value.to_be_bytes());
190            }
191            if raw.len() >= 104 {
192                let cx = be_f64_at(raw, 88) * scale + dx;
193                let cy = be_f64_at(raw, 96) * scale + dy;
194                put(raw, 88, &cx.to_be_bytes());
195                put(raw, 96, &cy.to_be_bytes());
196            }
197        }
198    }
199}
200
201/// All objects of one `VectorData` payload.
202#[derive(Debug, Clone, PartialEq)]
203pub struct VectorData {
204    /// The first u32 (always 0 in the corpus).
205    pub lead: u32,
206    /// The objects, in drawing order.
207    pub objects: Vec<VectorObject>,
208    /// Bytes after the declared length (none in the corpus).
209    pub trailing: Vec<u8>,
210}
211
212impl VectorData {
213    /// Parses a `VectorObjectList.VectorData` external payload.
214    pub fn parse(payload: &[u8]) -> Result<Self> {
215        let mut reader = Reader::new(payload);
216        let lead = reader.be_u32()?;
217        let length = reader.be_u32()? as usize;
218        let end = 8usize
219            .checked_add(length)
220            .filter(|end| *end <= payload.len())
221            .ok_or_else(|| crate::Error::format("VectorData length exceeds the payload"))?;
222        let mut objects = Vec::new();
223        while reader.position() < end {
224            let start = reader.position();
225            let header_size = Reader::at(payload, start).be_u32()? as usize;
226            let point_size = Reader::at(payload, start + 8).be_u32()? as usize;
227            let point_count = Reader::at(payload, start + 16).be_u32()? as usize;
228            if header_size < 24 || point_size < 16 {
229                bail!("Vector object at {start}: header {header_size} / point {point_size} bytes");
230            }
231            if point_count.checked_mul(point_size).is_none_or(|bytes| start + header_size + bytes > end) {
232                bail!("Vector object at {start} overruns the data");
233            }
234            let header = reader.bytes(header_size)?.to_vec();
235            let mut points = Vec::with_capacity(point_count);
236            for _ in 0..point_count {
237                points.push(reader.bytes(point_size)?.to_vec());
238            }
239            objects.push(VectorObject { header, point_size, points });
240        }
241        Ok(VectorData { lead, objects, trailing: payload[end..].to_vec() })
242    }
243
244    /// The payload; unchanged objects give the same bytes.
245    pub fn to_bytes(&self) -> Vec<u8> {
246        let mut body = Writer::new();
247        for object in &self.objects {
248            body.bytes(&object.header);
249            for point in &object.points {
250                body.bytes(point);
251            }
252        }
253        let mut writer = Writer::new();
254        writer.be_u32(self.lead).be_u32(body.len() as u32).bytes(&body.data).bytes(&self.trailing);
255        writer.into_inner()
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    fn object(points: &[(f64, f64)]) -> VectorObject {
264        let mut header = vec![0u8; 88];
265        put(&mut header, 0, &88u32.to_be_bytes());
266        put(&mut header, 4, &72u32.to_be_bytes());
267        put(&mut header, 8, &88u32.to_be_bytes());
268        put(&mut header, 12, &88u32.to_be_bytes());
269        put(&mut header, 16, &(points.len() as u32).to_be_bytes());
270        put(&mut header, 20, &0x2081u32.to_be_bytes());
271        put(&mut header, 76, &10f64.to_be_bytes());
272        let points = points
273            .iter()
274            .map(|(x, y)| {
275                let mut raw = vec![0u8; 88];
276                put(&mut raw, 0, &x.to_be_bytes());
277                put(&mut raw, 8, &y.to_be_bytes());
278                raw
279            })
280            .collect();
281        VectorObject { header, point_size: 88, points }
282    }
283
284    #[test]
285    fn vector_data_round_trips() {
286        let data = VectorData { lead: 0, objects: vec![object(&[(1.0, 2.0), (3.5, 4.5)])], trailing: vec![] };
287        let bytes = data.to_bytes();
288        let parsed = VectorData::parse(&bytes).unwrap();
289        assert_eq!(parsed, data);
290        assert_eq!(parsed.objects[0].coordinates(), vec![(1.0, 2.0), (3.5, 4.5)]);
291        assert_eq!(parsed.objects[0].brush_size(), Some(10.0));
292        assert!(VectorData::parse(&bytes[..bytes.len() - 3]).is_err());
293    }
294
295    #[test]
296    fn transform_moves_points_and_size() {
297        let mut item = object(&[(1.0, 2.0)]);
298        item.transform(2.0, 10.0, 20.0);
299        assert_eq!(item.coordinates(), vec![(12.0, 24.0)]);
300        assert_eq!(item.brush_size(), Some(20.0));
301    }
302}