All formats

libzip

LanguageC (usable from C++)
LicenseBSD-3-Clause
Repositorygithub.com/nih-at/libzip
Documentationlibzip.org/documentation
Depends onzlib; optional bzip2, liblzma, zstd, OpenSSL/GnuTLS for AES
Read / writeyes / yes (ZIP, ZIP64, AES encryption)

libzip reads, creates and modifies ZIP archives. It reads the central directory when it opens a file, so you can get the list of entries and open any entry by index or name without touching the others. That makes it a good base for a comic reader: list and sort the pages once, then decompress a page when the user turns to it.

libzip only handles ZIP. For CBR, CB7 and CBT in the same code base, use libarchive.

Installation#

Shell
# Debian / Ubuntu
sudo apt install libzip-dev
# Fedora
sudo dnf install libzip-devel
# macOS
brew install libzip
# Windows (vcpkg)
vcpkg install libzip

With CMake:

cmake
find_package(libzip REQUIRED)
target_link_libraries(myapp PRIVATE libzip::zip)

Basic usage: list pages in natural order#

A comic reader sorts image entries by name with a natural sort (2.jpg before 10.jpg) and skips everything else:

C++
#include <zip.h>
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <string>
#include <vector>

// natural, case-insensitive "a < b": digit runs compare as numbers
bool naturalLess(const std::string& a, const std::string& b) {
    size_t i = 0, j = 0;
    while (i < a.size() && j < b.size()) {
        if (isdigit((unsigned char)a[i]) && isdigit((unsigned char)b[j])) {
            size_t ie = i, je = j;
            while (ie < a.size() && isdigit((unsigned char)a[ie])) ie++;
            while (je < b.size() && isdigit((unsigned char)b[je])) je++;
            while (i < ie - 1 && a[i] == '0') i++;  // skip leading zeros
            while (j < je - 1 && b[j] == '0') j++;
            if (ie - i != je - j) return ie - i < je - j;  // fewer digits = smaller
            int c = a.compare(i, ie - i, b, j, je - j);
            if (c != 0) return c < 0;
            i = ie;
            j = je;
        } else {
            int ca = tolower((unsigned char)a[i]), cb = tolower((unsigned char)b[j]);
            if (ca != cb) return ca < cb;
            i++;
            j++;
        }
    }
    return a.size() - i < b.size() - j;
}

bool isPage(const std::string& name) {
    if (name.find("__MACOSX/") != std::string::npos) return false;
    size_t slash = name.find_last_of('/');
    std::string base = slash == std::string::npos ? name : name.substr(slash + 1);
    if (base.empty() || base[0] == '.') return false;  // directories, .DS_Store, ._001.jpg
    size_t dot = base.find_last_of('.');
    if (dot == std::string::npos) return false;
    std::string ext = base.substr(dot + 1);
    for (auto& c : ext) c = (char)tolower((unsigned char)c);
    for (const char* e : {"jpg", "jpeg", "png", "gif", "webp", "bmp", "avif", "jxl"})
        if (ext == e) return true;
    return false;
}

std::vector<std::string> listPages(zip_t* za) {
    std::vector<std::string> pages;
    zip_int64_t n = zip_get_num_entries(za, 0);
    for (zip_int64_t i = 0; i < n; i++) {
        const char* name = zip_get_name(za, i, 0);
        if (name && isPage(name)) pages.push_back(name);
    }
    std::sort(pages.begin(), pages.end(), naturalLess);
    return pages;
}

int main(int argc, char** argv) {
    int err = 0;
    zip_t* za = zip_open(argv[1], ZIP_RDONLY, &err);
    if (!za) {
        zip_error_t ze;
        zip_error_init_with_code(&ze, err);
        fprintf(stderr, "can't open %s: %s\n", argv[1], zip_error_strerror(&ze));
        zip_error_fini(&ze);
        return 1;
    }
    std::vector<std::string> pages = listPages(za);
    for (size_t i = 0; i < pages.size(); i++)
        printf("%3zu %s\n", i + 1, pages[i].c_str());
    zip_close(za);
    return 0;
}

How to read one page into memory#

zip_stat gives the uncompressed size, zip_fopen opens the entry by name:

C++
std::vector<unsigned char> readEntry(zip_t* za, const std::string& name) {
    std::vector<unsigned char> data;
    zip_stat_t st;
    zip_stat_init(&st);
    if (zip_stat(za, name.c_str(), 0, &st) != 0 || !(st.valid & ZIP_STAT_SIZE)) return data;
    zip_file_t* zf = zip_fopen(za, name.c_str(), 0);
    if (!zf) return data;
    data.resize(st.size);
    zip_int64_t got = zip_fread(zf, data.data(), st.size);
    zip_fclose(zf);
    if (got != (zip_int64_t)st.size) data.clear();
    return data;
}

Pass the bytes to an image decoder (stb_image, libjpeg-turbo, libpng, libwebp).

How to extract the cover#

The cover is the first page in natural order:

C++
std::vector<std::string> pages = listPages(za);
if (!pages.empty()) {
    std::vector<unsigned char> cover = readEntry(za, pages[0]);
    std::string ext = pages[0].substr(pages[0].find_last_of('.'));
    FILE* f = fopen(("cover" + ext).c_str(), "wb");
    if (f) {
        fwrite(cover.data(), 1, cover.size(), f);
        fclose(f);
    }
}

If ComicInfo.xml has a <Page Type="FrontCover">, its Image attribute is the index into this sorted list (see below).

How to read ComicInfo.xml#

Find the entry case-insensitively with ZIP_FL_NOCASE and parse it with an XML library (here pugixml):

C++
#include <pugixml.hpp>
#include <cstring>

void printComicInfo(zip_t* za) {
    zip_int64_t idx = zip_name_locate(za, "ComicInfo.xml", ZIP_FL_NOCASE);
    if (idx < 0) {
        printf("no ComicInfo.xml\n");
        return;
    }
    std::vector<unsigned char> xml = readEntry(za, zip_get_name(za, idx, 0));
    pugi::xml_document doc;
    if (!doc.load_buffer(xml.data(), xml.size())) return;
    pugi::xml_node ci = doc.child("ComicInfo");
    printf("Series: %s #%s\n", ci.child_value("Series"), ci.child_value("Number"));
    printf("Title: %s\n", ci.child_value("Title"));
    printf("Writer: %s\n", ci.child_value("Writer"));
    printf("Pages: %s\n", ci.child_value("PageCount"));
    printf("Right to left: %s\n",
           strcmp(ci.child_value("Manga"), "YesAndRightToLeft") == 0 ? "yes" : "no");
    for (pugi::xml_node p : ci.child("Pages").children("Page")) {
        if (strstr(p.attribute("Type").value(), "FrontCover"))
            printf("cover is page index %d\n", p.attribute("Image").as_int());
    }
}

How to create a CBZ from a folder of images#

Add the images in sorted order with zero-padded names, and store them without compression: JPEG and PNG are already compressed.

C++
#include <zip.h>
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <vector>
namespace fs = std::filesystem;

int makeCbz(const fs::path& dir, const char* out) {
    std::vector<fs::path> files;
    for (auto& e : fs::directory_iterator(dir))
        if (e.is_regular_file() && isPage(e.path().filename().string())) files.push_back(e.path());
    std::sort(files.begin(), files.end(), [](const fs::path& a, const fs::path& b) {
        return naturalLess(a.filename().string(), b.filename().string());
    });

    int err = 0;
    zip_t* za = zip_open(out, ZIP_CREATE | ZIP_TRUNCATE, &err);
    if (!za) return 1;
    for (size_t i = 0; i < files.size(); i++) {
        char name[64];
        snprintf(name, sizeof(name), "%03zu%s", i + 1, files[i].extension().string().c_str());
        zip_source_t* src = zip_source_file(za, files[i].string().c_str(), 0, ZIP_LENGTH_TO_END);
        zip_int64_t idx = src ? zip_file_add(za, name, src, ZIP_FL_ENC_UTF_8) : -1;
        if (idx < 0) {
            zip_source_free(src);
            zip_discard(za);
            return 1;
        }
        zip_set_file_compression(za, idx, ZIP_CM_STORE, 0);
    }
    fs::path info = dir / "ComicInfo.xml";
    if (fs::exists(info)) {
        zip_source_t* src = zip_source_file(za, info.string().c_str(), 0, ZIP_LENGTH_TO_END);
        if (src && zip_file_add(za, "ComicInfo.xml", src, ZIP_FL_ENC_UTF_8) < 0) zip_source_free(src);
    }
    return zip_close(za) == 0 ? 0 : 1;  // the archive is written here
}

Tips#