diff --git a/Cargo.lock b/Cargo.lock index cbcdd9932..ef60f4aeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,17 +8,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -55,12 +44,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - [[package]] name = "bitcode" version = "0.6.3" @@ -91,15 +74,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "bumpalo" version = "3.15.4" @@ -118,36 +92,11 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.11+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" -dependencies = [ - "jobserver", - "libc", -] [[package]] name = "cfg-if" @@ -190,16 +139,6 @@ dependencies = [ "phf_codegen", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -210,12 +149,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - [[package]] name = "convert_case" version = "0.6.0" @@ -231,15 +164,6 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" -[[package]] -name = "cpufeatures" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.4.0" @@ -255,16 +179,6 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "csv" version = "1.3.0" @@ -305,17 +219,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - [[package]] name = "either" version = "1.10.0" @@ -332,16 +235,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.13" @@ -365,15 +258,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - [[package]] name = "iana-time-zone" version = "0.1.60" @@ -403,27 +287,20 @@ version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - [[package]] name = "ironcalc" version = "0.5.0" dependencies = [ "bitcode", "chrono", + "flate2", "ironcalc_base", "itertools", "roxmltree", "serde", "serde_json", "thiserror", + "time", "uuid", "zip", ] @@ -472,15 +349,6 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" -[[package]] -name = "jobserver" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" version = "0.3.69" @@ -625,29 +493,6 @@ dependencies = [ "regex", ] -[[package]] -name = "password-hash" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - -[[package]] -name = "pbkdf2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" -dependencies = [ - "digest", - "hmac", - "password-hash", - "sha2", -] - [[package]] name = "phf" version = "0.11.2" @@ -686,12 +531,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "pkg-config" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" - [[package]] name = "portable-atomic" version = "1.7.0" @@ -932,40 +771,12 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" -[[package]] -name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - [[package]] name = "syn" version = "2.0.77" @@ -1010,6 +821,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", + "js-sys", "num-conv", "powerfmt", "serde", @@ -1022,12 +834,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - [[package]] name = "unicode-ident" version = "1.0.12" @@ -1056,12 +862,6 @@ dependencies = [ "serde", ] -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1072,6 +872,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" name = "wasm" version = "0.5.0" dependencies = [ + "ironcalc", "ironcalc_base", "serde", "serde-wasm-bindgen", @@ -1252,45 +1053,9 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ - "aes", "byteorder", - "bzip2", - "constant_time_eq", "crc32fast", "crossbeam-utils", "flate2", - "hmac", - "pbkdf2", - "sha1", "time", - "zstd", -] - -[[package]] -name = "zstd" -version = "0.11.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "5.0.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.10+zstd.1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" -dependencies = [ - "cc", - "pkg-config", ] diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index 231521091..9ec795465 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -14,6 +14,7 @@ crate-type = ["cdylib"] # Uses `../ironcalc/base` when used locally, and uses # the inicated version from crates.io when published. # https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations +ironcalc = { path = "../../xlsx", version = "0.5.0" } ironcalc_base = { path = "../../base", version = "0.5", features = ["use_regex_lite"] } serde = { version = "1.0", features = ["derive"] } wasm-bindgen = "0.2.92" diff --git a/bindings/wasm/README.md b/bindings/wasm/README.md index b54082e32..fc0b21159 100644 --- a/bindings/wasm/README.md +++ b/bindings/wasm/README.md @@ -1,7 +1,6 @@ # IronCalc Web bindings -This crate is used to build the web bindings for IronCalc. -Note that it does not contain the xlsx writer and reader, only the engine. +This package contains web bindings for IronCalc. It exposes the engine and helper functions to import or export workbooks as XLSX or IronCalc (icalc) byte arrays. The built-in XLSX support focuses on core spreadsheet features like cell values, formulas, and styling. https://www.npmjs.com/package/@ironcalc/wasm?activeTab=readme diff --git a/bindings/wasm/README.pkg.md b/bindings/wasm/README.pkg.md index 8e62d0ec5..e26a45301 100644 --- a/bindings/wasm/README.pkg.md +++ b/bindings/wasm/README.pkg.md @@ -1,6 +1,6 @@ # IronCalc Web bindings -This package contains web bindings for IronCalc. Note that it does not contain the xlsx writer and reader, only the engine. +This package contains web bindings for IronCalc. It exposes the engine and helper functions to import or export workbooks as XLSX or IronCalc (icalc) byte arrays. The built-in XLSX support focuses on core spreadsheet features like cell values, formulas, and styling. ## Usage @@ -31,3 +31,22 @@ function compute() { compute(); ``` + +### Importing and exporting bytes + +The `Model` class provides helpers to load or save workbooks as raw byte arrays. + +```ts +// create a new workbook and export as XLSX bytes +const model = new Model('Workbook1', 'en', 'UTC'); +model.setUserInput(0, 1, 1, '42'); +const xlsxBytes = model.saveToXlsx(); + +// load from those bytes +const roundTripped = Model.fromXlsxBytes(xlsxBytes, 'Workbook1', 'en', 'UTC'); + +// same helpers exist for IronCalc's internal format +const icalcBytes = model.saveToIcalc(); +const restored = Model.fromIcalcBytes(icalcBytes); +``` + diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 5767a2edf..fbc33b7da 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -4,11 +4,15 @@ use wasm_bindgen::{ JsValue, }; +use ironcalc::{ + base::Model as BaseWorkbookModel, export::save_xlsx_to_writer, import::load_from_xlsx_bytes, +}; use ironcalc_base::{ expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column}, types::{CellType, Style}, BorderArea, ClipboardData, UserModel as BaseModel, }; +use std::io::{BufWriter, Cursor}; fn to_js_error(error: String) -> JsError { JsError::new(&error.to_string()) @@ -55,6 +59,27 @@ impl Model { Ok(Model { model }) } + #[wasm_bindgen(js_name = "fromICalcBytes")] + pub fn from_icalc_bytes(bytes: &[u8]) -> Result { + let model = BaseModel::from_bytes(bytes).map_err(to_js_error)?; + Ok(Model { model }) + } + + #[wasm_bindgen(js_name = "fromXLSXBytes")] + pub fn from_xlsx_bytes( + bytes: &[u8], + name: &str, + locale: &str, + timezone: &str, + ) -> Result { + let workbook = load_from_xlsx_bytes(bytes, name, locale, timezone) + .map_err(|e| to_js_error(e.to_string()))?; + let base_model = + BaseWorkbookModel::from_workbook(workbook).map_err(|e| to_js_error(e.to_string()))?; + let user_model = BaseModel::from_model(base_model); + Ok(Model { model: user_model }) + } + pub fn undo(&mut self) -> Result<(), JsError> { self.model.undo().map_err(to_js_error) } @@ -579,6 +604,21 @@ impl Model { self.model.to_bytes() } + #[wasm_bindgen(js_name = "saveToICalc")] + pub fn save_to_icalc(&self) -> Vec { + self.model.to_bytes() + } + + #[wasm_bindgen(js_name = "saveToXLSX")] + pub fn save_to_xlsx(&self) -> Result, JsError> { + let mut buffer: Vec = Vec::new(); + let cursor = Cursor::new(&mut buffer); + let writer = BufWriter::new(cursor); + save_xlsx_to_writer(self.model.get_model(), writer) + .map_err(|e| JsError::new(&e.to_string()))?; + Ok(buffer) + } + #[wasm_bindgen(js_name = "getName")] pub fn get_name(&self) -> String { self.model.get_name() diff --git a/bindings/wasm/tests/test.mjs b/bindings/wasm/tests/test.mjs index 374669966..c3ab1d471 100644 --- a/bindings/wasm/tests/test.mjs +++ b/bindings/wasm/tests/test.mjs @@ -130,5 +130,52 @@ test("autofill", () => { assert.strictEqual(result, "23"); }); +test('saveToXLSX returns data', () => { + const model = new Model('Workbook1', 'en', 'UTC'); + const bytes = model.saveToXLSX(); + assert.ok(bytes instanceof Uint8Array); + assert.ok(bytes.length > 0); +}); +test('saveToICalc returns data', () => { + const model = new Model('Workbook1', 'en', 'UTC'); + const bytes = model.saveToICalc(); + assert.ok(bytes instanceof Uint8Array); + assert.ok(bytes.length > 0); +}); + +test('fromICalcBytes loads model', () => { + const model = new Model('Workbook1', 'en', 'UTC'); + model.setUserInput(0, 1, 1, '42'); + const bytes = model.saveToICalc(); + const m2 = Model.fromICalcBytes(bytes); + assert.strictEqual(m2.getCellContent(0, 1, 1), '42'); +}); +test('fromXLSXBytes loads model', () => { + const model = new Model('Workbook1', 'en', 'UTC'); + model.setUserInput(0, 1, 1, '5'); + const bytes = model.saveToXLSX(); + const m2 = Model.fromXLSXBytes(bytes, 'Workbook1', 'en', 'UTC'); + assert.strictEqual(m2.getCellContent(0, 1, 1), '5'); +}); + +test('roundtrip via xlsx bytes', () => { + const m1 = new Model('Workbook1', 'en', 'UTC'); + m1.setUserInput(0, 1, 1, '7'); + m1.setUserInput(0, 1, 2, '=A1*3'); + const bytes = m1.saveToXLSX(); + const m2 = Model.fromXLSXBytes(bytes, 'Workbook1', 'en', 'UTC'); + m2.evaluate(); + assert.strictEqual(m2.getFormattedCellValue(0, 1, 2), '21'); +}); + +test('roundtrip via icalc bytes', () => { + const m1 = new Model('Workbook1', 'en', 'UTC'); + m1.setUserInput(0, 1, 1, '9'); + m1.setUserInput(0, 1, 2, '=A1*4'); + const bytes = m1.saveToICalc(); + const m2 = Model.fromICalcBytes(bytes); + m2.evaluate(); + assert.strictEqual(m2.getFormattedCellValue(0, 1, 2), '36'); +}); diff --git a/xlsx/Cargo.toml b/xlsx/Cargo.toml index 713d3bbe1..79d7549c6 100644 --- a/xlsx/Cargo.toml +++ b/xlsx/Cargo.toml @@ -12,7 +12,9 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -zip = "0.6" +zip = { version = "0.6.6", default-features = false, features = ["deflate", "time"] } +flate2 = { version = "1.0", default-features = false, features = ["rust_backend"] } +time = { version = "0.3", features = ["wasm-bindgen"] } roxmltree = "0.19" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/xlsx/src/import/mod.rs b/xlsx/src/import/mod.rs index 7ca509eeb..feaaef804 100644 --- a/xlsx/src/import/mod.rs +++ b/xlsx/src/import/mod.rs @@ -153,3 +153,10 @@ pub fn load_from_icalc(file_name: &str) -> Result { .map_err(|e| XlsxError::IO(format!("Failed to decode file: {}", e)))?; Model::from_workbook(workbook).map_err(XlsxError::Workbook) } + +/// Loads a [`Model`] from the bytes of an `ic` file. +pub fn load_from_icalc_bytes(bytes: &[u8]) -> Result { + let workbook: Workbook = bitcode::decode(bytes) + .map_err(|e| XlsxError::IO(format!("Failed to decode file: {}", e)))?; + Model::from_workbook(workbook).map_err(XlsxError::Workbook) +} diff --git a/xlsx/tests/test.rs b/xlsx/tests/test.rs index 0845da5c0..376bbcdde 100644 --- a/xlsx/tests/test.rs +++ b/xlsx/tests/test.rs @@ -7,7 +7,9 @@ use uuid::Uuid; use ironcalc::compare::{test_file, test_load_and_saving}; use ironcalc::export::save_to_xlsx; -use ironcalc::import::{load_from_icalc, load_from_xlsx, load_from_xlsx_bytes}; +use ironcalc::import::{ + load_from_icalc, load_from_icalc_bytes, load_from_xlsx, load_from_xlsx_bytes, +}; use ironcalc_base::types::{HorizontalAlignment, VerticalAlignment}; use ironcalc_base::{Model, UserModel}; @@ -62,6 +64,16 @@ fn test_load_from_xlsx_bytes() { assert_eq!(workbook.views[&0].sheet, 7); } +#[test] +fn test_load_from_icalc_bytes() { + let mut file = fs::File::open("tests/example.ic").unwrap(); + let mut bytes = Vec::new(); + file.read_to_end(&mut bytes).unwrap(); + let model = load_from_icalc_bytes(&bytes).unwrap(); + let model_from_file = load_from_icalc("tests/example.ic").unwrap(); + assert_eq!(model.workbook, model_from_file.workbook); +} + #[test] fn no_grid() { let model = load_from_xlsx("tests/NoGrid.xlsx", "en", "UTC").unwrap();