feat(gif): support gif file

See .gif file extension support and more exception handling
Fixes #105
This commit is contained in:
Antoine Aflalo
2025-09-03 21:04:51 -04:00
parent f6bdc3cd86
commit 6205e3ea28
3 changed files with 34 additions and 19 deletions

View File

@@ -113,8 +113,9 @@ func ConvertCbzCommand(cmd *cobra.Command, args []string) error {
// Channel to manage the files to process // Channel to manage the files to process
fileChan := make(chan string) fileChan := make(chan string)
// Channel to collect errors // Slice to collect errors with mutex for thread safety
errorChan := make(chan error, parallelism) var errs []error
var errMutex sync.Mutex
// WaitGroup to wait for all goroutines to finish // WaitGroup to wait for all goroutines to finish
var wg sync.WaitGroup var wg sync.WaitGroup
@@ -138,7 +139,9 @@ func ConvertCbzCommand(cmd *cobra.Command, args []string) error {
}) })
if err != nil { if err != nil {
log.Error().Int("worker_id", workerID).Str("file_path", path).Err(err).Msg("Worker encountered error") log.Error().Int("worker_id", workerID).Str("file_path", path).Err(err).Msg("Worker encountered error")
errorChan <- fmt.Errorf("error processing file %s: %w", path, err) errMutex.Lock()
errs = append(errs, fmt.Errorf("error processing file %s: %w", path, err))
errMutex.Unlock()
} else { } else {
log.Debug().Int("worker_id", workerID).Str("file_path", path).Msg("Worker completed file successfully") log.Debug().Int("worker_id", workerID).Str("file_path", path).Msg("Worker completed file successfully")
} }
@@ -177,13 +180,6 @@ func ConvertCbzCommand(cmd *cobra.Command, args []string) error {
log.Debug().Msg("File channel closed, waiting for workers to complete") log.Debug().Msg("File channel closed, waiting for workers to complete")
wg.Wait() // Wait for all workers to finish wg.Wait() // Wait for all workers to finish
log.Debug().Msg("All workers completed") log.Debug().Msg("All workers completed")
close(errorChan) // Close the error channel
var errs []error
for err := range errorChan {
errs = append(errs, err)
log.Error().Err(err).Msg("Collected processing error")
}
if len(errs) > 0 { if len(errs) > 0 {
log.Error().Int("error_count", len(errs)).Msg("Command completed with errors") log.Error().Int("error_count", len(errs)).Msg("Command completed with errors")

View File

@@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"image" "image"
_ "image/gif"
_ "image/jpeg" _ "image/jpeg"
"image/png" "image/png"
"runtime" "runtime"
@@ -167,11 +168,16 @@ func (converter *Converter) ConvertChapter(ctx context.Context, chapter *manga.C
splitNeeded, img, format, err := converter.checkPageNeedsSplit(page, split) splitNeeded, img, format, err := converter.checkPageNeedsSplit(page, split)
if err != nil { if err != nil {
var pageIgnoredError *converterrors.PageIgnoredError
if errors.As(err, &pageIgnoredError) {
log.Info().Err(err).Msg("Page ignored due to image decode error")
} else {
select { select {
case errChan <- err: case errChan <- err:
case <-ctx.Done(): case <-ctx.Done():
return return
} }
}
if img != nil { if img != nil {
wgConvertedPages.Add(1) wgConvertedPages.Add(1)
select { select {
@@ -357,7 +363,7 @@ func (converter *Converter) checkPageNeedsSplit(page *manga.Page, splitRequested
img, format, err := image.Decode(reader) img, format, err := image.Decode(reader)
if err != nil { if err != nil {
log.Debug().Uint16("page_index", page.Index).Err(err).Msg("Failed to decode page image") log.Debug().Uint16("page_index", page.Index).Err(err).Msg("Failed to decode page image")
return false, nil, format, err return false, nil, format, converterrors.NewPageIgnored(fmt.Sprintf("page %d: failed to decode image (%s)", page.Index, err.Error()))
} }
bounds := img.Bounds() bounds := img.Bounds()

View File

@@ -5,6 +5,7 @@ import (
"context" "context"
"image" "image"
"image/color" "image/color"
"image/gif"
"image/jpeg" "image/jpeg"
"image/png" "image/png"
"sync" "sync"
@@ -44,6 +45,11 @@ func encodeImage(img image.Image, format string) (*bytes.Buffer, string, error)
return nil, "", err return nil, "", err
} }
return buf, ".jpg", nil return buf, ".jpg", nil
case "gif":
if err := gif.Encode(buf, img, nil); err != nil {
return nil, "", err
}
return buf, ".gif", nil
case "webp": case "webp":
PrepareEncoder() PrepareEncoder()
if err := Encode(buf, img, 80); err != nil { if err := Encode(buf, img, 80); err != nil {
@@ -131,10 +137,11 @@ func TestConverter_ConvertChapter(t *testing.T) {
pages: []*manga.Page{ pages: []*manga.Page{
createTestPage(t, 1, 800, 1200, "png"), createTestPage(t, 1, 800, 1200, "png"),
createTestPage(t, 2, 800, 1200, "jpeg"), createTestPage(t, 2, 800, 1200, "jpeg"),
createTestPage(t, 3, 800, 1200, "gif"),
}, },
split: false, split: false,
expectSplit: false, expectSplit: false,
numExpected: 2, numExpected: 3,
}, },
{ {
name: "Tall image with split enabled", name: "Tall image with split enabled",
@@ -147,7 +154,7 @@ func TestConverter_ConvertChapter(t *testing.T) {
name: "Tall image without split", name: "Tall image without split",
pages: []*manga.Page{createTestPage(t, 1, 800, webpMaxHeight+100, "png")}, pages: []*manga.Page{createTestPage(t, 1, 800, webpMaxHeight+100, "png")},
split: false, split: false,
expectError: true, expectError: false,
numExpected: 1, numExpected: 1,
}, },
} }
@@ -236,6 +243,12 @@ func TestConverter_convertPage(t *testing.T) {
isToBeConverted: true, isToBeConverted: true,
expectWebP: true, expectWebP: true,
}, },
{
name: "Convert GIF to WebP",
format: "gif",
isToBeConverted: true,
expectWebP: true,
},
{ {
name: "Already WebP", name: "Already WebP",
format: "webp", format: "webp",
@@ -333,8 +346,8 @@ func TestConverter_ConvertChapter_Timeout(t *testing.T) {
// Create a test chapter with a few pages // Create a test chapter with a few pages
pages := []*manga.Page{ pages := []*manga.Page{
createTestPage(t, 1, 800, 1200, "jpeg"), createTestPage(t, 1, 800, 1200, "jpeg"),
createTestPage(t, 2, 800, 1200, "jpeg"), createTestPage(t, 2, 800, 1200, "png"),
createTestPage(t, 3, 800, 1200, "jpeg"), createTestPage(t, 3, 800, 1200, "gif"),
} }
chapter := &manga.Chapter{ chapter := &manga.Chapter{