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}