use std::collections::HashMap;
use byteorder::{BE, ReadBytesExt};
use raqote::{DrawOptions, DrawTarget, PathBuilder, Source, StrokeStyle};
use crate::{
error::Result,
id::{PageUuid, PointsUuid, StrokeUuid},
};
#[derive(Debug, Clone, PartialEq)]
pub struct Header {
pub version: u32,
pub page_id: PageUuid,
pub points_id: PointsUuid,
}
impl Header {
pub fn read(reader: impl std::io::Read + std::io::Seek) -> Result<Self> {
let mut reader = reader;
reader.seek(std::io::SeekFrom::Start(0))?;
let version = reader.read_u32::<BE>()?;
let mut buffer = [0; 36];
reader.read_exact(&mut buffer)?;
let page_id = PageUuid::from_byte_str(&buffer)?;
buffer.fill(0);
reader.read_exact(&mut buffer)?;
let points_id = PointsUuid::from_byte_str(&buffer)?;
Ok(Self {
version,
page_id,
points_id,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PointsTableEntry {
pub stroke_id: StrokeUuid,
pub start_addr: u32,
pub point_count: u32,
pub flag: u8,
}
impl PointsTableEntry {
pub fn read(reader: impl std::io::Read + std::io::Seek) -> Result<Self> {
let mut reader = reader;
let mut buffer = [0; 36];
reader.read_exact(&mut buffer)?;
let stroke_uuid = StrokeUuid::from_byte_str(&buffer)?;
let start_addr = reader.read_u32::<BE>()?;
let packed = reader.read_u32::<BE>()?;
let point_count = (packed >> 4) & 0x0FFFFFFF; let flag = (packed & 0xF) as u8;
Ok(Self {
stroke_id: stroke_uuid,
start_addr,
point_count,
flag,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Point {
pub timestamp_rel: u32,
pub x: f32,
pub y: f32,
pub tilt_x: i8,
pub tilt_y: i8,
pub pressure: u16,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Stroke {
pub points: Vec<Point>,
}
impl Stroke {
pub fn read(
reader: impl std::io::Read + std::io::Seek,
entry: &PointsTableEntry,
) -> Result<Self> {
let mut reader = reader;
reader.seek(std::io::SeekFrom::Start(entry.start_addr as u64))?;
let mut points = Vec::with_capacity(entry.point_count as usize);
for _ in 0..entry.point_count {
let timestamp_rel = reader.read_u32::<BE>()?;
let x = reader.read_f32::<BE>()?;
let y = reader.read_f32::<BE>()?;
let tilt_x = reader.read_i8()?;
let tilt_y = reader.read_i8()?;
let pressure = reader.read_u16::<BE>()?;
points.push(Point {
timestamp_rel,
x,
y,
tilt_x,
tilt_y,
pressure,
});
}
Ok(Self { points })
}
pub fn render(
&self,
draw_target: &mut DrawTarget,
draw_options: &DrawOptions,
stroke_style: &StrokeStyle,
) -> Result<()> {
self.render_with_color(
draw_target,
draw_options,
stroke_style,
raqote::Color::new(255, 0, 0, 0),
)
}
pub fn render_with_color(
&self,
draw_target: &mut DrawTarget,
draw_options: &DrawOptions,
stroke_style: &StrokeStyle,
color: raqote::Color,
) -> Result<()> {
if self.points.is_empty() {
log::warn!("No points to draw for stroke");
return Ok(());
}
let mut path = PathBuilder::new();
let mut first_point = true;
for point in &self.points {
if first_point {
path.move_to(point.x, point.y);
first_point = false;
} else {
path.line_to(point.x, point.y);
}
}
draw_target.stroke(
&path.finish(),
&Source::Solid(color.into()),
stroke_style,
draw_options,
);
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PointsFile {
header: Header,
points: HashMap<StrokeUuid, Stroke>,
}
impl PointsFile {
pub fn read(mut reader: impl std::io::Read + std::io::Seek) -> Result<Self> {
let header = Header::read(&mut reader)?;
let points_table_end = reader.seek(std::io::SeekFrom::End(-4))?;
let points_table_start = reader.read_u32::<BE>()?;
let mut points_table = Vec::new();
reader.seek(std::io::SeekFrom::Start(points_table_start as u64))?;
while reader.stream_position()? < points_table_end {
let entry = PointsTableEntry::read(&mut reader)?;
points_table.push(entry);
}
let mut points = HashMap::new();
for entry in points_table {
let stroke = Stroke::read(&mut reader, &entry)?;
points.insert(entry.stroke_id, stroke);
}
Ok(Self { header, points })
}
pub fn header(&self) -> &Header {
&self.header
}
pub fn get_stroke(&self, stroke_id: &StrokeUuid) -> Option<&Stroke> {
self.points.get(stroke_id)
}
}
#[cfg(test)]
mod tests {
use byteorder::{BE, WriteBytesExt};
use super::{Header, PointsTableEntry};
use crate::id::{PageUuid, PointsUuid, StrokeUuid};
#[test]
fn header_parses_padded_uuid_fields() {
let mut bytes = Vec::new();
bytes.write_u32::<BE>(1).unwrap();
bytes.extend_from_slice(b"ba338e220eda49268c7126a02970a160 ");
bytes.extend_from_slice(b"e858c829-d2f3-4994-b9fb-95bcc8003fa3");
let header = Header::read(std::io::Cursor::new(bytes)).unwrap();
assert_eq!(header.version, 1);
assert_eq!(
header.page_id,
PageUuid::from_str("ba338e220eda49268c7126a02970a160").unwrap()
);
assert_eq!(
header.points_id,
PointsUuid::from_str("e858c829-d2f3-4994-b9fb-95bcc8003fa3").unwrap()
);
}
#[test]
fn points_table_entry_parses_padded_stroke_id() {
let mut bytes = Vec::new();
bytes.extend_from_slice(b"92c1ab734ec14f70907adc11dcb0806d ");
bytes.write_u32::<BE>(64).unwrap();
bytes.write_u32::<BE>((3 << 4) | 5).unwrap();
let entry = PointsTableEntry::read(std::io::Cursor::new(bytes)).unwrap();
assert_eq!(
entry.stroke_id,
StrokeUuid::from_str("92c1ab73-4ec1-4f70-907a-dc11dcb0806d").unwrap()
);
assert_eq!(entry.start_addr, 64);
assert_eq!(entry.point_count, 3);
assert_eq!(entry.flag, 5);
}
}