11 Commits

Author SHA1 Message Date
Antoine Aflalo
9ecd5ff3a5 fix(webp): fix the actual maximum limit 2024-08-28 11:53:26 -04:00
Antoine Aflalo
a63d2395f0 fix(webp): better handling of error for page too big for webp 2024-08-28 11:51:06 -04:00
Antoine Aflalo
839ad9ed9d fix(cbz): make pages be the first in the cbz by only be number 2024-08-28 09:16:19 -04:00
Antoine Aflalo
c8879349e1 feat(split): Make the split configurable for the watch command 2024-08-28 09:10:08 -04:00
Antoine Aflalo
5ac59a93c5 feat(split): Make the split configurable for the optimize command 2024-08-28 09:06:49 -04:00
Antoine Aflalo
72c6776793 fix: make the progress more readable 2024-08-27 20:42:26 -04:00
Antoine Aflalo
e0b6d7fcef ci: add bash completion to image 2024-08-27 20:29:38 -04:00
Antoine Aflalo
9305c8fa76 perf: add completion to bash docker image 2024-08-27 20:27:45 -04:00
Antoine Aflalo
9cc45e75cf fix(ci): fix built date 2024-08-27 20:26:29 -04:00
Antoine Aflalo
f451b660be fix: get proper version 2024-08-27 20:22:22 -04:00
Antoine Aflalo
c8fe726a96 fix: quality value validation 2024-08-27 20:18:36 -04:00
14 changed files with 104 additions and 65 deletions

View File

@@ -36,7 +36,7 @@ builds:
# trims path
- -trimpath
ldflags:
- -s -w -X meta.Version={{.Version}} -X meta.Commit={{.Commit}} -X meta.Date={{ .CommitDate }}
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }}
env:
- CGO_ENABLED=0
# config the checksum filename

View File

@@ -15,7 +15,7 @@ RUN mkdir -p "${CONFIG_FOLDER}" && adduser \
COPY CBZOptimizer /usr/local/bin/CBZOptimizer
RUN apk add --no-cache inotify-tools && chmod +x /usr/local/bin/CBZOptimizer
RUN apk add --no-cache inotify-tools bash-completion && chmod +x /usr/local/bin/CBZOptimizer && /usr/local/bin/CBZOptimizer completion bash > /etc/bash_completion.d/CBZOptimizer
USER ${USER}
ENTRYPOINT ["/usr/local/bin/CBZOptimizer"]

View File

@@ -30,10 +30,10 @@ func WriteChapterToCBZ(chapter *manga.Chapter, outputFilePath string) error {
var fileName string
if page.IsSplitted {
// Use the format page%03d-%02d for split pages
fileName = fmt.Sprintf("page_%04d-%02d%s", page.Index, page.SplitPartIndex, page.Extension)
fileName = fmt.Sprintf("%04d-%02d%s", page.Index, page.SplitPartIndex, page.Extension)
} else {
// Use the format page%03d for non-split pages
fileName = fmt.Sprintf("page_%04d%s", page.Index, page.Extension)
fileName = fmt.Sprintf("%04d%s", page.Index, page.Extension)
}
// Create a new file in the ZIP archive

View File

@@ -31,7 +31,7 @@ func TestWriteChapterToCBZ(t *testing.T) {
IsConverted: true,
ConvertedTime: time.Now(),
},
expectedFiles: []string{"page_0000.jpg", "ComicInfo.xml", "Converted.txt"},
expectedFiles: []string{"0000.jpg", "ComicInfo.xml", "Converted.txt"},
},
//test case where there is only one page and no
{
@@ -45,7 +45,7 @@ func TestWriteChapterToCBZ(t *testing.T) {
},
},
},
expectedFiles: []string{"page_0000.jpg"},
expectedFiles: []string{"0000.jpg"},
},
{
name: "Multiple pages with ComicInfo",
@@ -64,7 +64,7 @@ func TestWriteChapterToCBZ(t *testing.T) {
},
ComicInfoXml: "<Series>Boundless Necromancer</Series>",
},
expectedFiles: []string{"page_0000.jpg", "page_0001.jpg", "ComicInfo.xml"},
expectedFiles: []string{"0000.jpg", "0001.jpg", "ComicInfo.xml"},
},
{
name: "Split page",
@@ -79,7 +79,7 @@ func TestWriteChapterToCBZ(t *testing.T) {
},
},
},
expectedFiles: []string{"page_0000-01.jpg"},
expectedFiles: []string{"0000-01.jpg"},
},
}

View File

@@ -29,6 +29,7 @@ func init() {
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")
command.Flags().BoolP("split", "s", false, "Split long pages into smaller chunks")
command.PersistentFlags().VarP(
formatFlag,
"format", "f",
@@ -49,7 +50,7 @@ func ConvertCbzCommand(cmd *cobra.Command, args []string) error {
}
quality, err := cmd.Flags().GetUint8("quality")
if err != nil {
if err != nil || quality <= 0 || quality > 100 {
return fmt.Errorf("invalid quality value")
}
@@ -58,6 +59,11 @@ func ConvertCbzCommand(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid quality value")
}
split, err := cmd.Flags().GetBool("split")
if err != nil {
return fmt.Errorf("invalid split value")
}
parallelism, err := cmd.Flags().GetInt("parallelism")
if err != nil || parallelism < 1 {
return fmt.Errorf("invalid parallelism value")
@@ -86,7 +92,13 @@ func ConvertCbzCommand(cmd *cobra.Command, args []string) error {
go func() {
defer wg.Done()
for path := range fileChan {
err := utils.Optimize(chapterConverter, path, quality, override)
err := utils.Optimize(&utils.OptimizeOptions{
ChapterConverter: chapterConverter,
Path: path,
Quality: quality,
Override: override,
Split: split,
})
if err != nil {
errorChan <- fmt.Errorf("error processing file %s: %w", path, err)
}

View File

@@ -17,17 +17,16 @@ import (
// MockConverter is a mock implementation of the Converter interface
type MockConverter struct{}
func (m *MockConverter) Format() constant.ConversionFormat {
return constant.WebP
}
func (m *MockConverter) ConvertChapter(chapter *manga.Chapter, quality uint8, progress func(string)) (*manga.Chapter, error) {
// Simulate conversion by setting the IsConverted flag
func (m *MockConverter) ConvertChapter(chapter *manga.Chapter, quality uint8, split bool, progress func(message string, current uint32, total uint32)) (*manga.Chapter, error) {
chapter.IsConverted = true
chapter.ConvertedTime = time.Now()
return chapter, nil
}
func (m *MockConverter) Format() constant.ConversionFormat {
return constant.WebP
}
func (m *MockConverter) PrepareConverter() error {
return nil
}
@@ -79,6 +78,7 @@ func TestConvertCbzCommand(t *testing.T) {
cmd.Flags().Uint8P("quality", "q", 85, "Quality for conversion (0-100)")
cmd.Flags().IntP("parallelism", "n", 2, "Number of chapters to convert in parallel")
cmd.Flags().BoolP("override", "o", false, "Override the original CBZ files")
cmd.Flags().BoolP("split", "s", false, "Split long pages into smaller chunks")
// Execute the command
err = ConvertCbzCommand(cmd, []string{tempDir})

View File

@@ -14,6 +14,10 @@ var rootCmd = &cobra.Command{
Short: "Convert CBZ files using a specified converter",
}
func SetVersionInfo(version, commit, date string) {
rootCmd.Version = fmt.Sprintf("%s (Built on %s from Git SHA %s)", version, date, commit)
}
func getPath() string {
return filepath.Join(map[string]string{
"windows": filepath.Join(os.Getenv("APPDATA")),

View File

@@ -1,21 +0,0 @@
package cmd
import (
"fmt"
"github.com/belphemur/CBZOptimizer/meta"
"github.com/spf13/cobra"
)
func init() {
command := &cobra.Command{
Use: "version",
Short: "Print the version of the application",
Long: "Print the version of the application",
Run: VersionCommand,
}
AddCommand(command)
}
func VersionCommand(_ *cobra.Command, _ []string) {
fmt.Printf("CBZOptimizer %s [%s] built [%s]\n", meta.Version, meta.Commit, meta.Date)
}

View File

@@ -35,6 +35,9 @@ func init() {
command.Flags().BoolP("override", "o", true, "Override the original CBZ files")
_ = viper.BindPFlag("override", command.Flags().Lookup("override"))
command.Flags().BoolP("split", "s", false, "Split long pages into smaller chunks")
_ = viper.BindPFlag("split", command.Flags().Lookup("split"))
command.PersistentFlags().VarP(
formatFlag,
"format", "f",
@@ -55,12 +58,14 @@ func WatchCommand(_ *cobra.Command, args []string) error {
}
quality := uint8(viper.GetUint16("quality"))
if quality <= 0 {
if quality <= 0 || quality > 100 {
return fmt.Errorf("invalid quality value")
}
override := viper.GetBool("override")
split := viper.GetBool("split")
converterType := constant.FindConversionFormat(viper.GetString("format"))
chapterConverter, err := converter.Get(converterType)
if err != nil {
@@ -109,7 +114,13 @@ func WatchCommand(_ *cobra.Command, args []string) error {
for _, e := range event.Events {
switch e {
case inotifywaitgo.CLOSE_WRITE, inotifywaitgo.MOVE:
err := utils.Optimize(chapterConverter, event.Filename, quality, override)
err := utils.Optimize(&utils.OptimizeOptions{
ChapterConverter: chapterConverter,
Path: path,
Quality: quality,
Override: override,
Split: split,
})
if err != nil {
errors <- fmt.Errorf("error processing file %s: %w", event.Filename, err)
}

View File

@@ -12,7 +12,7 @@ import (
type Converter interface {
// Format of the converter
Format() (format constant.ConversionFormat)
ConvertChapter(chapter *manga.Chapter, quality uint8, progress func(string)) (*manga.Chapter, error)
ConvertChapter(chapter *manga.Chapter, quality uint8, split bool, progress func(message string, current uint32, total uint32)) (*manga.Chapter, error)
PrepareConverter() error
}

View File

@@ -14,18 +14,27 @@ func TestConvertChapter(t *testing.T) {
testCases := []struct {
name string
genTestChapter func(path string) (*manga.Chapter, error)
split bool
}{
{
name: "All split pages",
genTestChapter: genBigPages,
split: true,
},
{
name: "Big Pages, no split",
genTestChapter: genBigPages,
split: false,
},
{
name: "No split pages",
genTestChapter: genSmallPages,
split: false,
},
{
name: "Mix of split and no split pages",
genTestChapter: genMixSmallBig,
split: true,
},
}
// Load test genTestChapter from testdata
@@ -50,11 +59,11 @@ func TestConvertChapter(t *testing.T) {
quality := uint8(80)
progress := func(msg string) {
progress := func(msg string, current uint32, total uint32) {
t.Log(msg)
}
convertedChapter, err := converter.ConvertChapter(chapter, quality, progress)
convertedChapter, err := converter.ConvertChapter(chapter, quality, false, progress)
if err != nil {
t.Fatalf("failed to convert genTestChapter: %v", err)
}
@@ -83,7 +92,7 @@ func genBigPages(path string) (*manga.Chapter, error) {
var pages []*manga.Page
for i := 0; i < 5; i++ { // Assuming there are 5 pages for the test
img := image.NewRGBA(image.Rect(0, 0, 300, 10000))
img := image.NewRGBA(image.Rect(0, 0, 300, 17000))
buf := new(bytes.Buffer)
err := jpeg.Encode(buf, img, nil)
if err != nil {

View File

@@ -4,7 +4,7 @@ import (
"bytes"
"fmt"
"github.com/belphemur/CBZOptimizer/converter/constant"
packer2 "github.com/belphemur/CBZOptimizer/manga"
"github.com/belphemur/CBZOptimizer/manga"
"github.com/oliamb/cutter"
"golang.org/x/exp/slices"
_ "golang.org/x/image/webp"
@@ -17,6 +17,8 @@ import (
"sync/atomic"
)
const webpMaxHeight = 16383
type Converter struct {
maxHeight int
cropHeight int
@@ -48,7 +50,7 @@ func (converter *Converter) PrepareConverter() error {
return nil
}
func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uint8, progress func(string)) (*packer2.Chapter, error) {
func (converter *Converter) ConvertChapter(chapter *manga.Chapter, quality uint8, split bool, progress func(message string, current uint32, total uint32)) (*manga.Chapter, error) {
err := converter.PrepareConverter()
if err != nil {
return nil, err
@@ -57,7 +59,7 @@ func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uin
var wgConvertedPages sync.WaitGroup
maxGoroutines := runtime.NumCPU()
pagesChan := make(chan *packer2.PageContainer, maxGoroutines)
pagesChan := make(chan *manga.PageContainer, maxGoroutines)
errChan := make(chan error, maxGoroutines)
var wgPages sync.WaitGroup
@@ -65,13 +67,13 @@ func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uin
guard := make(chan struct{}, maxGoroutines)
pagesMutex := sync.Mutex{}
var pages []*packer2.Page
var pages []*manga.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) {
go func(pageToConvert *manga.PageContainer) {
defer wgConvertedPages.Done()
convertedPage, err := converter.convertPage(pageToConvert, quality)
if err != nil {
@@ -93,7 +95,7 @@ func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uin
}
pagesMutex.Lock()
pages = append(pages, convertedPage.Page)
progress(fmt.Sprintf("Converted %d/%d pages to %s format", len(pages), totalPages, converter.Format()))
progress(fmt.Sprintf("Converted %d/%d pages to %s format", len(pages), totalPages, converter.Format()), uint32(len(pages)), totalPages)
pagesMutex.Unlock()
<-guard
}(page)
@@ -101,10 +103,12 @@ func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uin
}()
for _, page := range chapter.Pages {
go func(page *packer2.Page) {
go func(page *manga.Page) {
defer wgPages.Done()
splitNeeded, img, format, err := converter.checkPageNeedsSplit(page)
// Respect choice to split or not
splitNeeded = split && splitNeeded
if err != nil {
errChan <- fmt.Errorf("error checking if page %d of genTestChapter %s needs split: %v", page.Index, chapter.FilePath, err)
return
@@ -112,7 +116,7 @@ func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uin
if !splitNeeded {
wgConvertedPages.Add(1)
pagesChan <- packer2.NewContainer(page, img, format)
pagesChan <- manga.NewContainer(page, img, format)
return
}
images, err := converter.cropImage(img)
@@ -123,9 +127,9 @@ func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uin
atomic.AddUint32(&totalPages, uint32(len(images)-1))
for i, img := range images {
page := &packer2.Page{Index: page.Index, IsSplitted: true, SplitPartIndex: uint16(i)}
page := &manga.Page{Index: page.Index, IsSplitted: true, SplitPartIndex: uint16(i)}
wgConvertedPages.Add(1)
pagesChan <- packer2.NewContainer(page, img, "N/A")
pagesChan <- manga.NewContainer(page, img, "N/A")
}
}(page)
}
@@ -144,7 +148,7 @@ func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uin
return nil, fmt.Errorf("encountered errors: %v", errList)
}
slices.SortFunc(pages, func(a, b *packer2.Page) int {
slices.SortFunc(pages, func(a, b *manga.Page) int {
if a.Index == b.Index {
return int(b.SplitPartIndex - a.SplitPartIndex)
}
@@ -190,7 +194,7 @@ func (converter *Converter) cropImage(img image.Image) ([]image.Image, error) {
return parts, nil
}
func (converter *Converter) checkPageNeedsSplit(page *packer2.Page) (bool, image.Image, string, error) {
func (converter *Converter) checkPageNeedsSplit(page *manga.Page) (bool, image.Image, string, error) {
reader := io.Reader(bytes.NewBuffer(page.Contents.Bytes()))
img, format, err := image.Decode(reader)
if err != nil {
@@ -200,10 +204,13 @@ func (converter *Converter) checkPageNeedsSplit(page *packer2.Page) (bool, image
bounds := img.Bounds()
height := bounds.Dy()
if height >= webpMaxHeight {
return false, img, format, fmt.Errorf("page[%d] height %d exceeds maximum height %d of webp format", page.Index, height, webpMaxHeight)
}
return height >= converter.maxHeight, img, format, nil
}
func (converter *Converter) convertPage(container *packer2.PageContainer, quality uint8) (*packer2.PageContainer, error) {
func (converter *Converter) convertPage(container *manga.PageContainer, quality uint8) (*manga.PageContainer, error) {
if container.Format == "webp" {
return container, nil
}

View File

@@ -4,6 +4,13 @@ import (
"github.com/belphemur/CBZOptimizer/cmd"
)
var (
version = "dev"
commit = "none"
date = "unknown"
)
func main() {
cmd.SetVersionInfo(version, commit, date)
cmd.Execute()
}

View File

@@ -8,24 +8,34 @@ import (
"strings"
)
type OptimizeOptions struct {
ChapterConverter converter.Converter
Path string
Quality uint8
Override bool
Split bool
}
// Optimize optimizes a CBZ file using the specified converter.
func Optimize(chapterConverter converter.Converter, path string, quality uint8, override bool) error {
log.Printf("Processing file: %s\n", path)
func Optimize(options *OptimizeOptions) error {
log.Printf("Processing file: %s\n", options.Path)
// Load the chapter
chapter, err := cbz.LoadChapter(path)
chapter, err := cbz.LoadChapter(options.Path)
if err != nil {
return fmt.Errorf("failed to load chapter: %v", err)
}
if chapter.IsConverted {
log.Printf("Chapter already converted: %s", path)
log.Printf("Chapter already converted: %s", options.Path)
return nil
}
// Convert the chapter
convertedChapter, err := chapterConverter.ConvertChapter(chapter, quality, func(msg string) {
log.Printf("[%s]%s", path, msg)
convertedChapter, err := options.ChapterConverter.ConvertChapter(chapter, options.Quality, options.Split, func(msg string, current uint32, total uint32) {
if current%10 == 0 || current == total {
log.Printf("[%s] Converting: %d/%d", chapter.FilePath, current, total)
}
})
if err != nil {
return fmt.Errorf("failed to convert chapter: %v", err)
@@ -33,9 +43,9 @@ func Optimize(chapterConverter converter.Converter, path string, quality uint8,
convertedChapter.SetConverted()
// Write the converted chapter back to a CBZ file
outputPath := path
if !override {
outputPath = strings.TrimSuffix(path, ".cbz") + "_converted.cbz"
outputPath := options.Path
if !options.Override {
outputPath = strings.TrimSuffix(options.Path, ".cbz") + "_converted.cbz"
}
err = cbz.WriteChapterToCBZ(convertedChapter, outputPath)
if err != nil {