Skip to main content

ferroui_controls_inking/
persistence.rs

1//! Versioned, bounded native ink document encoding. This format preserves all
2//! portable stroke telemetry; it is deliberately distinct from Windows ISF.
3
4use super::ink_drawing_attributes::{InkDrawingAttributes, InkDrawingAttributesKind, PenTipShape};
5use super::ink_point::{InkError, InkPoint};
6use super::ink_stroke::InkStroke;
7use ferroui_base::media::Color;
8use ferroui_base::{Matrix, Point, Size};
9use std::collections::HashSet;
10use std::rc::Rc;
11
12pub const INK_MIME_TYPE: &str = "application/x-ferroui-ink";
13const MAGIC: &[u8; 8] = b"FERROINK";
14const VERSION: u32 = 1;
15const HEADER_SIZE: usize = 20;
16const POINT_SIZE: usize = 40;
17const STROKE_FIXED_SIZE: usize = 136;
18
19/// Codec resource limits apply before allocation and cumulatively across
20/// records. The defaults permit a 64 MiB payload, 100,000 strokes, 65,536
21/// samples per stroke and 1,000,000 samples in total.
22///
23/// These limits bound serialized input and raw telemetry. Stroke construction
24/// and collection admission additionally reserve derived geometry against
25/// [`InkStroke::MAX_RENDERED_VERTICES`] and
26/// [`super::InkStrokeContainer::MAX_RENDERED_VERTICES`]. Changing codec limits
27/// does not raise those limits or the collection's raw sample limits. Geometry
28/// rejection returns [`InkError::LimitExceeded`] before the collection changes.
29/// Cached raw/smoothed samples, contour vectors and renderer-owned geometry
30/// occupy separate allocations; the serialized byte cap is not a total heap
31/// memory cap.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub struct InkPersistenceLimits {
34    pub max_bytes: usize,
35    pub max_strokes: usize,
36    pub max_points_per_stroke: usize,
37    pub max_total_points: usize,
38}
39impl InkPersistenceLimits {
40    pub const DEFAULT: Self = Self {
41        max_bytes: 64 * 1024 * 1024,
42        max_strokes: 100_000,
43        max_points_per_stroke: 65_536,
44        max_total_points: 1_000_000,
45    };
46}
47impl Default for InkPersistenceLimits {
48    fn default() -> Self {
49        Self::DEFAULT
50    }
51}
52
53pub(crate) fn encode(
54    strokes: &[Rc<InkStroke>],
55    limits: InkPersistenceLimits,
56) -> Result<Vec<u8>, InkError> {
57    if strokes.len() > limits.max_strokes || strokes.len() > u32::MAX as usize {
58        return Err(InkError::LimitExceeded);
59    }
60    let mut size = HEADER_SIZE + 4;
61    let mut total_points = 0usize;
62    let mut seen = HashSet::with_capacity(strokes.len());
63    for s in strokes {
64        if !seen.insert(s.id()) {
65            return Err(InkError::DuplicateStrokeId(s.id()));
66        }
67        let n = s.ink_points().len();
68        total_points = total_points.checked_add(n).ok_or(InkError::LimitExceeded)?;
69        size = size
70            .checked_add(4 + STROKE_FIXED_SIZE)
71            .and_then(|v| n.checked_mul(POINT_SIZE).and_then(|n| v.checked_add(n)))
72            .ok_or(InkError::LimitExceeded)?;
73        if n > limits.max_points_per_stroke || total_points > limits.max_total_points {
74            return Err(InkError::LimitExceeded);
75        }
76    }
77    if size > limits.max_bytes || size - HEADER_SIZE > u32::MAX as usize {
78        return Err(InkError::LimitExceeded);
79    }
80    let mut out = Vec::with_capacity(size);
81    out.extend_from_slice(MAGIC);
82    write_u32(&mut out, VERSION);
83    write_u32(&mut out, (size - HEADER_SIZE) as u32);
84    write_u32(&mut out, 0);
85    write_u32(&mut out, strokes.len() as u32);
86    for s in strokes {
87        let a = s.drawing_attributes();
88        write_u32(
89            &mut out,
90            (STROKE_FIXED_SIZE + s.ink_points().len() * POINT_SIZE) as u32,
91        );
92        write_u32(&mut out, s.id());
93        out.push(u8::from(s.selected()));
94        out.push(
95            u8::from(a.draw_as_highlighter)
96                | (u8::from(a.ignore_pressure) << 1)
97                | (u8::from(a.fit_to_curve) << 2)
98                | (u8::from(a.ignore_tilt) << 3),
99        );
100        out.push(a.pen_tip as u8);
101        out.push(a.kind as u8);
102        out.extend_from_slice(&[a.color.a, a.color.r, a.color.g, a.color.b]);
103        write_f64(&mut out, a.size.width);
104        write_f64(&mut out, a.size.height);
105        write_f64(&mut out, a.pencil_opacity);
106        write_matrix(&mut out, a.pen_tip_transform);
107        write_matrix(&mut out, s.point_transform());
108        write_u32(&mut out, s.ink_points().len() as u32);
109        for p in s.ink_points() {
110            write_f64(&mut out, p.position.x);
111            write_f64(&mut out, p.position.y);
112            for value in [p.pressure, p.tilt_x, p.tilt_y, p.twist] {
113                out.extend_from_slice(&value.to_le_bytes());
114            }
115            out.extend_from_slice(&p.timestamp.to_le_bytes());
116        }
117    }
118    debug_assert_eq!(out.len(), size);
119    let checksum = crc32(&out[HEADER_SIZE..]);
120    out[16..20].copy_from_slice(&checksum.to_le_bytes());
121    Ok(out)
122}
123
124pub(crate) fn decode(
125    data: &[u8],
126    limits: InkPersistenceLimits,
127) -> Result<Vec<Rc<InkStroke>>, InkError> {
128    if data.len() > limits.max_bytes {
129        return Err(InkError::LimitExceeded);
130    }
131    let mut reader = Reader::new(data);
132    if reader.take(8)? != MAGIC {
133        return Err(InkError::UnsupportedFormat);
134    }
135    if reader.u32()? != VERSION {
136        return Err(InkError::UnsupportedFormat);
137    }
138    let length = reader.u32()? as usize;
139    let checksum = reader.u32()?;
140    if length != reader.remaining() {
141        return Err(InkError::InvalidData("payload length mismatch"));
142    }
143    if checksum != crc32(&data[HEADER_SIZE..]) {
144        return Err(InkError::InvalidData("checksum mismatch"));
145    }
146    let count = reader.u32()? as usize;
147    if count > limits.max_strokes {
148        return Err(InkError::LimitExceeded);
149    }
150    if count > reader.remaining() / (4 + STROKE_FIXED_SIZE + POINT_SIZE) {
151        return Err(InkError::InvalidData(
152            "stroke count exceeds remaining bytes",
153        ));
154    }
155    let mut result = Vec::with_capacity(count);
156    let mut seen = HashSet::with_capacity(count);
157    let mut total_points = 0usize;
158    for _ in 0..count {
159        let record_len = reader.u32()? as usize;
160        let mut record = Reader::new(reader.take(record_len)?);
161        let id = record.u32()?;
162        if !seen.insert(id) {
163            return Err(InkError::DuplicateStrokeId(id));
164        }
165        let selected = match record.byte()? {
166            0 => false,
167            1 => true,
168            _ => return Err(InkError::InvalidData("invalid selection flag")),
169        };
170        let flags = record.byte()?;
171        if flags & !15 != 0 {
172            return Err(InkError::InvalidData("unknown drawing flags"));
173        }
174        let pen_tip = match record.byte()? {
175            0 => PenTipShape::Circle,
176            1 => PenTipShape::Rectangle,
177            _ => return Err(InkError::InvalidData("unknown pen tip")),
178        };
179        let kind = match record.byte()? {
180            0 => InkDrawingAttributesKind::Default,
181            1 => InkDrawingAttributesKind::Pencil,
182            _ => return Err(InkError::InvalidData("unknown drawing kind")),
183        };
184        let color = record.take(4)?;
185        let color = Color::from_argb(color[0], color[1], color[2], color[3]);
186        let size = Size::new(record.f64()?, record.f64()?);
187        let pencil_opacity = record.f64()?;
188        let pen_tip_transform = record.matrix()?;
189        let transform = record.matrix()?;
190        let count = record.u32()? as usize;
191        total_points = total_points
192            .checked_add(count)
193            .ok_or(InkError::LimitExceeded)?;
194        if count > limits.max_points_per_stroke || total_points > limits.max_total_points {
195            return Err(InkError::LimitExceeded);
196        }
197        if count == 0 || count.checked_mul(POINT_SIZE) != Some(record.remaining()) {
198            return Err(InkError::InvalidData(
199                "point count does not match record length",
200            ));
201        }
202        let mut points = Vec::with_capacity(count);
203        for _ in 0..count {
204            points.push(InkPoint {
205                position: Point::new(record.f64()?, record.f64()?),
206                pressure: record.f32()?,
207                tilt_x: record.f32()?,
208                tilt_y: record.f32()?,
209                twist: record.f32()?,
210                timestamp: record.u64()?,
211            });
212        }
213        let attributes = InkDrawingAttributes {
214            color,
215            size,
216            pen_tip,
217            pen_tip_transform,
218            draw_as_highlighter: flags & 1 != 0,
219            ignore_pressure: flags & 2 != 0,
220            fit_to_curve: flags & 4 != 0,
221            ignore_tilt: flags & 8 != 0,
222            kind,
223            pencil_opacity,
224        };
225        result.push(InkStroke::restore(
226            id, points, attributes, transform, selected,
227        )?);
228    }
229    if reader.remaining() != 0 {
230        return Err(InkError::InvalidData("trailing data"));
231    }
232    Ok(result)
233}
234
235fn write_u32(out: &mut Vec<u8>, value: u32) {
236    out.extend_from_slice(&value.to_le_bytes());
237}
238fn write_f64(out: &mut Vec<u8>, value: f64) {
239    out.extend_from_slice(&value.to_le_bytes());
240}
241fn write_matrix(out: &mut Vec<u8>, value: Matrix) {
242    for v in [
243        value.m11, value.m12, value.m21, value.m22, value.m31, value.m32,
244    ] {
245        write_f64(out, v);
246    }
247}
248struct Reader<'a> {
249    data: &'a [u8],
250    position: usize,
251}
252impl<'a> Reader<'a> {
253    fn new(data: &'a [u8]) -> Self {
254        Self { data, position: 0 }
255    }
256    fn remaining(&self) -> usize {
257        self.data.len() - self.position
258    }
259    fn take(&mut self, length: usize) -> Result<&'a [u8], InkError> {
260        let end = self
261            .position
262            .checked_add(length)
263            .ok_or(InkError::InvalidData("record length overflow"))?;
264        let result = self
265            .data
266            .get(self.position..end)
267            .ok_or(InkError::InvalidData("truncated document"))?;
268        self.position = end;
269        Ok(result)
270    }
271    fn byte(&mut self) -> Result<u8, InkError> {
272        Ok(self.take(1)?[0])
273    }
274    fn u32(&mut self) -> Result<u32, InkError> {
275        let mut b = [0; 4];
276        b.copy_from_slice(self.take(4)?);
277        Ok(u32::from_le_bytes(b))
278    }
279    fn u64(&mut self) -> Result<u64, InkError> {
280        let mut b = [0; 8];
281        b.copy_from_slice(self.take(8)?);
282        Ok(u64::from_le_bytes(b))
283    }
284    fn f32(&mut self) -> Result<f32, InkError> {
285        Ok(f32::from_bits(self.u32()?))
286    }
287    fn f64(&mut self) -> Result<f64, InkError> {
288        Ok(f64::from_bits(self.u64()?))
289    }
290    fn matrix(&mut self) -> Result<Matrix, InkError> {
291        Ok(Matrix::new(
292            self.f64()?,
293            self.f64()?,
294            self.f64()?,
295            self.f64()?,
296            self.f64()?,
297            self.f64()?,
298        ))
299    }
300}
301
302const fn crc_table() -> [u32; 256] {
303    let mut table = [0u32; 256];
304    let mut i = 0;
305    while i < 256 {
306        let mut value = i as u32;
307        let mut j = 0;
308        while j < 8 {
309            value = if value & 1 != 0 {
310                0xedb8_8320 ^ (value >> 1)
311            } else {
312                value >> 1
313            };
314            j += 1;
315        }
316        table[i] = value;
317        i += 1;
318    }
319    table
320}
321const CRC_TABLE: [u32; 256] = crc_table();
322fn crc32(bytes: &[u8]) -> u32 {
323    let mut crc = !0u32;
324    for byte in bytes {
325        crc = CRC_TABLE[((crc ^ u32::from(*byte)) & 255) as usize] ^ (crc >> 8);
326    }
327    !crc
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    fn stroke() -> Rc<InkStroke> {
334        let p = InkPoint {
335            position: Point::new(10.0, -4.0),
336            pressure: 0.75,
337            tilt_x: 30.0,
338            tilt_y: -20.0,
339            twist: 359.0,
340            timestamp: 12345,
341        };
342        let a = InkDrawingAttributes {
343            color: Color::from_argb(128, 255, 80, 20),
344            size: Size::new(7.0, 3.0),
345            kind: InkDrawingAttributesKind::Pencil,
346            pencil_opacity: 0.65,
347            ..Default::default()
348        };
349        let s = InkStroke::new(vec![p], a).unwrap();
350        s.set_selected(true);
351        s.set_point_transform(Matrix::create_translation(4.0, 9.0))
352            .unwrap();
353        s
354    }
355    #[test]
356    fn round_trip_preserves_all_samples_and_attributes() {
357        let s = stroke();
358        let bytes = encode(&[s.clone()], InkPersistenceLimits::DEFAULT).unwrap();
359        let loaded = decode(&bytes, InkPersistenceLimits::DEFAULT).unwrap();
360        let copy = &loaded[0];
361        assert_eq!(copy.id(), s.id());
362        assert_eq!(copy.ink_points(), s.ink_points());
363        assert_eq!(copy.drawing_attributes(), s.drawing_attributes());
364        assert_eq!(copy.point_transform(), s.point_transform());
365        assert!(copy.selected());
366    }
367    #[test]
368    fn every_truncation_is_rejected() {
369        let bytes = encode(&[stroke()], InkPersistenceLimits::DEFAULT).unwrap();
370        for n in 0..bytes.len() {
371            assert!(
372                decode(&bytes[..n], InkPersistenceLimits::DEFAULT).is_err(),
373                "length {n}"
374            );
375        }
376    }
377    #[test]
378    fn corruption_is_detected_and_limits_precede_allocations() {
379        let mut bytes = encode(&[stroke()], InkPersistenceLimits::DEFAULT).unwrap();
380        let last = bytes.len() - 1;
381        bytes[last] ^= 1;
382        assert_eq!(
383            decode(&bytes, InkPersistenceLimits::DEFAULT).unwrap_err(),
384            InkError::InvalidData("checksum mismatch")
385        );
386        assert!(matches!(
387            decode(
388                &bytes,
389                InkPersistenceLimits {
390                    max_bytes: 8,
391                    ..Default::default()
392                }
393            ),
394            Err(InkError::LimitExceeded)
395        ));
396    }
397    #[test]
398    fn hostile_count_with_valid_checksum_is_rejected() {
399        let mut bytes = encode(&[stroke()], InkPersistenceLimits::DEFAULT).unwrap();
400        bytes[20..24].copy_from_slice(&u32::MAX.to_le_bytes());
401        let crc = crc32(&bytes[HEADER_SIZE..]);
402        bytes[16..20].copy_from_slice(&crc.to_le_bytes());
403        assert_eq!(
404            decode(&bytes, InkPersistenceLimits::DEFAULT).unwrap_err(),
405            InkError::LimitExceeded
406        );
407    }
408    #[test]
409    fn arbitrary_input_never_panics() {
410        for len in 0..256 {
411            let bytes: Vec<_> = (0..len)
412                .map(|i| ((i * 137 + len * 19) % 256) as u8)
413                .collect();
414            let _ = decode(&bytes, InkPersistenceLimits::DEFAULT);
415        }
416    }
417    #[test]
418    fn empty_document_round_trips() {
419        let bytes = encode(&[], InkPersistenceLimits::DEFAULT).unwrap();
420        assert!(decode(&bytes, InkPersistenceLimits::DEFAULT)
421            .unwrap()
422            .is_empty());
423    }
424}