Apache Commons Compress
| Language | Java |
|---|---|
| License | Apache-2.0 |
| Repository | github.com/apache/commons-compress |
| Documentation | commons.apache.org/proper/commons-compress, ZIP notes |
| Depends on | commons-io, commons-codec, commons-lang3; optional xz, zstd-jni, brotli |
| Read / write | yes / 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#
ZipFile.builder().setCharset("Cp437")reads names from old archives that don’t set the UTF-8 flag.zip.getEntriesInPhysicalOrder()returns entries in file order; reading them in that order is faster on slow storage.- The
ZipFile(File)constructors still work but are deprecated in favor ofZipFile.builder().