mirror of
https://github.com/gen2brain/cbconvert
synced 2026-06-30 09:11:54 +02:00
Add page spin to preview other pages
This commit is contained in:
+23
-10
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
+114
-46
@@ -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]
|
||||||
|
|
||||||
|
// On a new file, fetch the count first; the Page POSTMESSAGE handler clamps previewPage and renders.
|
||||||
|
if file.Path != previewPath {
|
||||||
|
previewPath = file.Path
|
||||||
iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES")
|
iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES")
|
||||||
if strings.ToLower(iup.GetGlobal("DRIVER")) == "motif" {
|
go pageCountPost(file)
|
||||||
iup.GetHandle("Preview").SetAttribute("IMAGE", "")
|
|
||||||
|
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()
|
||||||
|
if len(files) == 0 {
|
||||||
|
clearPreview()
|
||||||
|
} else {
|
||||||
previewPost()
|
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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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,34 +111,63 @@ 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.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")
|
iup.GetHandle("Loading").SetAttributes("VISIBLE=NO, STOP=YES")
|
||||||
|
|
||||||
if img.Image != nil && len(s) == 0 {
|
if img.Image != nil && len(s) == 0 {
|
||||||
iup.Destroy(iup.GetHandle("cover"))
|
iup.Destroy(iup.GetHandle("cover"))
|
||||||
iup.ImageFromImage(img.Image).SetHandle("cover")
|
iup.ImageFromImage(img.Image).SetHandle("cover")
|
||||||
|
hasCover = true
|
||||||
ih.SetAttribute("IMAGE", "cover")
|
|
||||||
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", fmt.Sprintf("%s (%dx%d)", img.SizeHuman, img.Width, img.Height))
|
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", fmt.Sprintf("%s (%dx%d)", img.SizeHuman, img.Width, img.Height))
|
||||||
} else {
|
} else {
|
||||||
ih.SetAttribute("IMAGE", "logo")
|
hasCover = false
|
||||||
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
|
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
|
||||||
|
|
||||||
sp := strings.Split(s, ": ")
|
sp := strings.Split(s, ": ")
|
||||||
@@ -160,11 +176,48 @@ func preview() iup.Ihandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
return iup.DEFAULT
|
||||||
})),
|
})),
|
||||||
iup.Label("").SetAttributes("EXPAND=HORIZONTAL, ALIGNMENT=ACENTER").SetHandle("PreviewInfo"),
|
iup.Space().SetAttribute("SIZE", "3"),
|
||||||
),
|
iup.Label("").SetHandle("PageCount"),
|
||||||
)
|
).SetAttributes("ALIGNMENT=ACENTER, VISIBLE=NO").SetHandle("PageBox")
|
||||||
}
|
}
|
||||||
|
|
||||||
func tabInput() iup.Ihandle {
|
func tabInput() iup.Ihandle {
|
||||||
@@ -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"),
|
||||||
|
|||||||
Reference in New Issue
Block a user