Skip to content

Commit

Permalink
Merge pull request #22 from Pixboost/feature/avif-target-size
Browse files Browse the repository at this point in the history
Feature/avif target size
  • Loading branch information
dooman87 authored Dec 22, 2020
2 parents a75bd07 + 6118434 commit 78a4c96
Show file tree
Hide file tree
Showing 30 changed files with 394 additions and 160 deletions.
6 changes: 5 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ services:
- docker

go:
- "1.8"
- "1.15"

script:
- docker run --entrypoint=/go/src/github.com/Pixboost/transformimgs/test.sh -v $(pwd):/go/src/github.com/Pixboost/transformimgs transformimgs

before_install:
- docker build -t transformimgs -f Dockerfile.dev .

after_success:
- bash <(curl -s https://codecov.io/bash)

5 changes: 1 addition & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
FROM golang:1.14-buster AS build

#Installing godeps
RUN go get github.com/golang/dep/cmd/dep

RUN mkdir -p /go/src/github.com/Pixboost/
WORKDIR /go/src/github.com/Pixboost/
RUN git clone https://github.com/Pixboost/transformimgs.git

WORKDIR /go/src/github.com/Pixboost/transformimgs/
RUN dep ensure
RUN go mod vendor

WORKDIR /go/src/github.com/Pixboost/transformimgs/cmd

Expand Down
3 changes: 0 additions & 3 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ ENV PATH $GOPATH/bin:$PATH
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
WORKDIR $GOPATH

#Installing godeps
RUN go get github.com/golang/dep/cmd/dep

ENV IM_HOME /usr/local/bin

VOLUME /go/src/github.com/Pixboost/transformimgs/
Expand Down
27 changes: 16 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
# TransformImgs

[![Build Status](https://travis-ci.org/Pixboost/transformimgs.svg?branch=master)](https://travis-ci.org/Pixboost/transformimgs)
[![codecov](https://codecov.io/gh/Pixboost/transformimgs/branch/master/graph/badge.svg)](https://codecov.io/gh/Pixboost/transformimgs)
[![Docker Pulls](https://img.shields.io/docker/pulls/pixboost/transformimgs)](https://hub.docker.com/r/pixboost/transformimgs/)
[![Docker Automated build](https://img.shields.io/docker/automated/jrottenberg/ffmpeg.svg)](https://hub.docker.com/r/pixboost/transformimgs/)

Image transformations web service. Provides Http API to image
manipulation operations backed by [Imagemagick](http://imagemagick.org) CLI.
Open Source [Image CDN](https://web.dev/image-cdns/) that provides image transformation API and supports
the latest image formats, such as WebP, AVIF.

There are two ways of using the service:

* Deploy on your own infrastructure using docker image
* Use as SaaS at [pixboost.com](https://pixboost.com?source=github)

## Table of Contents

Expand Down Expand Up @@ -62,21 +69,19 @@ $ docker-compose up

### Building and Running from sources

Dependencies:
Prerequisites:

* Go 1.8+
* [Gorilla MUX](https://github.com/gorilla/mux) for HTTP routing
* [kolibri](https://github.com/dooman87/kolibri) for healthcheck and testing
* [glogi](https://github.com/dooman87/glogi) for logging interface
* Installed [imagemagick](http://imagemagick.org)
* Go with [modules support](https://golang.org/ref/mod)
* Installed [imagemagick v7.0.25+](http://imagemagick.org) with AVIF support. Run script assumes that binaries are in `/usr/local/bin`

```
$ go get github.com/golang/dep/cmd/dep
$ go get github.com/Pixboost/transformimgs
$ cd $GOPATH/src/github.com/Pixboost/transformimgs
$ git clone [email protected]:Pixboost/transformimgs.git
$ cd transformimgs
$ ./run.sh
```

Go modules have been introduced in v6.

### Performance tests

There is a [JMeter](https://jmeter.apache.org) performance test you can run against a service. To run tests:
Expand Down
6 changes: 3 additions & 3 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ package main

import (
"flag"
"github.com/Pixboost/transformimgs/img"
"github.com/Pixboost/transformimgs/img/loader"
"github.com/Pixboost/transformimgs/img/processor"
"github.com/Pixboost/transformimgs/v6/img"
"github.com/Pixboost/transformimgs/v6/img/loader"
"github.com/Pixboost/transformimgs/v6/img/processor"
"github.com/dooman87/kolibri/health"
"net/http"
"os"
Expand Down
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/Pixboost/transformimgs/v6

go 1.15

require (
github.com/dooman87/glogi v0.0.0-20171229170332-1a9ee96f1380
github.com/dooman87/kolibri v0.0.0-20170117194222-c194ff118b67
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f // indirect
github.com/gorilla/mux v1.3.0
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
github.com/dooman87/glogi v0.0.0-20171229170332-1a9ee96f1380 h1:OC9HJVXdYvuq5z4t2lS7650zQKnXvUtFXellMX2zu7E=
github.com/dooman87/glogi v0.0.0-20171229170332-1a9ee96f1380/go.mod h1:uWlPVNZ0PJcbKCdXMJL/MGta7m/H+wg0nzy6ZKYvEGw=
github.com/dooman87/kolibri v0.0.0-20170117194222-c194ff118b67 h1:5zx4LUSP0iPn0KL6ciINexzNAw4imx4Db7B+LHCIP3s=
github.com/dooman87/kolibri v0.0.0-20170117194222-c194ff118b67/go.mod h1:IGXOwI2+tWhVzcLeKONI0eXxxFVC4+A5ZFCup6fuQqE=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f h1:9oNbS1z4rVpbnkHBdPZU4jo9bSmrLpII768arSyMFgk=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.3.0 h1:HwSEKGN6U5T2aAQTfu5pW8fiwjSp3IgwdRbkICydk/c=
github.com/gorilla/mux v1.3.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
2 changes: 1 addition & 1 deletion img/loader/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package loader
import (
"context"
"fmt"
"github.com/Pixboost/transformimgs/img"
"github.com/Pixboost/transformimgs/v6/img"
"io/ioutil"
"net/http"
)
Expand Down
2 changes: 1 addition & 1 deletion img/loader/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package loader_test

import (
"context"
"github.com/Pixboost/transformimgs/img/loader"
"github.com/Pixboost/transformimgs/v6/img/loader"
"github.com/dooman87/kolibri/test"
"net/http"
"net/http/httptest"
Expand Down
18 changes: 0 additions & 18 deletions img/processor/dummy.go

This file was deleted.

89 changes: 51 additions & 38 deletions img/processor/imagemagick.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package processor
import (
"bytes"
"fmt"
"github.com/Pixboost/transformimgs/img"
"github.com/Pixboost/transformimgs/v6/img"
"github.com/Pixboost/transformimgs/v6/img/processor/internal"
"os/exec"
)

Expand All @@ -16,15 +17,7 @@ type ImageMagick struct {
// GetAdditionalArgs could return additional argument to ImageMagick "convert" command.
// "op" is the name of the operation: "optimise", "resize" or "fit".
// Argument name and value should be in separate array elements.
GetAdditionalArgs func(op string, image []byte, imageInfo *ImageInfo) []string
}

type ImageInfo struct {
format string
quality int
opaque bool
width int
height int
GetAdditionalArgs func(op string, image []byte, imageInfo *img.Info) []string
}

var convertOpts = []string{
Expand Down Expand Up @@ -90,23 +83,30 @@ func NewImageMagick(im string, idi string) (*ImageMagick, error) {
//
// Format of the size argument is WIDTHxHEIGHT with any of the dimension could be dropped, e.g. 300, x200, 300x200.
func (p *ImageMagick) Resize(data []byte, size string, imgId string, supportedFormats []string) (*img.Image, error) {
imgInfo, err := p.loadImageInfo(bytes.NewReader(data), imgId)
source, err := p.loadImageInfo(bytes.NewReader(data), imgId)
if err != nil {
return nil, err
}

outputFormatArg, mimeType := getOutputFormat(imgInfo, supportedFormats)
target := &img.Info{
Opaque: source.Opaque,
}
err = internal.CalculateTargetSizeForResize(source, target, size)
if err != nil {
img.Log.Errorf("could not calculate target size for [%s], size: [%s]\n", imgId, size)
}
outputFormatArg, mimeType := getOutputFormat(source, target, supportedFormats)

args := make([]string, 0)
args = append(args, "-") //Input
args = append(args, "-resize", size)
args = append(args, getQualityOptions(imgInfo, mimeType)...)
args = append(args, getQualityOptions(source, mimeType)...)
args = append(args, p.AdditionalArgs...)
if p.GetAdditionalArgs != nil {
args = append(args, p.GetAdditionalArgs("resize", data, imgInfo)...)
args = append(args, p.GetAdditionalArgs("resize", data, source)...)
}
args = append(args, convertOpts...)
args = append(args, getConvertFormatOptions(imgInfo)...)
args = append(args, getConvertFormatOptions(source)...)
args = append(args, outputFormatArg) //Output

outputImageData, err := p.execImagemagick(bytes.NewReader(data), args, imgId)
Expand All @@ -125,26 +125,33 @@ func (p *ImageMagick) Resize(data []byte, size string, imgId string, supportedFo
//
// Format of the size argument is WIDTHxHEIGHT, e.g. 300x200. Both dimensions must be included.
func (p *ImageMagick) FitToSize(data []byte, size string, imgId string, supportedFormats []string) (*img.Image, error) {
imgInfo, err := p.loadImageInfo(bytes.NewReader(data), imgId)
source, err := p.loadImageInfo(bytes.NewReader(data), imgId)
if err != nil {
return nil, err
}

outputFormatArg, mimeType := getOutputFormat(imgInfo, supportedFormats)
target := &img.Info{
Opaque: source.Opaque,
}
err = internal.CalculateTargetSizeForFit(target, size)
if err != nil {
img.Log.Errorf("could not calculate target size for [%s], size: [%s]\n", imgId, size)
}
outputFormatArg, mimeType := getOutputFormat(source, target, supportedFormats)

args := make([]string, 0)
args = append(args, "-") //Input
args = append(args, "-resize", size+"^")

args = append(args, getQualityOptions(imgInfo, mimeType)...)
args = append(args, getQualityOptions(source, mimeType)...)
args = append(args, p.AdditionalArgs...)
if p.GetAdditionalArgs != nil {
args = append(args, p.GetAdditionalArgs("fit", data, imgInfo)...)
args = append(args, p.GetAdditionalArgs("fit", data, source)...)
}
args = append(args, convertOpts...)
args = append(args, cutToFitOpts...)
args = append(args, "-extent", size)
args = append(args, getConvertFormatOptions(imgInfo)...)
args = append(args, getConvertFormatOptions(source)...)
args = append(args, outputFormatArg) //Output

outputImageData, err := p.execImagemagick(bytes.NewReader(data), args, imgId)
Expand All @@ -159,23 +166,28 @@ func (p *ImageMagick) FitToSize(data []byte, size string, imgId string, supporte
}

func (p *ImageMagick) Optimise(data []byte, imgId string, supportedFormats []string) (*img.Image, error) {
imgInfo, err := p.loadImageInfo(bytes.NewReader(data), imgId)
source, err := p.loadImageInfo(bytes.NewReader(data), imgId)
if err != nil {
return nil, err
}

outputFormatArg, mimeType := getOutputFormat(imgInfo, supportedFormats)
target := &img.Info{
Opaque: source.Opaque,
Width: source.Width,
Height: source.Height,
}
outputFormatArg, mimeType := getOutputFormat(source, target, supportedFormats)

args := make([]string, 0)
args = append(args, "-") //Input

args = append(args, getQualityOptions(imgInfo, mimeType)...)
args = append(args, getQualityOptions(source, mimeType)...)
args = append(args, p.AdditionalArgs...)
if p.GetAdditionalArgs != nil {
args = append(args, p.GetAdditionalArgs("optimise", data, imgInfo)...)
args = append(args, p.GetAdditionalArgs("optimise", data, source)...)
}
args = append(args, convertOpts...)
args = append(args, getConvertFormatOptions(imgInfo)...)
args = append(args, getConvertFormatOptions(source)...)
args = append(args, outputFormatArg) //Output

result, err := p.execImagemagick(bytes.NewReader(data), args, imgId)
Expand Down Expand Up @@ -217,7 +229,7 @@ func (p *ImageMagick) execImagemagick(in *bytes.Reader, args []string, imgId str
return out.Bytes(), nil
}

func (p *ImageMagick) loadImageInfo(in *bytes.Reader, imgId string) (*ImageInfo, error) {
func (p *ImageMagick) loadImageInfo(in *bytes.Reader, imgId string) (*img.Info, error) {
var out, cmderr bytes.Buffer
cmd := exec.Command(p.identifyCmd)
cmd.Args = append(cmd.Args, "-format", "%m %Q %[opaque] %w %h", "-")
Expand All @@ -236,25 +248,26 @@ func (p *ImageMagick) loadImageInfo(in *bytes.Reader, imgId string) (*ImageInfo,
return nil, err
}

imageInfo := &ImageInfo{}
_, err = fmt.Sscanf(out.String(), "%s %d %t %d %d", &imageInfo.format, &imageInfo.quality, &imageInfo.opaque, &imageInfo.width, &imageInfo.height)
imageInfo := &img.Info{}
_, err = fmt.Sscanf(out.String(), "%s %d %t %d %d", &imageInfo.Format, &imageInfo.Quality, &imageInfo.Opaque, &imageInfo.Width, &imageInfo.Height)
if err != nil {
return nil, err
}

return imageInfo, nil
}

func getOutputFormat(inf *ImageInfo, supportedFormats []string) (string, string) {
func getOutputFormat(src *img.Info, target *img.Info, supportedFormats []string) (string, string) {
webP := false
avif := false
for _, f := range supportedFormats {
if f == "image/webp" && inf.height < MaxWebpHeight && inf.width < MaxWebpWidth {
if f == "image/webp" && src.Height < MaxWebpHeight && src.Width < MaxWebpWidth {
webP = true
}
// ImageMagick doesn't support encoding of alpha channel for AVIF.
// Converting 1000x1000 image into AVIF will consume about 500Mb of RAM.
if f == "image/avif" && inf.opaque && (inf.width*inf.height) < (1000*1000) {
targetSize := target.Width * target.Height
if f == "image/avif" && src.Opaque && targetSize < (1000*1000) && targetSize != 0 {
avif = true
}
}
Expand All @@ -269,12 +282,12 @@ func getOutputFormat(inf *ImageInfo, supportedFormats []string) (string, string)
return "-", ""
}

func getConvertFormatOptions(inf *ImageInfo) []string {
if inf.format == "PNG" {
func getConvertFormatOptions(inf *img.Info) []string {
if inf.Format == "PNG" {
opts := []string{
"-define", "webp:lossless=true",
}
if inf.opaque {
if inf.Opaque {
opts = append(opts, "-colors", "256")
}
return opts
Expand All @@ -283,15 +296,15 @@ func getConvertFormatOptions(inf *ImageInfo) []string {
return []string{}
}

func getQualityOptions(inf *ImageInfo, outputMimeType string) []string {
if inf.quality == 100 {
func getQualityOptions(inf *img.Info, outputMimeType string) []string {
if inf.Quality == 100 {
return []string{"-quality", "82"}
}

if outputMimeType == "image/avif" {
if inf.quality > 85 {
if inf.Quality > 85 {
return []string{"-quality", "70"}
} else if inf.quality > 75 {
} else if inf.Quality > 75 {
return []string{"-quality", "60"}
} else {
return []string{"-quality", "50"}
Expand Down
Loading

0 comments on commit 78a4c96

Please sign in to comment.