From 689aaa034d8383c0ddfc9c92bff432c8803699b0 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Wed, 7 Aug 2024 23:06:24 +0200 Subject: [PATCH 01/36] zstd comp + decomp functions --- crates/rnote-engine/Cargo.toml | 1 + .../src/fileformats/rnoteformat/mod.rs | 98 ++++++++++++++++++- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/crates/rnote-engine/Cargo.toml b/crates/rnote-engine/Cargo.toml index c8add5331c..f52e88a73c 100644 --- a/crates/rnote-engine/Cargo.toml +++ b/crates/rnote-engine/Cargo.toml @@ -55,6 +55,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 } diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index 9826a02bc4..548bf94270 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -42,10 +42,10 @@ fn decompress_from_gzip(compressed: &[u8]) -> Result, anyhow::Error> { .len() .checked_sub(4) // only happens if the file has less than 4 bytes - .ok_or_else(|| { - anyhow::anyhow!("Invalid file") - .context("Failed to get the size of the decompressed data") - })?; + .ok_or( + anyhow::anyhow!("Not a valid gzip-compressed file") + .context("Failed to get the size of the decompressed data"), + )?; decompressed_size.copy_from_slice(&compressed[idx_start..]); // u32 -> usize to avoid issues on 32-bit architectures // also more reasonable since the uncompressed size is given by 4 bytes @@ -57,6 +57,96 @@ fn decompress_from_gzip(compressed: &[u8]) -> Result, anyhow::Error> { Ok(bytes) } +/// Decompress bytes with zstd +pub fn decompress_from_zstd(compressed: &[u8]) -> Result, anyhow::Error> { + // Optimization for the zstd format, less pretty than for gzip but this does shave off a bit of time + // https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#frame_header + let mut bytes: Vec = { + let frame_header_descriptor = compressed.get(4).ok_or( + anyhow::anyhow!("Not a valid zstd-compressed file") + .context("Failed to get the frame header descriptor of the file"), + )?; + + let frame_content_size_flag = frame_header_descriptor >> 6; + let single_segment_flag = (frame_header_descriptor >> 5) & 1; + let did_field_size = { + let dictionary_id_flag = frame_header_descriptor & 11; + if dictionary_id_flag == 3 { + 4 + } else { + dictionary_id_flag + } + }; + // frame header size start index + let fcs_sidx = (6 + did_field_size - single_segment_flag) as usize; + // magic number: 4 bytes + window descriptor: 1 byte if single segment flag is not set + frame header descriptor: 1 byte + dict. field size: 0-4 bytes + // testing suggests that dicts. don't improve the compression ratio and worsen writing/reading speeds, therefore they won't be used + // thus this part could be simplified, but wouldn't strictly adhere to zstd standards + + match frame_content_size_flag { + // not worth it to potentially pre-allocate a maximum of 255 bytes + 0 => Vec::new(), + 1 => { + let mut decompressed_size: [u8; 2] = [0; 2]; + decompressed_size.copy_from_slice( + compressed.get(fcs_sidx..fcs_sidx + 2).ok_or( + anyhow::anyhow!("Not a valid zstd-compressed file").context( + "Failed to get the uncompressed size of the data from two bytes", + ), + )?, + ); + Vec::with_capacity(usize::from(256 + u16::from_le_bytes(decompressed_size))) + } + 2 => { + let mut decompressed_size: [u8; 4] = [0; 4]; + decompressed_size.copy_from_slice( + compressed.get(fcs_sidx..fcs_sidx + 4).ok_or( + anyhow::anyhow!("Not a valid zstd-compressed file").context( + "Failed to get the uncompressed size of the data from four bytes", + ), + )?, + ); + Vec::with_capacity( + u32::from_le_bytes(decompressed_size) + .try_into() + .unwrap_or(usize::MAX), + ) + } + // in practice this should not happen, as a rnote file being larger than 4 GiB is very unlikely + 3 => { + let mut decompressed_size: [u8; 8] = [0; 8]; + decompressed_size.copy_from_slice(compressed.get(fcs_sidx..fcs_sidx + 8).ok_or( + anyhow::anyhow!("Not a valid zstd-compressed file").context( + "Failed to get the uncompressed size of the data from eight bytes", + ), + )?); + Vec::with_capacity( + u64::from_le_bytes(decompressed_size) + .try_into() + .unwrap_or(usize::MAX), + ) + } + // unreachable since our u8 is formed by only 2 bits + 4.. => unreachable!(), + } + }; + let mut decoder = zstd::Decoder::new(compressed)?; + decoder.read_to_end(&mut bytes)?; + Ok(bytes) +} + +/// Compress bytes with zstd +pub fn compress_to_zstd(to_compress: &[u8]) -> Result, anyhow::Error> { + let mut encoder = zstd::Encoder::new(Vec::::new(), 9)?; + encoder.set_pledged_src_size(Some(to_compress.len() as u64))?; + encoder.include_contentsize(true)?; + if let Ok(num_workers) = std::thread::available_parallelism() { + encoder.multithread(num_workers.get() as u32)?; + } + encoder.write_all(to_compress)?; + Ok(encoder.finish()?) +} + /// The rnote file wrapper. /// /// Used to extract and match the version up front, before deserializing the data. From 1b64bfb0dd630d07f2938f008cf389e0a4982e3e Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Wed, 7 Aug 2024 23:19:42 +0200 Subject: [PATCH 02/36] deps that I forgot to commit --- Cargo.lock | 29 +++++++++++++++++++++++++++++ Cargo.toml | 1 + 2 files changed, 30 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index b38a33f7f1..1c4329a54d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3404,6 +3404,7 @@ dependencies = [ "unicode-segmentation", "usvg", "xmlwriter", + "zstd", ] [[package]] @@ -4877,6 +4878,34 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 6a6aa16a7c..2dd154d581 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,6 +86,7 @@ winresource = "0.1.17" xmlwriter = "0.1.0" # Enabling feature > v20_9 causes linker errors on mingw poppler-rs = { version = "0.23.0", features = ["v20_9"] } +zstd = { version = "0.13", features = ["zstdmt"] } [patch.crates-io] # once a new piet (current v0.6.2) is released with updated cairo and kurbo deps, this can be removed. From 11fb351078ac576923910d514463c98b0c332ba5 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:30:48 +0200 Subject: [PATCH 03/36] simpler than I thought + backwards compatible --- .../src/fileformats/rnoteformat/mod.rs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index 548bf94270..d73a339a71 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -170,9 +170,22 @@ impl RnoteFile { impl FileFormatLoader for RnoteFile { fn load_from_bytes(bytes: &[u8]) -> anyhow::Result { - let wrapper = serde_json::from_slice::( - &decompress_from_gzip(bytes).context("decompressing bytes failed.")?, - ) + let wrapper = serde_json::from_slice::(&{ + // zstd magic number + if bytes.starts_with(&[0x28, 0xb5, 0x2f, 0xfd]) { + tracing::info!("using zstd to decompress"); + decompress_from_zstd(bytes)? + } + // gzip ID1 and ID2 + else if bytes.starts_with(&[0x1f, 0x8b]) { + tracing::info!("using gzip to decompress"); + decompress_from_gzip(bytes)? + } else { + Err(anyhow::anyhow!( + "Unknown compression format, expected zstd or gzip" + ))? + } + }) .context("deserializing RnotefileWrapper from bytes failed.")?; // Conversions for older file format versions happen here @@ -224,7 +237,7 @@ impl FileFormatSaver for RnoteFile { version: semver::Version::parse(Self::SEMVER).unwrap(), data: ijson::to_value(self).context("converting RnoteFile to JSON value failed.")?, }; - let compressed = compress_to_gzip( + let compressed = compress_to_zstd( &serde_json::to_vec(&wrapper).context("Serializing RnoteFileWrapper failed.")?, ) .context("compressing bytes failed.")?; From a5ae6b946ffabb32e2b49acceba1ae13794e1944 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Thu, 8 Aug 2024 18:49:30 +0200 Subject: [PATCH 04/36] removed tracing code and compress_to_gzip --- crates/rnote-engine/src/fileformats/rnoteformat/mod.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index d73a339a71..9338d032d5 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -21,13 +21,6 @@ use anyhow::Context; use serde::{Deserialize, Serialize}; use std::io::{Read, Write}; -/// Compress bytes with gzip. -fn compress_to_gzip(to_compress: &[u8]) -> Result, anyhow::Error> { - let mut encoder = flate2::write::GzEncoder::new(Vec::::new(), flate2::Compression::new(5)); - encoder.write_all(to_compress)?; - Ok(encoder.finish()?) -} - /// Decompress from gzip. fn decompress_from_gzip(compressed: &[u8]) -> Result, anyhow::Error> { // Optimization for the gzip format, defined by RFC 1952 @@ -95,6 +88,7 @@ pub fn decompress_from_zstd(compressed: &[u8]) -> Result, anyhow::Error> ), )?, ); + // 256 offset Vec::with_capacity(usize::from(256 + u16::from_le_bytes(decompressed_size))) } 2 => { @@ -173,12 +167,10 @@ impl FileFormatLoader for RnoteFile { let wrapper = serde_json::from_slice::(&{ // zstd magic number if bytes.starts_with(&[0x28, 0xb5, 0x2f, 0xfd]) { - tracing::info!("using zstd to decompress"); decompress_from_zstd(bytes)? } // gzip ID1 and ID2 else if bytes.starts_with(&[0x1f, 0x8b]) { - tracing::info!("using gzip to decompress"); decompress_from_gzip(bytes)? } else { Err(anyhow::anyhow!( From 11da431f62074cfe6ca857e4ac4e706acae0b08f Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:54:50 +0200 Subject: [PATCH 05/36] starting to take shape --- crates/rnote-engine/src/engine/export.rs | 1 + .../src/fileformats/rnoteformat/legacy.rs | 101 ++++++ .../src/fileformats/rnoteformat/maj0min12.rs | 47 +++ .../src/fileformats/rnoteformat/mod.rs | 308 +++++++----------- 4 files changed, 267 insertions(+), 190 deletions(-) create mode 100644 crates/rnote-engine/src/fileformats/rnoteformat/legacy.rs create mode 100644 crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs diff --git a/crates/rnote-engine/src/engine/export.rs b/crates/rnote-engine/src/engine/export.rs index 7c592379d3..b316506941 100644 --- a/crates/rnote-engine/src/engine/export.rs +++ b/crates/rnote-engine/src/engine/export.rs @@ -332,6 +332,7 @@ impl Engine { rayon::spawn(move || { let result = || -> anyhow::Result> { let rnote_file = RnoteFile { + header: RnoteHeader::default(), engine_snapshot: ijson::to_value(&engine_snapshot)?, }; rnote_file.save_as_bytes(&file_name) diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/legacy.rs b/crates/rnote-engine/src/fileformats/rnoteformat/legacy.rs new file mode 100644 index 0000000000..b862b3ffb0 --- /dev/null +++ b/crates/rnote-engine/src/fileformats/rnoteformat/legacy.rs @@ -0,0 +1,101 @@ +// Imports +use super::maj0min5patch8::RnoteFileMaj0Min5Patch8; +use super::maj0min5patch9::RnoteFileMaj0Min5Patch9; +use super::maj0min6::RnoteFileMaj0Min6; +use super::maj0min9::RnoteFileMaj0Min9; +use super::FileFormatLoader; +use anyhow::Context; +use serde::{Deserialize, Serialize}; +use std::io::Read; + +/// Decompress from gzip. +fn decompress_from_gzip(compressed: &[u8]) -> Result, anyhow::Error> { + // Optimization for the gzip format, defined by RFC 1952 + // capacity of the vector defined by the size of the uncompressed data + // given in little endian format, by the last 4 bytes of "compressed" + // + // ISIZE (Input SIZE) + // This contains the size of the original (uncompressed) input data modulo 2^32. + let mut bytes: Vec = { + let mut decompressed_size: [u8; 4] = [0; 4]; + let idx_start = compressed + .len() + .checked_sub(4) + // only happens if the file has less than 4 bytes + .ok_or( + anyhow::anyhow!("Not a valid gzip-compressed file") + .context("Failed to get the size of the decompressed data"), + )?; + decompressed_size.copy_from_slice(&compressed[idx_start..]); + // u32 -> usize to avoid issues on 32-bit architectures + // also more reasonable since the uncompressed size is given by 4 bytes + Vec::with_capacity(u32::from_le_bytes(decompressed_size) as usize) + }; + + let mut decoder = flate2::read::MultiGzDecoder::new(compressed); + decoder.read_to_end(&mut bytes)?; + Ok(bytes) +} + +/// The rnote file wrapper. +/// +/// Used to extract and match the version up front, before deserializing the data. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename = "rnotefile_wrapper")] +struct LegacyRnoteFileWrapper { + #[serde(rename = "version")] + version: semver::Version, + #[serde(rename = "data")] + data: ijson::IValue, +} + +pub type LegacyRnoteFile = RnoteFileMaj0Min9; + +impl FileFormatLoader for LegacyRnoteFile { + fn load_from_bytes(bytes: &[u8]) -> anyhow::Result { + let wrapper = + serde_json::from_slice::(&decompress_from_gzip(bytes)?) + .context("deserializing RnotefileWrapper from bytes failed.")?; + + // Conversions for older file format versions happen here + if semver::VersionReq::parse(">=0.9.0") + .unwrap() + .matches(&wrapper.version) + { + ijson::from_value::(&wrapper.data) + .context("deserializing RnoteFileMaj0Min9 failed.") + } else if semver::VersionReq::parse(">=0.5.10") + .unwrap() + .matches(&wrapper.version) + { + ijson::from_value::(&wrapper.data) + .context("deserializing RnoteFileMaj0Min6 failed.") + .and_then(RnoteFileMaj0Min9::try_from) + .context("converting RnoteFileMaj0Min6 to newest file version failed.") + } else if semver::VersionReq::parse(">=0.5.9") + .unwrap() + .matches(&wrapper.version) + { + ijson::from_value::(&wrapper.data) + .context("deserializing RnoteFileMaj0Min5Patch9 failed.") + .and_then(RnoteFileMaj0Min6::try_from) + .and_then(RnoteFileMaj0Min9::try_from) + .context("converting RnoteFileMaj0Min5Patch9 to newest file version failed.") + } else if semver::VersionReq::parse(">=0.5.0") + .unwrap() + .matches(&wrapper.version) + { + ijson::from_value::(&wrapper.data) + .context("deserializing RnoteFileMaj0Min5Patch8 failed") + .and_then(RnoteFileMaj0Min5Patch9::try_from) + .and_then(RnoteFileMaj0Min6::try_from) + .and_then(RnoteFileMaj0Min9::try_from) + .context("converting RnoteFileMaj0Min5Patch8 to newest file version failed.") + } else { + Err(anyhow::anyhow!( + "failed to load rnote file from bytes, unsupported version: {}.", + wrapper.version + )) + } + } +} diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs new file mode 100644 index 0000000000..289624d836 --- /dev/null +++ b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs @@ -0,0 +1,47 @@ +use super::{maj0min9::RnoteFileMaj0Min9, CompressionMethods, SerializationMethods}; +use serde::{Deserialize, Serialize}; + +/// ## Rnote 0.12.0 format +/// ### rnote magic number +/// [u8; 9] "RNOTEϕλ"" +/// ### version +/// [u8; 3] [major, minor, patch] +/// ### header size +/// size of the json-encoded header, represented by two bytes, little endian +/// [u8; 2] +/// ### header +/// describes how to decompress and deserialize the data +/// ### data +/// serialized and compressed engine snapshot +#[derive(Debug, Clone)] +pub struct RnoteFileMaj0Min12 { + pub header: RnoteHeaderMaj0Min12, + /// A compressed and serialized snapshot of the engine. + pub engine_snapshot: Vec, +} + +impl RnoteFileMaj0Min12 { + // "RNOTEΦΛ" + pub const MAGIC_NUMBER: [u8; 9] = [0x52, 0x4e, 0x4f, 0x54, 0x45, 0xce, 0xa6, 0xce, 0x9b]; + pub const VERSION: [u8; 3] = [0, 12, 0]; +} + +impl From for RnoteFileMaj0Min12 { + fn from(value: RnoteFileMaj0Min9) -> Self { + Self { + header: RnoteHeaderMaj0Min12 { + serialization: SerializationMethods::Json, + compression: CompressionMethods::None, + size: 0, + }, + engine_snapshot: serde_json::to_vec(&value.engine_snapshot)?, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RnoteHeaderMaj0Min12 { + pub serialization: SerializationMethods, + pub compression: CompressionMethods, + pub size: u64, +} diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index 9338d032d5..fe0e5e8b03 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -6,125 +6,50 @@ //! Then [TryFrom] can be implemented to allow conversions and chaining from older to newer versions. // Modules +pub(crate) mod legacy; +pub(crate) mod maj0min12; pub(crate) mod maj0min5patch8; pub(crate) mod maj0min5patch9; pub(crate) mod maj0min6; pub(crate) mod maj0min9; -// Imports -use self::maj0min5patch8::RnoteFileMaj0Min5Patch8; -use self::maj0min5patch9::RnoteFileMaj0Min5Patch9; -use self::maj0min6::RnoteFileMaj0Min6; -use self::maj0min9::RnoteFileMaj0Min9; +use crate::engine::EngineSnapshot; + use super::{FileFormatLoader, FileFormatSaver}; -use anyhow::Context; +use legacy::LegacyRnoteFile; use serde::{Deserialize, Serialize}; -use std::io::{Read, Write}; +use std::{ + io::{Read, Write}, + usize, +}; -/// Decompress from gzip. -fn decompress_from_gzip(compressed: &[u8]) -> Result, anyhow::Error> { - // Optimization for the gzip format, defined by RFC 1952 - // capacity of the vector defined by the size of the uncompressed data - // given in little endian format, by the last 4 bytes of "compressed" - // - // ISIZE (Input SIZE) - // This contains the size of the original (uncompressed) input data modulo 2^32. - let mut bytes: Vec = { - let mut decompressed_size: [u8; 4] = [0; 4]; - let idx_start = compressed - .len() - .checked_sub(4) - // only happens if the file has less than 4 bytes - .ok_or( - anyhow::anyhow!("Not a valid gzip-compressed file") - .context("Failed to get the size of the decompressed data"), - )?; - decompressed_size.copy_from_slice(&compressed[idx_start..]); - // u32 -> usize to avoid issues on 32-bit architectures - // also more reasonable since the uncompressed size is given by 4 bytes - Vec::with_capacity(u32::from_le_bytes(decompressed_size) as usize) - }; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum CompressionMethods { + None, + Gzip, + Zstd, +} - let mut decoder = flate2::read::MultiGzDecoder::new(compressed); - decoder.read_to_end(&mut bytes)?; - Ok(bytes) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum SerializationMethods { + Bincode, + Json, } -/// Decompress bytes with zstd -pub fn decompress_from_zstd(compressed: &[u8]) -> Result, anyhow::Error> { - // Optimization for the zstd format, less pretty than for gzip but this does shave off a bit of time - // https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#frame_header - let mut bytes: Vec = { - let frame_header_descriptor = compressed.get(4).ok_or( - anyhow::anyhow!("Not a valid zstd-compressed file") - .context("Failed to get the frame header descriptor of the file"), - )?; - - let frame_content_size_flag = frame_header_descriptor >> 6; - let single_segment_flag = (frame_header_descriptor >> 5) & 1; - let did_field_size = { - let dictionary_id_flag = frame_header_descriptor & 11; - if dictionary_id_flag == 3 { - 4 - } else { - dictionary_id_flag - } - }; - // frame header size start index - let fcs_sidx = (6 + did_field_size - single_segment_flag) as usize; - // magic number: 4 bytes + window descriptor: 1 byte if single segment flag is not set + frame header descriptor: 1 byte + dict. field size: 0-4 bytes - // testing suggests that dicts. don't improve the compression ratio and worsen writing/reading speeds, therefore they won't be used - // thus this part could be simplified, but wouldn't strictly adhere to zstd standards - - match frame_content_size_flag { - // not worth it to potentially pre-allocate a maximum of 255 bytes - 0 => Vec::new(), - 1 => { - let mut decompressed_size: [u8; 2] = [0; 2]; - decompressed_size.copy_from_slice( - compressed.get(fcs_sidx..fcs_sidx + 2).ok_or( - anyhow::anyhow!("Not a valid zstd-compressed file").context( - "Failed to get the uncompressed size of the data from two bytes", - ), - )?, - ); - // 256 offset - Vec::with_capacity(usize::from(256 + u16::from_le_bytes(decompressed_size))) - } - 2 => { - let mut decompressed_size: [u8; 4] = [0; 4]; - decompressed_size.copy_from_slice( - compressed.get(fcs_sidx..fcs_sidx + 4).ok_or( - anyhow::anyhow!("Not a valid zstd-compressed file").context( - "Failed to get the uncompressed size of the data from four bytes", - ), - )?, - ); - Vec::with_capacity( - u32::from_le_bytes(decompressed_size) - .try_into() - .unwrap_or(usize::MAX), - ) - } - // in practice this should not happen, as a rnote file being larger than 4 GiB is very unlikely - 3 => { - let mut decompressed_size: [u8; 8] = [0; 8]; - decompressed_size.copy_from_slice(compressed.get(fcs_sidx..fcs_sidx + 8).ok_or( - anyhow::anyhow!("Not a valid zstd-compressed file").context( - "Failed to get the uncompressed size of the data from eight bytes", - ), - )?); - Vec::with_capacity( - u64::from_le_bytes(decompressed_size) - .try_into() - .unwrap_or(usize::MAX), - ) - } - // unreachable since our u8 is formed by only 2 bits - 4.. => unreachable!(), - } - }; - let mut decoder = zstd::Decoder::new(compressed)?; +/// Compress bytes with gzip. +fn compress_to_gzip(to_compress: &[u8]) -> Result, anyhow::Error> { + let mut encoder = flate2::write::GzEncoder::new(Vec::::new(), flate2::Compression::new(5)); + encoder.write_all(to_compress)?; + Ok(encoder.finish()?) +} + +/// Decompress from gzip. +fn decompress_from_gzip( + compressed: &[u8], + uncompressed_siez: usize, +) -> Result, anyhow::Error> { + let mut bytes: Vec = Vec::with_capacity(uncompressed_siez); + let mut decoder = flate2::read::MultiGzDecoder::new(compressed); decoder.read_to_end(&mut bytes)?; Ok(bytes) } @@ -132,8 +57,6 @@ pub fn decompress_from_zstd(compressed: &[u8]) -> Result, anyhow::Error> /// Compress bytes with zstd pub fn compress_to_zstd(to_compress: &[u8]) -> Result, anyhow::Error> { let mut encoder = zstd::Encoder::new(Vec::::new(), 9)?; - encoder.set_pledged_src_size(Some(to_compress.len() as u64))?; - encoder.include_contentsize(true)?; if let Ok(num_workers) = std::thread::available_parallelism() { encoder.multithread(num_workers.get() as u32)?; } @@ -141,99 +64,104 @@ pub fn compress_to_zstd(to_compress: &[u8]) -> Result, anyhow::Error> { Ok(encoder.finish()?) } -/// The rnote file wrapper. -/// -/// Used to extract and match the version up front, before deserializing the data. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename = "rnotefile_wrapper")] -struct RnotefileWrapper { - #[serde(rename = "version")] - version: semver::Version, - #[serde(rename = "data")] - data: ijson::IValue, +/// Decompress bytes with zstd +pub fn decompress_from_zstd( + compressed: &[u8], + uncompressed_siez: usize, +) -> Result, anyhow::Error> { + let mut bytes: Vec = Vec::with_capacity(uncompressed_siez); + let mut decoder = zstd::Decoder::new(compressed)?; + decoder.read_to_end(&mut bytes)?; + Ok(bytes) } -/// The Rnote file in the newest format version. -/// -/// This struct exists to allow for upgrading older versions before loading the file in. -pub type RnoteFile = RnoteFileMaj0Min9; +pub type RnoteFile = maj0min12::RnoteFileMaj0Min12; -impl RnoteFile { - pub const SEMVER: &'static str = crate::utils::crate_version(); +impl FileFormatSaver for RnoteFile { + fn save_as_bytes(&self, _file_name: &str) -> anyhow::Result> { + let json_header = serde_json::to_vec(&self.header)?; + let header = [ + &Self::MAGIC_NUMBER[..], + &Self::VERSION[..], + &u16::try_from(json_header.len())?.to_le_bytes(), + &json_header, + ] + .concat(); + let mut buffer: Vec = Vec::new(); + buffer.write_all(&header)?; + buffer.write_all(&self.engine_snapshot)?; + Ok(buffer) + } } impl FileFormatLoader for RnoteFile { - fn load_from_bytes(bytes: &[u8]) -> anyhow::Result { - let wrapper = serde_json::from_slice::(&{ - // zstd magic number - if bytes.starts_with(&[0x28, 0xb5, 0x2f, 0xfd]) { - decompress_from_zstd(bytes)? - } - // gzip ID1 and ID2 - else if bytes.starts_with(&[0x1f, 0x8b]) { - decompress_from_gzip(bytes)? + fn load_from_bytes(bytes: &[u8]) -> anyhow::Result + where + Self: Sized, + { + let magic_number = bytes + .get(..9) + .ok_or(anyhow::anyhow!("Failed to get magic number"))?; + + if magic_number != Self::MAGIC_NUMBER { + if magic_number[..2] == [0x1f, 0x8b] { + return Ok(RnoteFile::from(LegacyRnoteFile::load_from_bytes(bytes)?)); } else { - Err(anyhow::anyhow!( - "Unknown compression format, expected zstd or gzip" - ))? + Err(anyhow::anyhow!("Unkown file"))?; } - }) - .context("deserializing RnotefileWrapper from bytes failed.")?; - - // Conversions for older file format versions happen here - if semver::VersionReq::parse(">=0.9.0") - .unwrap() - .matches(&wrapper.version) - { - ijson::from_value::(&wrapper.data) - .context("deserializing RnoteFileMaj0Min9 failed.") - } else if semver::VersionReq::parse(">=0.5.10") - .unwrap() - .matches(&wrapper.version) - { - ijson::from_value::(&wrapper.data) - .context("deserializing RnoteFileMaj0Min6 failed.") - .and_then(RnoteFileMaj0Min9::try_from) - .context("converting RnoteFileMaj0Min6 to newest file version failed.") - } else if semver::VersionReq::parse(">=0.5.9") - .unwrap() - .matches(&wrapper.version) - { - ijson::from_value::(&wrapper.data) - .context("deserializing RnoteFileMaj0Min5Patch9 failed.") - .and_then(RnoteFileMaj0Min6::try_from) - .and_then(RnoteFileMaj0Min9::try_from) - .context("converting RnoteFileMaj0Min5Patch9 to newest file version failed.") - } else if semver::VersionReq::parse(">=0.5.0") - .unwrap() - .matches(&wrapper.version) - { - ijson::from_value::(&wrapper.data) - .context("deserializing RnoteFileMaj0Min5Patch8 failed") - .and_then(RnoteFileMaj0Min5Patch9::try_from) - .and_then(RnoteFileMaj0Min6::try_from) - .and_then(RnoteFileMaj0Min9::try_from) - .context("converting RnoteFileMaj0Min5Patch8 to newest file version failed.") - } else { - Err(anyhow::anyhow!( - "failed to load rnote file from bytes, unsupported version: {}.", - wrapper.version - )) } + + let mut version: [u8; 3] = [0; 3]; + version.copy_from_slice( + bytes + .get(9..12) + .ok_or(anyhow::anyhow!("Failed to get version"))?, + ); + + let mut header_size: [u8; 2] = [0; 2]; + header_size.copy_from_slice( + bytes + .get(12..14) + .ok_or(anyhow::anyhow!("Failed to get header size"))?, + ); + let header_size = u16::from_le_bytes(header_size); + + let header_slice = bytes + .get(14..14 + header_size as usize) + .ok_or(anyhow::anyhow!("Missing header"))?; + + let body_slice = bytes + .get(14 + header_size as usize..) + .ok_or(anyhow::anyhow!("Missing body"))?; + + Ok(Self { + header: serde_json::from_slice(header_slice)?, + engine_snapshot: body_slice.to_vec(), + }) } } -impl FileFormatSaver for RnoteFile { - fn save_as_bytes(&self, _file_name: &str) -> anyhow::Result> { - let wrapper = RnotefileWrapper { - version: semver::Version::parse(Self::SEMVER).unwrap(), - data: ijson::to_value(self).context("converting RnoteFile to JSON value failed.")?, +impl TryFrom for EngineSnapshot { + type Error = anyhow::Error; + + fn try_from(value: RnoteFile) -> Result { + let uncompressed = match value.header.compression { + CompressionMethods::None => value.engine_snapshot, + CompressionMethods::Gzip => decompress_from_gzip( + &value.engine_snapshot, + usize::try_from(value.header.size).unwrap_or(usize::MAX), + )?, + CompressionMethods::Zstd => decompress_from_zstd( + &value.engine_snapshot, + usize::try_from(value.header.size).unwrap_or(usize::MAX), + )?, }; - let compressed = compress_to_zstd( - &serde_json::to_vec(&wrapper).context("Serializing RnoteFileWrapper failed.")?, - ) - .context("compressing bytes failed.")?; - Ok(compressed) + match value.header.serialization { + SerializationMethods::Json => Ok(ijson::from_value(&serde_json::from_slice::< + ijson::IValue, + >(&uncompressed)?)?), + SerializationMethods::Bincode => unreachable!(), + } } } From eab08bb58fded2b4f54f030fd4732274ab6f25d3 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Wed, 14 Aug 2024 17:43:30 +0200 Subject: [PATCH 06/36] far from over, but it works at least --- crates/rnote-engine/src/engine/export.rs | 7 ++--- crates/rnote-engine/src/engine/snapshot.rs | 2 +- .../src/fileformats/rnoteformat/maj0min12.rs | 9 +++--- .../src/fileformats/rnoteformat/mod.rs | 30 +++++++++++++++++-- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/crates/rnote-engine/src/engine/export.rs b/crates/rnote-engine/src/engine/export.rs index b316506941..0ffe71c92a 100644 --- a/crates/rnote-engine/src/engine/export.rs +++ b/crates/rnote-engine/src/engine/export.rs @@ -1,6 +1,6 @@ // Imports use super::{Engine, EngineConfig, StrokeContent}; -use crate::fileformats::rnoteformat::RnoteFile; +use crate::fileformats::rnoteformat::maj0min12::RnoteFileMaj0Min12; use crate::fileformats::{xoppformat, FileFormatSaver}; use crate::CloneConfig; use anyhow::Context; @@ -331,10 +331,7 @@ impl Engine { let engine_snapshot = self.take_snapshot(); rayon::spawn(move || { let result = || -> anyhow::Result> { - let rnote_file = RnoteFile { - header: RnoteHeader::default(), - engine_snapshot: ijson::to_value(&engine_snapshot)?, - }; + let rnote_file = RnoteFileMaj0Min12::try_from(engine_snapshot)?; rnote_file.save_as_bytes(&file_name) }; if oneshot_sender.send(result()).is_err() { diff --git a/crates/rnote-engine/src/engine/snapshot.rs b/crates/rnote-engine/src/engine/snapshot.rs index 233bbea31b..7ac4fe61df 100644 --- a/crates/rnote-engine/src/engine/snapshot.rs +++ b/crates/rnote-engine/src/engine/snapshot.rs @@ -51,7 +51,7 @@ impl EngineSnapshot { let result = || -> anyhow::Result { let rnote_file = rnoteformat::RnoteFile::load_from_bytes(&bytes) .context("loading RnoteFile from bytes failed.")?; - Ok(ijson::from_value(&rnote_file.engine_snapshot)?) + Ok(Self::try_from(rnote_file)?) }; if let Err(_data) = snapshot_sender.send(result()) { diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs index 289624d836..10e19c7b1e 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs @@ -26,16 +26,17 @@ impl RnoteFileMaj0Min12 { pub const VERSION: [u8; 3] = [0, 12, 0]; } -impl From for RnoteFileMaj0Min12 { - fn from(value: RnoteFileMaj0Min9) -> Self { - Self { +impl TryFrom for RnoteFileMaj0Min12 { + type Error = anyhow::Error; + fn try_from(value: RnoteFileMaj0Min9) -> Result { + Ok(Self { header: RnoteHeaderMaj0Min12 { serialization: SerializationMethods::Json, compression: CompressionMethods::None, size: 0, }, engine_snapshot: serde_json::to_vec(&value.engine_snapshot)?, - } + }) } } diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index fe0e5e8b03..f59d84c01c 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -17,6 +17,7 @@ use crate::engine::EngineSnapshot; use super::{FileFormatLoader, FileFormatSaver}; use legacy::LegacyRnoteFile; +use maj0min12::RnoteFileMaj0Min12; use serde::{Deserialize, Serialize}; use std::{ io::{Read, Write}, @@ -76,6 +77,7 @@ pub fn decompress_from_zstd( } pub type RnoteFile = maj0min12::RnoteFileMaj0Min12; +pub type RnoteHeader = maj0min12::RnoteHeaderMaj0Min12; impl FileFormatSaver for RnoteFile { fn save_as_bytes(&self, _file_name: &str) -> anyhow::Result> { @@ -105,7 +107,9 @@ impl FileFormatLoader for RnoteFile { if magic_number != Self::MAGIC_NUMBER { if magic_number[..2] == [0x1f, 0x8b] { - return Ok(RnoteFile::from(LegacyRnoteFile::load_from_bytes(bytes)?)); + return Ok(RnoteFile::try_from(LegacyRnoteFile::load_from_bytes( + bytes, + )?)?); } else { Err(anyhow::anyhow!("Unkown file"))?; } @@ -141,10 +145,10 @@ impl FileFormatLoader for RnoteFile { } } -impl TryFrom for EngineSnapshot { +impl TryFrom for EngineSnapshot { type Error = anyhow::Error; - fn try_from(value: RnoteFile) -> Result { + fn try_from(value: RnoteFileMaj0Min12) -> Result { let uncompressed = match value.header.compression { CompressionMethods::None => value.engine_snapshot, CompressionMethods::Gzip => decompress_from_gzip( @@ -165,3 +169,23 @@ impl TryFrom for EngineSnapshot { } } } + +impl TryFrom for RnoteFile { + type Error = anyhow::Error; + + fn try_from(value: EngineSnapshot) -> Result { + let json = serde_json::to_vec(&ijson::to_value(value)?)?; + let size = json.len() as u64; + + let engine_snapshot = compress_to_zstd(&json)?; + + Ok(Self { + header: RnoteHeader { + compression: CompressionMethods::Zstd, + serialization: SerializationMethods::Json, + size, + }, + engine_snapshot, + }) + } +} From b1195cba824adf1ed20a9203a095993f8186b0e2 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Fri, 16 Aug 2024 11:40:59 +0200 Subject: [PATCH 07/36] lots of clean up, set max header size to u32::MAX instead of u16::MAX out of paranoia --- .../{ => legacy}/maj0min5patch8.rs | 0 .../{ => legacy}/maj0min5patch9.rs | 0 .../rnoteformat/{ => legacy}/maj0min6.rs | 0 .../rnoteformat/{ => legacy}/maj0min9.rs | 0 .../rnoteformat/{legacy.rs => legacy/mod.rs} | 14 +- .../src/fileformats/rnoteformat/maj0min12.rs | 40 ++--- .../src/fileformats/rnoteformat/methods.rs | 73 +++++++++ .../src/fileformats/rnoteformat/mod.rs | 148 +++++------------- 8 files changed, 143 insertions(+), 132 deletions(-) rename crates/rnote-engine/src/fileformats/rnoteformat/{ => legacy}/maj0min5patch8.rs (100%) rename crates/rnote-engine/src/fileformats/rnoteformat/{ => legacy}/maj0min5patch9.rs (100%) rename crates/rnote-engine/src/fileformats/rnoteformat/{ => legacy}/maj0min6.rs (100%) rename crates/rnote-engine/src/fileformats/rnoteformat/{ => legacy}/maj0min9.rs (100%) rename crates/rnote-engine/src/fileformats/rnoteformat/{legacy.rs => legacy/mod.rs} (94%) create mode 100644 crates/rnote-engine/src/fileformats/rnoteformat/methods.rs diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min5patch8.rs b/crates/rnote-engine/src/fileformats/rnoteformat/legacy/maj0min5patch8.rs similarity index 100% rename from crates/rnote-engine/src/fileformats/rnoteformat/maj0min5patch8.rs rename to crates/rnote-engine/src/fileformats/rnoteformat/legacy/maj0min5patch8.rs diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min5patch9.rs b/crates/rnote-engine/src/fileformats/rnoteformat/legacy/maj0min5patch9.rs similarity index 100% rename from crates/rnote-engine/src/fileformats/rnoteformat/maj0min5patch9.rs rename to crates/rnote-engine/src/fileformats/rnoteformat/legacy/maj0min5patch9.rs diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min6.rs b/crates/rnote-engine/src/fileformats/rnoteformat/legacy/maj0min6.rs similarity index 100% rename from crates/rnote-engine/src/fileformats/rnoteformat/maj0min6.rs rename to crates/rnote-engine/src/fileformats/rnoteformat/legacy/maj0min6.rs diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min9.rs b/crates/rnote-engine/src/fileformats/rnoteformat/legacy/maj0min9.rs similarity index 100% rename from crates/rnote-engine/src/fileformats/rnoteformat/maj0min9.rs rename to crates/rnote-engine/src/fileformats/rnoteformat/legacy/maj0min9.rs diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/legacy.rs b/crates/rnote-engine/src/fileformats/rnoteformat/legacy/mod.rs similarity index 94% rename from crates/rnote-engine/src/fileformats/rnoteformat/legacy.rs rename to crates/rnote-engine/src/fileformats/rnoteformat/legacy/mod.rs index b862b3ffb0..51d8675f79 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/legacy.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/legacy/mod.rs @@ -1,10 +1,16 @@ +// Modules +mod maj0min5patch8; +mod maj0min5patch9; +mod maj0min6; +pub mod maj0min9; + // Imports -use super::maj0min5patch8::RnoteFileMaj0Min5Patch8; -use super::maj0min5patch9::RnoteFileMaj0Min5Patch9; -use super::maj0min6::RnoteFileMaj0Min6; -use super::maj0min9::RnoteFileMaj0Min9; use super::FileFormatLoader; use anyhow::Context; +use maj0min5patch8::RnoteFileMaj0Min5Patch8; +use maj0min5patch9::RnoteFileMaj0Min5Patch9; +use maj0min6::RnoteFileMaj0Min6; +use maj0min9::RnoteFileMaj0Min9; use serde::{Deserialize, Serialize}; use std::io::Read; diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs index 10e19c7b1e..efb123d4a8 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs @@ -1,48 +1,52 @@ -use super::{maj0min9::RnoteFileMaj0Min9, CompressionMethods, SerializationMethods}; +use super::{ + legacy::maj0min9::RnoteFileMaj0Min9, + methods::{CompM, SerM}, +}; use serde::{Deserialize, Serialize}; -/// ## Rnote 0.12.0 format +/// ## Description of Rnote's 0.12.0 file format /// ### rnote magic number -/// [u8; 9] "RNOTEϕλ"" +/// the magic number present at the start of each rnote file, used to identify them +/// "RNOTEϕλ" -> "RNOTEFILE" (a useful pun, phi-lambda ~ file) +/// [u8; 9] /// ### version /// [u8; 3] [major, minor, patch] /// ### header size -/// size of the json-encoded header, represented by two bytes, little endian -/// [u8; 2] +/// size of the json-encoded header, represented by 4 bytes, little endian +/// [u8; 4] /// ### header /// describes how to decompress and deserialize the data /// ### data /// serialized and compressed engine snapshot #[derive(Debug, Clone)] pub struct RnoteFileMaj0Min12 { - pub header: RnoteHeaderMaj0Min12, - /// A compressed and serialized snapshot of the engine. - pub engine_snapshot: Vec, + pub head: RnoteHeaderMaj0Min12, + /// A serialized and (potentially) compressed engine snapshot + pub body: Vec, } impl RnoteFileMaj0Min12 { - // "RNOTEΦΛ" - pub const MAGIC_NUMBER: [u8; 9] = [0x52, 0x4e, 0x4f, 0x54, 0x45, 0xce, 0xa6, 0xce, 0x9b]; pub const VERSION: [u8; 3] = [0, 12, 0]; + pub const SEMVER: semver::Version = semver::Version::new(0, 12, 0); } impl TryFrom for RnoteFileMaj0Min12 { type Error = anyhow::Error; fn try_from(value: RnoteFileMaj0Min9) -> Result { Ok(Self { - header: RnoteHeaderMaj0Min12 { - serialization: SerializationMethods::Json, - compression: CompressionMethods::None, - size: 0, + head: RnoteHeaderMaj0Min12 { + serialization: SerM::Json, + compression: CompM::None, + uc_size: 0, }, - engine_snapshot: serde_json::to_vec(&value.engine_snapshot)?, + body: serde_json::to_vec(&value.engine_snapshot)?, }) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RnoteHeaderMaj0Min12 { - pub serialization: SerializationMethods, - pub compression: CompressionMethods, - pub size: u64, + pub serialization: SerM, + pub compression: CompM, + pub uc_size: u64, } diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs new file mode 100644 index 0000000000..aae1d8ccfa --- /dev/null +++ b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs @@ -0,0 +1,73 @@ +use serde::{Deserialize, Serialize}; +use std::io::{Read, Write}; + +use crate::engine::EngineSnapshot; + +#[derive(Debug, Clone, Serialize, Deserialize)] +/// Compression methods that can be applied to the serialized engine snapshot +pub enum CompM { + None, + Gzip, + Zstd, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +/// Serialization methods that can be applied to a snapshot of the engine +pub enum SerM { + Bincode, + Json, +} + +impl CompM { + pub fn compress(&self, data: Vec) -> anyhow::Result> { + match self { + Self::None => Ok(data), + Self::Gzip => { + let mut encoder = + flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::new(5)); + encoder.write_all(&data)?; + Ok(encoder.finish()?) + } + Self::Zstd => { + let mut encoder = zstd::Encoder::new(Vec::::new(), 9)?; + if let Ok(num_workers) = std::thread::available_parallelism() { + encoder.multithread(num_workers.get() as u32)?; + } + encoder.write_all(&data)?; + Ok(encoder.finish()?) + } + } + } + pub fn decompress(&self, uc_size: usize, data: Vec) -> anyhow::Result> { + match self { + Self::None => Ok(data), + Self::Gzip => { + let mut bytes: Vec = Vec::with_capacity(uc_size); + let mut decoder = flate2::read::MultiGzDecoder::new(&data[..]); + decoder.read_to_end(&mut bytes)?; + Ok(bytes) + } + Self::Zstd => { + let mut bytes: Vec = Vec::with_capacity(uc_size); + let mut decoder = zstd::Decoder::new(&data[..])?; + decoder.read_to_end(&mut bytes)?; + Ok(bytes) + } + } + } +} + +impl SerM { + pub fn serialize(&self, engine_snapshot: &EngineSnapshot) -> anyhow::Result> { + match self { + Self::Json => Ok(serde_json::to_vec(&ijson::to_value(engine_snapshot)?)?), + Self::Bincode => unreachable!(), + } + } + pub fn deserialize(&self, data: &[u8]) -> anyhow::Result { + match self { + Self::Json => Ok(ijson::from_value(&serde_json::from_slice(data)?)?), + Self::Bincode => unreachable!(), + } + } +} diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index f59d84c01c..ecabdee3db 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -8,90 +8,34 @@ // Modules pub(crate) mod legacy; pub(crate) mod maj0min12; -pub(crate) mod maj0min5patch8; -pub(crate) mod maj0min5patch9; -pub(crate) mod maj0min6; -pub(crate) mod maj0min9; - -use crate::engine::EngineSnapshot; +pub(crate) mod methods; +// Imports use super::{FileFormatLoader, FileFormatSaver}; +use crate::engine::EngineSnapshot; use legacy::LegacyRnoteFile; use maj0min12::RnoteFileMaj0Min12; -use serde::{Deserialize, Serialize}; -use std::{ - io::{Read, Write}, - usize, -}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum CompressionMethods { - None, - Gzip, - Zstd, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum SerializationMethods { - Bincode, - Json, -} - -/// Compress bytes with gzip. -fn compress_to_gzip(to_compress: &[u8]) -> Result, anyhow::Error> { - let mut encoder = flate2::write::GzEncoder::new(Vec::::new(), flate2::Compression::new(5)); - encoder.write_all(to_compress)?; - Ok(encoder.finish()?) -} - -/// Decompress from gzip. -fn decompress_from_gzip( - compressed: &[u8], - uncompressed_siez: usize, -) -> Result, anyhow::Error> { - let mut bytes: Vec = Vec::with_capacity(uncompressed_siez); - let mut decoder = flate2::read::MultiGzDecoder::new(compressed); - decoder.read_to_end(&mut bytes)?; - Ok(bytes) -} - -/// Compress bytes with zstd -pub fn compress_to_zstd(to_compress: &[u8]) -> Result, anyhow::Error> { - let mut encoder = zstd::Encoder::new(Vec::::new(), 9)?; - if let Ok(num_workers) = std::thread::available_parallelism() { - encoder.multithread(num_workers.get() as u32)?; - } - encoder.write_all(to_compress)?; - Ok(encoder.finish()?) -} - -/// Decompress bytes with zstd -pub fn decompress_from_zstd( - compressed: &[u8], - uncompressed_siez: usize, -) -> Result, anyhow::Error> { - let mut bytes: Vec = Vec::with_capacity(uncompressed_siez); - let mut decoder = zstd::Decoder::new(compressed)?; - decoder.read_to_end(&mut bytes)?; - Ok(bytes) -} +use methods::{CompM, SerM}; +use std::io::Write; pub type RnoteFile = maj0min12::RnoteFileMaj0Min12; pub type RnoteHeader = maj0min12::RnoteHeaderMaj0Min12; +pub const MAGIC_NUMBER: [u8; 9] = [0x52, 0x4e, 0x4f, 0x54, 0x45, 0xce, 0xa6, 0xce, 0x9b]; + impl FileFormatSaver for RnoteFile { fn save_as_bytes(&self, _file_name: &str) -> anyhow::Result> { - let json_header = serde_json::to_vec(&self.header)?; + let json_header = serde_json::to_vec(&self.head)?; let header = [ - &Self::MAGIC_NUMBER[..], + &MAGIC_NUMBER[..], &Self::VERSION[..], - &u16::try_from(json_header.len())?.to_le_bytes(), + &u32::try_from(json_header.len())?.to_le_bytes(), &json_header, ] .concat(); let mut buffer: Vec = Vec::new(); buffer.write_all(&header)?; - buffer.write_all(&self.engine_snapshot)?; + buffer.write_all(&self.body)?; Ok(buffer) } } @@ -105,13 +49,11 @@ impl FileFormatLoader for RnoteFile { .get(..9) .ok_or(anyhow::anyhow!("Failed to get magic number"))?; - if magic_number != Self::MAGIC_NUMBER { + if magic_number != MAGIC_NUMBER { if magic_number[..2] == [0x1f, 0x8b] { - return Ok(RnoteFile::try_from(LegacyRnoteFile::load_from_bytes( - bytes, - )?)?); + return RnoteFile::try_from(LegacyRnoteFile::load_from_bytes(bytes)?); } else { - Err(anyhow::anyhow!("Unkown file"))?; + Err(anyhow::anyhow!("Unkown file format"))?; } } @@ -122,25 +64,25 @@ impl FileFormatLoader for RnoteFile { .ok_or(anyhow::anyhow!("Failed to get version"))?, ); - let mut header_size: [u8; 2] = [0; 2]; + let mut header_size: [u8; 4] = [0; 4]; header_size.copy_from_slice( bytes - .get(12..14) + .get(12..16) .ok_or(anyhow::anyhow!("Failed to get header size"))?, ); - let header_size = u16::from_le_bytes(header_size); + let header_size = u32::from_le_bytes(header_size); let header_slice = bytes - .get(14..14 + header_size as usize) - .ok_or(anyhow::anyhow!("Missing header"))?; + .get(16..16 + usize::try_from(header_size)?) + .ok_or(anyhow::anyhow!("File head missing"))?; let body_slice = bytes - .get(14 + header_size as usize..) - .ok_or(anyhow::anyhow!("Missing body"))?; + .get(16 + usize::try_from(header_size)?..) + .ok_or(anyhow::anyhow!("File body missing"))?; Ok(Self { - header: serde_json::from_slice(header_slice)?, - engine_snapshot: body_slice.to_vec(), + head: serde_json::from_slice(header_slice)?, + body: body_slice.to_vec(), }) } } @@ -149,43 +91,29 @@ impl TryFrom for EngineSnapshot { type Error = anyhow::Error; fn try_from(value: RnoteFileMaj0Min12) -> Result { - let uncompressed = match value.header.compression { - CompressionMethods::None => value.engine_snapshot, - CompressionMethods::Gzip => decompress_from_gzip( - &value.engine_snapshot, - usize::try_from(value.header.size).unwrap_or(usize::MAX), - )?, - CompressionMethods::Zstd => decompress_from_zstd( - &value.engine_snapshot, - usize::try_from(value.header.size).unwrap_or(usize::MAX), - )?, - }; - - match value.header.serialization { - SerializationMethods::Json => Ok(ijson::from_value(&serde_json::from_slice::< - ijson::IValue, - >(&uncompressed)?)?), - SerializationMethods::Bincode => unreachable!(), - } + let uc_size = usize::try_from(value.head.uc_size).unwrap_or(usize::MAX); + let uc_body = value.head.compression.decompress(uc_size, value.body)?; + value.head.serialization.deserialize(&uc_body) } } -impl TryFrom for RnoteFile { +impl TryFrom<&EngineSnapshot> for RnoteFile { type Error = anyhow::Error; - fn try_from(value: EngineSnapshot) -> Result { - let json = serde_json::to_vec(&ijson::to_value(value)?)?; - let size = json.len() as u64; - - let engine_snapshot = compress_to_zstd(&json)?; + fn try_from(value: &EngineSnapshot) -> Result { + let serialization = SerM::Json; + let compression = CompM::Zstd; + let uc_data = serialization.serialize(value)?; + let uc_size = uc_data.len() as u64; + let data = compression.compress(uc_data)?; Ok(Self { - header: RnoteHeader { - compression: CompressionMethods::Zstd, - serialization: SerializationMethods::Json, - size, + head: RnoteHeader { + compression, + serialization, + uc_size, }, - engine_snapshot, + body: data, }) } } From 89704a006577d5e823ccd50f9578ec238e08afed Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Fri, 16 Aug 2024 12:14:52 +0200 Subject: [PATCH 08/36] meson.build fix, forward-comp header --- crates/rnote-engine/src/engine/export.rs | 2 +- .../src/fileformats/rnoteformat/maj0min12.rs | 5 --- .../src/fileformats/rnoteformat/mod.rs | 32 ++++++++++++++++--- crates/rnote-engine/src/meson.build | 11 ++++--- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/crates/rnote-engine/src/engine/export.rs b/crates/rnote-engine/src/engine/export.rs index 0ffe71c92a..1d4178acd9 100644 --- a/crates/rnote-engine/src/engine/export.rs +++ b/crates/rnote-engine/src/engine/export.rs @@ -331,7 +331,7 @@ impl Engine { let engine_snapshot = self.take_snapshot(); rayon::spawn(move || { let result = || -> anyhow::Result> { - let rnote_file = RnoteFileMaj0Min12::try_from(engine_snapshot)?; + let rnote_file = RnoteFileMaj0Min12::try_from(&engine_snapshot)?; rnote_file.save_as_bytes(&file_name) }; if oneshot_sender.send(result()).is_err() { diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs index efb123d4a8..0c8a6181de 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs @@ -25,11 +25,6 @@ pub struct RnoteFileMaj0Min12 { pub body: Vec, } -impl RnoteFileMaj0Min12 { - pub const VERSION: [u8; 3] = [0, 12, 0]; - pub const SEMVER: semver::Version = semver::Version::new(0, 12, 0); -} - impl TryFrom for RnoteFileMaj0Min12 { type Error = anyhow::Error; fn try_from(value: RnoteFileMaj0Min9) -> Result { diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index ecabdee3db..a6db15ce2a 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -21,13 +21,17 @@ use std::io::Write; pub type RnoteFile = maj0min12::RnoteFileMaj0Min12; pub type RnoteHeader = maj0min12::RnoteHeaderMaj0Min12; -pub const MAGIC_NUMBER: [u8; 9] = [0x52, 0x4e, 0x4f, 0x54, 0x45, 0xce, 0xa6, 0xce, 0x9b]; +impl RnoteFileMaj0Min12 { + pub const MAGIC_NUMBER: [u8; 9] = [0x52, 0x4e, 0x4f, 0x54, 0x45, 0xce, 0xa6, 0xce, 0x9b]; + pub const VERSION: [u8; 3] = [0, 12, 0]; + pub const SEMVER: semver::Version = semver::Version::new(0, 12, 0); +} impl FileFormatSaver for RnoteFile { fn save_as_bytes(&self, _file_name: &str) -> anyhow::Result> { let json_header = serde_json::to_vec(&self.head)?; let header = [ - &MAGIC_NUMBER[..], + &Self::MAGIC_NUMBER[..], &Self::VERSION[..], &u32::try_from(json_header.len())?.to_le_bytes(), &json_header, @@ -49,7 +53,8 @@ impl FileFormatLoader for RnoteFile { .get(..9) .ok_or(anyhow::anyhow!("Failed to get magic number"))?; - if magic_number != MAGIC_NUMBER { + if magic_number != Self::MAGIC_NUMBER { + // Gzip magic number if magic_number[..2] == [0x1f, 0x8b] { return RnoteFile::try_from(LegacyRnoteFile::load_from_bytes(bytes)?); } else { @@ -63,6 +68,11 @@ impl FileFormatLoader for RnoteFile { .get(9..12) .ok_or(anyhow::anyhow!("Failed to get version"))?, ); + let version = semver::Version::new( + u64::from(version[0]), + u64::from(version[1]), + u64::from(version[2]), + ); let mut header_size: [u8; 4] = [0; 4]; header_size.copy_from_slice( @@ -71,7 +81,6 @@ impl FileFormatLoader for RnoteFile { .ok_or(anyhow::anyhow!("Failed to get header size"))?, ); let header_size = u32::from_le_bytes(header_size); - let header_slice = bytes .get(16..16 + usize::try_from(header_size)?) .ok_or(anyhow::anyhow!("File head missing"))?; @@ -81,12 +90,25 @@ impl FileFormatLoader for RnoteFile { .ok_or(anyhow::anyhow!("File body missing"))?; Ok(Self { - head: serde_json::from_slice(header_slice)?, + head: RnoteHeader::load_from_slice(header_slice, &version)?, body: body_slice.to_vec(), }) } } +impl RnoteHeader { + fn load_from_slice(slice: &[u8], version: &semver::Version) -> anyhow::Result { + if semver::VersionReq::parse(">=0.12.0") + .unwrap() + .matches(version) + { + Ok(serde_json::from_slice(slice)?) + } else { + Err(anyhow::anyhow!("Unrecognized header")) + } + } +} + impl TryFrom for EngineSnapshot { type Error = anyhow::Error; diff --git a/crates/rnote-engine/src/meson.build b/crates/rnote-engine/src/meson.build index eb75fb20d4..02658f34fc 100644 --- a/crates/rnote-engine/src/meson.build +++ b/crates/rnote-engine/src/meson.build @@ -11,11 +11,14 @@ rnote_engine_sources = files( 'engine/strokecontent.rs', 'engine/visual_debug.rs', 'fileformats/mod.rs', - 'fileformats/rnoteformat/maj0min5patch8.rs', - 'fileformats/rnoteformat/maj0min5patch9.rs', - 'fileformats/rnoteformat/maj0min6.rs', - 'fileformats/rnoteformat/maj0min9.rs', + 'fileformats/rnoteformat/legacy/mod.rs', + 'fileformats/rnoteformat/legacy/maj0min5patch8.rs', + 'fileformats/rnoteformat/legacy/maj0min5patch9.rs', + 'fileformats/rnoteformat/legacy/maj0min6.rs', + 'fileformats/rnoteformat/legacy/maj0min9.rs', 'fileformats/rnoteformat/mod.rs', + 'fileformats/rnoteformat/methods.rs', + 'fileformats/rnoteformat/maj0min12.rs', 'fileformats/xoppformat.rs', 'pens/brush.rs', 'pens/eraser.rs', From a5d63d820fb05f9789e1fcde909eca94e7642b67 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Fri, 16 Aug 2024 16:19:24 +0200 Subject: [PATCH 09/36] testing code, will be removed later, bitcode seems really good --- Cargo.lock | 137 +++++++++++++++++- Cargo.toml | 5 + crates/rnote-engine/Cargo.toml | 5 + .../src/fileformats/rnoteformat/methods.rs | 48 +++++- .../src/fileformats/rnoteformat/mod.rs | 11 +- 5 files changed, 197 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1c4329a54d..738fc77f68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,6 +294,15 @@ version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -346,6 +355,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" version = "0.69.4" @@ -372,6 +390,30 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" +[[package]] +name = "bitcode" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1bce7608560cd4bf0296a4262d0dbf13e6bcec5ff2105724c8ab88cc7fc784" +dependencies = [ + "arrayvec", + "bitcode_derive", + "bytemuck", + "glam", + "serde", +] + +[[package]] +name = "bitcode_derive" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a539389a13af092cd345a2b47ae7dec12deb306d660b2223d25cd3419b253ebe" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -539,6 +581,33 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -590,6 +659,12 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + [[package]] name = "color_quant" version = "1.1.0" @@ -692,6 +767,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" + [[package]] name = "crossbeam-channel" version = "0.5.13" @@ -904,6 +985,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1414,6 +1501,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "glam" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94" + [[package]] name = "glib" version = "0.19.9" @@ -1601,6 +1694,15 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hash32" version = "0.3.1" @@ -1620,13 +1722,27 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32 0.2.1", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + [[package]] name = "heapless" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ - "hash32", + "hash32 0.3.1", "stable_deref_trait", ] @@ -2980,6 +3096,18 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" +[[package]] +name = "postcard" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" +dependencies = [ + "cobs", + "embedded-io", + "heapless 0.7.17", + "serde", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -3361,8 +3489,11 @@ dependencies = [ "anyhow", "approx", "base64", + "bincode", + "bitcode", "cairo-rs", "chrono", + "ciborium", "clap", "flate2", "futures", @@ -3383,6 +3514,7 @@ dependencies = [ "piet", "piet-cairo", "poppler-rs", + "postcard", "rand", "rand_distr", "rand_pcg", @@ -3400,6 +3532,7 @@ dependencies = [ "slotmap", "svg", "thiserror", + "toml 0.8.16", "tracing", "unicode-segmentation", "usvg", @@ -3471,7 +3604,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "133315eb94c7b1e8d0cb097e5a710d850263372fd028fff18969de708afc7008" dependencies = [ - "heapless", + "heapless 0.8.0", "num-traits", "smallvec", ] diff --git a/Cargo.toml b/Cargo.toml index 2dd154d581..67504ee1c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,8 +26,11 @@ approx = "0.5.1" async-fs = "2.1" atty = "0.2.14" base64 = "0.22.1" +bincode = "1.3" +bitcode = { version = "0.6", features = ["serde"] } cairo-rs = { version = "0.19.4", features = ["v1_18", "png", "svg", "pdf"] } chrono = "0.4.38" +ciborium = { version = "0.2.2"} clap = { version = "4.5", features = ["derive"] } dialoguer = "0.11.0" flate2 = "1.0" @@ -58,6 +61,7 @@ parry2d-f64 = { version = "0.17.0", features = ["serde-serialize"] } path-absolutize = "3.1" piet = "0.6.2" piet-cairo = "0.6.2" +postcard = { version = "1.0", features = ["alloc"]} rand = "0.8.5" rand_distr = "0.4.3" rand_pcg = "0.3.1" @@ -77,6 +81,7 @@ slotmap = { version = "1.0", features = ["serde"] } smol = "2.0" svg = "0.17.0" thiserror = "1.0" +toml = "0.8" tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } unicode-segmentation = "1.11" diff --git a/crates/rnote-engine/Cargo.toml b/crates/rnote-engine/Cargo.toml index f52e88a73c..9e6662128a 100644 --- a/crates/rnote-engine/Cargo.toml +++ b/crates/rnote-engine/Cargo.toml @@ -14,8 +14,11 @@ rnote-compose = { workspace = true } anyhow = { workspace = true } approx = { workspace = true } base64 = { workspace = true } +bincode = { workspace = true } +bitcode = { workspace = true, features = ["serde"] } cairo-rs = { workspace = true } chrono = { workspace = true } +ciborium = { workspace = true } clap = { workspace = true, optional = true } flate2 = { workspace = true } futures = { workspace = true } @@ -35,6 +38,7 @@ parry2d-f64 = { workspace = true } piet = { workspace = true } piet-cairo = { workspace = true } poppler-rs = { workspace = true } +postcard = { workspace = true, features = ["alloc"] } rand = { workspace = true } rand_distr = { workspace = true } rand_pcg = { workspace = true } @@ -51,6 +55,7 @@ serde_json = { workspace = true } slotmap = { workspace = true } svg = { workspace = true } thiserror = { workspace = true } +toml = { workspace = true } tracing = { workspace = true } unicode-segmentation = { workspace = true } usvg = { workspace = true } diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs index aae1d8ccfa..be395914d7 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs @@ -1,5 +1,8 @@ use serde::{Deserialize, Serialize}; -use std::io::{Read, Write}; +use std::{ + io::{Read, Write}, + str::FromStr, +}; use crate::engine::EngineSnapshot; @@ -15,7 +18,11 @@ pub enum CompM { /// Serialization methods that can be applied to a snapshot of the engine pub enum SerM { Bincode, + Bitcode, + Cbor, Json, + Postcard, + Toml, } impl CompM { @@ -29,7 +36,7 @@ impl CompM { Ok(encoder.finish()?) } Self::Zstd => { - let mut encoder = zstd::Encoder::new(Vec::::new(), 9)?; + let mut encoder = zstd::Encoder::new(Vec::::new(), 14)?; if let Ok(num_workers) = std::thread::available_parallelism() { encoder.multithread(num_workers.get() as u32)?; } @@ -59,15 +66,44 @@ impl CompM { impl SerM { pub fn serialize(&self, engine_snapshot: &EngineSnapshot) -> anyhow::Result> { - match self { + let res = match self { + Self::Bincode => Ok::, anyhow::Error>(bincode::serialize(engine_snapshot)?), + Self::Bitcode => Ok(bitcode::serialize(engine_snapshot)?), + Self::Cbor => { + let mut writer = Vec::new(); + ciborium::into_writer(engine_snapshot, &mut writer)?; + Ok(writer) + } Self::Json => Ok(serde_json::to_vec(&ijson::to_value(engine_snapshot)?)?), - Self::Bincode => unreachable!(), - } + Self::Postcard => Ok(postcard::to_allocvec(engine_snapshot)?), + Self::Toml => Ok(toml::to_string(engine_snapshot)?.into_bytes()), + }?; + tracing::info!("{} MB", res.len() as f64 / 1e6); + Ok(res) } pub fn deserialize(&self, data: &[u8]) -> anyhow::Result { match self { + Self::Bincode => Ok(bincode::deserialize(data)?), + Self::Bitcode => Ok(bitcode::deserialize(data)?), + Self::Cbor => Ok(ciborium::from_reader(data)?), Self::Json => Ok(ijson::from_value(&serde_json::from_slice(data)?)?), - Self::Bincode => unreachable!(), + Self::Postcard => Ok(postcard::from_bytes(data)?), + Self::Toml => Ok(toml::from_str(std::str::from_utf8(data)?)?), + } + } +} + +impl FromStr for SerM { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s { + "Bincode" => Ok(Self::Bincode), + "Bitcode" => Ok(Self::Bitcode), + "Cbor" => Ok(Self::Cbor), + "Json" => Ok(Self::Json), + "Postcard" => Ok(Self::Postcard), + "Toml" => Ok(Self::Toml), + _ => Err("Unknown SerM"), } } } diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index a6db15ce2a..ec84c2da55 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -16,7 +16,11 @@ use crate::engine::EngineSnapshot; use legacy::LegacyRnoteFile; use maj0min12::RnoteFileMaj0Min12; use methods::{CompM, SerM}; -use std::io::Write; +use std::{ + env, + io::{Read, Write}, + str::FromStr, +}; pub type RnoteFile = maj0min12::RnoteFileMaj0Min12; pub type RnoteHeader = maj0min12::RnoteHeaderMaj0Min12; @@ -124,6 +128,11 @@ impl TryFrom<&EngineSnapshot> for RnoteFile { fn try_from(value: &EngineSnapshot) -> Result { let serialization = SerM::Json; + // FOR TESTING ONLY + let ser_string = env::var("SER").unwrap(); + let serialization = SerM::from_str(ser_string.trim()).unwrap(); + tracing::info!(ser_string); + // END OF TESTING let compression = CompM::Zstd; let uc_data = serialization.serialize(value)?; let uc_size = uc_data.len() as u64; From bdfc9100502a795d8b1d82836a09512e758cf3d5 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:20:23 +0200 Subject: [PATCH 10/36] kept postcard, json, bitcode, bincode --- Cargo.lock | 29 ------------------ Cargo.toml | 4 +-- crates/rnote-engine/Cargo.toml | 2 -- .../src/fileformats/rnoteformat/methods.rs | 30 +++++-------------- .../src/fileformats/rnoteformat/mod.rs | 7 +---- 5 files changed, 10 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 738fc77f68..f5471aa8c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,33 +581,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - [[package]] name = "clang-sys" version = "1.8.1" @@ -3493,7 +3466,6 @@ dependencies = [ "bitcode", "cairo-rs", "chrono", - "ciborium", "clap", "flate2", "futures", @@ -3532,7 +3504,6 @@ dependencies = [ "slotmap", "svg", "thiserror", - "toml 0.8.16", "tracing", "unicode-segmentation", "usvg", diff --git a/Cargo.toml b/Cargo.toml index 67504ee1c0..988f17ca66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,6 @@ bincode = "1.3" bitcode = { version = "0.6", features = ["serde"] } cairo-rs = { version = "0.19.4", features = ["v1_18", "png", "svg", "pdf"] } chrono = "0.4.38" -ciborium = { version = "0.2.2"} clap = { version = "4.5", features = ["derive"] } dialoguer = "0.11.0" flate2 = "1.0" @@ -61,7 +60,7 @@ parry2d-f64 = { version = "0.17.0", features = ["serde-serialize"] } path-absolutize = "3.1" piet = "0.6.2" piet-cairo = "0.6.2" -postcard = { version = "1.0", features = ["alloc"]} +postcard = { version = "1.0", features = ["alloc"] } rand = "0.8.5" rand_distr = "0.4.3" rand_pcg = "0.3.1" @@ -81,7 +80,6 @@ slotmap = { version = "1.0", features = ["serde"] } smol = "2.0" svg = "0.17.0" thiserror = "1.0" -toml = "0.8" tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } unicode-segmentation = "1.11" diff --git a/crates/rnote-engine/Cargo.toml b/crates/rnote-engine/Cargo.toml index 9e6662128a..22b819af2a 100644 --- a/crates/rnote-engine/Cargo.toml +++ b/crates/rnote-engine/Cargo.toml @@ -18,7 +18,6 @@ bincode = { workspace = true } bitcode = { workspace = true, features = ["serde"] } cairo-rs = { workspace = true } chrono = { workspace = true } -ciborium = { workspace = true } clap = { workspace = true, optional = true } flate2 = { workspace = true } futures = { workspace = true } @@ -55,7 +54,6 @@ serde_json = { workspace = true } slotmap = { workspace = true } svg = { workspace = true } thiserror = { workspace = true } -toml = { workspace = true } tracing = { workspace = true } unicode-segmentation = { workspace = true } usvg = { workspace = true } diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs index be395914d7..07ed23034e 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs @@ -19,10 +19,8 @@ pub enum CompM { pub enum SerM { Bincode, Bitcode, - Cbor, Json, Postcard, - Toml, } impl CompM { @@ -36,7 +34,7 @@ impl CompM { Ok(encoder.finish()?) } Self::Zstd => { - let mut encoder = zstd::Encoder::new(Vec::::new(), 14)?; + let mut encoder = zstd::Encoder::new(Vec::::new(), 12)?; if let Ok(num_workers) = std::thread::available_parallelism() { encoder.multithread(num_workers.get() as u32)?; } @@ -66,29 +64,19 @@ impl CompM { impl SerM { pub fn serialize(&self, engine_snapshot: &EngineSnapshot) -> anyhow::Result> { - let res = match self { + match self { Self::Bincode => Ok::, anyhow::Error>(bincode::serialize(engine_snapshot)?), Self::Bitcode => Ok(bitcode::serialize(engine_snapshot)?), - Self::Cbor => { - let mut writer = Vec::new(); - ciborium::into_writer(engine_snapshot, &mut writer)?; - Ok(writer) - } Self::Json => Ok(serde_json::to_vec(&ijson::to_value(engine_snapshot)?)?), Self::Postcard => Ok(postcard::to_allocvec(engine_snapshot)?), - Self::Toml => Ok(toml::to_string(engine_snapshot)?.into_bytes()), - }?; - tracing::info!("{} MB", res.len() as f64 / 1e6); - Ok(res) + } } pub fn deserialize(&self, data: &[u8]) -> anyhow::Result { match self { Self::Bincode => Ok(bincode::deserialize(data)?), Self::Bitcode => Ok(bitcode::deserialize(data)?), - Self::Cbor => Ok(ciborium::from_reader(data)?), Self::Json => Ok(ijson::from_value(&serde_json::from_slice(data)?)?), Self::Postcard => Ok(postcard::from_bytes(data)?), - Self::Toml => Ok(toml::from_str(std::str::from_utf8(data)?)?), } } } @@ -97,13 +85,11 @@ impl FromStr for SerM { type Err = &'static str; fn from_str(s: &str) -> Result { match s { - "Bincode" => Ok(Self::Bincode), - "Bitcode" => Ok(Self::Bitcode), - "Cbor" => Ok(Self::Cbor), - "Json" => Ok(Self::Json), - "Postcard" => Ok(Self::Postcard), - "Toml" => Ok(Self::Toml), - _ => Err("Unknown SerM"), + "Bincode" | "bincode" => Ok(Self::Bincode), + "Bitcode" | "bitcode" => Ok(Self::Bitcode), + "Json" | "JSON" | "json" => Ok(Self::Json), + "Postcard" | "postcard" => Ok(Self::Postcard), + _ => Err("Unknown serialization method"), } } } diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index ec84c2da55..9e1be6be37 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -127,12 +127,7 @@ impl TryFrom<&EngineSnapshot> for RnoteFile { type Error = anyhow::Error; fn try_from(value: &EngineSnapshot) -> Result { - let serialization = SerM::Json; - // FOR TESTING ONLY - let ser_string = env::var("SER").unwrap(); - let serialization = SerM::from_str(ser_string.trim()).unwrap(); - tracing::info!(ser_string); - // END OF TESTING + let serialization = SerM::Bitcode; let compression = CompM::Zstd; let uc_data = serialization.serialize(value)?; let uc_size = uc_data.len() as u64; From a01c7f18dc829317d5945e5cfab31b08d48345a2 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Sun, 18 Aug 2024 15:15:27 +0200 Subject: [PATCH 11/36] customizable compression levels --- .../src/fileformats/rnoteformat/methods.rs | 21 +++++++++++-------- .../src/fileformats/rnoteformat/mod.rs | 10 ++++----- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs index 07ed23034e..531d8d7760 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs @@ -10,8 +10,8 @@ use crate::engine::EngineSnapshot; /// Compression methods that can be applied to the serialized engine snapshot pub enum CompM { None, - Gzip, - Zstd, + Gzip { compression_level: u8 }, + Zstd { compression_level: u8 }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -27,14 +27,17 @@ impl CompM { pub fn compress(&self, data: Vec) -> anyhow::Result> { match self { Self::None => Ok(data), - Self::Gzip => { - let mut encoder = - flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::new(5)); + Self::Gzip { compression_level } => { + let mut encoder = flate2::write::GzEncoder::new( + Vec::new(), + flate2::Compression::new(u32::from(*compression_level)), + ); encoder.write_all(&data)?; Ok(encoder.finish()?) } - Self::Zstd => { - let mut encoder = zstd::Encoder::new(Vec::::new(), 12)?; + Self::Zstd { compression_level } => { + let mut encoder = + zstd::Encoder::new(Vec::::new(), i32::from(*compression_level))?; if let Ok(num_workers) = std::thread::available_parallelism() { encoder.multithread(num_workers.get() as u32)?; } @@ -46,13 +49,13 @@ impl CompM { pub fn decompress(&self, uc_size: usize, data: Vec) -> anyhow::Result> { match self { Self::None => Ok(data), - Self::Gzip => { + Self::Gzip { .. } => { let mut bytes: Vec = Vec::with_capacity(uc_size); let mut decoder = flate2::read::MultiGzDecoder::new(&data[..]); decoder.read_to_end(&mut bytes)?; Ok(bytes) } - Self::Zstd => { + Self::Zstd { .. } => { let mut bytes: Vec = Vec::with_capacity(uc_size); let mut decoder = zstd::Decoder::new(&data[..])?; decoder.read_to_end(&mut bytes)?; diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index 9e1be6be37..cfcda98933 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -16,11 +16,7 @@ use crate::engine::EngineSnapshot; use legacy::LegacyRnoteFile; use maj0min12::RnoteFileMaj0Min12; use methods::{CompM, SerM}; -use std::{ - env, - io::{Read, Write}, - str::FromStr, -}; +use std::io::Write; pub type RnoteFile = maj0min12::RnoteFileMaj0Min12; pub type RnoteHeader = maj0min12::RnoteHeaderMaj0Min12; @@ -128,7 +124,9 @@ impl TryFrom<&EngineSnapshot> for RnoteFile { fn try_from(value: &EngineSnapshot) -> Result { let serialization = SerM::Bitcode; - let compression = CompM::Zstd; + let compression = CompM::Zstd { + compression_level: 9, + }; let uc_data = serialization.serialize(value)?; let uc_size = uc_data.len() as u64; let data = compression.compress(uc_data)?; From 07cad72a1ce9a3eb56182fc86b1976387d6be7db Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:27:53 +0200 Subject: [PATCH 12/36] [in progress] CompressionLevel, SavePrefs --- crates/rnote-engine/src/engine/export.rs | 1 + crates/rnote-engine/src/engine/import.rs | 2 + crates/rnote-engine/src/engine/mod.rs | 9 ++ crates/rnote-engine/src/engine/save.rs | 112 ++++++++++++++++++ crates/rnote-engine/src/engine/snapshot.rs | 7 +- .../src/fileformats/rnoteformat/maj0min12.rs | 37 +++--- .../src/fileformats/rnoteformat/methods.rs | 20 +++- .../src/fileformats/rnoteformat/mod.rs | 43 ++++++- crates/rnote-ui/data/ui/settingspanel.ui | 21 +++- crates/rnote-ui/src/settingspanel/mod.rs | 19 ++- 10 files changed, 242 insertions(+), 29 deletions(-) create mode 100644 crates/rnote-engine/src/engine/save.rs diff --git a/crates/rnote-engine/src/engine/export.rs b/crates/rnote-engine/src/engine/export.rs index 1d4178acd9..e9f424d7e6 100644 --- a/crates/rnote-engine/src/engine/export.rs +++ b/crates/rnote-engine/src/engine/export.rs @@ -351,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_config(), pen_sounds: self.pen_sounds(), optimize_epd: self.optimize_epd(), } diff --git a/crates/rnote-engine/src/engine/import.rs b/crates/rnote-engine/src/engine/import.rs index 7ba5359f31..e5945c04a1 100644 --- a/crates/rnote-engine/src/engine/import.rs +++ b/crates/rnote-engine/src/engine/import.rs @@ -161,6 +161,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; // Set the pen sounds to update the audioplayer self.set_pen_sounds(engine_config.pen_sounds, data_dir); @@ -195,6 +196,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; // Set the pen sounds to update the audioplayer self.set_pen_sounds(engine_config.pen_sounds, data_dir); diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs index 2e7ad19093..9b1c2c8cb2 100644 --- a/crates/rnote-engine/src/engine/mod.rs +++ b/crates/rnote-engine/src/engine/mod.rs @@ -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; @@ -11,6 +12,7 @@ pub use export::ExportPrefs; use futures::channel::mpsc::UnboundedReceiver; use futures::StreamExt; pub use import::ImportPrefs; +pub use save::CompressionLevel; pub use snapshot::EngineSnapshot; pub use strokecontent::StrokeContent; @@ -30,6 +32,7 @@ use rnote_compose::eventresult::EventPropagation; use rnote_compose::ext::AabbExt; use rnote_compose::penevent::{PenEvent, ShortcutKey}; use rnote_compose::{Color, SplitOrder}; +use save::SavePrefs; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::sync::Arc; @@ -119,6 +122,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")] @@ -167,6 +172,8 @@ pub struct Engine { pub import_prefs: ImportPrefs, #[serde(rename = "export_prefs")] pub export_prefs: ExportPrefs, + #[serde(skip)] + pub save_prefs: SavePrefs, #[serde(rename = "pen_sounds")] pen_sounds: bool, #[serde(rename = "optimize_epd")] @@ -207,6 +214,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, @@ -328,6 +336,7 @@ 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: Some(self.save_prefs.clone_config()), } } diff --git a/crates/rnote-engine/src/engine/save.rs b/crates/rnote-engine/src/engine/save.rs new file mode 100644 index 0000000000..b0520a962d --- /dev/null +++ b/crates/rnote-engine/src/engine/save.rs @@ -0,0 +1,112 @@ +use serde::{Deserialize, Serialize}; + +use crate::fileformats::rnoteformat::{ + methods::{CompM, SerM}, + RnoteHeader, +}; + +// Rnote file save preferences +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename = "save_prefs")] +pub struct SavePrefs { + #[serde(rename = "serialization")] + pub serialization: SerM, + #[serde(rename = "compression")] + pub compression: CompM, +} + +impl SavePrefs { + pub fn clone_config(&self) -> Self { + self.clone() + } +} + +impl Default for SavePrefs { + fn default() -> Self { + Self { + serialization: SerM::default(), + compression: CompM::default(), + } + } +} + +impl From for SavePrefs { + fn from(value: RnoteHeader) -> Self { + Self { + serialization: value.serialization, + compression: value.compression, + } + } +} + +#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive)] +pub enum CompressionLevel { + VeryHigh, + High, + Medium, + Low, + VeryLow, + None, +} + +impl TryFrom for CompressionLevel { + type Error = anyhow::Error; + + fn try_from(value: u32) -> Result { + num_traits::FromPrimitive::from_u32(value).ok_or_else(|| { + anyhow::anyhow!( + "CompressionLevel try_from::() for value {} failed", + value + ) + }) + } +} + +impl CompM { + pub fn get_compression_level(&self) -> CompressionLevel { + match self { + Self::None => CompressionLevel::None, + Self::Gzip(val) => match *val { + 1..=2 => CompressionLevel::VeryLow, + 3..=4 => CompressionLevel::Low, + 5 => CompressionLevel::Medium, + 6..=7 => CompressionLevel::High, + 8..=9 => CompressionLevel::VeryHigh, + _ => unreachable!(), + }, + Self::Zstd(val) => match *val { + 1..=4 => CompressionLevel::VeryLow, + 5..=8 => CompressionLevel::Low, + 9..=12 => CompressionLevel::Medium, + 13..=16 => CompressionLevel::High, + 17..=21 => CompressionLevel::VeryHigh, + _ => unreachable!(), + }, + } + } + pub fn set_compression_level(&mut self, level: CompressionLevel) { + match self { + Self::None => (), + Self::Gzip(ref mut val) => { + *val = match level { + CompressionLevel::VeryHigh => 8, + CompressionLevel::High => 6, + CompressionLevel::Medium => 5, + CompressionLevel::Low => 3, + CompressionLevel::VeryLow => 1, + CompressionLevel::None => unreachable!(), + } + } + Self::Zstd(ref mut val) => { + *val = match level { + CompressionLevel::VeryHigh => 17, + CompressionLevel::High => 13, + CompressionLevel::Medium => 9, + CompressionLevel::Low => 5, + CompressionLevel::VeryLow => 1, + CompressionLevel::None => unreachable!(), + } + } + } + } +} diff --git a/crates/rnote-engine/src/engine/snapshot.rs b/crates/rnote-engine/src/engine/snapshot.rs index 7ac4fe61df..76f2cd0509 100644 --- a/crates/rnote-engine/src/engine/snapshot.rs +++ b/crates/rnote-engine/src/engine/snapshot.rs @@ -12,6 +12,8 @@ use slotmap::{HopSlotMap, SecondaryMap}; use std::sync::Arc; use tracing::error; +use super::save::SavePrefs; + // An engine snapshot, used when loading/saving the current document from/into a file. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default, rename = "engine_snapshot")] @@ -26,6 +28,8 @@ pub struct EngineSnapshot { pub chrono_components: Arc>>, #[serde(rename = "chrono_counter")] pub chrono_counter: u32, + #[serde(skip, default)] + pub save_prefs: Option, } impl Default for EngineSnapshot { @@ -36,6 +40,7 @@ impl Default for EngineSnapshot { stroke_components: Arc::new(HopSlotMap::with_key()), chrono_components: Arc::new(SecondaryMap::new()), chrono_counter: 0, + save_prefs: None, } } } @@ -51,7 +56,7 @@ impl EngineSnapshot { let result = || -> anyhow::Result { let rnote_file = rnoteformat::RnoteFile::load_from_bytes(&bytes) .context("loading RnoteFile from bytes failed.")?; - Ok(Self::try_from(rnote_file)?) + Self::try_from(rnote_file) }; if let Err(_data) = snapshot_sender.send(result()) { diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs index 0c8a6181de..e0a72acf82 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs @@ -4,20 +4,19 @@ use super::{ }; use serde::{Deserialize, Serialize}; -/// ## Description of Rnote's 0.12.0 file format -/// ### rnote magic number -/// the magic number present at the start of each rnote file, used to identify them -/// "RNOTEϕλ" -> "RNOTEFILE" (a useful pun, phi-lambda ~ file) -/// [u8; 9] -/// ### version -/// [u8; 3] [major, minor, patch] -/// ### header size -/// size of the json-encoded header, represented by 4 bytes, little endian -/// [u8; 4] -/// ### header -/// describes how to decompress and deserialize the data -/// ### data -/// serialized and compressed engine snapshot +/// # Rnote File Format Specifications +/// ## Prelude (not included in this struct) +/// * magic number: [u8; 9] = [0x52, 0x4e, 0x4f, 0x54, 0x45, 0xce, 0xa6, 0xce, 0x9b], "RNOTEϕλ" +/// * version: [u8; 3] = [major, minor, patch] +/// * header size: [u8; 4], little endian repr. +/// ## Header +/// the header is a forward-compatible json-encoded struct +/// containing additional information on the file +/// * serialization: method used to serialize/deserialize the engine snapshot +/// * compression: method used to compress/decompress the serialized engine snapshot +/// * uncompressed size: size of the uncompressed and serialized engine snapshot +/// ## Body +/// the body contains the serialized and (potentially) compressed engine snapshot #[derive(Debug, Clone)] pub struct RnoteFileMaj0Min12 { pub head: RnoteHeaderMaj0Min12, @@ -33,6 +32,7 @@ impl TryFrom for RnoteFileMaj0Min12 { serialization: SerM::Json, compression: CompM::None, uc_size: 0, + method_lock: false, }, body: serde_json::to_vec(&value.engine_snapshot)?, }) @@ -40,8 +40,17 @@ impl TryFrom for RnoteFileMaj0Min12 { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename = "header")] pub struct RnoteHeaderMaj0Min12 { + /// method used to serialize/deserialize the engine snapshot + #[serde(rename = "serialization")] pub serialization: SerM, + /// method used to compress/decompress the serialized engine snapshot + #[serde(rename = "compression")] pub compression: CompM, + /// size of the uncompressed and serialized engine snapshot + #[serde(rename = "uncompressed_size")] pub uc_size: u64, + #[serde(rename = "method_lock")] + pub method_lock: bool, } diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs index 531d8d7760..bc38db16bd 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs @@ -10,8 +10,8 @@ use crate::engine::EngineSnapshot; /// Compression methods that can be applied to the serialized engine snapshot pub enum CompM { None, - Gzip { compression_level: u8 }, - Zstd { compression_level: u8 }, + Gzip(u8), + Zstd(u8), } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -27,7 +27,7 @@ impl CompM { pub fn compress(&self, data: Vec) -> anyhow::Result> { match self { Self::None => Ok(data), - Self::Gzip { compression_level } => { + Self::Gzip(compression_level) => { let mut encoder = flate2::write::GzEncoder::new( Vec::new(), flate2::Compression::new(u32::from(*compression_level)), @@ -35,7 +35,7 @@ impl CompM { encoder.write_all(&data)?; Ok(encoder.finish()?) } - Self::Zstd { compression_level } => { + Self::Zstd(compression_level) => { let mut encoder = zstd::Encoder::new(Vec::::new(), i32::from(*compression_level))?; if let Ok(num_workers) = std::thread::available_parallelism() { @@ -65,6 +65,12 @@ impl CompM { } } +impl Default for CompM { + fn default() -> Self { + Self::Zstd(9) + } +} + impl SerM { pub fn serialize(&self, engine_snapshot: &EngineSnapshot) -> anyhow::Result> { match self { @@ -84,6 +90,12 @@ impl SerM { } } +impl Default for SerM { + fn default() -> Self { + Self::Bitcode + } +} + impl FromStr for SerM { type Err = &'static str; fn from_str(s: &str) -> Result { diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index cfcda98933..af26452252 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -12,7 +12,7 @@ pub(crate) mod methods; // Imports use super::{FileFormatLoader, FileFormatSaver}; -use crate::engine::EngineSnapshot; +use crate::engine::{save::SavePrefs, EngineSnapshot}; use legacy::LegacyRnoteFile; use maj0min12::RnoteFileMaj0Min12; use methods::{CompM, SerM}; @@ -115,7 +115,26 @@ impl TryFrom for EngineSnapshot { fn try_from(value: RnoteFileMaj0Min12) -> Result { let uc_size = usize::try_from(value.head.uc_size).unwrap_or(usize::MAX); let uc_body = value.head.compression.decompress(uc_size, value.body)?; - value.head.serialization.deserialize(&uc_body) + + tracing::info!( + "loaded rnote file\ncomp: {:?}\nseri: {:?}\nlock: {}\n", + value.head.compression, + value.head.serialization, + value.head.method_lock, + ); + + let mut engine_snapshot = value.head.serialization.deserialize(&uc_body)?; + // save preferences are only kept if method_lock is true or both the ser. method and comp. method are "defaults" + if value.head.method_lock + | (matches!(value.head.compression, CompM::Zstd(_)) + && matches!(value.head.serialization, SerM::Bitcode)) + { + engine_snapshot + .save_prefs + .replace(SavePrefs::from(value.head)); + } + + Ok(engine_snapshot) } } @@ -123,19 +142,31 @@ impl TryFrom<&EngineSnapshot> for RnoteFile { type Error = anyhow::Error; fn try_from(value: &EngineSnapshot) -> Result { - let serialization = SerM::Bitcode; - let compression = CompM::Zstd { - compression_level: 9, - }; + let save_prefs = value + .save_prefs + .to_owned() + .ok_or(anyhow::anyhow!("No save preferences specified"))?; + + let compression = save_prefs.compression; + let serialization = save_prefs.serialization; + let uc_data = serialization.serialize(value)?; let uc_size = uc_data.len() as u64; let data = compression.compress(uc_data)?; + // if the compression method or serialization methid differ from the default, assume method lock is true + let method_lock = + !matches!(compression, CompM::Zstd(_)) | !matches!(serialization, SerM::Bitcode); + + tracing::info!( + "saving rnote file\ncomp: {compression:?}\nseri: {serialization:?}\nlock: {method_lock}\n" + ); Ok(Self { head: RnoteHeader { compression, serialization, uc_size, + method_lock, }, body: data, }) diff --git a/crates/rnote-ui/data/ui/settingspanel.ui b/crates/rnote-ui/data/ui/settingspanel.ui index e1c205a8e7..cb6f73ec14 100644 --- a/crates/rnote-ui/data/ui/settingspanel.ui +++ b/crates/rnote-ui/data/ui/settingspanel.ui @@ -383,17 +383,34 @@ gets disabled. - + Invert Color Brightness Invert the brightness of all background pattern colors - + center Invert + + + Compression Level + + + + Very High + High + Medium + Low + Very Low + None + + + + + diff --git a/crates/rnote-ui/src/settingspanel/mod.rs b/crates/rnote-ui/src/settingspanel/mod.rs index f2a3a0923c..74ab7b6409 100644 --- a/crates/rnote-ui/src/settingspanel/mod.rs +++ b/crates/rnote-ui/src/settingspanel/mod.rs @@ -19,6 +19,7 @@ use rnote_compose::penevent::ShortcutKey; use rnote_engine::document::background::PatternStyle; use rnote_engine::document::format::{self, Format, PredefinedFormat}; use rnote_engine::document::Layout; +use rnote_engine::engine::CompressionLevel; use rnote_engine::ext::GdkRGBAExt; use std::cell::RefCell; @@ -94,7 +95,9 @@ mod imp { #[template_child] pub(crate) doc_background_pattern_height_unitentry: TemplateChild, #[template_child] - pub(crate) background_pattern_invert_color_button: TemplateChild diff --git a/crates/rnote-ui/src/settingspanel/mod.rs b/crates/rnote-ui/src/settingspanel/mod.rs index 03a6102049..252947d1c6 100644 --- a/crates/rnote-ui/src/settingspanel/mod.rs +++ b/crates/rnote-ui/src/settingspanel/mod.rs @@ -399,11 +399,7 @@ impl RnSettingsPanel { let background = canvas.engine_ref().document.background; let format = canvas.engine_ref().document.format; let document_layout = canvas.engine_ref().document.layout; - let compression_level = canvas - .engine_ref() - .save_prefs - .compression - .get_compression_level(); + let compression = canvas.engine_ref().save_prefs.compression; imp.doc_background_color_button .set_rgba(&gdk::RGBA::from_compose_color(background.color)); @@ -419,8 +415,16 @@ impl RnSettingsPanel { imp.doc_background_pattern_height_unitentry .set_value_in_px(background.pattern_size[1]); self.set_document_layout(&document_layout); + // set the compression level row to invisible if CompM is None imp.doc_compression_level_row - .set_selected(compression_level.to_u32().unwrap()) + .set_visible(!matches!(compression, CompM::None)); + + match compression.get_compression_level() { + CompressionLevel::None => (), + other => imp + .doc_compression_level_row + .set_selected(other.to_u32().unwrap()), + } } fn refresh_shortcuts_ui(&self, active_tab: &RnCanvasWrapper) { @@ -770,15 +774,7 @@ impl RnSettingsPanel { imp.doc_compression_level_row.get().connect_selected_item_notify(clone!(@weak self as settings_panel, @weak appwindow => move |_| { let canvas = appwindow.active_tab_wrapper().canvas(); - let mut compression_level = CompressionLevel::try_from(settings_panel.imp().doc_compression_level_row.get().selected()).unwrap(); - - // None cannot be selected unless CompM is None iteself, otherwise selects Very Low instead - if matches!(compression_level, CompressionLevel::None) - && !matches!(canvas.engine_mut().save_prefs.compression, CompM::None) - { - compression_level = CompressionLevel::VeryLow; - settings_panel.imp().doc_compression_level_row.set_selected(compression_level.to_u32().unwrap()); - } + let compression_level = CompressionLevel::try_from(settings_panel.imp().doc_compression_level_row.get().selected()).unwrap(); canvas.engine_mut().save_prefs.compression.set_compression_level(compression_level); })); } From 531db89507500c476b595d4c176d0abf351595c0 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:35:33 +0200 Subject: [PATCH 18/36] minor changes --- crates/rnote-cli/src/cli.rs | 8 ++++---- crates/rnote-cli/src/mutate.rs | 17 +++++++++-------- crates/rnote-engine/src/engine/export.rs | 4 ++-- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/rnote-cli/src/cli.rs b/crates/rnote-cli/src/cli.rs index 511b61e24a..4516252f4b 100644 --- a/crates/rnote-cli/src/cli.rs +++ b/crates/rnote-cli/src/cli.rs @@ -71,10 +71,7 @@ pub(crate) enum Command { open: bool, }, /// Mutates one or more of the following for the specified Rnote file(s):{n} - /// * compression method - /// * compression level - /// * serialization method - /// * method lock + /// compression method, compression level, serialization method, method lock Mutate { /// The rnote save file(s) to mutate rnote_files: Vec, @@ -82,8 +79,11 @@ pub(crate) enum Command { #[arg(long = "not-in-place", alias = "nip", action = clap::ArgAction::SetTrue)] not_in_place: bool, /// Locks the compression and serialization methods used by the rnote save file(s) + /// Useful if either the desired serialization or compression methods differ from the defaults + /// Note that the compression level is not taken into account when comparing with the default methods #[arg(short = 'l', long, action = clap::ArgAction::SetTrue, conflicts_with = "unlock")] lock: bool, + /// Unlocks the compression and serialization methods used by the rnote save file(s) #[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(rnote_engine::fileformats::rnoteformat::SerM::VALID_STR_ARRAY))] diff --git a/crates/rnote-cli/src/mutate.rs b/crates/rnote-cli/src/mutate.rs index 8674c02b2f..444aa4fe93 100644 --- a/crates/rnote-cli/src/mutate.rs +++ b/crates/rnote-cli/src/mutate.rs @@ -16,11 +16,13 @@ pub(crate) async fn run_mutate( compression_method: Option, compression_level: Option, ) -> anyhow::Result<()> { - for mut file in rnote_files.into_iter() { + let total_len = rnote_files.len(); + for (idx, mut filepath) in rnote_files.into_iter().enumerate() { + println!("Working on file {} out of {}", idx + 1, total_len); let mut bytes: Vec = Vec::new(); OpenOptions::new() .read(true) - .open(&file) + .open(&filepath) .await? .read_to_end(&mut bytes) .await?; @@ -30,13 +32,13 @@ pub(crate) async fn run_mutate( let serialization = if let Some(ref str) = serialization_method { rnote_engine::fileformats::rnoteformat::SerM::from_str(str).unwrap() } else { - rnote_file.head.serialization.clone() + rnote_file.head.serialization }; let mut compression = if let Some(ref str) = compression_method { rnote_engine::fileformats::rnoteformat::CompM::from_str(str).unwrap() } else { - rnote_file.head.compression.clone() + rnote_file.head.compression }; if let Some(lvl) = compression_level { @@ -44,7 +46,6 @@ pub(crate) async fn run_mutate( } let method_lock = (rnote_file.head.method_lock | lock) && !unlock; - let uc_data = serialization.serialize(&EngineSnapshot::try_from(rnote_file)?)?; let uc_size = uc_data.len() as u64; @@ -59,12 +60,12 @@ pub(crate) async fn run_mutate( }; if not_in_place { - let file_stem = file + let file_stem = filepath .file_stem() .ok_or(anyhow::anyhow!("File does not contain a valid file stem"))? .to_str() .ok_or(anyhow::anyhow!("File does not contain a valid file stem"))?; - file.set_file_name(format!("{}_mutated.rnote", file_stem)); + filepath.set_file_name(format!("{}_mutated.rnote", file_stem)); } let data = rnote_file.save_as_bytes("")?; @@ -77,7 +78,7 @@ pub(crate) async fn run_mutate( .create(true) .truncate(true) .write(true) - .open(&file) + .open(&filepath) .await?; file.write_all(&data).await?; diff --git a/crates/rnote-engine/src/engine/export.rs b/crates/rnote-engine/src/engine/export.rs index 8f8a8ec0d6..2a4b55e4f1 100644 --- a/crates/rnote-engine/src/engine/export.rs +++ b/crates/rnote-engine/src/engine/export.rs @@ -1,6 +1,6 @@ // Imports use super::{Engine, EngineConfig, StrokeContent}; -use crate::fileformats::rnoteformat::maj0min12::RnoteFileMaj0Min12; +use crate::fileformats::rnoteformat::RnoteFile; use crate::fileformats::{xoppformat, FileFormatSaver}; use crate::CloneConfig; use anyhow::Context; @@ -331,7 +331,7 @@ impl Engine { let engine_snapshot = self.take_snapshot(); rayon::spawn(move || { let result = || -> anyhow::Result> { - let rnote_file = RnoteFileMaj0Min12::try_from(&engine_snapshot)?; + let rnote_file = RnoteFile::try_from(&engine_snapshot)?; rnote_file.save_as_bytes(&file_name) }; if oneshot_sender.send(result()).is_err() { From b13c27ecda86fdfefe10171c925537c55503941f Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:43:59 +0200 Subject: [PATCH 19/36] serde renames for consistency --- crates/rnote-engine/src/engine/save.rs | 2 +- .../src/fileformats/rnoteformat/methods.rs | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/rnote-engine/src/engine/save.rs b/crates/rnote-engine/src/engine/save.rs index 87d1544d1b..3c0aa97bc7 100644 --- a/crates/rnote-engine/src/engine/save.rs +++ b/crates/rnote-engine/src/engine/save.rs @@ -58,7 +58,7 @@ impl From for SavePrefs { } } -#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive)] +#[derive(Debug, Clone, Copy, num_derive::FromPrimitive, num_derive::ToPrimitive)] pub enum CompressionLevel { VeryHigh, High, diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs index 383b21cd06..1fcb876298 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs @@ -6,21 +6,30 @@ use std::{ use crate::engine::EngineSnapshot; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] /// Compression methods that can be applied to the serialized engine snapshot +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename = "compression_method")] pub enum CompM { + #[serde(rename = "none")] None, + #[serde(rename = "gzip")] Gzip(u8), /// Zstd supports negative compression levels but I don't see the point in allowing these for rnote files + #[serde(rename = "zstd")] Zstd(u8), } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] /// Serialization methods that can be applied to a snapshot of the engine +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename = "serialization_method")] pub enum SerM { + #[serde(rename = "bincode")] Bincode, + #[serde(rename = "bitcode")] Bitcode, + #[serde(rename = "json")] Json, + #[serde(rename = "postcard")] Postcard, } From 0b4035a83e5126f18b2eda1d61ab33340a0f1903 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:54:16 +0200 Subject: [PATCH 20/36] Ijson, + big issue fix on mutate --- crates/rnote-cli/src/mutate.rs | 3 ++- crates/rnote-engine/src/fileformats/rnoteformat/mod.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/rnote-cli/src/mutate.rs b/crates/rnote-cli/src/mutate.rs index 444aa4fe93..84bac89c4d 100644 --- a/crates/rnote-cli/src/mutate.rs +++ b/crates/rnote-cli/src/mutate.rs @@ -48,6 +48,7 @@ pub(crate) async fn run_mutate( let method_lock = (rnote_file.head.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 { head: RnoteHeader { @@ -56,7 +57,7 @@ pub(crate) async fn run_mutate( uc_size, method_lock, }, - body: uc_data, + body: data, }; if not_in_place { diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index de3665bd13..219b21d010 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -31,7 +31,7 @@ impl RnoteFileMaj0Min12 { impl FileFormatSaver for RnoteFile { fn save_as_bytes(&self, _file_name: &str) -> anyhow::Result> { - let json_header = serde_json::to_vec(&self.head)?; + let json_header = serde_json::to_vec(&ijson::to_value(&self.head)?)?; let header = [ &Self::MAGIC_NUMBER[..], &Self::VERSION[..], @@ -104,7 +104,7 @@ impl RnoteHeader { .unwrap() .matches(version) { - Ok(serde_json::from_slice(slice)?) + Ok(ijson::from_value(&serde_json::from_slice(slice)?)?) } else { Err(anyhow::anyhow!("Unrecognized header")) } From e83d177fc611058a236bcc24883d4ebbcd2c8354 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Sun, 25 Aug 2024 19:20:50 +0200 Subject: [PATCH 21/36] (improved) two step file save process, lazy evaluation (in-progress) --- Cargo.lock | 2 + Cargo.toml | 1 + crates/rnote-cli/src/mutate.rs | 53 ++++++++------- crates/rnote-engine/Cargo.toml | 2 + crates/rnote-engine/src/utils.rs | 92 ++++++++++++++++++++++++++ crates/rnote-ui/src/canvas/imexport.rs | 43 ++---------- 6 files changed, 130 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5471aa8c4..1ea83828ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3461,12 +3461,14 @@ version = "0.11.0" dependencies = [ "anyhow", "approx", + "async-fs", "base64", "bincode", "bitcode", "cairo-rs", "chrono", "clap", + "crc32fast", "flate2", "futures", "geo", diff --git a/Cargo.toml b/Cargo.toml index 988f17ca66..893152f3d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ bitcode = { version = "0.6", features = ["serde"] } cairo-rs = { version = "0.19.4", 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" diff --git a/crates/rnote-cli/src/mutate.rs b/crates/rnote-cli/src/mutate.rs index 84bac89c4d..0e8003b65b 100644 --- a/crates/rnote-cli/src/mutate.rs +++ b/crates/rnote-cli/src/mutate.rs @@ -2,7 +2,6 @@ use rnote_engine::engine::EngineSnapshot; use rnote_engine::fileformats::rnoteformat::RnoteHeader; use rnote_engine::fileformats::FileFormatSaver; use rnote_engine::fileformats::{rnoteformat::RnoteFile, FileFormatLoader}; -use smol::io::AsyncWriteExt; use smol::{fs::OpenOptions, io::AsyncReadExt}; use std::path::PathBuf; use std::str::FromStr; @@ -17,16 +16,30 @@ pub(crate) async fn run_mutate( compression_level: Option, ) -> 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 mut bytes: Vec = Vec::new(); - OpenOptions::new() - .read(true) - .open(&filepath) - .await? - .read_to_end(&mut bytes) - .await?; + let file_read_operation = async { + let mut read_file = OpenOptions::new().read(true).open(&filepath).await?; + + let mut bytes: Vec = { + 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::, 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 { @@ -63,29 +76,19 @@ pub(crate) async fn run_mutate( if not_in_place { let file_stem = filepath .file_stem() - .ok_or(anyhow::anyhow!("File does not contain a valid file stem"))? + .ok_or_else(|| anyhow::anyhow!("File does not contain a valid file stem"))? .to_str() - .ok_or(anyhow::anyhow!("File does not contain a valid file stem"))?; + .ok_or_else(|| anyhow::anyhow!("File does not contain a valid file stem"))?; filepath.set_file_name(format!("{}_mutated.rnote", file_stem)); } let data = rnote_file.save_as_bytes("")?; - println!( - "attempting to write {:.3} [MB] of data", - data.len() as f64 / 1e6 - ); - - let mut file = OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(&filepath) - .await?; - - file.write_all(&data).await?; - file.sync_all().await?; + let new_size_mb = data.len() as f64 / 1e6; + rnote_engine::utils::atomic_save_to_file(&filepath, &data).await?; - // TODO - use a two-step file saving process (once and if the file format is approved) + println!("{:.2} MB → {:.2} MB", old_size_mb, new_size_mb,); + total_delta += new_size_mb - old_size_mb; } + println!("\n ⇒ ∆ = {:.2}", total_delta); Ok(()) } diff --git a/crates/rnote-engine/Cargo.toml b/crates/rnote-engine/Cargo.toml index 22b819af2a..4f218eba38 100644 --- a/crates/rnote-engine/Cargo.toml +++ b/crates/rnote-engine/Cargo.toml @@ -13,12 +13,14 @@ rnote-compose = { workspace = true } anyhow = { workspace = true } approx = { workspace = true } +async-fs = { workspace = true } base64 = { workspace = true } bincode = { 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 } diff --git a/crates/rnote-engine/src/utils.rs b/crates/rnote-engine/src/utils.rs index 1f3fa341d2..3b7b0d576c 100644 --- a/crates/rnote-engine/src/utils.rs +++ b/crates/rnote-engine/src/utils.rs @@ -1,5 +1,7 @@ // Imports use crate::fileformats::xoppformat; +use anyhow::Context; +use futures::{AsyncReadExt, AsyncWriteExt}; use geo::line_string; use p2d::bounding_volume::Aabb; use rnote_compose::Color; @@ -96,3 +98,93 @@ pub mod glib_bytes_base64 { rnote_compose::serialize::sliceu8_base64::deserialize(d).map(glib::Bytes::from_owned) } } + +pub async fn atomic_save_to_file + std::fmt::Debug>( + filepath: Q, + bytes: &[u8], +) -> anyhow::Result<()> { + let filepath = filepath.as_ref().to_owned(); + + // checks that the extension is not already 'tmp' + if filepath + .extension() + .ok_or_else(|| anyhow::anyhow!("Missing file extension"))? + .to_str() + .ok_or_else(|| anyhow::anyhow!("Invalid extension encoding"))? + == "tmp" + { + Err(anyhow::anyhow!("File extension cannot be 'tmp'"))?; + } + + let mut tmp_filepath = filepath.clone(); + tmp_filepath.set_extension("tmp"); + + let file_write_operation = async { + let mut write_file = async_fs::OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(&tmp_filepath) + .await + .with_context(|| { + format!( + "Failed to create/open/truncate tmp file for path '{}'", + tmp_filepath.display() + ) + })?; + write_file.write_all(bytes).await.with_context(|| { + format!( + "Failed to write bytes to tmp file with path '{}'", + tmp_filepath.display() + ) + })?; + write_file.sync_all().await.with_context(|| { + format!( + "Failed to sync tmp file after writing with path '{}'", + &tmp_filepath.display() + ) + })?; + + Ok::<(), anyhow::Error>(()) + }; + file_write_operation.await?; + + let file_check_operation = async { + let size = bytes.len(); + let internal_checksum = crc32fast::hash(bytes); + + let mut read_file = async_fs::OpenOptions::new() + .read(true) + .open(&tmp_filepath) + .await + .with_context(|| { + format!( + "Failed to open/read tmp file for path '{}'", + &tmp_filepath.display() + ) + })?; + let mut data: Vec = Vec::with_capacity(size); + read_file.read_to_end(&mut data).await?; + let external_checksum = crc32fast::hash(&data); + + if internal_checksum != external_checksum { + return Err(anyhow::anyhow!( + "Mismatch between the internal and external checksums, temporary file most likely corrupted" + )); + } + + Ok::<(), anyhow::Error>(()) + }; + file_check_operation.await?; + + let file_swap_operation = async { + async_fs::rename(&tmp_filepath, &filepath) + .await + .context("Failed to rename the temporary file into the main one")?; + + Ok::<(), anyhow::Error>(()) + }; + file_swap_operation.await?; + + Ok(()) +} diff --git a/crates/rnote-ui/src/canvas/imexport.rs b/crates/rnote-ui/src/canvas/imexport.rs index 8a8b0da871..fb5c1e98f2 100644 --- a/crates/rnote-ui/src/canvas/imexport.rs +++ b/crates/rnote-ui/src/canvas/imexport.rs @@ -1,8 +1,6 @@ // Imports use super::RnCanvas; -use anyhow::Context; use futures::channel::oneshot; -use futures::AsyncWriteExt; use gtk4::{gio, prelude::*}; use rnote_compose::ext::Vector2Ext; use rnote_engine::engine::export::{DocExportPrefs, DocPagesExportPrefs, SelectionExportPrefs}; @@ -211,7 +209,7 @@ impl RnCanvas { self.set_save_in_progress(true); debug!("Saving file is now in progress"); - let file_path = file + let filepath = file .path() .ok_or_else(|| anyhow::anyhow!("Could not get a path for file: `{file:?}`."))?; let basename = file @@ -220,43 +218,11 @@ impl RnCanvas { let rnote_bytes_receiver = self .engine_ref() .save_as_rnote_bytes(basename.to_string_lossy().to_string()); - let mut skip_set_output_file = false; - if let Some(output_file_path) = self.output_file().and_then(|f| f.path()) { - if crate::utils::paths_abs_eq(output_file_path, &file_path).unwrap_or(false) { - skip_set_output_file = true; - } - } self.dismiss_output_file_modified_toast(); - let file_write_operation = async move { - let bytes = rnote_bytes_receiver.await??; - self.set_output_file_expect_write(true); - let mut write_file = async_fs::OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(&file_path) - .await - .context(format!( - "Failed to create/open/truncate file for path '{}'", - file_path.display() - ))?; - if !skip_set_output_file { - // this installs the file watcher. - self.set_output_file(Some(file.to_owned())); - } - write_file.write_all(&bytes).await.context(format!( - "Failed to write bytes to file with path '{}'", - file_path.display() - ))?; - write_file.sync_all().await.context(format!( - "Failed to sync file after writing with path '{}'", - file_path.display() - ))?; - Ok(()) - }; - - if let Err(e) = file_write_operation.await { + let bytes = rnote_bytes_receiver.await??; + self.set_output_file_expect_write(true); + if let Err(e) = rnote_engine::utils::atomic_save_to_file(&filepath, &bytes).await { self.set_save_in_progress(false); // If the file operations failed in any way, we make sure to clear the expect_write flag // because we can't know for sure if the output-file watcher will be able to. @@ -264,6 +230,7 @@ impl RnCanvas { return Err(e); } + self.set_output_file(Some(gio::File::for_path(&filepath))); debug!("Saving file has finished successfully"); self.set_unsaved_changes(false); self.set_save_in_progress(false); From df3459b8e831b13047487600e7b090f955c88004 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Sun, 25 Aug 2024 19:32:27 +0200 Subject: [PATCH 22/36] formatting, lazy evals --- crates/rnote-cli/src/cli.rs | 2 +- crates/rnote-cli/src/mutate.rs | 2 +- .../src/fileformats/rnoteformat/legacy/mod.rs | 6 +++--- .../src/fileformats/rnoteformat/maj0min12.rs | 1 + crates/rnote-engine/src/fileformats/rnoteformat/mod.rs | 10 +++++----- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/rnote-cli/src/cli.rs b/crates/rnote-cli/src/cli.rs index 4516252f4b..81bd715aca 100644 --- a/crates/rnote-cli/src/cli.rs +++ b/crates/rnote-cli/src/cli.rs @@ -275,7 +275,7 @@ pub(crate) async fn run() -> anyhow::Result<()> { compression_method, compression_level, } => { - println!("Mutating.."); + println!("Mutating..\n"); mutate::run_mutate( rnote_files, not_in_place, diff --git a/crates/rnote-cli/src/mutate.rs b/crates/rnote-cli/src/mutate.rs index 0e8003b65b..be7a24ec47 100644 --- a/crates/rnote-cli/src/mutate.rs +++ b/crates/rnote-cli/src/mutate.rs @@ -89,6 +89,6 @@ pub(crate) async fn run_mutate( println!("{:.2} MB → {:.2} MB", old_size_mb, new_size_mb,); total_delta += new_size_mb - old_size_mb; } - println!("\n ⇒ ∆ = {:.2}", total_delta); + println!("\n⇒ ∆ = {:.2} MB", total_delta); Ok(()) } diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/legacy/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/legacy/mod.rs index 51d8675f79..63af11111b 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/legacy/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/legacy/mod.rs @@ -28,10 +28,10 @@ fn decompress_from_gzip(compressed: &[u8]) -> Result, anyhow::Error> { .len() .checked_sub(4) // only happens if the file has less than 4 bytes - .ok_or( + .ok_or_else(|| { anyhow::anyhow!("Not a valid gzip-compressed file") - .context("Failed to get the size of the decompressed data"), - )?; + .context("Failed to get the size of the decompressed data") + })?; decompressed_size.copy_from_slice(&compressed[idx_start..]); // u32 -> usize to avoid issues on 32-bit architectures // also more reasonable since the uncompressed size is given by 4 bytes diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs index e0a72acf82..1a2c84127d 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs @@ -15,6 +15,7 @@ use serde::{Deserialize, Serialize}; /// * serialization: method used to serialize/deserialize the engine snapshot /// * compression: method used to compress/decompress the serialized engine snapshot /// * uncompressed size: size of the uncompressed and serialized engine snapshot +/// * method_lock: if set to true, the file can keep using non-standard methods and will not be forced back into using defaults /// ## Body /// the body contains the serialized and (potentially) compressed engine snapshot #[derive(Debug, Clone)] diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index 219b21d010..7c6203a50a 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -53,7 +53,7 @@ impl FileFormatLoader for RnoteFile { { let magic_number = bytes .get(..9) - .ok_or(anyhow::anyhow!("Failed to get magic number"))?; + .ok_or_else(|| anyhow::anyhow!("Failed to get magic number"))?; if magic_number != Self::MAGIC_NUMBER { // Gzip magic number @@ -68,7 +68,7 @@ impl FileFormatLoader for RnoteFile { version.copy_from_slice( bytes .get(9..12) - .ok_or(anyhow::anyhow!("Failed to get version"))?, + .ok_or_else(|| anyhow::anyhow!("Failed to get version"))?, ); let version = semver::Version::new( u64::from(version[0]), @@ -80,16 +80,16 @@ impl FileFormatLoader for RnoteFile { header_size.copy_from_slice( bytes .get(12..16) - .ok_or(anyhow::anyhow!("Failed to get header size"))?, + .ok_or_else(|| anyhow::anyhow!("Failed to get header size"))?, ); let header_size = u32::from_le_bytes(header_size); let header_slice = bytes .get(16..16 + usize::try_from(header_size)?) - .ok_or(anyhow::anyhow!("File head missing"))?; + .ok_or_else(|| anyhow::anyhow!("File head missing"))?; let body_slice = bytes .get(16 + usize::try_from(header_size)?..) - .ok_or(anyhow::anyhow!("File body missing"))?; + .ok_or_else(|| anyhow::anyhow!("File body missing"))?; Ok(Self { head: RnoteHeader::load_from_slice(header_slice, &version)?, From e004f8bda255ef2485ed8b42dd8ac86b75013b3c Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Sun, 25 Aug 2024 23:38:19 +0200 Subject: [PATCH 23/36] comments, prettier code, faster legacy loading time --- crates/rnote-engine/src/engine/save.rs | 13 ++++++------ crates/rnote-engine/src/engine/snapshot.rs | 11 ++++++++++ .../src/fileformats/rnoteformat/maj0min12.rs | 20 +------------------ .../src/fileformats/rnoteformat/methods.rs | 2 +- .../src/fileformats/rnoteformat/mod.rs | 13 +++++------- 5 files changed, 25 insertions(+), 34 deletions(-) diff --git a/crates/rnote-engine/src/engine/save.rs b/crates/rnote-engine/src/engine/save.rs index 3c0aa97bc7..503fa50dc6 100644 --- a/crates/rnote-engine/src/engine/save.rs +++ b/crates/rnote-engine/src/engine/save.rs @@ -1,9 +1,10 @@ -use serde::{Deserialize, Serialize}; - +// Imports use crate::fileformats::rnoteformat::{ methods::{CompM, SerM}, RnoteHeader, }; +use serde::{Deserialize, Serialize}; +use std::mem::discriminant; /// Rnote file save preferences /// a subset of RnoteHeader @@ -23,12 +24,12 @@ impl SavePrefs { self.clone() } pub fn conforms_to_default(&self) -> bool { - std::mem::discriminant(&self.serialization) == std::mem::discriminant(&SerM::default()) - && std::mem::discriminant(&self.compression) - == std::mem::discriminant(&CompM::default()) + discriminant(&self.serialization) == discriminant(&SerM::default()) + && discriminant(&self.compression) == discriminant(&CompM::default()) } /// The EngineExport should only contain SavePrefs that conform to the default - /// otherwise, for example, new files could be created without any compression and encoded in JSON + /// otherwise for example, after having opened an uncompressed and JSON-encoded Rnote + /// save file while debugging, all new save files would be using the same methods pub fn clone_conformed_config(&self) -> Self { if self.conforms_to_default() { self.clone_config() diff --git a/crates/rnote-engine/src/engine/snapshot.rs b/crates/rnote-engine/src/engine/snapshot.rs index c3c3d2cea7..66e0c72181 100644 --- a/crates/rnote-engine/src/engine/snapshot.rs +++ b/crates/rnote-engine/src/engine/snapshot.rs @@ -54,6 +54,17 @@ impl EngineSnapshot { rayon::spawn(move || { let result = || -> anyhow::Result { + // support for legacy files + // gzip magic number + if bytes + .get(..2) + .ok_or_else(|| anyhow::anyhow!("Not an rnote file"))? + == [0x1f, 0x8b] + { + let legacy = rnoteformat::legacy::LegacyRnoteFile::load_from_bytes(&bytes)?; + return Ok(ijson::from_value(&legacy.engine_snapshot)?); + } + let rnote_file = rnoteformat::RnoteFile::load_from_bytes(&bytes) .context("loading RnoteFile from bytes failed.")?; Self::try_from(rnote_file) diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs index 1a2c84127d..79ae1d622a 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs @@ -1,7 +1,4 @@ -use super::{ - legacy::maj0min9::RnoteFileMaj0Min9, - methods::{CompM, SerM}, -}; +use super::methods::{CompM, SerM}; use serde::{Deserialize, Serialize}; /// # Rnote File Format Specifications @@ -25,21 +22,6 @@ pub struct RnoteFileMaj0Min12 { pub body: Vec, } -impl TryFrom for RnoteFileMaj0Min12 { - type Error = anyhow::Error; - fn try_from(value: RnoteFileMaj0Min9) -> Result { - Ok(Self { - head: RnoteHeaderMaj0Min12 { - serialization: SerM::Json, - compression: CompM::None, - uc_size: 0, - method_lock: false, - }, - body: serde_json::to_vec(&value.engine_snapshot)?, - }) - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename = "header")] pub struct RnoteHeaderMaj0Min12 { diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs index 1fcb876298..502c28b52e 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs @@ -14,7 +14,7 @@ pub enum CompM { None, #[serde(rename = "gzip")] Gzip(u8), - /// Zstd supports negative compression levels but I don't see the point in allowing these for rnote files + /// Zstd supports negative compression levels but I don't see the point in allowing these for Rnote files #[serde(rename = "zstd")] Zstd(u8), } diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index 7c6203a50a..5c56d881f6 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -16,7 +16,6 @@ pub use methods::{CompM, SerM}; // Imports use super::{FileFormatLoader, FileFormatSaver}; use crate::engine::{save::SavePrefs, EngineSnapshot}; -use legacy::LegacyRnoteFile; use maj0min12::RnoteFileMaj0Min12; use std::io::Write; @@ -24,7 +23,10 @@ pub type RnoteFile = maj0min12::RnoteFileMaj0Min12; pub type RnoteHeader = maj0min12::RnoteHeaderMaj0Min12; impl RnoteFileMaj0Min12 { + // ideally, this should never change pub const MAGIC_NUMBER: [u8; 9] = [0x52, 0x4e, 0x4f, 0x54, 0x45, 0xce, 0xa6, 0xce, 0x9b]; + // version not directly linked with CARGO_PKG_VERSION, it should only be updated + // (to the newest Rnote version) when changes are made pub const VERSION: [u8; 3] = [0, 12, 0]; pub const SEMVER: semver::Version = semver::Version::new(0, 12, 0); } @@ -56,12 +58,7 @@ impl FileFormatLoader for RnoteFile { .ok_or_else(|| anyhow::anyhow!("Failed to get magic number"))?; if magic_number != Self::MAGIC_NUMBER { - // Gzip magic number - if magic_number[..2] == [0x1f, 0x8b] { - return RnoteFile::try_from(LegacyRnoteFile::load_from_bytes(bytes)?); - } else { - Err(anyhow::anyhow!("Unkown file format"))?; - } + Err(anyhow::anyhow!("Unknown file format"))?; } let mut version: [u8; 3] = [0; 3]; @@ -106,7 +103,7 @@ impl RnoteHeader { { Ok(ijson::from_value(&serde_json::from_slice(slice)?)?) } else { - Err(anyhow::anyhow!("Unrecognized header")) + Err(anyhow::anyhow!("Unrecognized header version")) } } } From 48ed535a91b23bcee655c01dafee1f731614428a Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Mon, 26 Aug 2024 00:32:28 +0200 Subject: [PATCH 24/36] re-added the slower less efficient conversion for rnote-cli mutate --- crates/rnote-engine/src/engine/snapshot.rs | 4 ++-- .../src/fileformats/rnoteformat/maj0min12.rs | 20 ++++++++++++++++++- .../src/fileformats/rnoteformat/methods.rs | 2 -- .../src/fileformats/rnoteformat/mod.rs | 10 +++++++++- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/crates/rnote-engine/src/engine/snapshot.rs b/crates/rnote-engine/src/engine/snapshot.rs index 66e0c72181..b4bb3d5c63 100644 --- a/crates/rnote-engine/src/engine/snapshot.rs +++ b/crates/rnote-engine/src/engine/snapshot.rs @@ -58,7 +58,7 @@ impl EngineSnapshot { // gzip magic number if bytes .get(..2) - .ok_or_else(|| anyhow::anyhow!("Not an rnote file"))? + .ok_or_else(|| anyhow::anyhow!("Not an Rnote file"))? == [0x1f, 0x8b] { let legacy = rnoteformat::legacy::LegacyRnoteFile::load_from_bytes(&bytes)?; @@ -66,7 +66,7 @@ impl EngineSnapshot { } let rnote_file = rnoteformat::RnoteFile::load_from_bytes(&bytes) - .context("loading RnoteFile from bytes failed.")?; + .context("Loading RnoteFile from bytes failed.")?; Self::try_from(rnote_file) }; diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs index 79ae1d622a..aec75df105 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs @@ -1,4 +1,7 @@ -use super::methods::{CompM, SerM}; +use super::{ + legacy::maj0min9::RnoteFileMaj0Min9, + methods::{CompM, SerM}, +}; use serde::{Deserialize, Serialize}; /// # Rnote File Format Specifications @@ -37,3 +40,18 @@ pub struct RnoteHeaderMaj0Min12 { #[serde(rename = "method_lock")] pub method_lock: bool, } + +impl TryFrom for RnoteFileMaj0Min12 { + type Error = anyhow::Error; + fn try_from(value: RnoteFileMaj0Min9) -> Result { + Ok(Self { + head: RnoteHeaderMaj0Min12 { + serialization: SerM::Json, + compression: CompM::None, + uc_size: 0, + method_lock: false, + }, + body: serde_json::to_vec(&value.engine_snapshot)?, + }) + } +} diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs index 502c28b52e..1b8b548abd 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs @@ -8,7 +8,6 @@ use crate::engine::EngineSnapshot; /// Compression methods that can be applied to the serialized engine snapshot #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename = "compression_method")] pub enum CompM { #[serde(rename = "none")] None, @@ -21,7 +20,6 @@ pub enum CompM { /// Serialization methods that can be applied to a snapshot of the engine #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename = "serialization_method")] pub enum SerM { #[serde(rename = "bincode")] Bincode, diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index 5c56d881f6..8ce9df0669 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -16,6 +16,7 @@ pub use methods::{CompM, SerM}; // Imports use super::{FileFormatLoader, FileFormatSaver}; use crate::engine::{save::SavePrefs, EngineSnapshot}; +use legacy::LegacyRnoteFile; use maj0min12::RnoteFileMaj0Min12; use std::io::Write; @@ -58,7 +59,14 @@ impl FileFormatLoader for RnoteFile { .ok_or_else(|| anyhow::anyhow!("Failed to get magic number"))?; if magic_number != Self::MAGIC_NUMBER { - Err(anyhow::anyhow!("Unknown file format"))?; + // Gzip magic number + // The legacy file is generally caught first in Snapshot::load_from_rnote_bytes + // as this conversion is less efficient, but necessary to avoid spaghetti code in rnote-cli + if magic_number[..2] == [0x1f, 0x8b] { + return RnoteFile::try_from(LegacyRnoteFile::load_from_bytes(bytes)?); + } else { + Err(anyhow::anyhow!("Unrecognized file format"))?; + } } let mut version: [u8; 3] = [0; 3]; From 607ff75df9c782399a15c139e5bec94386957d77 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Mon, 26 Aug 2024 00:46:11 +0200 Subject: [PATCH 25/36] removed bincode + postcard --- Cargo.lock | 77 +------------------ Cargo.toml | 2 - crates/rnote-cli/src/cli.rs | 5 +- crates/rnote-cli/src/mutate.rs | 2 +- crates/rnote-engine/Cargo.toml | 2 - .../src/fileformats/rnoteformat/methods.rs | 14 +--- 6 files changed, 7 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ea83828ad..aa008e4766 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,15 +294,6 @@ version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" -[[package]] -name = "atomic-polyfill" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -355,15 +346,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bindgen" version = "0.69.4" @@ -632,12 +614,6 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" -[[package]] -name = "cobs" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" - [[package]] name = "color_quant" version = "1.1.0" @@ -740,12 +716,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "critical-section" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" - [[package]] name = "crossbeam-channel" version = "0.5.13" @@ -958,12 +928,6 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - [[package]] name = "encode_unicode" version = "0.3.6" @@ -1667,15 +1631,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "hash32" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" -dependencies = [ - "byteorder", -] - [[package]] name = "hash32" version = "0.3.1" @@ -1695,27 +1650,13 @@ dependencies = [ "allocator-api2", ] -[[package]] -name = "heapless" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" -dependencies = [ - "atomic-polyfill", - "hash32 0.2.1", - "rustc_version", - "serde", - "spin", - "stable_deref_trait", -] - [[package]] name = "heapless" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ - "hash32 0.3.1", + "hash32", "stable_deref_trait", ] @@ -3069,18 +3010,6 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" -[[package]] -name = "postcard" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" -dependencies = [ - "cobs", - "embedded-io", - "heapless 0.7.17", - "serde", -] - [[package]] name = "ppv-lite86" version = "0.2.17" @@ -3463,7 +3392,6 @@ dependencies = [ "approx", "async-fs", "base64", - "bincode", "bitcode", "cairo-rs", "chrono", @@ -3488,7 +3416,6 @@ dependencies = [ "piet", "piet-cairo", "poppler-rs", - "postcard", "rand", "rand_distr", "rand_pcg", @@ -3577,7 +3504,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "133315eb94c7b1e8d0cb097e5a710d850263372fd028fff18969de708afc7008" dependencies = [ - "heapless 0.8.0", + "heapless", "num-traits", "smallvec", ] diff --git a/Cargo.toml b/Cargo.toml index 893152f3d4..a07862ebc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,6 @@ approx = "0.5.1" async-fs = "2.1" atty = "0.2.14" base64 = "0.22.1" -bincode = "1.3" bitcode = { version = "0.6", features = ["serde"] } cairo-rs = { version = "0.19.4", features = ["v1_18", "png", "svg", "pdf"] } chrono = "0.4.38" @@ -61,7 +60,6 @@ parry2d-f64 = { version = "0.17.0", features = ["serde-serialize"] } path-absolutize = "3.1" piet = "0.6.2" piet-cairo = "0.6.2" -postcard = { version = "1.0", features = ["alloc"] } rand = "0.8.5" rand_distr = "0.4.3" rand_pcg = "0.3.1" diff --git a/crates/rnote-cli/src/cli.rs b/crates/rnote-cli/src/cli.rs index 81bd715aca..dc5235d766 100644 --- a/crates/rnote-cli/src/cli.rs +++ b/crates/rnote-cli/src/cli.rs @@ -9,6 +9,7 @@ use rnote_engine::engine::export::{ SelectionExportPrefs, }; use rnote_engine::engine::import::XoppImportPrefs; +use rnote_engine::fileformats::rnoteformat; use rnote_engine::SelectionCollision; use smol::fs::File; use smol::io::{AsyncReadExt, AsyncWriteExt}; @@ -86,9 +87,9 @@ pub(crate) enum Command { /// Unlocks the compression and serialization methods used by the rnote save file(s) #[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(rnote_engine::fileformats::rnoteformat::SerM::VALID_STR_ARRAY))] + #[arg(short = 's', long, action = clap::ArgAction::Set, value_parser = PossibleValuesParser::new(rnoteformat::SerM::VALID_STR_ARRAY))] serialization_method: Option, - #[arg(short = 'c', long, action = clap::ArgAction::Set, value_parser = PossibleValuesParser::new(rnote_engine::fileformats::rnoteformat::CompM::VALID_STR_ARRAY))] + #[arg(short = 'c', long, action = clap::ArgAction::Set, value_parser = PossibleValuesParser::new(rnoteformat::CompM::VALID_STR_ARRAY))] compression_method: Option, #[arg(short = 'v', long, action = clap::ArgAction::Set)] compression_level: Option, diff --git a/crates/rnote-cli/src/mutate.rs b/crates/rnote-cli/src/mutate.rs index be7a24ec47..2c51e2e030 100644 --- a/crates/rnote-cli/src/mutate.rs +++ b/crates/rnote-cli/src/mutate.rs @@ -79,7 +79,7 @@ pub(crate) async fn run_mutate( .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!("{}_mutated.rnote", file_stem)); + filepath.set_file_name(format!("{}_mut.rnote", file_stem)); } let data = rnote_file.save_as_bytes("")?; diff --git a/crates/rnote-engine/Cargo.toml b/crates/rnote-engine/Cargo.toml index 4f218eba38..aea64a62a6 100644 --- a/crates/rnote-engine/Cargo.toml +++ b/crates/rnote-engine/Cargo.toml @@ -15,7 +15,6 @@ anyhow = { workspace = true } approx = { workspace = true } async-fs = { workspace = true } base64 = { workspace = true } -bincode = { workspace = true } bitcode = { workspace = true, features = ["serde"] } cairo-rs = { workspace = true } chrono = { workspace = true } @@ -39,7 +38,6 @@ parry2d-f64 = { workspace = true } piet = { workspace = true } piet-cairo = { workspace = true } poppler-rs = { workspace = true } -postcard = { workspace = true, features = ["alloc"] } rand = { workspace = true } rand_distr = { workspace = true } rand_pcg = { workspace = true } diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs index 1b8b548abd..8e79d0e14f 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs @@ -21,14 +21,10 @@ pub enum CompM { /// Serialization methods that can be applied to a snapshot of the engine #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum SerM { - #[serde(rename = "bincode")] - Bincode, #[serde(rename = "bitcode")] Bitcode, #[serde(rename = "json")] Json, - #[serde(rename = "postcard")] - Postcard, } impl CompM { @@ -123,23 +119,17 @@ impl FromStr for CompM { impl SerM { pub fn serialize(&self, engine_snapshot: &EngineSnapshot) -> anyhow::Result> { match self { - Self::Bincode => Ok::, anyhow::Error>(bincode::serialize(engine_snapshot)?), Self::Bitcode => Ok(bitcode::serialize(engine_snapshot)?), Self::Json => Ok(serde_json::to_vec(&ijson::to_value(engine_snapshot)?)?), - Self::Postcard => Ok(postcard::to_allocvec(engine_snapshot)?), } } pub fn deserialize(&self, data: &[u8]) -> anyhow::Result { match self { - Self::Bincode => Ok(bincode::deserialize(data)?), Self::Bitcode => Ok(bitcode::deserialize(data)?), Self::Json => Ok(ijson::from_value(&serde_json::from_slice(data)?)?), - Self::Postcard => Ok(postcard::from_bytes(data)?), } } - pub const VALID_STR_ARRAY: [&'static str; 9] = [ - "Bincode", "bincode", "Bitcode", "bitcode", "Json", "JSON", "json", "Postcard", "postcard", - ]; + pub const VALID_STR_ARRAY: [&'static str; 5] = ["Bitcode", "bitcode", "Json", "JSON", "json"]; } impl Default for SerM { @@ -152,10 +142,8 @@ impl FromStr for SerM { type Err = &'static str; fn from_str(s: &str) -> Result { match s { - "Bincode" | "bincode" => Ok(Self::Bincode), "Bitcode" | "bitcode" => Ok(Self::Bitcode), "Json" | "JSON" | "json" => Ok(Self::Json), - "Postcard" | "postcard" => Ok(Self::Postcard), _ => Err("Unknown serialization method"), } } From 40131edb881909f9c47d9f76e6df809eae1a2116 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Mon, 2 Sep 2024 14:52:35 +0200 Subject: [PATCH 26/36] renamed CompM to CompressionMethod and SerM to SerializationMethod --- Cargo.lock | 2 +- crates/rnote-cli/src/cli.rs | 4 ++-- crates/rnote-cli/src/mutate.rs | 4 ++-- crates/rnote-engine/src/engine/save.rs | 16 ++++++++-------- .../src/fileformats/rnoteformat/maj0min12.rs | 10 +++++----- .../src/fileformats/rnoteformat/methods.rs | 16 ++++++++-------- .../src/fileformats/rnoteformat/mod.rs | 2 +- crates/rnote-ui/src/settingspanel/mod.rs | 6 +++--- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa008e4766..ff40825daf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -393,7 +393,7 @@ checksum = "a539389a13af092cd345a2b47ae7dec12deb306d660b2223d25cd3419b253ebe" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.75", ] [[package]] diff --git a/crates/rnote-cli/src/cli.rs b/crates/rnote-cli/src/cli.rs index dc5235d766..cc1d750385 100644 --- a/crates/rnote-cli/src/cli.rs +++ b/crates/rnote-cli/src/cli.rs @@ -87,9 +87,9 @@ pub(crate) enum Command { /// Unlocks the compression and serialization methods used by the rnote save file(s) #[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::SerM::VALID_STR_ARRAY))] + #[arg(short = 's', long, action = clap::ArgAction::Set, value_parser = PossibleValuesParser::new(rnoteformat::SerializationMethod::VALID_STR_ARRAY))] serialization_method: Option, - #[arg(short = 'c', long, action = clap::ArgAction::Set, value_parser = PossibleValuesParser::new(rnoteformat::CompM::VALID_STR_ARRAY))] + #[arg(short = 'c', long, action = clap::ArgAction::Set, value_parser = PossibleValuesParser::new(rnoteformat::CompressionMethod::VALID_STR_ARRAY))] compression_method: Option, #[arg(short = 'v', long, action = clap::ArgAction::Set)] compression_level: Option, diff --git a/crates/rnote-cli/src/mutate.rs b/crates/rnote-cli/src/mutate.rs index 2c51e2e030..5460027dd8 100644 --- a/crates/rnote-cli/src/mutate.rs +++ b/crates/rnote-cli/src/mutate.rs @@ -43,13 +43,13 @@ pub(crate) async fn run_mutate( let rnote_file = RnoteFile::load_from_bytes(&bytes)?; let serialization = if let Some(ref str) = serialization_method { - rnote_engine::fileformats::rnoteformat::SerM::from_str(str).unwrap() + rnote_engine::fileformats::rnoteformat::SerializationMethod::from_str(str).unwrap() } else { rnote_file.head.serialization }; let mut compression = if let Some(ref str) = compression_method { - rnote_engine::fileformats::rnoteformat::CompM::from_str(str).unwrap() + rnote_engine::fileformats::rnoteformat::CompressionMethod::from_str(str).unwrap() } else { rnote_file.head.compression }; diff --git a/crates/rnote-engine/src/engine/save.rs b/crates/rnote-engine/src/engine/save.rs index 503fa50dc6..38956bad12 100644 --- a/crates/rnote-engine/src/engine/save.rs +++ b/crates/rnote-engine/src/engine/save.rs @@ -1,6 +1,6 @@ // Imports use crate::fileformats::rnoteformat::{ - methods::{CompM, SerM}, + methods::{CompressionMethod, SerializationMethod}, RnoteHeader, }; use serde::{Deserialize, Serialize}; @@ -12,9 +12,9 @@ use std::mem::discriminant; #[serde(default, rename = "save_prefs")] pub struct SavePrefs { #[serde(rename = "serialization")] - pub serialization: SerM, + pub serialization: SerializationMethod, #[serde(rename = "compression")] - pub compression: CompM, + pub compression: CompressionMethod, #[serde(rename = "method_lock")] pub method_lock: bool, } @@ -24,8 +24,8 @@ impl SavePrefs { self.clone() } pub fn conforms_to_default(&self) -> bool { - discriminant(&self.serialization) == discriminant(&SerM::default()) - && discriminant(&self.compression) == discriminant(&CompM::default()) + discriminant(&self.serialization) == discriminant(&SerializationMethod::default()) + && discriminant(&self.compression) == discriminant(&CompressionMethod::default()) } /// The EngineExport should only contain SavePrefs that conform to the default /// otherwise for example, after having opened an uncompressed and JSON-encoded Rnote @@ -42,8 +42,8 @@ impl SavePrefs { impl Default for SavePrefs { fn default() -> Self { Self { - serialization: SerM::default(), - compression: CompM::default(), + serialization: SerializationMethod::default(), + compression: CompressionMethod::default(), method_lock: false, } } @@ -82,7 +82,7 @@ impl TryFrom for CompressionLevel { } } -impl CompM { +impl CompressionMethod { pub fn get_compression_level(&self) -> CompressionLevel { match self { Self::None => CompressionLevel::None, diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs index aec75df105..a39ed9aca0 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs @@ -1,6 +1,6 @@ use super::{ legacy::maj0min9::RnoteFileMaj0Min9, - methods::{CompM, SerM}, + methods::{CompressionMethod, SerializationMethod}, }; use serde::{Deserialize, Serialize}; @@ -30,10 +30,10 @@ pub struct RnoteFileMaj0Min12 { pub struct RnoteHeaderMaj0Min12 { /// method used to serialize/deserialize the engine snapshot #[serde(rename = "serialization")] - pub serialization: SerM, + pub serialization: SerializationMethod, /// method used to compress/decompress the serialized engine snapshot #[serde(rename = "compression")] - pub compression: CompM, + pub compression: CompressionMethod, /// size of the uncompressed and serialized engine snapshot #[serde(rename = "uncompressed_size")] pub uc_size: u64, @@ -46,8 +46,8 @@ impl TryFrom for RnoteFileMaj0Min12 { fn try_from(value: RnoteFileMaj0Min9) -> Result { Ok(Self { head: RnoteHeaderMaj0Min12 { - serialization: SerM::Json, - compression: CompM::None, + serialization: SerializationMethod::Json, + compression: CompressionMethod::None, uc_size: 0, method_lock: false, }, diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs index 8e79d0e14f..f43197a74e 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs @@ -8,7 +8,7 @@ use crate::engine::EngineSnapshot; /// Compression methods that can be applied to the serialized engine snapshot #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum CompM { +pub enum CompressionMethod { #[serde(rename = "none")] None, #[serde(rename = "gzip")] @@ -20,14 +20,14 @@ pub enum CompM { /// Serialization methods that can be applied to a snapshot of the engine #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum SerM { +pub enum SerializationMethod { #[serde(rename = "bitcode")] Bitcode, #[serde(rename = "json")] Json, } -impl CompM { +impl CompressionMethod { pub fn compress(&self, data: Vec) -> anyhow::Result> { match self { Self::None => Ok(data), @@ -98,13 +98,13 @@ impl CompM { pub const VALID_STR_ARRAY: [&'static str; 6] = ["None", "none", "Gzip", "gzip", "Zstd", "zstd"]; } -impl Default for CompM { +impl Default for CompressionMethod { fn default() -> Self { Self::Zstd(9) } } -impl FromStr for CompM { +impl FromStr for CompressionMethod { type Err = &'static str; fn from_str(s: &str) -> Result { match s { @@ -116,7 +116,7 @@ impl FromStr for CompM { } } -impl SerM { +impl SerializationMethod { pub fn serialize(&self, engine_snapshot: &EngineSnapshot) -> anyhow::Result> { match self { Self::Bitcode => Ok(bitcode::serialize(engine_snapshot)?), @@ -132,13 +132,13 @@ impl SerM { pub const VALID_STR_ARRAY: [&'static str; 5] = ["Bitcode", "bitcode", "Json", "JSON", "json"]; } -impl Default for SerM { +impl Default for SerializationMethod { fn default() -> Self { Self::Bitcode } } -impl FromStr for SerM { +impl FromStr for SerializationMethod { type Err = &'static str; fn from_str(s: &str) -> Result { match s { diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index 8ce9df0669..ae246da2ce 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -11,7 +11,7 @@ pub(crate) mod maj0min12; pub(crate) mod methods; // Re-exports -pub use methods::{CompM, SerM}; +pub use methods::{CompressionMethod, SerializationMethod}; // Imports use super::{FileFormatLoader, FileFormatSaver}; diff --git a/crates/rnote-ui/src/settingspanel/mod.rs b/crates/rnote-ui/src/settingspanel/mod.rs index 252947d1c6..8a311af119 100644 --- a/crates/rnote-ui/src/settingspanel/mod.rs +++ b/crates/rnote-ui/src/settingspanel/mod.rs @@ -5,7 +5,7 @@ mod penshortcutrow; // Re-exports pub(crate) use penshortcutrow::RnPenShortcutRow; use rnote_compose::ext::Vector2Ext; -use rnote_engine::fileformats::rnoteformat::CompM; +use rnote_engine::fileformats::rnoteformat::CompressionMethod; // Imports use crate::{RnAppWindow, RnCanvasWrapper, RnIconPicker, RnUnitEntry}; @@ -415,9 +415,9 @@ impl RnSettingsPanel { imp.doc_background_pattern_height_unitentry .set_value_in_px(background.pattern_size[1]); self.set_document_layout(&document_layout); - // set the compression level row to invisible if CompM is None + // set the compression level row to invisible if CompressionMethod is None imp.doc_compression_level_row - .set_visible(!matches!(compression, CompM::None)); + .set_visible(!matches!(compression, CompressionMethod::None)); match compression.get_compression_level() { CompressionLevel::None => (), From b8062ecaf56d3619ffffe381c39dc70012eff3b0 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Mon, 2 Sep 2024 16:36:14 +0200 Subject: [PATCH 27/36] one-to-one repr of semver::Version in PRELUE --- Cargo.toml | 2 +- .../src/fileformats/rnoteformat/mod.rs | 134 +++++++++++++++--- 2 files changed, 113 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a07862ebc7..a6cdfc81f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,7 +88,7 @@ winresource = "0.1.17" xmlwriter = "0.1.0" # Enabling feature > v20_9 causes linker errors on mingw poppler-rs = { version = "0.23.0", features = ["v20_9"] } -zstd = { version = "0.13", features = ["zstdmt"] } +zstd = { version = "0.13", features = ["zstdmt"] } [patch.crates-io] # once a new piet (current v0.6.2) is released with updated cairo and kurbo deps, this can be removed. diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index ae246da2ce..239618680a 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -16,6 +16,7 @@ pub use methods::{CompressionMethod, SerializationMethod}; // Imports use super::{FileFormatLoader, FileFormatSaver}; use crate::engine::{save::SavePrefs, EngineSnapshot}; +use anyhow::Context; use legacy::LegacyRnoteFile; use maj0min12::RnoteFileMaj0Min12; use std::io::Write; @@ -26,22 +27,36 @@ pub type RnoteHeader = maj0min12::RnoteHeaderMaj0Min12; impl RnoteFileMaj0Min12 { // ideally, this should never change pub const MAGIC_NUMBER: [u8; 9] = [0x52, 0x4e, 0x4f, 0x54, 0x45, 0xce, 0xa6, 0xce, 0x9b]; - // version not directly linked with CARGO_PKG_VERSION, it should only be updated - // (to the newest Rnote version) when changes are made - pub const VERSION: [u8; 3] = [0, 12, 0]; - pub const SEMVER: semver::Version = semver::Version::new(0, 12, 0); + pub const SEMVER: &'static str = crate::utils::crate_version(); } impl FileFormatSaver for RnoteFile { fn save_as_bytes(&self, _file_name: &str) -> anyhow::Result> { + let version = semver::Version::parse(Self::SEMVER)?; + let pre_release = version.pre.as_str(); + let build_metadata = version.build.as_str(); + let json_header = serde_json::to_vec(&ijson::to_value(&self.head)?)?; let header = [ &Self::MAGIC_NUMBER[..], - &Self::VERSION[..], - &u32::try_from(json_header.len())?.to_le_bytes(), + &version.major.to_le_bytes(), + &version.minor.to_le_bytes(), + &version.patch.to_le_bytes(), + &u16::try_from(pre_release.len()) + .context("Prerelease text is too long")? + .to_le_bytes(), + pre_release.as_bytes(), + &u16::try_from(build_metadata.len()) + .context("BuildMetadata text is too long")? + .to_le_bytes(), + build_metadata.as_bytes(), + &u32::try_from(json_header.len()) + .context("Serialized header is too large")? + .to_le_bytes(), &json_header, ] .concat(); + let mut buffer: Vec = Vec::new(); buffer.write_all(&header)?; buffer.write_all(&self.body)?; @@ -54,14 +69,17 @@ impl FileFormatLoader for RnoteFile { where Self: Sized, { + let mut prelude_idx: usize = 0; + let magic_number = bytes - .get(..9) + .get(prelude_idx..9) .ok_or_else(|| anyhow::anyhow!("Failed to get magic number"))?; + prelude_idx += 9; if magic_number != Self::MAGIC_NUMBER { // Gzip magic number // The legacy file is generally caught first in Snapshot::load_from_rnote_bytes - // as this conversion is less efficient, but necessary to avoid spaghetti code in rnote-cli + // howver this less efficient catch is necessary for rnote-cli mutate if magic_number[..2] == [0x1f, 0x8b] { return RnoteFile::try_from(LegacyRnoteFile::load_from_bytes(bytes)?); } else { @@ -69,31 +87,100 @@ impl FileFormatLoader for RnoteFile { } } - let mut version: [u8; 3] = [0; 3]; - version.copy_from_slice( + let mut major: [u8; 8] = [0; 8]; + major.copy_from_slice( + bytes + .get(prelude_idx..prelude_idx + 8) + .ok_or_else(|| anyhow::anyhow!("Failed to get version"))?, + ); + prelude_idx += 8; + let major = u64::from_le_bytes(major); + + let mut minor: [u8; 8] = [0; 8]; + minor.copy_from_slice( + bytes + .get(prelude_idx..prelude_idx + 8) + .ok_or_else(|| anyhow::anyhow!("Failed to get version"))?, + ); + prelude_idx += 8; + let minor = u64::from_le_bytes(minor); + + let mut patch: [u8; 8] = [0; 8]; + patch.copy_from_slice( bytes - .get(9..12) + .get(prelude_idx..prelude_idx + 8) .ok_or_else(|| anyhow::anyhow!("Failed to get version"))?, ); - let version = semver::Version::new( - u64::from(version[0]), - u64::from(version[1]), - u64::from(version[2]), + prelude_idx += 8; + let patch = u64::from_le_bytes(patch); + + let mut pre_release_length: [u8; 2] = [0; 2]; + pre_release_length.copy_from_slice( + bytes + .get(prelude_idx..prelude_idx + 2) + .ok_or_else(|| anyhow::anyhow!("Failed to get Prerelease length"))?, + ); + prelude_idx += 2; + let pre_release_length = usize::from(u16::from_le_bytes(pre_release_length)); + + let pre_release = if pre_release_length == 0 { + semver::Prerelease::EMPTY + } else { + let text = core::str::from_utf8( + bytes + .get(prelude_idx..prelude_idx + pre_release_length) + .ok_or_else(|| anyhow::anyhow!("Failed to get Prerelease"))?, + )?; + prelude_idx += pre_release_length; + semver::Prerelease::new(text)? + }; + + let mut build_metadata_length: [u8; 2] = [0; 2]; + build_metadata_length.copy_from_slice( + bytes + .get(prelude_idx..prelude_idx + 2) + .ok_or_else(|| anyhow::anyhow!("Failed to get BuildMetadata length"))?, ); + prelude_idx += 2; + let build_metadata_length = usize::from(u16::from_le_bytes(build_metadata_length)); + + let build_metadata = if build_metadata_length == 0 { + semver::BuildMetadata::EMPTY + } else { + let text = core::str::from_utf8( + bytes + .get(prelude_idx..prelude_idx + build_metadata_length) + .ok_or_else(|| anyhow::anyhow!("Failed to get BuildMetadata"))?, + )?; + prelude_idx += build_metadata_length; + semver::BuildMetadata::new(text)? + }; + + let version = semver::Version { + major, + minor, + patch, + pre: pre_release, + build: build_metadata, + }; let mut header_size: [u8; 4] = [0; 4]; header_size.copy_from_slice( bytes - .get(12..16) + .get(prelude_idx..prelude_idx + 4) .ok_or_else(|| anyhow::anyhow!("Failed to get header size"))?, ); - let header_size = u32::from_le_bytes(header_size); + prelude_idx += 4; + let header_size = usize::try_from(u32::from_le_bytes(header_size)) + .context("Rnote file header is too large")?; + let header_slice = bytes - .get(16..16 + usize::try_from(header_size)?) - .ok_or_else(|| anyhow::anyhow!("File head missing"))?; + .get(prelude_idx..prelude_idx + header_size) + .ok_or_else(|| anyhow::anyhow!("File header missing"))?; + prelude_idx += header_size; let body_slice = bytes - .get(16 + usize::try_from(header_size)?..) + .get(prelude_idx..) .ok_or_else(|| anyhow::anyhow!("File body missing"))?; Ok(Self { @@ -105,13 +192,16 @@ impl FileFormatLoader for RnoteFile { impl RnoteHeader { fn load_from_slice(slice: &[u8], version: &semver::Version) -> anyhow::Result { - if semver::VersionReq::parse(">=0.12.0") + if semver::VersionReq::parse(">=0.11.0") .unwrap() .matches(version) { Ok(ijson::from_value(&serde_json::from_slice(slice)?)?) } else { - Err(anyhow::anyhow!("Unrecognized header version")) + Err(anyhow::anyhow!( + "Unrecognized version specifier '{}' for header", + version + )) } } } From 4d2d1ac1da7ebecb8f64aaf3c693892fae7b7e8b Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:54:38 +0200 Subject: [PATCH 28/36] more readable code and errors, concat, tested an owned from_bytes and into_bytes, is not worth it at all --- Cargo.lock | 2 +- crates/rnote-cli/src/mutate.rs | 8 +- crates/rnote-engine/src/engine/save.rs | 14 +- crates/rnote-engine/src/engine/snapshot.rs | 1 + .../src/fileformats/rnoteformat/legacy/mod.rs | 18 +-- .../src/fileformats/rnoteformat/maj0min12.rs | 20 ++- .../src/fileformats/rnoteformat/mod.rs | 145 ++++++++---------- 7 files changed, 107 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ff40825daf..aa008e4766 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -393,7 +393,7 @@ checksum = "a539389a13af092cd345a2b47ae7dec12deb306d660b2223d25cd3419b253ebe" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.72", ] [[package]] diff --git a/crates/rnote-cli/src/mutate.rs b/crates/rnote-cli/src/mutate.rs index 5460027dd8..1e284e9c33 100644 --- a/crates/rnote-cli/src/mutate.rs +++ b/crates/rnote-cli/src/mutate.rs @@ -45,26 +45,26 @@ pub(crate) async fn run_mutate( let serialization = if let Some(ref str) = serialization_method { rnote_engine::fileformats::rnoteformat::SerializationMethod::from_str(str).unwrap() } else { - rnote_file.head.serialization + 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.head.compression + rnote_file.header.compression }; if let Some(lvl) = compression_level { compression.update_compression_level(lvl)?; } - let method_lock = (rnote_file.head.method_lock | lock) && !unlock; + 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 { - head: RnoteHeader { + header: RnoteHeader { serialization, compression, uc_size, diff --git a/crates/rnote-engine/src/engine/save.rs b/crates/rnote-engine/src/engine/save.rs index 38956bad12..0253606dbc 100644 --- a/crates/rnote-engine/src/engine/save.rs +++ b/crates/rnote-engine/src/engine/save.rs @@ -6,8 +6,18 @@ use crate::fileformats::rnoteformat::{ use serde::{Deserialize, Serialize}; use std::mem::discriminant; -/// Rnote file save preferences -/// a subset of RnoteHeader +/// Rnote file save preferences, a subset of RnoteHeader +/// used by EngineSnapshot, Engine, and EngineConfig +/// +/// when loading in an Rnote file, SavePrefs will be created from RnoteHeader +/// if RnoteHeader's serialization and compression methods conform to the defaults, or method_lock is set to true +/// => SavePrefs override EngineSnapshot's default SavePrefs +/// => SavePrefs transferred from EngineSnapshot to Engine +/// +/// as for EngineConfig, if and only if Engine's SavePrefs conform to the defaults +/// => SavePrefs cloned from Engine into EngineConfig +/// +/// please note that the compression level is not used to check whether or not the methods conform to the defaults #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default, rename = "save_prefs")] pub struct SavePrefs { diff --git a/crates/rnote-engine/src/engine/snapshot.rs b/crates/rnote-engine/src/engine/snapshot.rs index b4bb3d5c63..a3bf3d5541 100644 --- a/crates/rnote-engine/src/engine/snapshot.rs +++ b/crates/rnote-engine/src/engine/snapshot.rs @@ -28,6 +28,7 @@ pub struct EngineSnapshot { pub chrono_components: Arc>>, #[serde(rename = "chrono_counter")] pub chrono_counter: u32, + // save_prefs is skipped as it is extracted and incorporated into the header when saving #[serde(skip, default)] pub save_prefs: SavePrefs, } diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/legacy/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/legacy/mod.rs index 63af11111b..cd96d92143 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/legacy/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/legacy/mod.rs @@ -61,7 +61,7 @@ impl FileFormatLoader for LegacyRnoteFile { fn load_from_bytes(bytes: &[u8]) -> anyhow::Result { let wrapper = serde_json::from_slice::(&decompress_from_gzip(bytes)?) - .context("deserializing RnotefileWrapper from bytes failed.")?; + .context("Deserializing RnotefileWrapper from bytes failed")?; // Conversions for older file format versions happen here if semver::VersionReq::parse(">=0.9.0") @@ -69,37 +69,37 @@ impl FileFormatLoader for LegacyRnoteFile { .matches(&wrapper.version) { ijson::from_value::(&wrapper.data) - .context("deserializing RnoteFileMaj0Min9 failed.") + .context("Deserializing RnoteFileMaj0Min9 failed") } else if semver::VersionReq::parse(">=0.5.10") .unwrap() .matches(&wrapper.version) { ijson::from_value::(&wrapper.data) - .context("deserializing RnoteFileMaj0Min6 failed.") + .context("Deserializing RnoteFileMaj0Min6 failed") .and_then(RnoteFileMaj0Min9::try_from) - .context("converting RnoteFileMaj0Min6 to newest file version failed.") + .context("Converting RnoteFileMaj0Min6 to newest file version failed") } else if semver::VersionReq::parse(">=0.5.9") .unwrap() .matches(&wrapper.version) { ijson::from_value::(&wrapper.data) - .context("deserializing RnoteFileMaj0Min5Patch9 failed.") + .context("Deserializing RnoteFileMaj0Min5Patch9 failed") .and_then(RnoteFileMaj0Min6::try_from) .and_then(RnoteFileMaj0Min9::try_from) - .context("converting RnoteFileMaj0Min5Patch9 to newest file version failed.") + .context("Converting RnoteFileMaj0Min5Patch9 to newest file version failed") } else if semver::VersionReq::parse(">=0.5.0") .unwrap() .matches(&wrapper.version) { ijson::from_value::(&wrapper.data) - .context("deserializing RnoteFileMaj0Min5Patch8 failed") + .context("Deserializing RnoteFileMaj0Min5Patch8 failed") .and_then(RnoteFileMaj0Min5Patch9::try_from) .and_then(RnoteFileMaj0Min6::try_from) .and_then(RnoteFileMaj0Min9::try_from) - .context("converting RnoteFileMaj0Min5Patch8 to newest file version failed.") + .context("Converting RnoteFileMaj0Min5Patch8 to newest file version failed") } else { Err(anyhow::anyhow!( - "failed to load rnote file from bytes, unsupported version: {}.", + "Failed to load rnote file from bytes, unsupported version: '{}'", wrapper.version )) } diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs index a39ed9aca0..31bc3ea6ce 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs @@ -5,22 +5,26 @@ use super::{ use serde::{Deserialize, Serialize}; /// # Rnote File Format Specifications -/// ## Prelude (not included in this struct) +/// +/// ## Prelude (not included in this struct, u16,u32,... are represented using little endian) /// * magic number: [u8; 9] = [0x52, 0x4e, 0x4f, 0x54, 0x45, 0xce, 0xa6, 0xce, 0x9b], "RNOTEϕλ" -/// * version: [u8; 3] = [major, minor, patch] -/// * header size: [u8; 4], little endian repr. +/// * version: [u64, u64, u64, u16, str, u16, str] (almost one-to-one representation of semver::Version) +/// [major, minor, patch, Prerelease size, Prerelease, BuildMetadata size, Buildmetadata] +/// * header size: u32 +/// /// ## Header -/// the header is a forward-compatible json-encoded struct -/// containing additional information on the file +/// a forward-compatible json-encoded struct /// * serialization: method used to serialize/deserialize the engine snapshot /// * compression: method used to compress/decompress the serialized engine snapshot /// * uncompressed size: size of the uncompressed and serialized engine snapshot -/// * method_lock: if set to true, the file can keep using non-standard methods and will not be forced back into using defaults +/// * method lock: if set to true, the file can keep using non-standard methods and will not be forced back into using defaults +/// /// ## Body /// the body contains the serialized and (potentially) compressed engine snapshot #[derive(Debug, Clone)] pub struct RnoteFileMaj0Min12 { - pub head: RnoteHeaderMaj0Min12, + /// called header and not head because head = prelude + header + pub header: RnoteHeaderMaj0Min12, /// A serialized and (potentially) compressed engine snapshot pub body: Vec, } @@ -45,7 +49,7 @@ impl TryFrom for RnoteFileMaj0Min12 { type Error = anyhow::Error; fn try_from(value: RnoteFileMaj0Min9) -> Result { Ok(Self { - head: RnoteHeaderMaj0Min12 { + header: RnoteHeaderMaj0Min12 { serialization: SerializationMethod::Json, compression: CompressionMethod::None, uc_size: 0, diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index 239618680a..535e21610e 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -19,7 +19,6 @@ use crate::engine::{save::SavePrefs, EngineSnapshot}; use anyhow::Context; use legacy::LegacyRnoteFile; use maj0min12::RnoteFileMaj0Min12; -use std::io::Write; pub type RnoteFile = maj0min12::RnoteFileMaj0Min12; pub type RnoteHeader = maj0min12::RnoteHeaderMaj0Min12; @@ -36,31 +35,30 @@ impl FileFormatSaver for RnoteFile { let pre_release = version.pre.as_str(); let build_metadata = version.build.as_str(); - let json_header = serde_json::to_vec(&ijson::to_value(&self.head)?)?; - let header = [ + let header = serde_json::to_vec(&ijson::to_value(&self.header)?)?; + let head = [ &Self::MAGIC_NUMBER[..], &version.major.to_le_bytes(), &version.minor.to_le_bytes(), &version.patch.to_le_bytes(), &u16::try_from(pre_release.len()) - .context("Prerelease text is too long")? + .context("Prerelease exceeds max size (u16::MAX)")? .to_le_bytes(), pre_release.as_bytes(), &u16::try_from(build_metadata.len()) - .context("BuildMetadata text is too long")? + .context("BuildMetadata exceeds max size (u16::MAX)")? .to_le_bytes(), build_metadata.as_bytes(), - &u32::try_from(json_header.len()) - .context("Serialized header is too large")? + &u32::try_from(header.len()) + .context("Serialized RnoteHeader exceeds max size (u32::MAX)")? .to_le_bytes(), - &json_header, + &header, ] .concat(); - let mut buffer: Vec = Vec::new(); - buffer.write_all(&header)?; - buffer.write_all(&self.body)?; - Ok(buffer) + // .concat is absurdly fast + // much faster than Vec::apend or Vec::extend + Ok([head.as_slice(), self.body.as_slice()].concat()) } } @@ -69,12 +67,12 @@ impl FileFormatLoader for RnoteFile { where Self: Sized, { - let mut prelude_idx: usize = 0; + let mut cursor: usize = 0; let magic_number = bytes - .get(prelude_idx..9) + .get(cursor..9) .ok_or_else(|| anyhow::anyhow!("Failed to get magic number"))?; - prelude_idx += 9; + cursor += 9; if magic_number != Self::MAGIC_NUMBER { // Gzip magic number @@ -83,76 +81,70 @@ impl FileFormatLoader for RnoteFile { if magic_number[..2] == [0x1f, 0x8b] { return RnoteFile::try_from(LegacyRnoteFile::load_from_bytes(bytes)?); } else { - Err(anyhow::anyhow!("Unrecognized file format"))?; + return Err(anyhow::anyhow!("Unrecognized file format")); } } let mut major: [u8; 8] = [0; 8]; major.copy_from_slice( - bytes - .get(prelude_idx..prelude_idx + 8) - .ok_or_else(|| anyhow::anyhow!("Failed to get version"))?, + bytes.get(cursor..cursor + 8).ok_or_else(|| { + anyhow::anyhow!("Failed to get version.major, insufficient bytes") + })?, ); - prelude_idx += 8; + cursor += 8; let major = u64::from_le_bytes(major); let mut minor: [u8; 8] = [0; 8]; minor.copy_from_slice( - bytes - .get(prelude_idx..prelude_idx + 8) - .ok_or_else(|| anyhow::anyhow!("Failed to get version"))?, + bytes.get(cursor..cursor + 8).ok_or_else(|| { + anyhow::anyhow!("Failed to get version.minor, insufficient bytes") + })?, ); - prelude_idx += 8; + cursor += 8; let minor = u64::from_le_bytes(minor); let mut patch: [u8; 8] = [0; 8]; patch.copy_from_slice( - bytes - .get(prelude_idx..prelude_idx + 8) - .ok_or_else(|| anyhow::anyhow!("Failed to get version"))?, + bytes.get(cursor..cursor + 8).ok_or_else(|| { + anyhow::anyhow!("Failed to get version.patch, insufficient bytes") + })?, ); - prelude_idx += 8; + cursor += 8; let patch = u64::from_le_bytes(patch); - let mut pre_release_length: [u8; 2] = [0; 2]; - pre_release_length.copy_from_slice( - bytes - .get(prelude_idx..prelude_idx + 2) - .ok_or_else(|| anyhow::anyhow!("Failed to get Prerelease length"))?, - ); - prelude_idx += 2; - let pre_release_length = usize::from(u16::from_le_bytes(pre_release_length)); + let mut pre_release_size: [u8; 2] = [0; 2]; + pre_release_size.copy_from_slice(bytes.get(cursor..cursor + 2).ok_or_else(|| { + anyhow::anyhow!("Failed to get size of version.pre, insufficient bytes") + })?); + cursor += 2; + let pre_release_size = usize::from(u16::from_le_bytes(pre_release_size)); - let pre_release = if pre_release_length == 0 { + let pre_release = if pre_release_size == 0 { semver::Prerelease::EMPTY } else { - let text = core::str::from_utf8( - bytes - .get(prelude_idx..prelude_idx + pre_release_length) - .ok_or_else(|| anyhow::anyhow!("Failed to get Prerelease"))?, - )?; - prelude_idx += pre_release_length; + let text = + core::str::from_utf8(bytes.get(cursor..cursor + pre_release_size).ok_or_else( + || anyhow::anyhow!("Failed to get version.pre, insufficient bytes"), + )?)?; + cursor += pre_release_size; semver::Prerelease::new(text)? }; - let mut build_metadata_length: [u8; 2] = [0; 2]; - build_metadata_length.copy_from_slice( - bytes - .get(prelude_idx..prelude_idx + 2) - .ok_or_else(|| anyhow::anyhow!("Failed to get BuildMetadata length"))?, - ); - prelude_idx += 2; - let build_metadata_length = usize::from(u16::from_le_bytes(build_metadata_length)); + let mut build_metadata_size: [u8; 2] = [0; 2]; + build_metadata_size.copy_from_slice(bytes.get(cursor..cursor + 2).ok_or_else(|| { + anyhow::anyhow!("Failed to get size of version.build, insufficient bytes") + })?); + cursor += 2; + let build_metadata_size = usize::from(u16::from_le_bytes(build_metadata_size)); - let build_metadata = if build_metadata_length == 0 { + let build_metadata = if build_metadata_size == 0 { semver::BuildMetadata::EMPTY } else { - let text = core::str::from_utf8( - bytes - .get(prelude_idx..prelude_idx + build_metadata_length) - .ok_or_else(|| anyhow::anyhow!("Failed to get BuildMetadata"))?, - )?; - prelude_idx += build_metadata_length; + let text = + core::str::from_utf8(bytes.get(cursor..cursor + build_metadata_size).ok_or_else( + || anyhow::anyhow!("Failed to get version.build, insufficient bytes"), + )?)?; + cursor += build_metadata_size; semver::BuildMetadata::new(text)? }; @@ -167,24 +159,25 @@ impl FileFormatLoader for RnoteFile { let mut header_size: [u8; 4] = [0; 4]; header_size.copy_from_slice( bytes - .get(prelude_idx..prelude_idx + 4) - .ok_or_else(|| anyhow::anyhow!("Failed to get header size"))?, + .get(cursor..cursor + 4) + .ok_or_else(|| anyhow::anyhow!("Failed to get header size, insufficient bytes"))?, ); - prelude_idx += 4; + cursor += 4; let header_size = usize::try_from(u32::from_le_bytes(header_size)) - .context("Rnote file header is too large")?; + .context("Serialized RnoteHeader exceeds max size (usize::MAX)")?; let header_slice = bytes - .get(prelude_idx..prelude_idx + header_size) - .ok_or_else(|| anyhow::anyhow!("File header missing"))?; - prelude_idx += header_size; + .get(cursor..cursor + header_size) + .ok_or_else(|| anyhow::anyhow!("Failed to get RnoteHeader, insufficient bytes"))?; + cursor += header_size; let body_slice = bytes - .get(prelude_idx..) - .ok_or_else(|| anyhow::anyhow!("File body missing"))?; + .get(cursor..) + .ok_or_else(|| anyhow::anyhow!("Failed to get body, insufficient bytes"))?; Ok(Self { - head: RnoteHeader::load_from_slice(header_slice, &version)?, + header: RnoteHeader::load_from_slice(header_slice, &version)?, + // faster than draining bytes body: body_slice.to_vec(), }) } @@ -198,10 +191,7 @@ impl RnoteHeader { { Ok(ijson::from_value(&serde_json::from_slice(slice)?)?) } else { - Err(anyhow::anyhow!( - "Unrecognized version specifier '{}' for header", - version - )) + Err(anyhow::anyhow!("Unsupported version: '{}'", version)) } } } @@ -210,15 +200,16 @@ impl TryFrom for EngineSnapshot { type Error = anyhow::Error; fn try_from(value: RnoteFile) -> Result { - let uc_size = usize::try_from(value.head.uc_size).unwrap_or(usize::MAX); - let uc_body = value.head.compression.decompress(uc_size, value.body)?; - let mut engine_snapshot = value.head.serialization.deserialize(&uc_body)?; + let uc_size = usize::try_from(value.header.uc_size).unwrap_or(usize::MAX); + let uc_body = value.header.compression.decompress(uc_size, value.body)?; + let mut engine_snapshot = value.header.serialization.deserialize(&uc_body)?; // save preferences are only kept if method_lock is true or both the ser. method and comp. method are "defaults" - let save_prefs = SavePrefs::from(value.head); + let save_prefs = SavePrefs::from(value.header); if save_prefs.method_lock | save_prefs.conforms_to_default() { engine_snapshot.save_prefs = save_prefs; } + Ok(engine_snapshot) } } @@ -238,7 +229,7 @@ impl TryFrom<&EngineSnapshot> for RnoteFile { let method_lock = save_prefs.method_lock; Ok(Self { - head: RnoteHeader { + header: RnoteHeader { compression, serialization, uc_size, From 4bca7e6256db6e9d71a7fd42efc1122ed33589b2 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:04:13 +0200 Subject: [PATCH 29/36] wasn't a need to skip serializing the SavePrefs when ser. the engine as it is only used for debug purposes --- crates/rnote-engine/src/engine/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs index da1c603fe8..ef094fb072 100644 --- a/crates/rnote-engine/src/engine/mod.rs +++ b/crates/rnote-engine/src/engine/mod.rs @@ -172,7 +172,7 @@ pub struct Engine { pub import_prefs: ImportPrefs, #[serde(rename = "export_prefs")] pub export_prefs: ExportPrefs, - #[serde(skip)] + #[serde(rename = "save_prefs")] pub save_prefs: SavePrefs, #[serde(rename = "pen_sounds")] pen_sounds: bool, From fcb116fd2cbfbf34564bb206e367c10fb43aa4b0 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Mon, 9 Sep 2024 19:16:17 +0200 Subject: [PATCH 30/36] misc, minor changes --- crates/rnote-cli/src/cli.rs | 8 ++--- .../src/fileformats/rnoteformat/methods.rs | 4 +-- crates/rnote-engine/src/meson.build | 1 + crates/rnote-engine/src/utils.rs | 34 +++++++++---------- 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/crates/rnote-cli/src/cli.rs b/crates/rnote-cli/src/cli.rs index cc1d750385..ead7f3521c 100644 --- a/crates/rnote-cli/src/cli.rs +++ b/crates/rnote-cli/src/cli.rs @@ -76,15 +76,13 @@ pub(crate) enum Command { Mutate { /// The rnote save file(s) to mutate rnote_files: Vec, - /// Keeps the original rnote save file(s) + /// Keep the original rnote save file(s) #[arg(long = "not-in-place", alias = "nip", action = clap::ArgAction::SetTrue)] not_in_place: bool, - /// Locks the compression and serialization methods used by the rnote save file(s) - /// Useful if either the desired serialization or compression methods differ from the defaults - /// Note that the compression level is not taken into account when comparing with the default methods + /// 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, - /// Unlocks the compression and serialization methods used by the rnote save file(s) + /// 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))] diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs index f43197a74e..a6daafacdf 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs @@ -1,11 +1,11 @@ +// Imports +use crate::engine::EngineSnapshot; use serde::{Deserialize, Serialize}; use std::{ io::{Read, Write}, str::FromStr, }; -use crate::engine::EngineSnapshot; - /// Compression methods that can be applied to the serialized engine snapshot #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum CompressionMethod { diff --git a/crates/rnote-engine/src/meson.build b/crates/rnote-engine/src/meson.build index 02658f34fc..b0f7c437b5 100644 --- a/crates/rnote-engine/src/meson.build +++ b/crates/rnote-engine/src/meson.build @@ -7,6 +7,7 @@ rnote_engine_sources = files( 'engine/import.rs', 'engine/mod.rs', 'engine/rendering.rs', + 'engine/save.rs', 'engine/snapshot.rs', 'engine/strokecontent.rs', 'engine/visual_debug.rs', diff --git a/crates/rnote-engine/src/utils.rs b/crates/rnote-engine/src/utils.rs index 3b7b0d576c..4ffc5d4d10 100644 --- a/crates/rnote-engine/src/utils.rs +++ b/crates/rnote-engine/src/utils.rs @@ -99,49 +99,48 @@ pub mod glib_bytes_base64 { } } -pub async fn atomic_save_to_file + std::fmt::Debug>( - filepath: Q, - bytes: &[u8], -) -> anyhow::Result<()> { +pub async fn atomic_save_to_file(filepath: Q, bytes: &[u8]) -> anyhow::Result<()> +where + Q: AsRef, +{ let filepath = filepath.as_ref().to_owned(); // checks that the extension is not already 'tmp' if filepath .extension() - .ok_or_else(|| anyhow::anyhow!("Missing file extension"))? + .ok_or_else(|| anyhow::anyhow!("Specified filepath does not have an extension"))? .to_str() - .ok_or_else(|| anyhow::anyhow!("Invalid extension encoding"))? + .ok_or_else(|| anyhow::anyhow!("The extension of the specified filepath is invalid"))? == "tmp" { - Err(anyhow::anyhow!("File extension cannot be 'tmp'"))?; + Err(anyhow::anyhow!("The extension of the file cannot be 'tmp'"))?; } - let mut tmp_filepath = filepath.clone(); - tmp_filepath.set_extension("tmp"); + let tmp_filepath = filepath.with_extension("tmp"); let file_write_operation = async { let mut write_file = async_fs::OpenOptions::new() .create(true) - .truncate(true) .write(true) + .truncate(true) .open(&tmp_filepath) .await .with_context(|| { format!( - "Failed to create/open/truncate tmp file for path '{}'", + "Failed to create/open/truncate tmp file with path '{}'", tmp_filepath.display() ) })?; write_file.write_all(bytes).await.with_context(|| { format!( - "Failed to write bytes to tmp file with path '{}'", + "Failed to write to tmp file with path '{}'", tmp_filepath.display() ) })?; write_file.sync_all().await.with_context(|| { format!( - "Failed to sync tmp file after writing with path '{}'", - &tmp_filepath.display() + "Failed to sync tmp file with path '{}'", + tmp_filepath.display() ) })?; @@ -150,7 +149,6 @@ pub async fn atomic_save_to_file + std::fmt::Debug>( file_write_operation.await?; let file_check_operation = async { - let size = bytes.len(); let internal_checksum = crc32fast::hash(bytes); let mut read_file = async_fs::OpenOptions::new() @@ -159,11 +157,11 @@ pub async fn atomic_save_to_file + std::fmt::Debug>( .await .with_context(|| { format!( - "Failed to open/read tmp file for path '{}'", + "Failed to open/read tmp file with path '{}'", &tmp_filepath.display() ) })?; - let mut data: Vec = Vec::with_capacity(size); + let mut data: Vec = Vec::with_capacity(bytes.len()); read_file.read_to_end(&mut data).await?; let external_checksum = crc32fast::hash(&data); @@ -180,7 +178,7 @@ pub async fn atomic_save_to_file + std::fmt::Debug>( let file_swap_operation = async { async_fs::rename(&tmp_filepath, &filepath) .await - .context("Failed to rename the temporary file into the main one")?; + .context("Failed to rename the temporary file into the original one")?; Ok::<(), anyhow::Error>(()) }; From f3b5c9317a6e224b99d101f6cdcaf96223240271 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:27:40 +0200 Subject: [PATCH 31/36] missed closing delim --- Cargo.lock | 2 +- crates/rnote-ui/src/settingspanel/mod.rs | 39 ++++++++++++++++-------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index efcf0d049e..25a21154f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,7 +386,7 @@ checksum = "a539389a13af092cd345a2b47ae7dec12deb306d660b2223d25cd3419b253ebe" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] diff --git a/crates/rnote-ui/src/settingspanel/mod.rs b/crates/rnote-ui/src/settingspanel/mod.rs index 16da3f75a6..7b97473c73 100644 --- a/crates/rnote-ui/src/settingspanel/mod.rs +++ b/crates/rnote-ui/src/settingspanel/mod.rs @@ -946,20 +946,33 @@ impl RnSettingsPanel { widget_flags.refresh_ui = true; widget_flags.store_modified = true; appwindow.handle_widget_flags(widget_flags, &canvas); - }), - ); + } + )); - imp.doc_compression_level_row.get().connect_selected_item_notify(clone!( - #[weak(rename_to=settings_panel] - self, - #[weak] - appwindow, - move |_| { - let canvas = appwindow.active_tab_wrapper().canvas(); - let compression_level = CompressionLevel::try_from(settings_panel.imp().doc_compression_level_row.get().selected()).unwrap(); - canvas.engine_mut().save_prefs.compression.set_compression_level(compression_level); - }), - ); + imp.doc_compression_level_row + .get() + .connect_selected_item_notify(clone!( + #[weak(rename_to=settings_panel)] + self, + #[weak] + appwindow, + move |_| { + let canvas = appwindow.active_tab_wrapper().canvas(); + let compression_level = CompressionLevel::try_from( + settings_panel + .imp() + .doc_compression_level_row + .get() + .selected(), + ) + .unwrap(); + canvas + .engine_mut() + .save_prefs + .compression + .set_compression_level(compression_level); + } + )); } fn setup_shortcuts(&self, appwindow: &RnAppWindow) { From d526e962ea2021f0c510081072192cea2c5c153c Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Sun, 10 Nov 2024 00:21:54 +0100 Subject: [PATCH 32/36] small fix --- Cargo.lock | 2 +- crates/rnote-ui/src/settingspanel/mod.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2452cf2bbe..12609b4175 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -395,7 +395,7 @@ checksum = "a539389a13af092cd345a2b47ae7dec12deb306d660b2223d25cd3419b253ebe" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] diff --git a/crates/rnote-ui/src/settingspanel/mod.rs b/crates/rnote-ui/src/settingspanel/mod.rs index 0753d1ab99..e3d1a464cb 100644 --- a/crates/rnote-ui/src/settingspanel/mod.rs +++ b/crates/rnote-ui/src/settingspanel/mod.rs @@ -973,7 +973,9 @@ impl RnSettingsPanel { #[weak] appwindow, move |_| { - let canvas = appwindow.active_tab_wrapper().canvas(); + let Some(canvas) = appwindow.active_tab_canvas() else { + return; + }; let compression_level = CompressionLevel::try_from( settings_panel .imp() From fdb675320d83e11bc8f40536a5e15f39da92a2fb Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:16:43 +0100 Subject: [PATCH 33/36] moved the prelude handling logic to it's own file, improved documentation --- Cargo.lock | 2 +- crates/rnote-engine/src/engine/snapshot.rs | 3 +- .../src/fileformats/rnoteformat/maj0min12.rs | 6 +- .../src/fileformats/rnoteformat/mod.rs | 170 ++++-------------- .../src/fileformats/rnoteformat/prelude.rs | 163 +++++++++++++++++ crates/rnote-engine/src/meson.build | 1 + 6 files changed, 200 insertions(+), 145 deletions(-) create mode 100644 crates/rnote-engine/src/fileformats/rnoteformat/prelude.rs diff --git a/Cargo.lock b/Cargo.lock index 67e626fc6c..32c442c411 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -389,7 +389,7 @@ checksum = "a539389a13af092cd345a2b47ae7dec12deb306d660b2223d25cd3419b253ebe" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] diff --git a/crates/rnote-engine/src/engine/snapshot.rs b/crates/rnote-engine/src/engine/snapshot.rs index 3c9eba666f..0c6cbf0b7c 100644 --- a/crates/rnote-engine/src/engine/snapshot.rs +++ b/crates/rnote-engine/src/engine/snapshot.rs @@ -55,8 +55,7 @@ impl EngineSnapshot { rayon::spawn(move || { let result = || -> anyhow::Result { - // support for legacy files - // gzip magic number + // Efficient support for legacy rnote files, by checking the existence of the gzip magic number at the start of the file, avoids the costly try_from conversion. if bytes .get(..2) .ok_or_else(|| anyhow::anyhow!("Not an Rnote file"))? diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs index 31bc3ea6ce..c46f4c2f23 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs @@ -23,9 +23,10 @@ use serde::{Deserialize, Serialize}; /// the body contains the serialized and (potentially) compressed engine snapshot #[derive(Debug, Clone)] pub struct RnoteFileMaj0Min12 { - /// called header and not head because head = prelude + header + /// The file's head is composed of the prelude plus the header (below). + /// Contains the necessary information to efficiently compress/decompress, serialize/deserialize the rnote file. pub header: RnoteHeaderMaj0Min12, - /// A serialized and (potentially) compressed engine snapshot + /// The serialized and (potentially) compressed engine snapshot. pub body: Vec, } @@ -47,6 +48,7 @@ pub struct RnoteHeaderMaj0Min12 { impl TryFrom for RnoteFileMaj0Min12 { type Error = anyhow::Error; + /// Inefficient conversion, as the legacy struct stores the ijson EngineSnapshot and not the compressed and serialized bytes, thankfully bypassed in EngineSnapshot::load_from_rnote_bytes. Therefore in general this would only be used by rnote-cli mutate. fn try_from(value: RnoteFileMaj0Min9) -> Result { Ok(Self { header: RnoteHeaderMaj0Min12 { diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index 535e21610e..d5f9040b9b 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -9,6 +9,7 @@ pub(crate) mod legacy; pub(crate) mod maj0min12; pub(crate) mod methods; +pub(crate) mod prelude; // Re-exports pub use methods::{CompressionMethod, SerializationMethod}; @@ -16,49 +17,25 @@ pub use methods::{CompressionMethod, SerializationMethod}; // Imports use super::{FileFormatLoader, FileFormatSaver}; use crate::engine::{save::SavePrefs, EngineSnapshot}; -use anyhow::Context; use legacy::LegacyRnoteFile; use maj0min12::RnoteFileMaj0Min12; +use prelude::{Prelude, PreludeError}; pub type RnoteFile = maj0min12::RnoteFileMaj0Min12; pub type RnoteHeader = maj0min12::RnoteHeaderMaj0Min12; impl RnoteFileMaj0Min12 { - // ideally, this should never change - pub const MAGIC_NUMBER: [u8; 9] = [0x52, 0x4e, 0x4f, 0x54, 0x45, 0xce, 0xa6, 0xce, 0x9b]; pub const SEMVER: &'static str = crate::utils::crate_version(); } impl FileFormatSaver for RnoteFile { fn save_as_bytes(&self, _file_name: &str) -> anyhow::Result> { let version = semver::Version::parse(Self::SEMVER)?; - let pre_release = version.pre.as_str(); - let build_metadata = version.build.as_str(); - let header = serde_json::to_vec(&ijson::to_value(&self.header)?)?; - let head = [ - &Self::MAGIC_NUMBER[..], - &version.major.to_le_bytes(), - &version.minor.to_le_bytes(), - &version.patch.to_le_bytes(), - &u16::try_from(pre_release.len()) - .context("Prerelease exceeds max size (u16::MAX)")? - .to_le_bytes(), - pre_release.as_bytes(), - &u16::try_from(build_metadata.len()) - .context("BuildMetadata exceeds max size (u16::MAX)")? - .to_le_bytes(), - build_metadata.as_bytes(), - &u32::try_from(header.len()) - .context("Serialized RnoteHeader exceeds max size (u32::MAX)")? - .to_le_bytes(), - &header, - ] - .concat(); - - // .concat is absurdly fast - // much faster than Vec::apend or Vec::extend - Ok([head.as_slice(), self.body.as_slice()].concat()) + let prelude = Prelude::new(version, header.len()).try_to_bytes()?; + + // From testing, using ".concat" seems to be the best choice, it's much faster than Vec::apend or Vec::extend. + Ok([prelude.as_slice(), header.as_slice(), self.body.as_slice()].concat()) } } @@ -67,119 +44,32 @@ impl FileFormatLoader for RnoteFile { where Self: Sized, { - let mut cursor: usize = 0; - - let magic_number = bytes - .get(cursor..9) - .ok_or_else(|| anyhow::anyhow!("Failed to get magic number"))?; - cursor += 9; - - if magic_number != Self::MAGIC_NUMBER { - // Gzip magic number - // The legacy file is generally caught first in Snapshot::load_from_rnote_bytes - // howver this less efficient catch is necessary for rnote-cli mutate - if magic_number[..2] == [0x1f, 0x8b] { - return RnoteFile::try_from(LegacyRnoteFile::load_from_bytes(bytes)?); - } else { - return Err(anyhow::anyhow!("Unrecognized file format")); + match Prelude::try_from_bytes(bytes) { + Ok((prelude, mut cursor)) => { + let header_slice = + bytes + .get(cursor..cursor + prelude.header_size) + .ok_or_else(|| { + anyhow::anyhow!("Failed to get RnoteHeader, insufficient bytes") + })?; + cursor += prelude.header_size; + + let body_slice = bytes + .get(cursor..) + .ok_or_else(|| anyhow::anyhow!("Failed to get body, insufficient bytes"))?; + + Ok(Self { + header: RnoteHeader::load_from_slice(header_slice, &prelude.version)?, + body: body_slice.to_vec(), + }) } + Err(error) => match error.downcast_ref::() { + Some(PreludeError::LegacyRnoteFile) => { + RnoteFile::try_from(LegacyRnoteFile::load_from_bytes(bytes)?) + } + None => Err(error), + }, } - - let mut major: [u8; 8] = [0; 8]; - major.copy_from_slice( - bytes.get(cursor..cursor + 8).ok_or_else(|| { - anyhow::anyhow!("Failed to get version.major, insufficient bytes") - })?, - ); - cursor += 8; - let major = u64::from_le_bytes(major); - - let mut minor: [u8; 8] = [0; 8]; - minor.copy_from_slice( - bytes.get(cursor..cursor + 8).ok_or_else(|| { - anyhow::anyhow!("Failed to get version.minor, insufficient bytes") - })?, - ); - cursor += 8; - let minor = u64::from_le_bytes(minor); - - let mut patch: [u8; 8] = [0; 8]; - patch.copy_from_slice( - bytes.get(cursor..cursor + 8).ok_or_else(|| { - anyhow::anyhow!("Failed to get version.patch, insufficient bytes") - })?, - ); - cursor += 8; - let patch = u64::from_le_bytes(patch); - - let mut pre_release_size: [u8; 2] = [0; 2]; - pre_release_size.copy_from_slice(bytes.get(cursor..cursor + 2).ok_or_else(|| { - anyhow::anyhow!("Failed to get size of version.pre, insufficient bytes") - })?); - cursor += 2; - let pre_release_size = usize::from(u16::from_le_bytes(pre_release_size)); - - let pre_release = if pre_release_size == 0 { - semver::Prerelease::EMPTY - } else { - let text = - core::str::from_utf8(bytes.get(cursor..cursor + pre_release_size).ok_or_else( - || anyhow::anyhow!("Failed to get version.pre, insufficient bytes"), - )?)?; - cursor += pre_release_size; - semver::Prerelease::new(text)? - }; - - let mut build_metadata_size: [u8; 2] = [0; 2]; - build_metadata_size.copy_from_slice(bytes.get(cursor..cursor + 2).ok_or_else(|| { - anyhow::anyhow!("Failed to get size of version.build, insufficient bytes") - })?); - cursor += 2; - let build_metadata_size = usize::from(u16::from_le_bytes(build_metadata_size)); - - let build_metadata = if build_metadata_size == 0 { - semver::BuildMetadata::EMPTY - } else { - let text = - core::str::from_utf8(bytes.get(cursor..cursor + build_metadata_size).ok_or_else( - || anyhow::anyhow!("Failed to get version.build, insufficient bytes"), - )?)?; - cursor += build_metadata_size; - semver::BuildMetadata::new(text)? - }; - - let version = semver::Version { - major, - minor, - patch, - pre: pre_release, - build: build_metadata, - }; - - let mut header_size: [u8; 4] = [0; 4]; - header_size.copy_from_slice( - bytes - .get(cursor..cursor + 4) - .ok_or_else(|| anyhow::anyhow!("Failed to get header size, insufficient bytes"))?, - ); - cursor += 4; - let header_size = usize::try_from(u32::from_le_bytes(header_size)) - .context("Serialized RnoteHeader exceeds max size (usize::MAX)")?; - - let header_slice = bytes - .get(cursor..cursor + header_size) - .ok_or_else(|| anyhow::anyhow!("Failed to get RnoteHeader, insufficient bytes"))?; - cursor += header_size; - - let body_slice = bytes - .get(cursor..) - .ok_or_else(|| anyhow::anyhow!("Failed to get body, insufficient bytes"))?; - - Ok(Self { - header: RnoteHeader::load_from_slice(header_slice, &version)?, - // faster than draining bytes - body: body_slice.to_vec(), - }) } } diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/prelude.rs b/crates/rnote-engine/src/fileformats/rnoteformat/prelude.rs new file mode 100644 index 0000000000..043614fe2d --- /dev/null +++ b/crates/rnote-engine/src/fileformats/rnoteformat/prelude.rs @@ -0,0 +1,163 @@ +// Imports +use anyhow::{anyhow, bail, Context}; +use thiserror::Error; + +/// # Prelude +/// * magic number: [u8; 9] = [0x52, 0x4e, 0x4f, 0x54, 0x45, 0xce, 0xa6, 0xce, 0x9b], "RNOTEϕλ" +/// * version: [u64, u64, u64, u16, str, u16, str] (almost one-to-one representation of semver::Version) +/// [major, minor, patch, Prerelease size, Prerelease, BuildMetadata size, Buildmetadata] +/// * header size: u32 +#[derive(Debug, Clone)] +pub struct Prelude { + pub version: semver::Version, + pub header_size: usize, +} + +impl Prelude { + /// The magic number used to identify rnote files, do not modify. + pub const MAGIC_NUMBER: [u8; 9] = [0x52, 0x4e, 0x4f, 0x54, 0x45, 0xce, 0xa6, 0xce, 0x9b]; + + /// Creates a new prelude. + pub fn new(version: semver::Version, header_size: usize) -> Self { + Self { + version, + header_size, + } + } + + /// Returns the byte representation of the prelude + pub fn try_to_bytes(self) -> anyhow::Result> { + let pre_release: &str = self.version.pre.as_str(); + let build_metadata: &str = self.version.build.as_str(); + + Ok([ + &Self::MAGIC_NUMBER[..], + &self.version.major.to_le_bytes(), + &self.version.minor.to_le_bytes(), + &self.version.patch.to_le_bytes(), + &u16::try_from(pre_release.len()) + .context("Prerelease exceeds maximum size (u16::MAX)")? + .to_le_bytes(), + pre_release.as_bytes(), + &u16::try_from(build_metadata.len()) + .context("BuildMetadata exceeds maximum size (u16::MAX)")? + .to_le_bytes(), + build_metadata.as_bytes(), + &u32::try_from(self.header_size) + .context("Serialized RnoteHeader exceeds maximum size (u32::MAX)")? + .to_le_bytes(), + ] + .concat()) + } + + /// Returns the prelude alongside the cursor which is the index at which it left off. + pub fn try_from_bytes(bytes: &[u8]) -> anyhow::Result<(Self, usize)> { + let mut cursor: usize = 0; + + let magic_number = bytes + .get(cursor..9) + .ok_or_else(|| anyhow!("Failed to get magic number"))?; + cursor += 9; + + if magic_number != Self::MAGIC_NUMBER { + // Checks for legacy files using the gzip magic number. + if magic_number[..2] == [0x1f, 0x8b] { + return Err(anyhow::Error::new(PreludeError::LegacyRnoteFile)); + } else { + bail!("Unrecognized file format"); + } + } + + let mut major: [u8; 8] = [0; 8]; + major.copy_from_slice( + bytes + .get(cursor..cursor + 8) + .ok_or_else(|| anyhow!("Failed to get version.major, insufficient bytes"))?, + ); + cursor += 8; + let major = u64::from_le_bytes(major); + + let mut minor: [u8; 8] = [0; 8]; + minor.copy_from_slice( + bytes + .get(cursor..cursor + 8) + .ok_or_else(|| anyhow!("Failed to get version.minor, insufficient bytes"))?, + ); + cursor += 8; + let minor = u64::from_le_bytes(minor); + + let mut patch: [u8; 8] = [0; 8]; + patch.copy_from_slice( + bytes + .get(cursor..cursor + 8) + .ok_or_else(|| anyhow!("Failed to get version.patch, insufficient bytes"))?, + ); + cursor += 8; + let patch = u64::from_le_bytes(patch); + + let mut pre_release_size: [u8; 2] = [0; 2]; + pre_release_size.copy_from_slice( + bytes + .get(cursor..cursor + 2) + .ok_or_else(|| anyhow!("Failed to get size of version.pre, insufficient bytes"))?, + ); + cursor += 2; + let pre_release = match usize::from(u16::from_le_bytes(pre_release_size)) { + 0 => semver::Prerelease::EMPTY, + len => { + let text = + core::str::from_utf8(bytes.get(cursor..cursor + len).ok_or_else(|| { + anyhow!("Failed to get version.pre, insufficient bytes") + })?)?; + cursor += len; + semver::Prerelease::new(text)? + } + }; + + let mut build_metadata_size: [u8; 2] = [0; 2]; + build_metadata_size.copy_from_slice( + bytes.get(cursor..cursor + 2).ok_or_else(|| { + anyhow!("Failed to get size of version.build, insufficient bytes") + })?, + ); + cursor += 2; + let build_metadata = match usize::from(u16::from_le_bytes(build_metadata_size)) { + 0 => semver::BuildMetadata::EMPTY, + len => { + let text = + core::str::from_utf8(bytes.get(cursor..cursor + len).ok_or_else(|| { + anyhow!("Failed to get version.build, insufficient bytes") + })?)?; + cursor += len; + semver::BuildMetadata::new(text)? + } + }; + + let version = semver::Version { + major, + minor, + patch, + pre: pre_release, + build: build_metadata, + }; + + let mut header_size: [u8; 4] = [0; 4]; + header_size.copy_from_slice( + bytes + .get(cursor..cursor + 4) + .ok_or_else(|| anyhow!("Failed to get header size, insufficient bytes"))?, + ); + cursor += 4; + let header_size = usize::try_from(u32::from_le_bytes(header_size)) + .context("Serialized RnoteHeader exceeds maximum size (usize::MAX)")?; + + Ok((Self::new(version, header_size), cursor)) + } +} + +/// Custom error used to handle legacy rnote files. +#[derive(Debug, Error)] +pub enum PreludeError { + #[error("")] + LegacyRnoteFile, +} diff --git a/crates/rnote-engine/src/meson.build b/crates/rnote-engine/src/meson.build index bb2f16db3f..9791b2636e 100644 --- a/crates/rnote-engine/src/meson.build +++ b/crates/rnote-engine/src/meson.build @@ -20,6 +20,7 @@ rnote_engine_sources = files( 'fileformats/rnoteformat/mod.rs', 'fileformats/rnoteformat/methods.rs', 'fileformats/rnoteformat/maj0min12.rs', + 'fileformats/rnoteformat/prelude.rs', 'fileformats/xoppformat.rs', 'pens/brush.rs', 'pens/eraser.rs', From 41ba8d2f34cfacc8953e48f5390bf6bf5d454d8a Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:16:09 +0100 Subject: [PATCH 34/36] separated methods.rs into compression.rs and serialization.rs, made CompressionLevel to be a re-export of Document instead of engine, improved documentation --- crates/rnote-engine/src/document/mod.rs | 1 + crates/rnote-engine/src/engine/mod.rs | 1 - crates/rnote-engine/src/engine/save.rs | 75 +---------- .../{methods.rs => compression.rs} | 120 +++++++++++------- .../src/fileformats/rnoteformat/maj0min12.rs | 4 +- .../src/fileformats/rnoteformat/mod.rs | 10 +- .../fileformats/rnoteformat/serialization.rs | 48 +++++++ crates/rnote-engine/src/meson.build | 3 +- crates/rnote-ui/src/settingspanel/mod.rs | 2 +- 9 files changed, 135 insertions(+), 129 deletions(-) rename crates/rnote-engine/src/fileformats/rnoteformat/{methods.rs => compression.rs} (62%) create mode 100644 crates/rnote-engine/src/fileformats/rnoteformat/serialization.rs diff --git a/crates/rnote-engine/src/document/mod.rs b/crates/rnote-engine/src/document/mod.rs index 53ccee1a58..32aa5d70c9 100644 --- a/crates/rnote-engine/src/document/mod.rs +++ b/crates/rnote-engine/src/document/mod.rs @@ -3,6 +3,7 @@ pub mod background; pub mod format; // Re-exports +pub use crate::fileformats::rnoteformat::compression::CompressionLevel; pub use background::Background; pub use format::Format; diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs index dd9fafc885..c5a44af358 100644 --- a/crates/rnote-engine/src/engine/mod.rs +++ b/crates/rnote-engine/src/engine/mod.rs @@ -12,7 +12,6 @@ pub use export::ExportPrefs; use futures::channel::mpsc::UnboundedReceiver; use futures::StreamExt; pub use import::ImportPrefs; -pub use save::CompressionLevel; pub use snapshot::EngineSnapshot; pub use strokecontent::StrokeContent; diff --git a/crates/rnote-engine/src/engine/save.rs b/crates/rnote-engine/src/engine/save.rs index 0253606dbc..c50ea3f638 100644 --- a/crates/rnote-engine/src/engine/save.rs +++ b/crates/rnote-engine/src/engine/save.rs @@ -1,7 +1,6 @@ // Imports use crate::fileformats::rnoteformat::{ - methods::{CompressionMethod, SerializationMethod}, - RnoteHeader, + compression::CompressionMethod, serialization::SerializationMethod, RnoteHeader, }; use serde::{Deserialize, Serialize}; use std::mem::discriminant; @@ -68,75 +67,3 @@ impl From for SavePrefs { } } } - -#[derive(Debug, Clone, Copy, num_derive::FromPrimitive, num_derive::ToPrimitive)] -pub enum CompressionLevel { - VeryHigh, - High, - Medium, - Low, - VeryLow, - None, -} - -impl TryFrom for CompressionLevel { - type Error = anyhow::Error; - - fn try_from(value: u32) -> Result { - num_traits::FromPrimitive::from_u32(value).ok_or_else(|| { - anyhow::anyhow!( - "CompressionLevel try_from::() for value {} failed", - value - ) - }) - } -} - -impl CompressionMethod { - pub fn get_compression_level(&self) -> CompressionLevel { - match self { - Self::None => CompressionLevel::None, - Self::Gzip(val) => match *val { - 0..=1 => CompressionLevel::VeryLow, - 2..=3 => CompressionLevel::Low, - 4..=5 => CompressionLevel::Medium, - 6..=7 => CompressionLevel::High, - 8..=9 => CompressionLevel::VeryHigh, - _ => unreachable!(), - }, - Self::Zstd(val) => match *val { - 0..=4 => CompressionLevel::VeryLow, - 5..=8 => CompressionLevel::Low, - 9..=12 => CompressionLevel::Medium, - 13..=16 => CompressionLevel::High, - 17..=22 => CompressionLevel::VeryHigh, - _ => unreachable!(), - }, - } - } - pub fn set_compression_level(&mut self, level: CompressionLevel) { - match self { - Self::None => (), - Self::Gzip(ref mut val) => { - *val = match level { - CompressionLevel::VeryHigh => 8, - CompressionLevel::High => 6, - CompressionLevel::Medium => 5, - CompressionLevel::Low => 3, - CompressionLevel::VeryLow => 1, - CompressionLevel::None => unreachable!(), - } - } - Self::Zstd(ref mut val) => { - *val = match level { - CompressionLevel::VeryHigh => 17, - CompressionLevel::High => 13, - CompressionLevel::Medium => 9, - CompressionLevel::Low => 5, - CompressionLevel::VeryLow => 1, - CompressionLevel::None => unreachable!(), - } - } - } - } -} diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs b/crates/rnote-engine/src/fileformats/rnoteformat/compression.rs similarity index 62% rename from crates/rnote-engine/src/fileformats/rnoteformat/methods.rs rename to crates/rnote-engine/src/fileformats/rnoteformat/compression.rs index a6daafacdf..68bfe84463 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/methods.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/compression.rs @@ -1,5 +1,4 @@ // Imports -use crate::engine::EngineSnapshot; use serde::{Deserialize, Serialize}; use std::{ io::{Read, Write}, @@ -18,16 +17,38 @@ pub enum CompressionMethod { Zstd(u8), } -/// Serialization methods that can be applied to a snapshot of the engine -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum SerializationMethod { - #[serde(rename = "bitcode")] - Bitcode, - #[serde(rename = "json")] - Json, +impl Default for CompressionMethod { + fn default() -> Self { + Self::Zstd(9) + } +} + +#[derive(Debug, Clone, Copy, num_derive::FromPrimitive, num_derive::ToPrimitive)] +pub enum CompressionLevel { + VeryHigh, + High, + Medium, + Low, + VeryLow, + None, +} + +impl TryFrom for CompressionLevel { + type Error = anyhow::Error; + + fn try_from(value: u32) -> Result { + num_traits::FromPrimitive::from_u32(value).ok_or_else(|| { + anyhow::anyhow!( + "CompressionLevel try_from::() for value {} failed", + value + ) + }) + } } impl CompressionMethod { + pub const VALID_STR_ARRAY: [&'static str; 6] = ["None", "none", "Gzip", "gzip", "Zstd", "zstd"]; + pub fn compress(&self, data: Vec) -> anyhow::Result> { match self { Self::None => Ok(data), @@ -95,12 +116,52 @@ impl CompressionMethod { } } } - pub const VALID_STR_ARRAY: [&'static str; 6] = ["None", "none", "Gzip", "gzip", "Zstd", "zstd"]; -} + pub fn get_compression_level(&self) -> CompressionLevel { + match self { + Self::None => CompressionLevel::None, + Self::Gzip(val) => match *val { + 0..=1 => CompressionLevel::VeryLow, + 2..=3 => CompressionLevel::Low, + 4..=5 => CompressionLevel::Medium, + 6..=7 => CompressionLevel::High, + 8..=9 => CompressionLevel::VeryHigh, + _ => unreachable!(), + }, + Self::Zstd(val) => match *val { + 0..=4 => CompressionLevel::VeryLow, + 5..=8 => CompressionLevel::Low, + 9..=12 => CompressionLevel::Medium, + 13..=16 => CompressionLevel::High, + 17..=22 => CompressionLevel::VeryHigh, + _ => unreachable!(), + }, + } + } -impl Default for CompressionMethod { - fn default() -> Self { - Self::Zstd(9) + pub fn set_compression_level(&mut self, level: CompressionLevel) { + match self { + Self::None => (), + Self::Gzip(ref mut val) => { + *val = match level { + CompressionLevel::VeryHigh => 8, + CompressionLevel::High => 6, + CompressionLevel::Medium => 5, + CompressionLevel::Low => 3, + CompressionLevel::VeryLow => 1, + CompressionLevel::None => unreachable!(), + } + } + Self::Zstd(ref mut val) => { + *val = match level { + CompressionLevel::VeryHigh => 17, + CompressionLevel::High => 13, + CompressionLevel::Medium => 9, + CompressionLevel::Low => 5, + CompressionLevel::VeryLow => 1, + CompressionLevel::None => unreachable!(), + } + } + } } } @@ -115,36 +176,3 @@ impl FromStr for CompressionMethod { } } } - -impl SerializationMethod { - pub fn serialize(&self, engine_snapshot: &EngineSnapshot) -> anyhow::Result> { - match self { - Self::Bitcode => Ok(bitcode::serialize(engine_snapshot)?), - Self::Json => Ok(serde_json::to_vec(&ijson::to_value(engine_snapshot)?)?), - } - } - pub fn deserialize(&self, data: &[u8]) -> anyhow::Result { - match self { - Self::Bitcode => Ok(bitcode::deserialize(data)?), - Self::Json => Ok(ijson::from_value(&serde_json::from_slice(data)?)?), - } - } - pub const VALID_STR_ARRAY: [&'static str; 5] = ["Bitcode", "bitcode", "Json", "JSON", "json"]; -} - -impl Default for SerializationMethod { - fn default() -> Self { - Self::Bitcode - } -} - -impl FromStr for SerializationMethod { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "Bitcode" | "bitcode" => Ok(Self::Bitcode), - "Json" | "JSON" | "json" => Ok(Self::Json), - _ => Err("Unknown serialization method"), - } - } -} diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs index c46f4c2f23..77ab9d1630 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min12.rs @@ -1,6 +1,6 @@ use super::{ - legacy::maj0min9::RnoteFileMaj0Min9, - methods::{CompressionMethod, SerializationMethod}, + compression::CompressionMethod, legacy::maj0min9::RnoteFileMaj0Min9, + serialization::SerializationMethod, }; use serde::{Deserialize, Serialize}; diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index d5f9040b9b..96d463c145 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -6,13 +6,15 @@ //! Then [TryFrom] can be implemented to allow conversions and chaining from older to newer versions. // Modules +pub(crate) mod compression; pub(crate) mod legacy; pub(crate) mod maj0min12; -pub(crate) mod methods; pub(crate) mod prelude; +pub(crate) mod serialization; // Re-exports -pub use methods::{CompressionMethod, SerializationMethod}; +pub use compression::CompressionMethod; +pub use serialization::SerializationMethod; // Imports use super::{FileFormatLoader, FileFormatSaver}; @@ -34,7 +36,7 @@ impl FileFormatSaver for RnoteFile { let header = serde_json::to_vec(&ijson::to_value(&self.header)?)?; let prelude = Prelude::new(version, header.len()).try_to_bytes()?; - // From testing, using ".concat" seems to be the best choice, it's much faster than Vec::apend or Vec::extend. + // From running simple tests, using ".concat" seems to be the best choice, it's much faster than Vec::apend or Vec::extend. Ok([prelude.as_slice(), header.as_slice(), self.body.as_slice()].concat()) } } @@ -94,7 +96,7 @@ impl TryFrom for EngineSnapshot { let uc_body = value.header.compression.decompress(uc_size, value.body)?; let mut engine_snapshot = value.header.serialization.deserialize(&uc_body)?; - // save preferences are only kept if method_lock is true or both the ser. method and comp. method are "defaults" + // Save preferences are only kept if method_lock is true or both the ser. method and comp. method are "defaults". let save_prefs = SavePrefs::from(value.header); if save_prefs.method_lock | save_prefs.conforms_to_default() { engine_snapshot.save_prefs = save_prefs; diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/serialization.rs b/crates/rnote-engine/src/fileformats/rnoteformat/serialization.rs new file mode 100644 index 0000000000..0b2f8c4e74 --- /dev/null +++ b/crates/rnote-engine/src/fileformats/rnoteformat/serialization.rs @@ -0,0 +1,48 @@ +// Imports +use crate::engine::EngineSnapshot; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +/// Serialization methods that can be applied to a snapshot of the engine +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum SerializationMethod { + #[serde(rename = "bitcode")] + Bitcode, + #[serde(rename = "json")] + Json, +} + +impl Default for SerializationMethod { + fn default() -> Self { + Self::Bitcode + } +} + +impl SerializationMethod { + pub const VALID_STR_ARRAY: [&'static str; 5] = ["Bitcode", "bitcode", "Json", "JSON", "json"]; + + pub fn serialize(&self, engine_snapshot: &EngineSnapshot) -> anyhow::Result> { + match self { + Self::Bitcode => Ok(bitcode::serialize(engine_snapshot)?), + Self::Json => Ok(serde_json::to_vec(&ijson::to_value(engine_snapshot)?)?), + } + } + + pub fn deserialize(&self, data: &[u8]) -> anyhow::Result { + match self { + Self::Bitcode => Ok(bitcode::deserialize(data)?), + Self::Json => Ok(ijson::from_value(&serde_json::from_slice(data)?)?), + } + } +} + +impl FromStr for SerializationMethod { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s { + "Bitcode" | "bitcode" => Ok(Self::Bitcode), + "Json" | "JSON" | "json" => Ok(Self::Json), + _ => Err("Unknown serialization method"), + } + } +} diff --git a/crates/rnote-engine/src/meson.build b/crates/rnote-engine/src/meson.build index 9791b2636e..08055c74b1 100644 --- a/crates/rnote-engine/src/meson.build +++ b/crates/rnote-engine/src/meson.build @@ -18,9 +18,10 @@ rnote_engine_sources = files( 'fileformats/rnoteformat/legacy/maj0min6.rs', 'fileformats/rnoteformat/legacy/maj0min9.rs', 'fileformats/rnoteformat/mod.rs', - 'fileformats/rnoteformat/methods.rs', + 'fileformats/rnoteformat/compression.rs', 'fileformats/rnoteformat/maj0min12.rs', 'fileformats/rnoteformat/prelude.rs', + 'fileformats/rnoteformat/serialization.rs', 'fileformats/xoppformat.rs', 'pens/brush.rs', 'pens/eraser.rs', diff --git a/crates/rnote-ui/src/settingspanel/mod.rs b/crates/rnote-ui/src/settingspanel/mod.rs index e3d1a464cb..714e8a8e60 100644 --- a/crates/rnote-ui/src/settingspanel/mod.rs +++ b/crates/rnote-ui/src/settingspanel/mod.rs @@ -19,8 +19,8 @@ use num_traits::ToPrimitive; use rnote_compose::penevent::ShortcutKey; use rnote_engine::document::background::PatternStyle; use rnote_engine::document::format::{self, Format, PredefinedFormat}; +use rnote_engine::document::CompressionLevel; use rnote_engine::document::Layout; -use rnote_engine::engine::CompressionLevel; use rnote_engine::ext::GdkRGBAExt; use std::cell::RefCell; From a65e87e70b26bce45986c2fcae3d52c078c78f33 Mon Sep 17 00:00:00 2001 From: anesthetice <118751106+anesthetice@users.noreply.github.com> Date: Mon, 10 Feb 2025 15:08:43 +0100 Subject: [PATCH 35/36] changed compression level defaults, changed SavePrefs a lot, put compression level into it's own file category in rnote-ui, and much more --- crates/rnote-cli/src/mutate.rs | 2 +- crates/rnote-engine/src/document/mod.rs | 1 - crates/rnote-engine/src/engine/export.rs | 2 +- crates/rnote-engine/src/engine/import.rs | 2 +- crates/rnote-engine/src/engine/mod.rs | 6 +- crates/rnote-engine/src/engine/save.rs | 134 +++++++++++++---- .../rnote-engine/src/engine/visual_debug.rs | 5 +- .../fileformats/rnoteformat/compression.rs | 141 +++++++++++------- .../src/fileformats/rnoteformat/mod.rs | 9 +- .../fileformats/rnoteformat/serialization.rs | 7 +- crates/rnote-ui/data/ui/settingspanel.ui | 8 +- crates/rnote-ui/src/canvas/imexport.rs | 2 + crates/rnote-ui/src/settingspanel/mod.rs | 70 ++++++--- 13 files changed, 263 insertions(+), 126 deletions(-) diff --git a/crates/rnote-cli/src/mutate.rs b/crates/rnote-cli/src/mutate.rs index 1e284e9c33..8db37b4f8d 100644 --- a/crates/rnote-cli/src/mutate.rs +++ b/crates/rnote-cli/src/mutate.rs @@ -55,7 +55,7 @@ pub(crate) async fn run_mutate( }; if let Some(lvl) = compression_level { - compression.update_compression_level(lvl)?; + compression.update_compression_integer(lvl)?; } let method_lock = (rnote_file.header.method_lock | lock) && !unlock; diff --git a/crates/rnote-engine/src/document/mod.rs b/crates/rnote-engine/src/document/mod.rs index 32aa5d70c9..53ccee1a58 100644 --- a/crates/rnote-engine/src/document/mod.rs +++ b/crates/rnote-engine/src/document/mod.rs @@ -3,7 +3,6 @@ pub mod background; pub mod format; // Re-exports -pub use crate::fileformats::rnoteformat::compression::CompressionLevel; pub use background::Background; pub use format::Format; diff --git a/crates/rnote-engine/src/engine/export.rs b/crates/rnote-engine/src/engine/export.rs index 2a4b55e4f1..8f89e2684d 100644 --- a/crates/rnote-engine/src/engine/export.rs +++ b/crates/rnote-engine/src/engine/export.rs @@ -351,7 +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_conformed_config(), + save_prefs: self.save_prefs.to_valid_engine_to_engineconfig(), pen_sounds: self.pen_sounds(), optimize_epd: self.optimize_epd(), } diff --git a/crates/rnote-engine/src/engine/import.rs b/crates/rnote-engine/src/engine/import.rs index 60e8990682..e749f7c10b 100644 --- a/crates/rnote-engine/src/engine/import.rs +++ b/crates/rnote-engine/src/engine/import.rs @@ -162,7 +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; + self.save_prefs = engine_config.save_prefs.to_valid_engineconfig_to_engine(); // Set the pen sounds to update the audioplayer self.set_pen_sounds(engine_config.pen_sounds, data_dir); diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs index c5a44af358..3b17e6476b 100644 --- a/crates/rnote-engine/src/engine/mod.rs +++ b/crates/rnote-engine/src/engine/mod.rs @@ -12,6 +12,7 @@ pub use export::ExportPrefs; use futures::channel::mpsc::UnboundedReceiver; use futures::StreamExt; pub use import::ImportPrefs; +pub use save::SavePrefs; pub use snapshot::EngineSnapshot; pub use strokecontent::StrokeContent; @@ -31,7 +32,6 @@ use rnote_compose::eventresult::EventPropagation; use rnote_compose::ext::AabbExt; use rnote_compose::penevent::{PenEvent, ShortcutKey}; use rnote_compose::{Color, SplitOrder}; -use save::SavePrefs; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::sync::Arc; @@ -382,7 +382,7 @@ 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_config(), + save_prefs: self.save_prefs.clone(), } } @@ -390,7 +390,7 @@ impl Engine { pub fn load_snapshot(&mut self, snapshot: EngineSnapshot) -> WidgetFlags { self.document = snapshot.document.clone_config(); self.camera = snapshot.camera.clone_config(); - self.save_prefs = snapshot.save_prefs.clone_config(); + self.save_prefs.merge(&snapshot.save_prefs); let mut widget_flags = self.store.import_from_snapshot(&snapshot) | self.doc_resize_autoexpand() diff --git a/crates/rnote-engine/src/engine/save.rs b/crates/rnote-engine/src/engine/save.rs index c50ea3f638..f337488e25 100644 --- a/crates/rnote-engine/src/engine/save.rs +++ b/crates/rnote-engine/src/engine/save.rs @@ -3,57 +3,124 @@ use crate::fileformats::rnoteformat::{ compression::CompressionMethod, serialization::SerializationMethod, RnoteHeader, }; use serde::{Deserialize, Serialize}; -use std::mem::discriminant; -/// Rnote file save preferences, a subset of RnoteHeader -/// used by EngineSnapshot, Engine, and EngineConfig -/// -/// when loading in an Rnote file, SavePrefs will be created from RnoteHeader -/// if RnoteHeader's serialization and compression methods conform to the defaults, or method_lock is set to true -/// => SavePrefs override EngineSnapshot's default SavePrefs -/// => SavePrefs transferred from EngineSnapshot to Engine -/// -/// as for EngineConfig, if and only if Engine's SavePrefs conform to the defaults -/// => SavePrefs cloned from Engine into EngineConfig -/// -/// please note that the compression level is not used to check whether or not the methods conform to the defaults -#[derive(Debug, Clone, Serialize, Deserialize)] +// Re-exports +pub use crate::fileformats::rnoteformat::compression::CompressionLevel; + +/// The SavePrefs struct is similar to RnoteHeader, it is used by Engine, EngineSnapshot, and EngineConfig. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(default, rename = "save_prefs")] pub struct SavePrefs { #[serde(rename = "serialization")] pub serialization: SerializationMethod, #[serde(rename = "compression")] pub compression: CompressionMethod, - #[serde(rename = "method_lock")] + #[serde(skip)] pub method_lock: bool, + #[serde(skip)] + pub on_next_save: Option<(SerializationMethod, CompressionMethod)>, } impl SavePrefs { - pub fn clone_config(&self) -> Self { - self.clone() + fn new( + serialization: SerializationMethod, + compression: CompressionMethod, + method_lock: bool, + on_next_save: Option<(SerializationMethod, CompressionMethod)>, + ) -> Self { + Self { + serialization, + compression, + method_lock, + on_next_save, + } } - pub fn conforms_to_default(&self) -> bool { - discriminant(&self.serialization) == discriminant(&SerializationMethod::default()) - && discriminant(&self.compression) == discriminant(&CompressionMethod::default()) + + fn new_skeleton(serialization: SerializationMethod, compression: CompressionMethod) -> Self { + Self { + serialization, + compression, + method_lock: false, + on_next_save: None, + } } - /// The EngineExport should only contain SavePrefs that conform to the default - /// otherwise for example, after having opened an uncompressed and JSON-encoded Rnote - /// save file while debugging, all new save files would be using the same methods - pub fn clone_conformed_config(&self) -> Self { - if self.conforms_to_default() { - self.clone_config() + + /// Checks that serialiazation and compression are default methods. + #[rustfmt::skip] + pub fn uses_default_methods(&self) -> bool { + self.serialization.is_similar_to(&SerializationMethod::default()) + && self.compression.is_similar_to(&CompressionMethod::default()) + } + + /// Used to export the SavePrefs of the Engine to EngineSnapshot + pub fn to_engine_to_enginesnapshot(&self) -> Self { + match self.on_next_save { + Some((serialization, compression)) => { + Self::new(serialization, compression, self.method_lock, None) + } + None => self.clone(), + } + } + + /// Used to load the SavePrefs of the EngineConfig into the Engine. + pub fn to_valid_engineconfig_to_engine(&self) -> Self { + if self.uses_default_methods() { + Self::new_skeleton(self.serialization, self.compression) } else { Self::default() } } -} -impl Default for SavePrefs { - fn default() -> Self { - Self { - serialization: SerializationMethod::default(), - compression: CompressionMethod::default(), - method_lock: false, + /// Used to export the SavePrefs of the Engine to EngineConfig. + pub fn to_valid_engine_to_engineconfig(&self) -> Self { + match (self.uses_default_methods(), self.on_next_save) { + (true | false, Some((serialization, compression))) => { + Self::new_skeleton(serialization, compression) + } + (true, None) => self.clone(), + (false, None) => Self::default(), + } + } + + /// Used by engine to merge the incoming SavePrefs of a file to its current SavePrefs. + pub fn merge(&mut self, new: &Self) { + let on_next_save = match (new.uses_default_methods(), new.method_lock) { + (true, true) | (true, false) | (false, true) => None, + (false, false) => Some((self.serialization, self.compression)), + }; + println!("pre-merge: {:?}", self); + self.serialization = new.serialization; + self.compression = new.compression; + self.method_lock = new.method_lock; + self.on_next_save = on_next_save; + println!("post-merge: {:?}", self); + } + + pub fn finished_saving(&mut self) { + if let Some((serialization, compression)) = self.on_next_save { + self.serialization = serialization; + self.compression = compression; + self.on_next_save = None; + } + } + + #[rustfmt::skip] + pub fn update_compression_level(&mut self, level: CompressionLevel) { + let file_level = self.compression.get_compression_level(); + if !file_level.eq(&level) { + match self.on_next_save { + Some((_, ref mut compression)) => compression.set_compression_level(&level), + None => { + self.on_next_save = Some((self.serialization, self.compression.clone_with_new_compression_level(&level))) + } + } + } + else { + if let Some((serialization, compression)) = self.on_next_save { + if self.serialization.is_similar_to(&serialization) && self.compression.is_similar_to(&compression) { + self.on_next_save = None + } + } } } } @@ -64,6 +131,7 @@ impl From for SavePrefs { serialization: value.serialization, compression: value.compression, method_lock: value.method_lock, + on_next_save: None, } } } diff --git a/crates/rnote-engine/src/engine/visual_debug.rs b/crates/rnote-engine/src/engine/visual_debug.rs index f50342d38c..c023dfb7c4 100644 --- a/crates/rnote-engine/src/engine/visual_debug.rs +++ b/crates/rnote-engine/src/engine/visual_debug.rs @@ -147,7 +147,7 @@ pub(crate) fn draw_statistics_to_gtk_snapshot( ], na::point![ surface_bounds.maxs[0] - 20.0, - surface_bounds.mins[1] + 120.0 + surface_bounds.mins[1] + 420.0 ], ); let cairo_cx = snapshot.append_cairo(&graphene::Rect::from_p2d_aabb(text_bounds)); @@ -166,12 +166,13 @@ pub(crate) fn draw_statistics_to_gtk_snapshot( .count(); let statistics_text_string = format!( - "strokes in store: {}\nstrokes in current viewport: {}\nstrokes selected: {}\nstroke trashed: {}\nstrokes holding images: {}", + "strokes in store: {}\nstrokes in current viewport: {}\nstrokes selected: {}\nstroke trashed: {}\nstrokes holding images: {}\n\n {:?}", strokes_total.len(), strokes_in_viewport.len(), selected_strokes.len(), trashed_strokes.len(), strokes_hold_image, + engine.save_prefs, ); let text_layout = piet_cx .text() diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/compression.rs b/crates/rnote-engine/src/fileformats/rnoteformat/compression.rs index 68bfe84463..a0340d8092 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/compression.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/compression.rs @@ -12,43 +12,21 @@ pub enum CompressionMethod { None, #[serde(rename = "gzip")] Gzip(u8), - /// Zstd supports negative compression levels but I don't see the point in allowing these for Rnote files #[serde(rename = "zstd")] Zstd(u8), } impl Default for CompressionMethod { fn default() -> Self { - Self::Zstd(9) - } -} - -#[derive(Debug, Clone, Copy, num_derive::FromPrimitive, num_derive::ToPrimitive)] -pub enum CompressionLevel { - VeryHigh, - High, - Medium, - Low, - VeryLow, - None, -} - -impl TryFrom for CompressionLevel { - type Error = anyhow::Error; - - fn try_from(value: u32) -> Result { - num_traits::FromPrimitive::from_u32(value).ok_or_else(|| { - anyhow::anyhow!( - "CompressionLevel try_from::() for value {} failed", - value - ) - }) + Self::Zstd(12) } } impl CompressionMethod { pub const VALID_STR_ARRAY: [&'static str; 6] = ["None", "none", "Gzip", "gzip", "Zstd", "zstd"]; - + pub fn is_similar_to(&self, other: &Self) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) + } pub fn compress(&self, data: Vec) -> anyhow::Result> { match self { Self::None => Ok(data), @@ -63,6 +41,9 @@ impl CompressionMethod { Self::Zstd(compression_level) => { let mut encoder = zstd::Encoder::new(Vec::::new(), i32::from(*compression_level))?; + let _ = encoder.set_parameter( + zstd::zstd_safe::CParameter::EnableLongDistanceMatching(true), + ); if let Ok(num_workers) = std::thread::available_parallelism() { encoder.multithread(num_workers.get() as u32)?; } @@ -88,7 +69,7 @@ impl CompressionMethod { } } } - pub fn update_compression_level(&mut self, new: u8) -> anyhow::Result<()> { + pub fn update_compression_integer(&mut self, new: u8) -> anyhow::Result<()> { match self { Self::None => { tracing::warn!("Cannot update the compression level of 'None'"); @@ -107,7 +88,7 @@ impl CompressionMethod { Self::Zstd(ref mut curr) => { if !zstd::compression_level_range().contains(&i32::from(new)) { Err(anyhow::anyhow!( - "Invalid compression level for Zstd, expected a value between 0 and 22" + "Invalid compression level for Zstd, expected a value between 1 and 22" )) } else { *curr = new; @@ -116,6 +97,8 @@ impl CompressionMethod { } } } + + // Uses unreachable!() as this function is only used by rnote-ui in a coherent way. pub fn get_compression_level(&self) -> CompressionLevel { match self { Self::None => CompressionLevel::None, @@ -125,42 +108,63 @@ impl CompressionMethod { 4..=5 => CompressionLevel::Medium, 6..=7 => CompressionLevel::High, 8..=9 => CompressionLevel::VeryHigh, - _ => unreachable!(), + 10.. => { + tracing::warn!("Compression integer of {self:?} is greater than expected"); + CompressionLevel::VeryHigh + } }, Self::Zstd(val) => match *val { - 0..=4 => CompressionLevel::VeryLow, - 5..=8 => CompressionLevel::Low, - 9..=12 => CompressionLevel::Medium, - 13..=16 => CompressionLevel::High, - 17..=22 => CompressionLevel::VeryHigh, - _ => unreachable!(), + 1..=5 => CompressionLevel::VeryLow, + 6..=9 => CompressionLevel::Low, + 10..=13 => CompressionLevel::Medium, + 14..=17 => CompressionLevel::High, + 18..=22 => CompressionLevel::VeryHigh, + 0 => { + tracing::warn!("Compression integer of {self:?} is lower than expected"); + CompressionLevel::VeryLow + } + 23.. => { + tracing::warn!("Compression integer of {self:?} is greater than expected"); + CompressionLevel::VeryHigh + } }, } } + fn get_compression_integer_from_compression_level(&self, level: &CompressionLevel) -> u8 { + match self { + Self::None => 0, + Self::Gzip(..) => match level { + &CompressionLevel::VeryHigh => 8, + &CompressionLevel::High => 6, + &CompressionLevel::Medium => 5, + &CompressionLevel::Low => 3, + &CompressionLevel::VeryLow => 1, + &CompressionLevel::None => unreachable!(), + }, + Self::Zstd(..) => match level { + &CompressionLevel::VeryHigh => 20, + &CompressionLevel::High => 16, + &CompressionLevel::Medium => 12, + &CompressionLevel::Low => 8, + &CompressionLevel::VeryLow => 3, + &CompressionLevel::None => unreachable!(), + }, + } + } + pub fn set_compression_level(&mut self, level: &CompressionLevel) { + let new_integer = self.get_compression_integer_from_compression_level(&level); + match self { + Self::None => unreachable!(), + Self::Gzip(ref mut integer) | Self::Zstd(ref mut integer) => *integer = new_integer, + } + } - pub fn set_compression_level(&mut self, level: CompressionLevel) { + pub fn clone_with_new_compression_level(&self, level: &CompressionLevel) -> Self { + let new_integer = self.get_compression_integer_from_compression_level(level); match self { - Self::None => (), - Self::Gzip(ref mut val) => { - *val = match level { - CompressionLevel::VeryHigh => 8, - CompressionLevel::High => 6, - CompressionLevel::Medium => 5, - CompressionLevel::Low => 3, - CompressionLevel::VeryLow => 1, - CompressionLevel::None => unreachable!(), - } - } - Self::Zstd(ref mut val) => { - *val = match level { - CompressionLevel::VeryHigh => 17, - CompressionLevel::High => 13, - CompressionLevel::Medium => 9, - CompressionLevel::Low => 5, - CompressionLevel::VeryLow => 1, - CompressionLevel::None => unreachable!(), - } - } + Self::None => Self::None, + Self::Gzip(..) => Self::Gzip(new_integer), + Self::Zstd(..) => Self::Zstd(new_integer), } } } @@ -176,3 +180,26 @@ impl FromStr for CompressionMethod { } } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, num_derive::FromPrimitive, num_derive::ToPrimitive)] +pub enum CompressionLevel { + VeryHigh, + High, + Medium, + Low, + VeryLow, + None, +} + +impl TryFrom for CompressionLevel { + type Error = anyhow::Error; + + fn try_from(value: u32) -> Result { + num_traits::FromPrimitive::from_u32(value).ok_or_else(|| { + anyhow::anyhow!( + "CompressionLevel try_from::() for value {} failed", + value + ) + }) + } +} diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs index 96d463c145..04d7888146 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs @@ -95,12 +95,7 @@ impl TryFrom for EngineSnapshot { let uc_size = usize::try_from(value.header.uc_size).unwrap_or(usize::MAX); let uc_body = value.header.compression.decompress(uc_size, value.body)?; let mut engine_snapshot = value.header.serialization.deserialize(&uc_body)?; - - // Save preferences are only kept if method_lock is true or both the ser. method and comp. method are "defaults". - let save_prefs = SavePrefs::from(value.header); - if save_prefs.method_lock | save_prefs.conforms_to_default() { - engine_snapshot.save_prefs = save_prefs; - } + engine_snapshot.save_prefs = SavePrefs::from(value.header); Ok(engine_snapshot) } @@ -110,7 +105,7 @@ impl TryFrom<&EngineSnapshot> for RnoteFile { type Error = anyhow::Error; fn try_from(value: &EngineSnapshot) -> Result { - let save_prefs = value.save_prefs.clone_config(); + let save_prefs = value.save_prefs.clone(); let compression = save_prefs.compression; let serialization = save_prefs.serialization; diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/serialization.rs b/crates/rnote-engine/src/fileformats/rnoteformat/serialization.rs index 0b2f8c4e74..7032725926 100644 --- a/crates/rnote-engine/src/fileformats/rnoteformat/serialization.rs +++ b/crates/rnote-engine/src/fileformats/rnoteformat/serialization.rs @@ -14,20 +14,23 @@ pub enum SerializationMethod { impl Default for SerializationMethod { fn default() -> Self { - Self::Bitcode + Self::Json } } impl SerializationMethod { pub const VALID_STR_ARRAY: [&'static str; 5] = ["Bitcode", "bitcode", "Json", "JSON", "json"]; + /// Keeping this function to mimic the behaviour of CompressionMethod and forward-comptability + pub fn is_similar_to(&self, other: &Self) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) + } pub fn serialize(&self, engine_snapshot: &EngineSnapshot) -> anyhow::Result> { match self { Self::Bitcode => Ok(bitcode::serialize(engine_snapshot)?), Self::Json => Ok(serde_json::to_vec(&ijson::to_value(engine_snapshot)?)?), } } - pub fn deserialize(&self, data: &[u8]) -> anyhow::Result { match self { Self::Bitcode => Ok(bitcode::deserialize(data)?), diff --git a/crates/rnote-ui/data/ui/settingspanel.ui b/crates/rnote-ui/data/ui/settingspanel.ui index f4751c2ead..73e90901f6 100644 --- a/crates/rnote-ui/data/ui/settingspanel.ui +++ b/crates/rnote-ui/data/ui/settingspanel.ui @@ -394,8 +394,14 @@ gets disabled. + + + + + + File - + Compression Level diff --git a/crates/rnote-ui/src/canvas/imexport.rs b/crates/rnote-ui/src/canvas/imexport.rs index 6f8ec62f04..74212bc810 100644 --- a/crates/rnote-ui/src/canvas/imexport.rs +++ b/crates/rnote-ui/src/canvas/imexport.rs @@ -231,8 +231,10 @@ impl RnCanvas { return Err(e); } + // required by atomic file saving self.set_output_file(Some(gio::File::for_path(&filepath))); debug!("Saving file has finished successfully"); + self.engine_mut().save_prefs.finished_saving(); self.set_unsaved_changes(false); self.set_save_in_progress(false); diff --git a/crates/rnote-ui/src/settingspanel/mod.rs b/crates/rnote-ui/src/settingspanel/mod.rs index 714e8a8e60..f2792ca22a 100644 --- a/crates/rnote-ui/src/settingspanel/mod.rs +++ b/crates/rnote-ui/src/settingspanel/mod.rs @@ -5,6 +5,7 @@ mod penshortcutrow; // Re-exports pub(crate) use penshortcutrow::RnPenShortcutRow; use rnote_compose::ext::Vector2Ext; +use rnote_engine::engine::SavePrefs; use rnote_engine::fileformats::rnoteformat::CompressionMethod; // Imports @@ -19,8 +20,8 @@ use num_traits::ToPrimitive; use rnote_compose::penevent::ShortcutKey; use rnote_engine::document::background::PatternStyle; use rnote_engine::document::format::{self, Format, PredefinedFormat}; -use rnote_engine::document::CompressionLevel; use rnote_engine::document::Layout; +use rnote_engine::engine::save::CompressionLevel; use rnote_engine::ext::GdkRGBAExt; use std::cell::RefCell; @@ -98,7 +99,7 @@ mod imp { #[template_child] pub(crate) doc_background_pattern_invert_color_button: TemplateChild