From e55c5de52237ca026a2d99cbf5fc141f348a56b0 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Fri, 10 Nov 2023 16:39:41 +1300 Subject: [PATCH 1/3] 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") From 6956f5fb08ace7c512a69a1ca1a6320236a920ce Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Fri, 10 Nov 2023 16:39:53 +1300 Subject: [PATCH 2/3] Update Go modules --- go.mod | 2 +- go.sum | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 19e60f3..9198cd2 100644 --- a/go.mod +++ b/go.mod @@ -6,5 +6,5 @@ require ( github.com/axllent/ghru v1.2.1 github.com/disintegration/imaging v1.6.2 github.com/spf13/pflag v1.0.5 - golang.org/x/image v0.13.0 + golang.org/x/image v0.14.0 ) diff --git a/go.sum b/go.sum index fe1b08a..4635347 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg= -golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= +golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= +golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -34,7 +34,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From e8f5085a11a1881863468ace8eeb4ca8508328c8 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Fri, 10 Nov 2023 16:40:00 +1300 Subject: [PATCH 3/3] 0.2.2 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e33ee6a..cb57f81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.2.2] + +- Optionally preserve exif data for supported formats +- Update Go modules + + ## [0.2.1] - Update core modules