msg_tool\scripts\yuris\img/
ydg.rs

1//! YU-RIS compressed image file (.ydg)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::img::*;
6use crate::utils::struct_pack::*;
7use anyhow::Result;
8use msg_tool_macro::*;
9use std::io::{Read, Seek, SeekFrom, Write};
10use std::sync::{Arc, Mutex};
11
12#[derive(StructPack, StructUnpack, Debug, Clone)]
13struct YDGHeader {
14    /// YDG
15    magic: [u8; 4],
16    /// YU-RIS
17    yuris_magic: [u8; 8],
18    /// Seems always 0x64
19    _unk: u32,
20    /// Header length
21    header_size: u32,
22    /// YDG file size
23    file_size: u32,
24    _unk1: u64,
25    /// Image width
26    width: u16,
27    /// Image height
28    height: u16,
29    #[pack_vec_len(self.header_size - 0x24)]
30    #[unpack_vec_len({
31        if header_size < 0x24 {
32            anyhow::bail!("Header size at least need 0x24 bytes.");
33        }
34        header_size - 0x24
35    })]
36    other: Vec<u8>,
37    #[pvec(u32)]
38    slices: Vec<Slice>,
39}
40
41#[derive(StructPack, StructUnpack, Debug, Clone)]
42struct Slice {
43    /// Slice start offset
44    offset: u32,
45    /// Slice size
46    size: u32,
47    x: u16,
48    height: u16,
49    _unk: u32,
50}
51
52#[derive(Debug)]
53/// YU-RIS compressed image file (.ydg) builder
54pub struct YDGImageBuilder {}
55
56impl YDGImageBuilder {
57    pub fn new() -> Self {
58        Self {}
59    }
60}
61
62impl ScriptBuilder for YDGImageBuilder {
63    fn default_encoding(&self) -> Encoding {
64        Encoding::Utf8
65    }
66
67    fn build_script(
68        &self,
69        buf: Vec<u8>,
70        _filename: &str,
71        _encoding: Encoding,
72        _archive_encoding: Encoding,
73        config: &ExtraConfig,
74        _archive: Option<&Box<dyn Script>>,
75    ) -> Result<Box<dyn Script + Send + Sync>> {
76        Ok(Box::new(YDGImage::new(MemReader::new(buf), config)?))
77    }
78
79    fn extensions(&self) -> &'static [&'static str] {
80        &["ydg"]
81    }
82
83    fn script_type(&self) -> &'static ScriptType {
84        &ScriptType::YurisYDG
85    }
86
87    fn is_image(&self) -> bool {
88        true
89    }
90
91    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
92        if buf_len >= 12 && buf.starts_with(b"YDG\0YU-RIS\0\0") {
93            return Some(50);
94        }
95        None
96    }
97
98    fn can_create_image_file(&self) -> bool {
99        true
100    }
101
102    fn create_image_file<'a>(
103        &'a self,
104        mut data: ImageData,
105        _filename: &str,
106        mut writer: Box<dyn WriteSeek + 'a>,
107        _options: &ExtraConfig,
108    ) -> Result<()> {
109        let mut header = YDGHeader {
110            magic: *b"YDG\0",
111            yuris_magic: *b"YU-RIS\0\0",
112            _unk: 0x64,
113            header_size: 0x30,
114            file_size: 0,
115            _unk1: 0,
116            width: data.width as u16,
117            height: data.height as u16,
118            other: vec![0; 0xC],
119            slices: vec![Slice {
120                offset: 0,
121                size: 0,
122                x: 0,
123                height: data.height as u16,
124                _unk: 0,
125            }],
126        };
127        header.pack(&mut writer, false, Encoding::Utf8, &None)?;
128        header.slices[0].offset = writer.stream_position()? as u32;
129        match data.color_type {
130            ImageColorType::Bgr => {
131                convert_bgr_to_rgb(&mut data)?;
132            }
133            ImageColorType::Bgra => {
134                convert_bgra_to_rgba(&mut data)?;
135            }
136            ImageColorType::Rgb | ImageColorType::Rgba => {}
137            ImageColorType::Grayscale => {
138                convert_grayscale_to_rgb(&mut data)?;
139            }
140        };
141        let encoder = qoi::Encoder::new(&data.data, data.width, data.height)?;
142        encoder.encode_to_stream(&mut writer)?;
143        let file_size = writer.stream_position()? as u32;
144        header.slices[0].size = file_size - header.slices[0].offset;
145        header.file_size = file_size;
146        writer.seek(SeekFrom::Start(0))?;
147        header.pack(&mut writer, false, Encoding::Utf8, &None)?;
148        Ok(())
149    }
150}
151
152#[derive(Debug)]
153pub struct YDGImage<T> {
154    inner: Arc<Mutex<T>>,
155    header: YDGHeader,
156}
157
158impl<T: Read + Seek> YDGImage<T> {
159    pub fn new(mut data: T, _config: &ExtraConfig) -> Result<Self> {
160        let header = YDGHeader::unpack(&mut data, false, Encoding::Utf8, &None)?;
161        if &header.magic != b"YDG\0" {
162            anyhow::bail!("Unknown YDG magic: {:?}", header.magic);
163        }
164        if &header.yuris_magic != b"YU-RIS\0\0" {
165            anyhow::bail!("Unknown YU-RIS magic: {:?}", header.yuris_magic);
166        }
167        Ok(Self {
168            inner: Arc::new(Mutex::new(data)),
169            header,
170        })
171    }
172
173    fn load_slice(&self, slice: &Slice) -> Result<ImageData> {
174        let mut data = StreamRegion::with_size(
175            MutexWrapper::new(self.inner.clone(), slice.offset as u64),
176            slice.size as u64,
177        )?;
178        let mut buf = [0; 12];
179        let readed = data.peek(&mut buf)?;
180        if readed == 12 && buf.starts_with(b"RIFF") && buf.ends_with(b"WEBP") {
181            load_webp(data)
182        } else {
183            load_qoi(data)
184        }
185    }
186}
187
188impl<T: Read + Seek + std::fmt::Debug> Script for YDGImage<T> {
189    fn default_output_script_type(&self) -> OutputScriptType {
190        OutputScriptType::Custom
191    }
192
193    fn is_output_supported(&self, output: OutputScriptType) -> bool {
194        matches!(output, OutputScriptType::Custom)
195    }
196
197    fn default_format_type(&self) -> FormatOptions {
198        FormatOptions::None
199    }
200
201    fn is_image(&self) -> bool {
202        true
203    }
204
205    fn export_image(&self) -> Result<ImageData> {
206        let slice = self
207            .header
208            .slices
209            .get(0)
210            .ok_or_else(|| anyhow::anyhow!("YDG image has no valid tiles."))?;
211        let mut y = 0;
212        let mut base = self.load_slice(slice)?;
213        convert_to_rgba(&mut base)?;
214        let mut base = draw_on_canvas(
215            base,
216            self.header.width as u32,
217            self.header.height as u32,
218            slice.x as u32,
219            y,
220        )?;
221        y += slice.height as u32;
222        for slice in &self.header.slices[1..] {
223            let mut diff = self.load_slice(slice)?;
224            convert_to_rgba(&mut diff)?;
225            draw_on_image(&mut base, &diff, slice.x as u32, y)?;
226            y += slice.height as u32;
227        }
228        Ok(base)
229    }
230
231    fn import_image<'a>(
232        &'a self,
233        mut data: ImageData,
234        _filename: &str,
235        mut file: Box<dyn WriteSeek + 'a>,
236    ) -> Result<()> {
237        if data.depth != 8 {
238            anyhow::bail!("Unsupported depth: {}", data.depth);
239        }
240        let mut header = self.header.clone();
241        header.slices.clear();
242        header.slices.push(Slice {
243            offset: 0,
244            size: 0,
245            x: 0,
246            height: data.height as u16,
247            _unk: 0,
248        });
249        header.pack(&mut file, false, Encoding::Utf8, &None)?;
250        header.slices[0].offset = file.stream_position()? as u32;
251        if header.width != data.width as u16 || header.height != data.height as u16 {
252            eprintln!(
253                "WARNING: image size dismatched, expected {}x{}, actually {}x{}.",
254                header.width, header.height, data.width, data.height
255            );
256            crate::COUNTER.inc_warning();
257            header.width = data.width as u16;
258            header.height = data.height as u16;
259        }
260        match data.color_type {
261            ImageColorType::Bgr => {
262                convert_bgr_to_rgb(&mut data)?;
263            }
264            ImageColorType::Bgra => {
265                convert_bgra_to_rgba(&mut data)?;
266            }
267            ImageColorType::Rgb | ImageColorType::Rgba => {}
268            ImageColorType::Grayscale => {
269                convert_grayscale_to_rgb(&mut data)?;
270            }
271        };
272        let encoder = qoi::Encoder::new(&data.data, data.width, data.height)?;
273        encoder.encode_to_stream(&mut file)?;
274        let file_size = file.stream_position()? as u32;
275        header.slices[0].size = file_size - header.slices[0].offset;
276        header.file_size = file_size;
277        file.seek(SeekFrom::Start(0))?;
278        header.pack(&mut file, false, Encoding::Utf8, &None)?;
279        Ok(())
280    }
281}