mirror of
				https://github.com/axllent/goptimize.git
				synced 2025-10-30 01:42:29 -04:00 
			
		
		
		
	Compare commits
	
		
			72 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 49d804950f | ||
|  | a6ee641f81 | ||
|  | 1234beaed0 | ||
|  | e44ce81125 | ||
|  | 59301ffc1a | ||
|  | e795f587ef | ||
|  | d94f93af58 | ||
|  | 065ff933c0 | ||
|  | 42a5284e57 | ||
|  | 3fde14166e | ||
|  | 0eb5cab8f8 | ||
|  | a48750e9a1 | ||
|  | e21049c75d | ||
|  | b6ff2a7a5c | ||
|  | c6ccb45a25 | ||
|  | 99756efb02 | ||
|  | 118c5bc9ac | ||
|  | 4d08aff7ec | ||
|  | 788c6ecf23 | ||
|  | dedca9b41d | ||
|  | 74cb212d22 | ||
|  | 1a6942417b | ||
|  | 2444204b32 | ||
|  | 423ebc1494 | ||
|  | e5ecbe9bdf | ||
|  | cc2b9d6b88 | ||
|  | 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 | ||
|  | ee6aa69087 | ||
|  | 8362617920 | ||
|  | b157fcd72b | ||
|  | 0c000fe262 | ||
|  | 97fa6c94c5 | ||
|  | 36e9b4c8fa | ||
|  | 5df486c53e | ||
|  | 120e5fefde | ||
|  | d2a591a6d9 | ||
|  | 8e5e7b6098 | ||
|  | 47024e030b | ||
|  | 45915d4b42 | 
							
								
								
									
										15
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| # To get started with Dependabot version updates, you'll need to specify which | ||||
| # package ecosystems to update and where the package manifests are located. | ||||
| # Please see the documentation for all configuration options: | ||||
| # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates | ||||
|  | ||||
| version: 2 | ||||
| updates: | ||||
|   - package-ecosystem: "gomod" | ||||
|     directory: "/" # Location of package manifests | ||||
|     schedule: | ||||
|       interval: "quarterly" | ||||
|   - package-ecosystem: "github-actions" | ||||
|     directory: "/" # Location of package manifests | ||||
|     schedule: | ||||
|       interval: "quarterly" | ||||
							
								
								
									
										35
									
								
								.github/workflows/build-release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								.github/workflows/build-release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| name: Release Go Binaries | ||||
|  | ||||
| on: | ||||
|   release: | ||||
|     types: [created] | ||||
|  | ||||
| jobs: | ||||
|   releases-matrix: | ||||
|     name: Release Go Binary | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         goos: [linux, darwin] | ||||
|         goarch: ["386", amd64, arm, arm64] | ||||
|         exclude: | ||||
|           - goarch: "386" | ||||
|             goos: darwin | ||||
|           - goarch: arm | ||||
|             goos: darwin | ||||
|  | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|  | ||||
|       - uses: wangyoucao577/go-release-action@v1 | ||||
|         with: | ||||
|           binary_name: "goptimize" | ||||
|           asset_name: "goptimize-${{ matrix.goos }}-${{ matrix.goarch }}" | ||||
|           extra_files: LICENSE README.md | ||||
|           github_token: ${{ secrets.GITHUB_TOKEN }} | ||||
|           md5sum: false | ||||
|           overwrite: true | ||||
|           goos: ${{ matrix.goos }} | ||||
|           goarch: ${{ matrix.goarch }} | ||||
|           pre_command: export GO386=softfloat CGO_ENABLED=0 | ||||
|           ldflags: -s -w -X "main.version=${{ github.ref_name }}" | ||||
							
								
								
									
										72
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| # For most projects, this workflow file will not need changing; you simply need | ||||
| # to commit it to your repository. | ||||
| # | ||||
| # You may wish to alter this file to override the set of languages analyzed, | ||||
| # or to provide custom queries or build logic. | ||||
| # | ||||
| # ******** NOTE ******** | ||||
| # We have attempted to detect the languages in your repository. Please check | ||||
| # the `language` matrix defined below to confirm you have the correct set of | ||||
| # supported CodeQL languages. | ||||
| # | ||||
| name: "CodeQL" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ "develop" ] | ||||
|   pull_request: | ||||
|     # The branches below must be a subset of the branches above | ||||
|     branches: [ "develop" ] | ||||
|   schedule: | ||||
|     - cron: '34 23 * * 4' | ||||
|  | ||||
| jobs: | ||||
|   analyze: | ||||
|     name: Analyze | ||||
|     runs-on: ubuntu-latest | ||||
|     permissions: | ||||
|       actions: read | ||||
|       contents: read | ||||
|       security-events: write | ||||
|  | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         language: [ 'go' ] | ||||
|         # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] | ||||
|         # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support | ||||
|  | ||||
|     steps: | ||||
|     - name: Checkout repository | ||||
|       uses: actions/checkout@v5 | ||||
|  | ||||
|     # Initializes the CodeQL tools for scanning. | ||||
|     - name: Initialize CodeQL | ||||
|       uses: github/codeql-action/init@v3 | ||||
|       with: | ||||
|         languages: ${{ matrix.language }} | ||||
|         # If you wish to specify custom queries, you can do so here or in a config file. | ||||
|         # By default, queries listed here will override any specified in a config file. | ||||
|         # Prefix the list here with "+" to use these queries and those in the config file. | ||||
|          | ||||
|         # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs | ||||
|         # queries: security-extended,security-and-quality | ||||
|  | ||||
|          | ||||
|     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java). | ||||
|     # If this step fails, then you should remove it and run the build manually (see below) | ||||
|     - name: Autobuild | ||||
|       uses: github/codeql-action/autobuild@v3 | ||||
|  | ||||
|     # ℹ️ Command-line programs to run using the OS shell. | ||||
|     # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun | ||||
|  | ||||
|     #   If the Autobuild fails above, remove it and uncomment the following three lines.  | ||||
|     #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. | ||||
|  | ||||
|     # - run: | | ||||
|     #   echo "Run, Build Application using script" | ||||
|     #   ./location_of_script_within_repo/buildscript.sh | ||||
|  | ||||
|     - name: Perform CodeQL Analysis | ||||
|       uses: github/codeql-action/analyze@v3 | ||||
							
								
								
									
										20
									
								
								.github/workflows/tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.github/workflows/tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| name: Tests | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: [develop, "feature/**"] | ||||
|   push: | ||||
|     branches: [develop, "feature/**"] | ||||
|  | ||||
| jobs: | ||||
|   golangci: | ||||
|     name: golangci-lint | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|       - uses: actions/setup-go@v6 | ||||
|         with: | ||||
|           go-version: stable | ||||
|       - name: golangci-lint | ||||
|         uses: golangci/golangci-lint-action@v8 | ||||
|         with: | ||||
|           version: v2.1 | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,2 @@ | ||||
| /dist/ | ||||
| goptimize | ||||
| goptimize* | ||||
							
								
								
									
										39
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,5 +1,44 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## [1.0.0] | ||||
|  | ||||
| - Use ghru/v2 for latest version checks and self-update functionality | ||||
| - Refactor code for consistency and error handling improvements | ||||
| - Update Go dependencies | ||||
| - Use GitHub Actions to build release binaries | ||||
| - Add quarterly dependabot module update | ||||
| - Add code quality checks & linting (golangci-lint) | ||||
|  | ||||
| ## [0.2.3] | ||||
|  | ||||
| - Switch to kovidgoyal/imaging to address CVE-2023-36308 | ||||
|  | ||||
| ## [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 | ||||
|  | ||||
| ## [0.0.1] | ||||
|  | ||||
| - Initial release | ||||
|   | ||||
							
								
								
									
										31
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								LICENSE
									
									
									
									
									
								
							| @@ -1,16 +1,21 @@ | ||||
| Copyright 2019 Ralph Slooten | ||||
| The MIT License (MIT) | ||||
| Copyright (c) 2019-Now() Ralph Slooten | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy of this software | ||||
| and associated documentation files (the "Software"), to deal in the Software without restriction, | ||||
| including without limitation the rights to use, copy, modify, merge, publish, distribute, | ||||
| sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
| Permission is hereby granted, free of charge, to any person obtaining | ||||
| a copy of this software and associated documentation files (the | ||||
| "Software"), to deal in the Software without restriction, including | ||||
| without limitation the rights to use, copy, modify, merge, publish, | ||||
| distribute, sublicense, and/or sell copies of the Software, and to | ||||
| permit persons to whom the Software is furnished to do so, subject to | ||||
| the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all copies or | ||||
| substantial portions of the Software. | ||||
| The above copyright notice and this permission notice shall be | ||||
| included in all copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING | ||||
| BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||||
| NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, | ||||
| DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||||
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||||
| NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||||
| LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||||
| OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||||
| WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||||
|   | ||||
							
								
								
									
										24
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								Makefile
									
									
									
									
									
								
							| @@ -1,24 +0,0 @@ | ||||
| TAG=`git describe --tags` | ||||
| VERSION ?= `git describe --tags` | ||||
| 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) \ | ||||
| 	&& bzip2 dist/goptimize_${VERSION}_$(1)_$(2) | ||||
|  | ||||
| goptimize: *.go | ||||
| 	go get github.com/disintegration/imaging | ||||
| 	go build ${LDFLAGS} -o goptimize | ||||
| 	rm -rf /tmp/go-* | ||||
|  | ||||
| clean: | ||||
| 	rm -f goptimize | ||||
|  | ||||
| release: | ||||
| 	mkdir -p dist | ||||
| 	rm -f dist/goptimize_${VERSION}_* | ||||
| 	$(call build,linux,amd64) | ||||
| 	$(call build,linux,386) | ||||
| 	$(call build,linux,arm) | ||||
| 	$(call build,linux,arm64) | ||||
| 	$(call build,darwin,amd64) | ||||
| 	$(call build,darwin,386) | ||||
							
								
								
									
										56
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										56
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,26 +1,27 @@ | ||||
| # Goptimizer - downscales and optimizes images | ||||
| # Goptimize - downscales and optimizes images | ||||
|  | ||||
| Goptimizer is a commandline utility written in Golang. It downscales and optimize existing images JPEG, PNG and Gif files. | ||||
| [](https://goreportcard.com/report/github.com/axllent/goptimize) | ||||
|  | ||||
| Image downscaling 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): | ||||
| Goptimize is a command-line utility written in Go. It downscales and optimizes JPEG, PNG, GIF, TIFF and BMP files. | ||||
|  | ||||
| - jpegoptim | ||||
| - jpegtran (`libjpeg-turbo-progs`) | ||||
| 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): | ||||
|  | ||||
| - jpegtran (`libjpeg-turbo-progs`) or jpegoptim | ||||
| - optipng | ||||
| - pngquant | ||||
| - gifsicle | ||||
|  | ||||
|  | ||||
| ## 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. | ||||
|  | ||||
| Goptimize will remove all exif data from JPEG files, auto-rotating those that relied on it. | ||||
| GGoptimize will by default remove all EXIF data from JPEG files and automatically rotate those that rely on it for orientation. This can be disabled by adding the `-e` flag. | ||||
|  | ||||
| It will also preserve (by default) the file's original modification times (`-p=false` to disable). | ||||
|  | ||||
| Animated GIF files are not supported and are ignored. | ||||
|  | ||||
| ## Usage options | ||||
|  | ||||
| @@ -28,29 +29,32 @@ It will also preserve (by default) the file's original modification times (`-p=f | ||||
| Usage: ./goptimize [options] <images> | ||||
|  | ||||
| Options: | ||||
|   -gifsicle string | ||||
|         gifsicle binary (default "gifsicle") | ||||
|   -jpegoptim string | ||||
|         jpegoptim binary (default "jpegoptim") | ||||
|   -jpegtran string | ||||
|         jpegtran binary (default "jpegtran") | ||||
|   -m string | ||||
|         downscale to a maximum width & height in pixels (<width>x<height>) | ||||
|   -o string | ||||
|         output directory (default overwrites original) | ||||
|   -optipng string | ||||
|         optipng binary (default "optipng") | ||||
|   -p    preserve file modification times (default true) | ||||
|   -pngquant string | ||||
|         pngquant binary (default "pngquant") | ||||
|   -q int | ||||
|         quality - JPEG only (default 75) | ||||
|   -q, --quality int        quality, JPEG only (default 75) | ||||
|   -m, --max string         downscale to a maximum width & height in pixels (<width>x<height>) | ||||
|   -o, --out string         output directory (default overwrites original) | ||||
|   -p, --preserve           preserve file modification times (default true) | ||||
|   -t, --threaded           run multi-threaded (use all CPU cores) | ||||
|   -u, --update             update to latest release | ||||
|   -v, --version            show version number | ||||
|   -h, --help               show help | ||||
|       --jpegtran string    jpegtran binary (default "jpegtran") | ||||
|       --jpegoptim string   jpegoptim binary (default "jpegoptim") | ||||
|       --gifsicle string    gifsicle binary (default "gifsicle") | ||||
|       --pngquant string    pngquant binary (default "pngquant") | ||||
|       --optipng string     optipng binary (default "optipng") | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ## Examples | ||||
|  | ||||
| - `./goptimize image.png` - optimize a PNG file | ||||
| - `./goptimize -m 800x800 *` - optimize and downscale all image files to a maximum size of 800x800px | ||||
| - `./goptimize -m 1200x0 image.jpg` - optimize and downscale a JPG file to a maximum size of width of 1200px | ||||
| - `./goptimize -o out/ image.jpg` - optimize a JPG file and save it to `out/` | ||||
|  | ||||
| ## Install | ||||
|  | ||||
| Download the appropriate binary from the [releases](https://github.com/axllent/goptimize/releases/latest), or if you have golang installed you can install from source: | ||||
|  | ||||
| ``` | ||||
| go install github.com/axllent/goptimize@latest | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										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 func() { _ = outFile.Close() }() | ||||
| 	writer := bufio.NewWriter(outFile) | ||||
|  | ||||
| 	imageFile, err := os.Open(imagePath) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer func() { _ = imageFile.Close() }() | ||||
| 	imageReader := bufio.NewReader(imageFile) | ||||
|  | ||||
| 	metaFile, err := os.Open(metadataImagePath) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer func() { _ = 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 func() { _ = os.Remove(copyPath) }() | ||||
| 	return copyMetadata(toPath, copyPath, fromPath) | ||||
| } | ||||
							
								
								
									
										12
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| module github.com/axllent/goptimize | ||||
|  | ||||
| go 1.24.0 | ||||
|  | ||||
| require ( | ||||
| 	github.com/axllent/ghru/v2 v2.0.2 | ||||
| 	github.com/kovidgoyal/imaging v1.6.4 | ||||
| 	github.com/spf13/pflag v1.0.10 | ||||
| 	golang.org/x/image v0.31.0 | ||||
| ) | ||||
|  | ||||
| require golang.org/x/mod v0.28.0 // indirect | ||||
							
								
								
									
										10
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| github.com/axllent/ghru/v2 v2.0.2 h1:xalJupjJAU8Kcs39AwpG53qbcbi3+WKM98BEoQWf/zU= | ||||
| github.com/axllent/ghru/v2 v2.0.2/go.mod h1:seMMjx8/0r5ZAL7c0vwTPIRoyN0AoTUqAylZEWZWGK4= | ||||
| github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk= | ||||
| github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY= | ||||
| github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= | ||||
| github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | ||||
| golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA= | ||||
| golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA= | ||||
| golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= | ||||
| golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= | ||||
							
								
								
									
										153
									
								
								goptimize.go
									
									
									
									
									
								
							
							
						
						
									
										153
									
								
								goptimize.go
									
									
									
									
									
								
							| @@ -2,51 +2,67 @@ package main | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"image" | ||||
| 	"image/gif" | ||||
| 	"image/jpeg" | ||||
| 	"image/png" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"math" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"github.com/disintegration/imaging" | ||||
| 	"github.com/kovidgoyal/imaging" | ||||
| 	"golang.org/x/image/bmp" | ||||
| 	"golang.org/x/image/tiff" | ||||
| ) | ||||
|  | ||||
| // Goptimize downscales and optimizes an existing image | ||||
| func Goptimize(file string) { | ||||
| func goptimize(file string) { | ||||
| 	info, err := os.Stat(file) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("%s doesn't exist\n", file) | ||||
| 		fmt.Printf("Error: %s doesn't exist\n", file) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !info.Mode().IsRegular() { | ||||
| 		// not a file | ||||
| 		fmt.Printf("%s is not a file\n", file) | ||||
| 		fmt.Printf("Error: %s is not a file\n", file) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// open original, rotate if neccesary | ||||
| 	src, err := imaging.Open(file, imaging.AutoOrientation(true)) | ||||
| 	var src image.Image | ||||
|  | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("%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) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Cannot detect format: %v\n", err) | ||||
| 		fmt.Printf("Error: cannot detect format: %v\n", err) | ||||
| 		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) | ||||
| 	outDir := filepath.Dir(file) | ||||
| 	dstFile := filepath.Join(outDir, outFilename) | ||||
| @@ -60,7 +76,7 @@ func Goptimize(file string) { | ||||
| 	srcW := srcBounds.Dx() | ||||
| 	srcH := srcBounds.Dy() | ||||
|  | ||||
| 	// Ensure scaling does not upscale image | ||||
| 	// do not upscale image | ||||
| 	imgMaxW := maxWidth | ||||
| 	if imgMaxW == 0 || imgMaxW > srcW { | ||||
| 		imgMaxW = srcW | ||||
| @@ -76,27 +92,28 @@ func Goptimize(file string) { | ||||
| 	resultW := dstBounds.Dx() | ||||
| 	resultH := dstBounds.Dy() | ||||
|  | ||||
| 	tmpFile, err := ioutil.TempFile(os.TempDir(), "Goptimized-") | ||||
| 	tmpFile, err := os.CreateTemp(os.TempDir(), "Goptimized-") | ||||
|  | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Cannot create temporary file: %v\n", err) | ||||
| 		fmt.Printf("Error: cannot create temporary file: %v\n", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	defer os.Remove(tmpFile.Name()) | ||||
| 	defer func() { _ = os.Remove(tmpFile.Name()) }() | ||||
|  | ||||
| 	if format.String() == "JPEG" { | ||||
| 	switch imgType := format.String(); imgType { | ||||
| 	case "JPEG": | ||||
| 		err = jpeg.Encode(tmpFile, resized, &jpeg.Options{Quality: quality}) | ||||
| 	} else if format.String() == "PNG" { | ||||
| 	case "PNG": | ||||
| 		err = png.Encode(tmpFile, resized) | ||||
| 	} else if format.String() == "GIF" { | ||||
| 	case "GIF": | ||||
| 		err = gif.Encode(tmpFile, resized, nil) | ||||
| 	} else if format.String() == "TIFF" { | ||||
| 	case "TIFF": | ||||
| 		err = tiff.Encode(tmpFile, resized, nil) | ||||
| 	} else if format.String() == "BMP" { | ||||
| 	case "BMP": | ||||
| 		err = bmp.Encode(tmpFile, resized) | ||||
| 	} else { | ||||
| 		fmt.Printf("Unsupported file type %s\n", file) | ||||
| 	default: | ||||
| 		fmt.Printf("Error: unsupported file type (%s)\n", file) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @@ -110,25 +127,35 @@ func Goptimize(file string) { | ||||
|  | ||||
| 	// immediately close the temp file to release pointers | ||||
| 	// so we can modify it with system processes | ||||
| 	tmpFile.Close() | ||||
| 	if err := tmpFile.Close(); err != nil { | ||||
| 		fmt.Printf("Error closing temporary file: %v\n", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Run through optimizers | ||||
| 	// run through optimizers | ||||
| 	if format.String() == "JPEG" { | ||||
| 		// run one or the other, running both has no advantage | ||||
| 		if jpegtran != "" { | ||||
| 			RunOptimiser(tmpFilename, true, jpegtran, "-optimize", "-outfile") | ||||
| 			runOptimizer(tmpFilename, true, jpegtran, "-optimize", "-outfile") | ||||
| 		} else if jpegoptim != "" { | ||||
| 			RunOptimiser(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" { | ||||
| 		if pngquant != "" { | ||||
| 			RunOptimiser(tmpFilename, true, pngquant, "-f", "--output") | ||||
| 			runOptimizer(tmpFilename, true, pngquant, "-f", "--output") | ||||
| 		} | ||||
| 		if optipng != "" { | ||||
| 			RunOptimiser(tmpFilename, true, optipng, "-out") | ||||
| 			runOptimizer(tmpFilename, true, optipng, "-out") | ||||
| 		} | ||||
| 	} else if format.String() == "GIF" && gifsicle != "" { | ||||
| 		RunOptimiser(tmpFilename, true, gifsicle, "-o") | ||||
| 		runOptimizer(tmpFilename, true, gifsicle, "-o") | ||||
| 	} | ||||
|  | ||||
| 	// re-open modified temporary file | ||||
| @@ -138,7 +165,7 @@ func Goptimize(file string) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	defer tmpFile.Close() | ||||
| 	defer func() { _ = tmpFile.Close() }() | ||||
|  | ||||
| 	// get th eoriginal file stats | ||||
| 	srcStat, _ := os.Stat(file) | ||||
| @@ -163,10 +190,10 @@ func Goptimize(file string) { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		defer out.Close() | ||||
| 		defer func() { _ = out.Close() }() | ||||
|  | ||||
| 		if _, err := io.Copy(out, tmpFile); err != nil { | ||||
| 			fmt.Printf("Error ovewriting original file: %v\n", err) | ||||
| 			fmt.Printf("Error overwriting original file: %v\n", err) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| @@ -177,22 +204,26 @@ func Goptimize(file string) { | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		fmt.Printf("Goptimized %s (%dx%d %s > %s %v%%)\n", dstFile, resultW, resultH, ByteCountSI(srcSize), ByteCountSI(dstSize), savedPercent) | ||||
| 		fmt.Printf("Optimized %s (%dx%d %s > %s %v%%)\n", dstFile, resultW, resultH, byteCountSI(srcSize), byteCountSI(dstSize), savedPercent) | ||||
| 	} 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 | ||||
| 		if outputDir != "" { | ||||
| 			out, err := os.Create(dstFile) | ||||
| 			if err != nil { | ||||
| 				fmt.Printf("Error creating new file: %v\n", err) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			defer func() { _ = out.Close() }() | ||||
|  | ||||
| 			orig, err := os.Open(file) | ||||
| 			if err != nil { | ||||
| 				fmt.Printf("Error opening original file: %v\n", err) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			defer out.Close() | ||||
|  | ||||
| 			orig, _ := os.Open(file) | ||||
|  | ||||
| 			defer orig.Close() | ||||
| 			defer func() { _ = orig.Close() }() | ||||
|  | ||||
| 			if _, err := io.Copy(out, orig); err != nil { | ||||
| 				fmt.Printf("Error ovewriting original file: %v\n", err) | ||||
| @@ -206,36 +237,35 @@ func Goptimize(file string) { | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			fmt.Printf("Copied %s (%dx%d %s %v%%)\n", dstFile, srcW, srcH, ByteCountSI(srcSize), 0) | ||||
| 			fmt.Printf("Copied %s (%dx%d %s %v%%)\n", dstFile, srcW, srcH, byteCountSI(srcSize), 0) | ||||
| 		} else { | ||||
| 			// we didn't actually change anything | ||||
| 			fmt.Printf("Skipped %s (%dx%d %s %v%%)\n", dstFile, srcW, srcH, ByteCountSI(srcSize), 0) | ||||
| 			fmt.Printf("Skipped %s (%dx%d %s %v%%)\n", dstFile, srcW, srcH, byteCountSI(srcSize), 0) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| // RunOptimiser will run the specified command on a copy of the temporary file, | ||||
| // runOptimizer will run the specified command on a copy of the temporary file, | ||||
| // and overwrite it if the output is smaller than the original | ||||
| func RunOptimiser(src string, outFileArg bool, args ...string) { | ||||
| func runOptimizer(src string, outFileArg bool, args ...string) { | ||||
| 	// create a new temp file | ||||
| 	tmpFile, err := ioutil.TempFile(os.TempDir(), "Goptimized-") | ||||
| 	tmpFile, err := os.CreateTemp(os.TempDir(), "goptimize-") | ||||
|  | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Cannot create temporary file: %v\n", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	defer os.Remove(tmpFile.Name()) | ||||
| 	defer func() { _ = os.Remove(tmpFile.Name()) }() | ||||
|  | ||||
| 	source, err := os.Open(src) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Cannot open temporary file: %v\n", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	defer source.Close() | ||||
| 	defer func() { _ = source.Close() }() | ||||
|  | ||||
| 	if _, err := io.Copy(tmpFile, source); err != nil { | ||||
| 		fmt.Printf("Cannot copy source file: %v\n", err) | ||||
| @@ -265,8 +295,8 @@ func RunOptimiser(src string, outFileArg bool, args ...string) { | ||||
| 	dstSize := dstStat.Size() | ||||
|  | ||||
| 	// ensure file pointers are closed before renaming | ||||
| 	tmpFile.Close() | ||||
| 	source.Close() | ||||
| 	func() { _ = tmpFile.Close() }() | ||||
| 	func() { _ = source.Close() }() | ||||
|  | ||||
| 	if dstSize < srcSize { | ||||
| 		if err := os.Rename(tmpFilename, src); err != nil { | ||||
| @@ -277,7 +307,7 @@ func RunOptimiser(src string, outFileArg bool, args ...string) { | ||||
| } | ||||
|  | ||||
| // ByteCountSI returns a human readable size from int64 bytes | ||||
| func ByteCountSI(b int64) string { | ||||
| func byteCountSI(b int64) string { | ||||
| 	const unit = 1000 | ||||
| 	if b < unit { | ||||
| 		return fmt.Sprintf("%dB", b) | ||||
| @@ -289,3 +319,24 @@ func ByteCountSI(b int64) string { | ||||
| 	} | ||||
| 	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, err := os.Open(gifFile) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("cannot open GIF file: %v", err) | ||||
| 	} | ||||
| 	defer func() { _ = 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("cannot optimize an animated gif") | ||||
| } | ||||
|   | ||||
							
								
								
									
										135
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										135
									
								
								main.go
									
									
									
									
									
								
							| @@ -1,14 +1,17 @@ | ||||
| // Package main is the main application | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"regexp" | ||||
| 	"runtime" | ||||
| 	"strconv" | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/axllent/gitrel" | ||||
| 	"github.com/axllent/ghru/v2" | ||||
| 	"github.com/spf13/pflag" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| @@ -22,15 +25,28 @@ var ( | ||||
| 	optipng          string | ||||
| 	pngquant         string | ||||
| 	gifsicle         string | ||||
| 	copyExif         bool | ||||
| 	threads          = 1 | ||||
| 	version          = "dev" | ||||
| 	// ghruConf is the configuration for the ghru package | ||||
| 	ghruConf = ghru.Config{ | ||||
| 		Repo:           "axllent/goptimize", | ||||
| 		ArchiveName:    "goptimize-{{.OS}}-{{.Arch}}", | ||||
| 		BinaryName:     "goptimize", | ||||
| 		CurrentVersion: version, | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	// set up new flag instance | ||||
| 	flag := pflag.NewFlagSet(os.Args[0], pflag.ExitOnError) | ||||
|  | ||||
| 	// set the default help | ||||
| 	flag.Usage = func() { | ||||
| 		fmt.Println("Goptimize - downscales and optimizes images") | ||||
| 		fmt.Printf("\nUsage: %s [options] <images>\n", os.Args[0]) | ||||
| 		fmt.Println("\nOptions:") | ||||
| 		flag.SortFlags = false | ||||
| 		flag.PrintDefaults() | ||||
| 		fmt.Println("\nExamples:") | ||||
| 		fmt.Printf("  %s image.png\n", os.Args[0]) | ||||
| @@ -38,33 +54,40 @@ func main() { | ||||
| 		fmt.Printf("  %s -o out/ -q 90 -m 1600x1600 *.jpg\n", os.Args[0]) | ||||
|  | ||||
| 		fmt.Println("\nDetected optimizers:") | ||||
| 		if err := displayDelectedOptimizer("jpegtran ", jpegtran); err != nil { | ||||
| 			displayDelectedOptimizer("jpegoptim", jpegoptim) | ||||
| 		if err := displayDetectedOptimizer("jpegtran ", jpegtran); err != nil { | ||||
| 			_ = displayDetectedOptimizer("jpegoptim", jpegoptim) | ||||
| 		} | ||||
| 		displayDelectedOptimizer("optipng  ", optipng) | ||||
| 		displayDelectedOptimizer("pngquant ", pngquant) | ||||
| 		displayDelectedOptimizer("gifsicle ", gifsicle) | ||||
| 		_ = displayDetectedOptimizer("optipng  ", optipng) | ||||
| 		_ = displayDetectedOptimizer("pngquant ", pngquant) | ||||
| 		_ = displayDetectedOptimizer("gifsicle ", gifsicle) | ||||
| 	} | ||||
|  | ||||
| 	var maxSizes string | ||||
| 	var update, showversion bool | ||||
| 	var multiThreaded, update, showVersion, showHelp bool | ||||
|  | ||||
| 	flag.IntVar(&quality, "q", 75, "quality - JPEG only") | ||||
| 	flag.StringVar(&outputDir, "o", "", "output directory (default overwrites original)") | ||||
| 	flag.BoolVar(&preserveModTimes, "p", true, "preserve file modification times") | ||||
| 	flag.StringVar(&maxSizes, "m", "", "downscale to a maximum width & height in pixels (<width>x<height>)") | ||||
| 	flag.BoolVar(&update, "u", false, "update to latest release") | ||||
| 	flag.BoolVar(&showversion, "v", false, "show version number") | ||||
| 	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(&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") | ||||
| 	flag.BoolVarP(&showHelp, "help", "h", false, "show help") | ||||
|  | ||||
| 	// third-party optimizers | ||||
| 	flag.StringVar(&gifsicle, "gifsicle", "gifsicle", "gifsicle binary") | ||||
| 	flag.StringVar(&jpegoptim, "jpegoptim", "jpegoptim", "jpegoptim binary") | ||||
| 	flag.StringVar(&jpegtran, "jpegtran", "jpegtran", "jpegtran binary") | ||||
| 	flag.StringVar(&optipng, "optipng", "optipng", "optipng binary") | ||||
| 	flag.StringVar(&jpegoptim, "jpegoptim", "jpegoptim", "jpegoptim binary") | ||||
| 	flag.StringVar(&gifsicle, "gifsicle", "gifsicle", "gifsicle binary") | ||||
| 	flag.StringVar(&pngquant, "pngquant", "pngquant", "pngquant binary") | ||||
| 	flag.StringVar(&optipng, "optipng", "optipng", "optipng binary") | ||||
|  | ||||
| 	// parse flags | ||||
| 	flag.Parse() | ||||
| 	flag.SortFlags = false | ||||
|  | ||||
| 	// parse args excluding os.Args[0] | ||||
| 	if err := flag.Parse(os.Args[1:]); err != nil { | ||||
| 		fmt.Printf("Error parsing flags: %s\n", err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	// detect optimizer paths | ||||
| 	gifsicle, _ = exec.LookPath(gifsicle) | ||||
| @@ -73,23 +96,44 @@ func main() { | ||||
| 	optipng, _ = exec.LookPath(optipng) | ||||
| 	pngquant, _ = exec.LookPath(pngquant) | ||||
|  | ||||
| 	if showversion { | ||||
| 		fmt.Println(fmt.Sprintf("Version: %s", version)) | ||||
| 		latest, _, _, err := gitrel.Latest("axllent/goptimize", "goptimize") | ||||
| 		if err == nil && latest != version { | ||||
| 			fmt.Printf("Update available: %s\nRun `%s -u` to update.\n", latest, os.Args[0]) | ||||
| 	if showHelp { | ||||
| 		flag.Usage() | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
|  | ||||
| 	if showVersion { | ||||
| 		fmt.Printf("Version: %s\n", version) | ||||
|  | ||||
| 		release, err := ghruConf.Latest() | ||||
| 		if err != nil { | ||||
| 			fmt.Printf("Error getting latest release: %s\n", err.Error()) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 		return | ||||
|  | ||||
| 		// The latest version is the same version | ||||
| 		if release.Tag == version { | ||||
| 			os.Exit(0) | ||||
| 		} | ||||
|  | ||||
| 		// A newer release is available | ||||
| 		fmt.Printf( | ||||
| 			"Update available: %s\nRun `%s -u` to update (requires read/write access to install directory).\n", | ||||
| 			release.Tag, | ||||
| 			os.Args[0], | ||||
| 		) | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
|  | ||||
| 	if update { | ||||
| 		rel, err := gitrel.Update("axllent/goptimize", "goptimize", version) | ||||
| 		// Update the app | ||||
| 		rel, err := ghruConf.SelfUpdate() | ||||
| 		if err != nil { | ||||
| 			fmt.Println(err) | ||||
| 			fmt.Println(err.Error()) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 		fmt.Printf("Updated %s to version %s", os.Args[0], rel) | ||||
| 		return | ||||
|  | ||||
| 		fmt.Printf("Updated %s to version %s\n", os.Args[0], rel.Tag) | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
|  | ||||
| 	if len(flag.Args()) < 1 { | ||||
| @@ -125,13 +169,38 @@ func main() { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for _, img := range args { | ||||
| 		Goptimize(img) | ||||
| 	if multiThreaded { | ||||
| 		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 | ||||
| func displayDelectedOptimizer(name, bin string) error { | ||||
| // displayDetectedOptimizer prints whether the optimizer was found | ||||
| func displayDetectedOptimizer(name, bin string) error { | ||||
| 	exe, err := exec.LookPath(bin) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
|   | ||||
		Reference in New Issue
	
	Block a user