feat(viewer): present subtitles, fonts, PNG + text in the PAK browser
Decode and present four more PAK item types in the pack browser's detail
pane. All content is decoded off-thread in build_pak_rows and attached to
each PakRow, then rendered with egui's own texture facilities — the Bevy
texture pipeline and egui's global font system are untouched.
formats crate (Bevy-free, reusable by CLI):
- ixud: parse IXUD localized-string tables as subtitle cue lists (UTF-16BE
pool, SUBTITLE header + (text, timecode) pairs -> Vec<Cue{start,end,text}>).
- font: OTF/TTF/ttcf metadata (family / faces / glyphs) via ttf-parser.
viewer:
- PakRow gains `content: PakContent` (Subtitle | Font | Png | Text | None);
classify_content fills it for IXUD / fonts / PNG / xml+text within the
existing decode budget. Font samples are pre-rasterized off-thread with
ab_glyph into an image (ImageRgba) — ab_glyph returns Option/Result at every
step, so a bad font yields no sample instead of panicking (the earlier
egui set_fonts approach crashed the app on atlas rebuild).
- draw_pak_browser dispatches on content: subtitle cue table, font metadata +
sample image, PNG image, scrollable text; else the existing IDXD detail.
Borrow-split PakView instead of cloning the (now heavy) rows each frame;
one hash-keyed egui texture cache serves both PNG and font-sample images.
Subtitle<->movie auto-matching is deferred: movie filenames don't hash to the
IXUD keys and no cross-reference table was found (the link is an internal
MSG_DEMO id), so the movie player gets no subtitle track yet.
Tests: ixud unit tests; disc tests for the real English subtitle track
(cues + timecodes), eng/deu localization, font metadata, and off-thread
rasterization of the real font.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
64
crates/sylpheed-formats/src/font.rs
Normal file
64
crates/sylpheed-formats/src/font.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
//! Font metadata for the embedded UI fonts.
|
||||
//!
|
||||
//! The packs carry sfnt fonts (`\x00\x01\x00\x00` TrueType, `OTTO` OpenType/CFF)
|
||||
//! and TrueType Collections (`ttcf`). We only need light metadata to *present*
|
||||
//! them — family name, face count, glyph count — which `ttf-parser` extracts
|
||||
//! safely (malformed input yields `None`, never a panic). The raw bytes are
|
||||
//! handed to egui separately for a live sample.
|
||||
|
||||
use ttf_parser::{fonts_in_collection, name_id, Face};
|
||||
|
||||
/// Light font metadata for the viewer.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct FontInfo {
|
||||
/// "TrueType", "OpenType (CFF)", or "TrueType Collection".
|
||||
pub kind: &'static str,
|
||||
/// Family / full name (best-effort; empty if the name table is unreadable).
|
||||
pub family: String,
|
||||
/// Number of faces (1 for a plain sfnt, N for a `ttcf` collection).
|
||||
pub faces: u32,
|
||||
/// Glyph count of the first face.
|
||||
pub glyphs: u16,
|
||||
}
|
||||
|
||||
/// Whether `bytes` starts with a recognized font signature.
|
||||
pub fn is_font(bytes: &[u8]) -> bool {
|
||||
matches!(
|
||||
bytes.get(0..4),
|
||||
Some(b"\x00\x01\x00\x00") | Some(b"OTTO") | Some(b"true") | Some(b"ttcf")
|
||||
)
|
||||
}
|
||||
|
||||
/// Extract light metadata, or `None` if the font can't be parsed.
|
||||
pub fn parse_info(bytes: &[u8]) -> Option<FontInfo> {
|
||||
let is_collection = bytes.get(0..4) == Some(b"ttcf");
|
||||
let faces = fonts_in_collection(bytes).unwrap_or(1).max(1);
|
||||
|
||||
// Face 0 gives the representative family + glyph count.
|
||||
let face = Face::parse(bytes, 0).ok()?;
|
||||
let glyphs = face.number_of_glyphs();
|
||||
|
||||
let kind = if is_collection {
|
||||
"TrueType Collection"
|
||||
} else if bytes.get(0..4) == Some(b"OTTO") {
|
||||
"OpenType (CFF)"
|
||||
} else {
|
||||
"TrueType"
|
||||
};
|
||||
|
||||
// Prefer the typographic/full family name; fall back to any readable name.
|
||||
let family = face
|
||||
.names()
|
||||
.into_iter()
|
||||
.filter(|n| n.name_id == name_id::FULL_NAME || n.name_id == name_id::FAMILY)
|
||||
.find_map(|n| n.to_string())
|
||||
.or_else(|| face.names().into_iter().find_map(|n| n.to_string()))
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(FontInfo {
|
||||
kind,
|
||||
family,
|
||||
faces,
|
||||
glyphs,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user