mirror of
https://github.com/Belphemur/CBZOptimizer.git
synced 2026-07-21 19:05:39 +02:00
refactor!: disk-first zero-copy architecture with file-to-file cwebp conversion
BREAKING CHANGE: Complete refactoring of the conversion pipeline. - Replace in-memory Page/PageContainer with disk-based PageFile struct - Extract archives to temp directory instead of loading into memory - Use cwebp InputFile/OutputFile for zero-copy file-to-file conversion - Use cwebp -crop for splitting (no Go-side image decode needed) - Check if already converted before extracting (fast pre-check) - Remove oliamb/cutter dependency (replaced by cwebp -crop) - Remove page_container.go (no longer needed) - Add comprehensive tests with improved coverage Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com>
This commit is contained in:
co-authored by
Belphemur
parent
e87e73eb28
commit
164dc577b9
+13
-83
@@ -1,86 +1,16 @@
|
||||
package manga
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
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. Nil when the page contents have been staged to
|
||||
// disk (see TempFilePath) to bound memory usage.
|
||||
Contents *bytes.Buffer `json:"-"`
|
||||
// TempFilePath, when non-empty, points to a file on disk (in a staging
|
||||
// temp folder) holding the page contents instead of keeping them fully
|
||||
// in memory. Use Open() to transparently read the page contents
|
||||
// regardless of where they are stored.
|
||||
TempFilePath string `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."`
|
||||
}
|
||||
|
||||
// Open returns a reader for the page contents, transparently handling
|
||||
// whether the contents are held in memory (Contents) or staged on disk
|
||||
// (TempFilePath). The caller is responsible for closing the returned
|
||||
// reader.
|
||||
func (page *Page) Open() (io.ReadCloser, error) {
|
||||
if page.TempFilePath != "" {
|
||||
file, err := os.Open(page.TempFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open staged page contents: %w", err)
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
if page.Contents == nil {
|
||||
return nil, fmt.Errorf("page has no contents: neither TempFilePath nor Contents is set")
|
||||
}
|
||||
return io.NopCloser(bytes.NewReader(page.Contents.Bytes())), nil
|
||||
}
|
||||
|
||||
// Stage writes the given content to a file in tempDir instead of keeping it
|
||||
// in memory, updating Extension, Size and TempFilePath accordingly and
|
||||
// clearing Contents. This is used after converting a page so that only the
|
||||
// pages currently being processed are held in memory, bounding memory usage
|
||||
// for chapters with many/large pages.
|
||||
func (page *Page) Stage(tempDir string, content *bytes.Buffer, extension string) error {
|
||||
file, err := os.CreateTemp(tempDir, "page-*.tmp")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create staging file: %w", err)
|
||||
}
|
||||
fileName := file.Name()
|
||||
|
||||
written, writeErr := file.Write(content.Bytes())
|
||||
closeErr := file.Close()
|
||||
if writeErr == nil && closeErr == nil && written == content.Len() {
|
||||
page.TempFilePath = fileName
|
||||
page.Extension = extension
|
||||
page.Size = uint64(written)
|
||||
page.Contents = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Something went wrong: remove the incomplete/partial staging file
|
||||
// rather than leaving corrupted data behind on disk.
|
||||
if removeErr := os.Remove(fileName); removeErr != nil {
|
||||
log.Warn().Str("file", fileName).Err(removeErr).Msg("Failed to remove incomplete staging file")
|
||||
}
|
||||
|
||||
if writeErr != nil {
|
||||
return fmt.Errorf("failed to write staging file: %w", writeErr)
|
||||
}
|
||||
if closeErr != nil {
|
||||
return fmt.Errorf("failed to close staging file: %w", closeErr)
|
||||
}
|
||||
return fmt.Errorf("short write to staging file: wrote %d of %d bytes", written, content.Len())
|
||||
// PageFile represents a single page image stored on disk.
|
||||
// No image data is held in memory — only metadata and a file path.
|
||||
type PageFile struct {
|
||||
// Index of the page in the chapter (original ordering).
|
||||
Index uint16
|
||||
// Extension of the page image file (e.g., ".webp", ".jpg").
|
||||
Extension string
|
||||
// FilePath is the absolute path to the image file on disk.
|
||||
FilePath string
|
||||
// IsSplitted indicates whether this page was split from a larger image.
|
||||
IsSplitted bool
|
||||
// SplitPartIndex is the part index when the page was split.
|
||||
SplitPartIndex uint16
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user