From f38dce9f3c5ae336bc68a83c2dbb9a059f61bac3 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Thu, 3 Jul 2025 17:39:56 +0800 Subject: [PATCH] Add dref img support --- Cargo.lock | 283 +++++++++++++++++++++++++++++ Cargo.toml | 3 +- src/scripts/kirikiri/image/dref.rs | 238 ++++++++++++++++++++++++ src/scripts/kirikiri/image/mod.rs | 1 + src/scripts/mod.rs | 2 + src/types.rs | 4 + src/utils/img.rs | 84 ++++++--- 7 files changed, 585 insertions(+), 30 deletions(-) create mode 100644 src/scripts/kirikiri/image/dref.rs diff --git a/Cargo.lock b/Cargo.lock index f5c7a01..8d39bfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -223,6 +223,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.15.0" @@ -345,6 +356,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -373,6 +393,113 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "inout" version = "0.1.4" @@ -436,6 +563,12 @@ dependencies = [ "overf", ] +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "memchr" version = "2.7.4" @@ -474,6 +607,7 @@ dependencies = [ "serde", "serde_json", "unicode-segmentation", + "url", "utf16string", "windows-sys", ] @@ -503,6 +637,12 @@ dependencies = [ "syn", ] +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "png" version = "0.17.16" @@ -516,6 +656,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -651,6 +800,18 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "strsim" version = "0.11.1" @@ -668,6 +829,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "typenum" version = "1.18.0" @@ -686,6 +868,17 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "utf16string" version = "0.2.0" @@ -695,6 +888,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -798,6 +997,36 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.25" @@ -817,3 +1046,57 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index f14fe25..4b64703 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ rand = { version = "0.9", optional = true } serde = { version = "1", features = ["derive"] } serde_json = "1" unicode-segmentation = "1.12" +url = { version = "2.5", optional = true } utf16string = "0.2" [features] @@ -36,7 +37,7 @@ circus = [] escude = ["int-enum"] escude-arc = ["escude", "rand", "utils-bit-stream"] kirikiri = ["emote-psb", "fancy-regex", "flate2", "utils-escape"] -kirikiri-img = ["kirikiri", "emote-psb", "image", "libtlg-rs"] +kirikiri-img = ["kirikiri", "emote-psb", "image", "libtlg-rs", "url"] yaneurao = [] yaneurao-itufuru = ["yaneurao"] # basic feature diff --git a/src/scripts/kirikiri/image/dref.rs b/src/scripts/kirikiri/image/dref.rs new file mode 100644 index 0000000..dbcfc2b --- /dev/null +++ b/src/scripts/kirikiri/image/dref.rs @@ -0,0 +1,238 @@ +use crate::ext::io::*; +use crate::ext::psb::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::*; +use crate::utils::img::*; +use anyhow::Result; +use emote_psb::PsbReader; +use std::collections::HashMap; +use std::io::Read; +use std::path::{Path, PathBuf}; +use url::Url; + +#[derive(Debug)] +pub struct DrefBuilder {} + +impl DrefBuilder { + pub fn new() -> Self { + Self {} + } +} + +impl ScriptBuilder for DrefBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn build_script( + &self, + buf: Vec, + filename: &str, + encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + Ok(Box::new(Dref::new(buf, encoding, filename, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["dref"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::KirikiriDref + } + + fn is_image(&self) -> bool { + true + } +} + +struct Dpak { + psb: VirtualPsbFixed, +} + +struct OffsetData { + left: u32, + top: u32, +} + +impl Dpak { + pub fn new>(path: P) -> Result { + let f = std::fs::File::open(path)?; + let mut f = std::io::BufReader::new(f); + let mut psb = PsbReader::open_psb(&mut f) + .map_err(|e| anyhow::anyhow!("Failed to read PSB from DPAK: {:?}", e))?; + let psb = psb + .load() + .map_err(|e| anyhow::anyhow!("Failed to load PSB from DPAK: {:?}", e))?; + let psb = psb.to_psb_fixed(); + Ok(Self { psb }) + } + + pub fn load_image(&self, name: &str) -> Result<(ImageData, Option)> { + let root = self.psb.root(); + let rid = root[name] + .resource_id() + .ok_or_else(|| anyhow::anyhow!("Resource ID for image '{}' not found in DPAK", name))? + as usize; + if rid >= self.psb.resources().len() { + return Err(anyhow::anyhow!( + "Resource ID {} out of bounds for DPAK with {} resources", + rid, + self.psb.resources().len() + )); + } + let resource = &self.psb.resources()[rid]; + Self::load_png(&resource) + } + + fn load_png(data: &[u8]) -> Result<(ImageData, Option)> { + let mut img = load_png(MemReaderRef::new(&data))?; + match img.color_type { + ImageColorType::Rgb => { + convert_rgb_to_rgba(&mut img)?; + } + _ => {} + } + Ok(( + img, + Self::try_read_offset_from_png(MemReaderRef::new(&data))?, + )) + } + + fn try_read_offset_from_png(mut data: MemReaderRef) -> Result> { + data.pos = 8; // Skip PNG signature + data.pos += 8; // Skip chunk size, type + data.pos += 17; // Skip IHDR chunk (length + type + width + height + bit depth + color type + compression method + filter method + interlace method) + loop { + let chunk_size = data.read_u32_be()?; + let mut chunk_type = [0u8; 4]; + data.read_exact(&mut chunk_type)?; + if &chunk_type == b"IDAT" || &chunk_type == b"IEND" { + break; + } + if &chunk_type == b"oFFs" { + let x = data.read_u32_be()?; + let y = data.read_u32_be()?; + if data.read_u8()? == 0 { + return Ok(Some(OffsetData { left: x, top: y })); + } + } + data.pos += chunk_size as usize + 4; // Skip chunk data and CRC + } + Ok(None) + } +} + +#[derive(Default)] +struct DpakLoader { + map: HashMap, +} + +impl DpakLoader { + pub fn load_image( + &mut self, + dir: &Path, + dpak: &str, + filename: &str, + ) -> Result<(ImageData, Option)> { + let dpak = match self.map.get(dpak) { + Some(d) => d, + None => { + let path = dir.join(dpak); + let ndpak = Dpak::new(&path)?; + self.map.insert(dpak.to_string(), ndpak); + self.map.get(dpak).unwrap() + } + }; + dpak.load_image(filename) + } +} + +#[derive(Debug)] +pub struct Dref { + urls: Vec, + dir: PathBuf, +} + +impl Dref { + pub fn new( + buf: Vec, + encoding: Encoding, + filename: &str, + _config: &ExtraConfig, + ) -> Result { + let text = decode_with_bom_detect(encoding, &buf)?.0; + let mut urls = Vec::new(); + for text in text.lines() { + let text = text.trim(); + if text.is_empty() { + continue; + } + urls.push(Url::parse(text)?); + } + let path = Path::new(filename); + let dir = if let Some(parent) = path.parent() { + parent.to_path_buf() + } else { + PathBuf::from(".") + }; + if urls.is_empty() { + return Err(anyhow::anyhow!("No URLs found in DREF file: {}", filename)); + } + for u in urls.iter() { + if u.scheme() != "psb" { + return Err(anyhow::anyhow!( + "Invalid URL scheme in DREF file: {} (expected 'psb')", + u + )); + } + } + Ok(Self { urls, dir }) + } +} + +impl Script for Dref { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Json + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + fn is_image(&self) -> bool { + true + } + + fn export_image(&self) -> Result { + let mut loader = DpakLoader::default(); + let base_url = &self.urls[0]; + let dpak = base_url.domain().ok_or(anyhow::anyhow!( + "Invalid URL in DREF file: {} (missing domain)", + base_url + ))?; + let (mut base_img, base_offset) = + loader.load_image(&self.dir, dpak, base_url.path().trim_start_matches("/"))?; + if let Some(o) = base_offset { + eprintln!("WARN: Base image offset: left={}, top={}", o.left, o.top); + crate::COUNTER.inc_warning(); + } + for url in &self.urls[1..] { + let dpak = url.domain().ok_or(anyhow::anyhow!( + "Invalid URL in DREF file: {} (missing domain)", + url + ))?; + let (img, img_offset) = + loader.load_image(&self.dir, dpak, url.path().trim_start_matches("/"))?; + let (top, left) = match img_offset { + Some(o) => (o.top, o.left), + None => (0, 0), + }; + draw_on_img_with_opacity(&mut base_img, &img, left, top, 0xff)?; + } + Ok(base_img) + } +} diff --git a/src/scripts/kirikiri/image/mod.rs b/src/scripts/kirikiri/image/mod.rs index 4b0cd5b..50e80f0 100644 --- a/src/scripts/kirikiri/image/mod.rs +++ b/src/scripts/kirikiri/image/mod.rs @@ -1,2 +1,3 @@ +pub mod dref; pub mod pimg; pub mod tlg; diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 2a50da2..f27e9dc 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -58,6 +58,8 @@ lazy_static::lazy_static! { Box::new(kirikiri::image::tlg::TlgImageBuilder::new()), #[cfg(feature = "kirikiri-img")] Box::new(kirikiri::image::pimg::PImgBuilder::new()), + #[cfg(feature = "kirikiri-img")] + Box::new(kirikiri::image::dref::DrefBuilder::new()), ]; pub static ref ALL_EXTS: Vec = BUILDER.iter().flat_map(|b| b.extensions()).map(|s| s.to_string()).collect(); diff --git a/src/types.rs b/src/types.rs index bf41b6d..6226484 100644 --- a/src/types.rs +++ b/src/types.rs @@ -294,6 +294,10 @@ pub enum ScriptType { #[value(alias("kr-pimg"))] /// Kirikiri PIMG image KirikiriPimg, + #[cfg(feature = "kirikiri-img")] + #[value(alias("kr-dref"))] + /// Kirikiri DREF(DPAK-referenced) image + KirikiriDref, #[cfg(feature = "yaneurao-itufuru")] #[value(alias("itufuru"))] /// Yaneurao Itufuru script diff --git a/src/utils/img.rs b/src/utils/img.rs index 6fdd9c6..cd139b9 100644 --- a/src/utils/img.rs +++ b/src/utils/img.rs @@ -52,6 +52,27 @@ pub fn convert_bgra_to_rgba(data: &mut ImageData) -> Result<()> { Ok(()) } +pub fn convert_rgb_to_rgba(data: &mut ImageData) -> Result<()> { + if data.color_type != ImageColorType::Rgb { + return Err(anyhow::anyhow!("Image is not RGB")); + } + if data.depth != 8 { + return Err(anyhow::anyhow!( + "RGB to RGBA conversion only supports 8-bit depth" + )); + } + let mut new_data = Vec::with_capacity(data.data.len() / 3 * 4); + for chunk in data.data.chunks_exact(3) { + new_data.push(chunk[0]); // R + new_data.push(chunk[1]); // G + new_data.push(chunk[2]); // B + new_data.push(255); // A + } + data.data = new_data; + data.color_type = ImageColorType::Rgba; + Ok(()) +} + pub fn convert_rgb_to_bgr(data: &mut ImageData) -> Result<()> { if data.color_type != ImageColorType::Rgb { return Err(anyhow::anyhow!("Image is not RGB")); @@ -124,39 +145,44 @@ pub fn encode_img(mut data: ImageData, typ: ImageOutputType, filename: &str) -> } } +pub fn load_png(data: R) -> Result { + let decoder = png::Decoder::new(data); + let mut reader = decoder.read_info()?; + let bit_depth = match reader.info().bit_depth { + png::BitDepth::One => 1, + png::BitDepth::Two => 2, + png::BitDepth::Four => 4, + png::BitDepth::Eight => 8, + png::BitDepth::Sixteen => 16, + }; + let color_type = match reader.info().color_type { + png::ColorType::Grayscale => ImageColorType::Grayscale, + png::ColorType::Rgb => ImageColorType::Rgb, + png::ColorType::Rgba => ImageColorType::Rgba, + _ => { + return Err(anyhow::anyhow!( + "Unsupported color type: {:?}", + reader.info().color_type + )); + } + }; + let stride = reader.info().width as usize * color_type.bpp(bit_depth) as usize / 8; + let mut data = vec![0; stride * reader.info().height as usize]; + reader.next_frame(&mut data)?; + Ok(ImageData { + width: reader.info().width, + height: reader.info().height, + depth: bit_depth, + color_type, + data, + }) +} + pub fn decode_img(typ: ImageOutputType, filename: &str) -> Result { match typ { ImageOutputType::Png => { let file = crate::utils::files::read_file(filename)?; - let decoder = png::Decoder::new(&file[..]); - let mut reader = decoder.read_info()?; - let bit_depth = match reader.info().bit_depth { - png::BitDepth::One => 1, - png::BitDepth::Two => 2, - png::BitDepth::Four => 4, - png::BitDepth::Eight => 8, - png::BitDepth::Sixteen => 16, - }; - let color_type = match reader.info().color_type { - png::ColorType::Grayscale => ImageColorType::Grayscale, - png::ColorType::Rgb => ImageColorType::Rgb, - png::ColorType::Rgba => ImageColorType::Rgba, - _ => { - return Err(anyhow::anyhow!( - "Unsupported color type: {:?}", - reader.info().color_type - )); - } - }; - let mut data = vec![0; reader.info().raw_bytes()]; - reader.next_frame(&mut data)?; - Ok(ImageData { - width: reader.info().width, - height: reader.info().height, - depth: bit_depth, - color_type, - data, - }) + load_png(&file[..]) } } }