25 Commits
0.0.1 ... 0.1.0

Author SHA1 Message Date
Ralph Slooten
f9bf16d6c4 Merge branch 'release/0.1.0' 2019-11-03 19:18:37 +13:00
Ralph Slooten
6c1aabaa70 0.1.0 2019-11-03 19:18:03 +13:00
Ralph Slooten
8c3e478384 Switch to axllent/ghru 2019-11-03 19:12:02 +13:00
Ralph Slooten
7ffb944b0f Switch to go mods 2019-10-26 22:19:46 +13:00
Ralph Slooten
cb4bde8c4e Update README 2019-09-07 23:11:06 +12:00
Ralph Slooten
49d7b9b12f Update README 2019-09-07 23:08:43 +12:00
Ralph Slooten
fcdbf3e90b Merge tag '0.0.3' into develop
Release 0.0.3
2019-09-07 22:52:48 +12:00
Ralph Slooten
5096e6fbf0 Merge branch 'release/0.0.3' 2019-09-07 22:52:45 +12:00
Ralph Slooten
d7d827fed7 Merge branch 'feature/gif' into develop 2019-09-07 22:51:55 +12:00
Ralph Slooten
19cf54b633 0.0.3 2019-09-07 22:51:37 +12:00
Ralph Slooten
57b1d33129 Detect & skip animated GIFs 2019-09-07 22:51:24 +12:00
Ralph Slooten
c239a54ead Add Go report card 2019-09-07 15:16:46 +12:00
Ralph Slooten
e6296f4719 Merge tag '0.0.2' into develop
Release 0.0.2
2019-08-24 23:32:00 +12:00
Ralph Slooten
ee6aa69087 Merge branch 'release/0.0.2' 2019-08-24 23:31:58 +12:00
Ralph Slooten
8362617920 Add installation instructions 2019-08-24 23:29:25 +12:00
Ralph Slooten
b157fcd72b Update README 2019-08-24 23:26:24 +12:00
Ralph Slooten
0c000fe262 Reorder flags 2019-08-24 23:22:19 +12:00
Ralph Slooten
97fa6c94c5 Switch to pflag 2019-08-24 23:17:55 +12:00
Ralph Slooten
36e9b4c8fa Typo 2019-08-23 19:17:56 +12:00
Ralph Slooten
5df486c53e Add all deps to Makefile 2019-08-03 19:21:42 +12:00
Ralph Slooten
120e5fefde Fix typo 2019-08-03 14:22:52 +12:00
Ralph Slooten
d2a591a6d9 Add a TODO 2019-08-03 11:15:54 +12:00
Ralph Slooten
8e5e7b6098 New line 2019-08-03 10:45:44 +12:00
Ralph Slooten
47024e030b Add update/version to README 2019-08-03 10:44:59 +12:00
Ralph Slooten
45915d4b42 Merge tag '0.0.1' into develop
Release 0.0.1
2019-08-03 10:40:03 +12:00
8 changed files with 163 additions and 69 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,2 @@
/dist/ /dist/
goptimize goptimize*

View File

@@ -1,5 +1,21 @@
# Changelog # Changelog
## [dev]
- 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] ## [0.0.1]
- Initial release - Initial release

View File

@@ -5,8 +5,7 @@ LDFLAGS=-ldflags "-s -w -X main.version=${VERSION}"
build = echo "\n\nBuilding $(1)-$(2)" && GOOS=$(1) GOARCH=$(2) go build ${LDFLAGS} -o dist/goptimize_${VERSION}_$(1)_$(2) \ build = echo "\n\nBuilding $(1)-$(2)" && GOOS=$(1) GOARCH=$(2) go build ${LDFLAGS} -o dist/goptimize_${VERSION}_$(1)_$(2) \
&& bzip2 dist/goptimize_${VERSION}_$(1)_$(2) && bzip2 dist/goptimize_${VERSION}_$(1)_$(2)
goptimize: *.go goptimize: *.go go.*
go get github.com/disintegration/imaging
go build ${LDFLAGS} -o goptimize go build ${LDFLAGS} -o goptimize
rm -rf /tmp/go-* rm -rf /tmp/go-*

View File

@@ -1,11 +1,12 @@
# Goptimizer - downscales and optimizes images # Goptimizer - downscales and optimizes images
Goptimizer is a commandline utility written in Golang. It downscales and 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 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`)
- jpegtran (`libjpeg-turbo-progs`) or jpegoptim
- optipng - optipng
- pngquant - pngquant
- gifsicle - gifsicle
@@ -13,14 +14,16 @@ Image downscaling is done within Goptimize (`-m <width>x<height>`, see [Usage](#
## Notes ## Notes
Both `jpegoptim` & `jpegtran` have almost identical optimization, so if both are installed then just `jpegtran` is used for JPG optimization. PNG optimization however will run through both `optipng` & `pngquant` (if installed) as this has definite advantages. Both `jpegoptim` & `jpegtran` have almost identical optimization, so if both are installed then just `jpegtran` is used for JPG optimization. PNG optimization however will run through both `optipng` & `pngquant` (if installed) as this can result in better optimization.
It is highly recommended to install the necessary optimization tools, however they are not required to run goptimize. It is highly recommended to install the necessary optimization tools, however they are not required to run goptimize.
Goptimize will remove all exif data from JPEG files, auto-rotating those that relied on it. Goptimize will remove all exif data from JPEG files, auto-rotating those that depend on it for orientation.
It will also preserve (by default) the file's original modification times (`-p=false` to disable). It will also preserve (by default) the file's original modification times (`-p=false` to disable).
Animated GIF files are not supported and automatically get skipped.
## Usage options ## Usage options
@@ -28,23 +31,18 @@ It will also preserve (by default) the file's original modification times (`-p=f
Usage: ./goptimize [options] <images> Usage: ./goptimize [options] <images>
Options: Options:
-gifsicle string -q, --quality int quality, JPEG only (default 75)
gifsicle binary (default "gifsicle") -m, --max string downscale to a maximum width & height in pixels (<width>x<height>)
-jpegoptim string -o, --out string output directory (default overwrites original)
jpegoptim binary (default "jpegoptim") -p, --preserve preserve file modification times (default true)
-jpegtran string -u, --update update to latest release
jpegtran binary (default "jpegtran") -v, --version show version number
-m string -h, --help show help
downscale to a maximum width & height in pixels (<width>x<height>) --jpegtran string jpegtran binary (default "jpegtran")
-o string --jpegoptim string jpegoptim binary (default "jpegoptim")
output directory (default overwrites original) --gifsicle string gifsicle binary (default "gifsicle")
-optipng string --pngquant string pngquant binary (default "pngquant")
optipng binary (default "optipng") --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)
``` ```
@@ -54,3 +52,24 @@ Options:
- `./goptimize -m 800x800 *` - optimize and downscale all image files to a maximum size of 800x800px - `./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 -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
Download the appropriate binary from the [releases](https://github.com/axllent/goptimize/releases/latest), or if you have golang installed
### Build requirements
Go >= 1.11 required.
```
go get github.com/axllent/goptimize
```
## TODO
Some ideas for the future:
- Dry run
- Option to copy exif data (how?)

10
go.mod Normal file
View File

@@ -0,0 +1,10 @@
module github.com/axllent/goptimize
go 1.13
require (
github.com/axllent/ghru v1.1.3
github.com/disintegration/imaging v1.6.1
github.com/spf13/pflag v1.0.5
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8
)

12
go.sum Normal file
View File

@@ -0,0 +1,12 @@
github.com/axllent/ghru v1.1.3 h1:n0jYsuqCYaHHAR6DraXZl8hpBY4j0XV47y5Lyym/jGo=
github.com/axllent/ghru v1.1.3/go.mod h1:rFvMhcO1UAv2Cv6bXscS8EOc7qqNpfe8ZLp23utzs88=
github.com/axllent/semver v0.0.0-20191103011746-394cefa91ee9 h1:LHNcCfePzgC/agAJs5a/5K3hFo8uW04bEdDKDkoX4do=
github.com/axllent/semver v0.0.0-20191103011746-394cefa91ee9/go.mod h1:2xSPzvG8n9mRfdtxSvWvfTfQGWfHsMsHO1iZnKATMSc=
github.com/disintegration/imaging v1.6.1 h1:JnBbK6ECIZb1NsWIikP9pd8gIlTIRx7fuDNpU9fsxOE=
github.com/disintegration/imaging v1.6.1/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
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.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
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=

View File

@@ -22,31 +22,38 @@ func Goptimize(file string) {
info, err := os.Stat(file) info, err := os.Stat(file)
if err != nil { if err != nil {
fmt.Printf("%s doesn't exist\n", file) fmt.Printf("Error: %s doesn't exist\n", file)
return return
} }
if !info.Mode().IsRegular() { if !info.Mode().IsRegular() {
// not a file fmt.Printf("Error: %s is not a file\n", file)
fmt.Printf("%s is not a file\n", file)
return return
} }
// open original, rotate if neccesary // open original, rotate if necessary
src, err := imaging.Open(file, imaging.AutoOrientation(true)) src, err := imaging.Open(file, imaging.AutoOrientation(true))
if err != nil { if err != nil {
fmt.Printf("%v (%s)\n", err, file) fmt.Printf("Error: %v (%s)\n", err, file)
return return
} }
format, err := imaging.FormatFromFilename(file) format, err := imaging.FormatFromFilename(file)
if err != nil { if err != nil {
fmt.Printf("Cannot detect format: %v\n", err) fmt.Printf("Error: cannot detect format: %v\n", err)
return return
} }
if format.String() == "GIF" {
// return if GIF is animated - unsupported
if err := IsGIFAnimated(file); err != nil {
fmt.Printf("Error: animated GIF not supported (%v)\n", file)
return
}
}
outFilename := filepath.Base(file) outFilename := filepath.Base(file)
outDir := filepath.Dir(file) outDir := filepath.Dir(file)
dstFile := filepath.Join(outDir, outFilename) dstFile := filepath.Join(outDir, outFilename)
@@ -60,7 +67,7 @@ func Goptimize(file string) {
srcW := srcBounds.Dx() srcW := srcBounds.Dx()
srcH := srcBounds.Dy() srcH := srcBounds.Dy()
// Ensure scaling does not upscale image // do not upscale image
imgMaxW := maxWidth imgMaxW := maxWidth
if imgMaxW == 0 || imgMaxW > srcW { if imgMaxW == 0 || imgMaxW > srcW {
imgMaxW = srcW imgMaxW = srcW
@@ -79,24 +86,25 @@ func Goptimize(file string) {
tmpFile, err := ioutil.TempFile(os.TempDir(), "Goptimized-") tmpFile, err := ioutil.TempFile(os.TempDir(), "Goptimized-")
if err != nil { if err != nil {
fmt.Printf("Cannot create temporary file: %v\n", err) fmt.Printf("Error: cannot create temporary file: %v\n", err)
return return
} }
defer os.Remove(tmpFile.Name()) defer os.Remove(tmpFile.Name())
if format.String() == "JPEG" { switch imgType := format.String(); imgType {
case "JPEG":
err = jpeg.Encode(tmpFile, resized, &jpeg.Options{Quality: quality}) err = jpeg.Encode(tmpFile, resized, &jpeg.Options{Quality: quality})
} else if format.String() == "PNG" { case "PNG":
err = png.Encode(tmpFile, resized) err = png.Encode(tmpFile, resized)
} else if format.String() == "GIF" { case "GIF":
err = gif.Encode(tmpFile, resized, nil) err = gif.Encode(tmpFile, resized, nil)
} else if format.String() == "TIFF" { case "TIFF":
err = tiff.Encode(tmpFile, resized, nil) err = tiff.Encode(tmpFile, resized, nil)
} else if format.String() == "BMP" { case "BMP":
err = bmp.Encode(tmpFile, resized) err = bmp.Encode(tmpFile, resized)
} else { default:
fmt.Printf("Unsupported file type %s\n", file) fmt.Printf("Error: unsupported file type (%s)\n", file)
return return
} }
@@ -112,23 +120,23 @@ func Goptimize(file string) {
// so we can modify it with system processes // so we can modify it with system processes
tmpFile.Close() tmpFile.Close()
// Run through optimizers // run through optimizers
if format.String() == "JPEG" { if format.String() == "JPEG" {
// run one or the other, running both has no advantage // run one or the other, running both has no advantage
if jpegtran != "" { if jpegtran != "" {
RunOptimiser(tmpFilename, true, jpegtran, "-optimize", "-outfile") RunOptimizer(tmpFilename, true, jpegtran, "-optimize", "-outfile")
} else if jpegoptim != "" { } else if jpegoptim != "" {
RunOptimiser(tmpFilename, false, jpegoptim, "-f", "-s", "-o") RunOptimizer(tmpFilename, false, jpegoptim, "-f", "-s", "-o")
} }
} else if format.String() == "PNG" { } else if format.String() == "PNG" {
if pngquant != "" { if pngquant != "" {
RunOptimiser(tmpFilename, true, pngquant, "-f", "--output") RunOptimizer(tmpFilename, true, pngquant, "-f", "--output")
} }
if optipng != "" { if optipng != "" {
RunOptimiser(tmpFilename, true, optipng, "-out") RunOptimizer(tmpFilename, true, optipng, "-out")
} }
} else if format.String() == "GIF" && gifsicle != "" { } else if format.String() == "GIF" && gifsicle != "" {
RunOptimiser(tmpFilename, true, gifsicle, "-o") RunOptimizer(tmpFilename, true, gifsicle, "-o")
} }
// re-open modified temporary file // re-open modified temporary file
@@ -166,7 +174,7 @@ func Goptimize(file string) {
defer out.Close() defer out.Close()
if _, err := io.Copy(out, tmpFile); err != nil { 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 return
} }
@@ -179,7 +187,7 @@ func Goptimize(file string) {
fmt.Printf("Goptimized %s (%dx%d %s > %s %v%%)\n", dstFile, resultW, resultH, ByteCountSI(srcSize), ByteCountSI(dstSize), savedPercent) fmt.Printf("Goptimized %s (%dx%d %s > %s %v%%)\n", dstFile, resultW, resultH, ByteCountSI(srcSize), ByteCountSI(dstSize), savedPercent)
} else { } else {
// If the output directory is not the same, // if the output directory is not the same,
// then write a copy of the original file // then write a copy of the original file
if outputDir != "" { if outputDir != "" {
out, err := os.Create(dstFile) out, err := os.Create(dstFile)
@@ -215,9 +223,9 @@ 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 // 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 // create a new temp file
tmpFile, err := ioutil.TempFile(os.TempDir(), "Goptimized-") tmpFile, err := ioutil.TempFile(os.TempDir(), "Goptimized-")
@@ -289,3 +297,21 @@ func ByteCountSI(b int64) string {
} }
return fmt.Sprintf("%.1f%cB", float64(b)/float64(div), "kMGTPE"[exp]) return fmt.Sprintf("%.1f%cB", float64(b)/float64(div), "kMGTPE"[exp])
} }
// IsGIFAnimated will return an error if the GIF file has more than 1 frame
func IsGIFAnimated(gifFile string) error {
file, _ := os.Open(gifFile)
defer file.Close()
g, err := gif.DecodeAll(file)
if err != nil {
return err
}
// Single frame = OK
if len(g.Image) == 1 {
return nil
}
return fmt.Errorf("Animated gif")
}

48
main.go
View File

@@ -1,14 +1,14 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"regexp" "regexp"
"strconv" "strconv"
"github.com/axllent/gitrel" "github.com/axllent/ghru"
"github.com/spf13/pflag"
) )
var ( var (
@@ -26,11 +26,15 @@ var (
) )
func main() { func main() {
// set up new flag instance
flag := pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)
// set the default help // set the default help
flag.Usage = func() { flag.Usage = func() {
fmt.Println("Goptimize - downscales and optimizes images") fmt.Println("Goptimize - downscales and optimizes images")
fmt.Printf("\nUsage: %s [options] <images>\n", os.Args[0]) fmt.Printf("\nUsage: %s [options] <images>\n", os.Args[0])
fmt.Println("\nOptions:") fmt.Println("\nOptions:")
flag.SortFlags = false
flag.PrintDefaults() flag.PrintDefaults()
fmt.Println("\nExamples:") fmt.Println("\nExamples:")
fmt.Printf(" %s image.png\n", os.Args[0]) fmt.Printf(" %s image.png\n", os.Args[0])
@@ -47,24 +51,27 @@ func main() {
} }
var maxSizes string var maxSizes string
var update, showversion bool var update, showversion, showhelp bool
flag.IntVar(&quality, "q", 75, "quality - JPEG only") flag.IntVarP(&quality, "quality", "q", 75, "quality, JPEG only")
flag.StringVar(&outputDir, "o", "", "output directory (default overwrites original)") flag.StringVarP(&maxSizes, "max", "m", "", "downscale to a maximum width & height in pixels (<width>x<height>)")
flag.BoolVar(&preserveModTimes, "p", true, "preserve file modification times") flag.StringVarP(&outputDir, "out", "o", "", "output directory (default overwrites original)")
flag.StringVar(&maxSizes, "m", "", "downscale to a maximum width & height in pixels (<width>x<height>)") flag.BoolVarP(&preserveModTimes, "preserve", "p", true, "preserve file modification times")
flag.BoolVar(&update, "u", false, "update to latest release") flag.BoolVarP(&update, "update", "u", false, "update to latest release")
flag.BoolVar(&showversion, "v", false, "show version number") 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(&gifsicle, "gifsicle", "gifsicle", "gifsicle binary")
flag.StringVar(&jpegoptim, "jpegoptim", "jpegoptim", "jpegoptim binary")
flag.StringVar(&jpegtran, "jpegtran", "jpegtran", "jpegtran 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(&pngquant, "pngquant", "pngquant", "pngquant binary")
flag.StringVar(&optipng, "optipng", "optipng", "optipng binary")
// parse flags flag.SortFlags = false
flag.Parse()
// parse args excluding os.Args[0]
flag.Parse(os.Args[1:])
// detect optimizer paths // detect optimizer paths
gifsicle, _ = exec.LookPath(gifsicle) gifsicle, _ = exec.LookPath(gifsicle)
@@ -73,22 +80,27 @@ func main() {
optipng, _ = exec.LookPath(optipng) optipng, _ = exec.LookPath(optipng)
pngquant, _ = exec.LookPath(pngquant) pngquant, _ = exec.LookPath(pngquant)
if showhelp {
flag.Usage()
os.Exit(1)
}
if showversion { if showversion {
fmt.Println(fmt.Sprintf("Version: %s", version)) fmt.Println(fmt.Sprintf("Version: %s", version))
latest, _, _, err := gitrel.Latest("axllent/goptimize", "goptimize") latest, _, _, err := ghru.Latest("axllent/goptimize", "goptimize")
if err == nil && latest != version { if err == nil && ghru.GreaterThan(latest, version) {
fmt.Printf("Update available: %s\nRun `%s -u` to update.\n", latest, os.Args[0]) fmt.Printf("Update available: %s\nRun `%s -u` to update.\n", latest, os.Args[0])
} }
return return
} }
if update { if update {
rel, err := gitrel.Update("axllent/goptimize", "goptimize", version) rel, err := ghru.Update("axllent/goptimize", "goptimize", version)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
fmt.Printf("Updated %s to version %s", os.Args[0], rel) fmt.Printf("Updated %s to version %s\n", os.Args[0], rel)
return return
} }