feat: implement cbz writing and pdf reading
All checks were successful
Build and test / Clippy (pull_request) Successful in 44s
Build and test / Tests (pull_request) Successful in 48s
Checking yaml / Run yamllint (pull_request) Successful in 5s
Checking Renovate configuration / validate (pull_request) Successful in 1m4s
Build and test / Build AMD64 (pull_request) Successful in 49s
Build and test / Generate Documentation (pull_request) Successful in 59s

This commit is contained in:
Marc Plano-Lesay 2025-10-13 16:52:25 +11:00
parent 3aa68fbe12
commit b35ccbe271
Signed by: kernald
GPG key ID: 66A41B08CC62A6CF
10 changed files with 643 additions and 57 deletions

96
tests/cbz_writer_tests.rs Normal file
View file

@ -0,0 +1,96 @@
use std::fs::File;
use std::io::Read;
use cbz2pdf::formats::cbz::CbzWriter;
use cbz2pdf::formats::FormatWriter;
use cbz2pdf::model::{Document, ImagePage};
fn make_tiny_jpeg() -> (Vec<u8>, image::DynamicImage) {
let img = image::DynamicImage::new_rgb8(1, 1).to_rgb8();
let mut buf = Vec::new();
{
let mut cursor = std::io::Cursor::new(&mut buf);
let mut enc = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut cursor, 80);
enc.encode(&img, 1, 1, image::ColorType::Rgb8.into())
.unwrap();
}
let decoded = image::load_from_memory(&buf).unwrap();
(buf, decoded)
}
#[test]
fn cbz_writer_preserves_dct_and_renames_non_jpg() {
// Prepare a page with original JPEG DCT data but a non-jpg name.
let (jpeg_dct, decoded) = make_tiny_jpeg();
let page = ImagePage {
name: "cover.png".to_string(),
image: decoded,
jpeg_dct: Some(jpeg_dct.clone()),
};
let doc = Document::new(vec![page]);
let temp_dir = tempfile::tempdir().expect("create temp dir");
let cbz_path = temp_dir.path().join("out.cbz");
let writer = CbzWriter;
writer.write(&doc, &cbz_path).expect("write cbz");
// Open the CBZ and verify it contains 001.jpg with the exact JPEG data.
let f = File::open(&cbz_path).unwrap();
let mut zip = zip::ZipArchive::new(f).unwrap();
// There should be exactly one file named 001.jpg
let mut found = false;
for i in 0..zip.len() {
let mut file = zip.by_index(i).unwrap();
let name = file.enclosed_name().unwrap().to_owned();
if name.file_name().unwrap() == "001.jpg" {
let mut data = Vec::new();
file.read_to_end(&mut data).unwrap();
assert_eq!(
data, jpeg_dct,
"writer should preserve original JPEG DCT bytes"
);
found = true;
}
}
assert!(found, "001.jpg not found in zip");
}
#[test]
fn cbz_writer_keeps_jpg_name() {
// If the page already has a .jpg name, the writer should keep it.
let (jpeg_dct, decoded) = make_tiny_jpeg();
let page = ImagePage {
name: "page01.jpg".to_string(),
image: decoded,
jpeg_dct: Some(jpeg_dct),
};
let doc = Document::new(vec![page]);
let temp_dir = tempfile::tempdir().expect("create temp dir");
let cbz_path = temp_dir.path().join("out.cbz");
let writer = CbzWriter;
writer.write(&doc, &cbz_path).expect("write cbz");
let f = File::open(&cbz_path).unwrap();
let mut zip = zip::ZipArchive::new(f).unwrap();
let mut names = Vec::new();
for i in 0..zip.len() {
let file = zip.by_index(i).unwrap();
let name = file
.enclosed_name()
.unwrap()
.file_name()
.unwrap()
.to_owned();
names.push(name.to_string_lossy().to_string());
}
assert_eq!(
names,
vec!["page01.jpg"],
"existing .jpg name should be kept"
);
}

View file

@ -34,7 +34,7 @@ fn job_new_sets_output_extension() {
#[test]
fn format_capabilities_consistent() {
assert!(FormatId::Cbz.can_read());
assert!(!FormatId::Cbz.can_write());
assert!(FormatId::Cbz.can_write());
assert!(FormatId::Pdf.can_write());
assert!(!FormatId::Pdf.can_read());
assert!(FormatId::Pdf.can_read());
}

51
tests/pdf_reader_tests.rs Normal file
View file

@ -0,0 +1,51 @@
use cbz2pdf::formats::pdf::{PdfReader, PdfWriter};
use cbz2pdf::formats::{FormatReader, FormatWriter};
use cbz2pdf::model::{Document, ImagePage};
fn make_small_jpeg(w: u32, h: u32, rgb: [u8; 3]) -> (Vec<u8>, image::DynamicImage) {
let mut img = image::ImageBuffer::<image::Rgb<u8>, _>::new(w, h);
for p in img.pixels_mut() {
*p = image::Rgb(rgb);
}
let dynimg = image::DynamicImage::ImageRgb8(img);
let mut buf = Vec::new();
{
let mut cursor = std::io::Cursor::new(&mut buf);
let mut enc = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut cursor, 85);
let rgb8 = dynimg.to_rgb8();
enc.encode(&rgb8, w, h, image::ColorType::Rgb8.into())
.unwrap();
}
(buf, dynimg)
}
#[test]
fn pdf_reader_extracts_jpeg_xobject_and_preserves_dct() {
// Build a PDF with one JPEG-backed page
let (jpeg_dct, dynimg) = make_small_jpeg(3, 2, [10, 20, 30]);
let page = ImagePage {
name: "p1.jpg".into(),
image: dynimg.clone(),
jpeg_dct: Some(jpeg_dct.clone()),
};
let doc = Document::new(vec![page]);
let temp_dir = tempfile::tempdir().expect("tmpdir");
let pdf_path = temp_dir.path().join("in.pdf");
PdfWriter.write(&doc, &pdf_path).expect("write pdf");
// Read back with PdfReader
let out = PdfReader.read(&pdf_path).expect("read pdf");
assert_eq!(out.pages.len(), 1, "should have one page extracted");
let p = &out.pages[0];
assert_eq!(p.image.width(), dynimg.width());
assert_eq!(p.image.height(), dynimg.height());
assert!(p.jpeg_dct.is_some(), "should preserve DCT for JPEG images");
assert_eq!(
p.jpeg_dct.as_ref().unwrap(),
&jpeg_dct,
"JPEG bytes should match"
);
}