diff --git a/Cargo.toml b/Cargo.toml index 8d9b706..40ba1b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ bgi-audio = ["bgi"] bgi-img = ["bgi", "image", "rand", "utils-bit-stream"] cat-system = ["fancy-regex", "flate2", "int-enum"] cat-system-arc = ["cat-system", "utils-blowfish", "utils-crc32"] -cat-system-img = ["cat-system", "flate2", "image", "utils-bit-stream"] +cat-system-img = ["cat-system", "flate2", "image", "mozjpeg", "utils-bit-stream"] circus = [] circus-arc = ["circus"] circus-audio = ["circus", "flate2", "int-enum", "utils-pcm"] diff --git a/src/ext/io.rs b/src/ext/io.rs index f95104c..4f573bb 100644 --- a/src/ext/io.rs +++ b/src/ext/io.rs @@ -1535,6 +1535,15 @@ impl StreamRegion { }) } + /// Creates a new `StreamRegion` with the specified stream and size. + /// + /// The start position is the current position of the stream, and the end position is calculated as `start_pos + size`. + pub fn with_size(mut stream: T, size: u64) -> Result { + let start_pos = stream.stream_position()?; + let end_pos = start_pos + size; + Self::new(stream, start_pos, end_pos) + } + /// Creates a new `StreamRegion` with the specified stream and start position. /// The end position is determined by the length of the stream. pub fn with_start_pos(mut stream: T, start_pos: u64) -> Result { diff --git a/src/scripts/cat_system/archive/int.rs b/src/scripts/cat_system/archive/int.rs index 4ccacbf..e0003dc 100644 --- a/src/scripts/cat_system/archive/int.rs +++ b/src/scripts/cat_system/archive/int.rs @@ -7,6 +7,7 @@ use crate::utils::blowfish::Blowfish; use crate::utils::crc32::CRC32NORMAL_TABLE; use crate::utils::encoding::{decode_to_string, encode_string}; use anyhow::Result; +use overf::wrapping; use std::io::{Read, Seek, SeekFrom}; use std::sync::{Arc, Mutex}; @@ -352,7 +353,7 @@ impl CSIntArc { } fn decrypt_name(name: &mut [u8; 0x40], key: u32, encoding: Encoding) -> Result { - let mut k = ((key >> 24) + (key >> 16) + (key >> 8) + key) & 0xFF; + let mut k = wrapping! {((key >> 24) + (key >> 16) + (key >> 8) + key) & 0xFF}; let mut i = 0; while i < 0x40 && name[i] != 0 { let v = name[i]; diff --git a/src/scripts/cat_system/image/hg3.rs b/src/scripts/cat_system/image/hg3.rs index 8e1fff8..124bdca 100644 --- a/src/scripts/cat_system/image/hg3.rs +++ b/src/scripts/cat_system/image/hg3.rs @@ -9,6 +9,7 @@ use anyhow::Result; use flate2::{Decompress, FlushDecompress}; use msg_tool_macro::*; use overf::wrapping; +use std::collections::HashMap; use std::io::{Read, Seek, Write}; #[derive(Debug)] @@ -360,6 +361,8 @@ impl<'a> Hg3Reader<'a> { self.m_input.read_exact(&mut image_type)?; if &image_type == b"img0000\0" { return self.unpack_img0000(); + } else if &image_type == b"img_jpg\0" { + return self.unpack_jpeg(); } else { return Err(anyhow::anyhow!("Unsupported image type: {:?}", image_type)); } @@ -418,4 +421,94 @@ impl<'a> Hg3Reader<'a> { flip_image(&mut img)?; Ok(img) } + + fn unpack_jpeg(&mut self) -> Result { + let toc = self.read_sections()?; + self.m_input.pos = (*toc + .get("img_jpg") + .ok_or(anyhow::anyhow!("Missing img_jpg section"))?) + as usize + + 12; + let jpeg_size = self.m_input.read_u32()?; + let mut data = { + let jpeg = StreamRegion::with_size(&mut self.m_input, jpeg_size as u64)?; + load_jpg(jpeg)? + }; + if data.color_type.bpp(1) < 3 { + return Err(anyhow::anyhow!( + "Unsupported JPEG color type: {:?} in HG-3 image", + data.color_type + )); + } + let src_pixel_size = data.color_type.bpp(1) as usize; + let alpha = if let Some(&alpha_offset) = toc.get("img_al") { + Some(self.read_alpha(alpha_offset as u32)?) + } else { + None + }; + let target_color_type = if alpha.is_some() { + ImageColorType::Rgba + } else { + data.color_type + }; + let pixel_size = target_color_type.bpp(1) as usize; + let stride = self.m_info.width as usize * pixel_size; + let mut pixels = vec![0; stride * self.m_info.height as usize]; + let mut src = 0; + let mut dst = 0; + let mut src_a = 0; + let src_g = 1; + let (src_b, src_r) = if toc.contains_key("imgmode") { + (2, 0) + } else { + (0, 2) + }; + for _ in 0..self.m_info.width as usize * self.m_info.height as usize { + pixels[dst] = data.data[src + src_b]; + pixels[dst + 1] = data.data[src + src_g]; + pixels[dst + 2] = data.data[src + src_r]; + if let Some(ref alpha_data) = alpha { + pixels[dst + 3] = alpha_data[src_a]; + src_a += 1; + } + dst += pixel_size; + src += src_pixel_size; + } + data.data = pixels; + data.color_type = target_color_type; + Ok(data) + } + + fn read_alpha(&mut self, start_pos: u32) -> Result> { + self.m_input.pos = start_pos as usize + 0x10; + let packed_size = self.m_input.read_u32()?; + let alpha_size = self.m_input.read_u32()?; + let alpha_in = StreamRegion::with_size(&mut self.m_input, packed_size as u64)?; + let mut alpha = Vec::new(); + flate2::read::ZlibDecoder::new(alpha_in).read_to_end(&mut alpha)?; + if alpha.len() != alpha_size as usize { + return Err(anyhow::anyhow!( + "Alpha data size {} does not match expected size {}", + alpha.len(), + alpha_size + )); + } + Ok(alpha) + } + + fn read_sections(&mut self) -> Result> { + let mut sections = HashMap::new(); + let mut next_offset = self.m_info.header_size; + loop { + self.m_input.pos = next_offset as usize; + let section_name = self.m_input.read_fstring(8, Encoding::Cp932, true)?; + let section_size = self.m_input.read_u32()?; + sections.insert(section_name, next_offset); + next_offset += section_size; + if section_size == 0 { + break; + } + } + Ok(sections) + } } diff --git a/src/utils/img.rs b/src/utils/img.rs index a63abe4..13cc87f 100644 --- a/src/utils/img.rs +++ b/src/utils/img.rs @@ -321,6 +321,37 @@ pub fn load_png(data: R) -> Result { }) } +#[cfg(feature = "mozjpeg")] +pub fn load_jpg(data: R) -> Result { + let decoder = mozjpeg::decompress::Decompress::new_reader(std::io::BufReader::new(data))?; + let color_type = match decoder.color_space() { + mozjpeg::ColorSpace::JCS_GRAYSCALE => ImageColorType::Grayscale, + mozjpeg::ColorSpace::JCS_RGB => ImageColorType::Rgb, + mozjpeg::ColorSpace::JCS_EXT_RGBA => ImageColorType::Rgba, + _ => ImageColorType::Rgb, // Convert other types to RGB + }; + let width = decoder.width() as u32; + let height = decoder.height() as u32; + let stride = width as usize * color_type.bpp(8) as usize / 8; + let mut data = vec![0; stride * height as usize]; + let mut re = match color_type { + ImageColorType::Grayscale => decoder.grayscale()?, + ImageColorType::Rgb => decoder.rgb()?, + ImageColorType::Rgba => decoder.rgba()?, + _ => { + unreachable!(); // We already checked the color type above + } + }; + re.read_scanlines_into(&mut data)?; + Ok(ImageData { + width, + height, + depth: 8, + color_type, + data, + }) +} + /// Decodes an image from the specified file path and returns its data. /// /// * `typ` - The type of the image to decode. @@ -334,33 +365,7 @@ pub fn decode_img(typ: ImageOutputType, filename: &str) -> Result { #[cfg(feature = "image-jpg")] ImageOutputType::Jpg => { let file = crate::utils::files::read_file(filename)?; - let decoder = mozjpeg::decompress::Decompress::new_mem(&file)?; - let color_type = match decoder.color_space() { - mozjpeg::ColorSpace::JCS_GRAYSCALE => ImageColorType::Grayscale, - mozjpeg::ColorSpace::JCS_RGB => ImageColorType::Rgb, - mozjpeg::ColorSpace::JCS_EXT_RGBA => ImageColorType::Rgba, - _ => ImageColorType::Rgb, // Convert other types to RGB - }; - let width = decoder.width() as u32; - let height = decoder.height() as u32; - let stride = width as usize * color_type.bpp(8) as usize / 8; - let mut data = vec![0; stride * height as usize]; - let mut re = match color_type { - ImageColorType::Grayscale => decoder.grayscale()?, - ImageColorType::Rgb => decoder.rgb()?, - ImageColorType::Rgba => decoder.rgba()?, - _ => { - unreachable!(); // We already checked the color type above - } - }; - re.read_scanlines_into(&mut data)?; - Ok(ImageData { - width, - height, - depth: 8, - color_type, - data, - }) + load_jpg(&file[..]) } #[cfg(feature = "image-webp")] ImageOutputType::Webp => {