mirror of
https://github.com/axllent/goptimize.git
synced 2025-07-05 16:38:25 -04:00
Merge branch 'release/1.0.0'
This commit is contained in:
commit
065ff933c0
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@v4
|
||||
|
||||
- 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@v4
|
||||
|
||||
# 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@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: v2.1
|
16
CHANGELOG.md
16
CHANGELOG.md
@ -1,42 +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.
|
||||
|
23
Makefile
23
Makefile
@ -1,23 +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.*
|
||||
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,arm64)
|
28
README.md
28
README.md
@ -1,8 +1,8 @@
|
||||
# Goptimizer - downscales and optimizes images
|
||||
# Goptimize - downscales and optimizes images
|
||||
|
||||
[](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.
|
||||
Goptimize is a command-line 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):
|
||||
|
||||
@ -11,19 +11,17 @@ Image downscaling/rotation is done within goptimize (`-m <width>x<height>`, see
|
||||
- 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 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 depend on it for orientation.
|
||||
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 automatically get skipped.
|
||||
|
||||
Animated GIF files are not supported and are ignored.
|
||||
|
||||
## Usage options
|
||||
|
||||
@ -46,7 +44,6 @@ Options:
|
||||
--optipng string optipng binary (default "optipng")
|
||||
```
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
- `./goptimize image.png` - optimize a PNG file
|
||||
@ -54,23 +51,10 @@ Options:
|
||||
- `./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
|
||||
|
||||
|
||||
### Build requirements
|
||||
|
||||
Go >= 1.21 required.
|
||||
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 get github.com/axllent/goptimize
|
||||
go install github.com/axllent/goptimize@latest
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
Some ideas for the future:
|
||||
|
||||
- Dry run
|
||||
- Option to copy exif data (how?)
|
||||
|
@ -115,21 +115,21 @@ func copyMetadata(outImagePath, imagePath, metadataImagePath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
defer func() { _ = outFile.Close() }()
|
||||
writer := bufio.NewWriter(outFile)
|
||||
|
||||
imageFile, err := os.Open(imagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer imageFile.Close()
|
||||
defer func() { _ = imageFile.Close() }()
|
||||
imageReader := bufio.NewReader(imageFile)
|
||||
|
||||
metaFile, err := os.Open(metadataImagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer metaFile.Close()
|
||||
defer func() { _ = metaFile.Close() }()
|
||||
metaReader := bufio.NewReader(metaFile)
|
||||
|
||||
_, err = writer.Write([]byte{0xFF, soi})
|
||||
@ -166,6 +166,6 @@ func exifCopy(fromPath, toPath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(copyPath)
|
||||
defer func() { _ = os.Remove(copyPath) }()
|
||||
return copyMetadata(toPath, copyPath, fromPath)
|
||||
}
|
||||
|
14
go.mod
14
go.mod
@ -1,14 +1,14 @@
|
||||
module github.com/axllent/goptimize
|
||||
|
||||
go 1.21
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.22.2
|
||||
toolchain go1.23.1
|
||||
|
||||
require (
|
||||
github.com/axllent/ghru v1.2.1
|
||||
github.com/kovidgoyal/imaging v1.6.3
|
||||
github.com/spf13/pflag v1.0.5
|
||||
golang.org/x/image v0.15.0
|
||||
github.com/axllent/ghru/v2 v2.0.1
|
||||
github.com/kovidgoyal/imaging v1.6.4
|
||||
github.com/spf13/pflag v1.0.6
|
||||
golang.org/x/image v0.28.0
|
||||
)
|
||||
|
||||
require github.com/axllent/semver v0.0.1 // indirect
|
||||
require golang.org/x/mod v0.25.0 // indirect
|
||||
|
20
go.sum
20
go.sum
@ -1,10 +1,10 @@
|
||||
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/kovidgoyal/imaging v1.6.3 h1:iNPpv7ygiaB/NOztc6APMT7yr9UwBS+rOZwIbAdtyY8=
|
||||
github.com/kovidgoyal/imaging v1.6.3/go.mod h1:sHvcLOOVhJuto2IoNdPLEqnAUoL5ZfHEF0PpNH+882g=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
github.com/axllent/ghru/v2 v2.0.1 h1:/9XHbMqJRGRxlyNMq2XiHQpBLz7hlq8xCdgJ4ZhjCoM=
|
||||
github.com/axllent/ghru/v2 v2.0.1/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.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
||||
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
|
73
goptimize.go
73
goptimize.go
@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
// Goptimize downscales and optimizes an existing image
|
||||
func Goptimize(file string) {
|
||||
func goptimize(file string) {
|
||||
info, err := os.Stat(file)
|
||||
|
||||
if err != nil {
|
||||
@ -57,7 +57,7 @@ func Goptimize(file string) {
|
||||
|
||||
if format.String() == "GIF" {
|
||||
// return if GIF is animated - unsupported
|
||||
if err := IsGIFAnimated(file); err != nil {
|
||||
if err := isGIFAnimated(file); err != nil {
|
||||
fmt.Printf("Error: animated GIF not supported (%v)\n", file)
|
||||
return
|
||||
}
|
||||
@ -99,7 +99,7 @@ func Goptimize(file string) {
|
||||
return
|
||||
}
|
||||
|
||||
defer os.Remove(tmpFile.Name())
|
||||
defer func() { _ = os.Remove(tmpFile.Name()) }()
|
||||
|
||||
switch imgType := format.String(); imgType {
|
||||
case "JPEG":
|
||||
@ -127,15 +127,18 @@ 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
|
||||
if format.String() == "JPEG" {
|
||||
// run one or the other, running both has no advantage
|
||||
if jpegtran != "" {
|
||||
RunOptimizer(tmpFilename, true, jpegtran, "-optimize", "-outfile")
|
||||
runOptimizer(tmpFilename, true, jpegtran, "-optimize", "-outfile")
|
||||
} else if jpegoptim != "" {
|
||||
RunOptimizer(tmpFilename, false, jpegoptim, "-f", "-s", "-o")
|
||||
runOptimizer(tmpFilename, false, jpegoptim, "-f", "-s", "-o")
|
||||
}
|
||||
|
||||
if copyExif {
|
||||
@ -146,13 +149,13 @@ func Goptimize(file string) {
|
||||
}
|
||||
} else if format.String() == "PNG" {
|
||||
if pngquant != "" {
|
||||
RunOptimizer(tmpFilename, true, pngquant, "-f", "--output")
|
||||
runOptimizer(tmpFilename, true, pngquant, "-f", "--output")
|
||||
}
|
||||
if optipng != "" {
|
||||
RunOptimizer(tmpFilename, true, optipng, "-out")
|
||||
runOptimizer(tmpFilename, true, optipng, "-out")
|
||||
}
|
||||
} else if format.String() == "GIF" && gifsicle != "" {
|
||||
RunOptimizer(tmpFilename, true, gifsicle, "-o")
|
||||
runOptimizer(tmpFilename, true, gifsicle, "-o")
|
||||
}
|
||||
|
||||
// re-open modified temporary file
|
||||
@ -162,7 +165,7 @@ func Goptimize(file string) {
|
||||
return
|
||||
}
|
||||
|
||||
defer tmpFile.Close()
|
||||
defer func() { _ = tmpFile.Close() }()
|
||||
|
||||
// get th eoriginal file stats
|
||||
srcStat, _ := os.Stat(file)
|
||||
@ -187,7 +190,7 @@ func Goptimize(file string) {
|
||||
return
|
||||
}
|
||||
|
||||
defer out.Close()
|
||||
defer func() { _ = out.Close() }()
|
||||
|
||||
if _, err := io.Copy(out, tmpFile); err != nil {
|
||||
fmt.Printf("Error overwriting original file: %v\n", err)
|
||||
@ -201,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,
|
||||
// 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)
|
||||
@ -230,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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// RunOptimizer 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 RunOptimizer(src string, outFileArg bool, args ...string) {
|
||||
func runOptimizer(src string, outFileArg bool, args ...string) {
|
||||
// create a new temp file
|
||||
tmpFile, err := os.CreateTemp(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)
|
||||
@ -289,8 +295,8 @@ func RunOptimizer(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 {
|
||||
@ -301,7 +307,7 @@ func RunOptimizer(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)
|
||||
@ -315,9 +321,12 @@ func ByteCountSI(b int64) string {
|
||||
}
|
||||
|
||||
// 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()
|
||||
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 {
|
||||
@ -329,5 +338,5 @@ func IsGIFAnimated(gifFile string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("Animated gif")
|
||||
return fmt.Errorf("cannot optimize an animated gif")
|
||||
}
|
||||
|
68
main.go
68
main.go
@ -1,3 +1,4 @@
|
||||
// Package main is the main application
|
||||
package main
|
||||
|
||||
import (
|
||||
@ -9,7 +10,7 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/axllent/ghru"
|
||||
"github.com/axllent/ghru/v2"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
@ -27,6 +28,13 @@ var (
|
||||
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() {
|
||||
@ -47,15 +55,15 @@ func main() {
|
||||
|
||||
fmt.Println("\nDetected optimizers:")
|
||||
if err := displayDetectedOptimizer("jpegtran ", jpegtran); err != nil {
|
||||
displayDetectedOptimizer("jpegoptim", jpegoptim)
|
||||
_ = displayDetectedOptimizer("jpegoptim", jpegoptim)
|
||||
}
|
||||
displayDetectedOptimizer("optipng ", optipng)
|
||||
displayDetectedOptimizer("pngquant ", pngquant)
|
||||
displayDetectedOptimizer("gifsicle ", gifsicle)
|
||||
_ = displayDetectedOptimizer("optipng ", optipng)
|
||||
_ = displayDetectedOptimizer("pngquant ", pngquant)
|
||||
_ = displayDetectedOptimizer("gifsicle ", gifsicle)
|
||||
}
|
||||
|
||||
var maxSizes string
|
||||
var multiThreaded, update, showversion, showhelp bool
|
||||
var multiThreaded, update, showVersion, showHelp bool
|
||||
|
||||
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>)")
|
||||
@ -64,8 +72,8 @@ func main() {
|
||||
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")
|
||||
flag.BoolVarP(&showVersion, "version", "v", false, "show version number")
|
||||
flag.BoolVarP(&showHelp, "help", "h", false, "show help")
|
||||
|
||||
// third-party optimizers
|
||||
flag.StringVar(&jpegtran, "jpegtran", "jpegtran", "jpegtran binary")
|
||||
@ -77,7 +85,9 @@ func main() {
|
||||
flag.SortFlags = false
|
||||
|
||||
// parse args excluding os.Args[0]
|
||||
flag.Parse(os.Args[1:])
|
||||
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)
|
||||
@ -86,28 +96,44 @@ func main() {
|
||||
optipng, _ = exec.LookPath(optipng)
|
||||
pngquant, _ = exec.LookPath(pngquant)
|
||||
|
||||
if showhelp {
|
||||
if showHelp {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if showversion {
|
||||
fmt.Println(fmt.Sprintf("Version: %s", version))
|
||||
latest, _, _, err := ghru.Latest("axllent/goptimize", "goptimize")
|
||||
if err == nil && ghru.GreaterThan(latest, version) {
|
||||
fmt.Printf("Update available: %s\nRun `%s -u` to update.\n", latest, os.Args[0])
|
||||
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 := ghru.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\n", 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 {
|
||||
@ -155,7 +181,7 @@ func main() {
|
||||
for i := 0; i < threads; i++ {
|
||||
go func() {
|
||||
for nextFile := range processChan {
|
||||
Goptimize(nextFile)
|
||||
goptimize(nextFile)
|
||||
}
|
||||
// Channel was closed, so we finished this goroutine.
|
||||
wg.Done() // Let main goroutine know we are done.
|
||||
|
Loading…
x
Reference in New Issue
Block a user