11 Commits

Author SHA1 Message Date
Antoine Aflalo
7c63ea49c0 fix(docker): fix docker image config folder 2024-08-28 14:36:14 -04:00
Antoine Aflalo
8a067939af Merge pull request #4 from Belphemur/renovate/major-github-artifact-actions
chore(deps): update actions/upload-artifact action to v4
2024-08-28 14:24:57 -04:00
Antoine Aflalo
f89974ac79 ci: Another attempt at reducing 2024-08-28 14:22:25 -04:00
Antoine Aflalo
ce365a6bdf ci: reduce size of page to pass tests
Fix failing test
2024-08-28 14:16:51 -04:00
renovate[bot]
9e61ff4634 chore(deps): update actions/upload-artifact action to v4 2024-08-28 17:56:03 +00:00
Antoine Aflalo
63a1b592c3 ci: add test result to pipeline 2024-08-28 13:55:33 -04:00
Antoine Aflalo
673484692b perf(webp): improve the error message for page too tall 2024-08-28 13:52:27 -04:00
Antoine Aflalo
ad35e2655f feat(webp): add partial success to conversion
So we only keep images that couldn't be optimized and return the chapter
2024-08-28 13:49:14 -04:00
Antoine Aflalo
d7f55fa886 fix(webp): improve error message in page not convertible 2024-08-28 12:09:40 -04:00
Antoine Aflalo
62638517e4 test: improve testing suite for expected failure 2024-08-28 12:03:33 -04:00
Antoine Aflalo
dbf7f6c262 fix(webp): be sure we split big page when requested 2024-08-28 11:55:53 -04:00
8 changed files with 147 additions and 38 deletions

View File

@@ -28,7 +28,17 @@ jobs:
mv go-junit-report /usr/local/bin/ mv go-junit-report /usr/local/bin/
- name: Run tests - name: Run tests
run: go test -v 2>&1 ./... -coverprofile=coverage.txt | go-junit-report -set-exit-code > junit.xml run: |
set -o pipefail
go test -v 2>&1 ./... -coverprofile=coverage.txt | tee test-results.txt
- name: Analyse test results
run: go-junit-report < test-results.txt > report.xml
- name: Upload test result artifact
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results.txt
retention-days: 7
- name: Upload results to Codecov - name: Upload results to Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4
with: with:

View File

@@ -3,13 +3,13 @@ LABEL authors="Belphemur"
ENV USER=abc ENV USER=abc
ENV CONFIG_FOLDER=/config ENV CONFIG_FOLDER=/config
ENV PUID=99 ENV PUID=99
RUN mkdir -p "${CONFIG_FOLDER}" && adduser \ RUN adduser \
--disabled-password \ --disabled-password \
--gecos "" \ --gecos "" \
--home "$(pwd)" \ --home "$(pwd)" \
--ingroup "users" \ --ingroup "users" \
--no-create-home \
--uid "${PUID}" \ --uid "${PUID}" \
--home "${CONFIG_FOLDER}" \
"${USER}" && \ "${USER}" && \
chown ${PUID}:${GUID} "${CONFIG_FOLDER}" chown ${PUID}:${GUID} "${CONFIG_FOLDER}"
@@ -17,5 +17,6 @@ COPY CBZOptimizer /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 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
VOLUME ${CONFIG_FOLDER}
USER ${USER} USER ${USER}
ENTRYPOINT ["/usr/local/bin/CBZOptimizer"] ENTRYPOINT ["/usr/local/bin/CBZOptimizer"]

View File

@@ -12,6 +12,9 @@ import (
type Converter interface { type Converter interface {
// Format of the converter // Format of the converter
Format() (format constant.ConversionFormat) Format() (format constant.ConversionFormat)
// ConvertChapter converts a manga chapter to the specified format.
//
// Returns partial success where some pages are converted and some are not.
ConvertChapter(chapter *manga.Chapter, quality uint8, split bool, progress func(message string, current uint32, total uint32)) (*manga.Chapter, error) ConvertChapter(chapter *manga.Chapter, quality uint8, split bool, progress func(message string, current uint32, total uint32)) (*manga.Chapter, error)
PrepareConverter() error PrepareConverter() error
} }

View File

@@ -2,7 +2,9 @@ package converter
import ( import (
"bytes" "bytes"
"github.com/belphemur/CBZOptimizer/converter/constant"
"github.com/belphemur/CBZOptimizer/manga" "github.com/belphemur/CBZOptimizer/manga"
"golang.org/x/exp/slices"
"image" "image"
"image/jpeg" "image/jpeg"
"os" "os"
@@ -12,29 +14,46 @@ import (
func TestConvertChapter(t *testing.T) { func TestConvertChapter(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
genTestChapter func(path string) (*manga.Chapter, error) genTestChapter func(path string) (*manga.Chapter, error)
split bool split bool
expectFailure []constant.ConversionFormat
expectPartialSuccess []constant.ConversionFormat
}{ }{
{ {
name: "All split pages", name: "All split pages",
genTestChapter: genBigPages, genTestChapter: genHugePage,
split: true, split: true,
expectFailure: []constant.ConversionFormat{},
expectPartialSuccess: []constant.ConversionFormat{},
}, },
{ {
name: "Big Pages, no split", name: "Big Pages, no split",
genTestChapter: genBigPages, genTestChapter: genHugePage,
split: false, split: false,
expectFailure: []constant.ConversionFormat{constant.WebP},
expectPartialSuccess: []constant.ConversionFormat{},
}, },
{ {
name: "No split pages", name: "No split pages",
genTestChapter: genSmallPages, genTestChapter: genSmallPages,
split: false, split: false,
expectFailure: []constant.ConversionFormat{},
expectPartialSuccess: []constant.ConversionFormat{},
}, },
{ {
name: "Mix of split and no split pages", name: "Mix of split and no split pages",
genTestChapter: genMixSmallBig, genTestChapter: genMixSmallBig,
split: true, split: true,
expectFailure: []constant.ConversionFormat{},
expectPartialSuccess: []constant.ConversionFormat{},
},
{
name: "Mix of Huge and small page",
genTestChapter: genMixSmallHuge,
split: false,
expectFailure: []constant.ConversionFormat{},
expectPartialSuccess: []constant.ConversionFormat{constant.WebP},
}, },
} }
// Load test genTestChapter from testdata // Load test genTestChapter from testdata
@@ -63,15 +82,29 @@ func TestConvertChapter(t *testing.T) {
t.Log(msg) t.Log(msg)
} }
convertedChapter, err := converter.ConvertChapter(chapter, quality, false, progress) convertedChapter, err := converter.ConvertChapter(chapter, quality, tc.split, progress)
if err != nil { if err != nil {
if convertedChapter != nil && slices.Contains(tc.expectPartialSuccess, converter.Format()) {
t.Logf("Partial success to convert genTestChapter: %v", err)
return
}
if slices.Contains(tc.expectFailure, converter.Format()) {
t.Logf("Expected failure to convert genTestChapter: %v", err)
return
}
t.Fatalf("failed to convert genTestChapter: %v", err) t.Fatalf("failed to convert genTestChapter: %v", err)
} else if slices.Contains(tc.expectFailure, converter.Format()) {
t.Fatalf("expected failure to convert genTestChapter didn't happen")
} }
if len(convertedChapter.Pages) == 0 { if len(convertedChapter.Pages) == 0 {
t.Fatalf("no pages were converted") t.Fatalf("no pages were converted")
} }
if len(convertedChapter.Pages) != len(chapter.Pages) {
t.Fatalf("converted chapter has different number of pages")
}
for _, page := range convertedChapter.Pages { for _, page := range convertedChapter.Pages {
if page.Extension != ".webp" { if page.Extension != ".webp" {
t.Errorf("page %d was not converted to webp format", page.Index) t.Errorf("page %d was not converted to webp format", page.Index)
@@ -83,7 +116,7 @@ func TestConvertChapter(t *testing.T) {
} }
} }
func genBigPages(path string) (*manga.Chapter, error) { func genHugePage(path string) (*manga.Chapter, error) {
file, err := os.Open(path) file, err := os.Open(path)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -91,8 +124,8 @@ func genBigPages(path string) (*manga.Chapter, error) {
defer file.Close() defer file.Close()
var pages []*manga.Page var pages []*manga.Page
for i := 0; i < 5; i++ { // Assuming there are 5 pages for the test for i := 0; i < 1; i++ { // Assuming there are 5 pages for the test
img := image.NewRGBA(image.Rect(0, 0, 300, 17000)) img := image.NewRGBA(image.Rect(0, 0, 1, 17000))
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err := jpeg.Encode(buf, img, nil) err := jpeg.Encode(buf, img, nil)
if err != nil { if err != nil {
@@ -169,3 +202,32 @@ func genMixSmallBig(path string) (*manga.Chapter, error) {
Pages: pages, Pages: pages,
}, nil }, nil
} }
func genMixSmallHuge(path string) (*manga.Chapter, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var pages []*manga.Page
for i := 0; i < 10; i++ { // Assuming there are 5 pages for the test
img := image.NewRGBA(image.Rect(0, 0, 1, 2000*(i+1)))
buf := new(bytes.Buffer)
err := jpeg.Encode(buf, img, nil)
if err != nil {
return nil, err
}
page := &manga.Page{
Index: uint16(i),
Contents: buf,
Extension: ".jpg",
}
pages = append(pages, page)
}
return &manga.Chapter{
FilePath: path,
Pages: pages,
}, nil
}

View File

@@ -0,0 +1,13 @@
package errors
type PageIgnoredError struct {
s string
}
func (e *PageIgnoredError) Error() string {
return e.s
}
func NewPageIgnored(text string) error {
return &PageIgnoredError{text}
}

View File

@@ -2,8 +2,10 @@ package webp
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"github.com/belphemur/CBZOptimizer/converter/constant" "github.com/belphemur/CBZOptimizer/converter/constant"
converterrors "github.com/belphemur/CBZOptimizer/converter/errors"
"github.com/belphemur/CBZOptimizer/manga" "github.com/belphemur/CBZOptimizer/manga"
"github.com/oliamb/cutter" "github.com/oliamb/cutter"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
@@ -106,22 +108,25 @@ func (converter *Converter) ConvertChapter(chapter *manga.Chapter, quality uint8
go func(page *manga.Page) { go func(page *manga.Page) {
defer wgPages.Done() defer wgPages.Done()
splitNeeded, img, format, err := converter.checkPageNeedsSplit(page) splitNeeded, img, format, err := converter.checkPageNeedsSplit(page, split)
// Respect choice to split or not
splitNeeded = split && splitNeeded
if err != nil { if err != nil {
errChan <- fmt.Errorf("error checking if page %d of genTestChapter %s needs split: %v", page.Index, chapter.FilePath, err) errChan <- err
// Partial error in this case, we want the page, but not converting it
if img != nil {
wgConvertedPages.Add(1)
pagesChan <- manga.NewContainer(page, img, format, false)
}
return return
} }
if !splitNeeded { if !splitNeeded {
wgConvertedPages.Add(1) wgConvertedPages.Add(1)
pagesChan <- manga.NewContainer(page, img, format) pagesChan <- manga.NewContainer(page, img, format, true)
return return
} }
images, err := converter.cropImage(img) images, err := converter.cropImage(img)
if err != nil { if err != nil {
errChan <- fmt.Errorf("error converting page %d of genTestChapter %s to webp: %v", page.Index, chapter.FilePath, err) errChan <- err
return return
} }
@@ -129,7 +134,7 @@ func (converter *Converter) ConvertChapter(chapter *manga.Chapter, quality uint8
for i, img := range images { for i, img := range images {
page := &manga.Page{Index: page.Index, IsSplitted: true, SplitPartIndex: uint16(i)} page := &manga.Page{Index: page.Index, IsSplitted: true, SplitPartIndex: uint16(i)}
wgConvertedPages.Add(1) wgConvertedPages.Add(1)
pagesChan <- manga.NewContainer(page, img, "N/A") pagesChan <- manga.NewContainer(page, img, "N/A", true)
} }
}(page) }(page)
} }
@@ -144,8 +149,9 @@ func (converter *Converter) ConvertChapter(chapter *manga.Chapter, quality uint8
errList = append(errList, err) errList = append(errList, err)
} }
var aggregatedError error = nil
if len(errList) > 0 { if len(errList) > 0 {
return nil, fmt.Errorf("encountered errors: %v", errList) aggregatedError = errors.Join(errList...)
} }
slices.SortFunc(pages, func(a, b *manga.Page) int { slices.SortFunc(pages, func(a, b *manga.Page) int {
@@ -158,7 +164,7 @@ func (converter *Converter) ConvertChapter(chapter *manga.Chapter, quality uint8
runtime.GC() runtime.GC()
return chapter, nil return chapter, aggregatedError
} }
func (converter *Converter) cropImage(img image.Image) ([]image.Image, error) { func (converter *Converter) cropImage(img image.Image) ([]image.Image, error) {
@@ -194,7 +200,7 @@ func (converter *Converter) cropImage(img image.Image) ([]image.Image, error) {
return parts, nil return parts, nil
} }
func (converter *Converter) checkPageNeedsSplit(page *manga.Page) (bool, image.Image, string, error) { func (converter *Converter) checkPageNeedsSplit(page *manga.Page, splitRequested bool) (bool, image.Image, string, error) {
reader := io.Reader(bytes.NewBuffer(page.Contents.Bytes())) reader := io.Reader(bytes.NewBuffer(page.Contents.Bytes()))
img, format, err := image.Decode(reader) img, format, err := image.Decode(reader)
if err != nil { if err != nil {
@@ -204,16 +210,19 @@ func (converter *Converter) checkPageNeedsSplit(page *manga.Page) (bool, image.I
bounds := img.Bounds() bounds := img.Bounds()
height := bounds.Dy() height := bounds.Dy()
if height >= webpMaxHeight { if height >= webpMaxHeight && !splitRequested {
return false, img, format, fmt.Errorf("page[%d] height %d exceeds maximum height %d of webp format", page.Index, height, webpMaxHeight) return false, img, format, converterrors.NewPageIgnored(fmt.Sprintf("page %d is too tall [max: %dpx] to be converted to webp format", page.Index, webpMaxHeight))
} }
return height >= converter.maxHeight, img, format, nil return height >= converter.maxHeight && splitRequested, img, format, nil
} }
func (converter *Converter) convertPage(container *manga.PageContainer, quality uint8) (*manga.PageContainer, error) { func (converter *Converter) convertPage(container *manga.PageContainer, quality uint8) (*manga.PageContainer, error) {
if container.Format == "webp" { if container.Format == "webp" {
return container, nil return container, nil
} }
if !container.IsToBeConverted {
return container, nil
}
converted, err := converter.convert(container.Image, uint(quality)) converted, err := converter.convert(container.Image, uint(quality))
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -10,8 +10,10 @@ type PageContainer struct {
Image image.Image Image image.Image
// Format is a string representing the format of the image (e.g., "png", "jpeg", "webp"). // Format is a string representing the format of the image (e.g., "png", "jpeg", "webp").
Format string Format string
// IsToBeConverted is a boolean flag indicating whether the image needs to be converted to another format.
IsToBeConverted bool
} }
func NewContainer(Page *Page, img image.Image, format string) *PageContainer { func NewContainer(Page *Page, img image.Image, format string, isToBeConverted bool) *PageContainer {
return &PageContainer{Page: Page, Image: img, Format: format} return &PageContainer{Page: Page, Image: img, Format: format, IsToBeConverted: isToBeConverted}
} }

View File

@@ -1,9 +1,11 @@
package utils package utils
import ( import (
"errors"
"fmt" "fmt"
"github.com/belphemur/CBZOptimizer/cbz" "github.com/belphemur/CBZOptimizer/cbz"
"github.com/belphemur/CBZOptimizer/converter" "github.com/belphemur/CBZOptimizer/converter"
errors2 "github.com/belphemur/CBZOptimizer/converter/errors"
"log" "log"
"strings" "strings"
) )
@@ -38,8 +40,15 @@ func Optimize(options *OptimizeOptions) error {
} }
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to convert chapter: %v", err) var pageIgnoredError *errors2.PageIgnoredError
if !errors.As(err, &pageIgnoredError) {
return fmt.Errorf("failed to convert chapter: %v", err)
}
} }
if convertedChapter == nil {
return fmt.Errorf("failed to convert chapter")
}
convertedChapter.SetConverted() convertedChapter.SetConverted()
// Write the converted chapter back to a CBZ file // Write the converted chapter back to a CBZ file