When generating EPUB files or specific ZIP-based container formats in Java, strict validation rules often apply. According to the official EPUB specification, the mimetype file must be the very first file in the ZIP archive, must contain only application/epub+zip, and most importantly, must be stored completely uncompressed (using the STORED method).

The Problem with ZipOutputStream.setLevel(0)

A common pitfall developers encounter is using ZipOutputStream.setLevel(0) or assuming ZipEntry defaults to uncompressed storage. However, setting the compression level to zero merely instructs the Deflater engine to store blocks without compression, but it still wraps the data in standard Deflate container streams. This results in the file method remaining DEFLATED (Method 8) instead of STORED (Method 0).

EPUB validators, like Pagina EPUB-Checker, check the underlying ZIP entry compression header and will fail the validation with an error indicating the mimetype file is compressed.

The Solution: Explicitly Set ZipEntry.STORED

To store a file without compression in Java using java.util.zip, you must explicitly set the entry method to ZipEntry.STORED. When doing this, Java requires you to manually provide three mandatory metadata attributes before writing the data:

  • Size: The uncompressed size in bytes.
  • Compressed Size: Exactly the same value as the uncompressed size.
  • CRC-32 Checksum: The 32-bit checksum of the raw bytes.

Step-by-Step Implementation

Here is how to properly write an uncompressed mimetype file followed by compressed archive entries in modern Java:

import java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.zip.CRC32;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipOutputStream;\n\npublic class EpubZipUtil {\n\n    public static void createEpub(Path epubContentsPath, Path outputEpubPath) throws IOException {\n        byte[] mimetypeBytes = Files.readAllBytes(epubContentsPath.resolve(\"mimetype\"));\n\n        // Compute CRC-32 checksum for the uncompressed mimetype file\n        CRC32 crc = new CRC32();\n        crc.update(mimetypeBytes);\n\n        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outputEpubPath.toFile()))) {\n            // 1. Configure the mimetype entry as uncompressed (STORED)\n            ZipEntry mimetypeEntry = new ZipEntry(\"mimetype\");\n            mimetypeEntry.setMethod(ZipEntry.STORED);\n            mimetypeEntry.setSize(mimetypeBytes.length);\n            mimetypeEntry.setCompressedSize(mimetypeBytes.length);\n            mimetypeEntry.setCrc(crc.getValue());\n\n            // Write the mimetype entry\n            zos.putNextEntry(mimetypeEntry);\n            zos.write(mimetypeBytes);\n            zos.closeEntry();\n\n            // 2. Add the remaining files using standard DEFLATED compression\n            Files.walk(epubContentsPath)\n                .filter(path -> !Files.isDirectory(path))\n                .filter(path -> !path.getFileName().toString().equals(\"mimetype\"))\n                .forEach(path -> {\n                    try {\n                        String zipPath = epubContentsPath.relativize(path).toString().replace(\\'\\\\\\', '/');\n                        ZipEntry entry = new ZipEntry(zipPath);\n                        entry.setMethod(ZipEntry.DEFLATED);\n                        zos.putNextEntry(entry);\n                        Files.copy(path, zos);\n                        zos.closeEntry();\n                    } catch (IOException e) {\n                        throw new RuntimeException(e);\n                    }\n                });\n        }\n    }\n}

Key Takeaways

  • Always calculate the CRC32: Failing to provide a CRC value or setting mismatched sizes when using ZipEntry.STORED will cause a ZipException: STORED entry missing size, compressed size, or crc-32.
  • Entry order matters for EPUBs: The mimetype entry must always be the first file added to the stream before any folders or other content files.
  • Switch back to DEFLATED: Ensure subsequent entries use entry.setMethod(ZipEntry.DEFLATED) so that remaining files (like HTML, CSS, images) are compressed efficiently.