mirror of
https://github.com/gen2brain/cbconvert
synced 2026-06-30 01:01:54 +02:00
Optimize cover preview
This commit is contained in:
@@ -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)
|
||||
|
||||
+48
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user