diff --git a/cbconvert.go b/cbconvert.go index 257e17e..6fda23f 100644 --- a/cbconvert.go +++ b/cbconvert.go @@ -102,6 +102,8 @@ type Converter struct { prefix string // Input root for the current file, used to build recursive output paths root string + // Target size for fast previews; when set, JPEG covers are IDCT-downscaled while decoding + previewWidth, previewHeight int // Number of files Nfiles int // Index of the current file @@ -554,6 +556,27 @@ func (c *Converter) Preview(fileName string, fileInfo os.FileInfo, width, height return c.previewImage(fileName, i, width, height) } +// CoverPreview returns the cover fitted into width x height, skipping the output-codec round-trip. +func (c *Converter) CoverPreview(fileName string, fileInfo os.FileInfo, width, height int) (Image, error) { + c.previewWidth, c.previewHeight = width, height + + i, err := c.coverImage(fileName, fileInfo) + if err != nil { + return Image{}, fmt.Errorf("%s: %w", fileName, err) + } + + if width != 0 && height != 0 { + i = fit(i, width, height, resampleFilter(c.Opts.Filter)) + } + + var img Image + img.Image = i + img.Width = i.Bounds().Dx() + img.Height = i.Bounds().Dy() + + return img, nil +} + // PreviewPage returns the page-th image (0-based) as an image preview. func (c *Converter) PreviewPage(fileName string, fileInfo os.FileInfo, page, width, height int) (Image, error) { i, err := c.pageImage(fileName, fileInfo, page) diff --git a/cbconvert_convert.go b/cbconvert_convert.go index ad17213..08b4221 100644 --- a/cbconvert_convert.go +++ b/cbconvert_convert.go @@ -1,6 +1,7 @@ package cbconvert import ( + "bufio" "bytes" "context" "fmt" @@ -16,6 +17,7 @@ import ( "github.com/gen2brain/avif" "github.com/gen2brain/go-fitz" "github.com/gen2brain/jpegli" + "github.com/gen2brain/jpegn" "github.com/gen2brain/jpegxl" "github.com/gen2brain/webp" "github.com/jsummers/gobmp" @@ -356,8 +358,42 @@ func (c *Converter) imageTransform(img image.Image) image.Image { } // imageDecode decodes image from reader. +// jpegnOptions decodes straight to RGBA with high-quality chroma upsampling. +var jpegnOptions = jpegn.Options{ToRGBA: true, UpsampleMethod: jpegn.CatmullRom} + func (c *Converter) imageDecode(reader io.Reader) (image.Image, error) { - img, _, err := image.Decode(reader) + br := bufio.NewReader(reader) + + if magic, err := br.Peek(2); err == nil && magic[0] == 0xff && magic[1] == 0xd8 { + opts := jpegnOptions + + if c.previewWidth > 0 && c.previewHeight > 0 { + data, err := io.ReadAll(br) + if err != nil { + return nil, fmt.Errorf("imageDecode: %w", err) + } + + if cfg, err := jpegn.DecodeConfig(bytes.NewReader(data)); err == nil { + opts.ScaleDenom = scaleDenom(cfg.Width, cfg.Height, c.previewWidth, c.previewHeight) + } + + img, err := jpegn.Decode(bytes.NewReader(data), &opts) + if err != nil { + return nil, fmt.Errorf("imageDecode: %w", err) + } + + return img, nil + } + + img, err := jpegn.Decode(br, &opts) + if err != nil { + return nil, fmt.Errorf("imageDecode: %w", err) + } + + return img, nil + } + + img, _, err := image.Decode(br) if err != nil { return img, fmt.Errorf("imageDecode: %w", err) } @@ -365,6 +401,17 @@ func (c *Converter) imageDecode(reader io.Reader) (image.Image, error) { return img, nil } +// scaleDenom returns the largest JPEG IDCT denominator (1, 2, 4, 8) that keeps w x h at or above tw x th. +func scaleDenom(w, h, tw, th int) int { + for _, d := range []int{8, 4, 2} { + if w/d >= tw && h/d >= th { + return d + } + } + + return 1 +} + // imageEncode encodes image to file. func (c *Converter) imageEncode(img image.Image, w io.Writer) error { var err error diff --git a/cmd/cbconvert-gui/dialogs.go b/cmd/cbconvert-gui/dialogs.go index 23c84e6..c200cba 100644 --- a/cmd/cbconvert-gui/dialogs.go +++ b/cmd/cbconvert-gui/dialogs.go @@ -70,10 +70,37 @@ func fileDlg(title string, multiple, directory bool, dirKey string) ([]string, e const dlgPreviewName = "_FILEDLGPREVIEW_" +// previewPad insets the cover from the preview pane edges, in pixels per side. +const previewPad = 8 + // previewCover returns a FILE_CB handler that draws the highlighted comic's cover in the dialog preview pane. +// Extracted covers are cached by path so re-highlighting a file doesn't re-extract it. func previewCover() iup.FileFunc { - var image iup.Ihandle - var lastFile string + const maxCache = 32 + + cache := make(map[string]iup.Ihandle) + order := make([]string, 0, maxCache) + + cover := func(path string, w, h int) iup.Ihandle { + if img, ok := cache[path]; ok { + return img + } + + img := loadCover(path, w, h) + cache[path] = img + order = append(order, path) + + if len(order) > maxCache { + old := order[0] + order = order[1:] + if oi := cache[old]; oi != 0 { + oi.Destroy() + } + delete(cache, old) + } + + return img + } return func(ih iup.Ihandle, filename, status string) int { switch status { @@ -82,19 +109,8 @@ func previewCover() iup.FileFunc { cw, ch := iup.DrawGetSize(ih) iup.DrawParentBackground(ih) - if filename != lastFile { - lastFile = filename - if image != 0 { - image.Destroy() - image = 0 - } - if img := loadCover(filename, cw, ch); img != 0 { - image = img - iup.SetHandle(dlgPreviewName, image) - } - } - - if image != 0 { + if image := cover(filename, cw-2*previewPad, ch-2*previewPad); image != 0 { + iup.SetHandle(dlgPreviewName, image) iw, iih, _ := iup.DrawGetImageInfo(dlgPreviewName) iup.DrawImage(ih, dlgPreviewName, (cw-iw)/2, (ch-iih)/2, iw, iih) } else { @@ -106,11 +122,13 @@ func previewCover() iup.FileFunc { iup.DrawEnd(ih) case "FINISH": - if image != 0 { - image.Destroy() - image = 0 + for _, img := range cache { + if img != 0 { + img.Destroy() + } } - lastFile = "" + cache = make(map[string]iup.Ihandle) + order = order[:0] } return iup.DEFAULT @@ -131,7 +149,7 @@ func loadCover(path string, w, h int) iup.Ihandle { opts := cbconvert.NewOptions() opts.DPI = 96 - img, err := cbconvert.New(opts).Preview(path, fi, w, h) + img, err := cbconvert.New(opts).CoverPreview(path, fi, w, h) if err != nil || img.Image == nil { return 0 } diff --git a/cmd/cbconvert-gui/go.mod b/cmd/cbconvert-gui/go.mod index e40d6e1..0e2baff 100644 --- a/cmd/cbconvert-gui/go.mod +++ b/cmd/cbconvert-gui/go.mod @@ -23,7 +23,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/ebitengine/purego v0.10.1 // indirect github.com/gen2brain/avif v0.5.1 // indirect - github.com/gen2brain/go-fitz v1.24.15 // indirect + github.com/gen2brain/go-fitz v1.28.0 // indirect github.com/gen2brain/jpegli v0.4.1 // indirect github.com/gen2brain/jpegxl v0.5.1 // indirect github.com/gen2brain/webp v0.6.1 // indirect @@ -31,7 +31,6 @@ require ( github.com/golang/geo v0.0.0-20260625163123-7c0e84413537 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect - github.com/jupiterrider/ffi v0.7.0 // indirect github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/mholt/archives v0.1.5 // indirect diff --git a/cmd/cbconvert-gui/go.sum b/cmd/cbconvert-gui/go.sum index b4f5fd9..26d04de 100644 --- a/cmd/cbconvert-gui/go.sum +++ b/cmd/cbconvert-gui/go.sum @@ -42,8 +42,7 @@ github.com/gen2brain/avif v0.5.1 h1:LQzLsJpWyGlsa4wuZ3D57qEbCiICIK7Yidz5ZPEwzTk= github.com/gen2brain/avif v0.5.1/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8= github.com/gen2brain/cbconvert v1.0.5-0.20260626071631-8155626dbb42 h1:p1K1jOk+rKDhTgh6fiYMKSqXJdIVUWFLK5jodTtwPOU= github.com/gen2brain/cbconvert v1.0.5-0.20260626071631-8155626dbb42/go.mod h1:qHzMhKZ7VBTffwDQ/9rc4yZ9FO5677ZSjSFZ7QNfaLw= -github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo= -github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo= +github.com/gen2brain/go-fitz v1.28.0 h1:RovqgQPAcOuyv5HZrWsTWl8qwlwbAHSKcAZXZUw0Vlk= github.com/gen2brain/iup-go/iup v0.32.1-0.20260626100855-f328861e3291 h1:ad/nhBGhknOGDpiHnQ0ZLltZccG82t4tAKK94SrQ8OY= github.com/gen2brain/iup-go/iup v0.32.1-0.20260626100855-f328861e3291/go.mod h1:V4f7tHOJAeHtjQ+ju795QKv6DGdLEb4L5cmWB1sjSzU= github.com/gen2brain/jpegli v0.4.1 h1:qc11IQU0jTYFltroulT4MXmbu9YRftqHV0YBZ0Bqz5o= @@ -69,8 +68,6 @@ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= -github.com/jupiterrider/ffi v0.7.0 h1:RKsl6Ascal+3kyAqR5Qcbp83LceQMLc1VZbPfHWoNzs= -github.com/jupiterrider/ffi v0.7.0/go.mod h1:9dauhpOfNqrqk28fxuu0kkdeFtT9Qr4vbfigiuIXN7c= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= diff --git a/cmd/cbconvert/go.mod b/cmd/cbconvert/go.mod index a584a5f..c1580c6 100644 --- a/cmd/cbconvert/go.mod +++ b/cmd/cbconvert/go.mod @@ -24,7 +24,7 @@ require ( github.com/ebitengine/purego v0.10.1 // indirect github.com/fvbommel/sortorder v1.1.0 // indirect github.com/gen2brain/avif v0.5.1 // indirect - github.com/gen2brain/go-fitz v1.24.15 // indirect + github.com/gen2brain/go-fitz v1.28.0 // indirect github.com/gen2brain/jpegli v0.4.1 // indirect github.com/gen2brain/jpegxl v0.5.1 // indirect github.com/gen2brain/webp v0.6.1 // indirect @@ -32,7 +32,6 @@ require ( github.com/golang/geo v0.0.0-20260625163123-7c0e84413537 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect - github.com/jupiterrider/ffi v0.7.0 // indirect github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/mattn/go-runewidth v0.0.24 // indirect diff --git a/cmd/cbconvert/go.sum b/cmd/cbconvert/go.sum index 0026352..5f3914c 100644 --- a/cmd/cbconvert/go.sum +++ b/cmd/cbconvert/go.sum @@ -46,8 +46,7 @@ github.com/gen2brain/avif v0.5.1 h1:LQzLsJpWyGlsa4wuZ3D57qEbCiICIK7Yidz5ZPEwzTk= github.com/gen2brain/avif v0.5.1/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8= github.com/gen2brain/cbconvert v1.0.5-0.20260626071631-8155626dbb42 h1:p1K1jOk+rKDhTgh6fiYMKSqXJdIVUWFLK5jodTtwPOU= github.com/gen2brain/cbconvert v1.0.5-0.20260626071631-8155626dbb42/go.mod h1:qHzMhKZ7VBTffwDQ/9rc4yZ9FO5677ZSjSFZ7QNfaLw= -github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo= -github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo= +github.com/gen2brain/go-fitz v1.28.0 h1:RovqgQPAcOuyv5HZrWsTWl8qwlwbAHSKcAZXZUw0Vlk= github.com/gen2brain/jpegli v0.4.1 h1:qc11IQU0jTYFltroulT4MXmbu9YRftqHV0YBZ0Bqz5o= github.com/gen2brain/jpegli v0.4.1/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk= github.com/gen2brain/jpegxl v0.5.1 h1:UuBUIkZ35DErImU3bTA6rltfV5zSgVNOA/K5a6JibfE= @@ -71,8 +70,6 @@ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= -github.com/jupiterrider/ffi v0.7.0 h1:RKsl6Ascal+3kyAqR5Qcbp83LceQMLc1VZbPfHWoNzs= -github.com/jupiterrider/ffi v0.7.0/go.mod h1:9dauhpOfNqrqk28fxuu0kkdeFtT9Qr4vbfigiuIXN7c= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= diff --git a/go.mod b/go.mod index e1ad75a..4f81bb1 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/gen2brain/avif v0.5.1 github.com/gen2brain/go-fitz v1.28.0 github.com/gen2brain/jpegli v0.4.1 + github.com/gen2brain/jpegn v0.4.2 github.com/gen2brain/jpegxl v0.5.1 github.com/gen2brain/webp v0.6.1 github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 diff --git a/go.sum b/go.sum index b2c607e..64b59dc 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,8 @@ github.com/gen2brain/go-fitz v1.28.0 h1:RovqgQPAcOuyv5HZrWsTWl8qwlwbAHSKcAZXZUw0 github.com/gen2brain/go-fitz v1.28.0/go.mod h1:pY2hqAjp9Zy7qfPI2gwbJMHBFAdZpVXOLrRxD82l3Bs= github.com/gen2brain/jpegli v0.4.1 h1:qc11IQU0jTYFltroulT4MXmbu9YRftqHV0YBZ0Bqz5o= github.com/gen2brain/jpegli v0.4.1/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk= +github.com/gen2brain/jpegn v0.4.2 h1:sxy2yolV1eNA02uYtnqBFm4EIC3ETnars98aG7Dc4LM= +github.com/gen2brain/jpegn v0.4.2/go.mod h1:YvcVOmVPSAsefH6yn9HBW3uY0EHlZwCMoiJXoAWfgL0= github.com/gen2brain/jpegxl v0.5.1 h1:UuBUIkZ35DErImU3bTA6rltfV5zSgVNOA/K5a6JibfE= github.com/gen2brain/jpegxl v0.5.1/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE= github.com/gen2brain/webp v0.6.1 h1:ei7Y1SWpQcdqz3YNDNyn4y2nQanxs9WLzwW5/2DKS64= diff --git a/go.work.sum b/go.work.sum index ec81f96..d14e5ef 100644 --- a/go.work.sum +++ b/go.work.sum @@ -95,6 +95,7 @@ golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=