ifstools 2.0__tar.gz → 2.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.
- {ifstools-2.0 → ifstools-2.1}/PKG-INFO +1 -1
- {ifstools-2.0 → ifstools-2.1}/pyproject.toml +1 -1
- {ifstools-2.0 → ifstools-2.1}/rust/dxt.rs +38 -18
- {ifstools-2.0 → ifstools-2.1}/rust/lib.rs +16 -0
- {ifstools-2.0 → ifstools-2.1}/rust/lz77.rs +39 -34
- {ifstools-2.0 → ifstools-2.1}/src/ifstools/handlers/image_decoders.py +4 -1
- {ifstools-2.0 → ifstools-2.1}/.github/workflows/maturin.yml +0 -0
- {ifstools-2.0 → ifstools-2.1}/.gitignore +0 -0
- {ifstools-2.0 → ifstools-2.1}/Cargo.lock +0 -0
- {ifstools-2.0 → ifstools-2.1}/Cargo.toml +0 -0
- {ifstools-2.0 → ifstools-2.1}/LICENSE +0 -0
- {ifstools-2.0 → ifstools-2.1}/README.md +0 -0
- {ifstools-2.0 → ifstools-2.1}/ifstools_bin.py +0 -0
- {ifstools-2.0 → ifstools-2.1}/rust/png_enc.rs +0 -0
- {ifstools-2.0 → ifstools-2.1}/src/ifstools/__init__.py +0 -0
- {ifstools-2.0 → ifstools-2.1}/src/ifstools/handlers/_lz77_py.py +0 -0
- {ifstools-2.0 → ifstools-2.1}/src/ifstools/handlers/afp_folder.py +0 -0
- {ifstools-2.0 → ifstools-2.1}/src/ifstools/handlers/generic_file.py +0 -0
- {ifstools-2.0 → ifstools-2.1}/src/ifstools/handlers/generic_folder.py +0 -0
- {ifstools-2.0 → ifstools-2.1}/src/ifstools/handlers/image_file.py +0 -0
- {ifstools-2.0 → ifstools-2.1}/src/ifstools/handlers/lz77.py +0 -0
- {ifstools-2.0 → ifstools-2.1}/src/ifstools/handlers/md5_folder.py +0 -0
- {ifstools-2.0 → ifstools-2.1}/src/ifstools/handlers/node.py +0 -0
- {ifstools-2.0 → ifstools-2.1}/src/ifstools/handlers/tex_folder.py +0 -0
- {ifstools-2.0 → ifstools-2.1}/src/ifstools/ifs.py +0 -0
- {ifstools-2.0 → ifstools-2.1}/src/ifstools/ifstools.py +0 -0
- {ifstools-2.0 → ifstools-2.1}/src/ifstools/utils.py +0 -0
|
@@ -9,11 +9,6 @@ use texpresso::Format;
|
|
|
9
9
|
#[derive(Debug)]
|
|
10
10
|
pub enum DxtError {
|
|
11
11
|
UnknownFormat(String),
|
|
12
|
-
SizeMismatch {
|
|
13
|
-
expected: usize,
|
|
14
|
-
got: usize,
|
|
15
|
-
format: &'static str,
|
|
16
|
-
},
|
|
17
12
|
OddByteCount(usize),
|
|
18
13
|
}
|
|
19
14
|
|
|
@@ -21,11 +16,6 @@ impl std::fmt::Display for DxtError {
|
|
|
21
16
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
22
17
|
match self {
|
|
23
18
|
DxtError::UnknownFormat(s) => write!(f, "unknown DXT format: {}", s),
|
|
24
|
-
DxtError::SizeMismatch { expected, got, format } => write!(
|
|
25
|
-
f,
|
|
26
|
-
"{}: expected {} compressed bytes, got {}",
|
|
27
|
-
format, expected, got
|
|
28
|
-
),
|
|
29
19
|
DxtError::OddByteCount(n) => write!(f, "input length {} is not a multiple of 2", n),
|
|
30
20
|
}
|
|
31
21
|
}
|
|
@@ -60,16 +50,12 @@ pub fn decode(
|
|
|
60
50
|
let expected = fmt.compressed_size(width, height);
|
|
61
51
|
|
|
62
52
|
// Konami's per-WORD byte swap, with zero padding if the source is short.
|
|
53
|
+
// n is always even: data.len() is checked above, expected is a DXT block size.
|
|
63
54
|
let mut swapped = vec![0u8; expected];
|
|
64
55
|
let n = data.len().min(expected);
|
|
65
|
-
|
|
66
|
-
for
|
|
67
|
-
|
|
68
|
-
swapped[i * 2 + 1] = data[i * 2];
|
|
69
|
-
}
|
|
70
|
-
if n % 2 != 0 {
|
|
71
|
-
// Trailing odd byte (only possible when source is shorter than expected).
|
|
72
|
-
swapped[n - 1] = data[n - 1];
|
|
56
|
+
swapped[..n].copy_from_slice(&data[..n]);
|
|
57
|
+
for chunk in swapped[..n].chunks_exact_mut(2) {
|
|
58
|
+
chunk.swap(0, 1);
|
|
73
59
|
}
|
|
74
60
|
let _ = fmt_name; // currently only used for error formatting
|
|
75
61
|
|
|
@@ -78,6 +64,27 @@ pub fn decode(
|
|
|
78
64
|
Ok(rgba)
|
|
79
65
|
}
|
|
80
66
|
|
|
67
|
+
/// Encode raw RGBA8 pixels (`width * height * 4` bytes) into Konami-format DXT
|
|
68
|
+
/// data. Inverse of `decode`: compress to canonical DXT, then apply the
|
|
69
|
+
/// per-WORD byte swap Konami's textures use.
|
|
70
|
+
pub fn encode(
|
|
71
|
+
rgba: &[u8],
|
|
72
|
+
width: usize,
|
|
73
|
+
height: usize,
|
|
74
|
+
format: &str,
|
|
75
|
+
) -> Result<Vec<u8>, DxtError> {
|
|
76
|
+
let (fmt, _) = parse_format(format)?;
|
|
77
|
+
|
|
78
|
+
let mut out = vec![0u8; fmt.compressed_size(width, height)];
|
|
79
|
+
fmt.compress(rgba, width, height, texpresso::Params::default(), &mut out);
|
|
80
|
+
|
|
81
|
+
// compressed_size is always a multiple of the (even) block size.
|
|
82
|
+
for chunk in out.chunks_exact_mut(2) {
|
|
83
|
+
chunk.swap(0, 1);
|
|
84
|
+
}
|
|
85
|
+
Ok(out)
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
#[cfg(test)]
|
|
82
89
|
mod tests {
|
|
83
90
|
use super::*;
|
|
@@ -106,6 +113,19 @@ mod tests {
|
|
|
106
113
|
}
|
|
107
114
|
}
|
|
108
115
|
|
|
116
|
+
#[test]
|
|
117
|
+
fn dxt5_roundtrip() {
|
|
118
|
+
// Encode a solid opaque red 4x4, decode it, expect it back (DXT is
|
|
119
|
+
// lossy but a single flat color survives exactly).
|
|
120
|
+
let mut rgba = Vec::new();
|
|
121
|
+
for _ in 0..16 {
|
|
122
|
+
rgba.extend_from_slice(&[0xFF, 0x00, 0x00, 0xFF]);
|
|
123
|
+
}
|
|
124
|
+
let konami = encode(&rgba, 4, 4, "dxt5").unwrap();
|
|
125
|
+
let decoded = decode(&konami, 4, 4, "dxt5").unwrap();
|
|
126
|
+
assert_eq!(decoded, rgba);
|
|
127
|
+
}
|
|
128
|
+
|
|
109
129
|
#[test]
|
|
110
130
|
fn unknown_format_errors() {
|
|
111
131
|
assert!(decode(&[0; 16], 4, 4, "garbage").is_err());
|
|
@@ -57,6 +57,21 @@ fn py_decode_dxt<'py>(
|
|
|
57
57
|
Ok(PyBytes::new(py, &out))
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
#[pyfunction]
|
|
61
|
+
#[pyo3(name = "encode_dxt")]
|
|
62
|
+
fn py_encode_dxt<'py>(
|
|
63
|
+
py: Python<'py>,
|
|
64
|
+
rgba: Vec<u8>,
|
|
65
|
+
width: usize,
|
|
66
|
+
height: usize,
|
|
67
|
+
format: &str,
|
|
68
|
+
) -> PyResult<Bound<'py, PyBytes>> {
|
|
69
|
+
let out = py
|
|
70
|
+
.detach(|| dxt::encode(&rgba, width, height, format))
|
|
71
|
+
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
72
|
+
Ok(PyBytes::new(py, &out))
|
|
73
|
+
}
|
|
74
|
+
|
|
60
75
|
#[pymodule]
|
|
61
76
|
#[pyo3(name = "_native")]
|
|
62
77
|
fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
@@ -64,5 +79,6 @@ fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
64
79
|
m.add_function(wrap_pyfunction!(py_compress, m)?)?;
|
|
65
80
|
m.add_function(wrap_pyfunction!(py_encode_png, m)?)?;
|
|
66
81
|
m.add_function(wrap_pyfunction!(py_decode_dxt, m)?)?;
|
|
82
|
+
m.add_function(wrap_pyfunction!(py_encode_dxt, m)?)?;
|
|
67
83
|
Ok(())
|
|
68
84
|
}
|
|
@@ -15,6 +15,10 @@ const HASH_SIZE: usize = 1 << HASH_BITS;
|
|
|
15
15
|
const HASH_MASK: u32 = HASH_SIZE as u32 - 1;
|
|
16
16
|
const NIL: u32 = u32::MAX;
|
|
17
17
|
|
|
18
|
+
// Greedy match-search depth. 128 hits a good speed/ratio knee on real-world
|
|
19
|
+
// texture data: ~1.5× faster than an unbounded chain at <0.5% size cost.
|
|
20
|
+
const MAX_CHAIN: u32 = 128;
|
|
21
|
+
|
|
18
22
|
#[derive(Debug)]
|
|
19
23
|
pub enum DecompressError {
|
|
20
24
|
Truncated,
|
|
@@ -33,9 +37,10 @@ impl std::error::Error for DecompressError {}
|
|
|
33
37
|
pub fn decompress(input: &[u8]) -> Result<Vec<u8>, DecompressError> {
|
|
34
38
|
let mut out: Vec<u8> = Vec::with_capacity(input.len() * 2);
|
|
35
39
|
let mut i = 0usize;
|
|
40
|
+
let n = input.len();
|
|
36
41
|
|
|
37
42
|
loop {
|
|
38
|
-
if i >=
|
|
43
|
+
if i >= n {
|
|
39
44
|
return Err(DecompressError::Truncated);
|
|
40
45
|
}
|
|
41
46
|
let flag = input[i];
|
|
@@ -43,13 +48,13 @@ pub fn decompress(input: &[u8]) -> Result<Vec<u8>, DecompressError> {
|
|
|
43
48
|
|
|
44
49
|
for bit in 0..8 {
|
|
45
50
|
if (flag >> bit) & 1 == 1 {
|
|
46
|
-
if i >=
|
|
51
|
+
if i >= n {
|
|
47
52
|
return Err(DecompressError::Truncated);
|
|
48
53
|
}
|
|
49
54
|
out.push(input[i]);
|
|
50
55
|
i += 1;
|
|
51
56
|
} else {
|
|
52
|
-
if i + 1 >=
|
|
57
|
+
if i + 1 >= n {
|
|
53
58
|
return Err(DecompressError::Truncated);
|
|
54
59
|
}
|
|
55
60
|
let w = u16::from_be_bytes([input[i], input[i + 1]]);
|
|
@@ -65,22 +70,31 @@ pub fn decompress(input: &[u8]) -> Result<Vec<u8>, DecompressError> {
|
|
|
65
70
|
// References into the virtual zero-prefilled window before stream start.
|
|
66
71
|
if pos > out.len() {
|
|
67
72
|
let diff = (pos - out.len()).min(len);
|
|
68
|
-
out.
|
|
73
|
+
let new_len = out.len() + diff;
|
|
74
|
+
out.resize(new_len, 0);
|
|
69
75
|
len -= diff;
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
|
|
78
|
+
if len == 0 {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
let start = out.len() - pos;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
out.
|
|
83
|
+
if pos >= len {
|
|
84
|
+
// Non-overlapping run: single memcpy.
|
|
85
|
+
out.extend_from_within(start..start + len);
|
|
86
|
+
} else {
|
|
87
|
+
// Self-overlapping copy: each output byte may feed the next.
|
|
88
|
+
for k in 0..len {
|
|
89
|
+
let b = out[start + k];
|
|
90
|
+
out.push(b);
|
|
91
|
+
}
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
}
|
|
80
95
|
}
|
|
81
96
|
}
|
|
82
97
|
|
|
83
|
-
/// Hash a 3-byte prefix into the chain table.
|
|
84
98
|
#[inline(always)]
|
|
85
99
|
fn hash3(a: u8, b: u8, c: u8) -> usize {
|
|
86
100
|
let h = ((a as u32) << 16) | ((b as u32) << 8) | (c as u32);
|
|
@@ -88,9 +102,7 @@ fn hash3(a: u8, b: u8, c: u8) -> usize {
|
|
|
88
102
|
((h >> (32 - HASH_BITS)) & HASH_MASK) as usize
|
|
89
103
|
}
|
|
90
104
|
|
|
91
|
-
/// Compress a buffer.
|
|
92
|
-
/// when the matcher decisions agree (greedy longest-match here vs. greedy
|
|
93
|
-
/// longest-match there).
|
|
105
|
+
/// Compress a buffer. Greedy longest-match with a bounded hash chain.
|
|
94
106
|
pub fn compress(input: &[u8]) -> Vec<u8> {
|
|
95
107
|
// Pre-pend a 4 KB zero window so matches at the start can legitimately
|
|
96
108
|
// reference into the zero-prefilled history that the decoder synthesises.
|
|
@@ -115,15 +127,17 @@ pub fn compress(input: &[u8]) -> Vec<u8> {
|
|
|
115
127
|
let mut pos = WINDOW;
|
|
116
128
|
|
|
117
129
|
while pos < buf_len {
|
|
130
|
+
// Reserve a flag byte we'll patch in once the group is done. Avoids the
|
|
131
|
+
// per-group scratch Vec the older impl allocated.
|
|
132
|
+
let flag_idx = out.len();
|
|
133
|
+
out.push(0);
|
|
118
134
|
let mut flag_byte: u8 = 0;
|
|
119
|
-
let mut group: Vec<u8> = Vec::with_capacity(8 * 2);
|
|
120
135
|
|
|
121
136
|
for bit in 0..8 {
|
|
122
137
|
if pos >= buf_len {
|
|
123
138
|
// Out of input mid-group: leave the bit as match (0). The
|
|
124
|
-
// decoder
|
|
125
|
-
//
|
|
126
|
-
// phantom slot.
|
|
139
|
+
// decoder reads our trailing 0x00 0x00 0x00 sentinel and
|
|
140
|
+
// exits on the position == 0 check before reaching this slot.
|
|
127
141
|
continue;
|
|
128
142
|
}
|
|
129
143
|
|
|
@@ -132,10 +146,9 @@ pub fn compress(input: &[u8]) -> Vec<u8> {
|
|
|
132
146
|
if best_len >= THRESHOLD {
|
|
133
147
|
let dist = best_dist as u16;
|
|
134
148
|
let info: u16 = (dist << 4) | ((best_len - THRESHOLD) as u16);
|
|
135
|
-
|
|
149
|
+
out.extend_from_slice(&info.to_be_bytes());
|
|
136
150
|
|
|
137
|
-
// Insert hash entries for every position covered by the match
|
|
138
|
-
// including the start (which we are about to skip past).
|
|
151
|
+
// Insert hash entries for every position covered by the match.
|
|
139
152
|
for k in 0..best_len {
|
|
140
153
|
let p = pos + k;
|
|
141
154
|
if p + 2 < buf_len {
|
|
@@ -146,7 +159,7 @@ pub fn compress(input: &[u8]) -> Vec<u8> {
|
|
|
146
159
|
}
|
|
147
160
|
pos += best_len;
|
|
148
161
|
} else {
|
|
149
|
-
|
|
162
|
+
out.push(buf[pos]);
|
|
150
163
|
flag_byte |= 1 << bit;
|
|
151
164
|
|
|
152
165
|
if pos + 2 < buf_len {
|
|
@@ -158,8 +171,7 @@ pub fn compress(input: &[u8]) -> Vec<u8> {
|
|
|
158
171
|
}
|
|
159
172
|
}
|
|
160
173
|
|
|
161
|
-
out
|
|
162
|
-
out.extend_from_slice(&group);
|
|
174
|
+
out[flag_idx] = flag_byte;
|
|
163
175
|
}
|
|
164
176
|
|
|
165
177
|
// EOS sentinel: flag byte saying "next code is a match", then a 0x0000
|
|
@@ -188,28 +200,24 @@ fn find_match(buf: &[u8], pos: usize, head: &[u32], prev: &[u32]) -> (usize, usi
|
|
|
188
200
|
|
|
189
201
|
let mut best_len = 0usize;
|
|
190
202
|
let mut best_dist = 0usize;
|
|
191
|
-
|
|
192
|
-
// Bound on chain depth — at N=4096 chains are short anyway, but cap to keep
|
|
193
|
-
// worst-case behaviour bounded on highly repetitive input.
|
|
194
|
-
let mut chain_remaining: u32 = 4096;
|
|
203
|
+
let mut chain_remaining = MAX_CHAIN;
|
|
195
204
|
|
|
196
205
|
while candidate != NIL {
|
|
197
206
|
let cand = candidate as usize;
|
|
198
207
|
if cand < limit {
|
|
199
208
|
break;
|
|
200
209
|
}
|
|
210
|
+
if chain_remaining == 0 {
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
201
213
|
chain_remaining -= 1;
|
|
202
214
|
|
|
203
215
|
// Quick reject: if the byte at best_len doesn't match, skip.
|
|
204
|
-
if best_len
|
|
205
|
-
// fall through to chain step
|
|
206
|
-
} else {
|
|
207
|
-
// Compare bytes up to max_len.
|
|
216
|
+
if best_len == 0 || buf[cand + best_len] == buf[pos + best_len] {
|
|
208
217
|
let mut len = 0usize;
|
|
209
218
|
while len < max_len && buf[cand + len] == buf[pos + len] {
|
|
210
219
|
len += 1;
|
|
211
220
|
}
|
|
212
|
-
|
|
213
221
|
if len > best_len {
|
|
214
222
|
best_len = len;
|
|
215
223
|
best_dist = pos - cand;
|
|
@@ -219,9 +227,6 @@ fn find_match(buf: &[u8], pos: usize, head: &[u32], prev: &[u32]) -> (usize, usi
|
|
|
219
227
|
}
|
|
220
228
|
}
|
|
221
229
|
|
|
222
|
-
if chain_remaining == 0 {
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
230
|
candidate = prev[cand & (WINDOW - 1)];
|
|
226
231
|
}
|
|
227
232
|
|
|
@@ -53,12 +53,15 @@ def decode_dxt5(ifs_img, data):
|
|
|
53
53
|
def decode_dxt1(ifs_img, data):
|
|
54
54
|
return decode_dxt(ifs_img, data, 'dxt1')
|
|
55
55
|
|
|
56
|
+
def encode_dxt5(ifs_img, image):
|
|
57
|
+
return _native.encode_dxt(image.tobytes('raw', 'RGBA'), image.width, image.height, 'dxt5')
|
|
58
|
+
|
|
56
59
|
|
|
57
60
|
image_formats = {
|
|
58
61
|
'argb8888rev' : {'decoder': decode_argb8888rev, 'encoder': encode_argb8888rev},
|
|
59
62
|
'argb4444' : {'decoder': decode_argb4444, 'encoder': None},
|
|
60
63
|
'dxt1' : {'decoder': decode_dxt1, 'encoder': None},
|
|
61
|
-
'dxt5' : {'decoder': decode_dxt5, 'encoder': None},
|
|
64
|
+
'dxt5' : {'decoder': decode_dxt5, 'encoder': encode_dxt5 if _native else None},
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
cachable_formats = [key for key, val in image_formats.items() if val['encoder'] is not None]
|
|
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
|
|
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
|
|
File without changes
|