zx0/
compressor.rs

1use crate::{
2    MAX_OFFSET_ZX0,
3    MAX_OFFSET_ZX7
4};
5
6use crate::compress::{Block, compress};
7use crate::optimize::optimize;
8
9/// A struct containing a vector representing the compressed data, as well as metadata related to
10/// the compression operation.
11pub struct CompressionResult {
12    /// A vector containing the compressed output data.
13    pub output: Vec<u8>,
14
15    /// This value represents the minimum gap that should be maintained between the compressed
16    /// data's end address and the uncompressed data's end address when decompressing in-place.
17    /// When using the backwards compression mode the gap has to be between the start of the
18    /// compressed data and the start of the uncompressed data.
19    ///
20    /// Please refer to the original C implementation's
21    /// [readme](https://github.com/einar-saukas/ZX0#compressing-with-prefix) for an in-depth
22    /// explanation.
23    pub delta: usize
24}
25
26pub type ProgressCallback<'a> = Box<dyn FnMut(f32) + 'a>;
27
28/// This struct provides a means of initializing and performing a ZX0 compression operation by
29/// leveraging the builder pattern.
30///
31/// By calling [`Compressor::new`] a new [`Compressor`] will be instantiated using the following
32/// default values:
33///
34/// - No prefix/suffix skipping
35/// - Quick mode disabled
36/// - Backwards mode disabled
37/// - Classic mode disabled
38///
39/// After constructing a [`Compressor`] instance the method [`compress`](Compressor::compress) is available to compress
40/// `u8` slices. The [`Compressor`] can be resued again afterwards.
41///
42/// In contrast to the original C implementation, compression using the Rust ZX0 compressor is
43/// thread-safe, and can therefore be used to compress several slices in parallel.
44pub struct Compressor<'a> {
45    skip: usize,
46    quick_mode: bool,
47    backwards_mode: bool,
48    classic_mode: bool,
49    progress_callback: ProgressCallback<'a>
50}
51
52impl<'a> Compressor<'a> {
53    /// Instantiate a new [`Compressor`] using the following default values:
54    ///
55    /// - No prefix/suffix skipping
56    /// - Quick mode disabled
57    /// - Backwards mode disabled
58    /// - Classic mode disabled
59    pub fn new() -> Self {
60        Self {
61            skip: 0,
62            quick_mode: false,
63            backwards_mode: false,
64            classic_mode: false,
65            progress_callback: Box::new(|_| ())
66        }
67    }
68
69    /// Change the value for the quick mode setting. When enabled, this will cause the ZX0
70    /// compressor to use a smaller dictionary size, at the cost of a less efficient compression
71    /// ratio.
72    ///
73    /// Enabling this setting can be useful when producing debug assets where a short feedback loop
74    /// is more important than getting a good compression ratio.
75    pub fn quick_mode(&mut self, quick_mode: bool) -> &mut Self {
76        self.quick_mode = quick_mode;
77        self
78    }
79
80    /// Change the value for the backwards compression mode setting. This will cause the ZX0
81    /// compressor to create compressed data that should be decompressed back-to-front. This can be
82    /// useful in situations where in-place decompression is desired, and the end of the compressed
83    /// data overlaps with the end of the region that the uncompressed data should be positioned
84    /// in.
85    ///
86    /// Please refer to the original C implementation's
87    /// [readme](https://github.com/einar-saukas/ZX0#compressing-backwards) for an in-depth
88    /// explanation.
89    pub fn backwards_mode(&mut self, backwards_mode: bool) -> &mut Self {
90        self.backwards_mode = backwards_mode;
91        self
92    }
93
94    /// Change the value for the classic compression mode setting. Enabling this will cause the ZX0
95    /// compressor to output compressed data in its legacy V1 file format. This can be useful when
96    /// compressing for one of the platforms that only provides a V1 decompression routine.
97    pub fn classic_mode(&mut self, classic_mode: bool) -> &mut Self {
98        self.classic_mode = classic_mode;
99        self
100    }
101
102    /// Set a progress callback. When providing a closure to this function, that closure will be
103    /// called repeatedly during compression. The closure will be called with a progress value
104    /// between `0.0` and `1.0`. Note that due to the nature of the compression algorithm, this
105    /// value is not increasing linearly with time, and thus should be interpreted as a rough
106    /// estimate.
107    pub fn progress_callback<C: FnMut(f32) + 'a>(&mut self, progress_callback: C) -> &mut Self {
108        self.progress_callback = Box::new(progress_callback);
109        self
110    }
111
112    /// Set the number of prefix/suffix bytes to skip during compression. This will cause the
113    /// compressor to create a dictionary based on data that will already be in memory before the
114    /// compressed data during decompression. Of course, for this to work the prefix (or suffix in
115    /// case of backwards mode) in the file must be 100% identical to the data that is in memory
116    /// before or after the block of compressed data when attempting to decompress it.
117    ///
118    /// Please refer to the original C implementation's
119    /// [readme](https://github.com/einar-saukas/ZX0#compressing-with-prefix) for an in-depth
120    /// explanation.
121    pub fn skip(&mut self, skip: usize) -> &mut Self {
122        self.skip = skip;
123        self
124    }
125
126    /// Compress the provided slice.
127    ///
128    /// This returns a [`CompressionResult`] struct containing both the compressed data as well as
129    /// metadata related to the compression operation.
130    ///
131    /// The [`Compressor`] does not have to be discarded after calling this method. It does not
132    /// contain any state (only the configuration) and thus can be reused again for compressing
133    /// additional data.
134    pub fn compress(&mut self, input: &[u8]) -> CompressionResult {
135        let chain = {
136            let (allocator, mut optimal) = optimize(
137                input,
138                self.skip,
139                if self.quick_mode { MAX_OFFSET_ZX7 } else { MAX_OFFSET_ZX0 },
140                &mut self.progress_callback
141            );
142
143            let mut chain = Vec::new();
144
145            while optimal != 0 {
146                let oblock = allocator.get(optimal);
147
148                chain.push(Block {
149                    bits: oblock.bits as u32,
150                    index: oblock.index as isize,
151                    offset: oblock.offset as usize
152                });
153
154                optimal = oblock.next_index;
155            }
156
157            chain
158        };
159
160        let invert_mode = !self.classic_mode && !self.backwards_mode;
161        let mut delta = 0;
162
163        let output = compress(
164            chain,
165            input,
166            self.skip,
167            self.backwards_mode,
168            invert_mode,
169            &mut delta
170        );
171
172        CompressionResult {
173            output,
174            delta
175        }
176    }
177}
178
179impl<'a> Default for Compressor<'a> {
180    fn default() -> Self {
181        Self::new()
182    }
183}