package format import ( "bytes" "fmt" "io" "os/exec" "path/filepath" "strconv" "strings" ) // Magick reads metadata with exiftool and resizes with ImageMagick. type Magick struct { name string exts []string } func NewMagick(name string, exts ...string) *Magick { return &Magick{name: name, exts: exts} } func (m *Magick) Name() string { return m.name } func (m *Magick) Match(path string) bool { return hasExt(path, m.exts) } func (m *Magick) Kind() Kind { return KindImage } func (m *Magick) Metadata(path string) (Meta, error) { // exiftool's argfile strips leading spaces and skips "#" lines, and a // leading "-" would be read as an option; an absolute path avoids all three. path, err := filepath.Abs(path) if err != nil { return Meta{}, err } out, err := sharedExiftool.run("-json", "-n", "-ImageWidth", "-ImageHeight", "-Orientation", "-DateTimeOriginal", "-CreateDate", "-Make", "-Model", "-LensModel", "-FNumber", "-ExposureTime", "-ISO", path) if err != nil { return Meta{}, fmt.Errorf("exiftool %s: %w", path, err) } return parseExif(out) } func (m *Magick) Resize(src string, width int, dst io.Writer) error { cmd := exec.Command("magick", resizeArgs(src, width)...) var stderr bytes.Buffer cmd.Stdout = dst cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return fmt.Errorf("magick %s: %w: %s", src, err, strings.TrimSpace(stderr.String())) } return nil } // resizeArgs caps ImageMagick's pixel cache so several concurrent resizes of // large sources cannot exhaust memory. func resizeArgs(src string, width int) []string { return []string{ "-limit", "memory", "512MiB", "-limit", "map", "1GiB", src, "-auto-orient", "-resize", strconv.Itoa(width) + "x", "-strip", "-quality", "82", "jpg:-", } } // Default returns the formats supported in v1. func Default() *Registry { r := &Registry{} r.Register(NewMagick("jpeg", ".jpg", ".jpeg")) r.Register(NewMagick("png", ".png")) return r } // CheckTools reports the first required external tool missing from PATH. func CheckTools() error { for _, tool := range []string{"magick", "exiftool"} { if _, err := exec.LookPath(tool); err != nil { return fmt.Errorf("%s not found on PATH", tool) } } return nil }