feat: import Saber notebooks (Issue #47)
The import now also accepts Saber files: .sbn2 (BSON) with its asset files next to it, .sba archives (zip with main.sbn2 and assets) and the old JSON .sbn, including the layout before format version 8 with strokes outside the pages. Taken over: strokes with pen pressure and perfect_freehand's thinning, highlighters, circle and rectangle shapes, typed text (Quill delta as one text field), images (SVG rasterized), background images and PDF pages as PDF backgrounds, paper patterns. BSON and zip are read by small built-in readers, so no new dependencies. The SVG rasterizer moved to src/import.rs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ca6f64ec8e
commit
397bf3bfe4
11 files changed
+970
-52
No files matched your search
+2
-33
@@ -380,7 +380,7 @@ fn bitmapimage(value: &Value) -> Option<RnoteItem> {
|
||||
if pixels.len() != width as usize * height as usize * 4 {
|
||||
return None;
|
||||
}
|
||||
unpremultiply(&mut pixels);
|
||||
import::unpremultiply(&mut pixels);
|
||||
Some(RnoteItem::Bitmap {
|
||||
width,
|
||||
height,
|
||||
@@ -407,37 +407,6 @@ fn rectangle_corners(rectangle: &Value) -> Option<[DVec2; 4]> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Premultiplied RGBA (Rnote, tiny-skia) to straight RGBA.
|
||||
fn unpremultiply(pixels: &mut [u8]) {
|
||||
for px in pixels.as_chunks_mut::<4>().0 {
|
||||
let a = px[3] as u32;
|
||||
if a > 0 && a < 255 {
|
||||
for c in &mut px[..3] {
|
||||
*c = ((*c as u32 * 255 + a / 2) / a).min(255) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rasterizes an SVG for a box of `size` page units (about 250 dpi).
|
||||
fn rasterize_svg(svg: &str, size: glam::Vec2) -> Option<(u32, u32, Vec<u8>)> {
|
||||
use resvg::{tiny_skia, usvg};
|
||||
let tree = usvg::Tree::from_str(svg, &usvg::Options::default()).ok()?;
|
||||
let max = crate::assets::MAX_IMPORT_DIMENSION as f32;
|
||||
let px_per_unit = 250.0 / 254.0;
|
||||
let width = (size.x * px_per_unit).round().clamp(1.0, max) as u32;
|
||||
let height = (size.y * px_per_unit).round().clamp(1.0, max) as u32;
|
||||
let mut pixmap = tiny_skia::Pixmap::new(width, height)?;
|
||||
let transform = tiny_skia::Transform::from_scale(
|
||||
width as f32 / tree.size().width(),
|
||||
height as f32 / tree.size().height(),
|
||||
);
|
||||
resvg::render(&tree, transform, &mut pixmap.as_mut());
|
||||
let mut pixels = pixmap.take();
|
||||
unpremultiply(&mut pixels);
|
||||
Some((width, height, pixels))
|
||||
}
|
||||
|
||||
/// Rnote's version as (major, minor, patch).
|
||||
fn version(text: &str) -> Option<(u64, u64, u64)> {
|
||||
let v = semver::Version::parse(text).ok()?;
|
||||
@@ -680,7 +649,7 @@ fn convert(doc: &RnoteDocument, notebook_dir: &Path) -> Imported {
|
||||
corners[0].distance(corners[1]),
|
||||
corners[0].distance(corners[3]),
|
||||
);
|
||||
let image = rasterize_svg(svg, size).and_then(|(width, height, pixels)| {
|
||||
let image = import::rasterize_svg(svg, size).and_then(|(width, height, pixels)| {
|
||||
assets.import_rgba(width, height, pixels).ok()
|
||||
});
|
||||
match image {
|
||||
|
||||
Reference in new issue
Block a user