junrar
| Language | Java (8+) |
|---|---|
| License | UnRAR license (freeware, not OSI-approved: can't be used to build a RAR compressor) |
| Repository | github.com/junrar/junrar |
| Documentation | README, Javadoc |
| Depends on | slf4j-api |
| Read / write | yes / 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:
<dependency>
<groupId>com.github.junrar</groupId>
<artifactId>junrar</artifactId>
<version>7.5.5</version>
</dependency>
Gradle:
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.
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#
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#
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:
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#
archive.getMainHeader().isSolid()tells you whether the archive is solid;archive.isEncrypted()whether headers are encrypted.- Password-protected CBR:
new Archive(file, "password"). getInputStreamdecompresses on a background thread through a pipe. For bulk work,extractFile(header, outputStream)is simpler and avoids the extra thread.- An
Archiveis not thread-safe: use one per thread.