New release, minor features, bimg upgrade and several fixes (#311)

* feat: add fly.io

* feat(readme): fly image width

* feat(readme): add html image + badge

* fix(docs): try space non-URL encoding

* Update README.md

* Update README.md

* Update README.md

* feat(docs): add fly.io info and links

* feat(#305, #309, #308, #305. #284)

* fix(ci): golinter

* fix(Dockerfile): use root folder for go test / ci linter

* fix(ci): allow to fail  go 1.14 build, still not sure why

* fix(ci): bad yaml format

* fix(ci): use travis-compatible regex

* fix(ci): lets skip go1.14 for now

* fix(mod): update dependencies
This commit is contained in:
Tom 2020-06-07 18:38:14 +02:00 committed by GitHub
parent b2a142adb8
commit 384bb00b15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 108 additions and 123 deletions

View File

@ -3,21 +3,20 @@ language: go
services: services:
- docker - docker
dist: trusty dist: focal
go: go:
- 1.12.x - "1.12"
- 1.11.x - "1.13"
# - "1.14"
env: env:
global: global:
- GOLANG_VERSION="${TRAVIS_GO_VERSION}" - GOLANG_VERSION="${TRAVIS_GO_VERSION}"
- IMAGINARY_VERSION="${TRAVIS_TAG:-dev}" - IMAGINARY_VERSION="${TRAVIS_TAG:-dev}"
matrix: matrix:
- LIBVIPS=8.7.3 - LIBVIPS=8.8.4
- LIBVIPS=8.7.4 - LIBVIPS=8.9.2
- LIBVIPS=8.8.0
- LIBVIPS=8.8.1
before_install: before_install:
- docker pull h2non/imaginary:latest || true - docker pull h2non/imaginary:latest || true
@ -28,14 +27,14 @@ install:
script: script:
- docker build --pull --cache-from h2non/imaginary:latest --build-arg GOLANG_VERSION="${GOLANG_VERSION%.x}" --build-arg LIBVIPS_VERSION="${LIBVIPS}" --build-arg IMAGINARY_VERSION="${IMAGINARY_VERSION#v}" --tag h2non/imaginary:${IMAGINARY_VERSION} . - docker build --pull --cache-from h2non/imaginary:latest --build-arg GOLANG_VERSION="${GOLANG_VERSION%.x}" --build-arg LIBVIPS_VERSION="${LIBVIPS}" --build-arg IMAGINARY_VERSION="${IMAGINARY_VERSION#v}" --tag h2non/imaginary:${IMAGINARY_VERSION} .
# before_deploy: before_deploy:
# - docker login -u "$REGISTRY_USER" -p "$REGISTRY_PASS" - docker login -u "$DOCKER_LOGIN" -p "$DOCKER_PWD"
# deploy: deploy:
# provider: script provider: script
# script: script:
# - docker tag h2non/imaginary:${IMAGINARY_VERSION} latest - docker tag h2non/imaginary:${IMAGINARY_VERSION} h2non/imaginary:latest
# - docker push h2non/imaginary:${IMAGINARY_VERSION} - docker push h2non/imaginary:${IMAGINARY_VERSION}
# - docker push h2non/imaginary:latest - docker push h2non/imaginary:latest
# on: on:
# condition: "${TRAVIS_TAG} =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$" condition: "${TRAVIS_TAG} =~ ^v([0-9]+).([0-9]+).([0-9]+)$ AND env(GOLANG_VERSION) = 1.13"

View File

@ -1,8 +1,8 @@
ARG GOLANG_VERSION=1.13.7 ARG GOLANG_VERSION=1.14
FROM golang:${GOLANG_VERSION} as builder FROM golang:${GOLANG_VERSION} as builder
ARG IMAGINARY_VERSION=dev ARG IMAGINARY_VERSION=dev
ARG LIBVIPS_VERSION=8.9.1 ARG LIBVIPS_VERSION=8.9.2
ARG GOLANGCILINT_VERSION=1.23.3 ARG GOLANGCILINT_VERSION=1.23.3
# Installs libvips + required libraries # Installs libvips + required libraries
@ -50,8 +50,8 @@ RUN go mod download
COPY . . COPY . .
# Run quality control # Run quality control
RUN go test -test.v -test.race -test.covermode=atomic ./... RUN go test -test.v -test.race -test.covermode=atomic .
RUN golangci-lint run ./... RUN golangci-lint run .
# Compile imaginary # Compile imaginary
RUN go build -a \ RUN go build -a \

View File

@ -1,4 +1,4 @@
# imaginary [![Build Status](https://travis-ci.org/h2non/imaginary.png)](https://travis-ci.org/h2non/imaginary) [![Docker](https://img.shields.io/badge/docker-h2non/imaginary-blue.svg)](https://hub.docker.com/r/h2non/imaginary/) [![Docker Registry](https://img.shields.io/docker/pulls/h2non/imaginary.svg)](https://hub.docker.com/r/h2non/imaginary/) [![Go Report Card](http://goreportcard.com/badge/h2non/imaginary)](https://goreportcard.com/report/h2non/imaginary) [![Fly.io](https://img.shields.io/badge/deploy-fly.io-blue.svg)](https://fly.io/launch/github/h2non/imaginary) # imaginary [![Build Status](https://travis-ci.org/h2non/imaginary.svg)](https://travis-ci.org/h2non/imaginary) [![Docker](https://img.shields.io/badge/docker-h2non/imaginary-blue.svg)](https://hub.docker.com/r/h2non/imaginary/) [![Docker Registry](https://img.shields.io/docker/pulls/h2non/imaginary.svg)](https://hub.docker.com/r/h2non/imaginary/) [![Go Report Card](http://goreportcard.com/badge/h2non/imaginary)](https://goreportcard.com/report/h2non/imaginary) [![Fly.io](https://img.shields.io/badge/deploy-fly.io-blue.svg)](https://fly.io/launch/github/h2non/imaginary)
**[Fast](#benchmarks) HTTP [microservice](http://microservices.io/patterns/microservices.html)** written in Go **for high-level image processing** backed by [bimg](https://github.com/h2non/bimg) and [libvips](https://github.com/jcupitt/libvips). `imaginary` can be used as private or public HTTP service for massive image processing with first-class support for [Docker](#docker) & [Fly.io](#flyio). **[Fast](#benchmarks) HTTP [microservice](http://microservices.io/patterns/microservices.html)** written in Go **for high-level image processing** backed by [bimg](https://github.com/h2non/bimg) and [libvips](https://github.com/jcupitt/libvips). `imaginary` can be used as private or public HTTP service for massive image processing with first-class support for [Docker](#docker) & [Fly.io](#flyio).
It's almost dependency-free and only uses [`net/http`](http://golang.org/pkg/net/http/) native package without additional abstractions for better [performance](#performance). It's almost dependency-free and only uses [`net/http`](http://golang.org/pkg/net/http/) native package without additional abstractions for better [performance](#performance).
@ -83,7 +83,7 @@ go get -u github.com/h2non/imaginary
Also, be sure you have the latest version of `bimg`: Also, be sure you have the latest version of `bimg`:
```bash ```bash
go get -u gopkg.in/h2non/bimg.v1 go get -u github.com/h2non/bimg
``` ```
### libvips ### libvips
@ -553,7 +553,7 @@ Image measures are always in pixels, unless otherwise indicated.
- **url** `string` - Fetch the image from a remote HTTP server. In order to use this you must pass the `-enable-url-source` flag. - **url** `string` - Fetch the image from a remote HTTP server. In order to use this you must pass the `-enable-url-source` flag.
- **colorspace** `string` - Use a custom color space for the output image. Allowed values are: `srgb` or `bw` (black&white) - **colorspace** `string` - Use a custom color space for the output image. Allowed values are: `srgb` or `bw` (black&white)
- **field** `string` - Custom image form field name if using `multipart/form`. Defaults to: `file` - **field** `string` - Custom image form field name if using `multipart/form`. Defaults to: `file`
- **extend** `string` - Extend represents the image extend mode used when the edges of an image are extended. Allowed values are: `black`, `copy`, `mirror`, `white` and `background`. If `background` value is specified, you can define the desired extend RGB color via `background` param, such as `?extend=background&background=250,20,10`. For more info, see [libvips docs](http://www.vips.ecs.soton.ac.uk/supported/8.4/doc/html/libvips/libvips-conversion.html#VIPS-EXTEND-BACKGROUND:CAPS). - **extend** `string` - Extend represents the image extend mode used when the edges of an image are extended. Defaults to `copy`. Allowed values are: `black`, `copy`, `mirror`, `white`, `lastpixel` and `background`. If `background` value is specified, you can define the desired extend RGB color via `background` param, such as `?extend=background&background=250,20,10`. For more info, see [libvips docs](http://www.vips.ecs.soton.ac.uk/supported/8.4/doc/html/libvips/libvips-conversion.html#VIPS-EXTEND-BACKGROUND:CAPS).
- **background** `string` - Background RGB decimal base color to use when flattening transparent PNGs. Example: `255,200,150` - **background** `string` - Background RGB decimal base color to use when flattening transparent PNGs. Example: `255,200,150`
- **sigma** `float` - Size of the gaussian mask to use when blurring an image. Example: `15.0` - **sigma** `float` - Size of the gaussian mask to use when blurring an image. Example: `15.0`
- **minampl** `float` - Minimum amplitude of the gaussian filter to use when blurring an image. Default: Example: `0.5` - **minampl** `float` - Minimum amplitude of the gaussian filter to use when blurring an image. Default: Example: `0.5`

View File

@ -44,7 +44,11 @@ func imageController(o ServerOptions, operation Operation) func(http.ResponseWri
buf, err := imageSource.GetImage(req) buf, err := imageSource.GetImage(req)
if err != nil { if err != nil {
ErrorReply(req, w, NewError(err.Error(), BadRequest), o) if xerr, ok := err.(Error); ok {
ErrorReply(req, w, xerr, o)
} else {
ErrorReply(req, w, NewError(err.Error(), http.StatusBadRequest), o)
}
return return
} }
@ -100,7 +104,7 @@ func imageHandler(w http.ResponseWriter, r *http.Request, buf []byte, operation
opts, err := buildParamsFromQuery(r.URL.Query()) opts, err := buildParamsFromQuery(r.URL.Query())
if err != nil { if err != nil {
ErrorReply(r, w, NewError("Error while processing parameters, "+err.Error(), BadRequest), o) ErrorReply(r, w, NewError("Error while processing parameters, "+err.Error(), http.StatusBadRequest), o)
return return
} }
@ -115,7 +119,7 @@ func imageHandler(w http.ResponseWriter, r *http.Request, buf []byte, operation
image, err := operation.Run(buf, opts) image, err := operation.Run(buf, opts)
if err != nil { if err != nil {
ErrorReply(r, w, NewError("Error while processing the image: "+err.Error(), BadRequest), o) ErrorReply(r, w, NewError("Error while processing the image: "+err.Error(), http.StatusBadRequest), o)
return return
} }

View File

@ -9,38 +9,26 @@ import (
"github.com/h2non/bimg" "github.com/h2non/bimg"
) )
const (
_ uint8 = iota
BadRequest
NotAllowed
Unsupported
Unauthorized
InternalError
NotFound
NotImplemented
Forbidden
NotAcceptable
)
var ( var (
ErrNotFound = NewError("not found", NotFound) ErrNotFound = NewError("Not found", http.StatusNotFound)
ErrInvalidAPIKey = NewError("invalid or missing API key", Unauthorized) ErrInvalidAPIKey = NewError("Invalid or missing API key", http.StatusUnauthorized)
ErrMethodNotAllowed = NewError("method not allowed", NotAllowed) ErrMethodNotAllowed = NewError("HTTP method not allowed. Try with a POST or GET method (-enable-url-source flag must be defined)", http.StatusMethodNotAllowed)
ErrUnsupportedMedia = NewError("unsupported media type", Unsupported) ErrGetMethodNotAllowed = NewError("GET method not allowed. Make sure remote URL source is enabled by using the flag: -enable-url-source", http.StatusMethodNotAllowed)
ErrOutputFormat = NewError("unsupported output image format", BadRequest) ErrUnsupportedMedia = NewError("Unsupported media type", http.StatusNotAcceptable)
ErrEmptyBody = NewError("empty image", BadRequest) ErrOutputFormat = NewError("Unsupported output image format", http.StatusBadRequest)
ErrMissingParamFile = NewError("missing required param: file", BadRequest) ErrEmptyBody = NewError("Empty or unreadable image", http.StatusBadRequest)
ErrInvalidFilePath = NewError("invalid file path", BadRequest) ErrMissingParamFile = NewError("Missing required param: file", http.StatusBadRequest)
ErrInvalidImageURL = NewError("invalid image URL", BadRequest) ErrInvalidFilePath = NewError("Invalid file path", http.StatusBadRequest)
ErrMissingImageSource = NewError("cannot process the image due to missing or invalid params", BadRequest) ErrInvalidImageURL = NewError("Unvalid image URL", http.StatusBadRequest)
ErrNotImplemented = NewError("not implemented endpoint", NotImplemented) ErrMissingImageSource = NewError("Cannot process the image due to missing or invalid params", http.StatusBadRequest)
ErrInvalidURLSignature = NewError("invalid URL signature", BadRequest) ErrNotImplemented = NewError("Not implemented endpoint", http.StatusNotImplemented)
ErrURLSignatureMismatch = NewError("URL signature mismatch", Forbidden) ErrInvalidURLSignature = NewError("Invalid URL signature", http.StatusBadRequest)
ErrURLSignatureMismatch = NewError("URL signature mismatch", http.StatusForbidden)
) )
type Error struct { type Error struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Code uint8 `json:"code"` Code int `json:"status"`
} }
func (e Error) JSON() []byte { func (e Error) JSON() []byte {
@ -53,34 +41,21 @@ func (e Error) Error() string {
} }
func (e Error) HTTPCode() int { func (e Error) HTTPCode() int {
var codes = map[uint8]int{ if e.Code >= 400 && e.Code <= 511 {
BadRequest: http.StatusBadRequest, return e.Code
NotAllowed: http.StatusMethodNotAllowed,
Unsupported: http.StatusUnsupportedMediaType,
InternalError: http.StatusInternalServerError,
Unauthorized: http.StatusUnauthorized,
NotFound: http.StatusNotFound,
NotImplemented: http.StatusNotImplemented,
Forbidden: http.StatusForbidden,
NotAcceptable: http.StatusNotAcceptable,
} }
if v, ok := codes[e.Code]; ok {
return v
}
return http.StatusServiceUnavailable return http.StatusServiceUnavailable
} }
func NewError(err string, code uint8) Error { func NewError(err string, code int) Error {
err = strings.Replace(err, "\n", "", -1) err = strings.Replace(err, "\n", "", -1)
return Error{err, code} return Error{Message: err, Code: code}
} }
func sendErrorResponse(w http.ResponseWriter, httpStatusCode int, imaginaryErrorCode uint8, err error) { func sendErrorResponse(w http.ResponseWriter, httpStatusCode int, err error) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(httpStatusCode) w.WriteHeader(httpStatusCode)
_, _ = w.Write([]byte(fmt.Sprintf("{\"error\":\"%s\", \"code\": %d}", err.Error(), imaginaryErrorCode))) _, _ = w.Write([]byte(fmt.Sprintf("{\"error\":\"%s\", \"status\": %d}", err.Error(), httpStatusCode)))
} }
func replyWithPlaceholder(req *http.Request, w http.ResponseWriter, errCaller Error, o ServerOptions) error { func replyWithPlaceholder(req *http.Request, w http.ResponseWriter, errCaller Error, o ServerOptions) error {
@ -94,20 +69,20 @@ func replyWithPlaceholder(req *http.Request, w http.ResponseWriter, errCaller Er
bimgOptions.Width, err = parseInt(req.URL.Query().Get("width")) bimgOptions.Width, err = parseInt(req.URL.Query().Get("width"))
if err != nil { if err != nil {
sendErrorResponse(w, http.StatusBadRequest, BadRequest, err) sendErrorResponse(w, http.StatusBadRequest, err)
return err return err
} }
bimgOptions.Height, err = parseInt(req.URL.Query().Get("height")) bimgOptions.Height, err = parseInt(req.URL.Query().Get("height"))
if err != nil { if err != nil {
sendErrorResponse(w, http.StatusBadRequest, BadRequest, err) sendErrorResponse(w, http.StatusBadRequest, err)
return err return err
} }
// Resize placeholder to expected output // Resize placeholder to expected output
buf, err := bimg.Resize(o.PlaceholderImage, bimgOptions) buf, err := bimg.Resize(o.PlaceholderImage, bimgOptions)
if err != nil { if err != nil {
sendErrorResponse(w, http.StatusBadRequest, BadRequest, err) sendErrorResponse(w, http.StatusBadRequest, err)
return err return err
} }

View File

@ -2,23 +2,23 @@ package main
import "testing" import "testing"
func TestError(t *testing.T) { func TestDefaultError(t *testing.T) {
err := NewError("oops!\n\n", 1) err := NewError("oops!\n\n", 503)
if err.Error() != "oops!" { if err.Error() != "oops!" {
t.Fatal("Invalid error message") t.Fatal("Invalid error message")
} }
if err.Code != 1 { if err.Code != 503 {
t.Fatal("Invalid error code") t.Fatal("Invalid error code")
} }
code := err.HTTPCode() code := err.HTTPCode()
if code != 400 { if code != 503 {
t.Fatalf("Invalid HTTP error status: %d", code) t.Fatalf("Invalid HTTP error status: %d", code)
} }
json := string(err.JSON()) json := string(err.JSON())
if json != "{\"message\":\"oops!\",\"code\":1}" { if json != "{\"message\":\"oops!\",\"status\":503}" {
t.Fatalf("Invalid JSON output: %s", json) t.Fatalf("Invalid JSON output: %s", json)
} }
} }

4
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/garyburd/redigo v1.6.0 // indirect github.com/garyburd/redigo v1.6.0 // indirect
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad // indirect github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad // indirect
github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3 github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3
github.com/h2non/bimg v1.0.20-0.20200405220655-daafbf6d972d github.com/h2non/bimg v1.1.0
github.com/h2non/filetype v1.0.12 github.com/h2non/filetype v1.1.0
gopkg.in/throttled/throttled.v2 v2.0.3 gopkg.in/throttled/throttled.v2 v2.0.3
) )

8
go.sum
View File

@ -1,12 +1,10 @@
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/h2non/bimg v1.1.0/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8=
github.com/h2non/filetype v1.1.0 h1:Or/gjocJrJRNK/Cri/TDEKFjAR+cfG6eK65NGYB6gBA=
github.com/h2non/filetype v1.1.0/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad h1:eMxs9EL0PvIGS9TTtxg4R+JxuPGav82J8rA+GFnY7po= github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad h1:eMxs9EL0PvIGS9TTtxg4R+JxuPGav82J8rA+GFnY7po=
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3 h1:86ukAHRTa2CXdBnWJHcjjPPGTyLGEF488OFRsbBAuFs= github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3 h1:86ukAHRTa2CXdBnWJHcjjPPGTyLGEF488OFRsbBAuFs=
github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/h2non/bimg v1.0.20-0.20200405220655-daafbf6d972d h1:AhGp7Xiew8DAYGJ1MojrE11chxHVOv4NvE/y5i2IvBI=
github.com/h2non/bimg v1.0.20-0.20200405220655-daafbf6d972d/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8=
github.com/h2non/filetype v1.0.12 h1:yHCsIe0y2cvbDARtJhGBTD2ecvqMSTvlIcph9En/Zao=
github.com/h2non/filetype v1.0.12/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
gopkg.in/throttled/throttled.v2 v2.0.3 h1:PGm7nfjjexecEyI2knw1akeLcrjzqxuYSU9a04R8rfU= gopkg.in/throttled/throttled.v2 v2.0.3 h1:PGm7nfjjexecEyI2knw1akeLcrjzqxuYSU9a04R8rfU=
gopkg.in/throttled/throttled.v2 v2.0.3/go.mod h1:L4cTNZO77XKEXtn8HNFRCMNGZPtRRKAhyuJBSvK/T90= gopkg.in/throttled/throttled.v2 v2.0.3/go.mod h1:L4cTNZO77XKEXtn8HNFRCMNGZPtRRKAhyuJBSvK/T90=

View File

@ -65,7 +65,7 @@ func Info(buf []byte, o ImageOptions) (Image, error) {
meta, err := bimg.Metadata(buf) meta, err := bimg.Metadata(buf)
if err != nil { if err != nil {
return image, NewError("Cannot retrieve image metadata: %s"+err.Error(), BadRequest) return image, NewError("Cannot retrieve image metadata: %s"+err.Error(), http.StatusBadRequest)
} }
info := ImageInfo{ info := ImageInfo{
@ -87,7 +87,7 @@ func Info(buf []byte, o ImageOptions) (Image, error) {
func Resize(buf []byte, o ImageOptions) (Image, error) { func Resize(buf []byte, o ImageOptions) (Image, error) {
if o.Width == 0 && o.Height == 0 { if o.Width == 0 && o.Height == 0 {
return Image{}, NewError("Missing required param: height or width", BadRequest) return Image{}, NewError("Missing required param: height or width", http.StatusBadRequest)
} }
opts := BimgOptions(o) opts := BimgOptions(o)
@ -102,7 +102,7 @@ func Resize(buf []byte, o ImageOptions) (Image, error) {
func Fit(buf []byte, o ImageOptions) (Image, error) { func Fit(buf []byte, o ImageOptions) (Image, error) {
if o.Width == 0 || o.Height == 0 { if o.Width == 0 || o.Height == 0 {
return Image{}, NewError("Missing required params: height, width", BadRequest) return Image{}, NewError("Missing required params: height, width", http.StatusBadRequest)
} }
metadata, err := bimg.Metadata(buf) metadata, err := bimg.Metadata(buf)
@ -113,7 +113,7 @@ func Fit(buf []byte, o ImageOptions) (Image, error) {
dims := metadata.Size dims := metadata.Size
if dims.Width == 0 || dims.Height == 0 { if dims.Width == 0 || dims.Height == 0 {
return Image{}, NewError("Width or height of requested image is zero", NotAcceptable) return Image{}, NewError("Width or height of requested image is zero", http.StatusNotAcceptable)
} }
// metadata.Orientation // metadata.Orientation
@ -165,7 +165,7 @@ func calculateDestinationFitDimension(imageWidth, imageHeight, fitWidth, fitHeig
func Enlarge(buf []byte, o ImageOptions) (Image, error) { func Enlarge(buf []byte, o ImageOptions) (Image, error) {
if o.Width == 0 || o.Height == 0 { if o.Width == 0 || o.Height == 0 {
return Image{}, NewError("Missing required params: height, width", BadRequest) return Image{}, NewError("Missing required params: height, width", http.StatusBadRequest)
} }
opts := BimgOptions(o) opts := BimgOptions(o)
@ -179,7 +179,7 @@ func Enlarge(buf []byte, o ImageOptions) (Image, error) {
func Extract(buf []byte, o ImageOptions) (Image, error) { func Extract(buf []byte, o ImageOptions) (Image, error) {
if o.AreaWidth == 0 || o.AreaHeight == 0 { if o.AreaWidth == 0 || o.AreaHeight == 0 {
return Image{}, NewError("Missing required params: areawidth or areaheight", BadRequest) return Image{}, NewError("Missing required params: areawidth or areaheight", http.StatusBadRequest)
} }
opts := BimgOptions(o) opts := BimgOptions(o)
@ -193,7 +193,7 @@ func Extract(buf []byte, o ImageOptions) (Image, error) {
func Crop(buf []byte, o ImageOptions) (Image, error) { func Crop(buf []byte, o ImageOptions) (Image, error) {
if o.Width == 0 && o.Height == 0 { if o.Width == 0 && o.Height == 0 {
return Image{}, NewError("Missing required param: height or width", BadRequest) return Image{}, NewError("Missing required param: height or width", http.StatusBadRequest)
} }
opts := BimgOptions(o) opts := BimgOptions(o)
@ -203,7 +203,7 @@ func Crop(buf []byte, o ImageOptions) (Image, error) {
func SmartCrop(buf []byte, o ImageOptions) (Image, error) { func SmartCrop(buf []byte, o ImageOptions) (Image, error) {
if o.Width == 0 && o.Height == 0 { if o.Width == 0 && o.Height == 0 {
return Image{}, NewError("Missing required param: height or width", BadRequest) return Image{}, NewError("Missing required param: height or width", http.StatusBadRequest)
} }
opts := BimgOptions(o) opts := BimgOptions(o)
@ -214,7 +214,7 @@ func SmartCrop(buf []byte, o ImageOptions) (Image, error) {
func Rotate(buf []byte, o ImageOptions) (Image, error) { func Rotate(buf []byte, o ImageOptions) (Image, error) {
if o.Rotate == 0 { if o.Rotate == 0 {
return Image{}, NewError("Missing required param: rotate", BadRequest) return Image{}, NewError("Missing required param: rotate", http.StatusBadRequest)
} }
opts := BimgOptions(o) opts := BimgOptions(o)
@ -235,7 +235,7 @@ func Flop(buf []byte, o ImageOptions) (Image, error) {
func Thumbnail(buf []byte, o ImageOptions) (Image, error) { func Thumbnail(buf []byte, o ImageOptions) (Image, error) {
if o.Width == 0 && o.Height == 0 { if o.Width == 0 && o.Height == 0 {
return Image{}, NewError("Missing required params: width or height", BadRequest) return Image{}, NewError("Missing required params: width or height", http.StatusBadRequest)
} }
return Process(buf, BimgOptions(o)) return Process(buf, BimgOptions(o))
@ -243,14 +243,14 @@ func Thumbnail(buf []byte, o ImageOptions) (Image, error) {
func Zoom(buf []byte, o ImageOptions) (Image, error) { func Zoom(buf []byte, o ImageOptions) (Image, error) {
if o.Factor == 0 { if o.Factor == 0 {
return Image{}, NewError("Missing required param: factor", BadRequest) return Image{}, NewError("Missing required param: factor", http.StatusBadRequest)
} }
opts := BimgOptions(o) opts := BimgOptions(o)
if o.Top > 0 || o.Left > 0 { if o.Top > 0 || o.Left > 0 {
if o.AreaWidth == 0 && o.AreaHeight == 0 { if o.AreaWidth == 0 && o.AreaHeight == 0 {
return Image{}, NewError("Missing required params: areawidth, areaheight", BadRequest) return Image{}, NewError("Missing required params: areawidth, areaheight", http.StatusBadRequest)
} }
opts.Top = o.Top opts.Top = o.Top
@ -269,10 +269,10 @@ func Zoom(buf []byte, o ImageOptions) (Image, error) {
func Convert(buf []byte, o ImageOptions) (Image, error) { func Convert(buf []byte, o ImageOptions) (Image, error) {
if o.Type == "" { if o.Type == "" {
return Image{}, NewError("Missing required param: type", BadRequest) return Image{}, NewError("Missing required param: type", http.StatusBadRequest)
} }
if ImageType(o.Type) == bimg.UNKNOWN { if ImageType(o.Type) == bimg.UNKNOWN {
return Image{}, NewError("Invalid image type: "+o.Type, BadRequest) return Image{}, NewError("Invalid image type: "+o.Type, http.StatusBadRequest)
} }
opts := BimgOptions(o) opts := BimgOptions(o)
@ -281,7 +281,7 @@ func Convert(buf []byte, o ImageOptions) (Image, error) {
func Watermark(buf []byte, o ImageOptions) (Image, error) { func Watermark(buf []byte, o ImageOptions) (Image, error) {
if o.Text == "" { if o.Text == "" {
return Image{}, NewError("Missing required param: text", BadRequest) return Image{}, NewError("Missing required param: text", http.StatusBadRequest)
} }
opts := BimgOptions(o) opts := BimgOptions(o)
@ -302,11 +302,11 @@ func Watermark(buf []byte, o ImageOptions) (Image, error) {
func WatermarkImage(buf []byte, o ImageOptions) (Image, error) { func WatermarkImage(buf []byte, o ImageOptions) (Image, error) {
if o.Image == "" { if o.Image == "" {
return Image{}, NewError("Missing required param: image", BadRequest) return Image{}, NewError("Missing required param: image", http.StatusBadRequest)
} }
response, err := http.Get(o.Image) response, err := http.Get(o.Image)
if err != nil { if err != nil {
return Image{}, NewError(fmt.Sprintf("Unable to retrieve watermark image. %s", o.Image), BadRequest) return Image{}, NewError(fmt.Sprintf("Unable to retrieve watermark image. %s", o.Image), http.StatusBadRequest)
} }
defer func() { defer func() {
_ = response.Body.Close() _ = response.Body.Close()
@ -322,7 +322,7 @@ func WatermarkImage(buf []byte, o ImageOptions) (Image, error) {
errMessage = fmt.Sprintf("%s. %s", errMessage, err.Error()) errMessage = fmt.Sprintf("%s. %s", errMessage, err.Error())
} }
return Image{}, NewError(errMessage, BadRequest) return Image{}, NewError(errMessage, http.StatusBadRequest)
} }
opts := BimgOptions(o) opts := BimgOptions(o)
@ -336,7 +336,7 @@ func WatermarkImage(buf []byte, o ImageOptions) (Image, error) {
func GaussianBlur(buf []byte, o ImageOptions) (Image, error) { func GaussianBlur(buf []byte, o ImageOptions) (Image, error) {
if o.Sigma == 0 && o.MinAmpl == 0 { if o.Sigma == 0 && o.MinAmpl == 0 {
return Image{}, NewError("Missing required param: sigma or minampl", BadRequest) return Image{}, NewError("Missing required param: sigma or minampl", http.StatusBadRequest)
} }
opts := BimgOptions(o) opts := BimgOptions(o)
return Process(buf, opts) return Process(buf, opts)
@ -344,10 +344,10 @@ func GaussianBlur(buf []byte, o ImageOptions) (Image, error) {
func Pipeline(buf []byte, o ImageOptions) (Image, error) { func Pipeline(buf []byte, o ImageOptions) (Image, error) {
if len(o.Operations) == 0 { if len(o.Operations) == 0 {
return Image{}, NewError("Missing or invalid pipeline operations JSON", BadRequest) return Image{}, NewError("Missing or invalid pipeline operations JSON", http.StatusBadRequest)
} }
if len(o.Operations) > 10 { if len(o.Operations) > 10 {
return Image{}, NewError("Maximum allowed pipeline operations exceeded", BadRequest) return Image{}, NewError("Maximum allowed pipeline operations exceeded", http.StatusBadRequest)
} }
// Validate and built operations // Validate and built operations
@ -355,7 +355,7 @@ func Pipeline(buf []byte, o ImageOptions) (Image, error) {
// Validate supported operation name // Validate supported operation name
var exists bool var exists bool
if operation.Operation, exists = OperationsMap[operation.Name]; !exists { if operation.Operation, exists = OperationsMap[operation.Name]; !exists {
return Image{}, NewError(fmt.Sprintf("Unsupported operation name: %s", operation.Name), BadRequest) return Image{}, NewError(fmt.Sprintf("Unsupported operation name: %s", operation.Name), http.StatusBadRequest)
} }
// Parse and construct operation options // Parse and construct operation options

View File

@ -114,7 +114,7 @@ type URLSignature struct {
func main() { func main() {
flag.Usage = func() { flag.Usage = func() {
_, _ = fmt.Fprint(os.Stderr, fmt.Sprintf(usage, Version, runtime.NumCPU())) _, _ = fmt.Fprint(os.Stderr, usage, Version, runtime.NumCPU())
} }
flag.Parse() flag.Parse()

View File

@ -9,8 +9,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/rs/cors"
"github.com/h2non/bimg" "github.com/h2non/bimg"
"github.com/rs/cors"
"gopkg.in/throttled/throttled.v2" "gopkg.in/throttled/throttled.v2"
"gopkg.in/throttled/throttled.v2/store/memstore" "gopkg.in/throttled/throttled.v2/store/memstore"
) )
@ -105,7 +105,7 @@ func validateImage(next http.Handler, o ServerOptions) http.Handler {
} }
if r.Method == http.MethodGet && o.Mount == "" && !o.EnableURLSource { if r.Method == http.MethodGet && o.Mount == "" && !o.EnableURLSource {
ErrorReply(r, w, ErrMethodNotAllowed, o) ErrorReply(r, w, ErrGetMethodNotAllowed, o)
return return
} }

View File

@ -344,9 +344,11 @@ func coerceInterlace(io *ImageOptions, param interface{}) (err error) {
} }
func buildParamsFromOperation(op PipelineOperation) (ImageOptions, error) { func buildParamsFromOperation(op PipelineOperation) (ImageOptions, error) {
var options ImageOptions var options ImageOptions
// Apply defaults
options.Extend = bimg.ExtendCopy
for key, value := range op.Params { for key, value := range op.Params {
fn, ok := paramTypeCoercions[key] fn, ok := paramTypeCoercions[key]
if !ok { if !ok {
@ -366,6 +368,9 @@ func buildParamsFromOperation(op PipelineOperation) (ImageOptions, error) {
func buildParamsFromQuery(query url.Values) (ImageOptions, error) { func buildParamsFromQuery(query url.Values) (ImageOptions, error) {
var options ImageOptions var options ImageOptions
// Apply defaults
options.Extend = bimg.ExtendCopy
// Extract only known parameters // Extract only known parameters
for key := range query { for key := range query {
fn, ok := paramTypeCoercions[key] fn, ok := paramTypeCoercions[key]
@ -448,8 +453,8 @@ func parseExtendMode(val string) bimg.Extend {
if val == "white" { if val == "white" {
return bimg.ExtendWhite return bimg.ExtendWhite
} }
if val == "copy" { if val == "black" {
return bimg.ExtendCopy return bimg.ExtendBlack
} }
if val == "mirror" { if val == "mirror" {
return bimg.ExtendMirror return bimg.ExtendMirror
@ -457,7 +462,10 @@ func parseExtendMode(val string) bimg.Extend {
if val == "background" { if val == "background" {
return bimg.ExtendBackground return bimg.ExtendBackground
} }
return bimg.ExtendBlack if val == "lastpixel" {
return bimg.ExtendLast
}
return bimg.ExtendCopy
} }
func parseGravity(val string) bimg.Gravity { func parseGravity(val string) bimg.Gravity {

View File

@ -141,10 +141,11 @@ func TestParseExtend(t *testing.T) {
{"black", bimg.ExtendBlack}, {"black", bimg.ExtendBlack},
{"copy", bimg.ExtendCopy}, {"copy", bimg.ExtendCopy},
{"mirror", bimg.ExtendMirror}, {"mirror", bimg.ExtendMirror},
{"lastpixel", bimg.ExtendLast},
{"background", bimg.ExtendBackground}, {"background", bimg.ExtendBackground},
{" BACKGROUND ", bimg.ExtendBackground}, {" BACKGROUND ", bimg.ExtendBackground},
{"invalid", bimg.ExtendBlack}, {"invalid", bimg.ExtendCopy},
{"", bimg.ExtendBlack}, {"", bimg.ExtendCopy},
} }
for _, extend := range cases { for _, extend := range cases {

View File

@ -41,11 +41,11 @@ func (s *HTTPImageSource) fetchImage(url *url.URL, ireq *http.Request) ([]byte,
req := newHTTPRequest(s, ireq, http.MethodHead, url) req := newHTTPRequest(s, ireq, http.MethodHead, url)
res, err := http.DefaultClient.Do(req) res, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("error fetching image http headers: %v", err) return nil, fmt.Errorf("error fetching remote http image headers: %v", err)
} }
_ = res.Body.Close() _ = res.Body.Close()
if res.StatusCode < 200 && res.StatusCode > 206 { if res.StatusCode < 200 && res.StatusCode > 206 {
return nil, fmt.Errorf("error fetching image http headers: (status=%d) (url=%s)", res.StatusCode, req.URL.String()) return nil, NewError(fmt.Sprintf("error fetching remote http image headers: (status=%d) (url=%s)", res.StatusCode, req.URL.String()), res.StatusCode)
} }
contentLength, _ := strconv.Atoi(res.Header.Get("Content-Length")) contentLength, _ := strconv.Atoi(res.Header.Get("Content-Length"))
@ -58,11 +58,11 @@ func (s *HTTPImageSource) fetchImage(url *url.URL, ireq *http.Request) ([]byte,
req := newHTTPRequest(s, ireq, http.MethodGet, url) req := newHTTPRequest(s, ireq, http.MethodGet, url)
res, err := http.DefaultClient.Do(req) res, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("error downloading image: %v", err) return nil, fmt.Errorf("error fetching remote http image: %v", err)
} }
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode != 200 { if res.StatusCode != 200 {
return nil, fmt.Errorf("error downloading image: (status=%d) (url=%s)", res.StatusCode, req.URL.String()) return nil, NewError(fmt.Sprintf("error fetching remote http image: (status=%d) (url=%s)", res.StatusCode, req.URL.String()), res.StatusCode)
} }
// Read the body // Read the body