1use super::rle::*;
3use crate::ext::io::*;
4use crate::ext::psb::*;
5use crate::scripts::base::*;
6use crate::types::*;
7use crate::utils::encoding::*;
8use crate::utils::files::*;
9use crate::utils::img::*;
10use anyhow::Result;
11use base64::Engine;
12use block_compression::BC7Settings;
13use clap::ValueEnum;
14use emote_psb::*;
15use libtlg_rs::*;
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use std::io::{Read, Seek, Write};
19
20#[derive(Debug)]
21pub struct PsbBuilder {}
22
23impl PsbBuilder {
24 pub fn new() -> Self {
25 Self {}
26 }
27}
28
29impl ScriptBuilder for PsbBuilder {
30 fn default_encoding(&self) -> Encoding {
31 Encoding::Utf8
32 }
33
34 fn build_script(
35 &self,
36 buf: Vec<u8>,
37 _filename: &str,
38 encoding: Encoding,
39 _archive_encoding: Encoding,
40 config: &ExtraConfig,
41 _archive: Option<&Box<dyn Script>>,
42 ) -> Result<Box<dyn Script>> {
43 Ok(Box::new(Psb::new(MemReader::new(buf), encoding, config)?))
44 }
45
46 fn build_script_from_reader(
47 &self,
48 reader: Box<dyn ReadSeek>,
49 _filename: &str,
50 encoding: Encoding,
51 _archive_encoding: Encoding,
52 config: &ExtraConfig,
53 _archive: Option<&Box<dyn Script>>,
54 ) -> Result<Box<dyn Script>> {
55 Ok(Box::new(Psb::new(reader, encoding, config)?))
56 }
57
58 fn build_script_from_file(
59 &self,
60 filename: &str,
61 encoding: Encoding,
62 _archive_encoding: Encoding,
63 config: &ExtraConfig,
64 _archive: Option<&Box<dyn Script>>,
65 ) -> Result<Box<dyn Script>> {
66 let file = std::fs::File::open(filename)?;
67 let f = std::io::BufReader::new(file);
68 Ok(Box::new(Psb::new(f, encoding, config)?))
69 }
70
71 fn extensions(&self) -> &'static [&'static str] {
72 &["psb"]
73 }
74
75 fn script_type(&self) -> &'static ScriptType {
76 &ScriptType::EmotePsb
77 }
78
79 fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
80 if buf_len >= 4 && buf.starts_with(b"PSB\0") {
81 return Some(10);
82 } else if buf_len >= 4 && buf.starts_with(&[0x04, 0x22, 0x4D, 0x18]) {
83 for i in 4..buf_len - 4 {
84 if buf[i..i + 4] == *b"PSB\0" {
85 return Some(10);
86 }
87 }
88 }
89 None
90 }
91
92 fn can_create_file(&self) -> bool {
93 true
94 }
95
96 fn create_file<'a>(
97 &'a self,
98 filename: &'a str,
99 writer: Box<dyn WriteSeek + 'a>,
100 encoding: Encoding,
101 file_encoding: Encoding,
102 config: &ExtraConfig,
103 ) -> Result<()> {
104 create_file(filename, writer, encoding, file_encoding, config)
105 }
106}
107
108#[derive(Debug, ValueEnum, Clone, Copy)]
109pub enum BC7Config {
110 UltraFast,
112 VeryFast,
114 Fast,
116 Basic,
118 Slow,
120}
121
122impl Default for BC7Config {
123 fn default() -> Self {
124 Self::Basic
125 }
126}
127
128impl Into<BC7Settings> for BC7Config {
129 fn into(self) -> BC7Settings {
130 match self {
131 Self::UltraFast => BC7Settings::alpha_ultrafast(),
132 Self::VeryFast => BC7Settings::alpha_very_fast(),
133 Self::Fast => BC7Settings::alpha_fast(),
134 Self::Basic => BC7Settings::alpha_basic(),
135 Self::Slow => BC7Settings::alpha_slow(),
136 }
137 }
138}
139
140#[derive(Debug)]
141pub struct Psb {
142 psb: VirtualPsbFixed,
143 encoding: Encoding,
144 config: ExtraConfig,
145}
146
147impl Psb {
148 pub fn new<R: Read + Seek>(
149 reader: R,
150 encoding: Encoding,
151 config: &ExtraConfig,
152 ) -> Result<Self> {
153 let psb = PsbReader::open_psb_v2(reader)?.to_psb_fixed();
154 Ok(Self {
155 psb,
156 encoding,
157 config: config.clone(),
158 })
159 }
160
161 fn output_resource(
162 &self,
163 folder_path: &std::path::PathBuf,
164 path: String,
165 data: &[u8],
166 ) -> Result<Resource> {
167 let mut res = Resource {
168 path,
169 ..Default::default()
170 };
171 if self.config.psb_process_tlg && is_valid_tlg(&data) {
172 let tlg = load_tlg(MemReaderRef::new(&data))?;
173 res.tlg = Some(TlgInfo::from_tlg(&tlg, self.encoding));
174 let outtype = self.config.image_type.unwrap_or(ImageOutputType::Png);
175 res.path = {
176 let mut pb = std::path::PathBuf::from(&res.path);
177 pb.set_extension(outtype.as_ref());
178 pb.to_string_lossy().to_string()
179 };
180 let path = folder_path.join(&res.path);
181 make_sure_dir_exists(&path)?;
182 let img = ImageData {
183 width: tlg.width as u32,
184 height: tlg.height as u32,
185 color_type: match tlg.color {
186 TlgColorType::Bgr24 => ImageColorType::Bgr,
187 TlgColorType::Bgra32 => ImageColorType::Bgra,
188 TlgColorType::Grayscale8 => ImageColorType::Grayscale,
189 },
190 depth: 8,
191 data: tlg.data,
192 };
193 encode_img(img, outtype, &path.to_string_lossy(), &self.config)?;
194 } else {
195 let path = folder_path.join(&res.path);
196 make_sure_dir_exists(&path)?;
197 std::fs::write(&path, data)?;
198 }
199 Ok(res)
200 }
201
202 fn output_rle_resource(
203 &self,
204 folder_path: &std::path::PathBuf,
205 path: String,
206 data: &[u8],
207 width: i64,
208 height: i64,
209 ) -> Result<Resource> {
210 let mut res = Resource {
211 path,
212 rle: Some(RLPixelInfo { width, height }),
213 ..Default::default()
214 };
215 let decompressed = rl_decompress(MemReaderRef::new(data), 4, None)?;
216 let outtype = self.config.image_type.unwrap_or(ImageOutputType::Png);
217 res.path = {
218 let mut pb = std::path::PathBuf::from(&res.path);
219 pb.set_extension(outtype.as_ref());
220 pb.to_string_lossy().to_string()
221 };
222 let path = folder_path.join(&res.path);
223 make_sure_dir_exists(&path)?;
224 let img = ImageData {
225 width: width as u32,
226 height: height as u32,
227 color_type: ImageColorType::Bgra,
228 depth: 8,
229 data: decompressed,
230 };
231 encode_img(img, outtype, &path.to_string_lossy(), &self.config)?;
232 Ok(res)
233 }
234
235 fn output_bc7_resource(
236 &self,
237 folder_path: &std::path::PathBuf,
238 path: String,
239 data: &[u8],
240 width: i64,
241 height: i64,
242 ) -> Result<Resource> {
243 let mut res = Resource {
244 path,
245 bc7: Some(BC7PixelInfo { width, height }),
246 ..Default::default()
247 };
248 let dst_size = (width * height * 4) as usize;
249 let mut decompressed_block = vec![0u8; dst_size];
250 let variant = block_compression::CompressionVariant::BC7(BC7Settings::alpha_basic());
251 let blocks_bytes = variant.blocks_byte_size(width as u32, height as u32);
252 if data.len() != blocks_bytes {
253 return Err(anyhow::anyhow!(
254 "BC7 compressed data size {} does not match expected size {} for image size {}x{}",
255 data.len(),
256 blocks_bytes,
257 width,
258 height
259 ));
260 }
261 block_compression::decode::decompress_blocks_as_rgba8(
262 variant,
263 width as u32,
264 height as u32,
265 data,
266 &mut decompressed_block,
267 );
268 let outtype = self.config.image_type.unwrap_or(ImageOutputType::Png);
269 res.path = {
270 let mut pb = std::path::PathBuf::from(&res.path);
271 pb.set_extension(outtype.as_ref());
272 pb.to_string_lossy().to_string()
273 };
274 let path = folder_path.join(&res.path);
275 make_sure_dir_exists(&path)?;
276 let img = ImageData {
277 width: width as u32,
278 height: height as u32,
279 color_type: ImageColorType::Rgba,
280 depth: 8,
281 data: decompressed_block,
282 };
283 encode_img(img, outtype, &path.to_string_lossy(), &self.config)?;
284 Ok(res)
285 }
286}
287
288#[derive(Debug, Deserialize, Serialize)]
289struct TlgInfo {
290 metadata: HashMap<String, String>,
291}
292
293impl TlgInfo {
294 fn from_tlg(tlg: &Tlg, encoding: Encoding) -> Self {
295 let mut metadata = HashMap::new();
296 for (k, v) in &tlg.tags {
297 let k = if let Ok(s) = decode_to_string(encoding, &k, true) {
298 s
299 } else {
300 format!(
301 "base64:{}",
302 base64::engine::general_purpose::STANDARD.encode(k)
303 )
304 };
305 let v = if let Ok(s) = decode_to_string(encoding, &v, true) {
306 s
307 } else {
308 format!(
309 "base64:{}",
310 base64::engine::general_purpose::STANDARD.encode(v)
311 )
312 };
313 metadata.insert(k, v);
314 }
315 Self { metadata }
316 }
317
318 fn to_tlg_tags(&self, encoding: Encoding) -> Result<HashMap<Vec<u8>, Vec<u8>>> {
319 let mut tags = HashMap::new();
320 for (k, v) in &self.metadata {
321 let k = if k.starts_with("base64:") {
322 base64::engine::general_purpose::STANDARD.decode(&k[7..])?
323 } else {
324 encode_string(encoding, k, false)?
325 };
326 let v = if v.starts_with("base64:") {
327 base64::engine::general_purpose::STANDARD.decode(&v[7..])?
328 } else {
329 encode_string(encoding, v, false)?
330 };
331 tags.insert(k, v);
332 }
333 Ok(tags)
334 }
335}
336
337#[derive(Debug, Deserialize, Serialize)]
338struct RLPixelInfo {
339 width: i64,
340 height: i64,
341}
342
343#[derive(Debug, Deserialize, Serialize)]
344struct BC7PixelInfo {
345 width: i64,
346 height: i64,
347}
348
349#[derive(Debug, Default, Deserialize, Serialize)]
350struct Resource {
351 path: String,
352 #[serde(skip_serializing_if = "Option::is_none")]
353 tlg: Option<TlgInfo>,
354 #[serde(skip_serializing_if = "Option::is_none")]
355 rle: Option<RLPixelInfo>,
356 #[serde(skip_serializing_if = "Option::is_none")]
357 bc7: Option<BC7PixelInfo>,
358}
359
360impl Script for Psb {
361 fn default_output_script_type(&self) -> OutputScriptType {
362 OutputScriptType::Custom
363 }
364
365 fn is_output_supported(&self, output: OutputScriptType) -> bool {
366 matches!(output, OutputScriptType::Custom)
367 }
368
369 fn default_format_type(&self) -> FormatOptions {
370 FormatOptions::None
371 }
372
373 fn custom_output_extension<'a>(&'a self) -> &'a str {
374 "json"
375 }
376
377 fn custom_export(&self, filename: &std::path::Path, encoding: Encoding) -> Result<()> {
378 let mut data = self.psb.to_json();
379 let mut resources = Vec::new();
380 let mut extra_resources = Vec::new();
381 let folder_path = {
382 let mut pb = filename.to_path_buf();
383 pb.set_extension("");
384 pb
385 };
386 for (i, data) in self.psb.resources().iter().enumerate() {
387 let i = i as u64;
388 let res_path = self.psb.root().find_resource_key(i, vec![]);
389 if let Some(path) = &res_path {
390 if path.len() >= 2 && *path.last().unwrap() == "pixel" {
391 let pb_data = self.psb.root();
392 let mut pb_data = &pb_data[*path.first().unwrap()];
393 for p in path.iter().take(path.len() - 1).skip(1) {
394 pb_data = &pb_data[*p];
395 }
396 let width = pb_data["width"].as_i64();
397 let height = pb_data["height"].as_i64();
398 let compress = pb_data["compress"].as_str();
399 let type_ = pb_data["type"].as_str();
400 if compress.is_some_and(|s| s == "RL") && (width.is_none() || height.is_none())
401 {
402 eprintln!(
403 "Warning: Resource {:?} is marked as RL compressed but width/height is missing (width={:?}, height={:?})",
404 path, pb_data["width"], pb_data["height"]
405 );
406 crate::COUNTER.inc_warning();
407 }
408 if type_.is_some_and(|s| s == "BC7") && (width.is_none() || height.is_none()) {
409 eprintln!(
410 "Warning: Resource {:?} is marked as BC7 compressed but width/height is missing (width={:?}, height={:?})",
411 path, pb_data["width"], pb_data["height"]
412 );
413 crate::COUNTER.inc_warning();
414 }
415 if let (Some(w), Some(h), Some(c)) = (width, height, compress) {
416 if c == "RL" {
417 let res_name: Vec<_> = path
418 .iter()
419 .take(path.len() - 1)
420 .map(|s| s.to_string())
421 .collect();
422 let res_name = res_name.join("/");
423 let res_name = sanitize_path(&res_name);
424 let res =
425 self.output_rle_resource(&folder_path, res_name, data, w, h)?;
426 resources.push(res);
427 continue;
428 }
429 }
430 if let (Some(w), Some(h), Some(t)) = (width, height, type_) {
431 if t == "BC7" {
432 let res_name: Vec<_> = path
433 .iter()
434 .take(path.len() - 1)
435 .map(|s| s.to_string())
436 .collect();
437 let res_name = res_name.join("/");
438 let res_name = sanitize_path(&res_name);
439 let res =
440 self.output_bc7_resource(&folder_path, res_name, data, w, h)?;
441 resources.push(res);
442 continue;
443 }
444 }
445 }
446 }
447 let res_name = res_path
448 .map(|s| s.join("/"))
449 .unwrap_or(format!("res_{}", i));
450 let res_name = sanitize_path(&res_name);
451 let res = self.output_resource(&folder_path, res_name, data)?;
452 resources.push(res);
453 }
454 for (i, data) in self.psb.extra().iter().enumerate() {
455 let i = i as u64;
456 let res_name = self
457 .psb
458 .root()
459 .find_resource_key(i, vec![])
460 .map(|s| format!("extra_{}", s.join("/")))
461 .unwrap_or(format!("extra_res_{}", i));
462 let res_name = sanitize_path(&res_name);
463 let res = self.output_resource(&folder_path, res_name, data)?;
464 extra_resources.push(res);
465 }
466 data["resources"] = json::parse(&serde_json::to_string(&resources)?)?;
467 data["extra_resources"] = json::parse(&serde_json::to_string(&extra_resources)?)?;
468 let s = json::stringify_pretty(data, 2);
469 let s = encode_string(encoding, &s, false)?;
470 let mut file = std::fs::File::create(filename)?;
471 file.write_all(&s)?;
472 Ok(())
473 }
474
475 fn custom_import<'a>(
476 &'a self,
477 custom_filename: &'a str,
478 file: Box<dyn WriteSeek + 'a>,
479 encoding: Encoding,
480 output_encoding: Encoding,
481 ) -> Result<()> {
482 create_file(
483 custom_filename,
484 file,
485 encoding,
486 output_encoding,
487 &self.config,
488 )
489 }
490}
491
492fn read_resource(
493 folder_path: &std::path::PathBuf,
494 res: &Resource,
495 encoding: Encoding,
496 cfg: &ExtraConfig,
497) -> Result<Vec<u8>> {
498 if let Some(tlg) = &res.tlg {
499 let path = folder_path.join(&res.path);
500 let imgfmt = ImageOutputType::try_from(path.as_path())?;
501 let mut img = decode_img(imgfmt, &path.to_string_lossy())?;
502 if img.depth != 8 {
503 return Err(anyhow::anyhow!(
504 "Only 8-bit images are supported for TLG conversion"
505 ));
506 }
507 let color_type = match img.color_type {
508 ImageColorType::Bgr => TlgColorType::Bgr24,
509 ImageColorType::Bgra => TlgColorType::Bgra32,
510 ImageColorType::Grayscale => TlgColorType::Grayscale8,
511 ImageColorType::Rgb => {
512 convert_rgb_to_bgr(&mut img)?;
513 TlgColorType::Bgr24
514 }
515 ImageColorType::Rgba => {
516 convert_rgba_to_bgra(&mut img)?;
517 TlgColorType::Bgra32
518 }
519 };
520 let tlg = Tlg {
521 width: img.width,
522 height: img.height,
523 version: 5,
524 color: color_type,
525 data: img.data,
526 tags: tlg.to_tlg_tags(encoding)?,
527 };
528 let mut writer = MemWriter::new();
529 save_tlg(&tlg, &mut writer)?;
530 Ok(writer.into_inner())
531 } else if let Some(rle) = &res.rle {
532 let path = folder_path.join(&res.path);
533 let imgfmt = ImageOutputType::try_from(path.as_path())?;
534 let mut img = decode_img(imgfmt, &path.to_string_lossy())?;
535 if img.depth != 8 {
536 return Err(anyhow::anyhow!(
537 "Only 8-bit images are supported for RLE conversion"
538 ));
539 }
540 if img.color_type == ImageColorType::Rgba {
541 convert_rgba_to_bgra(&mut img)?;
542 } else if img.color_type == ImageColorType::Rgb {
543 convert_rgb_to_bgr(&mut img)?;
544 convert_bgr_to_bgra(&mut img)?;
545 } else if img.color_type == ImageColorType::Bgr {
546 convert_bgr_to_bgra(&mut img)?;
547 }
548 if img.color_type != ImageColorType::Bgra {
549 return Err(anyhow::anyhow!(
550 "Only BGRA images are supported for RLE conversion"
551 ));
552 }
553 if img.width as i64 != rle.width {
554 eprintln!(
555 "Warning: Image width {} does not match RLE width {}",
556 img.width, rle.width
557 );
558 crate::COUNTER.inc_warning();
559 }
560 if img.height as i64 != rle.height {
561 eprintln!(
562 "Warning: Image height {} does not match RLE height {}",
563 img.height, rle.height
564 );
565 crate::COUNTER.inc_warning();
566 }
567 let compressed = rl_compress(MemReaderRef::new(&img.data), 4)?;
568 Ok(compressed)
569 } else if let Some(bc7) = &res.bc7 {
570 let path = folder_path.join(&res.path);
571 let imgfmt = ImageOutputType::try_from(path.as_path())?;
572 let mut img = decode_img(imgfmt, &path.to_string_lossy())?;
573 if img.depth != 8 {
574 return Err(anyhow::anyhow!(
575 "Only 8-bit images are supported for BC7 conversion"
576 ));
577 }
578 if img.width % 4 != 0 || img.height % 4 != 0 {
579 return Err(anyhow::anyhow!(
580 "Image dimensions must be multiples of 4 for BC7 conversion (width={}, height={})",
581 img.width,
582 img.height
583 ));
584 }
585 if bc7.height != img.height as i64 {
586 eprintln!(
587 "Warning: Image height {} does not match BC7 height {}",
588 img.height, bc7.height
589 );
590 crate::COUNTER.inc_warning();
591 }
592 if bc7.width != img.width as i64 {
593 eprintln!(
594 "Warning: Image width {} does not match BC7 width {}",
595 img.width, bc7.width
596 );
597 crate::COUNTER.inc_warning();
598 }
599 convert_to_rgba(&mut img)?;
600 let variant = block_compression::CompressionVariant::BC7(cfg.bc7.into());
601 let dst_size = variant.blocks_byte_size(img.width, img.height);
602 let mut compressed = vec![0u8; dst_size as usize];
603 block_compression::encode::compress_rgba8(
604 variant,
605 &img.data,
606 &mut compressed,
607 img.width,
608 img.height,
609 img.width * 4,
610 );
611 Ok(compressed)
612 } else {
613 let path = folder_path.join(&res.path);
614 Ok(std::fs::read(&path)?)
615 }
616}
617
618fn create_file<'a>(
619 custom_filename: &'a str,
620 mut writer: Box<dyn WriteSeek + 'a>,
621 encoding: Encoding,
622 output_encoding: Encoding,
623 cfg: &ExtraConfig,
624) -> Result<()> {
625 let input = read_file(custom_filename)?;
626 let s = decode_to_string(output_encoding, &input, true)?;
627 let data = json::parse(&s)?;
628 let resources: Vec<Resource> = serde_json::from_str(&data["resources"].dump())?;
629 let extra_resources: Vec<Resource> = serde_json::from_str(&data["extra_resources"].dump())?;
630 let mut psb = VirtualPsbFixed::with_json(&data)?;
631 psb.header_mut().encryption = 0; let folder_path = {
633 let mut pb = std::path::PathBuf::from(custom_filename);
634 pb.set_extension("");
635 pb
636 };
637 for res in resources {
638 let res = read_resource(&folder_path, &res, encoding, cfg)?;
639 psb.resources_mut().push(res);
640 }
641 for res in extra_resources {
642 let res = read_resource(&folder_path, &res, encoding, cfg)?;
643 psb.extra_mut().push(res);
644 }
645 let psb = psb.to_psb(false);
646 psb.finish_v4(&mut writer)
647 .map_err(|e| anyhow::anyhow!("Failed to write PSB file: {:?}", e))?;
648 Ok(())
649}