Add page spin to preview other pages

This commit is contained in:
Milan Nikolic
2026-06-25 11:03:27 +02:00
parent f289c9cd06
commit e1134cd902
8 changed files with 426 additions and 121 deletions
+23 -10
View File
@@ -43,7 +43,7 @@ type Options struct {
NoUpscale bool NoUpscale bool
// Document rendering resolution in DPI (PDF, EPUB, etc.); 0 uses the default // Document rendering resolution in DPI (PDF, EPUB, etc.); 0 uses the default
DPI int DPI int
// 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos // 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos
Filter int Filter int
// Do not convert the cover image // Do not convert the cover image
NoCover bool NoCover bool
@@ -410,7 +410,7 @@ func (c *Converter) Thumbnail(file File) error {
if c.Opts.Width > 0 || c.Opts.Height > 0 { if c.Opts.Width > 0 || c.Opts.Height > 0 {
cover = c.resizeFit(cover) cover = c.resizeFit(cover)
} else { } else {
cover = resize(cover, 256, 0, filters[c.Opts.Filter]) cover = resize(cover, 256, 0, resampleFilter(c.Opts.Filter))
} }
var buf bytes.Buffer var buf bytes.Buffer
@@ -542,15 +542,30 @@ func (c *Converter) Meta(fileName string) (any, error) {
return "", nil return "", nil
} }
// Preview returns image preview. // Preview returns the cover as an image preview.
func (c *Converter) Preview(fileName string, fileInfo os.FileInfo, width, height int) (Image, error) { func (c *Converter) Preview(fileName string, fileInfo os.FileInfo, width, height int) (Image, error) {
var img Image
i, err := c.coverImage(fileName, fileInfo) i, err := c.coverImage(fileName, fileInfo)
if err != nil { if err != nil {
return img, fmt.Errorf("%s: %w", fileName, err) return Image{}, fmt.Errorf("%s: %w", fileName, err)
} }
return c.previewImage(fileName, i, width, height)
}
// 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)
if err != nil {
return Image{}, fmt.Errorf("%s: %w", fileName, err)
}
return c.previewImage(fileName, i, width, height)
}
// previewImage applies the configured transforms and fits the result into width x height.
func (c *Converter) previewImage(fileName string, i image.Image, width, height int) (Image, error) {
var img Image
i = c.imageTransform(i) i = c.imageTransform(i)
var w bytes.Buffer var w bytes.Buffer
@@ -563,15 +578,13 @@ func (c *Converter) Preview(fileName string, fileInfo os.FileInfo, width, height
img.Height = i.Bounds().Dy() img.Height = i.Bounds().Dy()
img.SizeHuman = humanize.IBytes(uint64(len(w.Bytes()))) img.SizeHuman = humanize.IBytes(uint64(len(w.Bytes())))
r := bytes.NewReader(w.Bytes()) dec, err := c.imageDecode(bytes.NewReader(w.Bytes()))
dec, err := c.imageDecode(r)
if err != nil { if err != nil {
return img, fmt.Errorf("%s: %w", fileName, err) return img, fmt.Errorf("%s: %w", fileName, err)
} }
if width != 0 && height != 0 { if width != 0 && height != 0 {
dec = fit(dec, width, height, filters[c.Opts.Filter]) dec = fit(dec, width, height, resampleFilter(c.Opts.Filter))
} }
img.Image = dec img.Image = dec
+126
View File
@@ -85,6 +85,132 @@ func (c *Converter) coverDirectory(dir string) (image.Image, error) {
return img, nil return img, nil
} }
// pageArchive extracts the page-th image (natural reading order) from an archive.
func (c *Converter) pageArchive(fileName string, page int) (image.Image, error) {
contents, err := c.archiveList(fileName)
if err != nil {
return nil, fmt.Errorf("pageArchive: %w", err)
}
images := imagesFromSlice(contents)
sort.Sort(sortorder.Natural(images))
if page < 0 || page >= len(images) {
return nil, fmt.Errorf("pageArchive: page %d out of range (%d pages)", page+1, len(images))
}
data, err := c.archiveFile(fileName, images[page])
if err != nil {
return nil, fmt.Errorf("pageArchive: %w", err)
}
img, err := c.imageDecode(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("pageArchive: %w", err)
}
return img, nil
}
// pageDocument extracts the page-th rendered page from a document.
func (c *Converter) pageDocument(fileName string, page int) (image.Image, error) {
doc, err := fitz.New(fileName)
if err != nil {
return nil, fmt.Errorf("pageDocument: %w", err)
}
defer doc.Close()
if page < 0 || page >= doc.NumPage() {
return nil, fmt.Errorf("pageDocument: page %d out of range (%d pages)", page+1, doc.NumPage())
}
img, err := doc.ImageDPI(page, c.renderDPI())
if err != nil {
return nil, fmt.Errorf("pageDocument: %w", err)
}
return img, nil
}
// pageDirectory extracts the page-th image (natural reading order) from a directory.
func (c *Converter) pageDirectory(dir string, page int) (image.Image, error) {
contents, err := imagesFromPath(dir)
if err != nil {
return nil, fmt.Errorf("pageDirectory: %w", err)
}
images := imagesFromSlice(contents)
sort.Sort(sortorder.Natural(images))
if page < 0 || page >= len(images) {
return nil, fmt.Errorf("pageDirectory: page %d out of range (%d pages)", page+1, len(images))
}
file, err := os.Open(images[page])
if err != nil {
return nil, fmt.Errorf("pageDirectory: %w", err)
}
defer file.Close()
img, err := c.imageDecode(file)
if err != nil {
return nil, fmt.Errorf("pageDirectory: %w", err)
}
return img, nil
}
// pageImage returns the page-th image of a comic file, document or directory.
func (c *Converter) pageImage(fileName string, fileInfo os.FileInfo, page int) (image.Image, error) {
var err error
var img image.Image
switch {
case fileInfo.IsDir():
img, err = c.pageDirectory(fileName, page)
case isDocument(fileName):
img, err = c.pageDocument(fileName, page)
case isArchive(fileName):
img, err = c.pageArchive(fileName, page)
}
if err != nil {
return nil, fmt.Errorf("pageImage: %w", err)
}
return img, nil
}
// PageCount returns the number of pages (images) in a comic file, document or directory.
func (c *Converter) PageCount(fileName string, fileInfo os.FileInfo) (int, error) {
switch {
case fileInfo.IsDir():
contents, err := imagesFromPath(fileName)
if err != nil {
return 0, fmt.Errorf("PageCount: %w", err)
}
return len(imagesFromSlice(contents)), nil
case isDocument(fileName):
doc, err := fitz.New(fileName)
if err != nil {
return 0, fmt.Errorf("PageCount: %w", err)
}
defer doc.Close()
return doc.NumPage(), nil
case isArchive(fileName):
contents, err := c.archiveList(fileName)
if err != nil {
return 0, fmt.Errorf("PageCount: %w", err)
}
return len(imagesFromSlice(contents)), nil
}
return 0, nil
}
// coverName returns the filename that is the most likely to be the cover. // coverName returns the filename that is the most likely to be the cover.
func (c *Converter) coverName(images []string) string { func (c *Converter) coverName(images []string) string {
if len(images) == 0 { if len(images) == 0 {
+11 -2
View File
@@ -38,6 +38,15 @@ var filters = map[int]transform.ResampleFilter{
lanczos: transform.Lanczos, lanczos: transform.Lanczos,
} }
// resampleFilter returns the resample filter for index i, falling back to Linear for an unknown index.
func resampleFilter(i int) transform.ResampleFilter {
if f, ok := filters[i]; ok {
return f
}
return filters[linear]
}
func resize(img image.Image, width, height int, filter transform.ResampleFilter) *image.RGBA { func resize(img image.Image, width, height int, filter transform.ResampleFilter) *image.RGBA {
dstW, dstH := width, height dstW, dstH := width, height
@@ -96,14 +105,14 @@ func withinBounds(img image.Image, width, height int) bool {
// resizeFit resizes img to the configured width/height, honoring Fit and NoUpscale. // resizeFit resizes img to the configured width/height, honoring Fit and NoUpscale.
func (c *Converter) resizeFit(img image.Image) image.Image { func (c *Converter) resizeFit(img image.Image) image.Image {
if c.Opts.Fit { if c.Opts.Fit {
return fit(img, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter]) return fit(img, c.Opts.Width, c.Opts.Height, resampleFilter(c.Opts.Filter))
} }
if c.Opts.NoUpscale && withinBounds(img, c.Opts.Width, c.Opts.Height) { if c.Opts.NoUpscale && withinBounds(img, c.Opts.Width, c.Opts.Height) {
return img return img
} }
return resize(img, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter]) return resize(img, c.Opts.Width, c.Opts.Height, resampleFilter(c.Opts.Filter))
} }
func rotate(img image.Image, angle float64) *image.RGBA { func rotate(img image.Image, angle float64) *image.RGBA {
+34
View File
@@ -211,6 +211,40 @@ func TestConvertDPI(t *testing.T) {
} }
} }
func TestPreviewPage(t *testing.T) {
for _, name := range []string{"testdata/test.cbz", "testdata/test.pdf"} {
conv := New(NewOptions())
files, err := conv.Files([]string{name})
if err != nil {
t.Fatal(err)
}
if len(files) != 1 {
t.Fatalf("%s: expected 1 file, got %d", name, len(files))
}
file := files[0]
n, err := conv.PageCount(file.Path, file.Stat)
if err != nil {
t.Fatal(err)
}
if n < 2 {
t.Fatalf("%s: expected >= 2 pages, got %d", name, n)
}
for _, page := range []int{0, 1, n - 1} {
img, err := conv.PreviewPage(file.Path, file.Stat, page, 0, 0)
if err != nil || img.Image == nil {
t.Fatalf("%s: page %d: img=%v err=%v", name, page, img.Image, err)
}
}
if _, err := conv.PreviewPage(file.Path, file.Stat, n, 0, 0); err == nil {
t.Errorf("%s: page %d (out of range) should error", name, n)
}
}
}
func TestConvertResize(t *testing.T) { func TestConvertResize(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc") tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil { if err != nil {
+116 -48
View File
@@ -7,9 +7,10 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"slices" "slices"
"sort"
"strconv" "strconv"
"strings"
"github.com/fvbommel/sortorder"
"github.com/gen2brain/cbconvert" "github.com/gen2brain/cbconvert"
"github.com/gen2brain/iup-go/iup" "github.com/gen2brain/iup-go/iup"
) )
@@ -24,7 +25,7 @@ func selectRow(i int) {
iup.GetHandle("Table").SetAttribute("FOCUSCELL", fmt.Sprintf("%d:1", i+1)) iup.GetHandle("Table").SetAttribute("FOCUSCELL", fmt.Sprintf("%d:1", i+1))
} }
// onSort re-syncs the files slice to the table's displayed order after a sort, so rows keep mapping to the right file. // onSort re-syncs the files slice to the table's displayed order after a sort.
func onSort(ih iup.Ihandle, col int) int { func onSort(ih iup.Ihandle, col int) int {
n := len(files) n := len(files)
if n < 2 { if n < 2 {
@@ -73,16 +74,49 @@ func onSort(ih iup.Ihandle, col int) int {
return iup.DEFAULT return iup.DEFAULT
} }
// appendFile adds a file as a new row to the table and the files slice. // addFiles appends files, natural-sorts the list, and rebuilds the table.
func appendFile(file cbconvert.File) { func addFiles(fs []cbconvert.File) {
files = append(files, file) if len(fs) == 0 {
return
}
wasEmpty := len(files) == 0
var selPath string
if index >= 0 && index < len(files) {
selPath = files[index].Path
}
files = append(files, fs...)
sort.Slice(files, func(i, j int) bool {
return sortorder.NaturalLess(files[i].Name, files[j].Name)
})
t := iup.GetHandle("Table") t := iup.GetHandle("Table")
lin := len(files) t.SetAttribute("NUMLIN", strconv.Itoa(len(files)))
t.SetAttribute("NUMLIN", strconv.Itoa(lin)) for i, f := range files {
iup.SetAttributeId2(t, "", lin, 1, file.Name) lin := i + 1
iup.SetAttributeId2(t, "", lin, 2, cbconvert.FileType(file.Path)) iup.SetAttributeId2(t, "", lin, 1, f.Name)
iup.SetAttributeId2(t, "", lin, 3, strconv.FormatFloat(float64(file.Stat.Size())/(1024*1024), 'f', 2, 64)) iup.SetAttributeId2(t, "", lin, 2, cbconvert.FileType(f.Path))
iup.SetAttributeId2(t, "", lin, 3, strconv.FormatFloat(float64(f.Stat.Size())/(1024*1024), 'f', 2, 64))
}
if wasEmpty {
selectRow(0)
setActive()
previewPost()
return
}
index = -1
for i, f := range files {
if f.Path == selPath {
selectRow(i)
break
}
}
setActive()
} }
func previewPost() { func previewPost() {
@@ -90,30 +124,77 @@ func previewPost() {
return return
} }
width, height := previewSize() file := files[index]
iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES")
if strings.ToLower(iup.GetGlobal("DRIVER")) == "motif" { // On a new file, fetch the count first; the Page POSTMESSAGE handler clamps previewPage and renders.
iup.GetHandle("Preview").SetAttribute("IMAGE", "") if file.Path != previewPath {
previewPath = file.Path
iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES")
go pageCountPost(file)
return
} }
previewRender()
}
// previewRenderSize is the size the cover is rendered at.
const previewRenderSize = 1200
// previewRender renders the current file at previewPage off the UI thread.
func previewRender() {
if index == -1 || len(files) == 0 {
return
}
file := files[index]
iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES")
opts := options() opts := options()
page := previewPage
go func(opts cbconvert.Options) { go func(opts cbconvert.Options) {
conv := cbconvert.New(opts) conv := cbconvert.New(opts)
var s string var s string
file := files[index]
img, err := conv.Preview(file.Path, file.Stat, width, height) img, err := conv.PreviewPage(file.Path, file.Stat, page, previewRenderSize, previewRenderSize)
if err != nil { if err != nil {
s = err.Error() s = err.Error()
fmt.Println(err) fmt.Println(err)
} }
iup.PostMessage(iup.GetHandle("Preview"), s, 0, img) iup.PostMessage(iup.GetHandle("Preview"), s, page, img)
}(opts) }(opts)
} }
// pageCountPost computes the file's page count off the UI thread and posts it, tagged with the path, to the Page spin.
func pageCountPost(file cbconvert.File) {
n, err := cbconvert.New(cbconvert.NewOptions()).PageCount(file.Path, file.Stat)
if err != nil || n < 1 {
n = 1
}
iup.PostMessage(iup.GetHandle("Page"), file.Path, n, nil)
}
// onPageChanged re-renders the preview for the spin's page; dedupes so spin and typing don't both fire.
func onPageChanged() int {
page := iup.GetHandle("Page").GetInt("VALUE") - 1
if page < 0 {
page = 0
}
if page == previewPage {
return iup.DEFAULT
}
previewPage = page
previewRender()
return iup.DEFAULT
}
func onAddFiles(ih iup.Ihandle) int { func onAddFiles(ih iup.Ihandle) int {
args, err := fileDlg("Add Files", true, false, inputDirKey) args, err := fileDlg("Add Files", true, false, inputDirKey)
if err != nil { if err != nil {
@@ -134,21 +215,7 @@ func onAddFiles(ih iup.Ihandle) int {
return iup.DEFAULT return iup.DEFAULT
} }
wasEmpty := len(files) == 0 addFiles(fs)
for _, file := range fs {
appendFile(file)
}
if wasEmpty && len(files) > 0 {
selectRow(0)
}
setActive()
if wasEmpty {
previewPost()
}
} }
return iup.DEFAULT return iup.DEFAULT
@@ -174,21 +241,7 @@ func onAddDir(ih iup.Ihandle) int {
return iup.DEFAULT return iup.DEFAULT
} }
wasEmpty := len(files) == 0 addFiles(fs)
for _, file := range fs {
appendFile(file)
}
if wasEmpty && len(files) > 0 {
selectRow(0)
}
setActive()
if wasEmpty {
previewPost()
}
} }
return iup.DEFAULT return iup.DEFAULT
@@ -207,16 +260,31 @@ func onRemove(ih iup.Ihandle) int {
} }
setActive() setActive()
previewPost() if len(files) == 0 {
clearPreview()
} else {
previewPost()
}
return iup.DEFAULT return iup.DEFAULT
} }
// clearPreview resets the preview state and repaints the canvas to its empty state.
func clearPreview() {
hasCover = false
previewPath = ""
previewPage = 0
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
iup.Update(iup.GetHandle("Preview"))
}
func onRemoveAll(ih iup.Ihandle) int { func onRemoveAll(ih iup.Ihandle) int {
index = -1 index = -1
files = make([]cbconvert.File, 0) files = make([]cbconvert.File, 0)
iup.GetHandle("Table").SetAttribute("NUMLIN", "0") iup.GetHandle("Table").SetAttribute("NUMLIN", "0")
clearPreview()
setActive() setActive()
return iup.DEFAULT return iup.DEFAULT
+5 -10
View File
@@ -33,6 +33,10 @@ var (
activeConv *cbconvert.Converter activeConv *cbconvert.Converter
busy bool busy bool
previewPage int // 0-based page shown in the preview
previewPath string // path of the file whose page range is loaded
hasCover bool // whether a cover image is loaded for the preview canvas
) )
func init() { func init() {
@@ -78,7 +82,7 @@ func main() {
img, _ := png.Decode(bytes.NewReader(appLogo)) img, _ := png.Decode(bytes.NewReader(appLogo))
iup.ImageFromImage(img).SetHandle("logo") iup.ImageFromImage(img).SetHandle("logo")
dlg := iup.Dialog(layout()).SetAttributes(fmt.Sprintf(`TITLE="CBconvert %s", ICON=logo, SHRINK=YES`, appVersion)).SetHandle("dlg") dlg := iup.Dialog(layout()).SetAttributes(fmt.Sprintf(`TITLE="CBconvert %s", ICON=logo`, appVersion)).SetHandle("dlg")
dlg.SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int { dlg.SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
sp := strings.Split(s, ": ") sp := strings.Split(s, ": ")
@@ -89,15 +93,6 @@ func main() {
return iup.DEFAULT return iup.DEFAULT
})) }))
dlg.SetCallback("RESIZE_CB", iup.ResizeFunc(func(ih iup.Ihandle, width, height int) int {
iup.GetHandle("Preview").SetAttribute("IMAGE", "logo")
iup.Refresh(ih)
previewPost()
return iup.DEFAULT
}))
dlg.SetCallback("THEMECHANGED_CB", iup.ThemeChangedFunc(func(ih iup.Ihandle, darkMode int) int { dlg.SetCallback("THEMECHANGED_CB", iup.ThemeChangedFunc(func(ih iup.Ihandle, darkMode int) int {
t := iup.GetHandle("Table") t := iup.GetHandle("Table")
tableRowColors(t, darkMode == 1) tableRowColors(t, darkMode == 1)
+6
View File
@@ -10,6 +10,12 @@ func setActive() {
opts := options() opts := options()
count := iup.GetHandle("Table").GetInt("NUMLIN") count := iup.GetHandle("Table").GetInt("NUMLIN")
if count > 0 && index != -1 {
iup.GetHandle("PageBox").SetAttribute("VISIBLE", "YES")
} else {
iup.GetHandle("PageBox").SetAttribute("VISIBLE", "NO")
}
if count == 0 { if count == 0 {
iup.GetHandle("Remove").SetAttribute("ACTIVE", "NO") iup.GetHandle("Remove").SetAttribute("ACTIVE", "NO")
iup.GetHandle("RemoveAll").SetAttribute("ACTIVE", "NO") iup.GetHandle("RemoveAll").SetAttribute("ACTIVE", "NO")
+105 -51
View File
@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"image/gif" "image/gif"
"math"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
@@ -58,7 +59,7 @@ func list() iup.Ihandle {
"TITLE1": "Title", "TITLE1": "Title",
"TITLE2": "Type", "TITLE2": "Type",
"TITLE3": "Size (MiB)", "TITLE3": "Size (MiB)",
"WIDTH1": "150", "WIDTH1": "300",
"WIDTH2": "50", "WIDTH2": "50",
"WIDTH3": "100", "WIDTH3": "100",
"ALIGNMENT2": "ACENTER", "ALIGNMENT2": "ACENTER",
@@ -102,21 +103,7 @@ func list() iup.Ihandle {
return iup.DEFAULT return iup.DEFAULT
} }
wasEmpty := len(files) == 0 addFiles(fs)
for _, file := range fs {
appendFile(file)
}
if wasEmpty && len(files) > 0 {
selectRow(0)
}
setActive()
if wasEmpty {
previewPost()
}
return iup.DEFAULT return iup.DEFAULT
})) }))
@@ -124,49 +111,115 @@ func list() iup.Ihandle {
return iup.Vbox(t) return iup.Vbox(t)
} }
func previewSize() (int, int) {
var width, height int
sp := strings.Split(iup.GetHandle("Preview").GetAttribute("RASTERSIZE"), "x")
if len(sp) == 2 {
width, _ = strconv.Atoi(sp[0])
height, _ = strconv.Atoi(sp[1])
}
return width, height
}
func preview() iup.Ihandle { func preview() iup.Ihandle {
return iup.Frame( return iup.Frame(
iup.Vbox( iup.Vbox(
iup.Label("").SetAttributes("EXPAND=YES, ALIGNMENT=ACENTER, MINSIZE=400x, IMAGE=cover").SetHandle("Preview"). iup.Canvas().SetAttributes("EXPAND=YES, MINSIZE=400x, BORDER=NO").SetHandle("Preview").
SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int { SetCallback("ACTION", iup.ActionFunc(drawPreview)).
img := p.(cbconvert.Image) SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(previewMessage)),
iup.GetHandle("Loading").SetAttributes("VISIBLE=NO, STOP=YES")
if img.Image != nil && len(s) == 0 {
iup.Destroy(iup.GetHandle("cover"))
iup.ImageFromImage(img.Image).SetHandle("cover")
ih.SetAttribute("IMAGE", "cover")
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", fmt.Sprintf("%s (%dx%d)", img.SizeHuman, img.Width, img.Height))
} else {
ih.SetAttribute("IMAGE", "logo")
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
sp := strings.Split(s, ": ")
if len(sp) > 1 {
iup.MessageError(ih, fmt.Sprintf("%s\n\n%s", sp[0], strings.Join(sp[1:], ": ")))
}
}
return iup.DEFAULT
})),
iup.Label("").SetAttributes("EXPAND=HORIZONTAL, ALIGNMENT=ACENTER").SetHandle("PreviewInfo"), iup.Label("").SetAttributes("EXPAND=HORIZONTAL, ALIGNMENT=ACENTER").SetHandle("PreviewInfo"),
), ),
) )
} }
// drawPreview draws the cover scaled to fit, else the logo centered.
func drawPreview(ih iup.Ihandle) int {
iup.DrawBegin(ih)
defer iup.DrawEnd(ih)
cw, ch := iup.DrawGetSize(ih)
iup.DrawParentBackground(ih)
name := "logo"
if hasCover {
name = "cover"
}
iw, ihh, _ := iup.DrawGetImageInfo(name)
if iw <= 0 || ihh <= 0 {
return iup.DEFAULT
}
dw, dh := iw, ihh
if hasCover {
s := math.Min(float64(cw)/float64(iw), float64(ch)/float64(ihh))
dw = int(float64(iw) * s)
dh = int(float64(ihh) * s)
}
iup.DrawImage(ih, name, (cw-dw)/2, (ch-dh)/2, dw, dh)
return iup.DEFAULT
}
// previewMessage receives a rendered cover from previewRender and triggers a canvas redraw.
func previewMessage(ih iup.Ihandle, s string, i int, p any) int {
if i != previewPage {
return iup.DEFAULT
}
img := p.(cbconvert.Image)
iup.GetHandle("Loading").SetAttributes("VISIBLE=NO, STOP=YES")
if img.Image != nil && len(s) == 0 {
iup.Destroy(iup.GetHandle("cover"))
iup.ImageFromImage(img.Image).SetHandle("cover")
hasCover = true
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", fmt.Sprintf("%s (%dx%d)", img.SizeHuman, img.Width, img.Height))
} else {
hasCover = false
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
sp := strings.Split(s, ": ")
if len(sp) > 1 {
iup.MessageError(ih, fmt.Sprintf("%s\n\n%s", sp[0], strings.Join(sp[1:], ": ")))
}
}
iup.Update(ih)
return iup.DEFAULT
}
// pageBox is the page-navigation spin shown in the status bar; hidden until a comic is selected.
func pageBox() iup.Ihandle {
return iup.Hbox(
iup.Space().SetAttribute("SIZE", "5"),
iup.Label("Page:"),
iup.Space().SetAttribute("SIZE", "3"),
iup.Text().SetAttributes(`SPIN=YES, SPINMIN=1, SPINMAX=1, VALUE=1, VISIBLECOLUMNS=3, MASK="/d*"`).SetHandle("Page").
SetAttribute("TIP", "Preview a different page of the selected comic").
SetCallback("SPIN_CB", iup.SpinFunc(func(ih iup.Ihandle, pos int) int {
return onPageChanged()
})).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
return onPageChanged()
})).
SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
if s != previewPath {
return iup.DEFAULT
}
ih.SetAttribute("SPINMAX", strconv.Itoa(i))
iup.GetHandle("PageCount").SetAttribute("TITLE", fmt.Sprintf("/ %d", i))
if previewPage > i-1 {
previewPage = i - 1
}
if previewPage < 0 {
previewPage = 0
}
ih.SetAttribute("VALUE", strconv.Itoa(previewPage+1))
previewRender()
return iup.DEFAULT
})),
iup.Space().SetAttribute("SIZE", "3"),
iup.Label("").SetHandle("PageCount"),
).SetAttributes("ALIGNMENT=ACENTER, VISIBLE=NO").SetHandle("PageBox")
}
func tabInput() iup.Ihandle { func tabInput() iup.Ihandle {
return iup.Hbox( return iup.Hbox(
iup.Vbox( iup.Vbox(
@@ -586,6 +639,7 @@ func buttons() iup.Ihandle {
func status() iup.Ihandle { func status() iup.Ihandle {
return iup.Hbox( return iup.Hbox(
loading(), loading(),
pageBox(),
iup.Fill(), iup.Fill(),
iup.Label("File 1 of 1").SetHandle("LabelStatus1").SetAttributes("VISIBLE=NO"), iup.Label("File 1 of 1").SetHandle("LabelStatus1").SetAttributes("VISIBLE=NO"),
iup.Space().SetAttribute("SIZE", "5"), iup.Space().SetAttribute("SIZE", "5"),