mirror of
https://github.com/h2non/imaginary.git
synced 2026-05-29 11:18:41 +02:00
@@ -54,6 +54,7 @@ https://github.com/kr/heroku-buildpack-go.git
|
||||
- Extract area
|
||||
- Watermark (fully customizable text-based)
|
||||
- Format conversion (with additional quality/compression settings)
|
||||
- Info (image size, format, orientation, alpha...)
|
||||
|
||||
## Performance
|
||||
|
||||
@@ -67,15 +68,62 @@ Here you can see some performance test comparisons for multiple scenarios:
|
||||
## Usage
|
||||
|
||||
```
|
||||
imaginary server
|
||||
|
||||
Usage:
|
||||
imaginary -p 80
|
||||
imaginary -h | -help
|
||||
imaginary -v | -version
|
||||
|
||||
Options:
|
||||
-a <addr> bind address [default: *]
|
||||
-p <port> bind port [default: 8088]
|
||||
-h, -help output help
|
||||
-v, -version output version
|
||||
-cpus <num> Number of used cpu cores.
|
||||
(default for current machine is 8 cores)
|
||||
```
|
||||
|
||||
Start the server on a custom port
|
||||
```bash
|
||||
imaginary -p 8080
|
||||
```
|
||||
|
||||
You can pass it also as environment variable
|
||||
```bash
|
||||
POST=8080 imaginary
|
||||
```
|
||||
|
||||
## HTTP API
|
||||
|
||||
#### GET /crop
|
||||
#### GET /form
|
||||
|
||||
Serve a very ugly HTML form just for testing purposes
|
||||
|
||||
#### POST /crop
|
||||
|
||||
#### POST /resize
|
||||
|
||||
#### POST /enlarge
|
||||
|
||||
#### POST /zoom
|
||||
|
||||
#### POST /thumbnail
|
||||
|
||||
#### POST /rotate
|
||||
|
||||
#### POST /flip
|
||||
|
||||
#### POST /flop
|
||||
|
||||
#### POST /extract
|
||||
|
||||
#### POST /convert
|
||||
|
||||
#### POST /info
|
||||
|
||||
#### POST /watermark
|
||||
|
||||
## License
|
||||
|
||||
MIT - Tomas Aparicio
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
go build
|
||||
|
||||
./imgine -p 8088 > /dev/null &
|
||||
sleep 1
|
||||
|
||||
echo "Running resize tests"
|
||||
|
||||
ab -c 1 -n 1 -v 4 \
|
||||
-p benchmark/data.txt \
|
||||
-T "multipart/form-data; boundary=1234567890" \
|
||||
http://localhost:8088/resize
|
||||
|
||||
echo
|
||||
echo "Running crop tests"
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func indexController(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write([]byte("imaginary server " + Version))
|
||||
}
|
||||
|
||||
const formText = `
|
||||
<html>
|
||||
<body>
|
||||
<h1>Resize</h1>
|
||||
<form method="POST" action="/resize?width=300&height=200&type=png" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
<h1>Crop</h1>
|
||||
<form method="POST" action="/crop" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
<h1>Flip</h1>
|
||||
<form method="POST" action="/flip" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
<h1>Flop</h1>
|
||||
<form method="POST" action="/flop" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
<h1>Rotate (180)</h1>
|
||||
<form method="POST" action="/rotate?rotate=180" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
<h1>Thumbnail</h1>
|
||||
<form method="POST" action="/thumbnail?width=100" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
<h1>Zoom</h1>
|
||||
<form method="POST" action="/zoom?factor=2&width=300&height=300" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
<h1>Watermark</h1>
|
||||
<form method="POST" action="/watermark?text=Hello&font=sans%2014&opacity=0.5" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
<h1>Convert</h1>
|
||||
<form method="POST" action="/convert?type=png" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
func formController(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(formText))
|
||||
}
|
||||
|
||||
func infoController(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(formText))
|
||||
}
|
||||
|
||||
func mainController(fn Operation) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
imageController(w, r, Operation(fn))
|
||||
}
|
||||
}
|
||||
|
||||
func imageController(w http.ResponseWriter, r *http.Request, Operation Operation) {
|
||||
if r.Method != "POST" {
|
||||
errorResponse(w, "Method not allowed for this endpoint", NOT_ALLOWED)
|
||||
return
|
||||
}
|
||||
|
||||
var buf []byte
|
||||
var err error
|
||||
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(contentType, "multipart/") {
|
||||
err = r.ParseMultipartForm(maxMemory)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
buf, err = getFormPayload(r)
|
||||
if err != nil {
|
||||
errorResponse(w, "Error while reading the body: "+err.Error(), BAD_REQUEST)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
buf, _ = ioutil.ReadAll(r.Body)
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
errorResponse(w, "Empty or invalid body", BAD_REQUEST)
|
||||
return
|
||||
}
|
||||
|
||||
mimeType := http.DetectContentType(buf)
|
||||
if IsImageTypeSupported(mimeType) == false {
|
||||
errorResponse(w, "Unsupported media type: "+mimeType, UNSUPPORTED)
|
||||
return
|
||||
}
|
||||
|
||||
opts, err := parseQueryParams(r)
|
||||
if err != nil {
|
||||
errorResponse(w, err.Error(), BAD_REQUEST)
|
||||
return
|
||||
}
|
||||
|
||||
if opts.Type != "" {
|
||||
format := ImageType(opts.Type)
|
||||
if format == 0 {
|
||||
errorResponse(w, "Unsupported image format: "+opts.Type, BAD_REQUEST)
|
||||
return
|
||||
}
|
||||
mimeType = GetImageMimeType(format)
|
||||
}
|
||||
|
||||
debug("Options: %#v", opts)
|
||||
body, err := Operation.Run(buf, opts)
|
||||
if err != nil {
|
||||
errorResponse(w, "Error while processing the image: "+err.Error(), BAD_REQUEST)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", mimeType)
|
||||
w.Write(body)
|
||||
}
|
||||
|
||||
var allowedParams = map[string]string{
|
||||
"width": "int",
|
||||
"height": "int",
|
||||
"quality": "int",
|
||||
"top": "int",
|
||||
"left": "int",
|
||||
"compression": "int",
|
||||
"rotate": "int",
|
||||
"margin": "int",
|
||||
"factor": "int",
|
||||
"opacity": "float",
|
||||
"noreplicate": "bool",
|
||||
"text": "string",
|
||||
"font": "string",
|
||||
"format": "string",
|
||||
"type": "string",
|
||||
}
|
||||
|
||||
func parseQueryParams(r *http.Request) (ImageOptions, error) {
|
||||
var val interface{}
|
||||
query := r.URL.Query()
|
||||
params := make(map[string]interface{})
|
||||
|
||||
for key, kind := range allowedParams {
|
||||
key = strings.ToLower(key)
|
||||
param := query.Get(key)
|
||||
|
||||
switch kind {
|
||||
case "int":
|
||||
val, _ = strconv.Atoi(param)
|
||||
break
|
||||
case "float":
|
||||
val, _ = strconv.ParseFloat(param, 64)
|
||||
break
|
||||
case "string":
|
||||
val = param
|
||||
break
|
||||
case "bool":
|
||||
if param != "" {
|
||||
val = true
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
params[key] = val
|
||||
}
|
||||
|
||||
opts := ImageOptions{
|
||||
Width: params["width"].(int),
|
||||
Height: params["height"].(int),
|
||||
Quality: params["quality"].(int),
|
||||
Compression: params["compression"].(int),
|
||||
Rotate: params["rotate"].(int),
|
||||
Factor: params["factor"].(int),
|
||||
Text: params["text"].(string),
|
||||
Font: params["font"].(string),
|
||||
Type: params["type"].(string),
|
||||
Opacity: params["opacity"].(float64),
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func getFormPayload(r *http.Request) ([]byte, error) {
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buf, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(buf) == 0 {
|
||||
return nil, NewError("Empty payload", BAD_REQUEST)
|
||||
}
|
||||
|
||||
return buf, err
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
UNAVAILABLE uint8 = iota
|
||||
BAD_REQUEST
|
||||
NOT_ALLOWED
|
||||
UNSUPPORTED
|
||||
INTERNAL
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
Code uint8 `json:"code"`
|
||||
}
|
||||
|
||||
func (e *Error) JSON() []byte {
|
||||
buf, _ := json.Marshal(e)
|
||||
return buf
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (e *Error) HttpCode() int {
|
||||
code := http.StatusServiceUnavailable
|
||||
switch e.Code {
|
||||
case BAD_REQUEST:
|
||||
code = http.StatusBadRequest
|
||||
break
|
||||
case NOT_ALLOWED:
|
||||
code = http.StatusMethodNotAllowed
|
||||
break
|
||||
case UNSUPPORTED:
|
||||
code = http.StatusUnsupportedMediaType
|
||||
break
|
||||
case INTERNAL:
|
||||
code = http.StatusInternalServerError
|
||||
break
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
func NewError(err string, code uint8) *Error {
|
||||
err = strings.Replace(err, "\n", "", -1)
|
||||
return &Error{err, code}
|
||||
}
|
||||
|
||||
func errorResponse(w http.ResponseWriter, msg string, code uint8) {
|
||||
err := NewError(msg, code)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(err.HttpCode())
|
||||
w.Write(err.JSON())
|
||||
}
|
||||
@@ -1,34 +1,154 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"gopkg.in/h2non/bimg.v0"
|
||||
)
|
||||
|
||||
type ImageOptions struct {
|
||||
Width int
|
||||
Height int
|
||||
Quality int
|
||||
Width int
|
||||
Height int
|
||||
Quality int
|
||||
Compression int
|
||||
Rotate int
|
||||
Top int
|
||||
Left int
|
||||
Margin int
|
||||
Factor int
|
||||
Opacity float64
|
||||
Text string
|
||||
Font string
|
||||
Color string
|
||||
Type string
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
Body []byte
|
||||
type Operation func([]byte, ImageOptions) ([]byte, error)
|
||||
|
||||
func (o Operation) Run(buf []byte, opts ImageOptions) ([]byte, error) {
|
||||
return o(buf, opts)
|
||||
}
|
||||
|
||||
func validateParams(r *http.Request) (*ImageOptions, error) {
|
||||
query := r.URL.Query()
|
||||
width, _ := strconv.Atoi(query.Get("width"))
|
||||
height, _ := strconv.Atoi(query.Get("height"))
|
||||
quality, _ := strconv.Atoi(query.Get("quality"))
|
||||
func bimgOptions(o ImageOptions) bimg.Options {
|
||||
return bimg.Options{
|
||||
Width: o.Width,
|
||||
Height: o.Height,
|
||||
Quality: o.Quality,
|
||||
Compression: o.Quality,
|
||||
Type: ImageType(o.Type),
|
||||
}
|
||||
}
|
||||
|
||||
if width == 0 || height == 0 {
|
||||
return nil, errors.New("Missing required height and width params")
|
||||
func Resize(buf []byte, o ImageOptions) ([]byte, error) {
|
||||
if o.Width == 0 || o.Height == 0 {
|
||||
return nil, NewError("Missing required params: height, width", BAD_REQUEST)
|
||||
}
|
||||
|
||||
return &ImageOptions{
|
||||
Width: width,
|
||||
Height: height,
|
||||
Quality: quality,
|
||||
}, nil
|
||||
opts := bimgOptions(o)
|
||||
return Process(buf, opts)
|
||||
}
|
||||
|
||||
func Enlarge(buf []byte, o ImageOptions) ([]byte, error) {
|
||||
if o.Width == 0 || o.Height == 0 {
|
||||
return nil, NewError("Missing required params: height, width", BAD_REQUEST)
|
||||
}
|
||||
|
||||
opts := bimgOptions(o)
|
||||
opts.Enlarge = true
|
||||
return Process(buf, opts)
|
||||
}
|
||||
|
||||
func Crop(buf []byte, o ImageOptions) ([]byte, error) {
|
||||
opts := bimgOptions(o)
|
||||
opts.Crop = true
|
||||
return Process(buf, opts)
|
||||
}
|
||||
|
||||
func Rotate(buf []byte, o ImageOptions) ([]byte, error) {
|
||||
if o.Rotate == 0 {
|
||||
return nil, NewError("Missing rotate param", BAD_REQUEST)
|
||||
}
|
||||
|
||||
opts := bimgOptions(o)
|
||||
opts.Rotate = bimg.Angle(o.Rotate)
|
||||
return Process(buf, opts)
|
||||
}
|
||||
|
||||
func Flip(buf []byte, o ImageOptions) ([]byte, error) {
|
||||
opts := bimgOptions(o)
|
||||
opts.Flip = true
|
||||
return Process(buf, opts)
|
||||
}
|
||||
|
||||
func Flop(buf []byte, o ImageOptions) ([]byte, error) {
|
||||
opts := bimgOptions(o)
|
||||
opts.Flop = true
|
||||
return Process(buf, opts)
|
||||
}
|
||||
|
||||
func Thumbnail(buf []byte, o ImageOptions) ([]byte, error) {
|
||||
if o.Width == 0 && o.Height == 0 {
|
||||
return nil, NewError("Missing required params: width or height", BAD_REQUEST)
|
||||
}
|
||||
|
||||
opts := bimgOptions(o)
|
||||
return Process(buf, opts)
|
||||
}
|
||||
|
||||
func Zoom(buf []byte, o ImageOptions) ([]byte, error) {
|
||||
debug("Options: ")
|
||||
if o.Width == 0 || o.Height == 0 || o.Factor == 0 {
|
||||
return nil, NewError("Missing required params: width, height, factor", BAD_REQUEST)
|
||||
}
|
||||
|
||||
opts := bimgOptions(o)
|
||||
//opts.Crop = true
|
||||
opts.Zoom = o.Factor
|
||||
return Process(buf, opts)
|
||||
}
|
||||
|
||||
func Convert(buf []byte, o ImageOptions) ([]byte, error) {
|
||||
if o.Type == "" {
|
||||
return nil, NewError("Missing required params: type", BAD_REQUEST)
|
||||
}
|
||||
|
||||
opts := bimgOptions(o)
|
||||
return Process(buf, opts)
|
||||
}
|
||||
|
||||
func Watermark(buf []byte, o ImageOptions) ([]byte, error) {
|
||||
if o.Text == "" {
|
||||
return nil, NewError("Missing required params: text", BAD_REQUEST)
|
||||
}
|
||||
|
||||
opts := bimgOptions(o)
|
||||
opts.Watermark.Text = o.Text
|
||||
opts.Watermark.Font = o.Font
|
||||
opts.Watermark.Margin = o.Margin
|
||||
opts.Watermark.Opacity = float32(o.Opacity)
|
||||
return Process(buf, opts)
|
||||
}
|
||||
|
||||
func Extract(buf []byte, o ImageOptions) ([]byte, error) {
|
||||
if o.Top == 0 || o.Left == 0 {
|
||||
return nil, NewError("Missing required params: top, left", BAD_REQUEST)
|
||||
}
|
||||
|
||||
opts := bimgOptions(o)
|
||||
return Process(buf, opts)
|
||||
}
|
||||
|
||||
type ImageInfo struct {
|
||||
Size int `json:"size"`
|
||||
Width int `json:"width"`
|
||||
Format string `json:"format"`
|
||||
Height int `json:"height"`
|
||||
Orientation int `json:"orientation"`
|
||||
Alpha bool `json:"alpha"`
|
||||
}
|
||||
|
||||
func Info(buf []byte, o ImageOptions) ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func Process(buf []byte, opts bimg.Options) ([]byte, error) {
|
||||
return bimg.Resize(buf, opts)
|
||||
}
|
||||
|
||||
+4
-1
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
. "github.com/tj/go-debug"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -18,6 +19,8 @@ var (
|
||||
aCpus = flag.Int("cpus", runtime.GOMAXPROCS(-1), "")
|
||||
)
|
||||
|
||||
var debug = Debug("imaginary")
|
||||
|
||||
const usage = `imaginary server %s
|
||||
|
||||
Usage:
|
||||
@@ -59,7 +62,7 @@ func main() {
|
||||
|
||||
debug("imaginary server listening on port %d", port)
|
||||
|
||||
err := NewServer(opts)
|
||||
err := Server(opts)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cannot start the server: %s\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const formatPattern = "%s - - [%s] \"%s\" %d %d %.4f\n"
|
||||
|
||||
type LogRecord struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
responseBytes int64
|
||||
ip string
|
||||
method, uri, protocol string
|
||||
time time.Time
|
||||
elapsedTime time.Duration
|
||||
}
|
||||
|
||||
func (r *LogRecord) Log(out io.Writer) {
|
||||
timeFormat := r.time.Format("02/Jan/2006 03:04:05")
|
||||
request := fmt.Sprintf("%s %s %s", r.method, r.uri, r.protocol)
|
||||
fmt.Fprintf(out, formatPattern, r.ip, timeFormat, request, r.status, r.responseBytes, r.elapsedTime.Seconds())
|
||||
}
|
||||
|
||||
func (r *LogRecord) Write(p []byte) (int, error) {
|
||||
written, err := r.ResponseWriter.Write(p)
|
||||
r.responseBytes += int64(written)
|
||||
return written, err
|
||||
}
|
||||
|
||||
func (r *LogRecord) WriteHeader(status int) {
|
||||
r.status = status
|
||||
r.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
type LogHandler struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func NewLog(handler http.Handler) http.Handler {
|
||||
return &LogHandler{handler}
|
||||
}
|
||||
|
||||
func (h *LogHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
clientIP := r.RemoteAddr
|
||||
if colon := strings.LastIndex(clientIP, ":"); colon != -1 {
|
||||
clientIP = clientIP[:colon]
|
||||
}
|
||||
|
||||
record := &LogRecord{
|
||||
ResponseWriter: w,
|
||||
ip: clientIP,
|
||||
time: time.Time{},
|
||||
method: r.Method,
|
||||
uri: r.RequestURI,
|
||||
protocol: r.Proto,
|
||||
status: http.StatusOK,
|
||||
elapsedTime: time.Duration(0),
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
h.handler.ServeHTTP(record, r)
|
||||
finishTime := time.Now()
|
||||
|
||||
record.time = finishTime.UTC()
|
||||
record.elapsedTime = finishTime.Sub(startTime)
|
||||
|
||||
record.Log(os.Stdout)
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gopkg.in/h2non/bimg.v0"
|
||||
)
|
||||
|
||||
func NewOptions(width int, height int, quality int) bimg.Options {
|
||||
return bimg.Options{
|
||||
Width: width,
|
||||
Height: height,
|
||||
Quality: quality,
|
||||
Crop: false,
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gopkg.in/h2non/bimg.v0"
|
||||
)
|
||||
|
||||
func Resize(imageBuf []byte) ([]byte, error) {
|
||||
options := bimg.Options{
|
||||
Width: 562,
|
||||
Height: 562,
|
||||
Crop: true,
|
||||
Quality: 95,
|
||||
}
|
||||
|
||||
return bimg.Resize(imageBuf, options)
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/daaku/go.httpgzip"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -18,27 +14,28 @@ type ServerOptions struct {
|
||||
Address string
|
||||
}
|
||||
|
||||
func NewServer(o ServerOptions) error {
|
||||
func Server(o ServerOptions) error {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/form", middleware(uploadForm))
|
||||
mux.Handle("/extract", middleware(processImage))
|
||||
mux.Handle("/enlarge", middleware(processImage))
|
||||
mux.Handle("/resize", middleware(processImage))
|
||||
mux.Handle("/crop", middleware(processImage))
|
||||
mux.Handle("/thumbnail", middleware(processImage))
|
||||
mux.Handle("/rotate", middleware(processImage))
|
||||
mux.Handle("/flip", middleware(processImage))
|
||||
mux.Handle("/flop", middleware(processImage))
|
||||
mux.Handle("/zoom", middleware(processImage))
|
||||
mux.Handle("/format", middleware(processImage))
|
||||
mux.Handle("/convert", middleware(processImage))
|
||||
mux.Handle("/watermark", middleware(processImage))
|
||||
mux.Handle("/", middleware(indexHandler))
|
||||
|
||||
mux.Handle("/", middleware(indexController))
|
||||
mux.Handle("/form", middleware(formController))
|
||||
mux.Handle("/resize", imageHandler(Resize))
|
||||
mux.Handle("/enlarge", imageHandler(Enlarge))
|
||||
mux.Handle("/extract", imageHandler(Extract))
|
||||
mux.Handle("/crop", imageHandler(Crop))
|
||||
mux.Handle("/rotate", imageHandler(Rotate))
|
||||
mux.Handle("/flip", imageHandler(Flip))
|
||||
mux.Handle("/flop", imageHandler(Flop))
|
||||
mux.Handle("/thumbnail", imageHandler(Thumbnail))
|
||||
mux.Handle("/zoom", imageHandler(Zoom))
|
||||
mux.Handle("/convert", imageHandler(Convert))
|
||||
mux.Handle("/watermark", imageHandler(Watermark))
|
||||
mux.Handle("/info", imageHandler(Info))
|
||||
|
||||
addr := o.Address + ":" + strconv.Itoa(o.Port)
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
Handler: NewLog(mux),
|
||||
ReadTimeout: 60 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
@@ -47,146 +44,24 @@ func NewServer(o ServerOptions) error {
|
||||
return server.ListenAndServe()
|
||||
}
|
||||
|
||||
func handler(fn func(http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return middleware(http.HandlerFunc(fn))
|
||||
func imageHandler(fn Operation) http.Handler {
|
||||
return middleware(mainController(fn))
|
||||
}
|
||||
|
||||
func middleware(fn func(http.ResponseWriter, *http.Request)) http.Handler {
|
||||
next := httpgzip.NewHandler(http.HandlerFunc(fn))
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Server", "imaginary "+Version)
|
||||
logger(r)
|
||||
validateRequest(next).ServeHTTP(w, r)
|
||||
validate(next).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func logger(r *http.Request) {
|
||||
remoteAddr := r.Header.Get("X-Forwarded-For")
|
||||
if remoteAddr == "" {
|
||||
remoteAddr = r.RemoteAddr
|
||||
}
|
||||
log.Printf("[%s] %s %q\n", r.Method, remoteAddr, r.URL.String())
|
||||
}
|
||||
|
||||
func validateRequest(next http.Handler) http.Handler {
|
||||
func validate(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" && r.Method != "POST" {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
errorResponse(w, "Method not allowed: "+r.Method, NOT_ALLOWED)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "POST" && r.ContentLength == 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write([]byte("imaginary server " + Version))
|
||||
}
|
||||
|
||||
const formText = `
|
||||
<html>
|
||||
<body>
|
||||
<h1>Resize</h1>
|
||||
<form method="POST" action="/resize?width=400&height=300" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
<h1>Crop</h1>
|
||||
<form method="POST" action="/crop" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
<h1>Flip</h1>
|
||||
<form method="POST" action="/flip" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
<h1>Thumbnail</h1>
|
||||
<form method="POST" action="/thumbnail" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
func uploadForm(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(formText))
|
||||
}
|
||||
|
||||
func processImage(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
contentType = http.DetectContentType(buf)
|
||||
contentType = strings.Split(contentType, ";")[0]
|
||||
}
|
||||
|
||||
// temporal
|
||||
if !strings.HasPrefix(contentType, "multipart/") {
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
return
|
||||
}
|
||||
|
||||
err := r.ParseMultipartForm(maxMemory)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
file, mimeType, err := getPayload(r)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if mimeType != "image/jpeg" && mimeType != "image/png" {
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := Resize(file)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", mimeType)
|
||||
w.Write(body)
|
||||
}
|
||||
|
||||
func getPayload(r *http.Request) ([]byte, string, error) {
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buf, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if len(buf) == 0 {
|
||||
return nil, "", errors.New("Empty payload")
|
||||
}
|
||||
|
||||
mimeType := http.DetectContentType(buf)
|
||||
|
||||
return buf, mimeType, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gopkg.in/h2non/bimg.v0"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ExtractImageTypeFromMime(mime string) string {
|
||||
mime = strings.Split(mime, " ")[0]
|
||||
part := strings.Split(mime, "/")
|
||||
if len(part) < 2 {
|
||||
return ""
|
||||
}
|
||||
return strings.ToLower(part[1])
|
||||
}
|
||||
|
||||
func IsImageTypeSupported(mime string) bool {
|
||||
format := ExtractImageTypeFromMime(mime)
|
||||
return bimg.IsTypeNameSupported(format)
|
||||
}
|
||||
|
||||
func ImageType(mime string) bimg.ImageType {
|
||||
format := bimg.UNKNOWN
|
||||
switch strings.ToLower(mime) {
|
||||
case "jpeg":
|
||||
format = bimg.JPEG
|
||||
break
|
||||
case "png":
|
||||
format = bimg.PNG
|
||||
break
|
||||
case "webp":
|
||||
format = bimg.WEBP
|
||||
break
|
||||
case "tiff":
|
||||
format = bimg.TIFF
|
||||
break
|
||||
}
|
||||
return format
|
||||
}
|
||||
|
||||
func GetImageMimeType(code bimg.ImageType) string {
|
||||
mime := "image/jpeg"
|
||||
switch code {
|
||||
case bimg.PNG:
|
||||
mime = "image/png"
|
||||
break
|
||||
case bimg.WEBP:
|
||||
mime = "image/webp"
|
||||
break
|
||||
}
|
||||
return mime
|
||||
}
|
||||
Reference in New Issue
Block a user