All formats

SharpCompress

LanguageC# / .NET
LicenseMIT
Repositorygithub.com/adamhathcock/sharpcompress
DocumentationUSAGE.md, API.md
Depends onnothing (pure managed code)
Read / writeyes / yes (ZIP; also reads RAR, reads and writes 7z, TAR, GZip)

SharpCompress is a pure C# library for ZIP, RAR, 7z, TAR, GZip, BZip2, XZ and Zstandard. ArchiveFactory.OpenArchive detects the format from the content, so one code path opens CBZ, CBR, CB7 and CBT files, including .cbr files that are really ZIP. It targets .NET Framework 4.8, .NET Standard 2.0 and current .NET.

The examples use the API of SharpCompress 0.50: methods are named OpenArchive, CreateArchive, OpenReader and OpenWriter (older versions and many examples online use Open and Create).

Installation#

Shell
dotnet add package SharpCompress

Basic usage: list pages in natural order#

C#
using SharpCompress.Archives;

using IArchive archive = ArchiveFactory.OpenArchive(args[0]);
List<IArchiveEntry> pages = Comic.Pages(archive);
for (int i = 0; i < pages.Count; i++)
    Console.WriteLine($"{i + 1,3} {pages[i].Key} ({pages[i].Size} 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<IArchiveEntry> Pages(IArchive archive)
    {
        var pages = archive.Entries.Where(e => !e.IsDirectory && IsPage(e.Key ?? "")).ToList();
        pages.Sort((x, y) => NaturalCompare(x.Key ?? "", y.Key ?? ""));
        return pages;
    }
}

How to read a page into memory#

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

How to extract the cover#

C#
using SharpCompress.Archives;

using IArchive archive = ArchiveFactory.OpenArchive("comic.cbz");
IArchiveEntry? cover = Comic.Pages(archive).FirstOrDefault();
if (cover != null)
    cover.WriteToFile(Path.GetFullPath("cover" + Path.GetExtension(cover.Key)));  // needs a full path

How to read ComicInfo.xml#

C#
using System.Xml.Linq;
using SharpCompress.Archives;

using IArchive archive = ArchiveFactory.OpenArchive("comic.cbz");
IArchiveEntry? info = archive.Entries.FirstOrDefault(e =>
    string.Equals(e.Key, "ComicInfo.xml", StringComparison.OrdinalIgnoreCase));
if (info == null) return;

using Stream s = info.OpenEntryStream();
XElement ci = XDocument.Load(s).Root!;
Console.WriteLine($"{ci.Element("Series")?.Value} #{ci.Element("Number")?.Value}");
Console.WriteLine($"Title: {ci.Element("Title")?.Value}, pages: {ci.Element("PageCount")?.Value}");

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

How to create a CBZ from a folder of images#

ZipArchive.CreateArchive collects entries and writes them on SaveTo. Use CompressionType.None to store pages as they are:

C#
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;

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 var zip = ZipArchive.CreateArchive();
    for (int i = 0; i < files.Count; i++)
        zip.AddEntry($"{i + 1:D3}{Path.GetExtension(files[i]).ToLowerInvariant()}", files[i]);
    string info = Path.Combine(dir, "ComicInfo.xml");
    if (File.Exists(info))
        zip.AddEntry("ComicInfo.xml", info);
    zip.SaveTo(output, CompressionType.None);
}

How to convert any comic archive to CBZ#

Because ArchiveFactory reads every format, converting CBR, CB7 or CBT to CBZ is a loop over the entries:

C#
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Writers;
using SharpCompress.Writers.Zip;

using IArchive input = ArchiveFactory.OpenArchive("comic.cbr");
using Stream output = File.Create("comic.cbz");
using IWriter writer = WriterFactory.OpenWriter(output, ArchiveType.Zip, new ZipWriterOptions(CompressionType.None));
foreach (IArchiveEntry e in input.Entries.Where(e => !e.IsDirectory))
{
    using Stream s = e.OpenEntryStream();
    writer.Write(e.Key!, s, e.LastModifiedTime ?? DateTime.Now);
}

For solid RAR and 7z archives (input.IsSolid), iterate with input.ExtractAllEntries(), a reader that decodes entries in order; it throws on non-solid archives.

Tips#