commit 937f8b07b42a3a8ce23a96bb86010a336e58e991 Author: lifegpc Date: Wed Jul 2 19:28:54 2025 +0800 Add TLG5 decode support diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b2686f4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,339 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "clap" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "libtlg-rs" +version = "0.1.0" +dependencies = [ + "overf", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "overf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63f45a6333db8b6985d6648e4f6c7f2aa814c660c0855c6f58ff67fea8b9f24b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tlg" +version = "0.1.0" +dependencies = [ + "clap", + "libtlg-rs", + "png", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..caf6255 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "3" +members = ["libtlg-rs", "tlg"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..33c26f4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 lifegpc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..80b0df0 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# libtlg-rs +Rust version of libtlg. diff --git a/libtlg-rs/Cargo.toml b/libtlg-rs/Cargo.toml new file mode 100644 index 0000000..fdfc51b --- /dev/null +++ b/libtlg-rs/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "libtlg-rs" +version = "0.1.0" +description = "Rust version of libtlg" +edition = "2024" +license = "MIT" + +[dependencies] +overf = "0.1" diff --git a/libtlg-rs/src/lib.rs b/libtlg-rs/src/lib.rs new file mode 100644 index 0000000..76ba8c2 --- /dev/null +++ b/libtlg-rs/src/lib.rs @@ -0,0 +1,31 @@ +//! A Rust library for processing TLG files. +mod load_tlg; +mod stream; +mod tvpgl; +mod types; +use std::io::{Read, Seek}; + +pub use types::{Tlg, TlgColorType, TlgError}; +/// The result type for TLG operations. +pub type Result = std::result::Result; +pub use load_tlg::load_tlg; + +/// Check if it's a valid TLG. +/// +/// 11 bytes are needed. +pub fn is_valid_tlg(data: &[u8]) -> bool { + if data.len() < 11 { + return false; + } + data == b"TLG0.0\x00sds\x1a" || data == b"TLG5.0\x00raw\x1a" || data == b"TLG6.0\x00raw\x1a" +} + +/// Check if it's a valid TLG. +/// +/// Same as [`is_valid_tlg`] +pub fn check_tlg(mut data: T) -> Result { + let mut header = [0; 11]; + data.rewind()?; + data.read_exact(&mut header)?; + Ok(is_valid_tlg(&header)) +} diff --git a/libtlg-rs/src/load_tlg.rs b/libtlg-rs/src/load_tlg.rs new file mode 100644 index 0000000..4576f3a --- /dev/null +++ b/libtlg-rs/src/load_tlg.rs @@ -0,0 +1,252 @@ +use crate::stream::ReadExt; +use crate::tvpgl::*; +use crate::*; +use overf::wrapping; +use std::io::SeekFrom; + +fn load_tlg5(src: &mut T) -> Result { + let colors = src.read_u8()?; + let width = src.read_u32()?; + let height = src.read_u32()?; + let blockheight = src.read_u32()?; + let color = match colors { + 3 => TlgColorType::Bgr24, + 4 => TlgColorType::Bgra32, + _ => return Err(TlgError::UnsupportedColorType(colors)), + }; + let blockcount = ((height - 1) / blockheight) + 1; + src.seek_relative(blockcount as i64 * 4)?; + let stride = width as usize * colors as usize; + let mut output_data = vec![0u8; width as usize * height as usize * colors as usize]; + let mut text = [0u8; 4096]; + let mut inbuf = vec![0u8; blockheight as usize * width as usize + 10]; + let mut outbuf = vec![vec![0u8; blockheight as usize * width as usize + 10]; colors as usize]; + let mut prevline: Option> = None; + let mut r = 0; + for y_blk in (0..height).step_by(blockheight as usize) { + for c in 0..colors { + let mark = src.read_u8()?; + let size = src.read_u32()?; + if mark == 0 { + src.read_exact(&mut inbuf[..size as usize])?; + r = tlg5_decompress_slide( + &mut outbuf[c as usize], + &inbuf[..size as usize], + size as usize, + &mut text, + r, + ); + } else { + src.read_exact(&mut outbuf[c as usize][..size as usize])?; + } + } + let y_lim = (y_blk + blockheight).min(height); + let mut outbufp = Vec::new(); + for c in 0..colors { + outbufp.push(outbuf[c as usize].as_slice()); + } + for y in y_blk..y_lim { + let current = &mut output_data[(y as usize * stride)..(y as usize * stride + stride)]; + match prevline.take() { + Some(prev) => match color { + TlgColorType::Bgr24 => { + tlg5_compose_colors3(current, &prev, &outbufp, width); + outbufp[0] = &outbufp[0][width as usize..]; + outbufp[1] = &outbufp[1][width as usize..]; + outbufp[2] = &outbufp[2][width as usize..]; + } + TlgColorType::Bgra32 => { + tlg5_compose_colors4(current, &prev, &outbufp, width); + outbufp[0] = &outbufp[0][width as usize..]; + outbufp[1] = &outbufp[1][width as usize..]; + outbufp[2] = &outbufp[2][width as usize..]; + outbufp[3] = &outbufp[3][width as usize..]; + } + _ => {} + }, + None => match color { + TlgColorType::Bgra32 => { + let mut current_pos = 0usize; + let mut pr = 0u8; + let mut pg = 0u8; + let mut pb = 0u8; + let mut pa = 0u8; + for x in 0..width as usize { + let mut b = outbufp[0][x]; + let g = outbufp[1][x]; + let mut r = outbufp[2][x]; + let a = outbufp[3][x]; + wrapping! { + b += g; + r += g; + pb += b; + pg += g; + pr += r; + pa += a; + } + current[current_pos] = pb; + current_pos += 1; + current[current_pos] = pg; + current_pos += 1; + current[current_pos] = pr; + current_pos += 1; + current[current_pos] = pa; + current_pos += 1; + } + outbufp[0] = &outbufp[0][width as usize..]; + outbufp[1] = &outbufp[1][width as usize..]; + outbufp[2] = &outbufp[2][width as usize..]; + outbufp[3] = &outbufp[3][width as usize..]; + } + TlgColorType::Bgr24 => { + let mut current_pos = 0usize; + let mut pr = 0u8; + let mut pg = 0u8; + let mut pb = 0u8; + for x in 0..width as usize { + let mut b = outbufp[0][x]; + let g = outbufp[1][x]; + let mut r = outbufp[2][x]; + wrapping! { + b += g; + r += g; + pb += b; + pg += g; + pr += r; + } + current[current_pos] = pb; + current_pos += 1; + current[current_pos] = pg; + current_pos += 1; + current[current_pos] = pr; + current_pos += 1; + } + outbufp[0] = &outbufp[0][width as usize..]; + outbufp[1] = &outbufp[1][width as usize..]; + outbufp[2] = &outbufp[2][width as usize..]; + } + _ => {} + }, + } + prevline = Some(current.to_vec()); + } + } + Ok(Tlg { + tags: Default::default(), + version: 5, + width, + height, + color, + data: output_data, + }) +} + +fn load_tlg6(_src: &mut T) -> Result { + Err(TlgError::Str("TLG6 is not supported yet".to_string())) +} + +fn internal_load_tlg(src: &mut T) -> Result { + let mut mark = [0; 11]; + src.read_exact(&mut mark)?; + if &mark == b"TLG5.0\x00raw\x1a" { + load_tlg5(src) + } else if &mark == b"TLG6.0\x00raw\x1a" { + load_tlg6(src) + } else { + Err(TlgError::InvalidFormat) + } +} + +/// Decode TLG image +pub fn load_tlg(mut src: T) -> Result { + src.rewind()?; + let mut mark = [0; 11]; + src.read_exact(&mut mark)?; + if &mark == b"TLG0.0\x00sds\x1a" { + let rawlen = src.read_u32()?; + let mut tlg = internal_load_tlg(&mut src)?; + let newlen = rawlen as u64 + 15; + src.seek(SeekFrom::Start(newlen))?; + let mut check = true; + while check { + let mut chunkname = [0; 4]; + if src.read(&mut chunkname)? != 4 { + break; + } + let chunksize = src.read_u32()?; + if &chunkname == b"tags" { + let mut tag = vec![0; chunksize as usize]; + src.read_exact(&mut tag)?; + let mut i = 0; + let len = tag.len(); + while i < len { + let mut namelen = 0usize; + let mut c = tag[i]; + let mut ok = true; + while c >= b'0' && c <= b'9' { + namelen = namelen * 10 + (c - b'0') as usize; + i += 1; + if i >= len { + ok = false; + break; + } + c = tag[i]; + } + if !ok { + break; + } + if c != b':' { + check = false; + break; + } + i += 1; + let name = tag[i..i + namelen].to_vec(); + i += namelen; + if i >= len { + break; + } + let mut valuelen = 0usize; + c = tag[i]; + ok = true; + while c >= b'0' && c <= b'9' { + valuelen = valuelen * 10 + (c - b'0') as usize; + i += 1; + if i >= len { + ok = false; + break; + } + c = tag[i]; + } + if !ok { + break; + } + if c != b':' { + check = false; + break; + } + i += 1; + let value = tag[i..i + valuelen].to_vec(); + i += valuelen; + if i >= len { + check = false; + break; + } + c = tag[i]; + if c != b',' { + check = false; + break; + } + i += 1; + tlg.tags.insert(name, value); + } + } else { + // skip the chunk + src.seek_relative(chunksize as i64)?; + } + } + Ok(tlg) + } else { + src.rewind()?; + internal_load_tlg(&mut src) + } +} diff --git a/libtlg-rs/src/stream.rs b/libtlg-rs/src/stream.rs new file mode 100644 index 0000000..c7216e4 --- /dev/null +++ b/libtlg-rs/src/stream.rs @@ -0,0 +1,20 @@ +use std::io::Read; + +pub trait ReadExt { + fn read_u32(&mut self) -> std::io::Result; + fn read_u8(&mut self) -> std::io::Result; +} + +impl ReadExt for R { + fn read_u32(&mut self) -> std::io::Result { + let mut buf = [0; 4]; + self.read_exact(&mut buf)?; + Ok(u32::from_le_bytes(buf)) + } + + fn read_u8(&mut self) -> std::io::Result { + let mut buf = [0; 1]; + self.read_exact(&mut buf)?; + Ok(buf[0]) + } +} diff --git a/libtlg-rs/src/tvpgl.rs b/libtlg-rs/src/tvpgl.rs new file mode 100644 index 0000000..850f1f5 --- /dev/null +++ b/libtlg-rs/src/tvpgl.rs @@ -0,0 +1,122 @@ +use overf::wrapping; + +pub fn tlg5_compose_colors3(outp: &mut [u8], upper: &[u8], buf: &[&[u8]], width: u32) { + let mut outpos = 0usize; + let mut upper_pos = 0usize; + let mut pr = 0u8; + let mut pg = 0u8; + let mut pb = 0u8; + for x in 0..width as usize { + let mut b = buf[0][x]; + let g = buf[1][x]; + let mut r = buf[2][x]; + wrapping! { + b += g; + r += g; + } + wrapping! { + pb += b; + pg += g; + pr += r; + } + outp[outpos] = wrapping! { pb + upper[upper_pos]}; + outpos += 1; + upper_pos += 1; + outp[outpos] = wrapping! { pg + upper[upper_pos]}; + outpos += 1; + upper_pos += 1; + outp[outpos] = wrapping! { pr + upper[upper_pos]}; + outpos += 1; + upper_pos += 1; + } +} + +pub fn tlg5_compose_colors4(outp: &mut [u8], upper: &[u8], buf: &[&[u8]], width: u32) { + let mut outpos = 0usize; + let mut upper_pos = 0usize; + let mut pr = 0u8; + let mut pg = 0u8; + let mut pb = 0u8; + let mut pa = 0u8; + for x in 0..width as usize { + let mut b = buf[0][x]; + let g = buf[1][x]; + let mut r = buf[2][x]; + let a = buf[3][x]; + wrapping! { + b += g; + r += g; + } + wrapping! { + pb += b; + pg += g; + pr += r; + pa += a; + } + outp[outpos] = wrapping! { pb + upper[upper_pos]}; + outpos += 1; + upper_pos += 1; + outp[outpos] = wrapping! { pg + upper[upper_pos]}; + outpos += 1; + upper_pos += 1; + outp[outpos] = wrapping! { pr + upper[upper_pos]}; + outpos += 1; + upper_pos += 1; + outp[outpos] = wrapping! { pa + upper[upper_pos]}; + outpos += 1; + upper_pos += 1; + } +} + +pub fn tlg5_decompress_slide( + out: &mut [u8], + inp: &[u8], + insize: usize, + text: &mut [u8], + mut r: usize, +) -> usize { + let mut flags = 0u32; + let mut inpos = 0usize; + let mut outpos = 0usize; + while inpos < insize { + wrapping! { flags >>= 1 }; + if flags & 256 == 0 { + flags = inp[inpos] as u32 | 0xff00; + inpos += 1; + } + if flags & 1 != 0 { + let mut mpos = + wrapping! { inp[inpos] as usize | ((inp[inpos + 1] as usize & 0xf) << 8) }; + let mut mlen = wrapping! { (inp[inpos + 1] as usize & 0xf0) >> 4 }; + inpos += 2; + mlen += 3; + if mlen == 18 { + mlen += inp[inpos] as usize; + inpos += 1; + } + while mlen > 0 { + out[outpos] = text[mpos]; + outpos += 1; + text[r] = text[mpos]; + r += 1; + mpos += 1; + wrapping! { + mpos &= 4095; + r &= 4095; + } + mlen -= 1; + } + } else { + let c = inp[inpos]; + inpos += 1; + out[outpos] = c; + outpos += 1; + text[r] = c; + r += 1; + wrapping! { + r &= 4095; + } + } + } + r +} diff --git a/libtlg-rs/src/types.rs b/libtlg-rs/src/types.rs new file mode 100644 index 0000000..abd1c17 --- /dev/null +++ b/libtlg-rs/src/types.rs @@ -0,0 +1,79 @@ +use std::{collections::HashMap, hash::Hash}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +/// TLG Color Type +pub enum TlgColorType { + /// Grayscale 8-bit + Grayscale8, + /// BGR 8-bit + Bgr24, + /// BGRA 8-bit + Bgra32, +} + +#[derive(Debug, Clone)] +/// TLG Image +pub struct Tlg { + /// Tag dictionary + pub tags: HashMap, Vec>, + /// TLG Version: 0=unknown, 5=v5, 6=v6 + pub version: u32, + /// Image width + pub width: u32, + /// Image height + pub height: u32, + /// Color type + pub color: TlgColorType, + /// Image data + pub data: Vec, +} + +#[derive(Debug)] +/// TLG Error +pub enum TlgError { + /// IO Error + Io(std::io::Error), + /// Invalid TLG format + InvalidFormat, + /// Unsupported color type + UnsupportedColorType(u8), + /// String type error + Str(String), +} + +impl std::fmt::Display for TlgError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TlgError::Io(e) => write!(f, "IO Error: {}", e), + TlgError::InvalidFormat => write!(f, "Invalid TLG format"), + TlgError::UnsupportedColorType(c) => write!(f, "Unsupported color type: {}", c), + TlgError::Str(s) => write!(f, "{}", s), + } + } +} + +impl From for TlgError { + fn from(err: std::io::Error) -> Self { + TlgError::Io(err) + } +} + +impl From for TlgError { + fn from(err: String) -> Self { + TlgError::Str(err) + } +} + +impl From<&str> for TlgError { + fn from(err: &str) -> Self { + TlgError::Str(err.to_string()) + } +} + +impl From<&String> for TlgError { + fn from(err: &String) -> Self { + TlgError::Str(err.clone()) + } +} + +impl std::error::Error for TlgError {} diff --git a/tlg/Cargo.toml b/tlg/Cargo.toml new file mode 100644 index 0000000..da5da11 --- /dev/null +++ b/tlg/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "tlg" +version = "0.1.0" +description = "Tools to process TLG image file." +edition = "2024" +license = "MIT" + +[dependencies] +clap = { version = "4.5", features = ["derive"] } +libtlg-rs = { path = "../libtlg-rs" } +png = "0.17" diff --git a/tlg/src/arg.rs b/tlg/src/arg.rs new file mode 100644 index 0000000..935a762 --- /dev/null +++ b/tlg/src/arg.rs @@ -0,0 +1,17 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(version, about, long_about)] +/// A command line tool to process TLG files. +pub struct Arg { + /// Path to the input TLG/PNG file. + pub input: String, + /// Path to the output TLG/PNG file. + pub output: Option, +} + +impl Arg { + pub fn parse() -> Self { + Parser::parse() + } +} diff --git a/tlg/src/main.rs b/tlg/src/main.rs new file mode 100644 index 0000000..feda7ed --- /dev/null +++ b/tlg/src/main.rs @@ -0,0 +1,54 @@ +mod arg; +use std::io::Seek; + +fn convert_bgr_to_rgb(data: &mut libtlg_rs::Tlg) { + match data.color { + libtlg_rs::TlgColorType::Bgra32 => { + for i in (0..data.data.len()).step_by(4) { + let b = data.data[i]; + data.data[i] = data.data[i + 2]; + data.data[i + 2] = b; // Swap red and blue + } + } + libtlg_rs::TlgColorType::Bgr24 => { + for i in (0..data.data.len()).step_by(3) { + let b = data.data[i]; + data.data[i] = data.data[i + 2]; + data.data[i + 2] = b; // Swap red and blue + } + } + _ => {} + } +} + +fn main() { + let args = arg::Arg::parse(); + let file = std::fs::File::open(&args.input).expect("Failed to open input file"); + let mut file = std::io::BufReader::new(file); + if libtlg_rs::check_tlg(&mut file).expect("Failed to check TLG format") { + let mut tlg = libtlg_rs::load_tlg(&mut file).expect("Failed to load TLG file"); + let output = match &args.output { + Some(output) => output.clone(), + None => { + let mut pb = std::path::PathBuf::from(&args.input); + pb.set_extension("png"); + pb.to_string_lossy().to_string() + } + }; + convert_bgr_to_rgb(&mut tlg); + let mut output_file = std::fs::File::create(&output).expect("Failed to create output file"); + let mut encoder = png::Encoder::new(&mut output_file, tlg.width, tlg.height); + encoder.set_color(match tlg.color { + libtlg_rs::TlgColorType::Bgra32 => png::ColorType::Rgba, + libtlg_rs::TlgColorType::Bgr24 => png::ColorType::Rgb, + libtlg_rs::TlgColorType::Grayscale8 => png::ColorType::Grayscale, + }); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder.write_header().expect("Failed to write PNG header"); + writer + .write_image_data(&tlg.data) + .expect("Failed to write PNG image data"); + } else { + file.rewind().expect("Failed to rewind file"); + } +}