mirror of
https://github.com/Belphemur/CBZOptimizer.git
synced 2025-10-13 20:18:52 +02:00

Still need to figure out the memory issues Consolidates image conversion logic into a dedicated method. This change streamlines the conversion process by centralizing the setting of converted image data, extension, and size. It also introduces a flag to track whether an image has been converted. The old resource cleanup has been removed since it is not needed anymore.
261 lines
6.3 KiB
Go
261 lines
6.3 KiB
Go
package webp
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
_ "image/jpeg"
|
|
"image/png"
|
|
"runtime"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/belphemur/CBZOptimizer/v2/internal/manga"
|
|
"github.com/belphemur/CBZOptimizer/v2/pkg/converter/constant"
|
|
converterrors "github.com/belphemur/CBZOptimizer/v2/pkg/converter/errors"
|
|
"github.com/oliamb/cutter"
|
|
"golang.org/x/exp/slices"
|
|
_ "golang.org/x/image/webp"
|
|
)
|
|
|
|
const webpMaxHeight = 16383
|
|
|
|
type Converter struct {
|
|
maxHeight int
|
|
cropHeight int
|
|
isPrepared bool
|
|
}
|
|
|
|
func (converter *Converter) Format() (format constant.ConversionFormat) {
|
|
return constant.WebP
|
|
}
|
|
|
|
func New() *Converter {
|
|
return &Converter{
|
|
//maxHeight: 16383 / 2,
|
|
maxHeight: 4000,
|
|
cropHeight: 2000,
|
|
isPrepared: false,
|
|
}
|
|
}
|
|
|
|
func (converter *Converter) PrepareConverter() error {
|
|
if converter.isPrepared {
|
|
return nil
|
|
}
|
|
err := PrepareEncoder()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
converter.isPrepared = true
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
var wgConvertedPages sync.WaitGroup
|
|
maxGoroutines := runtime.NumCPU()
|
|
|
|
pagesChan := make(chan *manga.PageContainer, maxGoroutines)
|
|
errChan := make(chan error, maxGoroutines)
|
|
doneChan := make(chan struct{})
|
|
|
|
var wgPages sync.WaitGroup
|
|
wgPages.Add(len(chapter.Pages))
|
|
|
|
guard := make(chan struct{}, maxGoroutines)
|
|
pagesMutex := sync.Mutex{}
|
|
var pages []*manga.Page
|
|
var totalPages = uint32(len(chapter.Pages))
|
|
|
|
// Start the worker pool
|
|
go func() {
|
|
for page := range pagesChan {
|
|
guard <- struct{}{} // would block if guard channel is already filled
|
|
go func(pageToConvert *manga.PageContainer) {
|
|
defer func() {
|
|
wgConvertedPages.Done()
|
|
<-guard
|
|
}()
|
|
|
|
convertedPage, err := converter.convertPage(pageToConvert, quality)
|
|
if err != nil {
|
|
if convertedPage == nil {
|
|
errChan <- err
|
|
return
|
|
}
|
|
buffer := new(bytes.Buffer)
|
|
err := png.Encode(buffer, convertedPage.Image)
|
|
if err != nil {
|
|
errChan <- err
|
|
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()), uint32(len(pages)), totalPages)
|
|
pagesMutex.Unlock()
|
|
}(page)
|
|
}
|
|
close(doneChan)
|
|
}()
|
|
|
|
// Process pages
|
|
for _, page := range chapter.Pages {
|
|
go func(page *manga.Page) {
|
|
defer wgPages.Done()
|
|
|
|
splitNeeded, img, format, err := converter.checkPageNeedsSplit(page, split)
|
|
if err != nil {
|
|
errChan <- err
|
|
if img != nil {
|
|
wgConvertedPages.Add(1)
|
|
pagesChan <- manga.NewContainer(page, img, format, false)
|
|
}
|
|
return
|
|
}
|
|
|
|
if !splitNeeded {
|
|
wgConvertedPages.Add(1)
|
|
pagesChan <- manga.NewContainer(page, img, format, true)
|
|
return
|
|
}
|
|
|
|
images, err := converter.cropImage(img)
|
|
if err != nil {
|
|
errChan <- err
|
|
return
|
|
}
|
|
|
|
atomic.AddUint32(&totalPages, uint32(len(images)-1))
|
|
for i, img := range images {
|
|
newPage := &manga.Page{
|
|
Index: page.Index,
|
|
IsSplitted: true,
|
|
SplitPartIndex: uint16(i),
|
|
}
|
|
wgConvertedPages.Add(1)
|
|
pagesChan <- manga.NewContainer(newPage, img, "N/A", true)
|
|
}
|
|
}(page)
|
|
}
|
|
|
|
wgPages.Wait()
|
|
close(pagesChan)
|
|
|
|
// Wait for all conversions to complete
|
|
<-doneChan
|
|
wgConvertedPages.Wait()
|
|
close(errChan)
|
|
close(guard)
|
|
|
|
var errList []error
|
|
for err := range errChan {
|
|
errList = append(errList, err)
|
|
}
|
|
|
|
var aggregatedError error = nil
|
|
if len(errList) > 0 {
|
|
aggregatedError = errors.Join(errList...)
|
|
}
|
|
|
|
slices.SortFunc(pages, func(a, b *manga.Page) int {
|
|
if a.Index == b.Index {
|
|
return int(a.SplitPartIndex) - int(b.SplitPartIndex)
|
|
}
|
|
return int(a.Index) - int(b.Index)
|
|
})
|
|
chapter.Pages = pages
|
|
|
|
runtime.GC()
|
|
|
|
return chapter, aggregatedError
|
|
}
|
|
|
|
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 *manga.Page, splitRequested bool) (bool, image.Image, string, error) {
|
|
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()
|
|
|
|
if height >= webpMaxHeight && !splitRequested {
|
|
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 && splitRequested, img, format, nil
|
|
}
|
|
|
|
func (converter *Converter) convertPage(container *manga.PageContainer, quality uint8) (*manga.PageContainer, error) {
|
|
// Fix WebP format detection (case insensitive)
|
|
if container.Format == "webp" || container.Format == "WEBP" {
|
|
container.Page.Extension = ".webp"
|
|
return container, nil
|
|
}
|
|
if !container.IsToBeConverted {
|
|
return container, nil
|
|
}
|
|
converted, err := converter.convert(container.Image, uint(quality))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
container.SetConverted(converted, ".webp");
|
|
return container, nil
|
|
}
|
|
|
|
// convert converts an image to the WebP format. It decodes the image from the input buffer,
|
|
// encodes it as a WebP file using the webp.Encode() function, and returns the resulting WebP
|
|
// 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
|
|
}
|