mirror of
https://github.com/axllent/goptimize.git
synced 2025-07-05 16:38:25 -04:00
Compare commits
42 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 |
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
|
31
CHANGELOG.md
31
CHANGELOG.md
@ -1,21 +1,44 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [dev]
|
## [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 go mods - go (>= 1.11 required)
|
||||||
- Switch to axllent/semver for app updating
|
- Switch to axllent/semver for app updating
|
||||||
|
|
||||||
|
|
||||||
## [0.0.3]
|
## [0.0.3]
|
||||||
|
|
||||||
- Detect & skip animated GIFs
|
- Detect & skip animated GIFs
|
||||||
|
|
||||||
|
|
||||||
## [0.0.2]
|
## [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
|
||||||
|
|
||||||
|
|
||||||
## [0.0.1]
|
## [0.0.1]
|
||||||
|
|
||||||
- Initial release
|
- 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
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
and associated documentation files (the "Software"), to deal in the Software without restriction,
|
a copy of this software and associated documentation files (the
|
||||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
"Software"), to deal in the Software without restriction, including
|
||||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
furnished to do so, subject to the following conditions:
|
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
|
The above copyright notice and this permission notice shall be
|
||||||
substantial portions of the Software.
|
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
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
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,386)
|
|
29
README.md
29
README.md
@ -1,8 +1,8 @@
|
|||||||
# Goptimizer - downscales and optimizes images
|
# Goptimize - downscales and optimizes images
|
||||||
|
|
||||||
[](https://goreportcard.com/report/github.com/axllent/goptimize)
|
[](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 Go. 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):
|
||||||
|
|
||||||
@ -11,19 +11,17 @@ Image downscaling/rotation is done within goptimize (`-m <width>x<height>`, see
|
|||||||
- pngquant
|
- pngquant
|
||||||
- gifsicle
|
- gifsicle
|
||||||
|
|
||||||
|
|
||||||
## 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 can result in better optimization.
|
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 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).
|
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
|
## Usage options
|
||||||
|
|
||||||
@ -35,6 +33,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
|
||||||
@ -45,7 +44,6 @@ Options:
|
|||||||
--optipng string optipng binary (default "optipng")
|
--optipng string optipng binary (default "optipng")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
- `./goptimize image.png` - optimize a PNG file
|
- `./goptimize image.png` - optimize a PNG file
|
||||||
@ -53,23 +51,10 @@ Options:
|
|||||||
- `./goptimize -m 1200x0 image.jpg` - optimize and downscale a JPG file to a maximum size of width of 1200px
|
- `./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/`
|
- `./goptimize -o out/ image.jpg` - optimize a JPG file and save it to `out/`
|
||||||
|
|
||||||
|
|
||||||
## 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 you can install from source:
|
||||||
|
|
||||||
|
|
||||||
### Build requirements
|
|
||||||
|
|
||||||
Go >= 1.11 required.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
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?)
|
|
||||||
|
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)
|
||||||
|
}
|
14
go.mod
14
go.mod
@ -1,10 +1,14 @@
|
|||||||
module github.com/axllent/goptimize
|
module github.com/axllent/goptimize
|
||||||
|
|
||||||
go 1.13
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.23.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/axllent/ghru v1.1.3
|
github.com/axllent/ghru/v2 v2.0.1
|
||||||
github.com/disintegration/imaging v1.6.1
|
github.com/kovidgoyal/imaging v1.6.4
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.6
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8
|
golang.org/x/image v0.28.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require golang.org/x/mod v0.25.0 // indirect
|
||||||
|
22
go.sum
22
go.sum
@ -1,12 +1,10 @@
|
|||||||
github.com/axllent/ghru v1.1.3 h1:n0jYsuqCYaHHAR6DraXZl8hpBY4j0XV47y5Lyym/jGo=
|
github.com/axllent/ghru/v2 v2.0.1 h1:/9XHbMqJRGRxlyNMq2XiHQpBLz7hlq8xCdgJ4ZhjCoM=
|
||||||
github.com/axllent/ghru v1.1.3/go.mod h1:rFvMhcO1UAv2Cv6bXscS8EOc7qqNpfe8ZLp23utzs88=
|
github.com/axllent/ghru/v2 v2.0.1/go.mod h1:seMMjx8/0r5ZAL7c0vwTPIRoyN0AoTUqAylZEWZWGK4=
|
||||||
github.com/axllent/semver v0.0.0-20191103011746-394cefa91ee9 h1:LHNcCfePzgC/agAJs5a/5K3hFo8uW04bEdDKDkoX4do=
|
github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk=
|
||||||
github.com/axllent/semver v0.0.0-20191103011746-394cefa91ee9/go.mod h1:2xSPzvG8n9mRfdtxSvWvfTfQGWfHsMsHO1iZnKATMSc=
|
github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY=
|
||||||
github.com/disintegration/imaging v1.6.1 h1:JnBbK6ECIZb1NsWIikP9pd8gIlTIRx7fuDNpU9fsxOE=
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
github.com/disintegration/imaging v1.6.1/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
|
||||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
|
105
goptimize.go
105
goptimize.go
@ -2,23 +2,23 @@ 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"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
"github.com/kovidgoyal/imaging"
|
||||||
"golang.org/x/image/bmp"
|
"golang.org/x/image/bmp"
|
||||||
"golang.org/x/image/tiff"
|
"golang.org/x/image/tiff"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Goptimize downscales and optimizes an existing image
|
// Goptimize downscales and optimizes an existing image
|
||||||
func Goptimize(file string) {
|
func goptimize(file string) {
|
||||||
info, err := os.Stat(file)
|
info, err := os.Stat(file)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -31,12 +31,21 @@ func Goptimize(file string) {
|
|||||||
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("Error: %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)
|
||||||
@ -48,7 +57,7 @@ func Goptimize(file string) {
|
|||||||
|
|
||||||
if format.String() == "GIF" {
|
if format.String() == "GIF" {
|
||||||
// return if GIF is animated - unsupported
|
// 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)
|
fmt.Printf("Error: animated GIF not supported (%v)\n", file)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -83,14 +92,14 @@ 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("Error: 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 func() { _ = os.Remove(tmpFile.Name()) }()
|
||||||
|
|
||||||
switch imgType := format.String(); imgType {
|
switch imgType := format.String(); imgType {
|
||||||
case "JPEG":
|
case "JPEG":
|
||||||
@ -118,25 +127,35 @@ func Goptimize(file string) {
|
|||||||
|
|
||||||
// immediately close the temp file to release pointers
|
// immediately close the temp file to release pointers
|
||||||
// so we can modify it with system processes
|
// 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" {
|
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 != "" {
|
||||||
RunOptimizer(tmpFilename, true, jpegtran, "-optimize", "-outfile")
|
runOptimizer(tmpFilename, true, jpegtran, "-optimize", "-outfile")
|
||||||
} 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")
|
||||||
}
|
}
|
||||||
if optipng != "" {
|
if optipng != "" {
|
||||||
RunOptimizer(tmpFilename, true, optipng, "-out")
|
runOptimizer(tmpFilename, true, optipng, "-out")
|
||||||
}
|
}
|
||||||
} else if format.String() == "GIF" && gifsicle != "" {
|
} else if format.String() == "GIF" && gifsicle != "" {
|
||||||
RunOptimizer(tmpFilename, true, gifsicle, "-o")
|
runOptimizer(tmpFilename, true, gifsicle, "-o")
|
||||||
}
|
}
|
||||||
|
|
||||||
// re-open modified temporary file
|
// re-open modified temporary file
|
||||||
@ -146,7 +165,7 @@ func Goptimize(file string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer tmpFile.Close()
|
defer func() { _ = tmpFile.Close() }()
|
||||||
|
|
||||||
// get th eoriginal file stats
|
// get th eoriginal file stats
|
||||||
srcStat, _ := os.Stat(file)
|
srcStat, _ := os.Stat(file)
|
||||||
@ -171,7 +190,7 @@ func Goptimize(file string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer out.Close()
|
defer func() { _ = out.Close() }()
|
||||||
|
|
||||||
if _, err := io.Copy(out, tmpFile); err != nil {
|
if _, err := io.Copy(out, tmpFile); err != nil {
|
||||||
fmt.Printf("Error overwriting original file: %v\n", err)
|
fmt.Printf("Error overwriting original file: %v\n", err)
|
||||||
@ -185,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 {
|
} 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)
|
||||||
|
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 {
|
if err != nil {
|
||||||
fmt.Printf("Error opening original file: %v\n", err)
|
fmt.Printf("Error opening original file: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer out.Close()
|
defer func() { _ = orig.Close() }()
|
||||||
|
|
||||||
orig, _ := os.Open(file)
|
|
||||||
|
|
||||||
defer orig.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(out, orig); err != nil {
|
if _, err := io.Copy(out, orig); err != nil {
|
||||||
fmt.Printf("Error ovewriting original file: %v\n", err)
|
fmt.Printf("Error ovewriting original file: %v\n", err)
|
||||||
@ -214,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 {
|
} else {
|
||||||
// we didn't actually change anything
|
// 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
|
// 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(), "goptimize-")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Cannot create temporary file: %v\n", err)
|
fmt.Printf("Cannot create temporary file: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer os.Remove(tmpFile.Name())
|
defer func() { _ = os.Remove(tmpFile.Name()) }()
|
||||||
|
|
||||||
source, err := os.Open(src)
|
source, err := os.Open(src)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Cannot open temporary file: %v\n", err)
|
fmt.Printf("Cannot open temporary file: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer source.Close()
|
defer func() { _ = source.Close() }()
|
||||||
|
|
||||||
if _, err := io.Copy(tmpFile, source); err != nil {
|
if _, err := io.Copy(tmpFile, source); err != nil {
|
||||||
fmt.Printf("Cannot copy source file: %v\n", err)
|
fmt.Printf("Cannot copy source file: %v\n", err)
|
||||||
@ -273,8 +295,8 @@ func RunOptimizer(src string, outFileArg bool, args ...string) {
|
|||||||
dstSize := dstStat.Size()
|
dstSize := dstStat.Size()
|
||||||
|
|
||||||
// ensure file pointers are closed before renaming
|
// ensure file pointers are closed before renaming
|
||||||
tmpFile.Close()
|
func() { _ = tmpFile.Close() }()
|
||||||
source.Close()
|
func() { _ = source.Close() }()
|
||||||
|
|
||||||
if dstSize < srcSize {
|
if dstSize < srcSize {
|
||||||
if err := os.Rename(tmpFilename, src); err != nil {
|
if err := os.Rename(tmpFilename, src); err != nil {
|
||||||
@ -285,7 +307,7 @@ func RunOptimizer(src string, outFileArg bool, args ...string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ByteCountSI returns a human readable size from int64 bytes
|
// ByteCountSI returns a human readable size from int64 bytes
|
||||||
func ByteCountSI(b int64) string {
|
func byteCountSI(b int64) string {
|
||||||
const unit = 1000
|
const unit = 1000
|
||||||
if b < unit {
|
if b < unit {
|
||||||
return fmt.Sprintf("%dB", b)
|
return fmt.Sprintf("%dB", b)
|
||||||
@ -299,9 +321,12 @@ func ByteCountSI(b int64) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsGIFAnimated will return an error if the GIF file has more than 1 frame
|
// IsGIFAnimated will return an error if the GIF file has more than 1 frame
|
||||||
func IsGIFAnimated(gifFile string) error {
|
func isGIFAnimated(gifFile string) error {
|
||||||
file, _ := os.Open(gifFile)
|
file, err := os.Open(gifFile)
|
||||||
defer file.Close()
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot open GIF file: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = file.Close() }()
|
||||||
|
|
||||||
g, err := gif.DecodeAll(file)
|
g, err := gif.DecodeAll(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -313,5 +338,5 @@ func IsGIFAnimated(gifFile string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("Animated gif")
|
return fmt.Errorf("cannot optimize an animated gif")
|
||||||
}
|
}
|
||||||
|
107
main.go
107
main.go
@ -1,3 +1,4 @@
|
|||||||
|
// Package main is the main application
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -5,9 +6,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/axllent/ghru"
|
"github.com/axllent/ghru/v2"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,7 +25,16 @@ var (
|
|||||||
optipng string
|
optipng string
|
||||||
pngquant string
|
pngquant string
|
||||||
gifsicle string
|
gifsicle string
|
||||||
|
copyExif bool
|
||||||
|
threads = 1
|
||||||
version = "dev"
|
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() {
|
func main() {
|
||||||
@ -42,24 +54,26 @@ 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(&showversion, "version", "v", false, "show version number")
|
flag.BoolVarP(&multiThreaded, "threaded", "t", false, "run multi-threaded (use all CPU cores)")
|
||||||
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
|
// third-party optimizers
|
||||||
flag.StringVar(&jpegtran, "jpegtran", "jpegtran", "jpegtran binary")
|
flag.StringVar(&jpegtran, "jpegtran", "jpegtran", "jpegtran binary")
|
||||||
@ -71,7 +85,9 @@ func main() {
|
|||||||
flag.SortFlags = false
|
flag.SortFlags = false
|
||||||
|
|
||||||
// parse args excluding os.Args[0]
|
// 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
|
// detect optimizer paths
|
||||||
gifsicle, _ = exec.LookPath(gifsicle)
|
gifsicle, _ = exec.LookPath(gifsicle)
|
||||||
@ -80,28 +96,44 @@ func main() {
|
|||||||
optipng, _ = exec.LookPath(optipng)
|
optipng, _ = exec.LookPath(optipng)
|
||||||
pngquant, _ = exec.LookPath(pngquant)
|
pngquant, _ = exec.LookPath(pngquant)
|
||||||
|
|
||||||
if showhelp {
|
if showHelp {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if showversion {
|
if showVersion {
|
||||||
fmt.Println(fmt.Sprintf("Version: %s", version))
|
fmt.Printf("Version: %s\n", version)
|
||||||
latest, _, _, err := ghru.Latest("axllent/goptimize", "goptimize")
|
|
||||||
if err == nil && ghru.GreaterThan(latest, version) {
|
release, err := ghruConf.Latest()
|
||||||
fmt.Printf("Update available: %s\nRun `%s -u` to update.\n", latest, os.Args[0])
|
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 {
|
if update {
|
||||||
rel, err := ghru.Update("axllent/goptimize", "goptimize", version)
|
// Update the app
|
||||||
|
rel, err := ghruConf.SelfUpdate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err.Error())
|
||||||
os.Exit(1)
|
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 {
|
if len(flag.Args()) < 1 {
|
||||||
@ -137,13 +169,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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user