精简后的项目:仅保留 api/ui 源码、文档、构建依赖
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
package exiftool
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Exiftool launches an external `exiftool` process to query photos' exif info.
|
||||
// It doesn't support concurrency usage.
|
||||
type Exiftool struct {
|
||||
path string
|
||||
version string
|
||||
marker string
|
||||
|
||||
cmd *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
stdinBuf *bufio.Writer
|
||||
stdout *MarkReader
|
||||
stderr *MarkReader
|
||||
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
const marker = "{ready}\n"
|
||||
const bufferSize = 10240
|
||||
|
||||
// New returns a new instance of Exiftool.
|
||||
func New() (*Exiftool, error) {
|
||||
path, err := exec.LookPath("exiftool")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
voutput, err := exec.Command(path, "-ver").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("run `exiftool -ver` error: %w", err)
|
||||
}
|
||||
|
||||
version := string(bytes.Trim(voutput, " \t\n\r"))
|
||||
if version == "" {
|
||||
return nil, fmt.Errorf("run `exiftool -ver` error: no output")
|
||||
}
|
||||
|
||||
cmd := exec.Command(path, "-stay_open", "True", "-@", "-")
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get `exiftool -stay_open True -@ -` stdin error: %w", err)
|
||||
}
|
||||
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
defer stdin.Close()
|
||||
return nil, fmt.Errorf("get `exiftool -stay_open True -@ -` stdout error: %w", err)
|
||||
}
|
||||
|
||||
stdout, err := NewMarkReader(stdoutPipe, bufferSize, marker)
|
||||
if err != nil {
|
||||
defer stdin.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stderrPipe, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
defer stdin.Close()
|
||||
return nil, fmt.Errorf("get `exiftool -stay_open True -@ -` stderr error: %w", err)
|
||||
}
|
||||
|
||||
stderr, err := NewMarkReader(stderrPipe, bufferSize, marker)
|
||||
if err != nil {
|
||||
defer stdin.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
defer stdin.Close()
|
||||
return nil, fmt.Errorf("run `exiftool -stay_open True -@ -` error: %w", err)
|
||||
}
|
||||
|
||||
return &Exiftool{
|
||||
path: path,
|
||||
version: version,
|
||||
marker: marker,
|
||||
cmd: cmd,
|
||||
stdin: stdin,
|
||||
stdinBuf: bufio.NewWriterSize(stdin, bufferSize),
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BinaryPath returns the path of `exiftool` binary.
|
||||
func (e *Exiftool) BinaryPath() string {
|
||||
return e.path
|
||||
}
|
||||
|
||||
// Version returns the version of `exiftool` binary.
|
||||
func (e *Exiftool) Version() string {
|
||||
return e.version
|
||||
}
|
||||
|
||||
// Close stops the external process of `exiftool`.
|
||||
func (e *Exiftool) Close() (err error) {
|
||||
e.closeOnce.Do(func() {
|
||||
defer e.cmd.Wait()
|
||||
defer e.stdin.Close()
|
||||
|
||||
if err = e.rawSendCommand("-stay_open", "False"); err != nil {
|
||||
_ = e.cmd.Process.Kill()
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (e *Exiftool) rawSendCommand(args ...string) error {
|
||||
e.stdout.Reset()
|
||||
e.stderr.Reset()
|
||||
|
||||
for _, arg := range args {
|
||||
if _, err := e.stdinBuf.WriteString(arg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.stdinBuf.WriteByte('\n'); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, arg := range []string{"-echo4\n", "{ready}\n", "-execute\n"} {
|
||||
if _, err := e.stdinBuf.WriteString(arg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.stdinBuf.Flush()
|
||||
}
|
||||
|
||||
func (e *Exiftool) rawReadStderr() error {
|
||||
stderrOutput, err := io.ReadAll(e.stderr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outStr := string(bytes.Trim(stderrOutput, " \n\t\r"))
|
||||
if outStr == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New(outStr)
|
||||
}
|
||||
|
||||
func (e *Exiftool) rawGetTags(v any, args ...string) (err error) {
|
||||
if err = e.rawSendCommand(append(args, "-json")...); err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, e.stdout)
|
||||
err = e.rawReadStderr()
|
||||
}()
|
||||
|
||||
if err = json.NewDecoder(e.stdout).Decode(v); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Exiftool) rawSaveEmbedFile(outputPath string, args ...string) (hasEmbededFile bool, err error) {
|
||||
if err = e.rawSendCommand(append(args, "-b", "-W", outputPath)...); err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, e.stdout)
|
||||
err = e.rawReadStderr()
|
||||
}()
|
||||
|
||||
var output []byte
|
||||
if output, err = io.ReadAll(e.stdout); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
outStr := string(bytes.Trim(output, " \n\r\t"))
|
||||
switch outStr {
|
||||
case "0 output files created":
|
||||
return
|
||||
case "1 output files created":
|
||||
default:
|
||||
err = fmt.Errorf("invalid output: %s", outStr)
|
||||
return
|
||||
}
|
||||
|
||||
if err = e.rawReadStderr(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hasEmbededFile = true
|
||||
return
|
||||
}
|
||||
|
||||
func (e *Exiftool) rawUpdateFile(args ...string) (err error) {
|
||||
if err = e.rawSendCommand(args...); err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, e.stdout)
|
||||
err = e.rawReadStderr()
|
||||
}()
|
||||
|
||||
var output []byte
|
||||
if output, err = io.ReadAll(e.stdout); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
outStr := string(bytes.Trim(output, " \n\r\t"))
|
||||
if outStr != "1 image files updated" {
|
||||
err = fmt.Errorf("invalid output: %s", outStr)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// QueryJSONTagsByNumber queries the exif info of `file` with a given struct `value`. Tags are fields of the `value`. The values are a number if possible.
|
||||
// See values.go for example structs.
|
||||
func (e *Exiftool) QueryJSONTagsByNumber(file string, value any) error {
|
||||
rows := []any{value}
|
||||
if err := e.rawGetTags(&rows, "-n", file); err != nil {
|
||||
return fmt.Errorf("query %q tags error: %w", file, err)
|
||||
}
|
||||
|
||||
if len(rows) != 1 {
|
||||
return fmt.Errorf("query %q tags error: return %d responses, should be only 1", file, len(rows))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveJPEGPreview saves a preview jpeg from `src` to `previewOutput`.
|
||||
func (e *Exiftool) SaveJPEGPreview(src string, previewOutput string) (bool, error) {
|
||||
saved, err := e.rawSaveEmbedFile(previewOutput, "-JpgFromRaw", src)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("save jpeg preview for %q error: %w", src, err)
|
||||
}
|
||||
|
||||
if !saved {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err = e.rawUpdateFile("-TagsFromFile", src, "-overwrite_original", previewOutput); err != nil {
|
||||
return false, fmt.Errorf("save tags to jpeg preview for %q error: %w", src, err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
Reference in New Issue
Block a user