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 (TAR, ustar and GNU long names; also ZIP and 7z read/write, RAR read) |
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. If you only need TAR on .NET 7+, the built-in System.Formats.Tar has no dependency.
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.cbt");
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.cbt");
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 CBT from a folder of images#
WriterFactory.OpenWriter with ArchiveType.Tar and WriterOptions.ForTar() (no compression) writes a plain tar:
C#
using SharpCompress.Common;
using SharpCompress.Writers;
static void MakeCbt(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.Tar, WriterOptions.ForTar());
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);
}
}
How to convert CBT to CBZ#
ReaderFactory.OpenReader streams through the tar once; each entry goes straight into the ZIP:
C#
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Writers;
using SharpCompress.Writers.Zip;
using Stream input = File.OpenRead("comic.cbt");
using IReader reader = ReaderFactory.OpenReader(input);
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#
ReaderFactory.OpenReader(stream)reads a tar from a non-seekable stream (a network download, a pipe) in one pass.ArchiveFactory.IsArchive(path, out var type)tells you whether a.cbtis really TAR before opening it.- Entry streams have async versions (
OpenEntryStreamAsync) for server code.