All formats

sevenzip

LanguageGo
LicenseBSD-3-Clause
Repositorygithub.com/bodgit/sevenzip
Documentationpkg.go.dev/github.com/bodgit/sevenzip
Depends onpure Go (klauspost/compress, ulikunitz/xz and others)
Read / writeyes / no (LZMA, LZMA2, Deflate, BZip2, PPMd, Zstd, Brotli, LZ4, Copy, BCJ filters; encrypted, multi-volume)

sevenzip is a pure Go 7z reader modeled on archive/zip: sevenzip.OpenReader(path) returns a reader whose File slice lists every entry, and f.Open() returns the decompressed data. The reader implements fs.FS. It handles compressed and encrypted headers, multi-volume archives and every common 7z method.

7z archives are usually solid: files share one compressed stream. sevenzip caches the stream decoder, so reading files in archive order and closing each one before opening the next continues decoding where the last file ended. Opening files in random order restarts from the beginning of the block each time.

Installation#

Shell
go get github.com/bodgit/sevenzip

Basic usage: list pages in natural order#

isPage and naturalCompare are the helpers from the archive/zip CBZ page: an image-extension filter that skips __MACOSX and dotfiles, and a natural, case-insensitive compare.

Go
package main

import (
	"fmt"
	"log"
	"os"
	"slices"

	"github.com/bodgit/sevenzip"
)

func pages(r *sevenzip.Reader) []*sevenzip.File {
	var res []*sevenzip.File
	for _, f := range r.File {
		if !f.FileInfo().IsDir() && isPage(f.Name) {
			res = append(res, f)
		}
	}
	slices.SortFunc(res, func(a, b *sevenzip.File) int { return naturalCompare(a.Name, b.Name) })
	return res
}

func main() {
	r, err := sevenzip.OpenReader(os.Args[1])
	if err != nil {
		log.Fatal(err)
	}
	defer r.Close()
	for i, f := range pages(&r.Reader) {
		fmt.Printf("%3d %s (%d bytes)\n", i+1, f.Name, f.UncompressedSize)
	}
}

How to read all pages into memory#

Iterate r.File in archive order (not page order) so the decoder is reused, and close each file before the next:

Go
func readFile(f *sevenzip.File) ([]byte, error) {
	rc, err := f.Open()
	if err != nil {
		return nil, err
	}
	defer rc.Close() // must close before opening the next file
	return io.ReadAll(rc)
}

func readPages(path string) (map[string][]byte, error) {
	r, err := sevenzip.OpenReader(path)
	if err != nil {
		return nil, err
	}
	defer r.Close()
	data := map[string][]byte{}
	for _, f := range r.File { // archive order
		if f.FileInfo().IsDir() || !isPage(f.Name) {
			continue
		}
		b, err := readFile(f)
		if err != nil {
			return nil, err
		}
		data[f.Name] = b
	}
	return data, nil
}

How to extract the cover#

Go
r, err := sevenzip.OpenReader("comic.cb7")
if err != nil {
	log.Fatal(err)
}
defer r.Close()
if ps := pages(&r.Reader); len(ps) > 0 {
	b, err := readFile(ps[0])
	if err != nil {
		log.Fatal(err)
	}
	os.WriteFile("cover"+strings.ToLower(path.Ext(ps[0].Name)), b, 0o644)
}

How to read ComicInfo.xml#

Go
type ComicInfo struct {
	Title, Series, Number, Writer, Manga string
	PageCount                            int
	Pages                                []struct {
		Image int    `xml:"Image,attr"` // index in sorted page order
		Type  string `xml:"Type,attr"`
	} `xml:"Pages>Page"`
}

func readComicInfo(r *sevenzip.Reader) (*ComicInfo, error) {
	for _, f := range r.File {
		if strings.EqualFold(f.Name, "ComicInfo.xml") {
			b, err := readFile(f)
			if err != nil {
				return nil, err
			}
			var ci ComicInfo
			return &ci, xml.Unmarshal(b, &ci)
		}
	}
	return nil, nil
}

How to convert CB7 to CBZ#

Go
func cb7ToCbz(in, out string) error {
	r, err := sevenzip.OpenReader(in)
	if err != nil {
		return err
	}
	defer r.Close()
	f, err := os.Create(out)
	if err != nil {
		return err
	}
	defer f.Close()
	zw := zip.NewWriter(f)
	for _, sf := range r.File {
		if sf.FileInfo().IsDir() {
			continue
		}
		b, err := readFile(sf)
		if err != nil {
			return err
		}
		w, err := zw.CreateHeader(&zip.FileHeader{Name: sf.Name, Method: zip.Store, Modified: sf.Modified})
		if err != nil {
			return err
		}
		if _, err := w.Write(b); err != nil {
			return err
		}
	}
	return zw.Close()
}

Tips#