first commit
This commit is contained in:
commit
d390f3ce54
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/target
|
||||
|
||||
.idea
|
||||
3939
Cargo.lock
generated
Normal file
3939
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "compresscsdesign"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
eframe = "0.30.0"
|
||||
rfd = "0.15.2"
|
||||
egui-notify = "0.18.0"
|
||||
crc32fast = "1.3"
|
||||
sha2 = "0.10"
|
||||
6
readme.txt
Normal file
6
readme.txt
Normal file
@ -0,0 +1,6 @@
|
||||
构建流程:
|
||||
|
||||
1. 安装 Rust 和 Cargo 环境
|
||||
2. 运行 cargo run 进行调试
|
||||
3. 运行 cargo build -r 构建可执行文件
|
||||
(已构建 Windows 下可执行文件 "鹅鹅文件压缩器.exe" 于当前目录,可直接运行)
|
||||
BIN
src/assets/icon.bin
Normal file
BIN
src/assets/icon.bin
Normal file
Binary file not shown.
427
src/huffman.rs
Normal file
427
src/huffman.rs
Normal file
@ -0,0 +1,427 @@
|
||||
use std::{
|
||||
collections::{BinaryHeap, HashMap},
|
||||
io::{self, Read, Write},
|
||||
};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// Huffman 树节点
|
||||
#[derive(Eq, PartialEq)]
|
||||
struct Node {
|
||||
freq: u32,
|
||||
byte: Option<u8>,
|
||||
left: Option<Box<Node>>,
|
||||
right: Option<Box<Node>>,
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 实现 Ord/PartialOrd,使得 BinaryHeap 成为最小堆
|
||||
// -----------------------------------------------------
|
||||
impl PartialOrd for Node {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Node {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
// BinaryHeap 默认是大顶堆,但我们想要小顶堆效果,所以反转 freq
|
||||
other.freq.cmp(&self.freq)
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建 Huffman 树
|
||||
fn build_huffman_tree(freq_map: &HashMap<u8, u32>) -> Option<Box<Node>> {
|
||||
if freq_map.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// 按字节升序排序,保证同频时顺序稳定
|
||||
let mut keys: Vec<u8> = freq_map.keys().cloned().collect();
|
||||
keys.sort_unstable();
|
||||
|
||||
let mut heap = BinaryHeap::new();
|
||||
for b in keys {
|
||||
heap.push(Node {
|
||||
freq: freq_map[&b],
|
||||
byte: Some(b),
|
||||
left: None,
|
||||
right: None,
|
||||
});
|
||||
}
|
||||
|
||||
while heap.len() > 1 {
|
||||
let n1 = heap.pop().unwrap();
|
||||
let n2 = heap.pop().unwrap();
|
||||
let parent = Node {
|
||||
freq: n1.freq + n2.freq,
|
||||
byte: None,
|
||||
left: Some(Box::new(n1)),
|
||||
right: Some(Box::new(n2)),
|
||||
};
|
||||
heap.push(parent);
|
||||
}
|
||||
|
||||
let root = heap.pop().map(Box::new);
|
||||
|
||||
root
|
||||
}
|
||||
|
||||
/// 递归遍历 Huffman 树,构建 (字节 -> 比特序列) 的 map
|
||||
fn build_code_map(node: &Node, prefix: Vec<u8>, code_map: &mut HashMap<u8, Vec<u8>>) {
|
||||
if let Some(b) = node.byte {
|
||||
// 如果树只有一个节点,prefix 会是空,强行给它 [0]
|
||||
if prefix.is_empty() {
|
||||
code_map.insert(b, vec![0]);
|
||||
} else {
|
||||
code_map.insert(b, prefix);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 否则继续往左右子节点
|
||||
if let Some(ref left) = node.left {
|
||||
let mut left_path = prefix.clone();
|
||||
left_path.push(0);
|
||||
build_code_map(left, left_path, code_map);
|
||||
}
|
||||
if let Some(ref right) = node.right {
|
||||
let mut right_path = prefix.clone();
|
||||
right_path.push(1);
|
||||
build_code_map(right, right_path, code_map);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// BitWriter: 将比特流写入内存 buffer
|
||||
// -----------------------------------------------------
|
||||
struct BitWriter {
|
||||
buffer: Vec<u8>,
|
||||
current_byte: u8,
|
||||
bit_count: u8,
|
||||
}
|
||||
|
||||
impl BitWriter {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
buffer: Vec::new(),
|
||||
current_byte: 0,
|
||||
bit_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_bit(&mut self, bit: u8) {
|
||||
self.current_byte = (self.current_byte << 1) | (bit & 1);
|
||||
self.bit_count += 1;
|
||||
if self.bit_count == 8 {
|
||||
self.buffer.push(self.current_byte);
|
||||
self.current_byte = 0;
|
||||
self.bit_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) {
|
||||
if self.bit_count > 0 {
|
||||
self.current_byte <<= 8 - self.bit_count;
|
||||
self.buffer.push(self.current_byte);
|
||||
self.current_byte = 0;
|
||||
self.bit_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn into_inner(mut self) -> Vec<u8> {
|
||||
self.flush();
|
||||
self.buffer
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// BitReader: 从内存切片逐比特读
|
||||
// -----------------------------------------------------
|
||||
struct BitReader<'a> {
|
||||
data: &'a [u8],
|
||||
byte_pos: usize,
|
||||
bit_pos: u8,
|
||||
}
|
||||
|
||||
impl<'a> BitReader<'a> {
|
||||
fn new(data: &'a [u8]) -> Self {
|
||||
Self {
|
||||
data,
|
||||
byte_pos: 0,
|
||||
bit_pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_bit(&mut self) -> Option<u8> {
|
||||
if self.byte_pos >= self.data.len() {
|
||||
return None;
|
||||
}
|
||||
let byte = self.data[self.byte_pos];
|
||||
let bit = (byte >> (7 - self.bit_pos)) & 1;
|
||||
self.bit_pos += 1;
|
||||
if self.bit_pos == 8 {
|
||||
self.bit_pos = 0;
|
||||
self.byte_pos += 1;
|
||||
}
|
||||
Some(bit)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_stream<R: Read, W: Write, D: Write>(
|
||||
mut input: R,
|
||||
mut output: W,
|
||||
onprogress: &mut impl FnMut(usize),
|
||||
mut debug_out: Option<&mut D>,
|
||||
) -> io::Result<()> {
|
||||
// 1) 先把所有输入读到内存
|
||||
let mut buf = [0u8; 8192];
|
||||
let mut data = Vec::new();
|
||||
|
||||
let mut total_read = 0usize;
|
||||
let mut last_reported = 0usize;
|
||||
const REPORT_THRESHOLD: usize = 16 * 1024; // 16KB
|
||||
|
||||
loop {
|
||||
let n = input.read(&mut buf)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
data.extend_from_slice(&buf[..n]);
|
||||
total_read += n;
|
||||
|
||||
// 进度回调
|
||||
if total_read.saturating_sub(last_reported) >= REPORT_THRESHOLD {
|
||||
onprogress(total_read);
|
||||
last_reported = total_read;
|
||||
}
|
||||
}
|
||||
// 结束读输入时再更新一次
|
||||
onprogress(total_read);
|
||||
|
||||
// 2) 构建 freq_map
|
||||
let mut freq_map = HashMap::new();
|
||||
for &b in &data {
|
||||
*freq_map.entry(b).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
// 3) 构建 Huffman 树 & code_map
|
||||
let root = build_huffman_tree(&freq_map).unwrap();
|
||||
let mut code_map = HashMap::new();
|
||||
build_code_map(&root, Vec::new(), &mut code_map);
|
||||
|
||||
// 如果有 debug_out => 输出 frequency table
|
||||
if let Some(ref mut dbg) = debug_out {
|
||||
writeln!(dbg, "[Huffman encode] freq_map:")?;
|
||||
for (&byte, &freq) in &freq_map {
|
||||
writeln!(dbg, " byte = {:02X}, freq = {}", byte, freq)?;
|
||||
}
|
||||
writeln!(dbg, "[Huffman encode] code_map:")?;
|
||||
for (&byte, code_bits) in &code_map {
|
||||
// 将 bit 序列拼成字符串
|
||||
let bits_str = code_bits
|
||||
.iter()
|
||||
.map(|b| if *b == 0 { '0' } else { '1' })
|
||||
.collect::<String>();
|
||||
writeln!(dbg, " byte = {:02X}, code = {}", byte, bits_str)?;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 写 freq_table (256 个 u32,小端序)
|
||||
{
|
||||
let mut freq_array = vec![0u32; 256];
|
||||
for (&b, &f) in &freq_map {
|
||||
freq_array[b as usize] = f;
|
||||
}
|
||||
let mut freq_table_bytes = Vec::with_capacity(256 * 4);
|
||||
for &f in &freq_array {
|
||||
freq_table_bytes.extend_from_slice(&f.to_le_bytes());
|
||||
}
|
||||
output.write_all(&freq_table_bytes)?;
|
||||
}
|
||||
|
||||
// 5) 利用 code_map 把 data 编码为 bit_stream
|
||||
let mut bit_writer = BitWriter::new();
|
||||
let mut total_bits: u32 = 0;
|
||||
for &b in &data {
|
||||
if let Some(bits) = code_map.get(&b) {
|
||||
for &bit in bits {
|
||||
bit_writer.write_bit(bit);
|
||||
total_bits += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
bit_writer.flush();
|
||||
let bit_stream = bit_writer.into_inner();
|
||||
|
||||
// 6) 写 total_bits
|
||||
output.write_all(&total_bits.to_le_bytes())?;
|
||||
// 7) 写 bit_stream
|
||||
output.write_all(&bit_stream)?;
|
||||
|
||||
// 结束时 debug
|
||||
if let Some(ref mut dbg) = debug_out {
|
||||
writeln!(
|
||||
dbg,
|
||||
"[Huffman encode] data_len = {}, total_bits = {}, bit_stream_len(bytes) = {}",
|
||||
data.len(),
|
||||
total_bits,
|
||||
bit_stream.len()
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decode_stream<R: Read, W: Write, D: Write>(
|
||||
mut input: R,
|
||||
mut output: W,
|
||||
onprogress: &mut impl FnMut(usize),
|
||||
mut debug_out: Option<&mut D>,
|
||||
) -> io::Result<()> {
|
||||
let mut total_read = 0usize;
|
||||
let mut last_reported = 0usize;
|
||||
const REPORT_THRESHOLD: usize = 16 * 1024; // 16KB
|
||||
|
||||
// 帮助函数:读取固定大小到 buffer,并更新进度
|
||||
fn read_exact_progress<R: Read>(
|
||||
reader: &mut R,
|
||||
buf: &mut [u8],
|
||||
total_read: &mut usize,
|
||||
last_reported: &mut usize,
|
||||
onprogress: &mut impl FnMut(usize),
|
||||
) -> io::Result<()> {
|
||||
let mut offset = 0;
|
||||
while offset < buf.len() {
|
||||
let n = reader.read(&mut buf[offset..])?;
|
||||
if n == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"Unexpected EOF while reading Huffman stream",
|
||||
));
|
||||
}
|
||||
offset += n;
|
||||
*total_read += n;
|
||||
|
||||
// 回调
|
||||
if total_read.saturating_sub(*last_reported) >= REPORT_THRESHOLD {
|
||||
onprogress(*total_read);
|
||||
*last_reported = *total_read;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 1) 读取 freq_table
|
||||
let mut freq_table_bytes = vec![0u8; 256 * 4];
|
||||
read_exact_progress(
|
||||
&mut input,
|
||||
&mut freq_table_bytes,
|
||||
&mut total_read,
|
||||
&mut last_reported,
|
||||
onprogress,
|
||||
)?;
|
||||
|
||||
// 2) 构造 freq_map
|
||||
let mut freq_map = HashMap::new();
|
||||
for i in 0..256 {
|
||||
let freq = u32::from_le_bytes([
|
||||
freq_table_bytes[i * 4],
|
||||
freq_table_bytes[i * 4 + 1],
|
||||
freq_table_bytes[i * 4 + 2],
|
||||
freq_table_bytes[i * 4 + 3],
|
||||
]);
|
||||
if freq > 0 {
|
||||
freq_map.insert(i as u8, freq);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有 debug_out => 输出 frequency table
|
||||
if let Some(ref mut dbg) = debug_out {
|
||||
writeln!(dbg, "[Huffman decode] freq_map:")?;
|
||||
for (&byte, &freq) in &freq_map {
|
||||
writeln!(dbg, " byte = {:02X}, freq = {}", byte, freq)?;
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 重建 Huffman 树
|
||||
let root = build_huffman_tree(&freq_map).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Invalid Huffman frequency table",
|
||||
)
|
||||
})?;
|
||||
|
||||
// 4) 读取 total_bits
|
||||
let mut total_bits_buf = [0u8; 4];
|
||||
read_exact_progress(
|
||||
&mut input,
|
||||
&mut total_bits_buf,
|
||||
&mut total_read,
|
||||
&mut last_reported,
|
||||
onprogress,
|
||||
)?;
|
||||
let total_bits = u32::from_le_bytes(total_bits_buf) as usize;
|
||||
|
||||
// 5) 读取 bit_stream
|
||||
let bit_stream_len = (total_bits + 7) / 8;
|
||||
let mut bit_stream = vec![0u8; bit_stream_len];
|
||||
if bit_stream_len > 0 {
|
||||
read_exact_progress(
|
||||
&mut input,
|
||||
&mut bit_stream,
|
||||
&mut total_read,
|
||||
&mut last_reported,
|
||||
onprogress,
|
||||
)?;
|
||||
}
|
||||
|
||||
onprogress(total_read);
|
||||
|
||||
// 6) 解码
|
||||
let mut bit_reader = BitReader::new(&bit_stream);
|
||||
let mut decoded_buffer = Vec::with_capacity(8192);
|
||||
let mut current_node = &root;
|
||||
|
||||
for _ in 0..total_bits {
|
||||
let bit = bit_reader
|
||||
.read_bit()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "Bitstream ended early"))?;
|
||||
if bit == 0 {
|
||||
if let Some(ref left) = current_node.left {
|
||||
current_node = left;
|
||||
} else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Invalid Huffman bitstream: 0 leads to no left child",
|
||||
));
|
||||
}
|
||||
} else {
|
||||
if let Some(ref right) = current_node.right {
|
||||
current_node = right;
|
||||
} else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Invalid Huffman bitstream: 1 leads to no right child",
|
||||
));
|
||||
}
|
||||
}
|
||||
// 到达叶子节点 => 写入
|
||||
if let Some(b) = current_node.byte {
|
||||
decoded_buffer.push(b);
|
||||
current_node = &root;
|
||||
if decoded_buffer.len() >= 8192 {
|
||||
output.write_all(&decoded_buffer)?;
|
||||
decoded_buffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 写剩余数据
|
||||
if !decoded_buffer.is_empty() {
|
||||
output.write_all(&decoded_buffer)?;
|
||||
}
|
||||
|
||||
if let Some(ref mut dbg) = debug_out {
|
||||
writeln!(dbg, "[Huffman decode] Decoding finished. total_bits = {}", total_bits)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
27
src/lib.rs
Normal file
27
src/lib.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use crc32fast::Hasher as Crc32Hasher;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn compute_crc32_and_sha256(path: &Path) -> (Option<u32>, Option<String>) {
|
||||
let mut file = match File::open(path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return (None, None),
|
||||
};
|
||||
let mut buffer = Vec::new();
|
||||
if file.read_to_end(&mut buffer).is_err() {
|
||||
return (None, None);
|
||||
}
|
||||
|
||||
let mut crc_hasher = Crc32Hasher::new();
|
||||
crc_hasher.update(&buffer);
|
||||
let crc32 = Some(crc_hasher.finalize());
|
||||
|
||||
let mut sha_hasher = Sha256::new();
|
||||
sha_hasher.update(&buffer);
|
||||
let result = sha_hasher.finalize();
|
||||
let sha256 = Some(format!("{:x}", result));
|
||||
|
||||
(crc32, sha256)
|
||||
}
|
||||
378
src/lz77.rs
Normal file
378
src/lz77.rs
Normal file
@ -0,0 +1,378 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
io::{self, Read, Write},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Token {
|
||||
pub offset: u16,
|
||||
pub length: u8,
|
||||
pub next_char: u8,
|
||||
}
|
||||
|
||||
pub struct LZ77 {
|
||||
pub window_size: usize,
|
||||
pub lookahead_size: usize,
|
||||
}
|
||||
|
||||
impl LZ77 {
|
||||
pub fn new(window_size: usize, lookahead_size: usize) -> Self {
|
||||
Self {
|
||||
window_size,
|
||||
lookahead_size,
|
||||
}
|
||||
}
|
||||
|
||||
/// 在 window 中寻找与 lookahead 最长的匹配,返回 (offset, length) (usize, usize)。
|
||||
fn find_longest_match(&self, window: &[u8], lookahead: &[u8]) -> (usize, usize) {
|
||||
if lookahead.is_empty() || window.is_empty() {
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
let mut best_len = 0usize;
|
||||
let mut best_off = 0usize;
|
||||
|
||||
// 从 window 尾部往前搜索
|
||||
for start in (0..window.len()).rev() {
|
||||
let mut length = 0usize;
|
||||
while length < lookahead.len()
|
||||
&& length < self.lookahead_size
|
||||
&& start + length < window.len()
|
||||
&& window[start + length] == lookahead[length]
|
||||
{
|
||||
length += 1;
|
||||
}
|
||||
|
||||
if length > best_len {
|
||||
best_len = length;
|
||||
best_off = window.len() - start;
|
||||
if best_len == self.lookahead_size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(best_off, best_len)
|
||||
}
|
||||
|
||||
/// 将「匹配 Token」编码成 4 字节 [offset(2bytes,LE), length(1byte), next_char(1byte)]
|
||||
pub fn encode_token_to_bytes(token: &Token) -> [u8; 4] {
|
||||
let offset_bytes = token.offset.to_le_bytes();
|
||||
[
|
||||
offset_bytes[0],
|
||||
offset_bytes[1],
|
||||
token.length,
|
||||
token.next_char,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn compress_stream<R: Read, W: Write, D: Write>(
|
||||
&self,
|
||||
mut input: R,
|
||||
mut output: W,
|
||||
onprogress: &mut impl FnMut(usize),
|
||||
debug_out: Option<&mut D>,
|
||||
) -> io::Result<()> {
|
||||
let mut window: VecDeque<u8> = VecDeque::new();
|
||||
let mut lookahead = Vec::new();
|
||||
lookahead.reserve(self.lookahead_size);
|
||||
|
||||
let mut buffer = [0u8; 4096];
|
||||
|
||||
// 节流相关变量
|
||||
let mut total_read = 0usize;
|
||||
let mut last_reported = 0usize;
|
||||
const REPORT_THRESHOLD: usize = 16 * 1024; // 16KB
|
||||
|
||||
// 用于统计调试信息
|
||||
let mut match_count = 0usize;
|
||||
let mut total_match_length = 0usize;
|
||||
|
||||
// 初次读
|
||||
let bytes_read = input.read(&mut buffer)?;
|
||||
if bytes_read > 0 {
|
||||
total_read += bytes_read;
|
||||
onprogress(total_read);
|
||||
}
|
||||
lookahead.extend_from_slice(&buffer[..bytes_read]);
|
||||
|
||||
// 用于暂存无法匹配的字面量
|
||||
let mut literal_buf: Vec<u8> = Vec::new();
|
||||
|
||||
// 函数内联,用来一次性写出字面量块
|
||||
let flush_literal_buf = |out: &mut W, buf: &mut Vec<u8>| -> io::Result<()> {
|
||||
let run_len = buf.len();
|
||||
if run_len == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
if run_len > 255 {
|
||||
let mut start = 0;
|
||||
while start < run_len {
|
||||
let end = (start + 255).min(run_len);
|
||||
let chunk_size = end - start;
|
||||
|
||||
// 写头 [0,0, chunk_size, 0xFF]
|
||||
let head = [0u8, 0u8, chunk_size as u8, 0xFF];
|
||||
out.write_all(&head)?;
|
||||
|
||||
// 写 chunk_size 个字面量
|
||||
out.write_all(&buf[start..end])?;
|
||||
|
||||
start = end;
|
||||
}
|
||||
} else {
|
||||
// 写头 [0,0, run_len, 0xFF]
|
||||
let head = [0u8, 0u8, run_len as u8, 0xFF];
|
||||
out.write_all(&head)?;
|
||||
out.write_all(buf)?;
|
||||
}
|
||||
buf.clear();
|
||||
Ok(())
|
||||
};
|
||||
|
||||
while !lookahead.is_empty() {
|
||||
// 若 lookahead 不足,则再读
|
||||
while lookahead.len() < self.lookahead_size {
|
||||
match input.read(&mut buffer) {
|
||||
Ok(0) => break, // EOF
|
||||
Ok(n) => {
|
||||
total_read += n;
|
||||
if total_read - last_reported >= REPORT_THRESHOLD {
|
||||
onprogress(total_read);
|
||||
last_reported = total_read;
|
||||
}
|
||||
lookahead.extend_from_slice(&buffer[..n]);
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
// 在 window 中找最长匹配
|
||||
let window_slice: Vec<u8> = window.iter().copied().collect();
|
||||
let (offset, length) = self.find_longest_match(&window_slice, &lookahead);
|
||||
|
||||
if length > 0 {
|
||||
// 统计信息
|
||||
match_count += 1;
|
||||
total_match_length += length;
|
||||
|
||||
// 在此处先把字面量缓冲写出(如果有的话)
|
||||
if !literal_buf.is_empty() {
|
||||
flush_literal_buf(&mut output, &mut literal_buf)?;
|
||||
}
|
||||
|
||||
// 有匹配 => 写出一个匹配 Token
|
||||
if length < lookahead.len() {
|
||||
let next_char = lookahead[length];
|
||||
let token = Token {
|
||||
offset: offset as u16,
|
||||
length: length as u8,
|
||||
next_char,
|
||||
};
|
||||
output.write_all(&Self::encode_token_to_bytes(&token))?;
|
||||
|
||||
// 更新 window
|
||||
let advance = length + 1;
|
||||
for _ in 0..advance {
|
||||
if !lookahead.is_empty() {
|
||||
window.push_back(lookahead.remove(0));
|
||||
if window.len() > self.window_size {
|
||||
window.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// length == lookahead.len(),把最后的 next_char 逻辑改成逐字节写字面量
|
||||
for _ in 0..length {
|
||||
if let Some(&byte) = lookahead.get(0) {
|
||||
literal_buf.push(byte);
|
||||
lookahead.remove(0);
|
||||
window.push_back(byte);
|
||||
if window.len() > self.window_size {
|
||||
window.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 无匹配 => 把 lookahead[0] 当字面量放进 literal_buf
|
||||
let byte = lookahead.remove(0);
|
||||
literal_buf.push(byte);
|
||||
window.push_back(byte);
|
||||
if window.len() > self.window_size {
|
||||
window.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 文件处理结束后,可能还有剩余的字面量要一次性写出
|
||||
if !literal_buf.is_empty() {
|
||||
flush_literal_buf(&mut output, &mut literal_buf)?;
|
||||
}
|
||||
|
||||
// 最后确保所有读的字节都已报告
|
||||
if total_read > last_reported {
|
||||
onprogress(total_read);
|
||||
}
|
||||
|
||||
// 如果有 debug_out,就输出一下匹配统计等调试信息
|
||||
if let Some(dbg) = debug_out {
|
||||
let avg_match_len = if match_count > 0 {
|
||||
total_match_length as f64 / match_count as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
writeln!(
|
||||
dbg,
|
||||
"[LZ77 compress] match_count = {}, total_match_length = {}, average_match_len = {:.2}",
|
||||
match_count, total_match_length, avg_match_len
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decompress_stream<R: Read, W: Write, D: Write>(
|
||||
&self,
|
||||
mut input: R,
|
||||
mut output: W,
|
||||
onprogress: &mut impl FnMut(usize),
|
||||
debug_out: Option<&mut D>,
|
||||
) -> io::Result<()> {
|
||||
let mut ring_buffer = vec![0u8; 65536];
|
||||
let mut pos = 0usize; // 已经解出的字节数
|
||||
|
||||
let mut token_buf = [0u8; 4];
|
||||
|
||||
// 节流相关变量
|
||||
let mut total_read = 0usize;
|
||||
let mut last_reported = 0usize;
|
||||
const REPORT_THRESHOLD: usize = 16 * 1024; // 16KB
|
||||
|
||||
// 统计信息
|
||||
let mut match_count = 0usize;
|
||||
let mut total_match_length = 0usize;
|
||||
let mut literal_count = 0usize;
|
||||
|
||||
loop {
|
||||
// 读取 4 字节 => 若 EOF 就结束
|
||||
let mut read_bytes = 0;
|
||||
while read_bytes < 4 {
|
||||
let n = input.read(&mut token_buf[read_bytes..])?;
|
||||
if n == 0 {
|
||||
return if read_bytes == 0 {
|
||||
// 正常EOF
|
||||
// 在结束时输出一下统计信息
|
||||
if let Some(dbg) = debug_out {
|
||||
if match_count > 0 {
|
||||
let avg_match_len = total_match_length as f64 / match_count as f64;
|
||||
writeln!(
|
||||
dbg,
|
||||
"[LZ77 decompress] match_count = {}, total_match_length = {}, average_match_len = {:.2}, literal_count = {}",
|
||||
match_count, total_match_length, avg_match_len, literal_count
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
dbg,
|
||||
"[LZ77 decompress] No matches, literal_count = {}",
|
||||
literal_count
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"Unexpected EOF while reading token header",
|
||||
))
|
||||
};
|
||||
}
|
||||
read_bytes += n;
|
||||
total_read += n;
|
||||
if total_read - last_reported >= REPORT_THRESHOLD {
|
||||
onprogress(total_read);
|
||||
last_reported = total_read;
|
||||
}
|
||||
}
|
||||
|
||||
let offset = u16::from_le_bytes([token_buf[0], token_buf[1]]);
|
||||
let length = token_buf[2];
|
||||
let next_char = token_buf[3];
|
||||
|
||||
// 检测是否是 批量字面量 Token 头 => [0,0, run_len, 0xFF]
|
||||
if offset == 0 && length > 0 && next_char == 0xFF {
|
||||
// length 表示 run_len
|
||||
let run_len = length as usize;
|
||||
literal_count += run_len;
|
||||
|
||||
// 继续从输入读取 run_len 个字面量
|
||||
let mut literal_data = vec![0u8; run_len];
|
||||
let mut read_now = 0;
|
||||
while read_now < run_len {
|
||||
let n = input.read(&mut literal_data[read_now..])?;
|
||||
if n == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
format!("EOF while reading literal run (need {} bytes)", run_len),
|
||||
));
|
||||
}
|
||||
read_now += n;
|
||||
total_read += n;
|
||||
if total_read - last_reported >= REPORT_THRESHOLD {
|
||||
onprogress(total_read);
|
||||
last_reported = total_read;
|
||||
}
|
||||
}
|
||||
|
||||
// 写出并更新 pos
|
||||
output.write_all(&literal_data)?;
|
||||
for &byte in &literal_data {
|
||||
let buffer_pos = pos % ring_buffer.len();
|
||||
ring_buffer[buffer_pos] = byte;
|
||||
pos += 1;
|
||||
}
|
||||
} else {
|
||||
// 否则 => 这是一个普通 MATCH Token
|
||||
// (offset, length, next_char)
|
||||
let offset_usize = offset as usize;
|
||||
let length_usize = length as usize;
|
||||
|
||||
// 如果有 match,统计
|
||||
if offset_usize > 0 && length_usize > 0 {
|
||||
match_count += 1;
|
||||
total_match_length += length_usize;
|
||||
} else {
|
||||
// 这意味着 offset=0/length=0,算是 1 字节 literal
|
||||
literal_count += 1;
|
||||
}
|
||||
|
||||
// 先复制 match
|
||||
if offset_usize > 0 && length_usize > 0 {
|
||||
if offset_usize > pos {
|
||||
// 无效 offset
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid offset: {} > {}", offset_usize, pos),
|
||||
));
|
||||
}
|
||||
for _ in 0..length_usize {
|
||||
let buffer_pos = pos % ring_buffer.len();
|
||||
let lookup_pos = (pos - offset_usize) % ring_buffer.len();
|
||||
let byte = ring_buffer[lookup_pos];
|
||||
ring_buffer[buffer_pos] = byte;
|
||||
output.write_all(&[byte])?;
|
||||
pos += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// next_char 始终写出
|
||||
{
|
||||
let buffer_pos = pos % ring_buffer.len();
|
||||
ring_buffer[buffer_pos] = next_char;
|
||||
output.write_all(&[next_char])?;
|
||||
pos += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
765
src/main.rs
Normal file
765
src/main.rs
Normal file
@ -0,0 +1,765 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
use eframe::egui::{Color32, IconData, Shadow};
|
||||
use eframe::{
|
||||
egui::{self, Align, Layout},
|
||||
App, Frame, NativeOptions,
|
||||
};
|
||||
use egui_notify::Toasts;
|
||||
use rfd::FileDialog;
|
||||
use std::borrow::Cow;
|
||||
use std::io::{BufWriter};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{fs, fs::File, io::{BufReader, Cursor}, path::PathBuf, sync::mpsc::{self, Receiver, Sender}, thread};
|
||||
use std::path::Path;
|
||||
|
||||
mod huffman;
|
||||
mod lz77;
|
||||
use compresscsdesign::compute_crc32_and_sha256;
|
||||
|
||||
enum CompressMsg {
|
||||
FileStart(usize),
|
||||
FileProgress(usize, usize), // (current_read, total_size)
|
||||
FileDone(usize),
|
||||
Error(String),
|
||||
AllDone,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum ProcessType {
|
||||
Compress,
|
||||
Decompress,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ChecksumFile {
|
||||
path: PathBuf,
|
||||
crc32: Option<u32>,
|
||||
sha256: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ChecksumWindow {
|
||||
show: bool,
|
||||
files: Vec<ChecksumFile>,
|
||||
}
|
||||
|
||||
struct MyApp {
|
||||
selected_files: Vec<PathBuf>,
|
||||
|
||||
enable_lz77: bool,
|
||||
enable_huffman: bool,
|
||||
enable_debug: bool,
|
||||
lz77_window_size: usize,
|
||||
lz77_lookahead_size: usize,
|
||||
|
||||
output_path: String,
|
||||
|
||||
process_type: ProcessType,
|
||||
is_compressing: bool,
|
||||
|
||||
compress_index: usize,
|
||||
compress_progress: usize,
|
||||
compress_progress_total: usize,
|
||||
|
||||
receiver: Receiver<CompressMsg>,
|
||||
sender: Option<Sender<CompressMsg>>,
|
||||
|
||||
// egui_notify
|
||||
toasts: Toasts,
|
||||
|
||||
checksum_window: ChecksumWindow,
|
||||
start_time: Instant,
|
||||
}
|
||||
fn read_font_file(paths: &[&str]) -> Option<Vec<u8>> {
|
||||
for &path in paths {
|
||||
if let Ok(data) = fs::read(Path::new(path)) {
|
||||
return Some(data);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
impl MyApp {
|
||||
fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
|
||||
let paths = [
|
||||
"C:/Users/feie9454/AppData/Local/Microsoft/Windows/Fonts/PingFang Medium.ttf",
|
||||
"C:/Windows/Fonts/msyh.ttc"
|
||||
];
|
||||
|
||||
match read_font_file(&paths) {
|
||||
Some(font_data) => {
|
||||
println!("Font loaded successfully, size: {} bytes", font_data.len()) ;
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
fonts.font_data.insert(
|
||||
"my_font".to_owned(),
|
||||
egui::FontData::from_owned(font_data).into(),
|
||||
);
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Proportional)
|
||||
.or_default()
|
||||
.insert(0, "my_font".to_owned());
|
||||
cc.egui_ctx.set_fonts(fonts);
|
||||
},
|
||||
None => eprintln!("Failed to load font from all specified paths."),
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
Self {
|
||||
selected_files: Vec::new(),
|
||||
enable_lz77: true,
|
||||
enable_huffman: true,
|
||||
enable_debug: false,
|
||||
lz77_window_size: 16384,
|
||||
lz77_lookahead_size: 127,
|
||||
output_path: String::new(),
|
||||
process_type: ProcessType::Compress,
|
||||
compress_index: 0,
|
||||
compress_progress: 0,
|
||||
compress_progress_total: 0,
|
||||
is_compressing: false,
|
||||
receiver: rx,
|
||||
sender: Some(tx),
|
||||
toasts: Toasts::default().with_shadow(Shadow {
|
||||
offset: Default::default(),
|
||||
blur: 10.0,
|
||||
spread: 5.0,
|
||||
color: Color32::from_black_alpha(15),
|
||||
}),
|
||||
checksum_window: ChecksumWindow {
|
||||
show: false,
|
||||
files: vec![],
|
||||
},
|
||||
start_time: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App for MyApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut Frame) {
|
||||
// -- 主窗口:处理文件拖拽事件 --
|
||||
ctx.input(|i| {
|
||||
if !i.raw.dropped_files.is_empty() && !self.checksum_window.show {
|
||||
// 只有在「校验工具」窗口关闭时才往 selected_files 里加
|
||||
// 避免在校验窗口打开时拖拽却误加到压缩列表
|
||||
for file in &i.raw.dropped_files {
|
||||
if let Some(path) = &file.path {
|
||||
self.selected_files.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// -- 从压缩/解压线程中接收消息 --
|
||||
while let Ok(msg) = self.receiver.try_recv() {
|
||||
match msg {
|
||||
CompressMsg::FileStart(index) => {
|
||||
// 切换到正在处理的文件
|
||||
println!("开始处理第 {} 个文件", index + 1);
|
||||
self.compress_index = index;
|
||||
self.compress_progress = 0;
|
||||
}
|
||||
CompressMsg::FileProgress(current_read, total_size) => {
|
||||
// 更新单个文件的进度百分比
|
||||
self.compress_progress = current_read;
|
||||
self.compress_progress_total = total_size;
|
||||
}
|
||||
CompressMsg::FileDone(index) => {
|
||||
// 某个文件处理结束
|
||||
if index + 1 >= self.selected_files.len() {
|
||||
self.compress_index = self.selected_files.len() - 1;
|
||||
} else {
|
||||
self.compress_index = index + 1;
|
||||
self.compress_progress = 0;
|
||||
}
|
||||
}
|
||||
CompressMsg::AllDone => {
|
||||
self.is_compressing = false;
|
||||
self.compress_index = self.selected_files.len() - 1;
|
||||
self.toasts.success(format!(
|
||||
"所有文件处理完毕!耗时 {}ms",
|
||||
(Instant::now() - self.start_time).as_millis()
|
||||
));
|
||||
}
|
||||
CompressMsg::Error(msg) => {
|
||||
self.toasts.error(format!("处理时发生错误:{}", msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 绘制主界面 ----
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.heading("鹅鹅文件压缩 / 解压");
|
||||
ui.colored_label(Color32::DARK_GRAY, "3230608044 查俊豪");
|
||||
});
|
||||
ui.separator();
|
||||
ui.label("基于 LZ77 和 Huffman 算法实现的压缩工具,支持多文件批量压缩 / 解压。");
|
||||
|
||||
if !self.selected_files.is_empty() {
|
||||
ui.label("已选择文件:");
|
||||
for path in &self.selected_files {
|
||||
ui.label(format!("- {}", path.display()));
|
||||
}
|
||||
ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
|
||||
if ui.button("清空").clicked() {
|
||||
self.selected_files.clear();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ui.label("请拖拽文件到窗口中以添加文件。");
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.label("处理类型:");
|
||||
ui.radio_value(&mut self.process_type, ProcessType::Compress, "压缩");
|
||||
if self.process_type == ProcessType::Compress {
|
||||
egui::CollapsingHeader::new("压缩设置")
|
||||
.default_open(true)
|
||||
.show(ui, |ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("预设:");
|
||||
if ui.button("极快").clicked() {
|
||||
self.enable_lz77 = false;
|
||||
self.enable_huffman = true;
|
||||
}
|
||||
if ui.button("平衡").clicked() {
|
||||
self.enable_lz77 = true;
|
||||
self.enable_huffman = true;
|
||||
self.lz77_window_size = 16384;
|
||||
self.lz77_lookahead_size = 127;
|
||||
}
|
||||
if ui.button("极限压缩").clicked() {
|
||||
self.enable_lz77 = true;
|
||||
self.enable_huffman = true;
|
||||
self.lz77_window_size = 65535;
|
||||
self.lz77_lookahead_size = 255;
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.enable_lz77, "启用 LZ77");
|
||||
});
|
||||
|
||||
if self.enable_lz77 {
|
||||
ui.add(
|
||||
egui::Slider::new(&mut self.lz77_window_size, 128..=65535)
|
||||
.text("LZ77 Window Size"),
|
||||
);
|
||||
ui.add(
|
||||
egui::Slider::new(&mut self.lz77_lookahead_size, 32..=255)
|
||||
.text("LZ77 Lookahead Size"),
|
||||
);
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.enable_huffman, "启用 Huffman");
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
});
|
||||
}
|
||||
ui.radio_value(&mut self.process_type, ProcessType::Decompress, "解压");
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("输出路径: ");
|
||||
ui.text_edit_singleline(&mut self.output_path);
|
||||
|
||||
if ui.button("选择...").clicked() {
|
||||
if let Some(folder) = FileDialog::new().pick_folder() {
|
||||
self.output_path = folder.display().to_string();
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
|
||||
ui.checkbox(&mut self.enable_debug, "输出调试信息");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label(format!(
|
||||
"正在处理文件: {}/{}",
|
||||
if self.is_compressing {
|
||||
self.compress_index + 1
|
||||
} else {
|
||||
0
|
||||
},
|
||||
self.selected_files.len()
|
||||
));
|
||||
|
||||
let current_file_progress = if self.compress_progress_total == 0 {
|
||||
0.0
|
||||
} else {
|
||||
self.compress_progress as f32 / self.compress_progress_total as f32
|
||||
};
|
||||
|
||||
// 总体进度条
|
||||
if self.selected_files.is_empty() || !self.is_compressing {
|
||||
ui.add(egui::ProgressBar::new(0.0).show_percentage());
|
||||
} else {
|
||||
ui.add(
|
||||
egui::ProgressBar::new(
|
||||
(self.compress_index as f32 + current_file_progress)
|
||||
/ (self.selected_files.len() as f32),
|
||||
)
|
||||
.show_percentage(),
|
||||
);
|
||||
};
|
||||
|
||||
// 当前文件进度条
|
||||
if self.selected_files.is_empty() || !self.is_compressing {
|
||||
ui.label("0 KiB / 0 KiB");
|
||||
ui.add(egui::ProgressBar::new(0.0).show_percentage());
|
||||
} else {
|
||||
ui.label(format!(
|
||||
"{}: {} KiB / {} KiB",
|
||||
if !self.selected_files.is_empty() {
|
||||
self.selected_files[self.compress_index]
|
||||
.file_name()
|
||||
.unwrap_or("".as_ref())
|
||||
.to_string_lossy()
|
||||
} else {
|
||||
Cow::from("")
|
||||
},
|
||||
self.compress_progress / 1024,
|
||||
self.compress_progress_total / 1024
|
||||
));
|
||||
ui.add(egui::ProgressBar::new(current_file_progress).show_percentage());
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.add_space(ui.available_height() - 24.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
|
||||
if ui.button("开始!").clicked() {
|
||||
if self.selected_files.is_empty() {
|
||||
self.toasts
|
||||
.info("请选择要处理的文件")
|
||||
.duration(Some(Duration::from_secs(3)));
|
||||
return;
|
||||
}
|
||||
if self.output_path.is_empty() {
|
||||
self.toasts
|
||||
.info("请指定输出路径")
|
||||
.duration(Some(Duration::from_secs(3)));
|
||||
return;
|
||||
}
|
||||
if self.is_compressing {
|
||||
self.toasts
|
||||
.info("请等待任务处理完成")
|
||||
.duration(Some(Duration::from_secs(3)));
|
||||
return;
|
||||
}
|
||||
match self.process_type {
|
||||
ProcessType::Decompress => {
|
||||
self.start_time = Instant::now();
|
||||
self.start_decompress_thread();
|
||||
}
|
||||
ProcessType::Compress => {
|
||||
self.start_time = Instant::now();
|
||||
self.start_compress_thread();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ui.button("校验工具").clicked() {
|
||||
// 打开校验窗口
|
||||
self.checksum_window.show = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
self.update_checksum_window(ctx);
|
||||
|
||||
self.toasts.show(ctx);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
impl MyApp {
|
||||
fn start_compress_thread(&mut self) {
|
||||
self.is_compressing = true;
|
||||
self.compress_progress = 0;
|
||||
self.compress_index = 0;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
self.sender = Some(tx.clone());
|
||||
self.receiver = rx;
|
||||
|
||||
let selected_files = self.selected_files.clone();
|
||||
let output_path = self.output_path.clone();
|
||||
|
||||
// 新增: 这几个为了在子线程里捕获
|
||||
let enable_lz77 = self.enable_lz77;
|
||||
let enable_huffman = self.enable_huffman;
|
||||
let enable_debug = self.enable_debug;
|
||||
let lz77_window_size = self.lz77_window_size;
|
||||
let lz77_lookahead_size = self.lz77_lookahead_size;
|
||||
|
||||
thread::spawn(move || {
|
||||
// 压缩线程逻辑:依次处理 selected_files
|
||||
for (i, file_path) in selected_files.iter().enumerate() {
|
||||
let _ = tx.send(CompressMsg::FileStart(i));
|
||||
|
||||
let total_size = file_path.metadata().map(|m| m.len()).unwrap_or(0);
|
||||
|
||||
let input_file = match File::open(file_path) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("无法打开输入文件: {}, 错误: {}", file_path.display(), e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let input_reader = BufReader::new(input_file);
|
||||
|
||||
// -----------------------------
|
||||
// 1) 组装输出文件名
|
||||
// -----------------------------
|
||||
let filename = file_path.file_name().unwrap_or_default().to_string_lossy();
|
||||
let mut out_filename = filename.to_string();
|
||||
|
||||
if enable_lz77 && enable_huffman {
|
||||
out_filename.push_str(".lz77.huf");
|
||||
} else if enable_lz77 {
|
||||
out_filename.push_str(".lz77");
|
||||
} else if enable_huffman {
|
||||
out_filename.push_str(".huf");
|
||||
} else {
|
||||
let _ = tx.send(CompressMsg::Error("未启用任何压缩算法!".to_string()));
|
||||
continue;
|
||||
}
|
||||
|
||||
let output_fullpath = format!("{}/{}", output_path, out_filename);
|
||||
let output_file = match File::create(&output_fullpath) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("无法创建输出文件: {}, 错误: {}", output_fullpath, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let output_writer = BufWriter::new(output_file);
|
||||
|
||||
// -----------------------------
|
||||
// 2) 如果 debug,创建同名log文件
|
||||
// -----------------------------
|
||||
// 用 Option<File> 来存日志句柄
|
||||
let mut debug_file: Option<File> = None;
|
||||
if enable_debug {
|
||||
let log_filename = format!("{}.log", out_filename);
|
||||
let log_filepath = format!("{}/{}", output_path, log_filename);
|
||||
match File::create(&log_filepath) {
|
||||
Ok(f) => {
|
||||
debug_file = Some(f);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("无法创建log文件: {}, 错误: {}", log_filepath, e);
|
||||
// 不要return;只是没有日志而已
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// 3) 定义进度回调
|
||||
// -----------------------------
|
||||
let mut bytes_read_so_far = 0usize;
|
||||
let mut onprogress = |read_bytes: usize| {
|
||||
bytes_read_so_far = read_bytes;
|
||||
let _ = tx.send(CompressMsg::FileProgress(
|
||||
bytes_read_so_far,
|
||||
total_size as usize,
|
||||
));
|
||||
};
|
||||
|
||||
// -----------------------------
|
||||
// 4) 执行压缩
|
||||
// -----------------------------
|
||||
let result = if enable_lz77 && enable_huffman {
|
||||
// 先 LZ77 => temp_buf => 再 Huffman => output
|
||||
let mut temp_buf = Vec::new();
|
||||
let lz77 = lz77::LZ77::new(lz77_window_size, lz77_lookahead_size);
|
||||
|
||||
// 第一次调用: LZ77压缩 => temp_buf
|
||||
// 注意:把 debug_file.as_mut() 传给 compress_stream
|
||||
let r1 = {
|
||||
let debug_out: Option<&mut File> = debug_file.as_mut(); // 取可变引用
|
||||
lz77.compress_stream(
|
||||
input_reader,
|
||||
&mut temp_buf,
|
||||
&mut onprogress,
|
||||
debug_out,
|
||||
)
|
||||
};
|
||||
|
||||
if r1.is_err() {
|
||||
r1
|
||||
} else {
|
||||
// 第二次调用: Huffman => output_writer
|
||||
let temp_reader = BufReader::new(Cursor::new(temp_buf));
|
||||
let debug_out: Option<&mut File> = debug_file.as_mut();
|
||||
huffman::encode_stream(
|
||||
temp_reader,
|
||||
output_writer,
|
||||
&mut onprogress,
|
||||
debug_out,
|
||||
)
|
||||
}
|
||||
} else if enable_lz77 {
|
||||
// 仅 LZ77
|
||||
let lz77 = lz77::LZ77::new(lz77_window_size, lz77_lookahead_size);
|
||||
let debug_out: Option<&mut File> = debug_file.as_mut();
|
||||
lz77.compress_stream(input_reader, output_writer, &mut onprogress, debug_out)
|
||||
} else {
|
||||
// 仅 Huffman
|
||||
let debug_out: Option<&mut File> = debug_file.as_mut();
|
||||
huffman::encode_stream(input_reader, output_writer, &mut onprogress, debug_out)
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
eprintln!("压缩过程出错: {}", e);
|
||||
let _ = tx.send(CompressMsg::Error(format!("压缩过程出错: {}", e)));
|
||||
}
|
||||
|
||||
// 单个文件结束
|
||||
let _ = tx.send(CompressMsg::FileDone(i));
|
||||
}
|
||||
|
||||
// 全部文件处理结束
|
||||
let _ = tx.send(CompressMsg::AllDone);
|
||||
});
|
||||
}
|
||||
|
||||
fn start_decompress_thread(&mut self) {
|
||||
self.is_compressing = true;
|
||||
self.compress_progress = 0;
|
||||
self.compress_index = 0;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
self.sender = Some(tx.clone());
|
||||
self.receiver = rx;
|
||||
|
||||
let selected_files = self.selected_files.clone();
|
||||
let output_path = self.output_path.clone();
|
||||
let enable_debug = self.enable_debug;
|
||||
|
||||
thread::spawn(move || {
|
||||
for (i, file_path) in selected_files.iter().enumerate() {
|
||||
let _ = tx.send(CompressMsg::FileStart(i));
|
||||
|
||||
let total_size = file_path.metadata().map(|m| m.len()).unwrap_or(0);
|
||||
|
||||
let input_file = match File::open(file_path) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("无法打开待解压文件: {}, 错误: {}", file_path.display(), e);
|
||||
let _ = tx.send(CompressMsg::Error(format!(
|
||||
"无法打开待解压文件: {}, 错误: {}",
|
||||
file_path.display(),
|
||||
e
|
||||
)));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let input_reader = BufReader::new(input_file);
|
||||
|
||||
let filename = file_path.file_name().unwrap_or_default().to_string_lossy();
|
||||
let mut out_filename = filename.to_string();
|
||||
|
||||
// -----------------------------
|
||||
// 2) 如果 debug,创建同名log文件
|
||||
// -----------------------------
|
||||
let mut debug_file: Option<File> = None;
|
||||
if enable_debug {
|
||||
let log_filename = format!("{}.log", out_filename);
|
||||
let log_filepath = format!("{}/{}", output_path, log_filename);
|
||||
match File::create(&log_filepath) {
|
||||
Ok(f) => {
|
||||
debug_file = Some(f);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("无法创建log文件: {}, 错误: {}", log_filepath, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let use_lz77 =
|
||||
out_filename.ends_with(".lz77") || out_filename.ends_with(".lz77.huf");
|
||||
let use_huffman = out_filename.ends_with(".huf");
|
||||
|
||||
// 去掉后缀,以得到解压后的“原始”文件名
|
||||
if out_filename.ends_with(".lz77.huf") {
|
||||
out_filename.truncate(out_filename.len() - ".lz77.huf".len());
|
||||
} else if out_filename.ends_with(".lz77") {
|
||||
out_filename.truncate(out_filename.len() - ".lz77".len());
|
||||
} else if out_filename.ends_with(".huf") {
|
||||
out_filename.truncate(out_filename.len() - ".huf".len());
|
||||
} else {
|
||||
let _ = tx.send(CompressMsg::Error(format!(
|
||||
"无法识别的压缩格式:{}",
|
||||
out_filename
|
||||
)));
|
||||
continue;
|
||||
}
|
||||
|
||||
let output_fullpath = format!("{}/{}", output_path, out_filename);
|
||||
let output_file = match File::create(&output_fullpath) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("无法创建解压后文件: {}, 错误: {}", output_fullpath, e);
|
||||
let _ = tx.send(CompressMsg::Error(format!(
|
||||
"无法创建解压后文件: {}, 错误: {}",
|
||||
output_fullpath, e
|
||||
)));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let output_writer = BufWriter::new(output_file);
|
||||
|
||||
// 定义进度回调
|
||||
let mut bytes_read_so_far = 0usize;
|
||||
let mut onprogress = |read_bytes: usize| {
|
||||
bytes_read_so_far = read_bytes;
|
||||
let _ = tx.send(CompressMsg::FileProgress(
|
||||
bytes_read_so_far,
|
||||
total_size as usize,
|
||||
));
|
||||
};
|
||||
|
||||
// -----------------------------
|
||||
// 3) 执行解压
|
||||
// -----------------------------
|
||||
let result = if use_lz77 && use_huffman {
|
||||
// 后缀是 .lz77.huf => 先 Huffman 解 => temp_buf => 再 LZ77 解 => output
|
||||
let mut temp_buf = Vec::new();
|
||||
// 第一次:Huffman decode => temp_buf
|
||||
let r1 = {
|
||||
let debug_out: Option<&mut File> = debug_file.as_mut();
|
||||
huffman::decode_stream(
|
||||
input_reader,
|
||||
&mut temp_buf,
|
||||
&mut onprogress,
|
||||
debug_out,
|
||||
)
|
||||
};
|
||||
if r1.is_err() {
|
||||
r1
|
||||
} else {
|
||||
let temp_reader = BufReader::new(Cursor::new(temp_buf));
|
||||
let lz = lz77::LZ77::new(0, 0);
|
||||
let debug_out: Option<&mut File> = debug_file.as_mut();
|
||||
lz.decompress_stream(temp_reader, output_writer, &mut onprogress, debug_out)
|
||||
}
|
||||
} else if use_lz77 {
|
||||
// .lz77 => 直接 LZ77 解
|
||||
let lz = lz77::LZ77::new(0, 0);
|
||||
let debug_out: Option<&mut File> = debug_file.as_mut();
|
||||
lz.decompress_stream(input_reader, output_writer, &mut onprogress, debug_out)
|
||||
} else {
|
||||
// .huf => 直接 Huffman 解
|
||||
let debug_out: Option<&mut File> = debug_file.as_mut();
|
||||
huffman::decode_stream(input_reader, output_writer, &mut onprogress, debug_out)
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
eprintln!("解压过程出错: {}", e);
|
||||
let _ = tx.send(CompressMsg::Error(format!("解压过程出错: {}", e)));
|
||||
}
|
||||
|
||||
let _ = tx.send(CompressMsg::FileDone(i));
|
||||
}
|
||||
let _ = tx.send(CompressMsg::AllDone);
|
||||
});
|
||||
}
|
||||
|
||||
fn update_checksum_window(&mut self, ctx: &egui::Context) {
|
||||
if self.checksum_window.show {
|
||||
egui::Window::new("校验工具")
|
||||
.open(&mut self.checksum_window.show)
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
ui.label("请将文件拖入此窗口,或重复拖拽多个文件。");
|
||||
|
||||
if ctx.input(|i| !i.raw.dropped_files.is_empty()) {
|
||||
ctx.input(|i| {
|
||||
for file in &i.raw.dropped_files {
|
||||
if let Some(path) = &file.path {
|
||||
// 如果文件已经存在,就不重复添加
|
||||
if !self.checksum_window.files.iter().any(|f| f.path == *path) {
|
||||
let mut cf = ChecksumFile {
|
||||
path: path.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let (crc32, sha256) = compute_crc32_and_sha256(&cf.path);
|
||||
cf.crc32 = crc32;
|
||||
cf.sha256 = sha256;
|
||||
self.checksum_window.files.push(cf);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
if self.checksum_window.files.is_empty() {
|
||||
ui.label("尚未添加任何文件。");
|
||||
} else {
|
||||
egui::ScrollArea::vertical()
|
||||
.max_height(400.0)
|
||||
.show(ui, |ui| {
|
||||
for file_info in &self.checksum_window.files {
|
||||
ui.group(|ui| {
|
||||
ui.label(format!("文件: {}", file_info.path.display()));
|
||||
if let Some(crc32) = file_info.crc32 {
|
||||
ui.label(format!("CRC32: {:08X}", crc32));
|
||||
} else {
|
||||
ui.label("CRC32: 计算失败或文件不可读");
|
||||
}
|
||||
|
||||
if let Some(ref sha256) = file_info.sha256 {
|
||||
ui.label(format!("SHA256: {}", sha256));
|
||||
} else {
|
||||
ui.label("SHA256: 计算失败或文件不可读");
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
});
|
||||
ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
|
||||
if ui.button("清空文件列表").clicked() {
|
||||
self.checksum_window.files.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_icon() -> IconData {
|
||||
IconData {
|
||||
rgba: include_bytes!(".\\assets\\icon.bin").to_vec(),
|
||||
width: 128,
|
||||
height: 128,
|
||||
}
|
||||
}
|
||||
fn main() -> eframe::Result<()> {
|
||||
let options = NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_title("鹅鹅文件压缩器")
|
||||
.with_icon(load_icon())
|
||||
.with_inner_size([500.0, 600.0]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"鹅鹅文件压缩器",
|
||||
options,
|
||||
Box::new(|cc| Ok(Box::new(MyApp::new(cc)))),
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user