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 / no (RAR4 and RAR5, solid, multi-volume, encrypted; no RAR writing) |
SharpCompress is a pure C# library for ZIP, RAR, 7z, TAR, GZip, BZip2, XZ and Zstandard. Its RAR decoder handles RAR4 and RAR5, solid archives, multi-volume sets and password-protected files. ArchiveFactory.OpenArchive detects the format from the content, so .cbr files that are really ZIP open with the same code, as do CBZ, CB7 and CBT. It targets .NET Framework 4.8, .NET Standard 2.0 and current .NET.
SharpCompress can’t create RAR archives (RAR compression is proprietary). To edit a CBR, read it and write a CBZ, shown below.
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();
}
In a solid RAR, opening one entry means decompressing every entry before it. To read all pages, stream through the archive once with a reader (works for solid and non-solid archives):
using SharpCompress.Readers;
var pages = new Dictionary<string, byte[]>();
using (Stream file = File.OpenRead("comic.cbr"))
using (IReader reader = ReaderFactory.OpenReader(file))
{
while (reader.MoveToNextEntry())
{
if (reader.Entry.IsDirectory || !Comic.IsPage(reader.Entry.Key ?? "")) continue;
using var ms = new MemoryStream();
reader.WriteEntryTo(ms);
pages[reader.Entry.Key!] = ms.ToArray();
}
}
var ordered = pages.Keys.Order(Comparer<string>.Create(Comic.NaturalCompare)).ToList();
archive.IsSolid on a RarArchive tells you whether the archive is solid.
How to extract the cover#
using SharpCompress.Archives;
using IArchive archive = ArchiveFactory.OpenArchive("comic.cbr");
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.cbr");
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 convert CBR to CBZ#
Read each entry and write it to a ZIP with CompressionType.None; the pages are copied without re-encoding:
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Writers;
using SharpCompress.Writers.Zip;
using Stream input = File.OpenRead("comic.cbr");
using IReader reader = ReaderFactory.OpenReader(input); // sequential: fast for solid RAR
using Stream output = File.Create("comic.cbz");
using IWriter writer = WriterFactory.OpenWriter(output, ArchiveType.Zip, new ZipWriterOptions(CompressionType.None));
while (reader.MoveToNextEntry())
{
if (reader.Entry.IsDirectory) continue;
using Stream s = reader.OpenEntryStream();
writer.Write(reader.Entry.Key!, s, reader.Entry.LastModifiedTime ?? DateTime.Now);
}
Tips#
ArchiveFactory.IsArchive(path, out var type)tells you whether a.cbris really RAR or ZIP before opening it.- Password-protected CBR:
RarArchive.OpenArchive(path, ReaderOptions.ForEncryptedArchive("secret")). - Multi-volume sets: pass all parts, e.g.
ArchiveFactory.OpenArchive(ArchiveFactory.GetFileParts("comic.part1.rar").Select(p => new FileInfo(p)).ToArray()). - Entry streams have async versions (
OpenEntryStreamAsync) for server code.