mirror of
				https://github.com/axllent/goptimize.git
				synced 2025-11-04 03:48:28 -05:00 
			
		
		
		
	Compare commits
	
		
			34 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					db2f3b8534 | ||
| 
						 | 
					b9e5afefd4 | ||
| 
						 | 
					e8f5085a11 | ||
| 
						 | 
					6956f5fb08 | ||
| 
						 | 
					e55c5de522 | ||
| 
						 | 
					75ca6b7017 | ||
| 
						 | 
					ea2d490686 | ||
| 
						 | 
					e8c7298c9c | ||
| 
						 | 
					bb18d90d0e | ||
| 
						 | 
					90240fef3b | ||
| 
						 | 
					2b1b34a4e0 | ||
| 
						 | 
					529ea6971c | ||
| 
						 | 
					4fa6fed8c9 | ||
| 
						 | 
					745bed1273 | ||
| 
						 | 
					d3cd263300 | ||
| 
						 | 
					88af2a0a0c | ||
| 
						 | 
					fbb22582d1 | ||
| 
						 | 
					adac20101d | ||
| 
						 | 
					521f3f24c1 | ||
| 
						 | 
					3f9f09880c | ||
| 
						 | 
					eadd535e4c | ||
| 
						 | 
					f9bf16d6c4 | ||
| 
						 | 
					6c1aabaa70 | ||
| 
						 | 
					8c3e478384 | ||
| 
						 | 
					7ffb944b0f | ||
| 
						 | 
					cb4bde8c4e | ||
| 
						 | 
					49d7b9b12f | ||
| 
						 | 
					fcdbf3e90b | ||
| 
						 | 
					5096e6fbf0 | ||
| 
						 | 
					d7d827fed7 | ||
| 
						 | 
					19cf54b633 | ||
| 
						 | 
					57b1d33129 | ||
| 
						 | 
					c239a54ead | ||
| 
						 | 
					e6296f4719 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,2 +1,2 @@
 | 
				
			|||||||
/dist/
 | 
					/dist/
 | 
				
			||||||
goptimize
 | 
					goptimize*
 | 
				
			||||||
							
								
								
									
										29
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,6 +1,33 @@
 | 
				
			|||||||
# Changelog
 | 
					# Changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## [dev]
 | 
					## [0.2.2]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Optionally preserve exif data for supported formats
 | 
				
			||||||
 | 
					- Update Go modules
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.2.1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Update core modules
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.2.0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Add threaded option (`-t`) to use all CPU cores
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.1.0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Switch to go mods - go (>= 1.11 required)
 | 
				
			||||||
 | 
					- Switch to axllent/semver for app updating
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.0.3]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Detect & skip animated GIFs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.0.2]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Switch to [pflag](https://github.com/spf13/pflag) for better flag management
 | 
					- Switch to [pflag](https://github.com/spf13/pflag) for better flag management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										6
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								Makefile
									
									
									
									
									
								
							@@ -5,8 +5,7 @@ LDFLAGS=-ldflags "-s -w -X main.version=${VERSION}"
 | 
				
			|||||||
build = echo "\n\nBuilding $(1)-$(2)" && GOOS=$(1) GOARCH=$(2) go build ${LDFLAGS} -o dist/goptimize_${VERSION}_$(1)_$(2) \
 | 
					build = echo "\n\nBuilding $(1)-$(2)" && GOOS=$(1) GOARCH=$(2) go build ${LDFLAGS} -o dist/goptimize_${VERSION}_$(1)_$(2) \
 | 
				
			||||||
	&& bzip2 dist/goptimize_${VERSION}_$(1)_$(2)
 | 
						&& bzip2 dist/goptimize_${VERSION}_$(1)_$(2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
goptimize: *.go
 | 
					goptimize: *.go go.*
 | 
				
			||||||
	go get github.com/disintegration/imaging golang.org/x/image/bmp golang.org/x/image/tiff github.com/axllent/gitrel github.com/spf13/pflag
 | 
					 | 
				
			||||||
	go build ${LDFLAGS} -o goptimize
 | 
						go build ${LDFLAGS} -o goptimize
 | 
				
			||||||
	rm -rf /tmp/go-*
 | 
						rm -rf /tmp/go-*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -16,10 +15,9 @@ clean:
 | 
				
			|||||||
release:
 | 
					release:
 | 
				
			||||||
	mkdir -p dist
 | 
						mkdir -p dist
 | 
				
			||||||
	rm -f dist/goptimize_${VERSION}_*
 | 
						rm -f dist/goptimize_${VERSION}_*
 | 
				
			||||||
	go get github.com/disintegration/imaging golang.org/x/image/bmp golang.org/x/image/tiff github.com/axllent/gitrel github.com/spf13/pflag
 | 
					 | 
				
			||||||
	$(call build,linux,amd64)
 | 
						$(call build,linux,amd64)
 | 
				
			||||||
	$(call build,linux,386)
 | 
						$(call build,linux,386)
 | 
				
			||||||
	$(call build,linux,arm)
 | 
						$(call build,linux,arm)
 | 
				
			||||||
	$(call build,linux,arm64)
 | 
						$(call build,linux,arm64)
 | 
				
			||||||
	$(call build,darwin,amd64)
 | 
						$(call build,darwin,amd64)
 | 
				
			||||||
	$(call build,darwin,386)
 | 
						$(call build,darwin,arm64)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										21
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								README.md
									
									
									
									
									
								
							@@ -1,11 +1,12 @@
 | 
				
			|||||||
# Goptimizer - downscales and optimizes images
 | 
					# Goptimizer - downscales and optimizes images
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Goptimizer is a commandline utility written in Golang. It downscales and optimizes JPEG, PNG and Gif files.
 | 
					[](https://goreportcard.com/report/github.com/axllent/goptimize)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Goptimizer is a commandline utility written in Golang. It downscales and optimizes JPEG, PNG, GIF, TIFF and BMP files.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Image downscaling/rotation is done within goptimize (`-m <width>x<height>`, see [Usage](#usage-options)), however optimization is done using the following additional tools (if they are installed):
 | 
					Image downscaling/rotation is done within goptimize (`-m <width>x<height>`, see [Usage](#usage-options)), however optimization is done using the following additional tools (if they are installed):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- jpegoptim
 | 
					- jpegtran (`libjpeg-turbo-progs`) or jpegoptim
 | 
				
			||||||
- jpegtran (`libjpeg-turbo-progs`)
 | 
					 | 
				
			||||||
- optipng
 | 
					- optipng
 | 
				
			||||||
- pngquant
 | 
					- pngquant
 | 
				
			||||||
- gifsicle
 | 
					- gifsicle
 | 
				
			||||||
@@ -13,14 +14,16 @@ Image downscaling/rotation is done within goptimize (`-m <width>x<height>`, see
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Notes
 | 
					## Notes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Both `jpegoptim` & `jpegtran` have almost identical optimization, so if both are installed then just `jpegtran` is used for JPG optimization. PNG optimization however will run through both `optipng` & `pngquant` (if installed) as this has definite advantages.
 | 
					Both `jpegoptim` & `jpegtran` have almost identical optimization, so if both are installed then just `jpegtran` is used for JPG optimization. PNG optimization however will run through both `optipng` & `pngquant` (if installed) as this can result in better optimization.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
It is highly recommended to install the necessary optimization tools, however they are not required to run goptimize.
 | 
					It is highly recommended to install the necessary optimization tools, however they are not required to run goptimize.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Goptimize will remove all exif data from JPEG files, auto-rotating those that relied on it.
 | 
					Goptimize will remove all exif data from JPEG files, auto-rotating those that depend on it for orientation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
It will also preserve (by default) the file's original modification times (`-p=false` to disable).
 | 
					It will also preserve (by default) the file's original modification times (`-p=false` to disable).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Animated GIF files are not supported and automatically get skipped.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Usage options
 | 
					## Usage options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -32,6 +35,7 @@ Options:
 | 
				
			|||||||
  -m, --max string         downscale to a maximum width & height in pixels (<width>x<height>)
 | 
					  -m, --max string         downscale to a maximum width & height in pixels (<width>x<height>)
 | 
				
			||||||
  -o, --out string         output directory (default overwrites original)
 | 
					  -o, --out string         output directory (default overwrites original)
 | 
				
			||||||
  -p, --preserve           preserve file modification times (default true)
 | 
					  -p, --preserve           preserve file modification times (default true)
 | 
				
			||||||
 | 
					  -t, --threaded           run multi-threaded (use all CPU cores)
 | 
				
			||||||
  -u, --update             update to latest release
 | 
					  -u, --update             update to latest release
 | 
				
			||||||
  -v, --version            show version number
 | 
					  -v, --version            show version number
 | 
				
			||||||
  -h, --help               show help
 | 
					  -h, --help               show help
 | 
				
			||||||
@@ -54,11 +58,16 @@ Options:
 | 
				
			|||||||
## Install
 | 
					## Install
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Download the appropriate binary from the [releases](https://github.com/axllent/goptimize/releases/latest), or if you have golang installed 
 | 
					Download the appropriate binary from the [releases](https://github.com/axllent/goptimize/releases/latest), or if you have golang installed 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Build requirements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Go >= 1.11 required.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
go get github.com/axllent/goptimize
 | 
					go get github.com/axllent/goptimize
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## TODO
 | 
					## TODO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Some ideas for the future:
 | 
					Some ideas for the future:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										171
									
								
								exifcopy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								exifcopy.go
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										10
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					module github.com/axllent/goptimize
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					go 1.13
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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.14.0
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										42
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					github.com/axllent/ghru v1.2.1 h1:PuJQQeILJJ42O9nvjTwObWQGZDyZ9/F71st9jNAqgoU=
 | 
				
			||||||
 | 
					github.com/axllent/ghru v1.2.1/go.mod h1:YgznIILRJpnII5x8N080q/G8Milzk3sy9Sh4dV1eGQw=
 | 
				
			||||||
 | 
					github.com/axllent/semver v0.0.1 h1:QqF+KSGxgj8QZzSXAvKFqjGWE5792ksOnQhludToK8E=
 | 
				
			||||||
 | 
					github.com/axllent/semver v0.0.1/go.mod h1:2xSPzvG8n9mRfdtxSvWvfTfQGWfHsMsHO1iZnKATMSc=
 | 
				
			||||||
 | 
					github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
 | 
				
			||||||
 | 
					github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
 | 
				
			||||||
 | 
					github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 | 
				
			||||||
 | 
					github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 | 
				
			||||||
 | 
					github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 | 
				
			||||||
 | 
					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.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=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 | 
				
			||||||
 | 
					golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
 | 
					golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
				
			||||||
 | 
					golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 | 
				
			||||||
 | 
					golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 | 
				
			||||||
 | 
					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.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=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 | 
				
			||||||
 | 
					golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
				
			||||||
							
								
								
									
										88
									
								
								goptimize.go
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								goptimize.go
									
									
									
									
									
								
							@@ -2,11 +2,11 @@ package main
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"image"
 | 
				
			||||||
	"image/gif"
 | 
						"image/gif"
 | 
				
			||||||
	"image/jpeg"
 | 
						"image/jpeg"
 | 
				
			||||||
	"image/png"
 | 
						"image/png"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"io/ioutil"
 | 
					 | 
				
			||||||
	"math"
 | 
						"math"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
						"os/exec"
 | 
				
			||||||
@@ -22,31 +22,47 @@ func Goptimize(file string) {
 | 
				
			|||||||
	info, err := os.Stat(file)
 | 
						info, err := os.Stat(file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		fmt.Printf("%s doesn't exist\n", file)
 | 
							fmt.Printf("Error: %s doesn't exist\n", file)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !info.Mode().IsRegular() {
 | 
						if !info.Mode().IsRegular() {
 | 
				
			||||||
		// not a file
 | 
							fmt.Printf("Error: %s is not a file\n", file)
 | 
				
			||||||
		fmt.Printf("%s is not a file\n", file)
 | 
					 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// open original, rotate if necessary
 | 
						var src image.Image
 | 
				
			||||||
	src, err := imaging.Open(file, imaging.AutoOrientation(true))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err != nil {
 | 
						if !copyExif {
 | 
				
			||||||
		fmt.Printf("%v (%s)\n", err, file)
 | 
							// rotate if necessary
 | 
				
			||||||
		return
 | 
							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)
 | 
						format, err := imaging.FormatFromFilename(file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		fmt.Printf("Cannot detect format: %v\n", err)
 | 
							fmt.Printf("Error: cannot detect format: %v\n", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if format.String() == "GIF" {
 | 
				
			||||||
 | 
							// return if GIF is animated - unsupported
 | 
				
			||||||
 | 
							if err := IsGIFAnimated(file); err != nil {
 | 
				
			||||||
 | 
								fmt.Printf("Error: animated GIF not supported (%v)\n", file)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	outFilename := filepath.Base(file)
 | 
						outFilename := filepath.Base(file)
 | 
				
			||||||
	outDir := filepath.Dir(file)
 | 
						outDir := filepath.Dir(file)
 | 
				
			||||||
	dstFile := filepath.Join(outDir, outFilename)
 | 
						dstFile := filepath.Join(outDir, outFilename)
 | 
				
			||||||
@@ -60,7 +76,7 @@ func Goptimize(file string) {
 | 
				
			|||||||
	srcW := srcBounds.Dx()
 | 
						srcW := srcBounds.Dx()
 | 
				
			||||||
	srcH := srcBounds.Dy()
 | 
						srcH := srcBounds.Dy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Ensure scaling does not upscale image
 | 
						// do not upscale image
 | 
				
			||||||
	imgMaxW := maxWidth
 | 
						imgMaxW := maxWidth
 | 
				
			||||||
	if imgMaxW == 0 || imgMaxW > srcW {
 | 
						if imgMaxW == 0 || imgMaxW > srcW {
 | 
				
			||||||
		imgMaxW = srcW
 | 
							imgMaxW = srcW
 | 
				
			||||||
@@ -76,27 +92,28 @@ func Goptimize(file string) {
 | 
				
			|||||||
	resultW := dstBounds.Dx()
 | 
						resultW := dstBounds.Dx()
 | 
				
			||||||
	resultH := dstBounds.Dy()
 | 
						resultH := dstBounds.Dy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	tmpFile, err := ioutil.TempFile(os.TempDir(), "Goptimized-")
 | 
						tmpFile, err := os.CreateTemp(os.TempDir(), "Goptimized-")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		fmt.Printf("Cannot create temporary file: %v\n", err)
 | 
							fmt.Printf("Error: cannot create temporary file: %v\n", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	defer os.Remove(tmpFile.Name())
 | 
						defer os.Remove(tmpFile.Name())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if format.String() == "JPEG" {
 | 
						switch imgType := format.String(); imgType {
 | 
				
			||||||
 | 
						case "JPEG":
 | 
				
			||||||
		err = jpeg.Encode(tmpFile, resized, &jpeg.Options{Quality: quality})
 | 
							err = jpeg.Encode(tmpFile, resized, &jpeg.Options{Quality: quality})
 | 
				
			||||||
	} else if format.String() == "PNG" {
 | 
						case "PNG":
 | 
				
			||||||
		err = png.Encode(tmpFile, resized)
 | 
							err = png.Encode(tmpFile, resized)
 | 
				
			||||||
	} else if format.String() == "GIF" {
 | 
						case "GIF":
 | 
				
			||||||
		err = gif.Encode(tmpFile, resized, nil)
 | 
							err = gif.Encode(tmpFile, resized, nil)
 | 
				
			||||||
	} else if format.String() == "TIFF" {
 | 
						case "TIFF":
 | 
				
			||||||
		err = tiff.Encode(tmpFile, resized, nil)
 | 
							err = tiff.Encode(tmpFile, resized, nil)
 | 
				
			||||||
	} else if format.String() == "BMP" {
 | 
						case "BMP":
 | 
				
			||||||
		err = bmp.Encode(tmpFile, resized)
 | 
							err = bmp.Encode(tmpFile, resized)
 | 
				
			||||||
	} else {
 | 
						default:
 | 
				
			||||||
		fmt.Printf("Unsupported file type %s\n", file)
 | 
							fmt.Printf("Error: unsupported file type (%s)\n", file)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -112,7 +129,7 @@ func Goptimize(file string) {
 | 
				
			|||||||
	// so we can modify it with system processes
 | 
						// so we can modify it with system processes
 | 
				
			||||||
	tmpFile.Close()
 | 
						tmpFile.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Run through optimizers
 | 
						// run through optimizers
 | 
				
			||||||
	if format.String() == "JPEG" {
 | 
						if format.String() == "JPEG" {
 | 
				
			||||||
		// run one or the other, running both has no advantage
 | 
							// run one or the other, running both has no advantage
 | 
				
			||||||
		if jpegtran != "" {
 | 
							if jpegtran != "" {
 | 
				
			||||||
@@ -120,6 +137,13 @@ func Goptimize(file string) {
 | 
				
			|||||||
		} else if jpegoptim != "" {
 | 
							} else if jpegoptim != "" {
 | 
				
			||||||
			RunOptimizer(tmpFilename, false, jpegoptim, "-f", "-s", "-o")
 | 
								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" {
 | 
						} else if format.String() == "PNG" {
 | 
				
			||||||
		if pngquant != "" {
 | 
							if pngquant != "" {
 | 
				
			||||||
			RunOptimizer(tmpFilename, true, pngquant, "-f", "--output")
 | 
								RunOptimizer(tmpFilename, true, pngquant, "-f", "--output")
 | 
				
			||||||
@@ -179,7 +203,7 @@ func Goptimize(file string) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		fmt.Printf("Goptimized %s (%dx%d %s > %s %v%%)\n", dstFile, resultW, resultH, ByteCountSI(srcSize), ByteCountSI(dstSize), savedPercent)
 | 
							fmt.Printf("Goptimized %s (%dx%d %s > %s %v%%)\n", dstFile, resultW, resultH, ByteCountSI(srcSize), ByteCountSI(dstSize), savedPercent)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		// If the output directory is not the same,
 | 
							// if the output directory is not the same,
 | 
				
			||||||
		// then write a copy of the original file
 | 
							// then write a copy of the original file
 | 
				
			||||||
		if outputDir != "" {
 | 
							if outputDir != "" {
 | 
				
			||||||
			out, err := os.Create(dstFile)
 | 
								out, err := os.Create(dstFile)
 | 
				
			||||||
@@ -219,7 +243,7 @@ func Goptimize(file string) {
 | 
				
			|||||||
// and overwrite it if the output is smaller than the original
 | 
					// and overwrite it if the output is smaller than the original
 | 
				
			||||||
func RunOptimizer(src string, outFileArg bool, args ...string) {
 | 
					func RunOptimizer(src string, outFileArg bool, args ...string) {
 | 
				
			||||||
	// create a new temp file
 | 
						// create a new temp file
 | 
				
			||||||
	tmpFile, err := ioutil.TempFile(os.TempDir(), "Goptimized-")
 | 
						tmpFile, err := os.CreateTemp(os.TempDir(), "Goptimized-")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		fmt.Printf("Cannot create temporary file: %v\n", err)
 | 
							fmt.Printf("Cannot create temporary file: %v\n", err)
 | 
				
			||||||
@@ -289,3 +313,21 @@ func ByteCountSI(b int64) string {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	return fmt.Sprintf("%.1f%cB", float64(b)/float64(div), "kMGTPE"[exp])
 | 
						return fmt.Sprintf("%.1f%cB", float64(b)/float64(div), "kMGTPE"[exp])
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsGIFAnimated will return an error if the GIF file has more than 1 frame
 | 
				
			||||||
 | 
					func IsGIFAnimated(gifFile string) error {
 | 
				
			||||||
 | 
						file, _ := os.Open(gifFile)
 | 
				
			||||||
 | 
						defer file.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g, err := gif.DecodeAll(file)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Single frame = OK
 | 
				
			||||||
 | 
						if len(g.Image) == 1 {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return fmt.Errorf("Animated gif")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										59
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								main.go
									
									
									
									
									
								
							@@ -5,9 +5,11 @@ import (
 | 
				
			|||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
						"os/exec"
 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
 | 
						"runtime"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/axllent/gitrel"
 | 
						"github.com/axllent/ghru"
 | 
				
			||||||
	"github.com/spf13/pflag"
 | 
						"github.com/spf13/pflag"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -22,6 +24,8 @@ var (
 | 
				
			|||||||
	optipng          string
 | 
						optipng          string
 | 
				
			||||||
	pngquant         string
 | 
						pngquant         string
 | 
				
			||||||
	gifsicle         string
 | 
						gifsicle         string
 | 
				
			||||||
 | 
						copyExif         bool
 | 
				
			||||||
 | 
						threads          = 1
 | 
				
			||||||
	version          = "dev"
 | 
						version          = "dev"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -42,22 +46,24 @@ func main() {
 | 
				
			|||||||
		fmt.Printf("  %s -o out/ -q 90 -m 1600x1600 *.jpg\n", os.Args[0])
 | 
							fmt.Printf("  %s -o out/ -q 90 -m 1600x1600 *.jpg\n", os.Args[0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		fmt.Println("\nDetected optimizers:")
 | 
							fmt.Println("\nDetected optimizers:")
 | 
				
			||||||
		if err := displayDelectedOptimizer("jpegtran ", jpegtran); err != nil {
 | 
							if err := displayDetectedOptimizer("jpegtran ", jpegtran); err != nil {
 | 
				
			||||||
			displayDelectedOptimizer("jpegoptim", jpegoptim)
 | 
								displayDetectedOptimizer("jpegoptim", jpegoptim)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		displayDelectedOptimizer("optipng  ", optipng)
 | 
							displayDetectedOptimizer("optipng  ", optipng)
 | 
				
			||||||
		displayDelectedOptimizer("pngquant ", pngquant)
 | 
							displayDetectedOptimizer("pngquant ", pngquant)
 | 
				
			||||||
		displayDelectedOptimizer("gifsicle ", gifsicle)
 | 
							displayDetectedOptimizer("gifsicle ", gifsicle)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var maxSizes string
 | 
						var maxSizes string
 | 
				
			||||||
	var update, showversion, showhelp bool
 | 
						var multiThreaded, update, showversion, showhelp bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	flag.IntVarP(&quality, "quality", "q", 75, "quality, JPEG only")
 | 
						flag.IntVarP(&quality, "quality", "q", 75, "quality, JPEG only")
 | 
				
			||||||
	flag.StringVarP(&maxSizes, "max", "m", "", "downscale to a maximum width & height in pixels (<width>x<height>)")
 | 
						flag.StringVarP(&maxSizes, "max", "m", "", "downscale to a maximum width & height in pixels (<width>x<height>)")
 | 
				
			||||||
	flag.StringVarP(&outputDir, "out", "o", "", "output directory (default overwrites original)")
 | 
						flag.StringVarP(&outputDir, "out", "o", "", "output directory (default overwrites original)")
 | 
				
			||||||
	flag.BoolVarP(&preserveModTimes, "preserve", "p", true, "preserve file modification times")
 | 
						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(&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")
 | 
						flag.BoolVarP(&showversion, "version", "v", false, "show version number")
 | 
				
			||||||
	flag.BoolVarP(&showhelp, "help", "h", false, "show help")
 | 
						flag.BoolVarP(&showhelp, "help", "h", false, "show help")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -87,15 +93,15 @@ func main() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	if showversion {
 | 
						if showversion {
 | 
				
			||||||
		fmt.Println(fmt.Sprintf("Version: %s", version))
 | 
							fmt.Println(fmt.Sprintf("Version: %s", version))
 | 
				
			||||||
		latest, _, _, err := gitrel.Latest("axllent/goptimize", "goptimize")
 | 
							latest, _, _, err := ghru.Latest("axllent/goptimize", "goptimize")
 | 
				
			||||||
		if err == nil && latest != version {
 | 
							if err == nil && ghru.GreaterThan(latest, version) {
 | 
				
			||||||
			fmt.Printf("Update available: %s\nRun `%s -u` to update.\n", latest, os.Args[0])
 | 
								fmt.Printf("Update available: %s\nRun `%s -u` to update.\n", latest, os.Args[0])
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if update {
 | 
						if update {
 | 
				
			||||||
		rel, err := gitrel.Update("axllent/goptimize", "goptimize", version)
 | 
							rel, err := ghru.Update("axllent/goptimize", "goptimize", version)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			fmt.Println(err)
 | 
								fmt.Println(err)
 | 
				
			||||||
			os.Exit(1)
 | 
								os.Exit(1)
 | 
				
			||||||
@@ -137,13 +143,38 @@ func main() {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, img := range args {
 | 
						if multiThreaded {
 | 
				
			||||||
		Goptimize(img)
 | 
							threads = runtime.NumCPU()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						processChan := make(chan string)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						wg := &sync.WaitGroup{} // Signal to main goroutine that worker goroutines are working/done working
 | 
				
			||||||
 | 
						wg.Add(threads)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i := 0; i < threads; i++ {
 | 
				
			||||||
 | 
							go func() {
 | 
				
			||||||
 | 
								for nextFile := range processChan {
 | 
				
			||||||
 | 
									Goptimize(nextFile)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// Channel was closed, so we finished this goroutine.
 | 
				
			||||||
 | 
								wg.Done() // Let main goroutine know we are done.
 | 
				
			||||||
 | 
							}()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, img := range args {
 | 
				
			||||||
 | 
							processChan <- img
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Close the channel.  This tells the worker goroutines that they can be done.
 | 
				
			||||||
 | 
						close(processChan)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Wait for all worker goroutines to finish processing the IPs
 | 
				
			||||||
 | 
						wg.Wait()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// displayDelectedOptimizer prints whether the optimizer was found
 | 
					// displayDetectedOptimizer prints whether the optimizer was found
 | 
				
			||||||
func displayDelectedOptimizer(name, bin string) error {
 | 
					func displayDetectedOptimizer(name, bin string) error {
 | 
				
			||||||
	exe, err := exec.LookPath(bin)
 | 
						exe, err := exec.LookPath(bin)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user