feat(formats,viewer): decode T8aD textures, RATC bundles, LSTA sprites
Add three static PAK-format decoders in the Bevy-free formats crate and present them in the explorer: - t8ad.rs: linear 32bpp A8R8G8B8 2D texture (UI/HUD art). Dims at 0x14/0x18, header size keyed by the type field @0x1c (container-safe). Decodes the ~85% RGBA variants; defers the rest (likely DXT) rather than misdecoding. - ratc.rs: nested resource bundle — lists named children by magic scan. - lsta.rs: sprite list — walks the inline T8aD frames. Viewer: PakContent gains T8ad/Lsta/Ratc, decoded off-thread and bounded by an 8M-texel per-entry cap; detail views show the texture, a sprite grid, and a child list with thumbnails. Decoded images carry a "colours unverified" note — channel-order/sRGB stays on the dynamic-RE backlog. Tests: 12 new unit + 3 real-disc (T8aD >=70% decode over the hangar pack, LSTA frames, RATC named children incl. a decodable T8aD). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -154,10 +154,26 @@ pub enum PakContent {
|
||||
},
|
||||
/// Decoded PNG image (RGBA8), shown via an egui texture.
|
||||
Png(ImageRgba),
|
||||
/// A decoded T8aD 2D texture.
|
||||
T8ad(ImageRgba),
|
||||
/// An LSTA sprite list — inline T8aD frames.
|
||||
Lsta(Vec<ImageRgba>),
|
||||
/// A RATC bundle — its listed children (T8aD children carry a decoded image).
|
||||
Ratc(Vec<RatcEntry>),
|
||||
/// Plain-text / XML payload, with its encoding label.
|
||||
Text { text: String, encoding: String },
|
||||
}
|
||||
|
||||
/// One child in a presented RATC bundle.
|
||||
#[derive(Clone)]
|
||||
pub struct RatcEntry {
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub size: usize,
|
||||
/// Decoded image when the child is a (decodable) T8aD within the pixel budget.
|
||||
pub image: Option<ImageRgba>,
|
||||
}
|
||||
|
||||
/// A plain RGBA8 image handed to the UI for an egui texture.
|
||||
#[derive(Clone)]
|
||||
pub struct ImageRgba {
|
||||
@@ -166,6 +182,19 @@ pub struct ImageRgba {
|
||||
pub rgba: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ImageRgba {
|
||||
fn from_t8ad(img: sylpheed_formats::T8adImage) -> Self {
|
||||
Self {
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
rgba: img.rgba,
|
||||
}
|
||||
}
|
||||
fn pixels(&self) -> usize {
|
||||
(self.width as usize) * (self.height as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// The parsed IDXD detail behind one entry, shown in the master-detail pane.
|
||||
#[derive(Clone)]
|
||||
pub struct PakDetail {
|
||||
@@ -253,9 +282,9 @@ pub struct PakView {
|
||||
pub rows: Vec<PakRow>,
|
||||
pub selected: Option<usize>,
|
||||
pub error: Option<String>,
|
||||
/// egui-side cache: (entry hash, texture) for the shown image — a PNG entry
|
||||
/// or a rasterized font sample.
|
||||
pub img_tex: Option<(u32, egui::TextureHandle)>,
|
||||
/// egui-side cache: (entry hash, textures) for the shown entry's image(s) —
|
||||
/// one for PNG/T8aD/font-sample, many for LSTA/RATC.
|
||||
pub img_tex: Option<(u32, Vec<egui::TextureHandle>)>,
|
||||
}
|
||||
|
||||
/// Total decompressed bytes we're willing to decode when labelling a pack's
|
||||
@@ -819,6 +848,11 @@ fn build_pak_rows(index: &[u8], data: Vec<u8>) -> Result<Vec<PakRow>, String> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const PAK_TEXT_CAP: usize = 512 * 1024;
|
||||
|
||||
/// Cap on decoded child-image texels per LSTA/RATC entry (~8 M texels = 32 MB
|
||||
/// RGBA). Further children past it are listed but not decoded to an image.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const PAK_IMAGE_TEXEL_BUDGET: usize = 8 * 1024 * 1024;
|
||||
|
||||
/// Classify a (non-IDXD) decompressed entry into presentable content: subtitle
|
||||
/// track, embedded font, PNG image, or plain text/XML. Runs off-thread.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -853,6 +887,58 @@ fn classify_content(payload: &[u8]) -> PakContent {
|
||||
});
|
||||
}
|
||||
}
|
||||
if sylpheed_formats::t8ad::is_t8ad(payload) {
|
||||
if let Some(img) = sylpheed_formats::t8ad::parse(payload) {
|
||||
return PakContent::T8ad(ImageRgba::from_t8ad(img));
|
||||
}
|
||||
}
|
||||
if sylpheed_formats::lsta::is_lsta(payload) {
|
||||
if let Some(frames) = sylpheed_formats::lsta::parse(payload) {
|
||||
let mut budget = PAK_IMAGE_TEXEL_BUDGET;
|
||||
let out: Vec<ImageRgba> = frames
|
||||
.into_iter()
|
||||
.map(ImageRgba::from_t8ad)
|
||||
.take_while(|f| {
|
||||
budget = budget.saturating_sub(f.pixels());
|
||||
budget > 0
|
||||
})
|
||||
.collect();
|
||||
if !out.is_empty() {
|
||||
return PakContent::Lsta(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
if sylpheed_formats::ratc::is_ratc(payload) {
|
||||
if let Some(children) = sylpheed_formats::ratc::parse(payload) {
|
||||
let mut budget = PAK_IMAGE_TEXEL_BUDGET;
|
||||
let entries: Vec<RatcEntry> = children
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
// Decode T8aD children up to the pixel budget; list the rest.
|
||||
let mut image = None;
|
||||
if c.kind == "T8aD" && budget > 0 {
|
||||
if let Some(img) = payload
|
||||
.get(c.offset..c.offset + c.size)
|
||||
.and_then(sylpheed_formats::t8ad::parse)
|
||||
.map(ImageRgba::from_t8ad)
|
||||
{
|
||||
budget = budget.saturating_sub(img.pixels());
|
||||
image = Some(img);
|
||||
}
|
||||
}
|
||||
RatcEntry {
|
||||
name: c.name,
|
||||
kind: c.kind,
|
||||
size: c.size,
|
||||
image,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if !entries.is_empty() {
|
||||
return PakContent::Ratc(entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
if payload.starts_with(b"<?xml") || vfs::identify_format(payload) == vfs::FileFormat::Text {
|
||||
let (mut text, encoding) = vfs::decode_text(payload);
|
||||
if text.len() > PAK_TEXT_CAP {
|
||||
|
||||
Reference in New Issue
Block a user