collapse data and transform packages into proxy

This commit is contained in:
Will Norris 2013-12-26 12:33:22 -08:00
parent d8aa853b3d
commit fc8552e6b4
4 changed files with 19 additions and 26 deletions

112
proxy/data.go Normal file
View file

@ -0,0 +1,112 @@
// Copyright 2013 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package data provides common shared data structures for imageproxy.
package proxy
import (
"fmt"
"net/url"
"strconv"
"strings"
"time"
)
// Options specifies transformations that can be performed on a
// requested image.
type Options struct {
Width float64 // requested width, in pixels
Height float64 // requested height, in pixels
// If true, resize the image to fit in the specified dimensions. Image
// will not be cropped, and aspect ratio will be maintained.
Fit bool
// Rotate image the specified degrees counter-clockwise. Valid values are 90, 180, 270.
Rotate int
FlipVertical bool
FlipHorizontal bool
}
func (o Options) String() string {
return fmt.Sprintf("%vx%v", o.Width, o.Height)
}
func ParseOptions(str string) *Options {
o := new(Options)
var h, w string
parts := strings.Split(str, ",")
// parse size
size := strings.SplitN(parts[0], "x", 2)
w = size[0]
if len(size) > 1 {
h = size[1]
} else {
h = w
}
if w != "" {
o.Width, _ = strconv.ParseFloat(w, 64)
}
if h != "" {
o.Height, _ = strconv.ParseFloat(h, 64)
}
for _, part := range parts[1:] {
if part == "fit" {
o.Fit = true
continue
}
if part == "fv" {
o.FlipVertical = true
continue
}
if part == "fh" {
o.FlipHorizontal = true
continue
}
if len(part) > 2 && strings.HasPrefix(part, "r") {
o.Rotate, _ = strconv.Atoi(part[1:])
continue
}
}
return o
}
type Request struct {
URL *url.URL // URL of the image to proxy
Options *Options // Image transformation to perform
}
// Image represents a remote image that is being proxied. It tracks where
// the image was originally retrieved from and how long the image can be cached.
type Image struct {
// URL of original remote image.
URL string
// Expires is the cache expiration time for the original image, as
// returned by the remote server.
Expires time.Time
// Etag returned from server when fetching image.
Etag string
// Bytes contains the actual image.
Bytes []byte
}

View file

@ -27,8 +27,6 @@ import (
"github.com/golang/glog"
"github.com/gregjones/httpcache"
"github.com/willnorris/imageproxy/data"
"github.com/willnorris/imageproxy/transform"
)
// URLError reports a malformed URL error.
@ -42,9 +40,9 @@ func (e URLError) Error() string {
}
// NewRequest parses an http.Request into an image request.
func NewRequest(r *http.Request) (*data.Request, error) {
func NewRequest(r *http.Request) (*Request, error) {
var err error
req := new(data.Request)
req := new(Request)
path := r.URL.Path[1:] // strip leading slash
req.URL, err = url.Parse(path)
@ -60,7 +58,7 @@ func NewRequest(r *http.Request) (*data.Request, error) {
return nil, URLError{fmt.Sprintf("unable to parse remote URL: %v", err), r.URL}
}
req.Options = data.ParseOptions(parts[0])
req.Options = ParseOptions(parts[0])
}
if !req.URL.IsAbs() {
@ -142,7 +140,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
b, _ := transform.Transform(image.Bytes, req.Options)
b, _ := Transform(image.Bytes, req.Options)
image.Bytes = b
w.Header().Add("Content-Length", strconv.Itoa(len(image.Bytes)))
@ -150,7 +148,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write(image.Bytes)
}
func (p *Proxy) fetchRemoteImage(u string) (*data.Image, error) {
func (p *Proxy) fetchRemoteImage(u string) (*Image, error) {
resp, err := p.Client.Get(u)
if err != nil {
return nil, err
@ -166,7 +164,7 @@ func (p *Proxy) fetchRemoteImage(u string) (*data.Image, error) {
return nil, err
}
return &data.Image{
return &Image{
URL: u,
Expires: parseExpires(resp),
Etag: resp.Header.Get("Etag"),

View file

@ -18,17 +18,13 @@ import (
"net/http"
"reflect"
"testing"
"github.com/willnorris/imageproxy/data"
)
var emptyOptions = new(data.Options)
func TestNewRequest(t *testing.T) {
tests := []struct {
URL string
RemoteURL string
Options *data.Options
Options *Options
ExpectError bool
}{
// invalid URLs
@ -52,7 +48,7 @@ func TestNewRequest(t *testing.T) {
},
{
"http://localhost/1xs/http://example.com/",
"http://example.com/", &data.Options{Width: 1}, false,
"http://example.com/", &Options{Width: 1}, false,
},
// valid URLs
@ -84,27 +80,27 @@ func TestNewRequest(t *testing.T) {
},
{
"http://localhost/1x/http://example.com/",
"http://example.com/", &data.Options{1, 0, false, 0, false, false}, false,
"http://example.com/", &Options{1, 0, false, 0, false, false}, false,
},
{
"http://localhost/x1/http://example.com/",
"http://example.com/", &data.Options{0, 1, false, 0, false, false}, false,
"http://example.com/", &Options{0, 1, false, 0, false, false}, false,
},
{
"http://localhost/1x2/http://example.com/",
"http://example.com/", &data.Options{1, 2, false, 0, false, false}, false,
"http://example.com/", &Options{1, 2, false, 0, false, false}, false,
},
{
"http://localhost/0.1x0.2/http://example.com/",
"http://example.com/", &data.Options{0.1, 0.2, false, 0, false, false}, false,
"http://example.com/", &Options{0.1, 0.2, false, 0, false, false}, false,
},
{
"http://localhost/,fit/http://example.com/",
"http://example.com/", &data.Options{0, 0, true, 0, false, false}, false,
"http://example.com/", &Options{0, 0, true, 0, false, false}, false,
},
{
"http://localhost/1x2,fit,r=90,fv,fh/http://example.com/",
"http://example.com/", &data.Options{1, 2, true, 90, true, true}, false,
"http://localhost/1x2,fit,r90,fv,fh/http://example.com/",
"http://example.com/", &Options{1, 2, true, 90, true, true}, false,
},
}

106
proxy/transform.go Normal file
View file

@ -0,0 +1,106 @@
// Copyright 2013 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package transform handles image transformation such as resizing.
package proxy
import (
"bytes"
"image"
"image/gif"
"image/jpeg"
"image/png"
"reflect"
"github.com/disintegration/imaging"
)
var emptyOptions = new(Options)
// Transform the provided image.
func Transform(img []byte, opt *Options) ([]byte, error) {
if opt == nil || reflect.DeepEqual(opt, emptyOptions) {
// bail if no transformation was requested
return img, nil
}
// decode image
m, format, err := image.Decode(bytes.NewReader(img))
if err != nil {
return nil, err
}
// convert percentage width and height values to absolute values
var h, w int
if opt.Width > 0 && opt.Width < 1 {
w = int(float64(m.Bounds().Max.X-m.Bounds().Min.X) * opt.Width)
} else {
w = int(opt.Width)
}
if opt.Height > 0 && opt.Height < 1 {
h = int(float64(m.Bounds().Max.Y-m.Bounds().Min.Y) * opt.Height)
} else {
h = int(opt.Height)
}
// resize
if w != 0 || h != 0 {
if opt.Fit {
m = imaging.Fit(m, w, h, imaging.Lanczos)
} else {
if w == 0 || h == 0 {
m = imaging.Resize(m, w, h, imaging.Lanczos)
} else {
m = imaging.Thumbnail(m, w, h, imaging.Lanczos)
}
}
}
// flip
if opt.FlipVertical {
m = imaging.FlipV(m)
}
if opt.FlipHorizontal {
m = imaging.FlipH(m)
}
// rotate
switch opt.Rotate {
case 90:
m = imaging.Rotate90(m)
break
case 180:
m = imaging.Rotate180(m)
break
case 270:
m = imaging.Rotate270(m)
break
}
// encode image
buf := new(bytes.Buffer)
switch format {
case "gif":
gif.Encode(buf, m, nil)
break
case "jpeg":
jpeg.Encode(buf, m, nil)
break
case "png":
png.Encode(buf, m)
break
}
return buf.Bytes(), nil
}