msg_tool\scripts\will_plus\img/
wip.rs

1use crate::ext::io::*;
2use crate::scripts::base::*;
3use crate::types::*;
4use crate::utils::img::*;
5use crate::utils::struct_pack::*;
6use anyhow::{Context, Result, anyhow};
7use msg_tool_macro::*;
8use std::convert::TryFrom;
9use std::io::{Read, Seek, SeekFrom, Write};
10use std::ops::Range;
11
12#[derive(Debug)]
13/// Builder for WillPlus WIP images.
14pub struct WillPlusWipImageBuilder {}
15
16impl WillPlusWipImageBuilder {
17    /// Creates a new `WillPlusWipImageBuilder` instance.
18    pub const fn new() -> Self {
19        Self {}
20    }
21}
22
23impl ScriptBuilder for WillPlusWipImageBuilder {
24    fn default_encoding(&self) -> Encoding {
25        Encoding::Cp932
26    }
27
28    fn build_script(
29        &self,
30        data: Vec<u8>,
31        _filename: &str,
32        _encoding: Encoding,
33        _archive_encoding: Encoding,
34        config: &ExtraConfig,
35        _archive: Option<&Box<dyn Script>>,
36    ) -> Result<Box<dyn Script>> {
37        Ok(Box::new(WillPlusWipImage::new(
38            MemReader::new(data),
39            config,
40        )?))
41    }
42
43    fn extensions(&self) -> &'static [&'static str] {
44        &["wip", "wi0", "msk", "mos"]
45    }
46
47    fn script_type(&self) -> &'static ScriptType {
48        &ScriptType::WillPlusWip
49    }
50
51    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
52        if buf_len >= 4 && buf.starts_with(b"WIPF") {
53            Some(10)
54        } else {
55            None
56        }
57    }
58
59    #[cfg(feature = "image")]
60    fn is_image(&self) -> bool {
61        true
62    }
63}
64
65#[derive(Debug, Clone, StructPack, StructUnpack)]
66struct FrameHeader {
67    width: u32,
68    height: u32,
69    offset_x: u32,
70    offset_y: u32,
71    _reserved: i32,
72    frame_size: u32,
73}
74
75#[derive(Debug, Clone)]
76struct WillPlusWipFrame {
77    index: usize,
78    header: FrameHeader,
79    data_range: Range<usize>,
80    palette_range: Option<Range<usize>>,
81}
82
83#[derive(Debug)]
84/// WillPlus WIP image reader.
85pub struct WillPlusWipImage {
86    data: MemReader,
87    frames: Vec<WillPlusWipFrame>,
88    bpp: u16,
89}
90
91impl WillPlusWipImage {
92    /// Creates a `WillPlusWipImage` from raw data.
93    pub fn new(mut data: MemReader, _config: &ExtraConfig) -> Result<Self> {
94        if data.data.len() < 8 {
95            return Err(anyhow!("WIP image too small"));
96        }
97        let mut header = [0u8; 4];
98        data.read_exact(&mut header)?;
99        if &header != b"WIPF" {
100            return Err(anyhow!("Invalid WIP image header"));
101        }
102
103        let frame_count = data.read_u16()? as usize;
104        if frame_count == 0 {
105            return Err(anyhow!("WIP image has no frames"));
106        }
107        let bpp = data.read_u16()?;
108        if bpp != 8 && bpp != 24 {
109            return Err(anyhow!("Unsupported WIP bits-per-pixel: {}", bpp));
110        }
111
112        let index_size = frame_count
113            .checked_mul(0x18)
114            .ok_or_else(|| anyhow!("Frame table size overflow"))?;
115        let mut data_offset = 8usize
116            .checked_add(index_size)
117            .ok_or_else(|| anyhow!("Frame data offset overflow"))?;
118        if data.data.len() < data_offset {
119            return Err(anyhow!("WIP image truncated before frame data"));
120        }
121
122        let mut frames = Vec::with_capacity(frame_count);
123        for frame_index in 0..frame_count {
124            let header_offset = 8 + frame_index * 0x18;
125            data.seek(SeekFrom::Start(header_offset as u64))?;
126            let header = FrameHeader::unpack(&mut data, false, Encoding::Utf8)
127                .with_context(|| format!("Failed to read header for frame {}", frame_index))?;
128            let frame_size = usize::try_from(header.frame_size)
129                .map_err(|_| anyhow!("Frame {} data size too large", frame_index))?;
130
131            let data_start = data_offset;
132            let data_end = data_start
133                .checked_add(frame_size)
134                .ok_or_else(|| anyhow!("Frame {} data range overflow", frame_index))?;
135            if data_end > data.data.len() {
136                return Err(anyhow!("Frame {} data exceeds file length", frame_index));
137            }
138
139            let (palette_range, next_offset) = if bpp == 8 {
140                let palette_start = data_end;
141                let palette_end = palette_start
142                    .checked_add(0x400)
143                    .ok_or_else(|| anyhow!("Frame {} palette range overflow", frame_index))?;
144                if palette_end > data.data.len() {
145                    return Err(anyhow!("Frame {} palette exceeds file length", frame_index));
146                }
147                (Some(palette_start..palette_end), palette_end)
148            } else {
149                (None, data_end)
150            };
151
152            frames.push(WillPlusWipFrame {
153                index: frame_index,
154                header,
155                data_range: data_start..data_end,
156                palette_range,
157            });
158            data_offset = next_offset;
159        }
160
161        if frames.is_empty() {
162            return Err(anyhow!("No valid frames found in WIP image"));
163        }
164
165        Ok(WillPlusWipImage { data, frames, bpp })
166    }
167
168    fn decode_frame(&self, frame: &WillPlusWipFrame) -> Result<ImageData> {
169        let width_usize = usize::try_from(frame.header.width)
170            .map_err(|_| anyhow!("Frame {} width is too large", frame.index))?;
171        let height_usize = usize::try_from(frame.header.height)
172            .map_err(|_| anyhow!("Frame {} height is too large", frame.index))?;
173        let plane_size = width_usize
174            .checked_mul(height_usize)
175            .ok_or_else(|| anyhow!("Frame {} dimensions overflow", frame.index))?;
176
177        let compressed = self
178            .data
179            .data
180            .get(frame.data_range.clone())
181            .ok_or_else(|| anyhow!("Frame {} data range is invalid", frame.index))?;
182
183        let expected = match self.bpp {
184            8 => plane_size,
185            24 => plane_size
186                .checked_mul(3)
187                .ok_or_else(|| anyhow!("Frame {} buffer size overflow for 24bpp", frame.index))?,
188            _ => return Err(anyhow!("Unsupported bits-per-pixel: {}", self.bpp)),
189        };
190
191        let decoded = lzss_decompress(compressed, expected)
192            .with_context(|| format!("Failed to decompress frame {}", frame.index))?;
193
194        match self.bpp {
195            24 => {
196                let required = plane_size
197                    .checked_mul(3)
198                    .ok_or_else(|| anyhow!("Frame {} plane size overflow", frame.index))?;
199                if decoded.len() < required {
200                    return Err(anyhow!(
201                        "Frame {} decompressed data too short: {} < {}",
202                        frame.index,
203                        decoded.len(),
204                        required
205                    ));
206                }
207                let mut pixels = Vec::with_capacity(required);
208                for i in 0..plane_size {
209                    let b = decoded[i];
210                    let g = decoded[i + plane_size];
211                    let r = decoded[i + plane_size * 2];
212                    pixels.push(b);
213                    pixels.push(g);
214                    pixels.push(r);
215                }
216                Ok(ImageData {
217                    width: frame.header.width,
218                    height: frame.header.height,
219                    color_type: ImageColorType::Bgr,
220                    depth: 8,
221                    data: pixels,
222                })
223            }
224            8 => {
225                let indices = decoded.get(0..plane_size).ok_or_else(|| {
226                    anyhow!(
227                        "Frame {} decompressed data too short for indices",
228                        frame.index
229                    )
230                })?;
231                let palette_range = frame.palette_range.as_ref().ok_or_else(|| {
232                    anyhow!("Frame {} missing palette data for 8bpp image", frame.index)
233                })?;
234                let palette = self
235                    .data
236                    .data
237                    .get(palette_range.clone())
238                    .ok_or_else(|| anyhow!("Frame {} palette range is invalid", frame.index))?;
239                convert_index_palette_to_normal_bitmap(
240                    indices,
241                    8,
242                    palette,
243                    PaletteFormat::RgbX,
244                    width_usize,
245                    height_usize,
246                )
247                .with_context(|| format!("Failed to apply palette for frame {}", frame.index))
248            }
249            _ => Err(anyhow!("Unsupported bits-per-pixel: {}", self.bpp)),
250        }
251    }
252}
253
254impl Script for WillPlusWipImage {
255    fn default_output_script_type(&self) -> OutputScriptType {
256        OutputScriptType::Json
257    }
258
259    fn default_format_type(&self) -> FormatOptions {
260        FormatOptions::None
261    }
262
263    fn is_image(&self) -> bool {
264        true
265    }
266
267    fn export_image(&self) -> Result<ImageData> {
268        if self.frames.len() > 1 {
269            eprintln!("WARN: WIP image contains multiple frames, exporting only the first frame");
270            crate::COUNTER.inc_warning();
271        }
272        self.frames
273            .get(0)
274            .ok_or_else(|| anyhow!("No frames available in WIP image"))
275            .and_then(|frame| self.decode_frame(frame))
276    }
277
278    fn is_multi_image(&self) -> bool {
279        self.frames.len() > 1
280    }
281
282    fn export_multi_image<'a>(
283        &'a self,
284    ) -> Result<Box<dyn Iterator<Item = Result<ImageDataWithName>> + 'a>> {
285        Ok(Box::new(WillPlusWipIterator {
286            image: self,
287            index: 0,
288        }))
289    }
290}
291
292struct WillPlusWipIterator<'a> {
293    image: &'a WillPlusWipImage,
294    index: usize,
295}
296
297impl<'a> Iterator for WillPlusWipIterator<'a> {
298    type Item = Result<ImageDataWithName>;
299
300    fn next(&mut self) -> Option<Self::Item> {
301        if let Some(frame) = self.image.frames.get(self.index) {
302            let frame_index = self.index;
303            self.index += 1;
304            Some(
305                self.image
306                    .decode_frame(frame)
307                    .map(|data| ImageDataWithName {
308                        name: format!("{:04}", frame_index),
309                        data,
310                    }),
311            )
312        } else {
313            None
314        }
315    }
316}
317
318fn lzss_decompress(compressed: &[u8], expected: usize) -> Result<Vec<u8>> {
319    let mut output = Vec::with_capacity(expected.max(0x1000));
320    let mut window = [0u8; 0x1000];
321    let mut window_index: usize = 1;
322    let mut control: u32 = 0;
323    let mut remaining = compressed.len();
324    let mut cursor = 0usize;
325
326    while remaining > 0 {
327        control >>= 1;
328        if control & 0x100 == 0 {
329            if remaining == 0 {
330                break;
331            }
332            let value = compressed[cursor];
333            cursor += 1;
334            remaining -= 1;
335            control = (value as u32) | 0xFF00;
336        }
337
338        if control & 1 != 0 {
339            if remaining < 1 {
340                return Err(anyhow!("Unexpected end of data while reading literal"));
341            }
342            let value = compressed[cursor];
343            cursor += 1;
344            remaining -= 1;
345            output.push(value);
346            window[window_index] = value;
347            window_index = (window_index + 1) & 0x0FFF;
348        } else {
349            if remaining < 2 {
350                return Err(anyhow!(
351                    "Unexpected end of data while reading back-reference"
352                ));
353            }
354            let hi = compressed[cursor] as usize;
355            let lo = compressed[cursor + 1] as usize;
356            cursor += 2;
357            remaining -= 2;
358            let mut offset = (hi << 4) | (lo >> 4);
359            let mut count = (lo & 0x0F) + 2;
360            while count > 0 {
361                let value = window[offset & 0x0FFF];
362                offset = offset.wrapping_add(1);
363                output.push(value);
364                window[window_index] = value;
365                window_index = (window_index + 1) & 0x0FFF;
366                count -= 1;
367            }
368        }
369    }
370
371    if expected > 0 && output.len() < expected {
372        return Err(anyhow!(
373            "Decompressed data shorter than expected: {} < {}",
374            output.len(),
375            expected
376        ));
377    }
378
379    Ok(output)
380}