All formats

zip

LanguageRust
LicenseMIT
Repositorygithub.com/zip-rs/zip2
Documentationdocs.rs/zip
Depends oncompression crates behind Cargo features (deflate, bzip2, zstd, xz, lzma, aes-crypto)
Read / writeyes / yes (ZIP, ZIP64, AES; Deflate, Deflate64, BZip2, Zstd, LZMA, XZ, PPMd)

zip reads and writes ZIP archives. ZipArchive::new parses the central directory, then by_index or by_name gives a reader for one entry, so a comic viewer decodes only the page on screen. ZipWriter writes entries with any supported method; use Stored for images.

The crate lives in the zip-rs/zip2 repository (it was mvdnes/zip-rs) and releases often (major versions change small parts of the API). The examples use the 9.x API with SimpleFileOptions.

Installation#

Shell
cargo add zip
# for ComicInfo.xml
cargo add roxmltree

Basic usage: list pages in natural order#

Rust
use std::cmp::Ordering;
use std::fs::File;

fn is_page(name: &str) -> bool {
    let name = name.replace('\\', "/");
    if name.contains("__MACOSX/") {
        return false;
    }
    let base = name.rsplit('/').next().unwrap_or("");
    if base.starts_with('.') {
        return false;
    }
    match base.rsplit_once('.') {
        Some((_, ext)) => matches!(
            ext.to_ascii_lowercase().as_str(),
            "jpg" | "jpeg" | "png" | "gif" | "webp" | "bmp" | "avif" | "jxl"
        ),
        None => false,
    }
}

/// Natural, case-insensitive order: "2.jpg" < "10.jpg".
fn natural_cmp(a: &str, b: &str) -> Ordering {
    let (a, b) = (a.to_lowercase(), b.to_lowercase());
    let (mut x, mut y) = (a.as_bytes(), b.as_bytes());
    while !x.is_empty() && !y.is_empty() {
        if x[0].is_ascii_digit() && y[0].is_ascii_digit() {
            let nx = x.iter().take_while(|c| c.is_ascii_digit()).count();
            let ny = y.iter().take_while(|c| c.is_ascii_digit()).count();
            let dx = &x[x[..nx].iter().take_while(|&&c| c == b'0').count()..nx];
            let dy = &y[y[..ny].iter().take_while(|&&c| c == b'0').count()..ny];
            let ord = dx.len().cmp(&dy.len()).then_with(|| dx.cmp(dy));
            if ord != Ordering::Equal {
                return ord;
            }
            x = &x[nx..];
            y = &y[ny..];
        } else {
            if x[0] != y[0] {
                return x[0].cmp(&y[0]);
            }
            x = &x[1..];
            y = &y[1..];
        }
    }
    x.len().cmp(&y.len())
}

fn pages<R: std::io::Read + std::io::Seek>(zip: &zip::ZipArchive<R>) -> Vec<String> {
    let mut pages: Vec<String> = zip
        .file_names()
        .filter_map(|n| n.ok()) // names are decoded on the fly; skip undecodable ones
        .filter(|n| !n.ends_with('/') && is_page(n))
        .map(|n| n.into_owned())
        .collect();
    pages.sort_by(|a, b| natural_cmp(a, b));
    pages
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = std::env::args().nth(1).ok_or("usage: pages <file.cbz>")?;
    let zip = zip::ZipArchive::new(File::open(path)?)?;
    for (i, name) in pages(&zip).iter().enumerate() {
        println!("{:3} {}", i + 1, name);
    }
    Ok(())
}

How to read a page and extract the cover#

Rust
use std::io::Read;

fn read_entry<R: Read + std::io::Seek>(zip: &mut zip::ZipArchive<R>, name: &str)
    -> zip::result::ZipResult<Vec<u8>> {
    let mut f = zip.by_name(name)?;
    let mut data = Vec::with_capacity(f.size() as usize);
    f.read_to_end(&mut data)?;
    Ok(data)
}

fn extract_cover(path: &str) -> Result<(), Box<dyn std::error::Error>> {
    let mut zip = zip::ZipArchive::new(File::open(path)?)?;
    let first = pages(&zip).into_iter().next().ok_or("no pages")?;
    let data = read_entry(&mut zip, &first)?;
    let ext = first.rsplit_once('.').map(|(_, e)| e.to_lowercase()).unwrap_or_default();
    std::fs::write(format!("cover.{ext}"), data)?;
    Ok(())
}

Decode the bytes with the image crate: image::load_from_memory(&data)?.

How to read ComicInfo.xml#

Rust
fn print_comic_info(path: &str) -> Result<(), Box<dyn std::error::Error>> {
    let mut zip = zip::ZipArchive::new(File::open(path)?)?;
    let Some(name) = zip
        .file_names()
        .filter_map(|n| n.ok())
        .find(|n| n.eq_ignore_ascii_case("ComicInfo.xml"))
        .map(|n| n.into_owned())
    else {
        return Ok(()); // no metadata
    };
    let xml = String::from_utf8(read_entry(&mut zip, &name)?)?;
    let doc = roxmltree::Document::parse(&xml)?;
    let root = doc.root_element();
    let get = |tag: &str| {
        root.children().find(|n| n.has_tag_name(tag)).and_then(|n| n.text()).unwrap_or("")
    };
    println!("{} #{}: {}", get("Series"), get("Number"), get("Title"));
    println!("pages: {}, manga: {}", get("PageCount"), get("Manga"));

    // <Pages><Page Image="0" Type="FrontCover"/>: Image is the index in sorted page order
    let all = pages(&zip);
    for p in root.descendants().filter(|n| n.has_tag_name("Page")) {
        if p.attribute("Type").unwrap_or("").contains("FrontCover") {
            let idx: usize = p.attribute("Image").unwrap_or("0").parse()?;
            println!("cover: {}", all.get(idx).map(String::as_str).unwrap_or("?"));
        }
    }
    Ok(())
}

How to create a CBZ from a folder of images#

Rust
use std::io::Write;
use zip::write::SimpleFileOptions;
use zip::CompressionMethod;

fn make_cbz(dir: &str, out: &str) -> Result<(), Box<dyn std::error::Error>> {
    let mut files: Vec<String> = std::fs::read_dir(dir)?
        .filter_map(|e| e.ok())
        .filter(|e| e.file_type().map(|t| t.is_file()).unwrap_or(false))
        .filter_map(|e| e.file_name().into_string().ok())
        .filter(|n| is_page(n))
        .collect();
    files.sort_by(|a, b| natural_cmp(a, b));

    let mut zip = zip::ZipWriter::new(File::create(out)?);
    let stored = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
    for (i, name) in files.iter().enumerate() {
        let ext = name.rsplit_once('.').map(|(_, e)| e.to_lowercase()).unwrap_or_default();
        zip.start_file(format!("{:03}.{ext}", i + 1), stored)?;
        zip.write_all(&std::fs::read(format!("{dir}/{name}"))?)?;
    }
    let info = format!("{dir}/ComicInfo.xml");
    if std::path::Path::new(&info).exists() {
        zip.start_file("ComicInfo.xml", SimpleFileOptions::default())?; // Deflate
        zip.write_all(&std::fs::read(&info)?)?;
    }
    zip.finish()?;
    Ok(())
}

Tips#