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 (7z with LZMA, LZMA2, BZip2, PPMd, Deflate, encrypted; writes LZMA/LZMA2 7z) |
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 the same code opens CBZ, CBR, CB7 and CBT. It targets .NET Framework 4.8, .NET Standard 2.0 and current .NET.
7z needs the archive API (SevenZipArchive / ArchiveFactory) for reading; the forward-only ReaderFactory doesn’t read 7z. Writing goes through WriterFactory.OpenWriter with SevenZipWriterOptions.
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 7z (the 7-Zip default), opening one entry decodes the block from its start. To read all pages, use ExtractAllEntries, which decodes entries in archive order in one pass:
using SharpCompress.Archives;
using SharpCompress.Archives.SevenZip;
using var archive = SevenZipArchive.OpenArchive("comic.cb7");
Console.WriteLine($"solid: {archive.IsSolid}");
var pages = new Dictionary<string, byte[]>();
using (var reader = archive.ExtractAllEntries())
{
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();
ExtractAllEntries works on 7z and solid RAR archives and throws for other formats.
How to extract the cover#
using SharpCompress.Archives;
using IArchive archive = ArchiveFactory.OpenArchive("comic.cb7");
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.cb7");
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 CB7 from a folder of images#
using SharpCompress.Common;
using SharpCompress.Writers;
using SharpCompress.Writers.SevenZip;
static void MakeCb7(string dir, string output)
{
var files = Directory.GetFiles(dir).Where(Comic.IsPage)
.OrderBy(f => Path.GetFileName(f), Comparer<string>.Create(Comic.NaturalCompare))
.ToList();
string info = Path.Combine(dir, "ComicInfo.xml");
using Stream stream = File.Create(output);
using IWriter writer = WriterFactory.OpenWriter(stream, ArchiveType.SevenZip,
new SevenZipWriterOptions(CompressionType.LZMA2));
for (int i = 0; i < files.Count; i++)
{
using Stream src = File.OpenRead(files[i]);
writer.Write($"{i + 1:D3}{Path.GetExtension(files[i]).ToLowerInvariant()}", src, DateTime.Now);
}
if (File.Exists(info))
{
using Stream src = File.OpenRead(info);
writer.Write("ComicInfo.xml", src, DateTime.Now);
}
}
The writer produces a non-solid archive (each file compressed on its own), so readers can open any page directly.
How to convert CB7 to CBZ#
using SharpCompress.Archives;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Common;
using SharpCompress.Writers;
using SharpCompress.Writers.Zip;
using var input = SevenZipArchive.OpenArchive("comic.cb7");
using Stream output = File.Create("comic.cbz");
using IWriter writer = WriterFactory.OpenWriter(output, ArchiveType.Zip, new ZipWriterOptions(CompressionType.None));
using var reader = input.ExtractAllEntries(); // one pass through the solid block
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.cb7is really 7z before opening it.- Encrypted 7z:
SevenZipArchive.OpenArchive(path, ReaderOptions.ForEncryptedArchive("secret")). - Entry streams have async versions (
OpenEntryStreamAsync) for server code.