feat: init with converting cbz files

This commit is contained in:
Antoine Aflalo
2024-08-26 22:48:54 -04:00
parent d06653c503
commit 2aeecd363a
20 changed files with 930 additions and 0 deletions

9
.idea/CBZOptimizer.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/CBZOptimizer.iml" filepath="$PROJECT_DIR$/.idea/CBZOptimizer.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

61
cbz/cbz_creator.go Normal file
View File

@@ -0,0 +1,61 @@
package cbz
import (
"CBZOptimizer/packer"
"archive/zip"
"fmt"
"os"
)
func WriteChapterToCBZ(chapter *packer.Chapter, outputFilePath string) error {
// Create a new ZIP file
zipFile, err := os.Create(outputFilePath)
if err != nil {
return fmt.Errorf("failed to create .cbz file: %w", err)
}
defer zipFile.Close()
// Create a new ZIP writer
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()
// Write each page to the ZIP archive
for _, page := range chapter.Pages {
// Construct the file name for the page
var fileName string
if page.IsSplitted {
// Use the format page%03d-%02d for split pages
fileName = fmt.Sprintf("page_%03d-%02d%s", page.Index, page.SplitPartIndex, page.Extension)
} else {
// Use the format page%03d for non-split pages
fileName = fmt.Sprintf("page_%03d%s", page.Index, page.Extension)
}
// Create a new file in the ZIP archive
fileWriter, err := zipWriter.Create(fileName)
if err != nil {
return fmt.Errorf("failed to create file in .cbz: %w", err)
}
// Write the page contents to the file
_, err = fileWriter.Write(page.Contents.Bytes())
if err != nil {
return fmt.Errorf("failed to write page contents: %w", err)
}
}
// Optionally, write the ComicInfo.xml file if present
if chapter.ComicInfoXml != "" {
comicInfoWriter, err := zipWriter.Create("ComicInfo.xml")
if err != nil {
return fmt.Errorf("failed to create ComicInfo.xml in .cbz: %w", err)
}
_, err = comicInfoWriter.Write([]byte(chapter.ComicInfoXml))
if err != nil {
return fmt.Errorf("failed to write ComicInfo.xml contents: %w", err)
}
}
return nil
}

115
cbz/cbz_creator_test.go Normal file
View File

@@ -0,0 +1,115 @@
package cbz
import (
"CBZOptimizer/packer"
"archive/zip"
"bytes"
"os"
"testing"
)
func TestWriteChapterToCBZ(t *testing.T) {
// Define test cases
testCases := []struct {
name string
chapter *packer.Chapter
expectedFiles []string
}{
{
name: "Single page, no ComicInfo",
chapter: &packer.Chapter{
Pages: []*packer.Page{
{
Index: 0,
Extension: ".jpg",
Contents: bytes.NewBuffer([]byte("image data")),
},
},
},
expectedFiles: []string{"page_000.jpg"},
},
{
name: "Multiple pages with ComicInfo",
chapter: &packer.Chapter{
Pages: []*packer.Page{
{
Index: 0,
Extension: ".jpg",
Contents: bytes.NewBuffer([]byte("image data 1")),
},
{
Index: 1,
Extension: ".jpg",
Contents: bytes.NewBuffer([]byte("image data 2")),
},
},
ComicInfoXml: "<Series>Boundless Necromancer</Series>",
},
expectedFiles: []string{"page_000.jpg", "page_001.jpg", "ComicInfo.xml"},
},
{
name: "Split page",
chapter: &packer.Chapter{
Pages: []*packer.Page{
{
Index: 0,
Extension: ".jpg",
Contents: bytes.NewBuffer([]byte("split image data")),
IsSplitted: true,
SplitPartIndex: 1,
},
},
},
expectedFiles: []string{"page_000-01.jpg"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a temporary file for the .cbz output
tempFile, err := os.CreateTemp("", "*.cbz")
if err != nil {
t.Fatalf("Failed to create temporary file: %v", err)
}
defer os.Remove(tempFile.Name())
// Write the chapter to the .cbz file
err = WriteChapterToCBZ(tc.chapter, tempFile.Name())
if err != nil {
t.Fatalf("Failed to write chapter to CBZ: %v", err)
}
// Open the .cbz file as a zip archive
r, err := zip.OpenReader(tempFile.Name())
if err != nil {
t.Fatalf("Failed to open CBZ file: %v", err)
}
defer r.Close()
// Collect the names of the files in the archive
var filesInArchive []string
for _, f := range r.File {
filesInArchive = append(filesInArchive, f.Name)
}
// Check if all expected files are present
for _, expectedFile := range tc.expectedFiles {
found := false
for _, actualFile := range filesInArchive {
if actualFile == expectedFile {
found = true
break
}
}
if !found {
t.Errorf("Expected file %s not found in archive", expectedFile)
}
}
// Check if there are no unexpected files
if len(filesInArchive) != len(tc.expectedFiles) {
t.Errorf("Expected %d files, but found %d", len(tc.expectedFiles), len(filesInArchive))
}
})
}
}

71
cbz/cbz_loader.go Normal file
View File

@@ -0,0 +1,71 @@
package cbz
import (
"CBZOptimizer/packer"
"archive/zip"
"bytes"
"fmt"
"io"
"io/ioutil"
"path/filepath"
"strings"
)
func LoadChapter(filePath string) (*packer.Chapter, error) {
// Open the .cbz file
r, err := zip.OpenReader(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open .cbz file: %w", err)
}
defer r.Close()
chapter := &packer.Chapter{
FilePath: filePath,
}
for _, f := range r.File {
if !f.FileInfo().IsDir() {
// Open the file inside the zip
rc, err := f.Open()
if err != nil {
return nil, fmt.Errorf("failed to open file inside .cbz: %w", err)
}
// Determine the file extension
ext := strings.ToLower(filepath.Ext(f.Name))
if ext == ".xml" && strings.ToLower(filepath.Base(f.Name)) == "comicinfo.xml" {
// Read the ComicInfo.xml file content
xmlContent, err := ioutil.ReadAll(rc)
if err != nil {
rc.Close()
return nil, fmt.Errorf("failed to read ComicInfo.xml content: %w", err)
}
chapter.ComicInfoXml = string(xmlContent)
} else {
// Read the file contents for page
buf := new(bytes.Buffer)
_, err = io.Copy(buf, rc)
if err != nil {
rc.Close()
return nil, fmt.Errorf("failed to read file contents: %w", err)
}
// Create a new Page object
page := &packer.Page{
Index: uint16(len(chapter.Pages)), // Simple index based on order
Extension: ext,
Size: uint64(buf.Len()),
Contents: buf,
IsSplitted: false,
}
// Add the page to the chapter
chapter.Pages = append(chapter.Pages, page)
}
rc.Close()
}
}
return chapter, nil
}

30
cbz/cbz_loader_test.go Normal file
View File

@@ -0,0 +1,30 @@
package cbz
import (
"strings"
"testing"
)
func TestLoadChapter(t *testing.T) {
// Define the path to the .cbz file
chapterFilePath := "../testdata/Chapter 1.cbz"
// Load the chapter
chapter, err := LoadChapter(chapterFilePath)
if err != nil {
t.Fatalf("Failed to load chapter: %v", err)
}
// Check the number of pages
expectedPages := 16
actualPages := len(chapter.Pages)
if actualPages != expectedPages {
t.Errorf("Expected %d pages, but got %d", expectedPages, actualPages)
}
// Check if ComicInfoXml contains the expected series name
expectedSeries := "<Series>Boundless Necromancer</Series>"
if !strings.Contains(chapter.ComicInfoXml, expectedSeries) {
t.Errorf("ComicInfoXml does not contain the expected series: %s", expectedSeries)
}
}

120
cmd/convert_cbz_command.go Normal file
View File

@@ -0,0 +1,120 @@
package cmd
import (
"CBZOptimizer/cbz"
"CBZOptimizer/converter"
"CBZOptimizer/converter/constant"
"fmt"
"github.com/spf13/cobra"
"os"
"path/filepath"
"strings"
"sync"
)
func init() {
command := &cobra.Command{
Use: "convert",
Short: "Convert CBZ files using a specified converter",
RunE: ConvertCbzCommand,
Args: cobra.ExactArgs(1),
}
command.Flags().Uint8P("quality", "q", 85, "Quality for conversion (0-100)")
command.Flags().IntP("parallelism", "n", 2, "Number of chapters to convert in parallel")
command.Flags().BoolP("override", "o", false, "Override the original CBZ files")
AddCommand(command)
}
func ConvertCbzCommand(cmd *cobra.Command, args []string) error {
path := args[0]
if path == "" {
return fmt.Errorf("path is required")
}
quality, err := cmd.Flags().GetUint8("quality")
if err != nil {
return fmt.Errorf("invalid quality value")
}
override, err := cmd.Flags().GetBool("override")
if err != nil {
return fmt.Errorf("invalid quality value")
}
parallelism, err := cmd.Flags().GetInt("parallelism")
if err != nil || parallelism < 1 {
return fmt.Errorf("invalid parallelism value")
}
chapterConverter, err := converter.Get(constant.ImageFormatWebP)
if err != nil {
return fmt.Errorf("failed to get chapterConverter: %v", err)
}
// Channel to manage the files to process
fileChan := make(chan string)
// WaitGroup to wait for all goroutines to finish
var wg sync.WaitGroup
// Start worker goroutines
for i := 0; i < parallelism; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for path := range fileChan {
fmt.Printf("Processing file: %s\n", path)
// Load the chapter
chapter, err := cbz.LoadChapter(path)
if err != nil {
fmt.Printf("Failed to load chapter: %v\n", err)
continue
}
// Convert the chapter
convertedChapter, err := chapterConverter.ConvertChapter(chapter, quality, func(msg string) {
fmt.Println(msg)
})
if err != nil {
fmt.Printf("Failed to convert chapter: %v\n", err)
continue
}
// Write the converted chapter back to a CBZ file
outputPath := path
if !override {
outputPath = strings.TrimSuffix(path, ".cbz") + "_converted.cbz"
}
err = cbz.WriteChapterToCBZ(convertedChapter, outputPath)
if err != nil {
fmt.Printf("Failed to write converted chapter: %v\n", err)
continue
}
fmt.Printf("Converted file written to: %s\n", outputPath)
}
}()
}
// Walk the path and send files to the channel
err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".cbz") {
fileChan <- path
}
return nil
})
if err != nil {
return fmt.Errorf("error walking the path: %w", err)
}
close(fileChan) // Close the channel to signal workers to stop
wg.Wait() // Wait for all workers to finish
return nil
}

23
cmd/rootcmd.go Normal file
View File

@@ -0,0 +1,23 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"os"
)
var rootCmd = &cobra.Command{
Use: "cbzconverter",
Short: "Convert CBZ files using a specified converter",
}
// Execute executes the root command.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func AddCommand(cmd *cobra.Command) {
rootCmd.AddCommand(cmd)
}

View File

@@ -0,0 +1,8 @@
package constant
type ConversionFormat string
const (
ImageFormatWebP ConversionFormat = "webp"
ImageFormatUnknown ConversionFormat = ""
)

37
converter/converter.go Normal file
View File

@@ -0,0 +1,37 @@
package converter
import (
"CBZOptimizer/converter/constant"
"CBZOptimizer/converter/webp"
"CBZOptimizer/packer"
"fmt"
"github.com/samber/lo"
"strings"
)
type Converter interface {
// Format of the converter
Format() (format constant.ConversionFormat)
ConvertChapter(chapter *packer.Chapter, quality uint8, progress func(string)) (*packer.Chapter, error)
}
var converters = map[constant.ConversionFormat]Converter{
constant.ImageFormatWebP: webp.New(),
}
// Available returns a list of available converters.
func Available() []constant.ConversionFormat {
return lo.Keys(converters)
}
// Get returns a packer by name.
// If the packer is not available, an error is returned.
func Get(name constant.ConversionFormat) (Converter, error) {
if packer, ok := converters[name]; ok {
return packer, nil
}
return nil, fmt.Errorf("unkown converter \"%s\", available options are %s", name, strings.Join(lo.Map(Available(), func(item constant.ConversionFormat, index int) string {
return string(item)
}), ", "))
}

View File

@@ -0,0 +1,203 @@
package webp
import (
"CBZOptimizer/converter/constant"
packer2 "CBZOptimizer/packer"
"bytes"
"fmt"
"github.com/oliamb/cutter"
"golang.org/x/exp/slices"
_ "golang.org/x/image/webp"
"image"
_ "image/jpeg"
"image/png"
"io"
"log"
"runtime"
"sync"
"sync/atomic"
)
type Converter struct {
maxHeight int
cropHeight int
}
func (converter *Converter) Format() (format constant.ConversionFormat) {
return constant.ImageFormatWebP
}
func New() *Converter {
return &Converter{
//maxHeight: 16383 / 2,
maxHeight: 4000,
cropHeight: 2000,
}
}
func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uint8, progress func(string)) (*packer2.Chapter, error) {
err := PrepareEncoder()
if err != nil {
return nil, err
}
var wgConvertedPages sync.WaitGroup
maxGoroutines := runtime.NumCPU()
pagesChan := make(chan *packer2.PageContainer, maxGoroutines)
var wgPages sync.WaitGroup
wgPages.Add(len(chapter.Pages))
guard := make(chan struct{}, maxGoroutines)
pagesMutex := sync.Mutex{}
var pages []*packer2.Page
var totalPages = uint32(len(chapter.Pages))
go func() {
for page := range pagesChan {
guard <- struct{}{} // would block if guard channel is already filled
go func(pageToConvert *packer2.PageContainer) {
defer wgConvertedPages.Done()
convertedPage, err := converter.convertPage(pageToConvert, quality)
if err != nil {
buffer := new(bytes.Buffer)
err := png.Encode(buffer, convertedPage.Image)
if err != nil {
<-guard
return
}
convertedPage.Page.Contents = buffer
convertedPage.Page.Extension = ".png"
convertedPage.Page.Size = uint64(buffer.Len())
}
pagesMutex.Lock()
pages = append(pages, convertedPage.Page)
progress(fmt.Sprintf("Converted %d/%d pages to %s format", len(pages), totalPages, converter.Format()))
pagesMutex.Unlock()
<-guard
}(page)
}
}()
for _, page := range chapter.Pages {
go func(page *packer2.Page) {
defer wgPages.Done()
splitNeeded, img, format, err := converter.checkPageNeedsSplit(page)
if err != nil {
log.Fatalf("error checking if page %d d of chapter %s needs split: %v", page.Index, chapter.FilePath, err)
return
}
if !splitNeeded {
wgConvertedPages.Add(1)
pagesChan <- packer2.NewContainer(page, img, format)
return
}
images, err := converter.cropImage(img)
if err != nil {
log.Fatalf("error converting page %d of chapter %s to webp: %v", page.Index, chapter.FilePath, err)
return
}
atomic.AddUint32(&totalPages, uint32(len(images)-1))
for i, img := range images {
page := &packer2.Page{Index: page.Index, IsSplitted: true, SplitPartIndex: uint16(i)}
wgConvertedPages.Add(1)
pagesChan <- packer2.NewContainer(page, img, "N/A")
}
}(page)
}
wgPages.Wait()
wgConvertedPages.Wait()
close(pagesChan)
slices.SortFunc(pages, func(a, b *packer2.Page) int {
if a.Index == b.Index {
return int(b.SplitPartIndex - a.SplitPartIndex)
}
return int(b.Index - a.Index)
})
chapter.Pages = pages
runtime.GC()
return chapter, nil
}
func (converter *Converter) cropImage(img image.Image) ([]image.Image, error) {
bounds := img.Bounds()
height := bounds.Dy()
numParts := height / converter.cropHeight
if height%converter.cropHeight != 0 {
numParts++
}
parts := make([]image.Image, numParts)
for i := 0; i < numParts; i++ {
partHeight := converter.cropHeight
if i == numParts-1 {
partHeight = height - i*converter.cropHeight
}
part, err := cutter.Crop(img, cutter.Config{
Width: bounds.Dx(),
Height: partHeight,
Anchor: image.Point{Y: i * converter.cropHeight},
Mode: cutter.TopLeft,
})
if err != nil {
return nil, fmt.Errorf("error cropping part %d: %v", i+1, err)
}
parts[i] = part
}
return parts, nil
}
func (converter *Converter) checkPageNeedsSplit(page *packer2.Page) (bool, image.Image, string, error) {
reader := io.Reader(bytes.NewBuffer(page.Contents.Bytes()))
img, format, err := image.Decode(reader)
if err != nil {
return false, nil, format, err
}
bounds := img.Bounds()
height := bounds.Dy()
return height >= converter.maxHeight, img, format, nil
}
func (converter *Converter) convertPage(container *packer2.PageContainer, quality uint8) (*packer2.PageContainer, error) {
if container.Format == "webp" {
return container, nil
}
converted, err := converter.convert(container.Image, uint(quality))
if err != nil {
return nil, err
}
container.Page.Contents = converted
container.Page.Extension = ".webp"
container.Page.Size = uint64(converted.Len())
return container, nil
}
// convert converts an image to the ImageFormatWebP format. It decodes the image from the input buffer,
// encodes it as a ImageFormatWebP file using the webp.Encode() function, and returns the resulting ImageFormatWebP
// file as a bytes.Buffer.
func (converter *Converter) convert(image image.Image, quality uint) (*bytes.Buffer, error) {
var buf bytes.Buffer
err := Encode(&buf, image, quality)
if err != nil {
return nil, err
}
return &buf, nil
}

View File

@@ -0,0 +1,83 @@
package webp
import (
"github.com/nickalie/go-binwrapper"
"github.com/nickalie/go-webpbin"
"image"
"io"
"os"
"path/filepath"
"runtime"
)
const libwebpVersion = "1.4.0"
// NewCWebP creates new CWebP instance.
func newCWebP(folder string) *webpbin.CWebP {
bin := &webpbin.CWebP{
BinWrapper: createBinWrapper(folder),
}
bin.ExecPath("cwebp")
return bin
}
func PrepareEncoder() error {
container := newCWebP(DefaultWebPDir)
return container.BinWrapper.Run()
}
// DefaultWebPDir for downloaded browser. For unix is "$HOME/.cache/webp/bin",
// for Windows it's "%APPDATA%\webp\bin"
var DefaultWebPDir = filepath.Join(map[string]string{
"windows": filepath.Join(os.Getenv("APPDATA")),
"darwin": filepath.Join(os.Getenv("HOME"), ".cache"),
"linux": filepath.Join(os.Getenv("HOME"), ".cache"),
}[runtime.GOOS], "webp", libwebpVersion, "bin")
func Encode(w io.Writer, m image.Image, quality uint) error {
return newCWebP(DefaultWebPDir).
Quality(quality).
InputImage(m).
Output(w).
Run()
}
func createBinWrapper(dest string) *binwrapper.BinWrapper {
base := "https://storage.googleapis.com/downloads.webmproject.org/releases/webp/"
b := binwrapper.NewBinWrapper().AutoExe()
b.Src(
binwrapper.NewSrc().
URL(base + "libwebp-" + libwebpVersion + "-mac-arm64.tar.gz").
Os("darwin").
Arch("arm64")).
Src(
binwrapper.NewSrc().
URL(base + "libwebp-" + libwebpVersion + "-mac-x86-64.tar.gz").
Os("darwin").
Arch("x64")).
Src(
binwrapper.NewSrc().
URL(base + "libwebp-" + libwebpVersion + "-linux-x86-32.tar.gz").
Os("linux").
Arch("x86")).
Src(
binwrapper.NewSrc().
URL(base + "libwebp-" + libwebpVersion + "-linux-x86-64.tar.gz").
Os("linux").
Arch("x64")).
Src(
binwrapper.NewSrc().
URL(base + "libwebp-" + libwebpVersion + "-windows-x64.zip").
Os("win32").
Arch("x64")).
Src(
binwrapper.NewSrc().
URL(base + "libwebp-" + libwebpVersion + "-windows-x86.zip").
Os("win32").
Arch("x86"))
return b.Strip(2).Dest(dest)
}

30
go.mod Normal file
View File

@@ -0,0 +1,30 @@
module CBZOptimizer
go 1.23
require (
github.com/nickalie/go-binwrapper v0.0.0-20190114141239-525121d43c84
github.com/nickalie/go-webpbin v0.0.0-20220110095747-f10016bf2dc1
github.com/oliamb/cutter v0.2.2
github.com/samber/lo v1.47.0
github.com/spf13/cobra v1.8.1
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
golang.org/x/image v0.19.0
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/frankban/quicktest v1.14.6 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mholt/archiver v3.1.1+incompatible // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
golang.org/x/text v0.17.0 // indirect
)

72
go.sum Normal file
View File

@@ -0,0 +1,72 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
github.com/nickalie/go-binwrapper v0.0.0-20190114141239-525121d43c84 h1:/6MoQlTdk1eAi0J9O89ypO8umkp+H7mpnSF2ggSL62Q=
github.com/nickalie/go-binwrapper v0.0.0-20190114141239-525121d43c84/go.mod h1:Eeech2fhQ/E4bS8cdc3+SGABQ+weQYGyWBvZ/mNr5uY=
github.com/nickalie/go-webpbin v0.0.0-20220110095747-f10016bf2dc1 h1:9awJsNP+gYOGCr3pQu9i217bCNsVwoQCmD3h7CYwxOw=
github.com/nickalie/go-webpbin v0.0.0-20220110095747-f10016bf2dc1/go.mod h1:m5oz0fmp+uyRBxxFkvciIpe1wd2JZ3pDVJ3x/D8/EGw=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/oliamb/cutter v0.2.2 h1:Lfwkya0HHNU1YLnGv2hTkzHfasrSMkgv4Dn+5rmlk3k=
github.com/oliamb/cutter v0.2.2/go.mod h1:4BenG2/4GuRBDbVm/OPahDVqbrOemzpPiG5mi1iryBU=
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

9
main.go Normal file
View File

@@ -0,0 +1,9 @@
package main
import (
"CBZOptimizer/cmd"
)
func main() {
cmd.Execute()
}

10
packer/chapter.go Normal file
View File

@@ -0,0 +1,10 @@
package packer
type Chapter struct {
// FilePath is the path to the chapter's directory.
FilePath string
// Pages is a slice of pointers to Page objects.
Pages []*Page
// ComicInfo is a string containing information about the chapter.
ComicInfoXml string
}

18
packer/page.go Normal file
View File

@@ -0,0 +1,18 @@
package packer
import "bytes"
type Page struct {
// Index of the page in the chapter.
Index uint16 `json:"index" jsonschema:"description=Index of the page in the chapter."`
// Extension of the page image.
Extension string `json:"extension" jsonschema:"description=Extension of the page image."`
// Size of the page in bytes
Size uint64 `json:"-"`
// Contents of the page
Contents *bytes.Buffer `json:"-"`
// IsSplitted tell us if the page was cropped to multiple pieces
IsSplitted bool `json:"is_cropped" jsonschema:"description=Was this page cropped."`
// SplitPartIndex represent the index of the crop if the page was cropped
SplitPartIndex uint16 `json:"crop_part_index" jsonschema:"description=Index of the crop if the image was cropped."`
}

17
packer/page_container.go Normal file
View File

@@ -0,0 +1,17 @@
package packer
import "image"
// PageContainer is a struct that holds a manga page, its image, and the image format.
type PageContainer struct {
// Page is a pointer to a manga page object.
Page *Page
// Image is the decoded image of the manga page.
Image image.Image
// Format is a string representing the format of the image (e.g., "png", "jpeg", "webp").
Format string
}
func NewContainer(Page *Page, img image.Image, format string) *PageContainer {
return &PageContainer{Page: Page, Image: img, Format: format}
}

BIN
testdata/Chapter 1.cbz vendored Normal file

Binary file not shown.