Add version and preview

This commit is contained in:
Milan Nikolic
2023-09-02 09:18:47 +02:00
parent 7dc21fc0b4
commit b1fcf530da
2 changed files with 180 additions and 46 deletions

View File

@@ -6,6 +6,8 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/md5" "crypto/md5"
"crypto/rand"
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@@ -25,7 +27,7 @@ import (
"image/png" "image/png"
"github.com/chai2010/webp" "github.com/chai2010/webp"
_ "github.com/hotei/bmp" // allow bmp decoding _ "github.com/hotei/bmp" // allow 4-bit bmp decoding
"github.com/strukturag/libheif/go/heif" "github.com/strukturag/libheif/go/heif"
"golang.org/x/image/tiff" "golang.org/x/image/tiff"
@@ -100,6 +102,8 @@ type Options struct {
Thumbnail bool Thumbnail bool
// CBZ metadata // CBZ metadata
Meta bool Meta bool
// Version
Version bool
// ZIP comment // ZIP comment
Comment bool Comment bool
// ZIP comment body // ZIP comment body
@@ -166,7 +170,15 @@ type Convertor struct {
type File struct { type File struct {
Name string Name string
Path string Path string
Size int64 Stat os.FileInfo
SizeHuman string
}
// Image type.
type Image struct {
Image image.Image
Width int
Height int
SizeHuman string SizeHuman string
} }
@@ -290,9 +302,10 @@ func (c *Convertor) convertArchive(fileName string) error {
var img image.Image var img image.Image
img, err = c.imageDecode(bytes.NewReader(data), pathName) img, err = c.imageDecode(bytes.NewReader(data), pathName)
if err != nil { if err != nil {
e := err
img, err = c.imDecode(bytes.NewReader(data), pathName) img, err = c.imDecode(bytes.NewReader(data), pathName)
if err != nil { if err != nil {
return fmt.Errorf("convertArchive: %w", err) return fmt.Errorf("convertArchive: %w: %w", e, err)
} }
} }
@@ -391,9 +404,15 @@ func (c *Convertor) convertDirectory(dirPath string) error {
var i image.Image var i image.Image
i, err = c.imageDecode(file, img) i, err = c.imageDecode(file, img)
if err != nil { if err != nil {
e := err
_, err = file.Seek(0, io.SeekStart)
if err != nil {
return fmt.Errorf("convertDirectory: %w: %w", e, err)
}
i, err = c.imDecode(file, img) i, err = c.imDecode(file, img)
if err != nil { if err != nil {
return fmt.Errorf("convertDirectory: %w", err) return fmt.Errorf("convertDirectory: %w: %w", e, err)
} }
} }
@@ -517,6 +536,10 @@ func (c *Convertor) imageTransform(img image.Image) image.Image {
i = imaging.AdjustContrast(i, float64(c.Opts.Contrast)) i = imaging.AdjustContrast(i, float64(c.Opts.Contrast))
} }
if c.Opts.Grayscale {
i = imageToGray(i)
}
return i return i
} }
@@ -552,9 +575,10 @@ func (c *Convertor) imageLevel(img image.Image) (image.Image, error) {
var i image.Image var i image.Image
i, err = c.imageDecode(bytes.NewReader(blob), "levels") i, err = c.imageDecode(bytes.NewReader(blob), "levels")
if err != nil { if err != nil {
e := err
i, err = c.imDecode(bytes.NewReader(blob), "levels") i, err = c.imDecode(bytes.NewReader(blob), "levels")
if err != nil { if err != nil {
return nil, fmt.Errorf("imageLevel: %w", err) return nil, fmt.Errorf("imageLevel: %w: %w", e, err)
} }
} }
@@ -622,10 +646,6 @@ func (c *Convertor) imageEncode(img image.Image, fileName string) error {
} }
defer file.Close() defer file.Close()
if c.Opts.Grayscale {
img = imageToGray(img)
}
switch filepath.Ext(fileName) { switch filepath.Ext(fileName) {
case ".png": case ".png":
err = png.Encode(file, img) err = png.Encode(file, img)
@@ -667,12 +687,6 @@ func (c *Convertor) imEncode(i image.Image, fileName string) error {
return fmt.Errorf("imEncode: %w", err) return fmt.Errorf("imEncode: %w", err)
} }
if c.Opts.Grayscale {
if err := mw.TransformImageColorspace(imagick.COLORSPACE_GRAY); err != nil {
return fmt.Errorf("imEncode: %w", err)
}
}
switch filepath.Ext(fileName) { switch filepath.Ext(fileName) {
case ".png": case ".png":
if err := mw.SetImageFormat("PNG"); err != nil { if err := mw.SetImageFormat("PNG"); err != nil {
@@ -1187,9 +1201,10 @@ func (c *Convertor) coverArchive(fileName string) (image.Image, error) {
var img image.Image var img image.Image
img, err = c.imageDecode(bytes.NewReader(data), cover) img, err = c.imageDecode(bytes.NewReader(data), cover)
if err != nil { if err != nil {
e := err
img, err = c.imDecode(bytes.NewReader(data), cover) img, err = c.imDecode(bytes.NewReader(data), cover)
if err != nil { if err != nil {
return nil, fmt.Errorf("coverArchive: %w", err) return nil, fmt.Errorf("coverArchive: %w: %w", e, err)
} }
} }
@@ -1232,9 +1247,15 @@ func (c *Convertor) coverDirectory(dir string) (image.Image, error) {
var img image.Image var img image.Image
img, err = c.imageDecode(file, cover) img, err = c.imageDecode(file, cover)
if err != nil { if err != nil {
e := err
_, err = file.Seek(0, io.SeekStart)
if err != nil {
return nil, fmt.Errorf("coverDirectory: %w: %w", e, err)
}
img, err = c.imDecode(file, cover) img, err = c.imDecode(file, cover)
if err != nil { if err != nil {
return nil, fmt.Errorf("coverDirectory: %w", err) return nil, fmt.Errorf("coverDirectory: %w: %w", e, err)
} }
} }
@@ -1448,6 +1469,13 @@ func (c *Convertor) baseNoExt(filename string) string {
return strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename)) return strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
} }
// tempName generates a temporary name.
func (c *Convertor) tempName(prefix, suffix string) string {
randBytes := make([]byte, 16)
_, _ = rand.Read(randBytes)
return filepath.Join(os.TempDir(), prefix+hex.EncodeToString(randBytes)+suffix)
}
// copyFile copies reader to file. // copyFile copies reader to file.
func (c *Convertor) copyFile(reader io.Reader, filename string) error { func (c *Convertor) copyFile(reader io.Reader, filename string) error {
err := os.MkdirAll(filepath.Dir(filename), 0755) err := os.MkdirAll(filepath.Dir(filename), 0755)
@@ -1487,8 +1515,8 @@ func (c *Convertor) Files(args []string) ([]File, error) {
var file File var file File
file.Name = filepath.Base(fp) file.Name = filepath.Base(fp)
file.Path = fp file.Path = fp
file.Size = f.Size() file.Stat = f
file.SizeHuman = humanize.IBytes(uint64(file.Size)) file.SizeHuman = humanize.IBytes(uint64(f.Size()))
return file return file
} }
@@ -1630,10 +1658,10 @@ func (c *Convertor) Cover(fileName string, fileInfo os.FileInfo) error {
} }
// Thumbnail extracts thumbnail. // Thumbnail extracts thumbnail.
func (c *Convertor) Thumbnail(fileName string, info os.FileInfo) error { func (c *Convertor) Thumbnail(fileName string, fileInfo os.FileInfo) error {
c.CurrFile++ c.CurrFile++
cover, err := c.coverImage(fileName, info) cover, err := c.coverImage(fileName, fileInfo)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", fileName, err) return fmt.Errorf("%s: %w", fileName, err)
} }
@@ -1691,10 +1719,10 @@ func (c *Convertor) Thumbnail(fileName string, info os.FileInfo) error {
if err := mw.SetImageProperty("Thumb::URI", fURI); err != nil { if err := mw.SetImageProperty("Thumb::URI", fURI); err != nil {
return fmt.Errorf("%s: %w", fileName, err) return fmt.Errorf("%s: %w", fileName, err)
} }
if err := mw.SetImageProperty("Thumb::MTime", strconv.FormatInt(info.ModTime().Unix(), 10)); err != nil { if err := mw.SetImageProperty("Thumb::MTime", strconv.FormatInt(fileInfo.ModTime().Unix(), 10)); err != nil {
return fmt.Errorf("%s: %w", fileName, err) return fmt.Errorf("%s: %w", fileName, err)
} }
if err := mw.SetImageProperty("Thumb::Size", strconv.FormatInt(info.Size(), 10)); err != nil { if err := mw.SetImageProperty("Thumb::Size", strconv.FormatInt(fileInfo.Size(), 10)); err != nil {
return fmt.Errorf("%s: %w", fileName, err) return fmt.Errorf("%s: %w", fileName, err)
} }
@@ -1753,12 +1781,88 @@ func (c *Convertor) Meta(fileName string) (any, error) {
return "", nil return "", nil
} }
// Preview returns image preview.
func (c *Convertor) Preview(fileName string, fileInfo os.FileInfo, width, height int) (Image, error) {
var img Image
i, err := c.coverImage(fileName, fileInfo)
if err != nil {
return img, fmt.Errorf("%s: %w", fileName, err)
}
i = c.imageTransform(i)
if c.Opts.LevelsInMin != 0 || c.Opts.LevelsInMax != 255 || c.Opts.LevelsGamma != 1.0 ||
c.Opts.LevelsOutMin != 0 || c.Opts.LevelsOutMax != 255 {
i, err = c.imageLevel(i)
if err != nil {
return img, fmt.Errorf("%s: %w", fileName, err)
}
}
tmpName := c.tempName("cbc", "."+c.Opts.Format)
switch c.Opts.Format {
case "jpeg", "png", "tiff", "webp", "avif":
if err := c.imageEncode(i, tmpName); err != nil {
return img, fmt.Errorf("%s: %w", fileName, err)
}
case "bmp":
if err := c.imEncode(i, tmpName); err != nil {
return img, fmt.Errorf("%s: %w", fileName, err)
}
}
stat, err := os.Stat(tmpName)
if err != nil {
return img, fmt.Errorf("%s: %w", fileName, err)
}
img.Width = i.Bounds().Dx()
img.Height = i.Bounds().Dy()
img.SizeHuman = humanize.IBytes(uint64(stat.Size()))
f, err := os.Open(tmpName)
if err != nil {
return img, fmt.Errorf("%s: %w", fileName, err)
}
defer os.Remove(tmpName)
dec, err := c.imageDecode(f, tmpName)
if err != nil {
e := err
_, err = f.Seek(0, io.SeekStart)
if err != nil {
return img, fmt.Errorf("%s: %w: %w", tmpName, e, err)
}
dec, err = c.imDecode(f, tmpName)
if err != nil {
return img, fmt.Errorf("%s: %w: %w", tmpName, e, err)
}
}
err = f.Close()
if err != nil {
return img, fmt.Errorf("%s: %w", fileName, err)
}
if width != 0 && height != 0 {
dec = imaging.Fit(dec, width, height, filters[c.Opts.Filter])
}
img.Image = dec
return img, nil
}
// Convert converts comic book. // Convert converts comic book.
func (c *Convertor) Convert(fileName string, info os.FileInfo) error { func (c *Convertor) Convert(fileName string, fileInfo os.FileInfo) error {
c.CurrFile++ c.CurrFile++
switch { switch {
case info.IsDir(): case fileInfo.IsDir():
if err := c.convertDirectory(fileName); err != nil { if err := c.convertDirectory(fileName); err != nil {
return fmt.Errorf("%s: %w", fileName, err) return fmt.Errorf("%s: %w", fileName, err)
} }

View File

@@ -7,6 +7,7 @@ import (
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime/debug"
"syscall" "syscall"
"github.com/gen2brain/cbconvert" "github.com/gen2brain/cbconvert"
@@ -14,8 +15,43 @@ import (
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
) )
var appVersion string
func init() {
if appVersion != "" {
return
}
info, ok := debug.ReadBuildInfo()
if !ok {
return
}
if info.Main.Version != "" {
appVersion = info.Main.Version
}
for _, kv := range info.Settings {
if kv.Value == "" {
continue
}
if kv.Key == "vcs.revision" {
appVersion = kv.Value
if len(appVersion) > 10 {
appVersion = kv.Value[:10]
}
}
}
}
func main() { func main() {
opts, args := parseFlags() opts, args := parseFlags()
if opts.Version {
fmt.Println(filepath.Base(os.Args[0]), appVersion)
os.Exit(0)
}
conv := cbconvert.New(opts) conv := cbconvert.New(opts)
c := make(chan os.Signal, 2) c := make(chan os.Signal, 2)
@@ -83,12 +119,6 @@ func main() {
} }
for _, file := range files { for _, file := range files {
stat, err := os.Stat(file.Path)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
switch { switch {
case opts.Meta: case opts.Meta:
ret, err := conv.Meta(file.Path) ret, err := conv.Meta(file.Path)
@@ -105,14 +135,14 @@ func main() {
continue continue
case opts.Cover: case opts.Cover:
if err := conv.Cover(file.Path, stat); err != nil { if err := conv.Cover(file.Path, file.Stat); err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
continue continue
case opts.Thumbnail: case opts.Thumbnail:
if err = conv.Thumbnail(file.Path, stat); err != nil { if err = conv.Thumbnail(file.Path, file.Stat); err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
@@ -120,7 +150,7 @@ func main() {
continue continue
} }
if err := conv.Convert(file.Path, stat); err != nil { if err := conv.Convert(file.Path, file.Stat); err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
@@ -197,34 +227,32 @@ func parseFlags() (cbconvert.Options, []string) {
meta.StringVar(&opts.FileAdd, "file-add", "", "Add file to archive") meta.StringVar(&opts.FileAdd, "file-add", "", "Add file to archive")
meta.StringVar(&opts.FileRemove, "file-remove", "", "Remove file from archive (glob pattern, i.e. *.xml)") meta.StringVar(&opts.FileRemove, "file-remove", "", "Remove file from archive (glob pattern, i.e. *.xml)")
convert.Usage = func() { flag.NewFlagSet("version", flag.ExitOnError)
flag.Usage = func() {
_, _ = fmt.Fprintf(os.Stderr, "Usage: %s <command> [<flags>] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0])) _, _ = fmt.Fprintf(os.Stderr, "Usage: %s <command> [<flags>] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0]))
_, _ = fmt.Fprintf(os.Stderr, "\nCommands:\n") _, _ = fmt.Fprintf(os.Stderr, "\nCommands:\n")
_, _ = fmt.Fprintf(os.Stderr, "\n convert\n \tConvert archive or document\n\n") _, _ = fmt.Fprintf(os.Stderr, "\n convert\n \tConvert archive or document\n\n")
convert.VisitAll(func(f *flag.Flag) { convert.VisitAll(func(f *flag.Flag) {
_, _ = fmt.Fprintf(os.Stderr, " --%s", f.Name) _, _ = fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)
_, _ = fmt.Fprintf(os.Stderr, "\n \t")
_, _ = fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue) _, _ = fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue)
}) })
_, _ = fmt.Fprintf(os.Stderr, "\n cover\n \tExtract cover\n\n") _, _ = fmt.Fprintf(os.Stderr, "\n cover\n \tExtract cover\n\n")
cover.VisitAll(func(f *flag.Flag) { cover.VisitAll(func(f *flag.Flag) {
_, _ = fmt.Fprintf(os.Stderr, " --%s", f.Name) _, _ = fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)
_, _ = fmt.Fprintf(os.Stderr, "\n \t")
_, _ = fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue) _, _ = fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue)
}) })
_, _ = fmt.Fprintf(os.Stderr, "\n thumbnail\n \tExtract cover thumbnail (freedesktop spec.)\n\n") _, _ = fmt.Fprintf(os.Stderr, "\n thumbnail\n \tExtract cover thumbnail (freedesktop spec.)\n\n")
thumbnail.VisitAll(func(f *flag.Flag) { thumbnail.VisitAll(func(f *flag.Flag) {
_, _ = fmt.Fprintf(os.Stderr, " --%s", f.Name) _, _ = fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)
_, _ = fmt.Fprintf(os.Stderr, "\n \t")
_, _ = fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue) _, _ = fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue)
}) })
_, _ = fmt.Fprintf(os.Stderr, "\n meta\n \tCBZ metadata\n\n") _, _ = fmt.Fprintf(os.Stderr, "\n meta\n \tCBZ metadata\n\n")
meta.VisitAll(func(f *flag.Flag) { meta.VisitAll(func(f *flag.Flag) {
_, _ = fmt.Fprintf(os.Stderr, " --%s", f.Name) _, _ = fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)
_, _ = fmt.Fprintf(os.Stderr, "\n \t")
_, _ = fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue) _, _ = fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue)
}) })
_, _ = fmt.Fprintf(os.Stderr, "\n") _, _ = fmt.Fprintf(os.Stderr, "\n version\n \tPrint version\n\n")
} }
if len(os.Args) < 2 { if len(os.Args) < 2 {
@@ -262,10 +290,12 @@ func parseFlags() (cbconvert.Options, []string) {
if !pipe { if !pipe {
args = meta.Args() args = meta.Args()
} }
case "version":
opts.Version = true
} }
if len(args) == 0 { if len(args) == 0 && !opts.Version {
convert.Usage() flag.Usage()
_, _ = fmt.Fprintf(os.Stderr, "no arguments\n") _, _ = fmt.Fprintf(os.Stderr, "no arguments\n")
os.Exit(1) os.Exit(1)
} }