From e55c5de52237ca026a2d99cbf5fc141f348a56b0 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Fri, 10 Nov 2023 16:39:41 +1300 Subject: [PATCH] Optionally preserve exif data --- exifcopy.go | 171 +++++++++++++++++++++++++++++++++++++++++++++++++++ goptimize.go | 27 ++++++-- main.go | 2 + 3 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 exifcopy.go diff --git a/exifcopy.go b/exifcopy.go new file mode 100644 index 0000000..8285782 --- /dev/null +++ b/exifcopy.go @@ -0,0 +1,171 @@ +// https://stackoverflow.com/a/76779756 +package main + +import ( + "bufio" + "errors" + "io" + "os" +) + +const ( + soi = 0xD8 + eoi = 0xD9 + sos = 0xDA + exif = 0xE1 + copyright = 0xEE + comment = 0xFE +) + +func isMetaTagType(tagType byte) bool { + // Adapt as needed + return tagType == exif || tagType == copyright || tagType == comment +} + +func copySegments(dst *bufio.Writer, src *bufio.Reader, filterSegment func(tagType byte) bool) error { + var buf [2]byte + _, err := io.ReadFull(src, buf[:]) + if err != nil { + return err + } + if buf != [2]byte{0xFF, soi} { + return errors.New("expected SOI") + } + for { + _, err := io.ReadFull(src, buf[:]) + if err != nil { + return err + } + if buf[0] != 0xFF { + return errors.New("invalid tag type") + } + if buf[1] == eoi { + // Hacky way to check for EOF + _, err := src.Read(buf[:1]) + if err != nil && err != io.EOF { + return err + } + // don't return an error as some cameras add the exif data at the end. + // if n > 0 { + // return errors.New("EOF expected after EOI") + // } + return nil + } + sos := buf[1] == 0xDA + filter := filterSegment(buf[1]) + if filter { + _, err = dst.Write(buf[:]) + if err != nil { + return err + } + } + + _, err = io.ReadFull(src, buf[:]) + if err != nil { + return err + } + if filter { + _, err = dst.Write(buf[:]) + if err != nil { + return err + } + } + + // Note: Includes the length, but not the tag, so subtract 2 + tagLength := ((uint16(buf[0]) << 8) | uint16(buf[1])) - 2 + if filter { + _, err = io.CopyN(dst, src, int64(tagLength)) + } else { + _, err = src.Discard(int(tagLength)) + } + if err != nil { + return err + } + if sos { + // Find next tag `FF xx` in the stream where `xx != 0` to skip ECS + // See https://stackoverflow.com/questions/2467137/parsing-jpeg-file-format-format-of-entropy-coded-segments-ecs + for { + bytes, err := src.Peek(2) + if err != nil { + return err + } + if bytes[0] == 0xFF { + data, rstMrk := bytes[1] == 0, bytes[1] >= 0xD0 && bytes[1] <= 0xD7 + if !data && !rstMrk { + break + } + } + if filter { + err = dst.WriteByte(bytes[0]) + if err != nil { + return err + } + } + _, err = src.Discard(1) + if err != nil { + return err + } + } + } + } +} + +func copyMetadata(outImagePath, imagePath, metadataImagePath string) error { + outFile, err := os.Create(outImagePath) + if err != nil { + return err + } + defer outFile.Close() + writer := bufio.NewWriter(outFile) + + imageFile, err := os.Open(imagePath) + if err != nil { + return err + } + defer imageFile.Close() + imageReader := bufio.NewReader(imageFile) + + metaFile, err := os.Open(metadataImagePath) + if err != nil { + return err + } + defer metaFile.Close() + metaReader := bufio.NewReader(metaFile) + + _, err = writer.Write([]byte{0xFF, soi}) + if err != nil { + return err + } + + // Copy metadata segments + // It seems that they need to come first! + err = copySegments(writer, metaReader, isMetaTagType) + if err != nil { + return err + } + // Copy all non-metadata segments + err = copySegments(writer, imageReader, func(tagType byte) bool { + return !isMetaTagType(tagType) + }) + if err != nil { + return err + } + + _, err = writer.Write([]byte{0xFF, eoi}) + if err != nil { + return err + } + + // Flush the writer, otherwise the last couple buffered writes (including the EOI) won't get written! + return writer.Flush() +} + +func exifCopy(fromPath, toPath string) error { + copyPath := toPath + "~" + err := os.Rename(toPath, copyPath) + if err != nil { + return err + } + defer os.Remove(copyPath) + return copyMetadata(toPath, copyPath, fromPath) +} diff --git a/goptimize.go b/goptimize.go index e7e2ee2..680549c 100644 --- a/goptimize.go +++ b/goptimize.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "image" "image/gif" "image/jpeg" "image/png" @@ -30,12 +31,21 @@ func Goptimize(file string) { return } - // open original, rotate if necessary - src, err := imaging.Open(file, imaging.AutoOrientation(true)) + var src image.Image - if err != nil { - fmt.Printf("Error: %v (%s)\n", err, file) - return + if !copyExif { + // rotate if necessary + src, err = imaging.Open(file, imaging.AutoOrientation(true)) + if err != nil { + fmt.Printf("Error: %v (%s)\n", err, file) + return + } + } else { + src, err = imaging.Open(file) + if err != nil { + fmt.Printf("Error: %v (%s)\n", err, file) + return + } } format, err := imaging.FormatFromFilename(file) @@ -127,6 +137,13 @@ func Goptimize(file string) { } else if jpegoptim != "" { RunOptimizer(tmpFilename, false, jpegoptim, "-f", "-s", "-o") } + + if copyExif { + if err := exifCopy(file, tmpFilename); err != nil { + fmt.Printf("Error copying exif data: %v (%s)\n", err, file) + return + } + } } else if format.String() == "PNG" { if pngquant != "" { RunOptimizer(tmpFilename, true, pngquant, "-f", "--output") diff --git a/main.go b/main.go index f2dca87..23eb3e6 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,7 @@ var ( optipng string pngquant string gifsicle string + copyExif bool threads = 1 version = "dev" ) @@ -60,6 +61,7 @@ func main() { flag.StringVarP(&maxSizes, "max", "m", "", "downscale to a maximum width & height in pixels (x)") flag.StringVarP(&outputDir, "out", "o", "", "output directory (default overwrites original)") flag.BoolVarP(&preserveModTimes, "preserve", "p", true, "preserve file modification times") + flag.BoolVarP(©Exif, "exif", "e", false, "copy exif data") flag.BoolVarP(&update, "update", "u", false, "update to latest release") flag.BoolVarP(&multiThreaded, "threaded", "t", false, "run multi-threaded (use all CPU cores)") flag.BoolVarP(&showversion, "version", "v", false, "show version number")