feat: support cbr reading
All checks were successful
Build and test / Tests (pull_request) Successful in 1m12s
Checking yaml / Run yamllint (pull_request) Successful in 4s
Build and test / Build AMD64 (pull_request) Successful in 1m3s
Build and test / Generate Documentation (pull_request) Successful in 58s
Checking Renovate configuration / validate (pull_request) Successful in 1m50s
Build and test / Clippy (pull_request) Successful in 1m2s
All checks were successful
Build and test / Tests (pull_request) Successful in 1m12s
Checking yaml / Run yamllint (pull_request) Successful in 4s
Build and test / Build AMD64 (pull_request) Successful in 1m3s
Build and test / Generate Documentation (pull_request) Successful in 58s
Checking Renovate configuration / validate (pull_request) Successful in 1m50s
Build and test / Clippy (pull_request) Successful in 1m2s
This commit is contained in:
parent
a16ec085b1
commit
6379e8a56b
10 changed files with 216 additions and 20 deletions
75
src/formats/cbr.rs
Normal file
75
src/formats/cbr.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::model::Document;
|
||||
|
||||
use super::{CbxReader, FormatReader};
|
||||
|
||||
pub struct CbrReader;
|
||||
|
||||
impl CbxReader for CbrReader {
|
||||
fn extract_images(&self, input: &Path) -> Result<Vec<(String, Vec<u8>)>> {
|
||||
let tempdir = tempfile::tempdir()?;
|
||||
let dest = tempdir.path().to_path_buf();
|
||||
{
|
||||
use std::env;
|
||||
use unrar::Archive;
|
||||
|
||||
let cwd = env::current_dir()?;
|
||||
let input_str = input.to_string_lossy().to_string();
|
||||
env::set_current_dir(&dest)?;
|
||||
let mut archive = Archive::new(&input_str)
|
||||
.open_for_processing()
|
||||
.map_err(|e| anyhow!("Failed to open RAR for processing: {e}"))?;
|
||||
|
||||
loop {
|
||||
match archive.read_header() {
|
||||
Ok(Some(header)) => {
|
||||
archive = if header.entry().is_file() {
|
||||
header
|
||||
.extract()
|
||||
.map_err(|e| anyhow!("Failed to extract entry: {e}"))?
|
||||
} else {
|
||||
header
|
||||
.skip()
|
||||
.map_err(|e| anyhow!("Failed to skip entry: {e}"))?
|
||||
};
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
let _ = env::set_current_dir(cwd);
|
||||
return Err(anyhow!("Failed to read RAR header: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
env::set_current_dir(cwd)?;
|
||||
}
|
||||
|
||||
let mut files: Vec<(String, Vec<u8>)> = Vec::new();
|
||||
for entry in WalkDir::new(&dest).into_iter().filter_map(Result::ok) {
|
||||
let path: PathBuf = entry.path().to_path_buf();
|
||||
if path.is_file() && path.extension() == Some(OsStr::new("jpg")) {
|
||||
let mut data = Vec::new();
|
||||
fs::File::open(&path)?.read_to_end(&mut data)?;
|
||||
let name = path
|
||||
.file_name()
|
||||
.and_then(OsStr::to_str)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| path.display().to_string());
|
||||
files.push((name, data));
|
||||
}
|
||||
}
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatReader for CbrReader {
|
||||
fn read(&self, input: &Path) -> Result<Document> {
|
||||
self.read_cbx(input)
|
||||
}
|
||||
}
|
||||
30
src/formats/cbx.rs
Normal file
30
src/formats/cbx.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::model::Document;
|
||||
|
||||
// Shared reader logic for CBx (CBZ/CBR) formats
|
||||
pub trait CbxReader: Send + Sync {
|
||||
// Implementors should return a list of (file_name, jpeg_bytes)
|
||||
fn extract_images(&self, input: &Path) -> Result<Vec<(String, Vec<u8>)>>;
|
||||
|
||||
// Build a Document from extracted JPEG bytes
|
||||
fn read_cbx(&self, input: &Path) -> Result<Document> {
|
||||
let files = self.extract_images(input)?;
|
||||
let mut pages: Vec<crate::model::ImagePage> = Vec::new();
|
||||
{
|
||||
use rayon::prelude::*;
|
||||
files
|
||||
.par_iter()
|
||||
.map(|(name, data)| crate::model::ImagePage {
|
||||
name: name.clone(),
|
||||
image: image::load_from_memory(data).expect("Failed to decode image"),
|
||||
jpeg_dct: Some(data.clone()),
|
||||
})
|
||||
.collect_into_vec(&mut pages);
|
||||
|
||||
pages.par_sort_by_key(|p| p.name.clone());
|
||||
}
|
||||
Ok(Document::new(pages))
|
||||
}
|
||||
}
|
||||
|
|
@ -4,17 +4,16 @@ use std::io::{Read, Write};
|
|||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use rayon::prelude::*;
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::model::{Document, ImagePage};
|
||||
use crate::model::Document;
|
||||
|
||||
use super::{FormatReader, FormatWriter};
|
||||
use super::{CbxReader, FormatReader, FormatWriter};
|
||||
|
||||
pub struct CbzReader;
|
||||
|
||||
impl FormatReader for CbzReader {
|
||||
fn read(&self, input: &Path) -> Result<Document> {
|
||||
impl CbxReader for CbzReader {
|
||||
fn extract_images(&self, input: &Path) -> Result<Vec<(String, Vec<u8>)>> {
|
||||
let mut zip = ZipArchive::new(File::open(input)?)?;
|
||||
let mut files: Vec<(String, Vec<u8>)> = Vec::new();
|
||||
for i in 0..zip.len() {
|
||||
|
|
@ -35,20 +34,13 @@ impl FormatReader for CbzReader {
|
|||
));
|
||||
}
|
||||
}
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
|
||||
let mut pages: Vec<ImagePage> = Vec::new();
|
||||
files
|
||||
.par_iter()
|
||||
.map(|(name, data)| ImagePage {
|
||||
name: name.clone(),
|
||||
image: image::load_from_memory(data).expect("Failed to decode image"),
|
||||
jpeg_dct: Some(data.clone()),
|
||||
})
|
||||
.collect_into_vec(&mut pages);
|
||||
|
||||
pages.par_sort_by_key(|p| p.name.clone());
|
||||
|
||||
Ok(Document::new(pages))
|
||||
impl FormatReader for CbzReader {
|
||||
fn read(&self, input: &Path) -> Result<Document> {
|
||||
self.read_cbx(input)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,14 @@ use anyhow::Result;
|
|||
|
||||
use crate::model::Document;
|
||||
|
||||
pub mod cbr;
|
||||
pub mod cbx;
|
||||
pub mod cbz;
|
||||
pub mod pdf;
|
||||
|
||||
pub use cbx::CbxReader;
|
||||
|
||||
use cbr::CbrReader;
|
||||
use cbz::{CbzReader, CbzWriter};
|
||||
use pdf::{PdfReader, PdfWriter};
|
||||
|
||||
|
|
@ -15,6 +20,7 @@ use pdf::{PdfReader, PdfWriter};
|
|||
pub enum FormatId {
|
||||
Cbz,
|
||||
Pdf,
|
||||
Cbr,
|
||||
}
|
||||
|
||||
impl FormatId {
|
||||
|
|
@ -32,6 +38,7 @@ impl FormatId {
|
|||
match path.extension().and_then(OsStr::to_str) {
|
||||
Some("cbz") => Some(FormatId::Cbz),
|
||||
Some("pdf") => Some(FormatId::Pdf),
|
||||
Some("cbr") => Some(FormatId::Cbr),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +56,7 @@ pub fn get_reader(format: FormatId) -> Option<Box<dyn FormatReader>> {
|
|||
match format {
|
||||
FormatId::Cbz => Some(Box::new(CbzReader)),
|
||||
FormatId::Pdf => Some(Box::new(PdfReader)),
|
||||
FormatId::Cbr => Some(Box::new(CbrReader)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,5 +64,6 @@ pub fn get_writer(format: FormatId) -> Option<Box<dyn FormatWriter>> {
|
|||
match format {
|
||||
FormatId::Pdf => Some(Box::new(PdfWriter)),
|
||||
FormatId::Cbz => Some(Box::new(CbzWriter)),
|
||||
FormatId::Cbr => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ impl Job {
|
|||
match to {
|
||||
FormatId::Pdf => output_path.set_extension("pdf"),
|
||||
FormatId::Cbz => output_path.set_extension("cbz"),
|
||||
FormatId::Cbr => output_path.set_extension("cbr"),
|
||||
};
|
||||
|
||||
Self {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue