mirror of
https://github.com/axllent/goptimize.git
synced 2024-06-09 10:10:54 +00:00
Compare commits
52 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 |
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
|||
/dist/
|
||||
goptimize
|
||||
goptimize*
|
37
CHANGELOG.md
37
CHANGELOG.md
|
@ -1,5 +1,42 @@
|
|||
# Changelog
|
||||
|
||||
## [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
|
||||
|
|
5
Makefile
5
Makefile
|
@ -5,8 +5,7 @@ LDFLAGS=-ldflags "-s -w -X main.version=${VERSION}"
|
|||
build = echo "\n\nBuilding $(1)-$(2)" && GOOS=$(1) GOARCH=$(2) go build ${LDFLAGS} -o dist/goptimize_${VERSION}_$(1)_$(2) \
|
||||
&& bzip2 dist/goptimize_${VERSION}_$(1)_$(2)
|
||||
|
||||
goptimize: *.go
|
||||
go get github.com/disintegration/imaging
|
||||
goptimize: *.go go.*
|
||||
go build ${LDFLAGS} -o goptimize
|
||||
rm -rf /tmp/go-*
|
||||
|
||||
|
@ -21,4 +20,4 @@ release:
|
|||
$(call build,linux,arm)
|
||||
$(call build,linux,arm64)
|
||||
$(call build,darwin,amd64)
|
||||
$(call build,darwin,386)
|
||||
$(call build,darwin,arm64)
|
||||
|
|
66
README.md
66
README.md
|
@ -1,11 +1,12 @@
|
|||
# Goptimizer - downscales and optimizes images
|
||||
|
||||
Goptimizer is a commandline utility written in Golang. It downscales and optimize existing images JPEG, PNG and Gif files.
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/axllent/goptimize)](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):
|
||||
Goptimizer is a commandline utility written in Golang. 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
|
||||
|
@ -13,14 +14,16 @@ Image downscaling is done within Goptimize (`-m <width>x<height>`, see [Usage](#
|
|||
|
||||
## 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.
|
||||
Goptimize will remove all exif data from JPEG files, auto-rotating those that depend on it for orientation.
|
||||
|
||||
It will also preserve (by default) the file's original modification times (`-p=false` to disable).
|
||||
|
||||
Animated GIF files are not supported and automatically get skipped.
|
||||
|
||||
|
||||
## Usage options
|
||||
|
||||
|
@ -28,23 +31,19 @@ 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")
|
||||
```
|
||||
|
||||
|
||||
|
@ -54,3 +53,24 @@ Options:
|
|||
- `./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
|
||||
|
||||
|
||||
### Build requirements
|
||||
|
||||
Go >= 1.21 required.
|
||||
|
||||
```
|
||||
go get github.com/axllent/goptimize
|
||||
```
|
||||
|
||||
## 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 outFile.Close()
|
||||
writer := bufio.NewWriter(outFile)
|
||||
|
||||
imageFile, err := os.Open(imagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer imageFile.Close()
|
||||
imageReader := bufio.NewReader(imageFile)
|
||||
|
||||
metaFile, err := os.Open(metadataImagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer metaFile.Close()
|
||||
metaReader := bufio.NewReader(metaFile)
|
||||
|
||||
_, err = writer.Write([]byte{0xFF, soi})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy metadata segments
|
||||
// It seems that they need to come first!
|
||||
err = copySegments(writer, metaReader, isMetaTagType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Copy all non-metadata segments
|
||||
err = copySegments(writer, imageReader, func(tagType byte) bool {
|
||||
return !isMetaTagType(tagType)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = writer.Write([]byte{0xFF, eoi})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Flush the writer, otherwise the last couple buffered writes (including the EOI) won't get written!
|
||||
return writer.Flush()
|
||||
}
|
||||
|
||||
func exifCopy(fromPath, toPath string) error {
|
||||
copyPath := toPath + "~"
|
||||
err := os.Rename(toPath, copyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(copyPath)
|
||||
return copyMetadata(toPath, copyPath, fromPath)
|
||||
}
|
14
go.mod
Normal file
14
go.mod
Normal file
|
@ -0,0 +1,14 @@
|
|||
module github.com/axllent/goptimize
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.22.2
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
require github.com/axllent/semver v0.0.1 // indirect
|
10
go.sum
Normal file
10
go.sum
Normal file
|
@ -0,0 +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=
|
106
goptimize.go
106
goptimize.go
|
@ -2,17 +2,17 @@ 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"
|
||||
)
|
||||
|
@ -22,31 +22,47 @@ 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())
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -112,23 +129,30 @@ func Goptimize(file string) {
|
|||
// so we can modify it with system processes
|
||||
tmpFile.Close()
|
||||
|
||||
// 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
|
||||
|
@ -166,7 +190,7 @@ func Goptimize(file string) {
|
|||
defer 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
|
||||
}
|
||||
|
||||
|
@ -179,7 +203,7 @@ func Goptimize(file string) {
|
|||
|
||||
fmt.Printf("Goptimized %s (%dx%d %s > %s %v%%)\n", dstFile, resultW, resultH, ByteCountSI(srcSize), ByteCountSI(dstSize), savedPercent)
|
||||
} 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)
|
||||
|
@ -215,11 +239,11 @@ func Goptimize(file string) {
|
|||
|
||||
}
|
||||
|
||||
// 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(), "Goptimized-")
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Cannot create temporary file: %v\n", err)
|
||||
|
@ -289,3 +313,21 @@ func ByteCountSI(b int64) string {
|
|||
}
|
||||
return fmt.Sprintf("%.1f%cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
// IsGIFAnimated will return an error if the GIF file has more than 1 frame
|
||||
func IsGIFAnimated(gifFile string) error {
|
||||
file, _ := os.Open(gifFile)
|
||||
defer file.Close()
|
||||
|
||||
g, err := gif.DecodeAll(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Single frame = OK
|
||||
if len(g.Image) == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("Animated gif")
|
||||
}
|
||||
|
|
97
main.go
97
main.go
|
@ -1,14 +1,16 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/axllent/gitrel"
|
||||
"github.com/axllent/ghru"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -22,15 +24,21 @@ var (
|
|||
optipng string
|
||||
pngquant string
|
||||
gifsicle string
|
||||
copyExif bool
|
||||
threads = 1
|
||||
version = "dev"
|
||||
)
|
||||
|
||||
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 +46,38 @@ 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]
|
||||
flag.Parse(os.Args[1:])
|
||||
|
||||
// detect optimizer paths
|
||||
gifsicle, _ = exec.LookPath(gifsicle)
|
||||
|
@ -73,22 +86,27 @@ func main() {
|
|||
optipng, _ = exec.LookPath(optipng)
|
||||
pngquant, _ = exec.LookPath(pngquant)
|
||||
|
||||
if showhelp {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if showversion {
|
||||
fmt.Println(fmt.Sprintf("Version: %s", version))
|
||||
latest, _, _, err := gitrel.Latest("axllent/goptimize", "goptimize")
|
||||
if err == nil && latest != 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])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if update {
|
||||
rel, err := gitrel.Update("axllent/goptimize", "goptimize", version)
|
||||
rel, err := ghru.Update("axllent/goptimize", "goptimize", version)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Updated %s to version %s", os.Args[0], rel)
|
||||
fmt.Printf("Updated %s to version %s\n", os.Args[0], rel)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -125,13 +143,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
|
||||
|
|
Loading…
Reference in New Issue
Block a user