ducpy 3.1.0__tar.gz → 3.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {ducpy-3.1.0 → ducpy-3.1.1}/Cargo.lock +9 -7
  2. {ducpy-3.1.0 → ducpy-3.1.1}/Cargo.toml +0 -1
  3. {ducpy-3.1.0 → ducpy-3.1.1}/PKG-INFO +1 -1
  4. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducpy/crate/Cargo.toml +1 -1
  5. ducpy-3.1.1/packages/ducrs/src/lib.rs +297 -0
  6. ducpy-3.1.1/packages/ducrs/src/serde_utils.rs +90 -0
  7. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/types.rs +22 -0
  8. ducpy-3.1.0/packages/ducrs/src/lib.rs +0 -94
  9. {ducpy-3.1.0 → ducpy-3.1.1}/LICENSE +0 -0
  10. {ducpy-3.1.0 → ducpy-3.1.1}/README.md +0 -0
  11. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducpy/crate/src/lib.rs +0 -0
  12. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/.gitignore +0 -0
  13. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/Cargo.toml +0 -0
  14. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/LICENSE +0 -0
  15. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/README.md +0 -0
  16. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/build.rs +0 -0
  17. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/package.json +0 -0
  18. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/release.config.cjs +0 -0
  19. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/api/document.rs +0 -0
  20. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/api/meta.rs +0 -0
  21. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/api/mod.rs +0 -0
  22. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/api/version_control.rs +0 -0
  23. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/db/bootstrap.rs +0 -0
  24. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/db/mod.rs +0 -0
  25. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/db/native.rs +0 -0
  26. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/db/wasm.rs +0 -0
  27. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/parse.rs +0 -0
  28. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/serialize.rs +0 -0
  29. {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/tests/.gitignore +0 -0
  30. {ducpy-3.1.0 → ducpy-3.1.1}/pyproject.toml +0 -0
  31. {ducpy-3.1.0 → ducpy-3.1.1}/src/ducpy_native/__init__.py +0 -0
@@ -411,7 +411,7 @@ dependencies = [
411
411
 
412
412
  [[package]]
413
413
  name = "ducpy-native"
414
- version = "3.1.0"
414
+ version = "3.1.1"
415
415
  dependencies = [
416
416
  "duc",
417
417
  "pyo3",
@@ -713,10 +713,11 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
713
713
 
714
714
  [[package]]
715
715
  name = "hipdf"
716
- version = "1.3.5"
716
+ version = "1.3.7"
717
717
  source = "registry+https://github.com/rust-lang/crates.io-index"
718
- checksum = "e3b986659f7ff291144addff668450799e4c2cd69686601cd48dfe23d7afc316"
718
+ checksum = "2dfe672796312ef1e60b2866d6dac03b67c32dc7276a71c6d34b573dd00502f1"
719
719
  dependencies = [
720
+ "image",
720
721
  "jpeg-decoder",
721
722
  "lopdf",
722
723
  "miniz_oxide",
@@ -764,6 +765,7 @@ dependencies = [
764
765
  "byteorder-lite",
765
766
  "color_quant",
766
767
  "gif",
768
+ "image-webp",
767
769
  "moxcms",
768
770
  "num-traits",
769
771
  "png 0.18.0",
@@ -2477,18 +2479,18 @@ dependencies = [
2477
2479
 
2478
2480
  [[package]]
2479
2481
  name = "zerocopy"
2480
- version = "0.8.41"
2482
+ version = "0.8.42"
2481
2483
  source = "registry+https://github.com/rust-lang/crates.io-index"
2482
- checksum = "96e13bc581734df6250836c59a5f44f3c57db9f9acb9dc8e3eaabdaf6170254d"
2484
+ checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
2483
2485
  dependencies = [
2484
2486
  "zerocopy-derive",
2485
2487
  ]
2486
2488
 
2487
2489
  [[package]]
2488
2490
  name = "zerocopy-derive"
2489
- version = "0.8.41"
2491
+ version = "0.8.42"
2490
2492
  source = "registry+https://github.com/rust-lang/crates.io-index"
2491
- checksum = "3545ea9e86d12ab9bba9fcd99b54c1556fd3199007def5a03c375623d05fac1c"
2493
+ checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
2492
2494
  dependencies = [
2493
2495
  "proc-macro2",
2494
2496
  "quote",
@@ -2,7 +2,6 @@
2
2
  resolver = "2"
3
3
  members = ["packages/ducrs", "packages/ducpy/crate"]
4
4
 
5
-
6
5
  [profile.release]
7
6
  lto = true
8
7
  opt-level = "s"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ducpy
3
- Version: 3.1.0
3
+ Version: 3.1.1
4
4
  Classifier: License :: OSI Approved :: MIT License
5
5
  Requires-Dist: nanoid>=2.0.0
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ducpy-native"
3
- version = "3.1.0"
3
+ version = "3.1.1"
4
4
  edition = "2021"
5
5
  description = "Python native extension for the duc file format (used by ducpy)"
6
6
  publish = false
@@ -0,0 +1,297 @@
1
+ #[allow(unused_imports)]
2
+ #[allow(non_camel_case_types)]
3
+ #[allow(non_snake_case)]
4
+ #[allow(non_upper_case_globals)]
5
+ pub mod parse;
6
+ pub mod serde_utils;
7
+ pub mod serialize;
8
+ pub mod types;
9
+
10
+ // SQLite storage layer and high-level document API
11
+ pub mod db;
12
+ pub mod api;
13
+
14
+ #[cfg(test)]
15
+ mod tests {
16
+ use std::collections::HashSet;
17
+ use std::fs;
18
+ use std::path::{Path, PathBuf};
19
+
20
+ use crate::parse;
21
+ use crate::serialize;
22
+ use crate::types::*;
23
+
24
+ fn assets_dir() -> PathBuf {
25
+ Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/testing/duc-files")
26
+ }
27
+
28
+ fn all_duc_files() -> Vec<PathBuf> {
29
+ let dir = assets_dir();
30
+ assert!(dir.exists(), "assets dir missing: {}", dir.display());
31
+ let mut files: Vec<PathBuf> = fs::read_dir(&dir)
32
+ .expect("read assets dir")
33
+ .filter_map(|e| e.ok())
34
+ .map(|e| e.path())
35
+ .filter(|p| p.extension().is_some_and(|ext| ext == "duc"))
36
+ .collect();
37
+ files.sort();
38
+ assert!(!files.is_empty(), "no .duc files in {}", dir.display());
39
+ files
40
+ }
41
+
42
+ fn load(path: &Path) -> Vec<u8> {
43
+ fs::read(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()))
44
+ }
45
+
46
+ // ── Parse every asset ────────────────────────────────────────────────────
47
+
48
+ #[test]
49
+ fn parse_all_assets() {
50
+ for path in all_duc_files() {
51
+ let buf = load(&path);
52
+ let state = parse::parse(&buf)
53
+ .unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
54
+
55
+ assert!(!state.version.is_empty(), "{}: version must not be empty", path.display());
56
+ assert!(!state.source.is_empty(), "{}: source must not be empty", path.display());
57
+ assert!(!state.elements.is_empty() || !state.layers.is_empty(),
58
+ "{}: must have elements or layers", path.display());
59
+ }
60
+ }
61
+
62
+ // ── Round-trip: parse → serialize → re-parse preserves data ──────────────
63
+
64
+ #[test]
65
+ fn roundtrip_all_assets() {
66
+ for path in all_duc_files() {
67
+ let original = load(&path);
68
+ let parsed = parse::parse(&original)
69
+ .unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
70
+
71
+ let serialized = serialize::serialize(&parsed)
72
+ .unwrap_or_else(|e| panic!("serialize {} failed: {e}", path.display()));
73
+ assert!(!serialized.is_empty(), "{}: serialized must not be empty", path.display());
74
+
75
+ let reparsed = parse::parse(&serialized)
76
+ .unwrap_or_else(|e| panic!("re-parse {} failed: {e}", path.display()));
77
+
78
+ assert_eq!(
79
+ parsed.elements.len(), reparsed.elements.len(),
80
+ "{}: element count mismatch", path.display()
81
+ );
82
+ assert_eq!(
83
+ parsed.layers.len(), reparsed.layers.len(),
84
+ "{}: layer count mismatch", path.display()
85
+ );
86
+ assert_eq!(
87
+ parsed.blocks.len(), reparsed.blocks.len(),
88
+ "{}: block count mismatch", path.display()
89
+ );
90
+ assert_eq!(
91
+ parsed.block_instances.len(), reparsed.block_instances.len(),
92
+ "{}: block instance count mismatch", path.display()
93
+ );
94
+ assert_eq!(
95
+ parsed.groups.len(), reparsed.groups.len(),
96
+ "{}: group count mismatch", path.display()
97
+ );
98
+ assert_eq!(
99
+ parsed.regions.len(), reparsed.regions.len(),
100
+ "{}: region count mismatch", path.display()
101
+ );
102
+ }
103
+ }
104
+
105
+ // ── Lazy parse skips external files ──────────────────────────────────────
106
+
107
+ #[test]
108
+ fn lazy_parse_skips_external_files() {
109
+ for path in all_duc_files() {
110
+ let buf = load(&path);
111
+ let full = parse::parse(&buf)
112
+ .unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
113
+ let lazy = parse::parse_lazy(&buf)
114
+ .unwrap_or_else(|e| panic!("parse_lazy {} failed: {e}", path.display()));
115
+
116
+ assert!(lazy.external_files.is_none(), "{}: lazy parse must omit external_files", path.display());
117
+ assert_eq!(full.elements.len(), lazy.elements.len(), "{}: element count diverged", path.display());
118
+ assert_eq!(full.layers.len(), lazy.layers.len(), "{}: layer count diverged", path.display());
119
+ }
120
+ }
121
+
122
+ // ── Element IDs are unique within each file ──────────────────────────────
123
+
124
+ #[test]
125
+ fn element_ids_unique() {
126
+ for path in all_duc_files() {
127
+ let buf = load(&path);
128
+ let state = parse::parse(&buf)
129
+ .unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
130
+
131
+ let mut seen = HashSet::new();
132
+ for wrapper in &state.elements {
133
+ let id = element_id(&wrapper.element);
134
+ assert!(seen.insert(id.clone()), "{}: duplicate element id '{id}'", path.display());
135
+ }
136
+ }
137
+ }
138
+
139
+ // ── Every element references a valid layer ───────────────────────────────
140
+
141
+ #[test]
142
+ fn element_layer_refs_valid() {
143
+ for path in all_duc_files() {
144
+ let buf = load(&path);
145
+ let state = parse::parse(&buf)
146
+ .unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
147
+
148
+ let layer_ids: HashSet<String> = state.layers.iter().map(|l| l.id.clone()).collect();
149
+
150
+ for wrapper in &state.elements {
151
+ let base = element_base(&wrapper.element);
152
+ if let Some(ref lid) = base.layer_id {
153
+ assert!(
154
+ layer_ids.contains(lid),
155
+ "{}: element '{}' references non-existent layer '{lid}'",
156
+ path.display(), base.id
157
+ );
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ // ── Block instances reference valid blocks ───────────────────────────────
164
+
165
+ #[test]
166
+ fn block_instance_refs_valid() {
167
+ for path in all_duc_files() {
168
+ let buf = load(&path);
169
+ let state = parse::parse(&buf)
170
+ .unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
171
+
172
+ let block_ids: HashSet<String> = state.blocks.iter().map(|b| b.id.clone()).collect();
173
+
174
+ for inst in &state.block_instances {
175
+ assert!(
176
+ block_ids.contains(&inst.block_id),
177
+ "{}: block instance '{}' references non-existent block '{}'",
178
+ path.display(), inst.id, inst.block_id
179
+ );
180
+ }
181
+ }
182
+ }
183
+
184
+ // ── External files: list ↔ get round-trip ────────────────────────────────
185
+
186
+ #[test]
187
+ fn external_files_list_get_consistent() {
188
+ for path in all_duc_files() {
189
+ let buf = load(&path);
190
+ let listing = parse::list_external_files(&buf)
191
+ .unwrap_or_else(|e| panic!("list_external_files {} failed: {e}", path.display()));
192
+
193
+ for meta in &listing {
194
+ let file = parse::get_external_file(&buf, &meta.id)
195
+ .unwrap_or_else(|e| panic!("get_external_file {} id={} failed: {e}", path.display(), meta.id));
196
+ assert!(file.is_some(), "{}: listed file '{}' not found via get", path.display(), meta.id);
197
+ let file = file.unwrap();
198
+ assert_eq!(file.id, meta.id);
199
+ assert!(!file.revisions.is_empty(), "{}: file '{}' has no revisions", path.display(), meta.id);
200
+ }
201
+ }
202
+ }
203
+
204
+ // ── DB layer: open in-memory, schema bootstraps correctly ────────────────
205
+
206
+ #[test]
207
+ fn db_open_memory_and_schema() {
208
+ use crate::api::DucDocument;
209
+
210
+ let doc = DucDocument::open_memory().expect("open_memory");
211
+ let version = doc.schema_version().expect("schema_version");
212
+ assert!(version > 0, "schema version must be positive, got {version}");
213
+ }
214
+
215
+ // ── Compression: serialized output is smaller than raw SQLite ────────────
216
+
217
+ #[test]
218
+ fn serialized_is_compressed() {
219
+ for path in all_duc_files() {
220
+ let buf = load(&path);
221
+ assert!(!parse::is_sqlite_header(&buf), "{}: assets should be compressed, not raw SQLite", path.display());
222
+
223
+ let raw = parse::decompress_duc_bytes(&buf)
224
+ .unwrap_or_else(|e| panic!("decompress {} failed: {e}", path.display()));
225
+
226
+ let state = parse::parse(&buf)
227
+ .unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
228
+ let recompressed = serialize::serialize(&state)
229
+ .unwrap_or_else(|e| panic!("serialize {} failed: {e}", path.display()));
230
+
231
+ assert!(
232
+ recompressed.len() < raw.len(),
233
+ "{}: compressed ({} B) should be smaller than raw SQLite ({} B)",
234
+ path.display(), recompressed.len(), raw.len()
235
+ );
236
+ }
237
+ }
238
+
239
+ // ── Global state invariants ──────────────────────────────────────────────
240
+
241
+ #[test]
242
+ fn global_state_valid() {
243
+ for path in all_duc_files() {
244
+ let buf = load(&path);
245
+ let state = parse::parse(&buf)
246
+ .unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
247
+
248
+ if let Some(ref gs) = state.duc_global_state {
249
+ assert!(!gs.view_background_color.is_empty(), "{}: background color must not be empty", path.display());
250
+ assert!(!gs.main_scope.is_empty(), "{}: main_scope must not be empty", path.display());
251
+ }
252
+ }
253
+ }
254
+
255
+ // ── Error handling: garbage input produces a clear error ──────────────────
256
+
257
+ #[test]
258
+ fn parse_rejects_garbage() {
259
+ let cases: &[(&str, &[u8])] = &[
260
+ ("empty", &[]),
261
+ ("random", &[0xFF; 64]),
262
+ ("text", b"Not a DUC file"),
263
+ ];
264
+ for (label, data) in cases {
265
+ assert!(
266
+ parse::parse(data).is_err(),
267
+ "parse should reject {label} input"
268
+ );
269
+ }
270
+ }
271
+
272
+ // ── Helpers ──────────────────────────────────────────────────────────────
273
+
274
+ fn element_id(el: &DucElementEnum) -> String {
275
+ element_base(el).id.clone()
276
+ }
277
+
278
+ fn element_base(el: &DucElementEnum) -> &DucElementBase {
279
+ match el {
280
+ DucElementEnum::DucRectangleElement(e) => &e.base,
281
+ DucElementEnum::DucPolygonElement(e) => &e.base,
282
+ DucElementEnum::DucEllipseElement(e) => &e.base,
283
+ DucElementEnum::DucEmbeddableElement(e) => &e.base,
284
+ DucElementEnum::DucPdfElement(e) => &e.base,
285
+ DucElementEnum::DucTableElement(e) => &e.base,
286
+ DucElementEnum::DucImageElement(e) => &e.base,
287
+ DucElementEnum::DucTextElement(e) => &e.base,
288
+ DucElementEnum::DucLinearElement(e) => &e.linear_base.base,
289
+ DucElementEnum::DucArrowElement(e) => &e.linear_base.base,
290
+ DucElementEnum::DucFreeDrawElement(e) => &e.base,
291
+ DucElementEnum::DucFrameElement(e) => &e.stack_element_base.base,
292
+ DucElementEnum::DucPlotElement(e) => &e.stack_element_base.base,
293
+ DucElementEnum::DucDocElement(e) => &e.base,
294
+ DucElementEnum::DucModelElement(e) => &e.base,
295
+ }
296
+ }
297
+ }
@@ -0,0 +1,90 @@
1
+ use serde::Deserializer;
2
+
3
+ /// Deserializes an `i32` field from any JSON number, truncating floats instead of erroring.
4
+ /// Annotate struct fields with: `#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]`
5
+ pub fn trunc_i32<'de, D>(deserializer: D) -> Result<i32, D::Error>
6
+ where
7
+ D: Deserializer<'de>,
8
+ {
9
+ struct Visitor;
10
+
11
+ impl<'de> serde::de::Visitor<'de> for Visitor {
12
+ type Value = i32;
13
+
14
+ fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
15
+ f.write_str("integer or float representable as i32")
16
+ }
17
+
18
+ fn visit_i8<E: serde::de::Error>(self, v: i8) -> Result<i32, E> {
19
+ Ok(v as i32)
20
+ }
21
+ fn visit_i16<E: serde::de::Error>(self, v: i16) -> Result<i32, E> {
22
+ Ok(v as i32)
23
+ }
24
+ fn visit_i32<E: serde::de::Error>(self, v: i32) -> Result<i32, E> {
25
+ Ok(v)
26
+ }
27
+ fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<i32, E> {
28
+ i32::try_from(v).map_err(E::custom)
29
+ }
30
+ fn visit_u8<E: serde::de::Error>(self, v: u8) -> Result<i32, E> {
31
+ Ok(v as i32)
32
+ }
33
+ fn visit_u16<E: serde::de::Error>(self, v: u16) -> Result<i32, E> {
34
+ Ok(v as i32)
35
+ }
36
+ fn visit_u32<E: serde::de::Error>(self, v: u32) -> Result<i32, E> {
37
+ i32::try_from(v).map_err(E::custom)
38
+ }
39
+ fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<i32, E> {
40
+ i32::try_from(v).map_err(E::custom)
41
+ }
42
+ fn visit_f32<E: serde::de::Error>(self, v: f32) -> Result<i32, E> {
43
+ Ok(v.trunc() as i32)
44
+ }
45
+ fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<i32, E> {
46
+ Ok(v.trunc() as i32)
47
+ }
48
+ }
49
+
50
+ deserializer.deserialize_any(Visitor)
51
+ }
52
+
53
+ struct FlexI32Seed;
54
+
55
+ impl<'de> serde::de::DeserializeSeed<'de> for FlexI32Seed {
56
+ type Value = i32;
57
+ fn deserialize<D: Deserializer<'de>>(self, deserializer: D) -> Result<i32, D::Error> {
58
+ trunc_i32(deserializer)
59
+ }
60
+ }
61
+
62
+ /// Deserializes a `Vec<i32>` from a JSON array of numbers, truncating any floats.
63
+ /// Annotate struct fields with: `#[serde(deserialize_with = "crate::serde_utils::trunc_vec_i32")]`
64
+ pub fn trunc_vec_i32<'de, D>(deserializer: D) -> Result<Vec<i32>, D::Error>
65
+ where
66
+ D: Deserializer<'de>,
67
+ {
68
+ struct SeqVisitor;
69
+
70
+ impl<'de> serde::de::Visitor<'de> for SeqVisitor {
71
+ type Value = Vec<i32>;
72
+
73
+ fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
74
+ f.write_str("a sequence of integers or floats representable as i32")
75
+ }
76
+
77
+ fn visit_seq<A>(self, mut seq: A) -> Result<Vec<i32>, A::Error>
78
+ where
79
+ A: serde::de::SeqAccess<'de>,
80
+ {
81
+ let mut v = Vec::with_capacity(seq.size_hint().unwrap_or(0));
82
+ while let Some(n) = seq.next_element_seed(FlexI32Seed)? {
83
+ v.push(n);
84
+ }
85
+ Ok(v)
86
+ }
87
+ }
88
+
89
+ deserializer.deserialize_seq(SeqVisitor)
90
+ }
@@ -349,6 +349,7 @@ pub struct Viewer3DMaterial {
349
349
  #[serde(rename_all = "camelCase")]
350
350
  pub struct Viewer3DZebra {
351
351
  pub active: bool,
352
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
352
353
  pub stripe_count: i32,
353
354
  pub stripe_direction: f64,
354
355
  /** Available: "blackwhite" | "colorful" | "grayscale" */
@@ -593,17 +594,20 @@ pub struct DucElementBase {
593
594
  * Random integer used to seed shape generation.
594
595
  * Doesn't differ across renders.
595
596
  */
597
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
596
598
  pub seed: i32,
597
599
  /**
598
600
  * Integer that is sequentially incremented on each change. Used to reconcile
599
601
  * elements during collaboration or when saving to server.
600
602
  */
603
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
601
604
  pub version: i32,
602
605
  /**
603
606
  * Random integer that is regenerated on each change.
604
607
  * Used for deterministic reconciliation of updates during collaboration,
605
608
  * in case the versions (see above) are identical.
606
609
  */
610
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
607
611
  pub version_nonce: i32,
608
612
  /** Epoch timestamp (ms) of last element update */
609
613
  pub updated: i64,
@@ -673,6 +677,7 @@ pub struct DucHead {
673
677
  #[serde(rename_all = "camelCase")]
674
678
  pub struct PointBindingPoint {
675
679
  /** The index of the target point within the element. */
680
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
676
681
  pub index: i32,
677
682
  /**
678
683
  * The offset from the point. Ranges from -1 to 1: 0 corresponds to the actual point.
@@ -709,6 +714,7 @@ pub struct DucPointBinding {
709
714
  #[serde(rename_all = "camelCase")]
710
715
  pub struct DucLineReference {
711
716
  /** Index of the point in the points array */
717
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
712
718
  pub index: i32,
713
719
  /** Bezier handle of the point on the line segment */
714
720
  pub handle: Option<GeometricPoint>,
@@ -724,6 +730,7 @@ pub struct DucLine {
724
730
  #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
725
731
  #[serde(rename_all = "camelCase")]
726
732
  pub struct DucPath {
733
+ #[serde(deserialize_with = "crate::serde_utils::trunc_vec_i32")]
727
734
  pub line_indices: Vec<i32>,
728
735
  /** Override the background and stroke from the base if different than null */
729
736
  pub background: Option<ElementBackground>,
@@ -990,6 +997,7 @@ pub struct DucPolygonElement {
990
997
  #[serde(flatten)]
991
998
  pub base: DucElementBase,
992
999
  /** Number of sides of the polygon */
1000
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
993
1001
  pub sides: i32,
994
1002
  }
995
1003
 
@@ -1015,6 +1023,7 @@ pub struct DucEmbeddableElement {
1015
1023
  #[serde(rename_all = "camelCase")]
1016
1024
  pub struct DocumentGridConfig {
1017
1025
  /** 1 = single, 2 = two-up, n = grid */
1026
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
1018
1027
  pub columns: i32,
1019
1028
  /** Horizontal spacing (px) */
1020
1029
  pub gap_x: f64,
@@ -1201,7 +1210,9 @@ pub struct DucModelElement {
1201
1210
  #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1202
1211
  #[serde(rename_all = "camelCase")]
1203
1212
  pub struct DucBlockDuplicationArray {
1213
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
1204
1214
  pub rows: i32,
1215
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
1205
1216
  pub cols: i32,
1206
1217
  pub row_spacing: f64,
1207
1218
  pub col_spacing: f64,
@@ -1211,6 +1222,7 @@ pub struct DucBlockDuplicationArray {
1211
1222
  #[serde(rename_all = "camelCase")]
1212
1223
  pub struct DucBlockMetadata {
1213
1224
  pub source: Option<String>,
1225
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
1214
1226
  pub usage_count: i32,
1215
1227
  /** Epoch timestamp (ms) of block creation */
1216
1228
  pub created_at: i64,
@@ -1237,6 +1249,7 @@ pub struct DucBlock {
1237
1249
  pub id: String,
1238
1250
  pub label: String,
1239
1251
  pub description: Option<String>,
1252
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
1240
1253
  pub version: i32,
1241
1254
  pub metadata: Option<DucBlockMetadata>,
1242
1255
  #[serde(with = "serde_bytes", default, skip_serializing_if = "Option::is_none")]
@@ -1250,6 +1263,7 @@ pub struct DucBlockInstance {
1250
1263
  /** The reference to the DucBlock definition this instance is based on */
1251
1264
  pub block_id: String,
1252
1265
  /** The version that should match the block_id's version, incremented on each change */
1266
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
1253
1267
  pub version: i32,
1254
1268
  pub element_overrides: Option<Vec<StringValueEntry>>,
1255
1269
  pub duplication_array: Option<DucBlockDuplicationArray>,
@@ -1353,6 +1367,7 @@ pub struct DucLocalState {
1353
1367
  * The user needs to manually update the graph for new versions to be saved in version control.
1354
1368
  */
1355
1369
  pub manual_save_mode: bool,
1370
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
1356
1371
  pub decimal_places: i32,
1357
1372
  }
1358
1373
 
@@ -1393,6 +1408,7 @@ pub struct Checkpoint {
1393
1408
  #[serde(flatten)]
1394
1409
  pub base: VersionBase,
1395
1410
  pub version_number: i64,
1411
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
1396
1412
  pub schema_version: i32,
1397
1413
  pub is_schema_boundary: bool,
1398
1414
  #[serde(with = "serde_bytes")]
@@ -1406,6 +1422,7 @@ pub struct Delta {
1406
1422
  #[serde(flatten)]
1407
1423
  pub base: VersionBase,
1408
1424
  pub version_number: i64,
1425
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
1409
1426
  pub schema_version: i32,
1410
1427
  pub base_checkpoint_id: String,
1411
1428
  /** Compressed binary data for the delta (zlib). When present, patch_string is ignored. */
@@ -1417,7 +1434,9 @@ pub struct Delta {
1417
1434
  #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1418
1435
  #[serde(rename_all = "camelCase")]
1419
1436
  pub struct SchemaMigration {
1437
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
1420
1438
  pub from_schema_version: i32,
1439
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
1421
1440
  pub to_schema_version: i32,
1422
1441
  pub migration_name: String,
1423
1442
  pub migration_checksum: Option<String>,
@@ -1429,6 +1448,7 @@ pub struct SchemaMigration {
1429
1448
  #[serde(rename_all = "camelCase")]
1430
1449
  pub struct VersionChain {
1431
1450
  pub id: String,
1451
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
1432
1452
  pub schema_version: i32,
1433
1453
  pub start_version: i64,
1434
1454
  pub end_version: Option<i64>,
@@ -1440,7 +1460,9 @@ pub struct VersionChain {
1440
1460
  #[serde(rename_all = "camelCase")]
1441
1461
  pub struct VersionGraphMetadata {
1442
1462
  pub current_version: i64,
1463
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
1443
1464
  pub current_schema_version: i32,
1465
+ #[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
1444
1466
  pub chain_count: i32,
1445
1467
  pub last_pruned: i64,
1446
1468
  pub total_size: i64,
@@ -1,94 +0,0 @@
1
- #[allow(unused_imports)]
2
- #[allow(non_camel_case_types)]
3
- #[allow(non_snake_case)]
4
- #[allow(non_upper_case_globals)]
5
- pub mod parse;
6
- pub mod serialize;
7
- pub mod types;
8
-
9
- // SQLite storage layer and high-level document API
10
- pub mod db;
11
- pub mod api;
12
-
13
- #[cfg(test)]
14
- mod tests {
15
- use std::fs;
16
- use std::path::Path;
17
-
18
- // Initialize logging for tests
19
- fn init_logging() {
20
- let _ = env_logger::builder()
21
- .filter_level(log::LevelFilter::Info)
22
- .is_test(true)
23
- .try_init();
24
- }
25
-
26
- #[test]
27
- fn test_parse_with_logging() {
28
- init_logging();
29
-
30
- // Try to load a test DUC file from assets folder
31
- let test_file_path = Path::new("assets/testing/duc-files/plot_elements.duc");
32
-
33
- if test_file_path.exists() {
34
- match fs::read(test_file_path) {
35
- Ok(data) => {
36
- println!("Loaded test file: {} bytes", data.len());
37
-
38
- // Call the parse function to trigger the logging
39
- match crate::parse::parse(&data) {
40
- Ok(parsed) => {
41
- println!("✅ Successfully parsed DUC file");
42
- println!("Layers count: {}", parsed.layers.len());
43
- }
44
- Err(e) => {
45
- println!("❌ Parse error: {}", e);
46
- }
47
- }
48
- }
49
- Err(e) => {
50
- println!("❌ Failed to read test file: {}", e);
51
- }
52
- }
53
- } else {
54
- println!("❌ Test file not found: {}", test_file_path.display());
55
- // Create minimal test data to at least test the parsing pipeline
56
- let minimal_data = vec![0u8; 100];
57
- match crate::parse::parse(&minimal_data) {
58
- Ok(_) => println!("✅ Minimal parse succeeded"),
59
- Err(e) => println!("❌ Minimal parse failed: {}", e),
60
- }
61
- }
62
- }
63
-
64
- #[test]
65
- fn test_serialize_roundtrip_compressed() {
66
- init_logging();
67
-
68
- let test_file_path = Path::new(env!("CARGO_MANIFEST_DIR"))
69
- .join("../../assets/testing/duc-files/universal.duc");
70
- if !test_file_path.exists() {
71
- println!("Skipped — test file not found: {}", test_file_path.display());
72
- return;
73
- }
74
-
75
- let original = fs::read(test_file_path).expect("read test file");
76
- let parsed = crate::parse::parse(&original).expect("parse original");
77
-
78
- let compressed = crate::serialize::serialize(&parsed).expect("serialize");
79
-
80
- assert!(!compressed.is_empty(), "serialized output must not be empty");
81
- println!(
82
- "original={} bytes, compressed={} bytes ({:.1}%)",
83
- original.len(),
84
- compressed.len(),
85
- compressed.len() as f64 / original.len() as f64 * 100.0
86
- );
87
-
88
- let reparsed = crate::parse::parse(&compressed).expect("parse compressed");
89
- assert_eq!(parsed.elements.len(), reparsed.elements.len());
90
- assert_eq!(parsed.layers.len(), reparsed.layers.len());
91
- assert_eq!(parsed.blocks.len(), reparsed.blocks.len());
92
- println!("Round-trip OK — {} elements preserved", reparsed.elements.len());
93
- }
94
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes