Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New rnote file format, better compression and serialization, atomic file saving #1177

Draft
wants to merge 41 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
689aaa0
zstd comp + decomp functions
anesthetice Aug 7, 2024
1b64bfb
deps that I forgot to commit
anesthetice Aug 7, 2024
11fb351
simpler than I thought + backwards compatible
anesthetice Aug 8, 2024
a5ae6b9
removed tracing code and compress_to_gzip
anesthetice Aug 8, 2024
11da431
starting to take shape
anesthetice Aug 14, 2024
eab08bb
far from over, but it works at least
anesthetice Aug 14, 2024
b1195cb
lots of clean up, set max header size to u32::MAX instead of u16::MAX…
anesthetice Aug 16, 2024
89704a0
meson.build fix, forward-comp header
anesthetice Aug 16, 2024
a5d63d8
testing code, will be removed later, bitcode seems really good
anesthetice Aug 16, 2024
bdfc910
kept postcard, json, bitcode, bincode
anesthetice Aug 16, 2024
a01c7f1
customizable compression levels
anesthetice Aug 18, 2024
07cad72
[in progress] CompressionLevel, SavePrefs
anesthetice Aug 19, 2024
e7a7f1a
made EngineConfig not sync across tabs (was very unintuitive)
anesthetice Aug 19, 2024
887bd46
mutate command for rnote-cli, removed option wrapper in EngineSnapsho…
anesthetice Aug 20, 2024
8ae90af
conformed_saveprefs
anesthetice Aug 20, 2024
b9437f5
comments, rnote doesn't crash if None is selected as a CompressionLev…
anesthetice Aug 20, 2024
9b72185
removed tracing code, made Compression Level invisible if set to None
anesthetice Aug 20, 2024
531db89
minor changes
anesthetice Aug 20, 2024
b13c27e
serde renames for consistency
anesthetice Aug 20, 2024
0b4035a
Ijson, + big issue fix on mutate
anesthetice Aug 21, 2024
e83d177
(improved) two step file save process, lazy evaluation (in-progress)
anesthetice Aug 25, 2024
df3459b
formatting, lazy evals
anesthetice Aug 25, 2024
e004f8b
comments, prettier code, faster legacy loading time
anesthetice Aug 25, 2024
48ed535
re-added the slower less efficient conversion for rnote-cli mutate
anesthetice Aug 25, 2024
607ff75
removed bincode + postcard
anesthetice Aug 25, 2024
40131ed
renamed CompM to CompressionMethod and SerM to SerializationMethod
anesthetice Sep 2, 2024
b8062ec
one-to-one repr of semver::Version in PRELUE
anesthetice Sep 2, 2024
4d2d1ac
more readable code and errors, concat, tested an owned from_bytes and…
anesthetice Sep 6, 2024
4bca7e6
wasn't a need to skip serializing the SavePrefs when ser. the engine …
anesthetice Sep 8, 2024
fcb116f
misc, minor changes
anesthetice Sep 9, 2024
4026a73
Merge branch 'main' into file
anesthetice Sep 17, 2024
f3b5c93
missed closing delim
anesthetice Sep 17, 2024
6cbfe26
Merge branch 'main' into file
anesthetice Oct 31, 2024
d526e96
small fix
anesthetice Nov 9, 2024
e88e663
Merge branch 'main' into file
anesthetice Feb 5, 2025
fdb6753
moved the prelude handling logic to it's own file, improved documenta…
anesthetice Feb 5, 2025
41ba8d2
separated methods.rs into compression.rs and serialization.rs, made C…
anesthetice Feb 5, 2025
a65e87e
changed compression level defaults, changed SavePrefs a lot, put comp…
anesthetice Feb 10, 2025
493c788
merged main, 2024 edition changes, +clippy
anesthetice Mar 11, 2025
57414b1
bugfixes, docs, expressive file row
anesthetice Apr 7, 2025
6be3ff7
merged main
anesthetice Apr 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,153 changes: 499 additions & 654 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ anyhow = "1.0"
approx = "0.5.1"
async-fs = "2.1"
base64 = "0.22.1"
bitcode = { version = "0.6", features = ["serde"] }
cairo-rs = { version = "0.20.1", features = ["v1_18", "png", "svg", "pdf"] }
chrono = "0.4.38"
clap = { version = "4.5", features = ["derive"] }
crc32fast = { version = "1.4" }
dialoguer = "0.11.0"
flate2 = "1.0"
fs_extra = "1.3"
Expand Down Expand Up @@ -83,7 +85,8 @@ url = "2.5"
usvg = "0.44.0"
winresource = "0.1.17"
xmlwriter = "0.1.0"
# Enabling feature > v20_9 causes linker errors on mingw
zstd = { version = "0.13", features = ["zstdmt"] }

# using a custom patched version to work on windows
# as long as https://gitlab.gnome.org/World/Rust/poppler-rs/-/issues/1
# is not fixed
Expand Down
48 changes: 47 additions & 1 deletion crates/rnote-cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
// Imports
use crate::{export, import, test, thumbnail};
use crate::{export, import, mutate, test, thumbnail};

use anyhow::Context;
use clap::Parser;
use clap::builder::PossibleValuesParser;
use rnote_compose::SplitOrder;
use rnote_engine::SelectionCollision;
use rnote_engine::engine::export::{
DocExportFormat, DocPagesExportFormat, DocPagesExportPrefs, SelectionExportFormat,
SelectionExportPrefs,
};
use rnote_engine::engine::import::XoppImportPrefs;
use rnote_engine::fileformats::rnoteformat;
use smol::fs::File;
use smol::io::{AsyncReadExt, AsyncWriteExt};
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -69,6 +72,27 @@ pub(crate) enum Command {
#[arg(long, action = clap::ArgAction::SetTrue, global = true)]
open: bool,
},
/// Mutates one or more of the following for the specified Rnote file(s):{n}
/// compression method, compression level, serialization method, method lock
Mutate {
/// The rnote save file(s) to mutate
rnote_files: Vec<PathBuf>,
/// Keep the original rnote save file(s)
#[arg(long = "not-in-place", alias = "nip", action = clap::ArgAction::SetTrue)]
not_in_place: bool,
/// Sets method_lock to true, allowing a rnote save file to keep using non-default methods to serialize and compress itself
#[arg(short = 'l', long, action = clap::ArgAction::SetTrue, conflicts_with = "unlock")]
lock: bool,
/// Sets method_lock to false, coercing the file to use default methods on the next save
#[arg(short = 'u', long, action = clap::ArgAction::SetTrue, conflicts_with = "lock")]
unlock: bool,
#[arg(short = 's', long, action = clap::ArgAction::Set, value_parser = PossibleValuesParser::new(rnoteformat::SerializationMethod::VALID_STR_ARRAY))]
serialization_method: Option<String>,
#[arg(short = 'c', long, action = clap::ArgAction::Set, value_parser = PossibleValuesParser::new(rnoteformat::CompressionMethod::VALID_STR_ARRAY))]
compression_method: Option<String>,
#[arg(short = 'v', long, action = clap::ArgAction::Set)]
compression_level: Option<u8>,
},
/// Generate rnote thumbail from a given file
Thumbnail {
/// Input rnote file
Expand Down Expand Up @@ -252,6 +276,28 @@ pub(crate) async fn run() -> anyhow::Result<()> {
.await?;
println!("Export finished!");
}
Command::Mutate {
rnote_files,
not_in_place,
lock,
unlock,
serialization_method,
compression_method,
compression_level,
} => {
println!("Mutating..\n");
mutate::run_mutate(
rnote_files,
not_in_place,
lock,
unlock,
serialization_method,
compression_method,
compression_level,
)
.await?;
println!("Mutate finished!");
}
Command::Thumbnail {
rnote_file,
size,
Expand Down
1 change: 1 addition & 0 deletions crates/rnote-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
pub(crate) mod cli;
pub(crate) mod export;
pub(crate) mod import;
pub(crate) mod mutate;
pub(crate) mod test;
pub(crate) mod thumbnail;
pub(crate) mod validators;
Expand Down
1 change: 1 addition & 0 deletions crates/rnote-cli/src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ rnote_cli_sources = files(
'export.rs',
'import.rs',
'main.rs',
'mutate.rs',
'test.rs',
'validators.rs',
)
94 changes: 94 additions & 0 deletions crates/rnote-cli/src/mutate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use rnote_engine::engine::EngineSnapshot;
use rnote_engine::fileformats::FileFormatSaver;
use rnote_engine::fileformats::rnoteformat::RnoteHeader;
use rnote_engine::fileformats::{FileFormatLoader, rnoteformat::RnoteFile};
use smol::{fs::OpenOptions, io::AsyncReadExt};
use std::path::PathBuf;
use std::str::FromStr;

pub(crate) async fn run_mutate(
rnote_files: Vec<PathBuf>,
not_in_place: bool,
lock: bool,
unlock: bool,
serialization_method: Option<String>,
compression_method: Option<String>,
compression_level: Option<u8>,
) -> anyhow::Result<()> {
let total_len = rnote_files.len();
let mut total_delta: f64 = 0.0;
for (idx, mut filepath) in rnote_files.into_iter().enumerate() {
println!("Working on file {} out of {}", idx + 1, total_len);

let file_read_operation = async {
let mut read_file = OpenOptions::new().read(true).open(&filepath).await?;

let mut bytes: Vec<u8> = {
match read_file.metadata().await {
Ok(metadata) => {
Vec::with_capacity(usize::try_from(metadata.len()).unwrap_or(usize::MAX))
}
Err(err) => {
eprintln!("Failed to read file metadata, '{err}'");
Vec::new()
}
}
};

read_file.read_to_end(&mut bytes).await?;
Ok::<Vec<u8>, anyhow::Error>(bytes)
};
let bytes = file_read_operation.await?;
let old_size_mb = bytes.len() as f64 / 1e6;
let rnote_file = RnoteFile::load_from_bytes(&bytes)?;

let serialization = if let Some(ref str) = serialization_method {
rnote_engine::fileformats::rnoteformat::SerializationMethod::from_str(str).unwrap()
} else {
rnote_file.header.serialization
};

let mut compression = if let Some(ref str) = compression_method {
rnote_engine::fileformats::rnoteformat::CompressionMethod::from_str(str).unwrap()
} else {
rnote_file.header.compression
};

if let Some(lvl) = compression_level {
compression.update_compression_integer(lvl)?;
}

let method_lock = (rnote_file.header.method_lock | lock) && !unlock;
let uc_data = serialization.serialize(&EngineSnapshot::try_from(rnote_file)?)?;
let uc_size = uc_data.len() as u64;
let data = compression.compress(uc_data)?;

let rnote_file = RnoteFile {
header: RnoteHeader {
serialization,
compression,
uc_size,
method_lock,
},
body: data,
};

if not_in_place {
let file_stem = filepath
.file_stem()
.ok_or_else(|| anyhow::anyhow!("File does not contain a valid file stem"))?
.to_str()
.ok_or_else(|| anyhow::anyhow!("File does not contain a valid file stem"))?;
filepath.set_file_name(format!("{}_mut.rnote", file_stem));
}

let data = rnote_file.save_as_bytes("")?;
let new_size_mb = data.len() as f64 / 1e6;
rnote_engine::utils::atomic_save_to_file(&filepath, &data).await?;

println!("{:.2} MB → {:.2} MB", old_size_mb, new_size_mb,);
total_delta += new_size_mb - old_size_mb;
}
println!("\n⇒ ∆ = {:.2} MB", total_delta);
Ok(())
}
4 changes: 4 additions & 0 deletions crates/rnote-engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ rnote-compose = { workspace = true }

anyhow = { workspace = true }
approx = { workspace = true }
async-fs = { workspace = true }
base64 = { workspace = true }
bitcode = { workspace = true, features = ["serde"] }
cairo-rs = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, optional = true }
crc32fast = { workspace = true }
flate2 = { workspace = true }
futures = { workspace = true }
geo = { workspace = true }
Expand Down Expand Up @@ -55,6 +58,7 @@ tracing = { workspace = true }
unicode-segmentation = { workspace = true }
usvg = { workspace = true }
xmlwriter = { workspace = true }
zstd = { workspace = true, features = ["zstdmt"] }
# the long-term plan is to remove the gtk4 dependency entirely after switching to another renderer.
gtk4 = { workspace = true, optional = true }

Expand Down
5 changes: 2 additions & 3 deletions crates/rnote-engine/src/engine/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,9 +331,7 @@ impl Engine {
let engine_snapshot = self.take_snapshot();
rayon::spawn(move || {
let result = || -> anyhow::Result<Vec<u8>> {
let rnote_file = RnoteFile {
engine_snapshot: ijson::to_value(&engine_snapshot)?,
};
let rnote_file = RnoteFile::try_from(&engine_snapshot)?;
rnote_file.save_as_bytes(&file_name)
};
if oneshot_sender.send(result()).is_err() {
Expand All @@ -353,6 +351,7 @@ impl Engine {
penholder: self.penholder.clone_config(),
import_prefs: self.import_prefs.clone_config(),
export_prefs: self.export_prefs.clone_config(),
save_prefs: self.save_prefs.clone_from_engine_to_engineconfig(),
pen_sounds: self.pen_sounds(),
optimize_epd: self.optimize_epd(),
}
Expand Down
1 change: 1 addition & 0 deletions crates/rnote-engine/src/engine/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ impl Engine {
self.penholder = engine_config.penholder;
self.import_prefs = engine_config.import_prefs;
self.export_prefs = engine_config.export_prefs;
self.save_prefs = engine_config.save_prefs.clone_from_engineconfig_to_engine();

// Set the pen sounds to update the audioplayer
self.set_pen_sounds(engine_config.pen_sounds, data_dir);
Expand Down
10 changes: 10 additions & 0 deletions crates/rnote-engine/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pub mod export;
pub mod import;
pub mod rendering;
pub mod save;
pub mod snapshot;
pub mod strokecontent;
pub mod visual_debug;
Expand All @@ -11,6 +12,7 @@ pub use export::ExportPrefs;
use futures::StreamExt;
use futures::channel::mpsc::UnboundedReceiver;
pub use import::ImportPrefs;
pub use save::SavePrefs;
pub use snapshot::EngineSnapshot;
pub use strokecontent::StrokeContent;

Expand Down Expand Up @@ -154,6 +156,8 @@ pub struct EngineConfig {
import_prefs: ImportPrefs,
#[serde(rename = "export_prefs")]
export_prefs: ExportPrefs,
#[serde(rename = "save_prefs")]
save_prefs: SavePrefs,
#[serde(rename = "pen_sounds")]
pen_sounds: bool,
#[serde(rename = "optimize_epd")]
Expand Down Expand Up @@ -235,6 +239,8 @@ pub struct Engine {
pub import_prefs: ImportPrefs,
#[serde(rename = "export_prefs")]
pub export_prefs: ExportPrefs,
#[serde(rename = "save_prefs")]
pub save_prefs: SavePrefs,
#[serde(rename = "pen_sounds")]
pen_sounds: bool,
#[serde(rename = "optimize_epd")]
Expand Down Expand Up @@ -277,6 +283,7 @@ impl Default for Engine {
penholder: PenHolder::default(),
import_prefs: ImportPrefs::default(),
export_prefs: ExportPrefs::default(),
save_prefs: SavePrefs::default(),
pen_sounds: false,
optimize_epd: false,

Expand Down Expand Up @@ -377,13 +384,16 @@ impl Engine {
stroke_components: Arc::clone(&store_history_entry.stroke_components),
chrono_components: Arc::clone(&store_history_entry.chrono_components),
chrono_counter: store_history_entry.chrono_counter,
save_prefs: self.save_prefs.clone_from_engine_to_enginesnapshot(),
}
}

/// Imports an engine snapshot. A save file should always be loaded with this method.
pub fn load_snapshot(&mut self, snapshot: EngineSnapshot) -> WidgetFlags {
self.document = snapshot.document.clone_config();
self.camera = snapshot.camera.clone_config();
self.save_prefs.merge(&snapshot.save_prefs);

let mut widget_flags = self.store.import_from_snapshot(&snapshot)
| self.doc_resize_autoexpand()
| self.current_pen_update_state()
Expand Down
Loading