All formats

Apache Commons Compress

LanguageJava
LicenseApache-2.0
Repositorygithub.com/apache/commons-compress
Documentationcommons.apache.org/proper/commons-compress, ZIP notes
Depends oncommons-io, commons-codec, commons-lang3; optional xz, zstd-jni, brotli
Read / writeyes / yes (ZIP, ZIP64, split archives; also 7z, TAR, AR, CPIO)

Commons Compress is the Apache library for archive and compression formats. Its ZipFile reads the central directory and opens entries directly, like the JDK’s, but handles more real-world ZIP files: names in legacy encodings, ZIP64 edge cases, unusual extra fields. ZipArchiveOutputStream writes stored entries to a file without precomputing CRCs (it seeks back to fill them in).

The same library reads and writes CB7 and CBT. It doesn’t read RAR; use junrar for CBR.

Installation#

Maven:

XML
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-compress</artifactId>
  <version>1.28.0</version>
</dependency>

Gradle:

groovy
implementation "org.apache.commons:commons-compress:1.28.0"

Basic usage: list pages in natural order#

naturalCompare and isPage are the same helpers as on the java.util.zip page: natural, case-insensitive sort and an image-extension filter that skips __MACOSX and dotfiles.

Java
import java.io.IOException;
import java.util.*;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;

public class CbzPages {
    static List<ZipArchiveEntry> pages(ZipFile zip) {
        List<ZipArchiveEntry> pages = new ArrayList<>();
        for (ZipArchiveEntry e : Collections.list(zip.getEntries()))
            if (!e.isDirectory() && Comic.isPage(e.getName())) pages.add(e);
        pages.sort((a, b) -> Comic.naturalCompare(a.getName(), b.getName()));
        return pages;
    }

    public static void main(String[] args) throws IOException {
        try (ZipFile zip = ZipFile.builder().setFile(args[0]).get()) {
            List<ZipArchiveEntry> pages = pages(zip);
            for (int i = 0; i < pages.size(); i++)
                System.out.printf("%3d %s (%d bytes)%n", i + 1, pages.get(i).getName(), pages.get(i).getSize());
        }
    }
}

How to extract the cover#

Java
try (ZipFile zip = ZipFile.builder().setFile("comic.cbz").get()) {
    List<ZipArchiveEntry> pages = CbzPages.pages(zip);
    if (!pages.isEmpty()) {
        ZipArchiveEntry cover = pages.get(0);
        String ext = cover.getName().substring(cover.getName().lastIndexOf('.'));
        try (InputStream in = zip.getInputStream(cover)) {
            Files.copy(in, Path.of("cover" + ext), StandardCopyOption.REPLACE_EXISTING);
        }
    }
}

How to read ComicInfo.xml#

Java
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Element;

try (ZipFile zip = ZipFile.builder().setFile("comic.cbz").get()) {
    ZipArchiveEntry entry = Collections.list(zip.getEntries()).stream()
        .filter(e -> e.getName().equalsIgnoreCase("ComicInfo.xml"))
        .findFirst().orElse(null);
    if (entry != null) {
        DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
        f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        try (InputStream in = zip.getInputStream(entry)) {
            Element ci = f.newDocumentBuilder().parse(in).getDocumentElement();
            System.out.println(ci.getElementsByTagName("Series").item(0).getTextContent());
            // <Page Image="0" Type="FrontCover"/>: Image is the index in sorted page order
            var list = ci.getElementsByTagName("Page");
            for (int i = 0; i < list.getLength(); i++) {
                Element p = (Element) list.item(i);
                if (p.getAttribute("Type").contains("FrontCover"))
                    System.out.println("cover index " + p.getAttribute("Image"));
            }
        }
    }
}

How to create a CBZ from a folder of images#

Writing to a file lets ZipArchiveOutputStream store entries without knowing their CRC in advance:

Java
import java.nio.file.*;
import java.util.*;
import java.util.zip.ZipEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;

static void makeCbz(Path dir, Path out) throws Exception {
    List<Path> files;
    try (var s = Files.list(dir)) {
        files = new ArrayList<>(s.filter(p -> Comic.isPage(p.getFileName().toString())).toList());
    }
    files.sort((a, b) -> Comic.naturalCompare(a.getFileName().toString(), b.getFileName().toString()));

    try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream(out)) {
        zos.setMethod(ZipEntry.STORED);  // pages are already compressed
        for (int i = 0; i < files.size(); i++) {
            String file = files.get(i).getFileName().toString();
            String name = String.format("%03d%s", i + 1, file.substring(file.lastIndexOf('.')).toLowerCase());
            zos.putArchiveEntry(new ZipArchiveEntry(files.get(i), name));
            Files.copy(files.get(i), zos);
            zos.closeArchiveEntry();
        }
        Path info = dir.resolve("ComicInfo.xml");
        if (Files.exists(info)) {
            zos.putArchiveEntry(new ZipArchiveEntry(info, "ComicInfo.xml"));
            Files.copy(info, zos);
            zos.closeArchiveEntry();
        }
        zos.finish();
    }
}

Tips#