libzip
| Language | C (usable from C++) |
|---|---|
| License | BSD-3-Clause |
| Repository | github.com/nih-at/libzip |
| Documentation | libzip.org/documentation |
| Depends on | zlib; optional bzip2, liblzma, zstd, OpenSSL/GnuTLS for AES |
| Read / write | yes / 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#
# Debian / Ubuntu
sudo apt install libzip-dev
# Fedora
sudo dnf install libzip-devel
# macOS
brew install libzip
# Windows (vcpkg)
vcpkg install libzip
With 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:
#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:
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:
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):
#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.
#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#
zip_closewrites the archive; until then nothing is on disk. Check its return value, and usezip_discardto abandon changes.- Open untrusted files with
ZIP_RDONLY | ZIP_CHECKCONSto make libzip check the archive’s consistency. - Before allocating a page buffer, check
st.sizeagainst a limit: a malicious ZIP can claim a huge uncompressed size. - To update
ComicInfo.xmlin an existing CBZ, open it withoutZIP_RDONLY, add the new file withzip_source_bufferandZIP_FL_OVERWRITE, thenzip_close. libzip copies the unchanged pages without recompressing them.