All formats

junrar

LanguageJava (8+)
LicenseUnRAR license (freeware, not OSI-approved: can't be used to build a RAR compressor)
Repositorygithub.com/junrar/junrar
DocumentationREADME, Javadoc
Depends onslf4j-api
Read / writeyes / no (RAR 1.5-4.x including solid, encrypted and multi-volume; no RAR5)

junrar is a pure-Java port of the UnRAR decoder. new Archive(file) reads the headers; getFileHeaders() lists the entries and getInputStream(header) decompresses one. There’s no native code, so it runs anywhere the JVM does, including Android.

The big limitation is RAR5: junrar throws UnsupportedRarV5Exception for archives created by WinRAR 5.0+ in its default format. Catch it and tell the user to convert the file, or fall back to an external unrar/7z process. Because it derives from UnRAR, the code is under the UnRAR license, which is free to use but not open source in the OSI sense.

Installation#

Maven:

XML
<dependency>
  <groupId>com.github.junrar</groupId>
  <artifactId>junrar</artifactId>
  <version>7.5.5</version>
</dependency>

Gradle:

groovy
implementation "com.github.junrar:junrar:7.5.5"

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 com.github.junrar.Archive;
import com.github.junrar.exception.RarException;
import com.github.junrar.exception.UnsupportedRarV5Exception;
import com.github.junrar.rarfile.FileHeader;
import java.io.File;
import java.io.IOException;
import java.util.*;

public class CbrPages {
    static List<FileHeader> pages(Archive archive) {
        List<FileHeader> pages = new ArrayList<>();
        for (FileHeader fh : archive.getFileHeaders())
            if (!fh.isDirectory() && Comic.isPage(fh.getFileName())) pages.add(fh);
        pages.sort((a, b) -> Comic.naturalCompare(a.getFileName(), b.getFileName()));
        return pages;
    }

    public static void main(String[] args) throws IOException {
        try (Archive archive = new Archive(new File(args[0]))) {
            List<FileHeader> pages = pages(archive);
            for (int i = 0; i < pages.size(); i++)
                System.out.printf("%3d %s (%d bytes)%n", i + 1, pages.get(i).getFileName(),
                                  pages.get(i).getFullUnpackSize());
        } catch (UnsupportedRarV5Exception e) {
            System.err.println("RAR5 archive: not supported by junrar");
        } catch (RarException e) {
            System.err.println("not a valid RAR file: " + e.getMessage());
        }
    }
}

getFileName() returns the name with \ separators as stored by WinRAR on Windows; isPage normalizes them.

How to extract the cover#

Java
try (Archive archive = new Archive(new File("comic.cbr"))) {
    List<FileHeader> pages = CbrPages.pages(archive);
    if (!pages.isEmpty()) {
        FileHeader cover = pages.get(0);
        String name = cover.getFileName();
        try (InputStream in = archive.getInputStream(cover)) {
            Files.copy(in, Path.of("cover" + name.substring(name.lastIndexOf('.'))),
                       StandardCopyOption.REPLACE_EXISTING);
        }
    }
}

In a solid archive, reading one entry decompresses all entries before it. That’s fine for a cover (usually the first entry); for a full reader, read all pages once in archive order.

How to read ComicInfo.xml#

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

try (Archive archive = new Archive(new File("comic.cbr"))) {
    FileHeader info = archive.getFileHeaders().stream()
        .filter(fh -> fh.getFileName().equalsIgnoreCase("ComicInfo.xml"))
        .findFirst().orElse(null);
    if (info != null) {
        DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
        f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        try (InputStream in = archive.getInputStream(info)) {
            Element ci = f.newDocumentBuilder().parse(in).getDocumentElement();
            NodeList series = ci.getElementsByTagName("Series");
            if (series.getLength() > 0) System.out.println("Series: " + series.item(0).getTextContent());
            NodeList 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: " + CbrPages.pages(archive).get(Integer.parseInt(p.getAttribute("Image"))).getFileName());
            }
        }
    }
}

How to convert CBR to CBZ#

Read the entries in archive order (fast for solid archives) and store them in a ZIP with Commons Compress, which writes stored entries without precomputing CRCs:

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

static void cbrToCbz(File cbr, Path cbz) throws Exception {
    try (Archive archive = new Archive(cbr);
         ZipArchiveOutputStream zos = new ZipArchiveOutputStream(cbz)) {
        zos.setMethod(ZipEntry.STORED);
        for (FileHeader fh : archive.getFileHeaders()) {
            if (fh.isDirectory()) continue;
            zos.putArchiveEntry(new ZipArchiveEntry(fh.getFileName().replace('\\', '/')));
            archive.extractFile(fh, zos);  // decompress straight into the ZIP
            zos.closeArchiveEntry();
        }
        zos.finish();
    }
}

Tips#