internal/format/magick.go
83 lines · 2263 bytes
1package format
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "os/exec"
8 "path/filepath"
9 "strconv"
10 "strings"
11)
12
13// Magick reads metadata with exiftool and resizes with ImageMagick.
14type Magick struct {
15 name string
16 exts []string
17}
18
19func NewMagick(name string, exts ...string) *Magick {
20 return &Magick{name: name, exts: exts}
21}
22
23func (m *Magick) Name() string { return m.name }
24func (m *Magick) Match(path string) bool { return hasExt(path, m.exts) }
25func (m *Magick) Kind() Kind { return KindImage }
26
27func (m *Magick) Metadata(path string) (Meta, error) {
28 // exiftool's argfile strips leading spaces and skips "#" lines, and a
29 // leading "-" would be read as an option; an absolute path avoids all three.
30 path, err := filepath.Abs(path)
31 if err != nil {
32 return Meta{}, err
33 }
34 out, err := sharedExiftool.run("-json", "-n",
35 "-ImageWidth", "-ImageHeight", "-Orientation",
36 "-DateTimeOriginal", "-CreateDate",
37 "-Make", "-Model", "-LensModel",
38 "-FNumber", "-ExposureTime", "-ISO",
39 path)
40 if err != nil {
41 return Meta{}, fmt.Errorf("exiftool %s: %w", path, err)
42 }
43 return parseExif(out)
44}
45
46func (m *Magick) Resize(src string, width int, dst io.Writer) error {
47 cmd := exec.Command("magick", resizeArgs(src, width)...)
48 var stderr bytes.Buffer
49 cmd.Stdout = dst
50 cmd.Stderr = &stderr
51 if err := cmd.Run(); err != nil {
52 return fmt.Errorf("magick %s: %w: %s", src, err, strings.TrimSpace(stderr.String()))
53 }
54 return nil
55}
56
57// resizeArgs caps ImageMagick's pixel cache so several concurrent resizes of
58// large sources cannot exhaust memory.
59func resizeArgs(src string, width int) []string {
60 return []string{
61 "-limit", "memory", "512MiB", "-limit", "map", "1GiB",
62 src, "-auto-orient", "-resize", strconv.Itoa(width) + "x",
63 "-strip", "-quality", "82", "jpg:-",
64 }
65}
66
67// Default returns the formats supported in v1.
68func Default() *Registry {
69 r := &Registry{}
70 r.Register(NewMagick("jpeg", ".jpg", ".jpeg"))
71 r.Register(NewMagick("png", ".png"))
72 return r
73}
74
75// CheckTools reports the first required external tool missing from PATH.
76func CheckTools() error {
77 for _, tool := range []string{"magick", "exiftool"} {
78 if _, err := exec.LookPath(tool); err != nil {
79 return fmt.Errorf("%s not found on PATH", tool)
80 }
81 }
82 return nil
83}