All formats

System.IO.Compression

LanguageC# / .NET
LicenseMIT (part of .NET)
Repositorygithub.com/dotnet/runtime
DocumentationZipArchive on Microsoft Learn
Depends onnothing (built into .NET)
Read / writeyes / yes (ZIP, ZIP64; no encryption)

System.IO.Compression is the ZIP implementation built into .NET. ZipFile.OpenRead reads the central directory, so archive.Entries lists every file at once and entry.Open() decompresses one entry on demand: exactly what a comic reader needs to show one page at a time. ZipFile.Open with ZipArchiveMode.Create writes new archives.

It handles only ZIP and has no encryption support. For CBR, CB7 and CBT use SharpCompress.

Installation#

Nothing to install on .NET Core 3.0+ / .NET 5+. ZipFile is in System.IO.Compression; on .NET Framework, reference System.IO.Compression.FileSystem.

Basic usage: list pages in natural order#

C#
using System.IO.Compression;

using ZipArchive zip = ZipFile.OpenRead(args[0]);
List<ZipArchiveEntry> pages = Comic.Pages(zip);
for (int i = 0; i < pages.Count; i++)
    Console.WriteLine($"{i + 1,3} {pages[i].FullName} ({pages[i].Length} bytes)");

static class Comic
{
    static readonly HashSet<string> ImageExts = new(StringComparer.OrdinalIgnoreCase)
        { ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".avif", ".jxl" };

    public static bool IsPage(string path)
    {
        path = path.Replace('\\', '/');
        if (path.Contains("__MACOSX/")) return false;
        string name = Path.GetFileName(path);
        return name.Length > 0 && name[0] != '.' && ImageExts.Contains(Path.GetExtension(name));
    }

    // natural, case-insensitive order: "2.jpg" < "10.jpg"
    public static int NaturalCompare(string a, string b)
    {
        int i = 0, j = 0;
        while (i < a.Length && j < b.Length)
        {
            if (char.IsAsciiDigit(a[i]) && char.IsAsciiDigit(b[j]))
            {
                int ie = i, je = j;
                while (ie < a.Length && char.IsAsciiDigit(a[ie])) ie++;
                while (je < b.Length && char.IsAsciiDigit(b[je])) je++;
                string na = a[i..ie].TrimStart('0'), nb = b[j..je].TrimStart('0');
                if (na.Length != nb.Length) return na.Length - nb.Length;
                int c = string.CompareOrdinal(na, nb);
                if (c != 0) return c;
                i = ie; j = je;
            }
            else
            {
                int c = char.ToLowerInvariant(a[i]).CompareTo(char.ToLowerInvariant(b[j]));
                if (c != 0) return c;
                i++; j++;
            }
        }
        return (a.Length - i) - (b.Length - j);
    }

    public static List<ZipArchiveEntry> Pages(ZipArchive zip)
    {
        var pages = zip.Entries.Where(e => IsPage(e.FullName)).ToList();
        pages.Sort((x, y) => NaturalCompare(x.FullName, y.FullName));
        return pages;
    }
}

How to read a page into memory#

C#
static byte[] ReadEntry(ZipArchiveEntry entry)
{
    using Stream s = entry.Open();
    using var ms = new MemoryStream((int)entry.Length);
    s.CopyTo(ms);
    return ms.ToArray();
}

entry.Length is the uncompressed size from the central directory. Check it against a limit before allocating when the file is untrusted.

How to extract the cover#

The cover is the first page in natural order:

C#
using ZipArchive zip = ZipFile.OpenRead("comic.cbz");
ZipArchiveEntry? cover = Comic.Pages(zip).FirstOrDefault();
if (cover != null)
    cover.ExtractToFile("cover" + Path.GetExtension(cover.Name), overwrite: true);

How to read ComicInfo.xml#

GetEntry is case-sensitive, so search the entries case-insensitively and parse with XDocument:

C#
using System.IO.Compression;
using System.Xml.Linq;

using ZipArchive zip = ZipFile.OpenRead("comic.cbz");
ZipArchiveEntry? info = zip.Entries.FirstOrDefault(e =>
    e.FullName.Equals("ComicInfo.xml", StringComparison.OrdinalIgnoreCase));
if (info == null) { Console.WriteLine("no ComicInfo.xml"); return; }

using Stream s = info.Open();
XElement ci = XDocument.Load(s).Root!;
string? Get(string name) => ci.Element(name)?.Value;

Console.WriteLine($"{Get("Series")} #{Get("Number")}: {Get("Title")}");
Console.WriteLine($"Writer: {Get("Writer")}, pages: {Get("PageCount")}");
bool rightToLeft = Get("Manga") == "YesAndRightToLeft";

// <Pages><Page Image="0" Type="FrontCover"/>: Image is the index in sorted page order
XElement? coverPage = ci.Element("Pages")?.Elements("Page")
    .FirstOrDefault(p => ((string?)p.Attribute("Type") ?? "").Contains("FrontCover"));
if (coverPage != null)
{
    int index = (int)coverPage.Attribute("Image")!;
    Console.WriteLine($"cover: {Comic.Pages(zip)[index].FullName}");
}

How to create a CBZ from a folder of images#

Store pages without compression (they’re already JPEG/PNG), name them with zero-padded numbers and add ComicInfo.xml if present:

C#
using System.IO.Compression;

static void MakeCbz(string dir, string output)
{
    var files = Directory.GetFiles(dir).Where(Comic.IsPage)
        .OrderBy(f => Path.GetFileName(f), Comparer<string>.Create(Comic.NaturalCompare))
        .ToList();

    using ZipArchive zip = ZipFile.Open(output, ZipArchiveMode.Create);
    for (int i = 0; i < files.Count; i++)
    {
        string name = $"{i + 1:D3}{Path.GetExtension(files[i]).ToLowerInvariant()}";
        zip.CreateEntryFromFile(files[i], name, CompressionLevel.NoCompression);
    }
    string info = Path.Combine(dir, "ComicInfo.xml");
    if (File.Exists(info))
        zip.CreateEntryFromFile(info, "ComicInfo.xml", CompressionLevel.Optimal);
}

How to add or replace ComicInfo.xml in an existing CBZ#

Open the archive in Update mode. .NET rewrites the archive when it’s disposed, so this loads the whole file into memory: fine for comics of a few hundred MB.

C#
using System.IO.Compression;

using ZipArchive zip = ZipFile.Open("comic.cbz", ZipArchiveMode.Update);
zip.Entries.FirstOrDefault(e =>
    e.FullName.Equals("ComicInfo.xml", StringComparison.OrdinalIgnoreCase))?.Delete();
ZipArchiveEntry entry = zip.CreateEntry("ComicInfo.xml");
using (var w = new StreamWriter(entry.Open()))
    w.Write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ComicInfo><Series>Batman</Series><Number>1</Number></ComicInfo>");

Tips#