Apache Commons Compress
| Language | Java |
|---|---|
| License | Apache-2.0 |
| Repository | github.com/apache/commons-compress |
| Documentation | 7z notes, SevenZFile Javadoc |
| Depends on | commons-io, commons-codec, commons-lang3; org.tukaani:xz for 7z (required, even for stored entries) |
| Read / write | yes / 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:
<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:
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.
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:
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#
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#
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.
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#
SevenZFile.builder().setPassword("secret".toCharArray())opens encrypted archives.- Random access with
getInputStream(entry)in a solid archive decodes from the start of the block; Commons Compress skips forward when you read entries in order, but jumping back restarts decoding. - For untrusted files, set
SevenZFile.builder().setMaxMemoryLimitKiB(...)to cap LZMA dictionary memory.