java.util.zip
| Language | Java |
|---|---|
| License | GPL-2.0 with Classpath Exception (part of OpenJDK) |
| Repository | github.com/openjdk/jdk |
| Documentation | java.util.zip Javadoc |
| Depends on | nothing (built into the JDK) |
| Read / write | yes / yes (ZIP, ZIP64; no encryption) |
java.util.zip is the ZIP support built into every JDK. ZipFile reads the central directory and opens any entry directly, which is what a comic reader needs. ZipOutputStream writes new archives. The Classpath Exception means you can use it in any application, including closed-source ones.
Limitations: no encryption, and stored (uncompressed) entries in ZipOutputStream need their size and CRC-32 set before writing. Apache Commons Compress removes that restriction.
Installation#
Nothing to install: java.util.zip is part of java.base.
Basic usage: list pages in natural order#
import java.io.IOException;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class Comic {
static final Set<String> IMAGE_EXTS =
Set.of("jpg", "jpeg", "png", "gif", "webp", "bmp", "avif", "jxl");
static boolean isPage(String path) {
path = path.replace('\\', '/');
if (path.contains("__MACOSX/")) return false;
String name = path.substring(path.lastIndexOf('/') + 1);
int dot = name.lastIndexOf('.');
return !name.isEmpty() && name.charAt(0) != '.' && dot > 0
&& IMAGE_EXTS.contains(name.substring(dot + 1).toLowerCase(Locale.ROOT));
}
// natural, case-insensitive order: "2.jpg" < "10.jpg"
static int naturalCompare(String a, String b) {
int i = 0, j = 0;
while (i < a.length() && j < b.length()) {
char ca = a.charAt(i), cb = b.charAt(j);
if (Character.isDigit(ca) && Character.isDigit(cb)) {
int ie = i, je = j;
while (ie < a.length() && Character.isDigit(a.charAt(ie))) ie++;
while (je < b.length() && Character.isDigit(b.charAt(je))) je++;
String na = a.substring(i, ie).replaceFirst("^0+(?=.)", "");
String nb = b.substring(j, je).replaceFirst("^0+(?=.)", "");
if (na.length() != nb.length()) return na.length() - nb.length();
int c = na.compareTo(nb);
if (c != 0) return c;
i = ie;
j = je;
} else {
int c = Character.compare(Character.toLowerCase(ca), Character.toLowerCase(cb));
if (c != 0) return c;
i++;
j++;
}
}
return (a.length() - i) - (b.length() - j);
}
static List<ZipEntry> pages(ZipFile zip) {
List<ZipEntry> pages = new ArrayList<>();
zip.stream().filter(e -> !e.isDirectory() && isPage(e.getName())).forEach(pages::add);
pages.sort((x, y) -> naturalCompare(x.getName(), y.getName()));
return pages;
}
public static void main(String[] args) throws IOException {
try (ZipFile zip = new ZipFile(args[0])) {
List<ZipEntry> pages = pages(zip);
for (int i = 0; i < pages.size(); i++)
System.out.printf("%3d %s%n", i + 1, pages.get(i).getName());
}
}
}
How to extract the cover#
The cover is the first page in natural order:
try (ZipFile zip = new ZipFile("comic.cbz")) {
List<ZipEntry> pages = Comic.pages(zip);
if (!pages.isEmpty()) {
ZipEntry cover = pages.get(0);
String ext = cover.getName().substring(cover.getName().lastIndexOf('.'));
try (InputStream in = zip.getInputStream(cover)) {
Files.copy(in, Path.of("cover" + ext), StandardCopyOption.REPLACE_EXISTING);
}
}
}
To decode it, pass the stream to ImageIO.read(in) (JPEG, PNG, GIF, BMP built in; WebP needs a plugin such as TwelveMonkeys).
How to read ComicInfo.xml#
import java.io.InputStream;
import java.util.zip.*;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.*;
static void printComicInfo(ZipFile zip) throws Exception {
ZipEntry entry = zip.stream()
.filter(e -> e.getName().equalsIgnoreCase("ComicInfo.xml"))
.findFirst().orElse(null);
if (entry == null) return;
DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); // no XXE
Element ci;
try (InputStream in = zip.getInputStream(entry)) {
ci = f.newDocumentBuilder().parse(in).getDocumentElement();
}
System.out.println(text(ci, "Series") + " #" + text(ci, "Number") + ": " + text(ci, "Title"));
System.out.println("Pages: " + text(ci, "PageCount") + ", manga: " + text(ci, "Manga"));
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"))
System.out.println("cover is page index " + p.getAttribute("Image"));
}
}
static String text(Element parent, String tag) {
NodeList n = parent.getElementsByTagName(tag);
return n.getLength() > 0 ? n.item(0).getTextContent() : "";
}
The Image attribute is the 0-based index into the sorted page list, not a file name.
How to create a CBZ from a folder of images#
Stored entries need size and CRC-32 up front, so compute them first. This is cheap compared with compressing:
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.zip.*;
static void makeCbz(Path dir, Path out) throws IOException {
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 (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(out))) {
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());
byte[] data = Files.readAllBytes(files.get(i));
CRC32 crc = new CRC32();
crc.update(data);
ZipEntry e = new ZipEntry(name);
e.setMethod(ZipEntry.STORED); // pages are already compressed
e.setSize(data.length);
e.setCompressedSize(data.length);
e.setCrc(crc.getValue());
zos.putNextEntry(e);
zos.write(data);
zos.closeEntry();
}
Path info = dir.resolve("ComicInfo.xml");
if (Files.exists(info)) {
zos.putNextEntry(new ZipEntry("ComicInfo.xml")); // DEFLATED by default
Files.copy(info, zos);
zos.closeEntry();
}
}
}
Tips#
new ZipFile(file, Charset.forName("Cp437"))reads archives whose names are not UTF-8 (old tools that didn’t set the UTF-8 flag).- To update
ComicInfo.xmlwithout rewriting the archive, use the ZIP file system provider:FileSystems.newFileSystem(path)thenFiles.write(fs.getPath("ComicInfo.xml"), bytes). - Check
entry.getSize()against a limit before reading a page fully from an untrusted file.