//! Import of Rnote notebooks (.rnote) (Issue #47). //! //! An .rnote file is gzip-compressed JSON: `{"version": "0.x.y", "data": {"engine_snapshot": //! {...}}}`. The snapshot is Rnote's internal state without a specification, so it is read //! leniently as JSON and only what we need is picked out; files from Rnote 0.5.10 on are //! supported (the layout of older ones differs too much). Coordinates are pixels at the //! document's dpi. //! //! Pages: fixed-size and continuous documents become one page per Rnote page (a stroke goes //! to the page its middle lies on), infinite documents an endless notebook. use base64::Engine; use glam::DVec2; use serde_json::Value; use std::path::Path; use crate::document::{Notebook, PageBackground}; use crate::import::{self, ImportReport, Imported}; use crate::storage::{NotebookLayout, NotebookManifest, StrokeData}; use crate::t; use crate::text::TextFont; /// At most this many pages are created (protects against nonsensical document sizes). const MAX_PAGES: usize = 2000; /// Is the file by its name an Rnote notebook? pub fn is_rnote_path(path: &Path) -> bool { path.extension() .and_then(|e| e.to_str()) .is_some_and(|e| e.eq_ignore_ascii_case("rnote")) } /// Parsed Rnote document, still in Rnote pixels. #[derive(Debug, Clone, PartialEq)] pub struct RnoteDocument { /// Page size in pixels and dpi pub format: DVec2, pub dpi: f64, /// Document bounds (upper left corner and size) pub origin: DVec2, pub size: DVec2, pub endless: bool, pub background: PageBackground, /// In drawing order pub items: Vec, } #[derive(Debug, Clone, PartialEq)] pub enum RnoteItem { Ink { color: [f32; 4], highlighter: bool, points: Vec, /// Drawn width per point widths: Vec, }, Text { content: String, size: f64, mono: bool, /// Upper left corner and axes affine: [DVec2; 3], max_width: Option, color: [f32; 4], }, Bitmap { width: u32, height: u32, /// Straight (not premultiplied) RGBA pixels: Vec, corners: [DVec2; 4], }, /// SVG image (Rnote stores imported PDF pages like this), rasterized on import Svg { svg: String, corners: [DVec2; 4] }, /// Element without a counterpart or unreadable Unsupported, } impl RnoteItem { /// Point that decides which page the element belongs to. fn anchor(&self) -> Option { match self { RnoteItem::Ink { points, .. } => { let (min, max) = points.iter().fold( (DVec2::splat(f64::MAX), DVec2::splat(f64::MIN)), |(a, b), &p| (a.min(p), b.max(p)), ); (!points.is_empty()).then(|| (min + max) / 2.0) } RnoteItem::Text { affine, .. } => Some(affine[2]), RnoteItem::Bitmap { corners, .. } | RnoteItem::Svg { corners, .. } => { Some((corners[0] + corners[2]) / 2.0) } RnoteItem::Unsupported => None, } } } fn num(value: &Value, key: &str) -> Option { value.get(key)?.as_f64().filter(|v| v.is_finite()) } /// Vectors are stored as `[x, y]` (older versions possibly as `{"x": .., "y": ..}`). fn vec2(value: &Value) -> Option { match value { Value::Array(a) if a.len() == 2 => Some(DVec2::new(a[0].as_f64()?, a[1].as_f64()?)), Value::Object(_) => Some(DVec2::new(num(value, "x")?, num(value, "y")?)), _ => None, } .filter(|v| v.is_finite()) } /// Affine transformation as x axis, y axis and translation. Since Rnote 0.15 `affine` holds /// the six values; before, `transform.affine` held a column-major 3×3 matrix. fn affine(value: &Value) -> Option<[DVec2; 3]> { let values = value .get("affine") .or_else(|| value.get("transform")?.get("affine"))? .as_array()?; let v: Vec = values.iter().filter_map(Value::as_f64).collect(); let [a, b, c, d, e, f] = match v.len() { 6 => [v[0], v[1], v[2], v[3], v[4], v[5]], 9 => [v[0], v[1], v[3], v[4], v[6], v[7]], _ => return None, }; Some([DVec2::new(a, b), DVec2::new(c, d), DVec2::new(e, f)]) } fn apply(affine: &[DVec2; 3], p: DVec2) -> DVec2 { affine[0] * p.x + affine[1] * p.y + affine[2] } fn color(value: Option<&Value>) -> Option<[f32; 4]> { let value = value.filter(|v| v.is_object())?; let c = |k| num(value, k).unwrap_or(0.0).clamp(0.0, 1.0) as f32; Some([ c("r"), c("g"), c("b"), num(value, "a").map_or(1.0, |a| a.clamp(0.0, 1.0) as f32), ]) } /// The one variant of an externally tagged enum: `{"name": {...}}`. fn variant(value: &Value) -> Option<(&str, &Value)> { let object = value.as_object()?; let (name, inner) = object.iter().next()?; Some((name.as_str(), inner)) } /// Width for the pressure according to Rnote's pressure curve. fn curve_width(curve: &str, width: f64, pressure: f64) -> f64 { let p = pressure.clamp(0.0, 1.0); match curve { "const" => width, "sqrt" => width * p.sqrt(), "cbrt" => width * p.cbrt(), "pow2" => width * p.powi(2), "pow3" => width * p.powi(3), _ => width * p, } } /// Style options (smooth, rough or textured). struct Style { width: f64, stroke_color: Option<[f32; 4]>, fill_color: Option<[f32; 4]>, pressure_curve: String, } fn style(value: Option<&Value>) -> Option