System.IO.Compression
| Language | C# / .NET |
|---|---|
| License | MIT (part of .NET) |
| Repository | github.com/dotnet/runtime |
| Documentation | ZipArchive on Microsoft Learn |
| Depends on | nothing (built into .NET) |
| Read / write | yes / 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#
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#
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:
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:
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:
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.
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#
ZipFile.OpenReaddecodes names as UTF-8 when the ZIP’s UTF-8 flag is set, otherwise with the system code page. For old Japanese archives pass an encoding:ZipFile.Open(path, ZipArchiveMode.Read, Encoding.GetEncoding(932))(registerCodePagesEncodingProviderfirst).ZipArchiveEntry.Open()streams aren’t seekable. Copy to aMemoryStreambefore handing a page to an image decoder that needs to seek.- .NET 10 adds async APIs (
ZipFile.OpenReadAsync,entry.OpenAsync) for server code.