All formats

Apache Commons Compress

LanguageJava
LicenseApache-2.0
Repositorygithub.com/apache/commons-compress
Documentation7z notes, SevenZFile Javadoc
Depends oncommons-io, commons-codec, commons-lang3; org.tukaani:xz for 7z (required, even for stored entries)
Read / writeyes / yes (LZMA, LZMA2, BZip2, Deflate, Deflate64, Copy, BCJ filters; AES decryption)

Commons Compress reads 7z with SevenZFile and writes it with SevenZOutputFile. SevenZFile.getEntries() lists every entry from the header at the end of the file; getInputStream(entry) gives random access to one entry, and getNextEntry() plus read() walks the archive sequentially, which is the fast way through a solid archive. The same library handles CBZ and CBT.

7z support needs the XZ for Java library (org.tukaani:xz) on the classpath. It’s an optional Maven dependency, so add it yourself or you get NoClassDefFoundError: org/tukaani/xz/... on the first 7z operation.

Installation#

Maven:

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

Gradle:

groovy
implementation "org.apache.commons:commons-compress:1.28.0"
implementation "org.tukaani:xz:1.10"

Basic usage: list pages in natural order#

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

Java
import java.io.File;
import java.io.IOException;
import java.util.*;
import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
import org.apache.commons.compress.archivers.sevenz.SevenZFile;

public class Cb7Pages {
    static List<SevenZArchiveEntry> pages(SevenZFile sz) {
        List<SevenZArchiveEntry> pages = new ArrayList<>();
        for (SevenZArchiveEntry e : sz.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 (SevenZFile sz = SevenZFile.builder().setFile(new File(args[0])).get()) {
            List<SevenZArchiveEntry> pages = pages(sz);
            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 read all pages in one pass#

getNextEntry() walks entries in archive order and read() reads the current one; in a solid archive this decodes the block once:

Java
static Map<String, byte[]> readPages(File cb7) throws IOException {
    Map<String, byte[]> pages = new HashMap<>();
    try (SevenZFile sz = SevenZFile.builder().setFile(cb7).get()) {
        SevenZArchiveEntry e;
        while ((e = sz.getNextEntry()) != null) {
            if (e.isDirectory() || !Comic.isPage(e.getName())) continue;
            byte[] data = new byte[(int) e.getSize()];
            int off = 0, n;
            while (off < data.length && (n = sz.read(data, off, data.length - off)) > 0) off += n;
            pages.put(e.getName(), data);
        }
    }
    return pages;
}

How to extract the cover#

Java
try (SevenZFile sz = SevenZFile.builder().setFile(new File("comic.cb7")).get()) {
    List<SevenZArchiveEntry> pages = Cb7Pages.pages(sz);
    if (!pages.isEmpty()) {
        SevenZArchiveEntry cover = pages.get(0);
        String ext = cover.getName().substring(cover.getName().lastIndexOf('.'));
        try (InputStream in = sz.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 (SevenZFile sz = SevenZFile.builder().setFile(new File("comic.cb7")).get()) {
    for (SevenZArchiveEntry e : sz.getEntries()) {
        if (!e.getName().equalsIgnoreCase("ComicInfo.xml")) continue;
        DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
        f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        try (InputStream in = sz.getInputStream(e)) {
            Element ci = f.newDocumentBuilder().parse(in).getDocumentElement();
            System.out.println(ci.getElementsByTagName("Series").item(0).getTextContent());
            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"))  // Image = index in sorted page list
                    System.out.println("cover index " + p.getAttribute("Image"));
            }
        }
    }
}

How to create a CB7 from a folder of images#

Use SevenZMethod.COPY for JPEG/PNG pages: they don’t compress further, and stored entries are cheap to read in any order.

Java
import java.nio.file.*;
import java.util.*;
import org.apache.commons.compress.archivers.sevenz.*;

static void makeCb7(Path dir, File 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()));
    Path info = dir.resolve("ComicInfo.xml");
    if (Files.exists(info)) files.add(info);

    try (SevenZOutputFile sz = new SevenZOutputFile(out)) {
        sz.setContentCompression(SevenZMethod.COPY);  // LZMA2 is the default
        int i = 0;
        for (Path p : files) {
            String file = p.getFileName().toString();
            String name = file.equals("ComicInfo.xml") ? file
                : String.format("%03d%s", ++i, file.substring(file.lastIndexOf('.')).toLowerCase());
            SevenZArchiveEntry e = sz.createArchiveEntry(p, name);
            sz.putArchiveEntry(e);
            sz.write(p);
            sz.closeArchiveEntry();
        }
    }
}

Tips#