SharpCompress
| Language | C# / .NET |
|---|---|
| License | MIT |
| Repository | github.com/adamhathcock/sharpcompress |
| Documentation | USAGE.md, API.md |
| Depends on | nothing (pure managed code) |
| Read / write | yes / 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#
dotnet add package SharpCompress
Basic usage: list pages in natural order#
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#
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#
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#
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:
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:
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#
- Entry streams have async versions (
OpenEntryStreamAsync) for server code. ArchiveFactory.IsArchive(path, out var type)tells you whether a.cbris really RAR or ZIP before opening it.- For ZIP files with names in a legacy code page, pass
new ReaderOptions { ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) } }toArchiveFactory.OpenArchive.