openigtlink_rust/
compression.rs

1//! Message compression support for OpenIGTLink
2//!
3//! Provides compression/decompression functionality for large messages
4//! (images, video, point clouds) to reduce network bandwidth.
5//!
6//! # Supported Algorithms
7//!
8//! - **Deflate (zlib)**: Standard compression, good balance of speed and ratio
9//! - **Gzip**: Compatible with standard gzip format
10//! - **None**: No compression (passthrough)
11//!
12//! # Examples
13//!
14//! ```
15//! use openigtlink_rust::compression::{compress, decompress, CompressionLevel, CompressionType};
16//!
17//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
18//! let data = vec![0u8; 10000]; // Large message
19//!
20//! // Compress with default level
21//! let compressed = compress(&data, CompressionType::Deflate, CompressionLevel::Default)?;
22//! println!("Original: {} bytes, Compressed: {} bytes", data.len(), compressed.len());
23//!
24//! // Decompress
25//! let decompressed = decompress(&compressed, CompressionType::Deflate)?;
26//! assert_eq!(data, decompressed);
27//! # Ok(())
28//! # }
29//! ```
30
31use crate::error::{IgtlError, Result};
32use flate2::read::{DeflateDecoder, GzDecoder};
33use flate2::write::{DeflateEncoder, GzEncoder};
34use flate2::Compression;
35use std::io::{Read, Write};
36use tracing::{debug, info, trace};
37
38/// Compression algorithm type
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum CompressionType {
41    /// No compression
42    None,
43    /// Deflate (zlib) compression
44    Deflate,
45    /// Gzip compression
46    Gzip,
47}
48
49impl CompressionType {
50    /// Get the compression type name
51    pub fn name(&self) -> &'static str {
52        match self {
53            Self::None => "none",
54            Self::Deflate => "deflate",
55            Self::Gzip => "gzip",
56        }
57    }
58
59    /// Check if compression is enabled
60    pub fn is_compressed(&self) -> bool {
61        !matches!(self, Self::None)
62    }
63}
64
65/// Compression level
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum CompressionLevel {
68    /// No compression (level 0)
69    None,
70    /// Fast compression, lower ratio (level 1)
71    Fast,
72    /// Default compression (level 6)
73    Default,
74    /// Best compression, slower (level 9)
75    Best,
76    /// Custom level (0-9)
77    Custom(u32),
78}
79
80impl CompressionLevel {
81    /// Convert to flate2 Compression level
82    fn to_flate2(self) -> Compression {
83        match self {
84            Self::None => Compression::none(),
85            Self::Fast => Compression::fast(),
86            Self::Default => Compression::default(),
87            Self::Best => Compression::best(),
88            Self::Custom(level) => Compression::new(level),
89        }
90    }
91
92    /// Get numeric level value
93    pub fn level(&self) -> u32 {
94        match self {
95            Self::None => 0,
96            Self::Fast => 1,
97            Self::Default => 6,
98            Self::Best => 9,
99            Self::Custom(level) => *level,
100        }
101    }
102}
103
104impl Default for CompressionLevel {
105    fn default() -> Self {
106        Self::Default
107    }
108}
109
110/// Compress data using the specified algorithm and level
111///
112/// # Arguments
113///
114/// * `data` - Raw data to compress
115/// * `compression_type` - Compression algorithm to use
116/// * `level` - Compression level
117///
118/// # Returns
119///
120/// Compressed data
121///
122/// # Examples
123///
124/// ```
125/// use openigtlink_rust::compression::{compress, CompressionLevel, CompressionType};
126///
127/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
128/// let data = vec![0u8; 1000];
129/// let compressed = compress(&data, CompressionType::Deflate, CompressionLevel::Best)?;
130/// assert!(compressed.len() < data.len());
131/// # Ok(())
132/// # }
133/// ```
134pub fn compress(
135    data: &[u8],
136    compression_type: CompressionType,
137    level: CompressionLevel,
138) -> Result<Vec<u8>> {
139    trace!(
140        compression_type = compression_type.name(),
141        level = level.level(),
142        input_size = data.len(),
143        "Starting compression"
144    );
145
146    let compressed = match compression_type {
147        CompressionType::None => {
148            debug!("No compression requested, returning original data");
149            data.to_vec()
150        }
151        CompressionType::Deflate => {
152            let mut encoder = DeflateEncoder::new(Vec::new(), level.to_flate2());
153            encoder.write_all(data).map_err(|e| {
154                IgtlError::Io(std::io::Error::new(
155                    e.kind(),
156                    format!("Deflate compression failed: {}", e),
157                ))
158            })?;
159            encoder.finish().map_err(|e| {
160                IgtlError::Io(std::io::Error::new(
161                    e.kind(),
162                    format!("Deflate compression finish failed: {}", e),
163                ))
164            })?
165        }
166        CompressionType::Gzip => {
167            let mut encoder = GzEncoder::new(Vec::new(), level.to_flate2());
168            encoder.write_all(data).map_err(|e| {
169                IgtlError::Io(std::io::Error::new(
170                    e.kind(),
171                    format!("Gzip compression failed: {}", e),
172                ))
173            })?;
174            encoder.finish().map_err(|e| {
175                IgtlError::Io(std::io::Error::new(
176                    e.kind(),
177                    format!("Gzip compression finish failed: {}", e),
178                ))
179            })?
180        }
181    };
182
183    let ratio = if !data.is_empty() {
184        (compressed.len() as f64 / data.len() as f64) * 100.0
185    } else {
186        0.0
187    };
188
189    info!(
190        compression_type = compression_type.name(),
191        level = level.level(),
192        original_size = data.len(),
193        compressed_size = compressed.len(),
194        ratio_pct = format!("{:.1}%", ratio),
195        "Compression completed"
196    );
197
198    Ok(compressed)
199}
200
201/// Decompress data using the specified algorithm
202///
203/// # Arguments
204///
205/// * `data` - Compressed data
206/// * `compression_type` - Compression algorithm used
207///
208/// # Returns
209///
210/// Decompressed data
211///
212/// # Examples
213///
214/// ```
215/// use openigtlink_rust::compression::{compress, decompress, CompressionLevel, CompressionType};
216///
217/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
218/// let original = vec![1, 2, 3, 4, 5];
219/// let compressed = compress(&original, CompressionType::Deflate, CompressionLevel::Default)?;
220/// let decompressed = decompress(&compressed, CompressionType::Deflate)?;
221/// assert_eq!(original, decompressed);
222/// # Ok(())
223/// # }
224/// ```
225pub fn decompress(data: &[u8], compression_type: CompressionType) -> Result<Vec<u8>> {
226    trace!(
227        compression_type = compression_type.name(),
228        compressed_size = data.len(),
229        "Starting decompression"
230    );
231
232    let decompressed = match compression_type {
233        CompressionType::None => {
234            debug!("No decompression needed, returning original data");
235            data.to_vec()
236        }
237        CompressionType::Deflate => {
238            let mut decoder = DeflateDecoder::new(data);
239            let mut decompressed = Vec::new();
240            decoder.read_to_end(&mut decompressed).map_err(|e| {
241                IgtlError::Io(std::io::Error::new(
242                    e.kind(),
243                    format!("Deflate decompression failed: {}", e),
244                ))
245            })?;
246            decompressed
247        }
248        CompressionType::Gzip => {
249            let mut decoder = GzDecoder::new(data);
250            let mut decompressed = Vec::new();
251            decoder.read_to_end(&mut decompressed).map_err(|e| {
252                IgtlError::Io(std::io::Error::new(
253                    e.kind(),
254                    format!("Gzip decompression failed: {}", e),
255                ))
256            })?;
257            decompressed
258        }
259    };
260
261    info!(
262        compression_type = compression_type.name(),
263        compressed_size = data.len(),
264        decompressed_size = decompressed.len(),
265        "Decompression completed"
266    );
267
268    Ok(decompressed)
269}
270
271/// Compression statistics
272#[derive(Debug, Clone)]
273pub struct CompressionStats {
274    /// Original data size
275    pub original_size: usize,
276    /// Compressed data size
277    pub compressed_size: usize,
278    /// Compression ratio (compressed/original)
279    pub ratio: f64,
280    /// Space saved in bytes
281    pub space_saved: usize,
282    /// Compression type used
283    pub compression_type: CompressionType,
284    /// Compression level used
285    pub level: CompressionLevel,
286}
287
288impl CompressionStats {
289    /// Calculate statistics for a compression operation
290    pub fn calculate(
291        original_size: usize,
292        compressed_size: usize,
293        compression_type: CompressionType,
294        level: CompressionLevel,
295    ) -> Self {
296        let ratio = if original_size > 0 {
297            compressed_size as f64 / original_size as f64
298        } else {
299            0.0
300        };
301
302        let space_saved = original_size.saturating_sub(compressed_size);
303
304        Self {
305            original_size,
306            compressed_size,
307            ratio,
308            space_saved,
309            compression_type,
310            level,
311        }
312    }
313
314    /// Get compression ratio as percentage
315    pub fn ratio_percent(&self) -> f64 {
316        self.ratio * 100.0
317    }
318
319    /// Get space saved as percentage
320    pub fn space_saved_percent(&self) -> f64 {
321        if self.original_size > 0 {
322            (self.space_saved as f64 / self.original_size as f64) * 100.0
323        } else {
324            0.0
325        }
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn test_no_compression() {
335        let data = vec![1, 2, 3, 4, 5];
336        let compressed = compress(&data, CompressionType::None, CompressionLevel::Default).unwrap();
337        assert_eq!(data, compressed);
338
339        let decompressed = decompress(&compressed, CompressionType::None).unwrap();
340        assert_eq!(data, decompressed);
341    }
342
343    #[test]
344    fn test_deflate_compression() {
345        let data = vec![0u8; 1000]; // Highly compressible
346        let compressed =
347            compress(&data, CompressionType::Deflate, CompressionLevel::Default).unwrap();
348
349        // Should be much smaller
350        assert!(compressed.len() < data.len());
351        assert!(compressed.len() < 100); // Should compress very well
352
353        let decompressed = decompress(&compressed, CompressionType::Deflate).unwrap();
354        assert_eq!(data, decompressed);
355    }
356
357    #[test]
358    fn test_gzip_compression() {
359        let data = vec![1u8; 1000];
360        let compressed = compress(&data, CompressionType::Gzip, CompressionLevel::Default).unwrap();
361
362        assert!(compressed.len() < data.len());
363
364        let decompressed = decompress(&compressed, CompressionType::Gzip).unwrap();
365        assert_eq!(data, decompressed);
366    }
367
368    #[test]
369    fn test_compression_levels() {
370        let data = vec![0u8; 10000];
371
372        let fast = compress(&data, CompressionType::Deflate, CompressionLevel::Fast).unwrap();
373        let default = compress(&data, CompressionType::Deflate, CompressionLevel::Default).unwrap();
374        let best = compress(&data, CompressionType::Deflate, CompressionLevel::Best).unwrap();
375
376        // Best should be smallest (or equal for highly compressible data)
377        assert!(best.len() <= default.len());
378        assert!(default.len() <= fast.len() || default.len() < 100); // May be same for zeros
379
380        // All should decompress correctly
381        assert_eq!(data, decompress(&fast, CompressionType::Deflate).unwrap());
382        assert_eq!(
383            data,
384            decompress(&default, CompressionType::Deflate).unwrap()
385        );
386        assert_eq!(data, decompress(&best, CompressionType::Deflate).unwrap());
387    }
388
389    #[test]
390    fn test_random_data_compression() {
391        // Random data is not very compressible
392        let data: Vec<u8> = (0..1000).map(|i| (i * 37 % 256) as u8).collect();
393
394        let compressed =
395            compress(&data, CompressionType::Deflate, CompressionLevel::Default).unwrap();
396
397        // May not compress much, but should still work
398        let decompressed = decompress(&compressed, CompressionType::Deflate).unwrap();
399        assert_eq!(data, decompressed);
400    }
401
402    #[test]
403    fn test_empty_data() {
404        let data = vec![];
405        let compressed =
406            compress(&data, CompressionType::Deflate, CompressionLevel::Default).unwrap();
407        let decompressed = decompress(&compressed, CompressionType::Deflate).unwrap();
408        assert_eq!(data, decompressed);
409    }
410
411    #[test]
412    fn test_compression_stats() {
413        let stats = CompressionStats::calculate(
414            1000,
415            500,
416            CompressionType::Deflate,
417            CompressionLevel::Default,
418        );
419
420        assert_eq!(stats.original_size, 1000);
421        assert_eq!(stats.compressed_size, 500);
422        assert_eq!(stats.ratio, 0.5);
423        assert_eq!(stats.space_saved, 500);
424        assert_eq!(stats.ratio_percent(), 50.0);
425        assert_eq!(stats.space_saved_percent(), 50.0);
426    }
427
428    #[test]
429    fn test_compression_type_names() {
430        assert_eq!(CompressionType::None.name(), "none");
431        assert_eq!(CompressionType::Deflate.name(), "deflate");
432        assert_eq!(CompressionType::Gzip.name(), "gzip");
433    }
434
435    #[test]
436    fn test_compression_level_values() {
437        assert_eq!(CompressionLevel::None.level(), 0);
438        assert_eq!(CompressionLevel::Fast.level(), 1);
439        assert_eq!(CompressionLevel::Default.level(), 6);
440        assert_eq!(CompressionLevel::Best.level(), 9);
441        assert_eq!(CompressionLevel::Custom(5).level(), 5);
442    }
443}