From 53a1239295ee3e8be11c4b2bafc882f2236ab459 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Thu, 14 Aug 2025 16:12:30 +0800 Subject: [PATCH] features(libtlg-rs): Add tlg5 encode support --- .gitignore | 1 + Cargo.lock | 4 +- libtlg-rs/Cargo.toml | 8 +- libtlg-rs/src/lib.rs | 13 +- libtlg-rs/src/save_tlg.rs | 63 ++++++++++ libtlg-rs/src/slide.rs | 228 ++++++++++++++++++++++++++++++++++++ libtlg-rs/src/stream.rs | 19 +++ libtlg-rs/src/tlg5_saver.rs | 107 +++++++++++++++++ libtlg-rs/src/types.rs | 6 + tlg/Cargo.toml | 6 +- tlg/src/main.rs | 58 +++++++++ 11 files changed, 508 insertions(+), 5 deletions(-) create mode 100644 libtlg-rs/src/save_tlg.rs create mode 100644 libtlg-rs/src/slide.rs create mode 100644 libtlg-rs/src/tlg5_saver.rs diff --git a/.gitignore b/.gitignore index 2f7896d..a221ac1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ target/ +.vscode/ diff --git a/Cargo.lock b/Cargo.lock index df34628..787c1ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,7 +164,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libtlg-rs" -version = "0.1.4" +version = "0.2.0" dependencies = [ "lazy_static", "overf", @@ -253,7 +253,7 @@ dependencies = [ [[package]] name = "tlg" -version = "0.1.4" +version = "0.2.0" dependencies = [ "clap", "libtlg-rs", diff --git a/libtlg-rs/Cargo.toml b/libtlg-rs/Cargo.toml index ca0b1fb..88b7b38 100644 --- a/libtlg-rs/Cargo.toml +++ b/libtlg-rs/Cargo.toml @@ -1,11 +1,17 @@ [package] name = "libtlg-rs" -version = "0.1.4" +version = "0.2.0" description = "Rust version of libtlg" edition = "2024" license = "MIT" repository = "https://github.com/lifegpc/libtlg-rs" +[features] +encode = [] + [dependencies] lazy_static = "1" overf = "0.1" + +[package.metadata.docs.rs] +all-features = true diff --git a/libtlg-rs/src/lib.rs b/libtlg-rs/src/lib.rs index 0d8ecda..64c348f 100644 --- a/libtlg-rs/src/lib.rs +++ b/libtlg-rs/src/lib.rs @@ -1,6 +1,12 @@ //! A Rust library for processing TLG files. mod load_tlg; +#[cfg(feature = "encode")] +mod save_tlg; +#[cfg(feature = "encode")] +mod slide; mod stream; +#[cfg(feature = "encode")] +mod tlg5_saver; mod tvpgl; mod types; use std::io::{Read, Seek}; @@ -9,6 +15,9 @@ pub use types::{Tlg, TlgColorType, TlgError}; /// The result type for TLG operations. pub type Result = std::result::Result; pub use load_tlg::load_tlg; +#[cfg(feature = "encode")] +#[cfg_attr(docsrs, doc(cfg(feature = "encode")))] +pub use save_tlg::save_tlg; /// Check if it's a valid TLG. /// @@ -17,7 +26,9 @@ pub fn is_valid_tlg(data: &[u8]) -> bool { if data.len() < 11 { return false; } - data.starts_with(b"TLG0.0\x00sds\x1a") || data.starts_with(b"TLG5.0\x00raw\x1a") || data.starts_with(b"TLG6.0\x00raw\x1a") + data.starts_with(b"TLG0.0\x00sds\x1a") + || data.starts_with(b"TLG5.0\x00raw\x1a") + || data.starts_with(b"TLG6.0\x00raw\x1a") } /// Check if it's a valid TLG. diff --git a/libtlg-rs/src/save_tlg.rs b/libtlg-rs/src/save_tlg.rs new file mode 100644 index 0000000..5c1630d --- /dev/null +++ b/libtlg-rs/src/save_tlg.rs @@ -0,0 +1,63 @@ +use super::*; +use crate::stream::*; +use crate::tlg5_saver::save_tlg5; +use std::io::{Seek, Write}; + +/// Encode TLG image +#[cfg_attr(docsrs, doc(cfg(feature = "encode")))] +pub fn save_tlg(img: &Tlg, mut writer: W) -> Result<()> { + let colors = match img.color { + TlgColorType::Bgra32 => 4, + TlgColorType::Bgr24 => 3, + TlgColorType::Grayscale8 => 1, + }; + let img_size = img.width as usize * colors as usize * img.height as usize; + if img.data.len() < img_size { + return Err(TlgError::EncodeError(format!( + "Image data size too small: expected {}, got {}", + img_size, + img.data.len() + ))); + } + if img.tags.is_empty() { + if img.version == 5 { + return save_tlg5(img, &mut writer); + } else { + return Err(TlgError::EncodeError(format!( + "Unsupported TLG version: {}", + img.version + ))); + } + } + writer.write_all(b"TLG0.0\x00sds\x1a")?; + let rawlenpos = writer.stream_position()?; + writer.write_u32(0)?; // Placeholder for raw data length + if img.version == 5 { + save_tlg5(img, &mut writer)?; + } else { + return Err(TlgError::EncodeError(format!( + "Unsupported TLG version: {}", + img.version + ))); + } + let pos_save = writer.stream_position()?; + writer.seek(std::io::SeekFrom::Start(rawlenpos))?; + let size = pos_save - rawlenpos - 4; + writer.write_u32(size as u32)?; + writer.seek(std::io::SeekFrom::Start(pos_save))?; + writer.write_all(b"tags")?; + let mut ss = Vec::new(); + for (k, v) in &img.tags { + ss.write_all(k.len().to_string().as_bytes())?; + ss.write_all(b":")?; + ss.write_all(k)?; + ss.write_all(b"=")?; + ss.write_all(v.len().to_string().as_bytes())?; + ss.write_all(b":")?; + ss.write_all(v)?; + ss.write_all(b",")?; + } + writer.write_u32(ss.len() as u32)?; + writer.write_all(&ss)?; + Ok(()) +} diff --git a/libtlg-rs/src/slide.rs b/libtlg-rs/src/slide.rs new file mode 100644 index 0000000..424d65c --- /dev/null +++ b/libtlg-rs/src/slide.rs @@ -0,0 +1,228 @@ +//! Slide Compressor +#[derive(Clone, Copy)] +struct Chain { + prev: i32, + next: i32, +} + +const SLIDE_N: usize = 4096; +const SLIDE_M: usize = 18 + 255; +const TEXT_SIZE: usize = SLIDE_N + SLIDE_M; +const MAP_SIZE: usize = 256 * 256; + +pub struct SlideCompressor { + text: Vec, + map: Vec, + chains: Vec, + text2: Vec, + map2: Vec, + chains2: Vec, + s: i32, + s2: i32, +} + +impl SlideCompressor { + pub fn new() -> Self { + let mut data = Self { + text: vec![0; TEXT_SIZE], + map: vec![-1; MAP_SIZE], + chains: vec![Chain { prev: -1, next: -1 }; SLIDE_N], + text2: vec![0; TEXT_SIZE], + map2: vec![0; MAP_SIZE], + chains2: vec![Chain { prev: 0, next: 0 }; SLIDE_N], + s: 0, + s2: 0, + }; + for i in (0..SLIDE_N).rev() { + data.add_map(i as i32); + } + data + } + + fn add_map(&mut self, p: i32) { + let place = self.text[p as usize] as i32 + + ((self.text[(p as usize + 1) & (SLIDE_N - 1)] as i32) << 8); + if self.map[place as usize] == -1 { + self.map[place as usize] = p; + } else { + let old = self.map[place as usize]; + self.map[place as usize] = p; + self.chains[old as usize].prev = p; + self.chains[p as usize].next = old; + self.chains[p as usize].prev = -1; + } + } + + fn delete_map(&mut self, p: i32) { + let p_us = p as usize; + let mut n = self.chains[p_us].next; + if n != -1 { + self.chains[n as usize].prev = self.chains[p_us].prev; + } + n = self.chains[p_us].prev; + if n != -1 { + self.chains[n as usize].next = self.chains[p_us].next; + } else if self.chains[p_us].next != -1 { + let place = + self.text[p_us] as i32 + ((self.text[(p_us + 1) & (SLIDE_N - 1)] as i32) << 8); + self.map[place as usize] = self.chains[p_us].next; + } else { + let place = + self.text[p_us] as i32 + ((self.text[(p_us + 1) & (SLIDE_N - 1)] as i32) << 8); + self.map[place as usize] = -1; + } + self.chains[p_us].prev = -1; + self.chains[p_us].next = -1; + } + + fn get_match(&self, cur: &[u8], s: i32) -> (i32, i32) { + if cur.len() < 3 { + return (0, 0); + } + let mut curlen = cur.len() as i32; + let place = cur[0] as i32 + ((cur[1] as i32) << 8); + let mut pos = 0; + let mut maxlen = 0; + let mut head = self.map[place as usize]; + if head == -1 { + return (0, 0); + } + curlen -= 1; + while head != -1 { + let place_org = head; + if s == place_org || s == ((place_org + 1) & (SLIDE_N as i32 - 1)) { + head = self.chains[place_org as usize].next; + continue; + } + let mut p = place_org + 2; + let mut lim = (if (SLIDE_M as i32) < curlen { + SLIDE_M as i32 + } else { + curlen + }) + place_org; + if lim >= SLIDE_N as i32 { + if place_org <= s && s < SLIDE_N as i32 { + lim = s; + } else if s < (lim & (SLIDE_N as i32 - 1)) { + lim = s + SLIDE_N as i32; + } + } else { + if place_org <= s && s < lim { + lim = s; + } + } + let mut c_index = 2; + while p < lim + && (c_index as usize) < cur.len() + && self.text[p as usize] == cur[c_index as usize] + { + p += 1; + c_index += 1; + } + let matchlen = p - place_org; + if matchlen > maxlen { + maxlen = matchlen; + pos = place_org; + if matchlen == SLIDE_M as i32 { + return (maxlen, pos); + } + } + head = self.chains[place_org as usize].next; + } + (maxlen, pos) + } + + pub fn encode_into(&mut self, input: &[u8], output: &mut Vec) -> usize { + if input.is_empty() { + return 0; + } + let mut code = [0u8; 40]; + let mut codeptr: usize = 1; + let mut mask: u8 = 1; + code[0] = 0; + let mut idx: usize = 0; + let mut remain = input.len(); + let mut s = self.s; + while remain > 0 { + let (len, pos) = { + let (l, p) = self.get_match(&input[idx..], s); + (l, p) + }; + if len >= 3 { + code[0] |= mask; + if len >= 18 { + code[codeptr] = (pos & 0xff) as u8; + codeptr += 1; + code[codeptr] = (((pos & 0xf00) >> 8) as u8) | 0xf0; + codeptr += 1; + code[codeptr] = (len - 18) as u8; + codeptr += 1; + } else { + code[codeptr] = (pos & 0xff) as u8; + codeptr += 1; + code[codeptr] = (((pos & 0xf00) >> 8) as u8) | (((len - 3) as u8) << 4); + codeptr += 1; + } + let mut l = len as usize; + while l > 0 { + let c = input[idx]; + idx += 1; + remain -= 1; + l -= 1; + let s_prev = (s - 1) & (SLIDE_N as i32 - 1); + self.delete_map(s_prev); + self.delete_map(s); + if (s as usize) < SLIDE_M - 1 { + self.text[(s as usize) + SLIDE_N] = c; + } + self.text[s as usize] = c; + self.add_map(s_prev); + self.add_map(s); + s = (s + 1) & (SLIDE_N as i32 - 1); + } + } else { + let c = input[idx]; + idx += 1; + remain -= 1; + let s_prev = (s - 1) & (SLIDE_N as i32 - 1); + self.delete_map(s_prev); + self.delete_map(s); + if (s as usize) < SLIDE_M - 1 { + self.text[(s as usize) + SLIDE_N] = c; + } + self.text[s as usize] = c; + self.add_map(s_prev); + self.add_map(s); + s = (s + 1) & (SLIDE_N as i32 - 1); + code[codeptr] = c; + codeptr += 1; + } + mask <<= 1; + if mask == 0 { + output.extend_from_slice(&code[..codeptr]); + mask = 1; + codeptr = 1; + code[0] = 0; + } + } + if mask != 1 { + output.extend_from_slice(&code[..codeptr]); + } + self.s = s; + output.len() + } + + pub fn store(&mut self) { + self.s2 = self.s; + self.text2.copy_from_slice(&self.text); + self.map2.copy_from_slice(&self.map); + self.chains2.copy_from_slice(&self.chains); + } + + pub fn restore(&mut self) { + self.s = self.s2; + self.text.copy_from_slice(&self.text2); + self.map.copy_from_slice(&self.map2); + self.chains.copy_from_slice(&self.chains2); + } +} diff --git a/libtlg-rs/src/stream.rs b/libtlg-rs/src/stream.rs index c7216e4..0e2ef6b 100644 --- a/libtlg-rs/src/stream.rs +++ b/libtlg-rs/src/stream.rs @@ -1,4 +1,6 @@ use std::io::Read; +#[cfg(feature = "encode")] +use std::io::Write; pub trait ReadExt { fn read_u32(&mut self) -> std::io::Result; @@ -18,3 +20,20 @@ impl ReadExt for R { Ok(buf[0]) } } + +#[cfg(feature = "encode")] +pub trait WriteExt { + fn write_u32(&mut self, value: u32) -> std::io::Result<()>; + fn write_u8(&mut self, value: u8) -> std::io::Result<()>; +} + +#[cfg(feature = "encode")] +impl WriteExt for W { + fn write_u32(&mut self, value: u32) -> std::io::Result<()> { + self.write_all(&value.to_le_bytes()) + } + + fn write_u8(&mut self, value: u8) -> std::io::Result<()> { + self.write_all(&[value]) + } +} diff --git a/libtlg-rs/src/tlg5_saver.rs b/libtlg-rs/src/tlg5_saver.rs new file mode 100644 index 0000000..c64ab64 --- /dev/null +++ b/libtlg-rs/src/tlg5_saver.rs @@ -0,0 +1,107 @@ +use super::*; +use crate::slide::*; +use crate::stream::*; +use overf::wrapping; +use std::io::{Seek, Write}; + +const BLOCK_HEIGHT: usize = 4; + +pub fn save_tlg5(tlg: &Tlg, writer: &mut W) -> Result<()> { + writer.write_all(b"TLG5.0\x00raw\x1a")?; + let colors = match tlg.color { + TlgColorType::Bgra32 => 4, + TlgColorType::Bgr24 => 3, + TlgColorType::Grayscale8 => 1, + }; + writer.write_u8(colors)?; + writer.write_u32(tlg.width)?; + writer.write_u32(tlg.height)?; + writer.write_u32(BLOCK_HEIGHT as u32)?; + let blockcount = ((tlg.height as usize - 1) / BLOCK_HEIGHT) + 1; + let mut compressor = SlideCompressor::new(); + let mut written = [0; 4]; + let mut blocksizes = vec![0; blockcount]; + let mut cmpinbuf = vec![vec![0u8; tlg.width as usize * BLOCK_HEIGHT]; colors as usize]; + let blocksizepos = writer.stream_position()?; + for _ in 0..blockcount { + writer.write_all(b" ")?; // Place holders + } + let mut block = 0; + for blk_y in (0..tlg.height as usize).step_by(BLOCK_HEIGHT) { + let ylim = (blk_y + BLOCK_HEIGHT).min(tlg.height as usize); + let mut inp = 0; + for y in blk_y..ylim { + let upper = if y != 0 { + &tlg.data[(y - 1) * tlg.width as usize * colors as usize + ..y * tlg.width as usize * colors as usize] + } else { + &[] + }; + let mut upper_pos = 0; + let current = &tlg.data[y * tlg.width as usize * colors as usize + ..(y + 1) * tlg.width as usize * colors as usize]; + let mut current_pos = 0; + let mut prevcl = [0; 4]; + let mut val = [0; 4]; + for _ in 0..tlg.width as usize { + for c in 0..colors as usize { + let cl = if y != 0 { + let c = current[current_pos]; + current_pos += 1; + let p = upper[upper_pos]; + upper_pos += 1; + wrapping! { c - p } + } else { + let c = current[current_pos]; + current_pos += 1; + c + } as i32; + val[c] = wrapping! { cl - prevcl[c] }; + prevcl[c] = cl; + } + if colors == 1 { + cmpinbuf[0][inp] = val[0] as u8; + } else if colors == 3 { + cmpinbuf[0][inp] = wrapping! { val[0] - val[1] } as u8; + cmpinbuf[1][inp] = val[1] as u8; + cmpinbuf[2][inp] = wrapping! { val[2] - val[1] } as u8; + } else if colors == 4 { + cmpinbuf[0][inp] = wrapping! { val[0] - val[1] } as u8; + cmpinbuf[1][inp] = val[1] as u8; + cmpinbuf[2][inp] = wrapping! { val[2] - val[1] } as u8; + cmpinbuf[3][inp] = val[3] as u8; + } + inp += 1; + } + } + // LZSS + let mut blocksize = 0; + for c in 0..colors as usize { + compressor.store(); + let mut outbuf = Vec::new(); + let wrote = compressor.encode_into(&cmpinbuf[c][..inp], &mut outbuf); + if wrote < inp { + writer.write_u8(0)?; + writer.write_u32(wrote as u32)?; + writer.write_all(&outbuf)?; + blocksize += wrote + 4 + 1; + } else { + compressor.restore(); + writer.write_u8(1)?; + writer.write_u32(inp as u32)?; + writer.write_all(&cmpinbuf[c][..inp])?; + blocksize += inp + 4 + 1; + } + written[c] += wrote; + } + blocksizes[block] = blocksize; + block += 1; + } + let pos_save = writer.stream_position()?; + writer.seek(std::io::SeekFrom::Start(blocksizepos))?; + for i in 0..blockcount { + writer.write_u32(blocksizes[i] as u32)?; + } + writer.seek(std::io::SeekFrom::Start(pos_save))?; + Ok(()) +} diff --git a/libtlg-rs/src/types.rs b/libtlg-rs/src/types.rs index 39a9ab5..f04185b 100644 --- a/libtlg-rs/src/types.rs +++ b/libtlg-rs/src/types.rs @@ -43,6 +43,10 @@ pub enum TlgError { UnsupportedCompressedMethod(u8), /// String type error Str(String), + #[cfg(feature = "encode")] + #[cfg_attr(docsrs, doc(cfg(feature = "encode")))] + /// Encoding error + EncodeError(String), } impl std::fmt::Display for TlgError { @@ -56,6 +60,8 @@ impl std::fmt::Display for TlgError { write!(f, "Unsupported compressed method: {}", m) } TlgError::Str(s) => write!(f, "{}", s), + #[cfg(feature = "encode")] + TlgError::EncodeError(s) => write!(f, "Encoding error: {}", s), } } } diff --git a/tlg/Cargo.toml b/tlg/Cargo.toml index 8023601..850bdcf 100644 --- a/tlg/Cargo.toml +++ b/tlg/Cargo.toml @@ -1,11 +1,15 @@ [package] name = "tlg" -version = "0.1.4" +version = "0.2.0" description = "Tools to process TLG image file." edition = "2024" license = "MIT" repository = "https://github.com/lifegpc/libtlg-rs" +[features] +default = ["encode"] +encode = ["libtlg-rs/encode"] + [dependencies] clap = { version = "4.5", features = ["derive"] } libtlg-rs = { path = "../libtlg-rs" } diff --git a/tlg/src/main.rs b/tlg/src/main.rs index fe1cc9c..2e8a5a1 100644 --- a/tlg/src/main.rs +++ b/tlg/src/main.rs @@ -1,4 +1,6 @@ mod arg; +#[cfg(feature = "encode")] +use std::io::BufRead; use std::io::{Seek, Write}; fn convert_bgr_to_rgb(data: &mut libtlg_rs::Tlg) { @@ -66,5 +68,61 @@ fn main() { } } else { file.rewind().expect("Failed to rewind file"); + #[cfg(feature = "encode")] + { + let decoder = png::Decoder::new(file); + let mut reader = decoder.read_info().expect("Failed to read PNG info"); + let width = reader.info().width; + let height = reader.info().height; + if reader.info().bit_depth != png::BitDepth::Eight { + panic!("Unsupported bit depth: {:?}", reader.info().bit_depth); + } + let color_type = match reader.info().color_type { + png::ColorType::Rgba => libtlg_rs::TlgColorType::Bgra32, + png::ColorType::Rgb => libtlg_rs::TlgColorType::Bgr24, + png::ColorType::Grayscale => libtlg_rs::TlgColorType::Grayscale8, + _ => panic!("Unsupported color type: {:?}", reader.info().color_type), + }; + let imgsize = width as usize * height as usize * reader.info().color_type.samples(); + let mut data = vec![0u8; imgsize]; + reader + .next_frame(&mut data) + .expect("Failed to read PNG frame"); + let mut tags = std::collections::HashMap::new(); + let tags_path = get_relative_path(&args.input, "tags"); + if std::path::Path::new(&tags_path).exists() { + let tags_file = std::fs::File::open(&tags_path).expect("Failed to open tags file"); + let mut tags_reader = std::io::BufReader::new(tags_file); + let mut line = String::new(); + while tags_reader + .read_line(&mut line) + .expect("Failed to read line") + > 0 + { + if let Some(eq_pos) = line.find('=') { + let key = line[..eq_pos].trim().as_bytes().to_vec(); + let value = line[eq_pos + 1..].trim().as_bytes().to_vec(); + tags.insert(key, value); + } + line.clear(); + } + } + let mut tlg = libtlg_rs::Tlg { + tags, + version: 5, + width, + height, + color: color_type, + data, + }; + convert_bgr_to_rgb(&mut tlg); + let output = match &args.output { + Some(output) => output.clone(), + None => get_relative_path(&args.input, "tlg"), + }; + let mut output_file = + std::fs::File::create(&output).expect("Failed to create output file"); + libtlg_rs::save_tlg(&tlg, &mut output_file).expect("Failed to save TLG file"); + } } }