features(libtlg-rs): Add tlg5 encode support

This commit is contained in:
2025-08-14 16:12:30 +08:00
parent 92b063e8bf
commit 53a1239295
11 changed files with 508 additions and 5 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
target/
.vscode/

4
Cargo.lock generated
View File

@@ -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",

View File

@@ -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

View File

@@ -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<T> = std::result::Result<T, TlgError>;
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.

63
libtlg-rs/src/save_tlg.rs Normal file
View File

@@ -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<W: Write + Seek>(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(())
}

228
libtlg-rs/src/slide.rs Normal file
View File

@@ -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<u8>,
map: Vec<i32>,
chains: Vec<Chain>,
text2: Vec<u8>,
map2: Vec<i32>,
chains2: Vec<Chain>,
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<u8>) -> 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);
}
}

View File

@@ -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<u32>;
@@ -18,3 +20,20 @@ impl<R: Read> 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<W: Write> 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])
}
}

107
libtlg-rs/src/tlg5_saver.rs Normal file
View File

@@ -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<W: Write + Seek>(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(())
}

View File

@@ -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),
}
}
}

View File

@@ -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" }

View File

@@ -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");
}
}
}