All formats

Apache Commons Compress

LanguageJava
LicenseApache-2.0
Repositorygithub.com/apache/commons-compress
DocumentationTAR notes, Javadoc
Depends oncommons-io, commons-codec, commons-lang3
Read / writeyes / yes (ustar, pax, GNU long names, star; compressed tar through the compressor classes)

Commons Compress has three ways to handle TAR. TarFile reads all headers of a file up front and then opens any entry by seeking to it: the most convenient for a comic reader, because it gives random access to pages in an uncompressed CBT. TarArchiveInputStream reads entries sequentially from any stream. TarArchiveOutputStream writes archives. The same library reads and writes CBZ and CB7.

The JDK has no TAR support, so this is the standard choice in Java.

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 helpers from the java.util.zip CBZ page: a natural, case-insensitive sort and an image-extension filter that skips __MACOSX and dotfiles (including macOS ._ files).

Java
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarFile;

public class CbtPages {
    static List<TarArchiveEntry> pages(TarFile tar) {
        List<TarArchiveEntry> pages = new ArrayList<>();
        for (TarArchiveEntry e : tar.getEntries())
            if (e.isFile() && 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 (TarFile tar = new TarFile(Path.of(args[0]))) {
            List<TarArchiveEntry> pages = pages(tar);
            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#

TarFile.getInputStream seeks straight to the entry’s data:

Java
try (TarFile tar = new TarFile(Path.of("comic.cbt"))) {
    List<TarArchiveEntry> pages = CbtPages.pages(tar);
    if (!pages.isEmpty()) {
        TarArchiveEntry cover = pages.get(0);
        String ext = cover.getName().substring(cover.getName().lastIndexOf('.'));
        try (InputStream in = tar.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 (TarFile tar = new TarFile(Path.of("comic.cbt"))) {
    for (TarArchiveEntry e : tar.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 = tar.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 read a CBT from a stream#

When the comic comes from the network or another archive, read it sequentially:

Java
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;

Map<String, byte[]> pages = new HashMap<>();
try (TarArchiveInputStream tin = new TarArchiveInputStream(inputStream)) {
    TarArchiveEntry e;
    while ((e = tin.getNextEntry()) != null) {
        if (e.isFile() && Comic.isPage(e.getName()))
            pages.put(e.getName(), tin.readAllBytes());  // reads only the current entry
    }
}

How to create a CBT from a folder of images#

Set LONGFILE_POSIX so names longer than 100 characters get a pax header instead of an error:

Java
import java.nio.file.*;
import java.util.*;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;

static void makeCbt(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 (TarArchiveOutputStream tos = new TarArchiveOutputStream(Files.newOutputStream(out))) {
        tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
        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());
            tos.putArchiveEntry(new TarArchiveEntry(files.get(i), name));
            Files.copy(files.get(i), tos);
            tos.closeArchiveEntry();
        }
        Path info = dir.resolve("ComicInfo.xml");
        if (Files.exists(info)) {
            tos.putArchiveEntry(new TarArchiveEntry(info, "ComicInfo.xml"));
            Files.copy(info, tos);
            tos.closeArchiveEntry();
        }
        tos.finish();
    }
}

Tips#