You've already forked imgproxy
mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-12-13 23:57:38 +02:00
Global refactoring
This commit is contained in:
@@ -98,7 +98,7 @@ jobs:
|
|||||||
command: curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | BINARY=golangci-lint sh -s -- -b $(go env GOPATH)/bin v1.18.0
|
command: curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | BINARY=golangci-lint sh -s -- -b $(go env GOPATH)/bin v1.18.0
|
||||||
- run:
|
- run:
|
||||||
name: Lint imgproxy
|
name: Lint imgproxy
|
||||||
command: golangci-lint run .
|
command: golangci-lint run
|
||||||
|
|
||||||
build:
|
build:
|
||||||
executor: imgproxy
|
executor: imgproxy
|
||||||
@@ -119,7 +119,7 @@ jobs:
|
|||||||
- go-modules-{{ checksum "go.sum" }}
|
- go-modules-{{ checksum "go.sum" }}
|
||||||
- run:
|
- run:
|
||||||
name: Build imgproxy
|
name: Build imgproxy
|
||||||
command: go test -v
|
command: go test -v ./...
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: go-modules-{{ checksum "go.sum" }}
|
key: go-modules-{{ checksum "go.sum" }}
|
||||||
paths:
|
paths:
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ issues:
|
|||||||
# False positives on CGO generated code
|
# False positives on CGO generated code
|
||||||
- linters: [staticcheck]
|
- linters: [staticcheck]
|
||||||
text: "SA4000:"
|
text: "SA4000:"
|
||||||
path: vips\.go
|
path: vips/*
|
||||||
|
|
||||||
# False positives on CGO generated code
|
# False positives on CGO generated code
|
||||||
- linters: [gocritic]
|
- linters: [gocritic]
|
||||||
text: "dupSubExpr"
|
text: "dupSubExpr"
|
||||||
path: vips\.go
|
path: vips/*
|
||||||
|
|
||||||
- linters: [stylecheck]
|
- linters: [stylecheck]
|
||||||
text: "ST1005:"
|
text: "ST1005:"
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
package main
|
package bufpool
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imath"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/metrics/prometheus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type intSlice []int
|
type intSlice []int
|
||||||
@@ -13,7 +17,7 @@ func (p intSlice) Len() int { return len(p) }
|
|||||||
func (p intSlice) Less(i, j int) bool { return p[i] < p[j] }
|
func (p intSlice) Less(i, j int) bool { return p[i] < p[j] }
|
||||||
func (p intSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
func (p intSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||||
|
|
||||||
type bufPool struct {
|
type Pool struct {
|
||||||
name string
|
name string
|
||||||
defaultSize int
|
defaultSize int
|
||||||
maxSize int
|
maxSize int
|
||||||
@@ -25,12 +29,12 @@ type bufPool struct {
|
|||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBufPool(name string, n int, defaultSize int) *bufPool {
|
func New(name string, n int, defaultSize int) *Pool {
|
||||||
pool := bufPool{
|
pool := Pool{
|
||||||
name: name,
|
name: name,
|
||||||
defaultSize: defaultSize,
|
defaultSize: defaultSize,
|
||||||
buffers: make([]*bytes.Buffer, n),
|
buffers: make([]*bytes.Buffer, n),
|
||||||
calls: make(intSlice, conf.BufferPoolCalibrationThreshold),
|
calls: make(intSlice, config.BufferPoolCalibrationThreshold),
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range pool.buffers {
|
for i := range pool.buffers {
|
||||||
@@ -40,7 +44,7 @@ func newBufPool(name string, n int, defaultSize int) *bufPool {
|
|||||||
return &pool
|
return &pool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *bufPool) calibrateAndClean() {
|
func (p *Pool) calibrateAndClean() {
|
||||||
sort.Sort(p.calls)
|
sort.Sort(p.calls)
|
||||||
|
|
||||||
pos := int(float64(len(p.calls)) * 0.95)
|
pos := int(float64(len(p.calls)) * 0.95)
|
||||||
@@ -49,8 +53,8 @@ func (p *bufPool) calibrateAndClean() {
|
|||||||
p.callInd = 0
|
p.callInd = 0
|
||||||
p.maxSize = p.normalizeSize(score)
|
p.maxSize = p.normalizeSize(score)
|
||||||
|
|
||||||
p.defaultSize = maxInt(p.defaultSize, p.calls[0])
|
p.defaultSize = imath.Max(p.defaultSize, p.calls[0])
|
||||||
p.maxSize = maxInt(p.defaultSize, p.maxSize)
|
p.maxSize = imath.Max(p.defaultSize, p.maxSize)
|
||||||
|
|
||||||
cleaned := false
|
cleaned := false
|
||||||
|
|
||||||
@@ -65,11 +69,11 @@ func (p *bufPool) calibrateAndClean() {
|
|||||||
runtime.GC()
|
runtime.GC()
|
||||||
}
|
}
|
||||||
|
|
||||||
setPrometheusBufferDefaultSize(p.name, p.defaultSize)
|
prometheus.SetBufferDefaultSize(p.name, p.defaultSize)
|
||||||
setPrometheusBufferMaxSize(p.name, p.maxSize)
|
prometheus.SetBufferMaxSize(p.name, p.maxSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *bufPool) Get(size int) *bytes.Buffer {
|
func (p *Pool) Get(size int) *bytes.Buffer {
|
||||||
p.mutex.Lock()
|
p.mutex.Lock()
|
||||||
defer p.mutex.Unlock()
|
defer p.mutex.Unlock()
|
||||||
|
|
||||||
@@ -111,7 +115,7 @@ func (p *bufPool) Get(size int) *bytes.Buffer {
|
|||||||
|
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
|
|
||||||
growSize := maxInt(size, p.defaultSize)
|
growSize := imath.Max(size, p.defaultSize)
|
||||||
|
|
||||||
if growSize > buf.Cap() {
|
if growSize > buf.Cap() {
|
||||||
buf.Grow(growSize)
|
buf.Grow(growSize)
|
||||||
@@ -120,7 +124,7 @@ func (p *bufPool) Get(size int) *bytes.Buffer {
|
|||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *bufPool) Put(buf *bytes.Buffer) {
|
func (p *Pool) Put(buf *bytes.Buffer) {
|
||||||
p.mutex.Lock()
|
p.mutex.Lock()
|
||||||
defer p.mutex.Unlock()
|
defer p.mutex.Unlock()
|
||||||
|
|
||||||
@@ -142,7 +146,7 @@ func (p *bufPool) Put(buf *bytes.Buffer) {
|
|||||||
p.buffers[i] = buf
|
p.buffers[i] = buf
|
||||||
|
|
||||||
if buf.Cap() > 0 {
|
if buf.Cap() > 0 {
|
||||||
observePrometheusBufferSize(p.name, buf.Cap())
|
prometheus.ObserveBufferSize(p.name, buf.Cap())
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -150,6 +154,6 @@ func (p *bufPool) Put(buf *bytes.Buffer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *bufPool) normalizeSize(n int) int {
|
func (p *Pool) normalizeSize(n int) int {
|
||||||
return (n/bytes.MinRead + 2) * bytes.MinRead
|
return (n/bytes.MinRead + 2) * bytes.MinRead
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,28 @@
|
|||||||
package main
|
package bufreader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type bufReader struct {
|
type Reader struct {
|
||||||
r io.Reader
|
r io.Reader
|
||||||
buf *bytes.Buffer
|
buf *bytes.Buffer
|
||||||
cur int
|
cur int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBufReader(r io.Reader, buf *bytes.Buffer) *bufReader {
|
func New(r io.Reader, buf *bytes.Buffer) *Reader {
|
||||||
br := bufReader{
|
br := Reader{
|
||||||
r: r,
|
r: r,
|
||||||
buf: buf,
|
buf: buf,
|
||||||
}
|
}
|
||||||
return &br
|
return &br
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *bufReader) Read(p []byte) (int, error) {
|
func (br *Reader) Read(p []byte) (int, error) {
|
||||||
if err := br.fill(br.cur + len(p)); err != nil {
|
if err := br.fill(br.cur + len(p)); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -30,7 +32,7 @@ func (br *bufReader) Read(p []byte) (int, error) {
|
|||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *bufReader) ReadByte() (byte, error) {
|
func (br *Reader) ReadByte() (byte, error) {
|
||||||
if err := br.fill(br.cur + 1); err != nil {
|
if err := br.fill(br.cur + 1); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -40,7 +42,7 @@ func (br *bufReader) ReadByte() (byte, error) {
|
|||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *bufReader) Discard(n int) (int, error) {
|
func (br *Reader) Discard(n int) (int, error) {
|
||||||
if n < 0 {
|
if n < 0 {
|
||||||
return 0, bufio.ErrNegativeCount
|
return 0, bufio.ErrNegativeCount
|
||||||
}
|
}
|
||||||
@@ -52,12 +54,12 @@ func (br *bufReader) Discard(n int) (int, error) {
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
n = minInt(n, br.buf.Len()-br.cur)
|
n = imath.Min(n, br.buf.Len()-br.cur)
|
||||||
br.cur += n
|
br.cur += n
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *bufReader) Peek(n int) ([]byte, error) {
|
func (br *Reader) Peek(n int) ([]byte, error) {
|
||||||
if n < 0 {
|
if n < 0 {
|
||||||
return []byte{}, bufio.ErrNegativeCount
|
return []byte{}, bufio.ErrNegativeCount
|
||||||
}
|
}
|
||||||
@@ -76,18 +78,18 @@ func (br *bufReader) Peek(n int) ([]byte, error) {
|
|||||||
return br.buf.Bytes()[br.cur : br.cur+n], nil
|
return br.buf.Bytes()[br.cur : br.cur+n], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *bufReader) Flush() error {
|
func (br *Reader) Flush() error {
|
||||||
_, err := br.buf.ReadFrom(br.r)
|
_, err := br.buf.ReadFrom(br.r)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *bufReader) fill(need int) error {
|
func (br *Reader) fill(need int) error {
|
||||||
n := need - br.buf.Len()
|
n := need - br.buf.Len()
|
||||||
if n <= 0 {
|
if n <= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
n = maxInt(4096, n)
|
n = imath.Max(4096, n)
|
||||||
|
|
||||||
if _, err := br.buf.ReadFrom(io.LimitReader(br.r, int64(n))); err != nil {
|
if _, err := br.buf.ReadFrom(io.LimitReader(br.r, int64(n))); err != nil {
|
||||||
return err
|
return err
|
||||||
607
config.go
607
config.go
@@ -1,607 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/hex"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func intEnvConfig(i *int, name string) {
|
|
||||||
if env, err := strconv.Atoi(os.Getenv(name)); err == nil {
|
|
||||||
*i = env
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func floatEnvConfig(i *float64, name string) {
|
|
||||||
if env, err := strconv.ParseFloat(os.Getenv(name), 64); err == nil {
|
|
||||||
*i = env
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func megaIntEnvConfig(f *int, name string) {
|
|
||||||
if env, err := strconv.ParseFloat(os.Getenv(name), 64); err == nil {
|
|
||||||
*f = int(env * 1000000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func strEnvConfig(s *string, name string) {
|
|
||||||
if env := os.Getenv(name); len(env) > 0 {
|
|
||||||
*s = env
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func strSliceEnvConfig(s *[]string, name string) {
|
|
||||||
if env := os.Getenv(name); len(env) > 0 {
|
|
||||||
parts := strings.Split(env, ",")
|
|
||||||
|
|
||||||
for i, p := range parts {
|
|
||||||
parts[i] = strings.TrimSpace(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
*s = parts
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
*s = []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func boolEnvConfig(b *bool, name string) {
|
|
||||||
if env, err := strconv.ParseBool(os.Getenv(name)); err == nil {
|
|
||||||
*b = env
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func imageTypesEnvConfig(it *[]imageType, name string) {
|
|
||||||
*it = []imageType{}
|
|
||||||
|
|
||||||
if env := os.Getenv(name); len(env) > 0 {
|
|
||||||
parts := strings.Split(env, ",")
|
|
||||||
|
|
||||||
for _, p := range parts {
|
|
||||||
pt := strings.TrimSpace(p)
|
|
||||||
if t, ok := imageTypes[pt]; ok {
|
|
||||||
*it = append(*it, t)
|
|
||||||
} else {
|
|
||||||
logWarning("Unknown image format to skip: %s", pt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatQualityEnvConfig(m map[imageType]int, name string) {
|
|
||||||
if env := os.Getenv(name); len(env) > 0 {
|
|
||||||
parts := strings.Split(env, ",")
|
|
||||||
|
|
||||||
for _, p := range parts {
|
|
||||||
i := strings.Index(p, "=")
|
|
||||||
if i < 0 {
|
|
||||||
logWarning("Invalid format quality string: %s", p)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
imgtypeStr, qStr := strings.TrimSpace(p[:i]), strings.TrimSpace(p[i+1:])
|
|
||||||
|
|
||||||
imgtype, ok := imageTypes[imgtypeStr]
|
|
||||||
if !ok {
|
|
||||||
logWarning("Invalid format: %s", p)
|
|
||||||
}
|
|
||||||
|
|
||||||
q, err := strconv.Atoi(qStr)
|
|
||||||
if err != nil || q <= 0 || q > 100 {
|
|
||||||
logWarning("Invalid quality: %s", p)
|
|
||||||
}
|
|
||||||
|
|
||||||
m[imgtype] = q
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hexEnvConfig(b *[]securityKey, name string) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if env := os.Getenv(name); len(env) > 0 {
|
|
||||||
parts := strings.Split(env, ",")
|
|
||||||
|
|
||||||
keys := make([]securityKey, len(parts))
|
|
||||||
|
|
||||||
for i, part := range parts {
|
|
||||||
if keys[i], err = hex.DecodeString(part); err != nil {
|
|
||||||
return fmt.Errorf("%s expected to be hex-encoded strings. Invalid: %s\n", name, part)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*b = keys
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func hexFileConfig(b *[]securityKey, filepath string) error {
|
|
||||||
if len(filepath) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Open(filepath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Can't open file %s\n", filepath)
|
|
||||||
}
|
|
||||||
|
|
||||||
keys := []securityKey{}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(f)
|
|
||||||
for scanner.Scan() {
|
|
||||||
part := scanner.Text()
|
|
||||||
|
|
||||||
if len(part) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if key, err := hex.DecodeString(part); err == nil {
|
|
||||||
keys = append(keys, key)
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("%s expected to contain hex-encoded strings. Invalid: %s\n", filepath, part)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return fmt.Errorf("Failed to read file %s: %s", filepath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*b = keys
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func presetEnvConfig(p presets, name string) error {
|
|
||||||
if env := os.Getenv(name); len(env) > 0 {
|
|
||||||
presetStrings := strings.Split(env, ",")
|
|
||||||
|
|
||||||
for _, presetStr := range presetStrings {
|
|
||||||
if err := parsePreset(p, presetStr); err != nil {
|
|
||||||
return fmt.Errorf(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func presetFileConfig(p presets, filepath string) error {
|
|
||||||
if len(filepath) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Open(filepath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Can't open file %s\n", filepath)
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(f)
|
|
||||||
for scanner.Scan() {
|
|
||||||
if err := parsePreset(p, scanner.Text()); err != nil {
|
|
||||||
return fmt.Errorf(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return fmt.Errorf("Failed to read presets file: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type config struct {
|
|
||||||
Network string
|
|
||||||
Bind string
|
|
||||||
ReadTimeout int
|
|
||||||
WriteTimeout int
|
|
||||||
KeepAliveTimeout int
|
|
||||||
DownloadTimeout int
|
|
||||||
Concurrency int
|
|
||||||
MaxClients int
|
|
||||||
|
|
||||||
TTL int
|
|
||||||
CacheControlPassthrough bool
|
|
||||||
SetCanonicalHeader bool
|
|
||||||
|
|
||||||
SoReuseport bool
|
|
||||||
|
|
||||||
PathPrefix string
|
|
||||||
|
|
||||||
MaxSrcResolution int
|
|
||||||
MaxSrcFileSize int
|
|
||||||
MaxAnimationFrames int
|
|
||||||
MaxSvgCheckBytes int
|
|
||||||
|
|
||||||
JpegProgressive bool
|
|
||||||
PngInterlaced bool
|
|
||||||
PngQuantize bool
|
|
||||||
PngQuantizationColors int
|
|
||||||
Quality int
|
|
||||||
FormatQuality map[imageType]int
|
|
||||||
StripMetadata bool
|
|
||||||
StripColorProfile bool
|
|
||||||
AutoRotate bool
|
|
||||||
|
|
||||||
EnableWebpDetection bool
|
|
||||||
EnforceWebp bool
|
|
||||||
EnableAvifDetection bool
|
|
||||||
EnforceAvif bool
|
|
||||||
EnableClientHints bool
|
|
||||||
|
|
||||||
SkipProcessingFormats []imageType
|
|
||||||
|
|
||||||
UseLinearColorspace bool
|
|
||||||
DisableShrinkOnLoad bool
|
|
||||||
|
|
||||||
Keys []securityKey
|
|
||||||
Salts []securityKey
|
|
||||||
AllowInsecure bool
|
|
||||||
SignatureSize int
|
|
||||||
|
|
||||||
Secret string
|
|
||||||
|
|
||||||
AllowOrigin string
|
|
||||||
|
|
||||||
UserAgent string
|
|
||||||
|
|
||||||
IgnoreSslVerification bool
|
|
||||||
DevelopmentErrorsMode bool
|
|
||||||
|
|
||||||
AllowedSources []string
|
|
||||||
LocalFileSystemRoot string
|
|
||||||
S3Enabled bool
|
|
||||||
S3Region string
|
|
||||||
S3Endpoint string
|
|
||||||
GCSEnabled bool
|
|
||||||
GCSKey string
|
|
||||||
ABSEnabled bool
|
|
||||||
ABSName string
|
|
||||||
ABSKey string
|
|
||||||
ABSEndpoint string
|
|
||||||
|
|
||||||
ETagEnabled bool
|
|
||||||
|
|
||||||
BaseURL string
|
|
||||||
|
|
||||||
Presets presets
|
|
||||||
OnlyPresets bool
|
|
||||||
|
|
||||||
WatermarkData string
|
|
||||||
WatermarkPath string
|
|
||||||
WatermarkURL string
|
|
||||||
WatermarkOpacity float64
|
|
||||||
|
|
||||||
FallbackImageData string
|
|
||||||
FallbackImagePath string
|
|
||||||
FallbackImageURL string
|
|
||||||
FallbackImageHTTPCode int
|
|
||||||
|
|
||||||
DataDogEnable bool
|
|
||||||
|
|
||||||
NewRelicAppName string
|
|
||||||
NewRelicKey string
|
|
||||||
|
|
||||||
PrometheusBind string
|
|
||||||
PrometheusNamespace string
|
|
||||||
|
|
||||||
BugsnagKey string
|
|
||||||
BugsnagStage string
|
|
||||||
HoneybadgerKey string
|
|
||||||
HoneybadgerEnv string
|
|
||||||
SentryDSN string
|
|
||||||
SentryEnvironment string
|
|
||||||
SentryRelease string
|
|
||||||
|
|
||||||
ReportDownloadingErrors bool
|
|
||||||
|
|
||||||
EnableDebugHeaders bool
|
|
||||||
|
|
||||||
FreeMemoryInterval int
|
|
||||||
DownloadBufferSize int
|
|
||||||
BufferPoolCalibrationThreshold int
|
|
||||||
}
|
|
||||||
|
|
||||||
var conf = config{
|
|
||||||
Network: "tcp",
|
|
||||||
Bind: ":8080",
|
|
||||||
ReadTimeout: 10,
|
|
||||||
WriteTimeout: 10,
|
|
||||||
KeepAliveTimeout: 10,
|
|
||||||
DownloadTimeout: 5,
|
|
||||||
Concurrency: runtime.NumCPU() * 2,
|
|
||||||
TTL: 3600,
|
|
||||||
MaxSrcResolution: 16800000,
|
|
||||||
MaxAnimationFrames: 1,
|
|
||||||
MaxSvgCheckBytes: 32 * 1024,
|
|
||||||
SignatureSize: 32,
|
|
||||||
PngQuantizationColors: 256,
|
|
||||||
Quality: 80,
|
|
||||||
FormatQuality: map[imageType]int{imageTypeAVIF: 50},
|
|
||||||
StripMetadata: true,
|
|
||||||
StripColorProfile: true,
|
|
||||||
AutoRotate: true,
|
|
||||||
UserAgent: fmt.Sprintf("imgproxy/%s", version),
|
|
||||||
Presets: make(presets),
|
|
||||||
WatermarkOpacity: 1,
|
|
||||||
FallbackImageHTTPCode: 200,
|
|
||||||
BugsnagStage: "production",
|
|
||||||
HoneybadgerEnv: "production",
|
|
||||||
SentryEnvironment: "production",
|
|
||||||
SentryRelease: fmt.Sprintf("imgproxy/%s", version),
|
|
||||||
ReportDownloadingErrors: true,
|
|
||||||
FreeMemoryInterval: 10,
|
|
||||||
BufferPoolCalibrationThreshold: 1024,
|
|
||||||
}
|
|
||||||
|
|
||||||
func configure() error {
|
|
||||||
keyPath := flag.String("keypath", "", "path of the file with hex-encoded key")
|
|
||||||
saltPath := flag.String("saltpath", "", "path of the file with hex-encoded salt")
|
|
||||||
presetsPath := flag.String("presets", "", "path of the file with presets")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if port := os.Getenv("PORT"); len(port) > 0 {
|
|
||||||
conf.Bind = fmt.Sprintf(":%s", port)
|
|
||||||
}
|
|
||||||
|
|
||||||
strEnvConfig(&conf.Network, "IMGPROXY_NETWORK")
|
|
||||||
strEnvConfig(&conf.Bind, "IMGPROXY_BIND")
|
|
||||||
intEnvConfig(&conf.ReadTimeout, "IMGPROXY_READ_TIMEOUT")
|
|
||||||
intEnvConfig(&conf.WriteTimeout, "IMGPROXY_WRITE_TIMEOUT")
|
|
||||||
intEnvConfig(&conf.KeepAliveTimeout, "IMGPROXY_KEEP_ALIVE_TIMEOUT")
|
|
||||||
intEnvConfig(&conf.DownloadTimeout, "IMGPROXY_DOWNLOAD_TIMEOUT")
|
|
||||||
intEnvConfig(&conf.Concurrency, "IMGPROXY_CONCURRENCY")
|
|
||||||
intEnvConfig(&conf.MaxClients, "IMGPROXY_MAX_CLIENTS")
|
|
||||||
|
|
||||||
intEnvConfig(&conf.TTL, "IMGPROXY_TTL")
|
|
||||||
boolEnvConfig(&conf.CacheControlPassthrough, "IMGPROXY_CACHE_CONTROL_PASSTHROUGH")
|
|
||||||
boolEnvConfig(&conf.SetCanonicalHeader, "IMGPROXY_SET_CANONICAL_HEADER")
|
|
||||||
|
|
||||||
boolEnvConfig(&conf.SoReuseport, "IMGPROXY_SO_REUSEPORT")
|
|
||||||
|
|
||||||
strEnvConfig(&conf.PathPrefix, "IMGPROXY_PATH_PREFIX")
|
|
||||||
|
|
||||||
megaIntEnvConfig(&conf.MaxSrcResolution, "IMGPROXY_MAX_SRC_RESOLUTION")
|
|
||||||
intEnvConfig(&conf.MaxSrcFileSize, "IMGPROXY_MAX_SRC_FILE_SIZE")
|
|
||||||
intEnvConfig(&conf.MaxSvgCheckBytes, "IMGPROXY_MAX_SVG_CHECK_BYTES")
|
|
||||||
|
|
||||||
intEnvConfig(&conf.MaxAnimationFrames, "IMGPROXY_MAX_ANIMATION_FRAMES")
|
|
||||||
|
|
||||||
strSliceEnvConfig(&conf.AllowedSources, "IMGPROXY_ALLOWED_SOURCES")
|
|
||||||
|
|
||||||
boolEnvConfig(&conf.JpegProgressive, "IMGPROXY_JPEG_PROGRESSIVE")
|
|
||||||
boolEnvConfig(&conf.PngInterlaced, "IMGPROXY_PNG_INTERLACED")
|
|
||||||
boolEnvConfig(&conf.PngQuantize, "IMGPROXY_PNG_QUANTIZE")
|
|
||||||
intEnvConfig(&conf.PngQuantizationColors, "IMGPROXY_PNG_QUANTIZATION_COLORS")
|
|
||||||
intEnvConfig(&conf.Quality, "IMGPROXY_QUALITY")
|
|
||||||
formatQualityEnvConfig(conf.FormatQuality, "IMGPROXY_FORMAT_QUALITY")
|
|
||||||
boolEnvConfig(&conf.StripMetadata, "IMGPROXY_STRIP_METADATA")
|
|
||||||
boolEnvConfig(&conf.StripColorProfile, "IMGPROXY_STRIP_COLOR_PROFILE")
|
|
||||||
boolEnvConfig(&conf.AutoRotate, "IMGPROXY_AUTO_ROTATE")
|
|
||||||
|
|
||||||
boolEnvConfig(&conf.EnableWebpDetection, "IMGPROXY_ENABLE_WEBP_DETECTION")
|
|
||||||
boolEnvConfig(&conf.EnforceWebp, "IMGPROXY_ENFORCE_WEBP")
|
|
||||||
boolEnvConfig(&conf.EnableAvifDetection, "IMGPROXY_ENABLE_AVIF_DETECTION")
|
|
||||||
boolEnvConfig(&conf.EnforceAvif, "IMGPROXY_ENFORCE_AVIF")
|
|
||||||
boolEnvConfig(&conf.EnableClientHints, "IMGPROXY_ENABLE_CLIENT_HINTS")
|
|
||||||
|
|
||||||
imageTypesEnvConfig(&conf.SkipProcessingFormats, "IMGPROXY_SKIP_PROCESSING_FORMATS")
|
|
||||||
|
|
||||||
boolEnvConfig(&conf.UseLinearColorspace, "IMGPROXY_USE_LINEAR_COLORSPACE")
|
|
||||||
boolEnvConfig(&conf.DisableShrinkOnLoad, "IMGPROXY_DISABLE_SHRINK_ON_LOAD")
|
|
||||||
|
|
||||||
if err := hexEnvConfig(&conf.Keys, "IMGPROXY_KEY"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := hexEnvConfig(&conf.Salts, "IMGPROXY_SALT"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
intEnvConfig(&conf.SignatureSize, "IMGPROXY_SIGNATURE_SIZE")
|
|
||||||
|
|
||||||
if err := hexFileConfig(&conf.Keys, *keyPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := hexFileConfig(&conf.Salts, *saltPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
strEnvConfig(&conf.Secret, "IMGPROXY_SECRET")
|
|
||||||
|
|
||||||
strEnvConfig(&conf.AllowOrigin, "IMGPROXY_ALLOW_ORIGIN")
|
|
||||||
|
|
||||||
strEnvConfig(&conf.UserAgent, "IMGPROXY_USER_AGENT")
|
|
||||||
|
|
||||||
boolEnvConfig(&conf.IgnoreSslVerification, "IMGPROXY_IGNORE_SSL_VERIFICATION")
|
|
||||||
boolEnvConfig(&conf.DevelopmentErrorsMode, "IMGPROXY_DEVELOPMENT_ERRORS_MODE")
|
|
||||||
|
|
||||||
strEnvConfig(&conf.LocalFileSystemRoot, "IMGPROXY_LOCAL_FILESYSTEM_ROOT")
|
|
||||||
|
|
||||||
boolEnvConfig(&conf.S3Enabled, "IMGPROXY_USE_S3")
|
|
||||||
strEnvConfig(&conf.S3Region, "IMGPROXY_S3_REGION")
|
|
||||||
strEnvConfig(&conf.S3Endpoint, "IMGPROXY_S3_ENDPOINT")
|
|
||||||
|
|
||||||
boolEnvConfig(&conf.GCSEnabled, "IMGPROXY_USE_GCS")
|
|
||||||
strEnvConfig(&conf.GCSKey, "IMGPROXY_GCS_KEY")
|
|
||||||
|
|
||||||
boolEnvConfig(&conf.ABSEnabled, "IMGPROXY_USE_ABS")
|
|
||||||
strEnvConfig(&conf.ABSName, "IMGPROXY_ABS_NAME")
|
|
||||||
strEnvConfig(&conf.ABSKey, "IMGPROXY_ABS_KEY")
|
|
||||||
strEnvConfig(&conf.ABSEndpoint, "IMGPROXY_ABS_ENDPOINT")
|
|
||||||
|
|
||||||
boolEnvConfig(&conf.ETagEnabled, "IMGPROXY_USE_ETAG")
|
|
||||||
|
|
||||||
strEnvConfig(&conf.BaseURL, "IMGPROXY_BASE_URL")
|
|
||||||
|
|
||||||
if err := presetEnvConfig(conf.Presets, "IMGPROXY_PRESETS"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := presetFileConfig(conf.Presets, *presetsPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
boolEnvConfig(&conf.OnlyPresets, "IMGPROXY_ONLY_PRESETS")
|
|
||||||
|
|
||||||
strEnvConfig(&conf.WatermarkData, "IMGPROXY_WATERMARK_DATA")
|
|
||||||
strEnvConfig(&conf.WatermarkPath, "IMGPROXY_WATERMARK_PATH")
|
|
||||||
strEnvConfig(&conf.WatermarkURL, "IMGPROXY_WATERMARK_URL")
|
|
||||||
floatEnvConfig(&conf.WatermarkOpacity, "IMGPROXY_WATERMARK_OPACITY")
|
|
||||||
|
|
||||||
strEnvConfig(&conf.FallbackImageData, "IMGPROXY_FALLBACK_IMAGE_DATA")
|
|
||||||
strEnvConfig(&conf.FallbackImagePath, "IMGPROXY_FALLBACK_IMAGE_PATH")
|
|
||||||
strEnvConfig(&conf.FallbackImageURL, "IMGPROXY_FALLBACK_IMAGE_URL")
|
|
||||||
intEnvConfig(&conf.FallbackImageHTTPCode, "IMGPROXY_FALLBACK_IMAGE_HTTP_CODE")
|
|
||||||
|
|
||||||
boolEnvConfig(&conf.DataDogEnable, "IMGPROXY_DATADOG_ENABLE")
|
|
||||||
|
|
||||||
strEnvConfig(&conf.NewRelicAppName, "IMGPROXY_NEW_RELIC_APP_NAME")
|
|
||||||
strEnvConfig(&conf.NewRelicKey, "IMGPROXY_NEW_RELIC_KEY")
|
|
||||||
|
|
||||||
strEnvConfig(&conf.PrometheusBind, "IMGPROXY_PROMETHEUS_BIND")
|
|
||||||
strEnvConfig(&conf.PrometheusNamespace, "IMGPROXY_PROMETHEUS_NAMESPACE")
|
|
||||||
|
|
||||||
strEnvConfig(&conf.BugsnagKey, "IMGPROXY_BUGSNAG_KEY")
|
|
||||||
strEnvConfig(&conf.BugsnagStage, "IMGPROXY_BUGSNAG_STAGE")
|
|
||||||
strEnvConfig(&conf.HoneybadgerKey, "IMGPROXY_HONEYBADGER_KEY")
|
|
||||||
strEnvConfig(&conf.HoneybadgerEnv, "IMGPROXY_HONEYBADGER_ENV")
|
|
||||||
strEnvConfig(&conf.SentryDSN, "IMGPROXY_SENTRY_DSN")
|
|
||||||
strEnvConfig(&conf.SentryEnvironment, "IMGPROXY_SENTRY_ENVIRONMENT")
|
|
||||||
strEnvConfig(&conf.SentryRelease, "IMGPROXY_SENTRY_RELEASE")
|
|
||||||
boolEnvConfig(&conf.ReportDownloadingErrors, "IMGPROXY_REPORT_DOWNLOADING_ERRORS")
|
|
||||||
boolEnvConfig(&conf.EnableDebugHeaders, "IMGPROXY_ENABLE_DEBUG_HEADERS")
|
|
||||||
|
|
||||||
intEnvConfig(&conf.FreeMemoryInterval, "IMGPROXY_FREE_MEMORY_INTERVAL")
|
|
||||||
intEnvConfig(&conf.DownloadBufferSize, "IMGPROXY_DOWNLOAD_BUFFER_SIZE")
|
|
||||||
intEnvConfig(&conf.BufferPoolCalibrationThreshold, "IMGPROXY_BUFFER_POOL_CALIBRATION_THRESHOLD")
|
|
||||||
|
|
||||||
if len(conf.Keys) != len(conf.Salts) {
|
|
||||||
return fmt.Errorf("Number of keys and number of salts should be equal. Keys: %d, salts: %d", len(conf.Keys), len(conf.Salts))
|
|
||||||
}
|
|
||||||
if len(conf.Keys) == 0 {
|
|
||||||
logWarning("No keys defined, so signature checking is disabled")
|
|
||||||
conf.AllowInsecure = true
|
|
||||||
}
|
|
||||||
if len(conf.Salts) == 0 {
|
|
||||||
logWarning("No salts defined, so signature checking is disabled")
|
|
||||||
conf.AllowInsecure = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.SignatureSize < 1 || conf.SignatureSize > 32 {
|
|
||||||
return fmt.Errorf("Signature size should be within 1 and 32, now - %d\n", conf.SignatureSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.Bind) == 0 {
|
|
||||||
return fmt.Errorf("Bind address is not defined")
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.ReadTimeout <= 0 {
|
|
||||||
return fmt.Errorf("Read timeout should be greater than 0, now - %d\n", conf.ReadTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.WriteTimeout <= 0 {
|
|
||||||
return fmt.Errorf("Write timeout should be greater than 0, now - %d\n", conf.WriteTimeout)
|
|
||||||
}
|
|
||||||
if conf.KeepAliveTimeout < 0 {
|
|
||||||
return fmt.Errorf("KeepAlive timeout should be greater than or equal to 0, now - %d\n", conf.KeepAliveTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.DownloadTimeout <= 0 {
|
|
||||||
return fmt.Errorf("Download timeout should be greater than 0, now - %d\n", conf.DownloadTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.Concurrency <= 0 {
|
|
||||||
return fmt.Errorf("Concurrency should be greater than 0, now - %d\n", conf.Concurrency)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.MaxClients <= 0 {
|
|
||||||
conf.MaxClients = conf.Concurrency * 10
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.TTL <= 0 {
|
|
||||||
return fmt.Errorf("TTL should be greater than 0, now - %d\n", conf.TTL)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.MaxSrcResolution <= 0 {
|
|
||||||
return fmt.Errorf("Max src resolution should be greater than 0, now - %d\n", conf.MaxSrcResolution)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.MaxSrcFileSize < 0 {
|
|
||||||
return fmt.Errorf("Max src file size should be greater than or equal to 0, now - %d\n", conf.MaxSrcFileSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.MaxAnimationFrames <= 0 {
|
|
||||||
return fmt.Errorf("Max animation frames should be greater than 0, now - %d\n", conf.MaxAnimationFrames)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.PngQuantizationColors < 2 {
|
|
||||||
return fmt.Errorf("Png quantization colors should be greater than 1, now - %d\n", conf.PngQuantizationColors)
|
|
||||||
} else if conf.PngQuantizationColors > 256 {
|
|
||||||
return fmt.Errorf("Png quantization colors can't be greater than 256, now - %d\n", conf.PngQuantizationColors)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.Quality <= 0 {
|
|
||||||
return fmt.Errorf("Quality should be greater than 0, now - %d\n", conf.Quality)
|
|
||||||
} else if conf.Quality > 100 {
|
|
||||||
return fmt.Errorf("Quality can't be greater than 100, now - %d\n", conf.Quality)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.IgnoreSslVerification {
|
|
||||||
logWarning("Ignoring SSL verification is very unsafe")
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.LocalFileSystemRoot != "" {
|
|
||||||
stat, err := os.Stat(conf.LocalFileSystemRoot)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Cannot use local directory: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !stat.IsDir() {
|
|
||||||
return fmt.Errorf("Cannot use local directory: not a directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.LocalFileSystemRoot == "/" {
|
|
||||||
logWarning("Exposing root via IMGPROXY_LOCAL_FILESYSTEM_ROOT is unsafe")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := os.LookupEnv("IMGPROXY_USE_GCS"); !ok && len(conf.GCSKey) > 0 {
|
|
||||||
logWarning("Set IMGPROXY_USE_GCS to true since it may be required by future versions to enable GCS support")
|
|
||||||
conf.GCSEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.WatermarkOpacity <= 0 {
|
|
||||||
return fmt.Errorf("Watermark opacity should be greater than 0")
|
|
||||||
} else if conf.WatermarkOpacity > 1 {
|
|
||||||
return fmt.Errorf("Watermark opacity should be less than or equal to 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.FallbackImageHTTPCode < 100 || conf.FallbackImageHTTPCode > 599 {
|
|
||||||
return fmt.Errorf("Fallback image HTTP code should be between 100 and 599")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.PrometheusBind) > 0 && conf.PrometheusBind == conf.Bind {
|
|
||||||
return fmt.Errorf("Can't use the same binding for the main server and Prometheus")
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.FreeMemoryInterval <= 0 {
|
|
||||||
return fmt.Errorf("Free memory interval should be greater than zero")
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.DownloadBufferSize < 0 {
|
|
||||||
return fmt.Errorf("Download buffer size should be greater than or equal to 0")
|
|
||||||
} else if conf.DownloadBufferSize > math.MaxInt32 {
|
|
||||||
return fmt.Errorf("Download buffer size can't be greater than %d", math.MaxInt32)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.BufferPoolCalibrationThreshold < 64 {
|
|
||||||
return fmt.Errorf("Buffer pool calibration threshold should be greater than or equal to 64")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
510
config/config.go
Normal file
510
config/config.go
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config/configurators"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Network string
|
||||||
|
Bind string
|
||||||
|
ReadTimeout int
|
||||||
|
WriteTimeout int
|
||||||
|
KeepAliveTimeout int
|
||||||
|
DownloadTimeout int
|
||||||
|
Concurrency int
|
||||||
|
MaxClients int
|
||||||
|
|
||||||
|
TTL int
|
||||||
|
CacheControlPassthrough bool
|
||||||
|
SetCanonicalHeader bool
|
||||||
|
|
||||||
|
SoReuseport bool
|
||||||
|
|
||||||
|
PathPrefix string
|
||||||
|
|
||||||
|
MaxSrcResolution int
|
||||||
|
MaxSrcFileSize int
|
||||||
|
MaxAnimationFrames int
|
||||||
|
MaxSvgCheckBytes int
|
||||||
|
|
||||||
|
JpegProgressive bool
|
||||||
|
PngInterlaced bool
|
||||||
|
PngQuantize bool
|
||||||
|
PngQuantizationColors int
|
||||||
|
Quality int
|
||||||
|
FormatQuality map[imagetype.Type]int
|
||||||
|
StripMetadata bool
|
||||||
|
StripColorProfile bool
|
||||||
|
AutoRotate bool
|
||||||
|
|
||||||
|
EnableWebpDetection bool
|
||||||
|
EnforceWebp bool
|
||||||
|
EnableAvifDetection bool
|
||||||
|
EnforceAvif bool
|
||||||
|
EnableClientHints bool
|
||||||
|
|
||||||
|
SkipProcessingFormats []imagetype.Type
|
||||||
|
|
||||||
|
UseLinearColorspace bool
|
||||||
|
DisableShrinkOnLoad bool
|
||||||
|
|
||||||
|
Keys [][]byte
|
||||||
|
Salts [][]byte
|
||||||
|
SignatureSize int
|
||||||
|
|
||||||
|
Secret string
|
||||||
|
|
||||||
|
AllowOrigin string
|
||||||
|
|
||||||
|
UserAgent string
|
||||||
|
|
||||||
|
IgnoreSslVerification bool
|
||||||
|
DevelopmentErrorsMode bool
|
||||||
|
|
||||||
|
AllowedSources []string
|
||||||
|
LocalFileSystemRoot string
|
||||||
|
S3Enabled bool
|
||||||
|
S3Region string
|
||||||
|
S3Endpoint string
|
||||||
|
GCSEnabled bool
|
||||||
|
GCSKey string
|
||||||
|
ABSEnabled bool
|
||||||
|
ABSName string
|
||||||
|
ABSKey string
|
||||||
|
ABSEndpoint string
|
||||||
|
|
||||||
|
ETagEnabled bool
|
||||||
|
|
||||||
|
BaseURL string
|
||||||
|
|
||||||
|
Presets []string
|
||||||
|
OnlyPresets bool
|
||||||
|
|
||||||
|
WatermarkData string
|
||||||
|
WatermarkPath string
|
||||||
|
WatermarkURL string
|
||||||
|
WatermarkOpacity float64
|
||||||
|
|
||||||
|
FallbackImageData string
|
||||||
|
FallbackImagePath string
|
||||||
|
FallbackImageURL string
|
||||||
|
FallbackImageHTTPCode int
|
||||||
|
|
||||||
|
DataDogEnable bool
|
||||||
|
|
||||||
|
NewRelicAppName string
|
||||||
|
NewRelicKey string
|
||||||
|
|
||||||
|
PrometheusBind string
|
||||||
|
PrometheusNamespace string
|
||||||
|
|
||||||
|
BugsnagKey string
|
||||||
|
BugsnagStage string
|
||||||
|
|
||||||
|
HoneybadgerKey string
|
||||||
|
HoneybadgerEnv string
|
||||||
|
|
||||||
|
SentryDSN string
|
||||||
|
SentryEnvironment string
|
||||||
|
SentryRelease string
|
||||||
|
|
||||||
|
ReportDownloadingErrors bool
|
||||||
|
|
||||||
|
EnableDebugHeaders bool
|
||||||
|
|
||||||
|
FreeMemoryInterval int
|
||||||
|
DownloadBufferSize int
|
||||||
|
BufferPoolCalibrationThreshold int
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reset() {
|
||||||
|
Network = "tcp"
|
||||||
|
Bind = ":8080"
|
||||||
|
ReadTimeout = 10
|
||||||
|
WriteTimeout = 10
|
||||||
|
KeepAliveTimeout = 10
|
||||||
|
DownloadTimeout = 5
|
||||||
|
Concurrency = runtime.NumCPU() * 2
|
||||||
|
MaxClients = 0
|
||||||
|
|
||||||
|
TTL = 3600
|
||||||
|
CacheControlPassthrough = false
|
||||||
|
SetCanonicalHeader = false
|
||||||
|
|
||||||
|
SoReuseport = false
|
||||||
|
|
||||||
|
PathPrefix = ""
|
||||||
|
|
||||||
|
MaxSrcResolution = 16800000
|
||||||
|
MaxSrcFileSize = 0
|
||||||
|
MaxAnimationFrames = 1
|
||||||
|
MaxSvgCheckBytes = 32 * 1024
|
||||||
|
|
||||||
|
JpegProgressive = false
|
||||||
|
PngInterlaced = false
|
||||||
|
PngQuantize = false
|
||||||
|
PngQuantizationColors = 256
|
||||||
|
Quality = 80
|
||||||
|
FormatQuality = map[imagetype.Type]int{imagetype.AVIF: 50}
|
||||||
|
StripMetadata = true
|
||||||
|
StripColorProfile = true
|
||||||
|
AutoRotate = true
|
||||||
|
|
||||||
|
EnableWebpDetection = false
|
||||||
|
EnforceWebp = false
|
||||||
|
EnableAvifDetection = false
|
||||||
|
EnforceAvif = false
|
||||||
|
EnableClientHints = false
|
||||||
|
|
||||||
|
SkipProcessingFormats = make([]imagetype.Type, 0)
|
||||||
|
|
||||||
|
UseLinearColorspace = false
|
||||||
|
DisableShrinkOnLoad = false
|
||||||
|
|
||||||
|
Keys = make([][]byte, 0)
|
||||||
|
Salts = make([][]byte, 0)
|
||||||
|
SignatureSize = 32
|
||||||
|
|
||||||
|
Secret = ""
|
||||||
|
|
||||||
|
AllowOrigin = ""
|
||||||
|
|
||||||
|
UserAgent = fmt.Sprintf("imgproxy/%s", version.Version())
|
||||||
|
|
||||||
|
IgnoreSslVerification = false
|
||||||
|
DevelopmentErrorsMode = false
|
||||||
|
|
||||||
|
AllowedSources = make([]string, 0)
|
||||||
|
LocalFileSystemRoot = ""
|
||||||
|
S3Enabled = false
|
||||||
|
S3Region = ""
|
||||||
|
S3Endpoint = ""
|
||||||
|
GCSEnabled = false
|
||||||
|
GCSKey = ""
|
||||||
|
ABSEnabled = false
|
||||||
|
ABSName = ""
|
||||||
|
ABSKey = ""
|
||||||
|
ABSEndpoint = ""
|
||||||
|
|
||||||
|
ETagEnabled = false
|
||||||
|
|
||||||
|
BaseURL = ""
|
||||||
|
|
||||||
|
Presets = make([]string, 0)
|
||||||
|
OnlyPresets = false
|
||||||
|
|
||||||
|
WatermarkData = ""
|
||||||
|
WatermarkPath = ""
|
||||||
|
WatermarkURL = ""
|
||||||
|
WatermarkOpacity = 1
|
||||||
|
|
||||||
|
FallbackImageData = ""
|
||||||
|
FallbackImagePath = ""
|
||||||
|
FallbackImageURL = ""
|
||||||
|
FallbackImageHTTPCode = 200
|
||||||
|
|
||||||
|
DataDogEnable = false
|
||||||
|
|
||||||
|
NewRelicAppName = ""
|
||||||
|
NewRelicKey = ""
|
||||||
|
|
||||||
|
PrometheusBind = ""
|
||||||
|
PrometheusNamespace = ""
|
||||||
|
|
||||||
|
BugsnagKey = ""
|
||||||
|
BugsnagStage = "production"
|
||||||
|
|
||||||
|
HoneybadgerKey = ""
|
||||||
|
HoneybadgerEnv = "production"
|
||||||
|
|
||||||
|
SentryDSN = ""
|
||||||
|
SentryEnvironment = "production"
|
||||||
|
SentryRelease = fmt.Sprintf("imgproxy/%s", version.Version())
|
||||||
|
|
||||||
|
ReportDownloadingErrors = true
|
||||||
|
|
||||||
|
EnableDebugHeaders = false
|
||||||
|
|
||||||
|
FreeMemoryInterval = 10
|
||||||
|
DownloadBufferSize = 0
|
||||||
|
BufferPoolCalibrationThreshold = 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
func Configure() error {
|
||||||
|
keyPath := flag.String("keypath", "", "path of the file with hex-encoded key")
|
||||||
|
saltPath := flag.String("saltpath", "", "path of the file with hex-encoded salt")
|
||||||
|
presetsPath := flag.String("presets", "", "path of the file with presets")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if port := os.Getenv("PORT"); len(port) > 0 {
|
||||||
|
Bind = fmt.Sprintf(":%s", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
configurators.String(&Network, "IMGPROXY_NETWORK")
|
||||||
|
configurators.String(&Bind, "IMGPROXY_BIND")
|
||||||
|
configurators.Int(&ReadTimeout, "IMGPROXY_READ_TIMEOUT")
|
||||||
|
configurators.Int(&WriteTimeout, "IMGPROXY_WRITE_TIMEOUT")
|
||||||
|
configurators.Int(&KeepAliveTimeout, "IMGPROXY_KEEP_ALIVE_TIMEOUT")
|
||||||
|
configurators.Int(&DownloadTimeout, "IMGPROXY_DOWNLOAD_TIMEOUT")
|
||||||
|
configurators.Int(&Concurrency, "IMGPROXY_CONCURRENCY")
|
||||||
|
configurators.Int(&MaxClients, "IMGPROXY_MAX_CLIENTS")
|
||||||
|
|
||||||
|
configurators.Int(&TTL, "IMGPROXY_TTL")
|
||||||
|
configurators.Bool(&CacheControlPassthrough, "IMGPROXY_CACHE_CONTROL_PASSTHROUGH")
|
||||||
|
configurators.Bool(&SetCanonicalHeader, "IMGPROXY_SET_CANONICAL_HEADER")
|
||||||
|
|
||||||
|
configurators.Bool(&SoReuseport, "IMGPROXY_SO_REUSEPORT")
|
||||||
|
|
||||||
|
configurators.String(&PathPrefix, "IMGPROXY_PATH_PREFIX")
|
||||||
|
|
||||||
|
configurators.MegaInt(&MaxSrcResolution, "IMGPROXY_MAX_SRC_RESOLUTION")
|
||||||
|
configurators.Int(&MaxSrcFileSize, "IMGPROXY_MAX_SRC_FILE_SIZE")
|
||||||
|
configurators.Int(&MaxSvgCheckBytes, "IMGPROXY_MAX_SVG_CHECK_BYTES")
|
||||||
|
|
||||||
|
configurators.Int(&MaxAnimationFrames, "IMGPROXY_MAX_ANIMATION_FRAMES")
|
||||||
|
|
||||||
|
configurators.StringSlice(&AllowedSources, "IMGPROXY_ALLOWED_SOURCES")
|
||||||
|
|
||||||
|
configurators.Bool(&JpegProgressive, "IMGPROXY_JPEG_PROGRESSIVE")
|
||||||
|
configurators.Bool(&PngInterlaced, "IMGPROXY_PNG_INTERLACED")
|
||||||
|
configurators.Bool(&PngQuantize, "IMGPROXY_PNG_QUANTIZE")
|
||||||
|
configurators.Int(&PngQuantizationColors, "IMGPROXY_PNG_QUANTIZATION_COLORS")
|
||||||
|
configurators.Int(&Quality, "IMGPROXY_QUALITY")
|
||||||
|
if err := configurators.ImageTypesQuality(FormatQuality, "IMGPROXY_FORMAT_QUALITY"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configurators.Bool(&StripMetadata, "IMGPROXY_STRIP_METADATA")
|
||||||
|
configurators.Bool(&StripColorProfile, "IMGPROXY_STRIP_COLOR_PROFILE")
|
||||||
|
configurators.Bool(&AutoRotate, "IMGPROXY_AUTO_ROTATE")
|
||||||
|
|
||||||
|
configurators.Bool(&EnableWebpDetection, "IMGPROXY_ENABLE_WEBP_DETECTION")
|
||||||
|
configurators.Bool(&EnforceWebp, "IMGPROXY_ENFORCE_WEBP")
|
||||||
|
configurators.Bool(&EnableAvifDetection, "IMGPROXY_ENABLE_AVIF_DETECTION")
|
||||||
|
configurators.Bool(&EnforceAvif, "IMGPROXY_ENFORCE_AVIF")
|
||||||
|
configurators.Bool(&EnableClientHints, "IMGPROXY_ENABLE_CLIENT_HINTS")
|
||||||
|
|
||||||
|
if err := configurators.ImageTypes(&SkipProcessingFormats, "IMGPROXY_SKIP_PROCESSING_FORMATS"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configurators.Bool(&UseLinearColorspace, "IMGPROXY_USE_LINEAR_COLORSPACE")
|
||||||
|
configurators.Bool(&DisableShrinkOnLoad, "IMGPROXY_DISABLE_SHRINK_ON_LOAD")
|
||||||
|
|
||||||
|
if err := configurators.Hex(&Keys, "IMGPROXY_KEY"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := configurators.Hex(&Salts, "IMGPROXY_SALT"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configurators.Int(&SignatureSize, "IMGPROXY_SIGNATURE_SIZE")
|
||||||
|
|
||||||
|
if err := configurators.HexFile(&Keys, *keyPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := configurators.HexFile(&Salts, *saltPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configurators.String(&Secret, "IMGPROXY_SECRET")
|
||||||
|
|
||||||
|
configurators.String(&AllowOrigin, "IMGPROXY_ALLOW_ORIGIN")
|
||||||
|
|
||||||
|
configurators.String(&UserAgent, "IMGPROXY_USER_AGENT")
|
||||||
|
|
||||||
|
configurators.Bool(&IgnoreSslVerification, "IMGPROXY_IGNORE_SSL_VERIFICATION")
|
||||||
|
configurators.Bool(&DevelopmentErrorsMode, "IMGPROXY_DEVELOPMENT_ERRORS_MODE")
|
||||||
|
|
||||||
|
configurators.String(&LocalFileSystemRoot, "IMGPROXY_LOCAL_FILESYSTEM_ROOT")
|
||||||
|
|
||||||
|
configurators.Bool(&S3Enabled, "IMGPROXY_USE_S3")
|
||||||
|
configurators.String(&S3Region, "IMGPROXY_S3_REGION")
|
||||||
|
configurators.String(&S3Endpoint, "IMGPROXY_S3_ENDPOINT")
|
||||||
|
|
||||||
|
configurators.Bool(&GCSEnabled, "IMGPROXY_USE_GCS")
|
||||||
|
configurators.String(&GCSKey, "IMGPROXY_GCS_KEY")
|
||||||
|
|
||||||
|
configurators.Bool(&ABSEnabled, "IMGPROXY_USE_ABS")
|
||||||
|
configurators.String(&ABSName, "IMGPROXY_ABS_NAME")
|
||||||
|
configurators.String(&ABSKey, "IMGPROXY_ABS_KEY")
|
||||||
|
configurators.String(&ABSEndpoint, "IMGPROXY_ABS_ENDPOINT")
|
||||||
|
|
||||||
|
configurators.Bool(&ETagEnabled, "IMGPROXY_USE_ETAG")
|
||||||
|
|
||||||
|
configurators.String(&BaseURL, "IMGPROXY_BASE_URL")
|
||||||
|
|
||||||
|
configurators.StringSlice(&Presets, "IMGPROXY_PRESETS")
|
||||||
|
if err := configurators.StringSliceFile(&Presets, *presetsPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configurators.Bool(&OnlyPresets, "IMGPROXY_ONLY_PRESETS")
|
||||||
|
|
||||||
|
configurators.String(&WatermarkData, "IMGPROXY_WATERMARK_DATA")
|
||||||
|
configurators.String(&WatermarkPath, "IMGPROXY_WATERMARK_PATH")
|
||||||
|
configurators.String(&WatermarkURL, "IMGPROXY_WATERMARK_URL")
|
||||||
|
configurators.Float(&WatermarkOpacity, "IMGPROXY_WATERMARK_OPACITY")
|
||||||
|
|
||||||
|
configurators.String(&FallbackImageData, "IMGPROXY_FALLBACK_IMAGE_DATA")
|
||||||
|
configurators.String(&FallbackImagePath, "IMGPROXY_FALLBACK_IMAGE_PATH")
|
||||||
|
configurators.String(&FallbackImageURL, "IMGPROXY_FALLBACK_IMAGE_URL")
|
||||||
|
configurators.Int(&FallbackImageHTTPCode, "IMGPROXY_FALLBACK_IMAGE_HTTP_CODE")
|
||||||
|
|
||||||
|
configurators.Bool(&DataDogEnable, "IMGPROXY_DATADOG_ENABLE")
|
||||||
|
|
||||||
|
configurators.String(&NewRelicAppName, "IMGPROXY_NEW_RELIC_APP_NAME")
|
||||||
|
configurators.String(&NewRelicKey, "IMGPROXY_NEW_RELIC_KEY")
|
||||||
|
|
||||||
|
configurators.String(&PrometheusBind, "IMGPROXY_PROMETHEUS_BIND")
|
||||||
|
configurators.String(&PrometheusNamespace, "IMGPROXY_PROMETHEUS_NAMESPACE")
|
||||||
|
|
||||||
|
configurators.String(&BugsnagKey, "IMGPROXY_BUGSNAG_KEY")
|
||||||
|
configurators.String(&BugsnagStage, "IMGPROXY_BUGSNAG_STAGE")
|
||||||
|
configurators.String(&HoneybadgerKey, "IMGPROXY_HONEYBADGER_KEY")
|
||||||
|
configurators.String(&HoneybadgerEnv, "IMGPROXY_HONEYBADGER_ENV")
|
||||||
|
configurators.String(&SentryDSN, "IMGPROXY_SENTRY_DSN")
|
||||||
|
configurators.String(&SentryEnvironment, "IMGPROXY_SENTRY_ENVIRONMENT")
|
||||||
|
configurators.String(&SentryRelease, "IMGPROXY_SENTRY_RELEASE")
|
||||||
|
configurators.Bool(&ReportDownloadingErrors, "IMGPROXY_REPORT_DOWNLOADING_ERRORS")
|
||||||
|
configurators.Bool(&EnableDebugHeaders, "IMGPROXY_ENABLE_DEBUG_HEADERS")
|
||||||
|
|
||||||
|
configurators.Int(&FreeMemoryInterval, "IMGPROXY_FREE_MEMORY_INTERVAL")
|
||||||
|
configurators.Int(&DownloadBufferSize, "IMGPROXY_DOWNLOAD_BUFFER_SIZE")
|
||||||
|
configurators.Int(&BufferPoolCalibrationThreshold, "IMGPROXY_BUFFER_POOL_CALIBRATION_THRESHOLD")
|
||||||
|
|
||||||
|
if len(Keys) != len(Salts) {
|
||||||
|
return fmt.Errorf("Number of keys and number of salts should be equal. Keys: %d, salts: %d", len(Keys), len(Salts))
|
||||||
|
}
|
||||||
|
if len(Keys) == 0 {
|
||||||
|
log.Warning("No keys defined, so signature checking is disabled")
|
||||||
|
}
|
||||||
|
if len(Salts) == 0 {
|
||||||
|
log.Warning("No salts defined, so signature checking is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if SignatureSize < 1 || SignatureSize > 32 {
|
||||||
|
return fmt.Errorf("Signature size should be within 1 and 32, now - %d\n", SignatureSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(Bind) == 0 {
|
||||||
|
return fmt.Errorf("Bind address is not defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ReadTimeout <= 0 {
|
||||||
|
return fmt.Errorf("Read timeout should be greater than 0, now - %d\n", ReadTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if WriteTimeout <= 0 {
|
||||||
|
return fmt.Errorf("Write timeout should be greater than 0, now - %d\n", WriteTimeout)
|
||||||
|
}
|
||||||
|
if KeepAliveTimeout < 0 {
|
||||||
|
return fmt.Errorf("KeepAlive timeout should be greater than or equal to 0, now - %d\n", KeepAliveTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if DownloadTimeout <= 0 {
|
||||||
|
return fmt.Errorf("Download timeout should be greater than 0, now - %d\n", DownloadTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if Concurrency <= 0 {
|
||||||
|
return fmt.Errorf("Concurrency should be greater than 0, now - %d\n", Concurrency)
|
||||||
|
}
|
||||||
|
|
||||||
|
if MaxClients <= 0 {
|
||||||
|
MaxClients = Concurrency * 10
|
||||||
|
}
|
||||||
|
|
||||||
|
if TTL <= 0 {
|
||||||
|
return fmt.Errorf("TTL should be greater than 0, now - %d\n", TTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if MaxSrcResolution <= 0 {
|
||||||
|
return fmt.Errorf("Max src resolution should be greater than 0, now - %d\n", MaxSrcResolution)
|
||||||
|
}
|
||||||
|
|
||||||
|
if MaxSrcFileSize < 0 {
|
||||||
|
return fmt.Errorf("Max src file size should be greater than or equal to 0, now - %d\n", MaxSrcFileSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if MaxAnimationFrames <= 0 {
|
||||||
|
return fmt.Errorf("Max animation frames should be greater than 0, now - %d\n", MaxAnimationFrames)
|
||||||
|
}
|
||||||
|
|
||||||
|
if PngQuantizationColors < 2 {
|
||||||
|
return fmt.Errorf("Png quantization colors should be greater than 1, now - %d\n", PngQuantizationColors)
|
||||||
|
} else if PngQuantizationColors > 256 {
|
||||||
|
return fmt.Errorf("Png quantization colors can't be greater than 256, now - %d\n", PngQuantizationColors)
|
||||||
|
}
|
||||||
|
|
||||||
|
if Quality <= 0 {
|
||||||
|
return fmt.Errorf("Quality should be greater than 0, now - %d\n", Quality)
|
||||||
|
} else if Quality > 100 {
|
||||||
|
return fmt.Errorf("Quality can't be greater than 100, now - %d\n", Quality)
|
||||||
|
}
|
||||||
|
|
||||||
|
if IgnoreSslVerification {
|
||||||
|
log.Warning("Ignoring SSL verification is very unsafe")
|
||||||
|
}
|
||||||
|
|
||||||
|
if LocalFileSystemRoot != "" {
|
||||||
|
stat, err := os.Stat(LocalFileSystemRoot)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Cannot use local directory: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stat.IsDir() {
|
||||||
|
return fmt.Errorf("Cannot use local directory: not a directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
if LocalFileSystemRoot == "/" {
|
||||||
|
log.Warning("Exposing root via IMGPROXY_LOCAL_FILESYSTEM_ROOT is unsafe")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := os.LookupEnv("IMGPROXY_USE_GCS"); !ok && len(GCSKey) > 0 {
|
||||||
|
log.Warning("Set IMGPROXY_USE_GCS to true since it may be required by future versions to enable GCS support")
|
||||||
|
GCSEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if WatermarkOpacity <= 0 {
|
||||||
|
return fmt.Errorf("Watermark opacity should be greater than 0")
|
||||||
|
} else if WatermarkOpacity > 1 {
|
||||||
|
return fmt.Errorf("Watermark opacity should be less than or equal to 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
if FallbackImageHTTPCode < 100 || FallbackImageHTTPCode > 599 {
|
||||||
|
return fmt.Errorf("Fallback image HTTP code should be between 100 and 599")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(PrometheusBind) > 0 && PrometheusBind == Bind {
|
||||||
|
return fmt.Errorf("Can't use the same binding for the main server and Prometheus")
|
||||||
|
}
|
||||||
|
|
||||||
|
if FreeMemoryInterval <= 0 {
|
||||||
|
return fmt.Errorf("Free memory interval should be greater than zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
if DownloadBufferSize < 0 {
|
||||||
|
return fmt.Errorf("Download buffer size should be greater than or equal to 0")
|
||||||
|
} else if DownloadBufferSize > math.MaxInt32 {
|
||||||
|
return fmt.Errorf("Download buffer size can't be greater than %d", math.MaxInt32)
|
||||||
|
}
|
||||||
|
|
||||||
|
if BufferPoolCalibrationThreshold < 64 {
|
||||||
|
return fmt.Errorf("Buffer pool calibration threshold should be greater than or equal to 64")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
186
config/configurators/configurators.go
Normal file
186
config/configurators/configurators.go
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
package configurators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Int(i *int, name string) {
|
||||||
|
if env, err := strconv.Atoi(os.Getenv(name)); err == nil {
|
||||||
|
*i = env
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Float(i *float64, name string) {
|
||||||
|
if env, err := strconv.ParseFloat(os.Getenv(name), 64); err == nil {
|
||||||
|
*i = env
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MegaInt(f *int, name string) {
|
||||||
|
if env, err := strconv.ParseFloat(os.Getenv(name), 64); err == nil {
|
||||||
|
*f = int(env * 1000000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func String(s *string, name string) {
|
||||||
|
if env := os.Getenv(name); len(env) > 0 {
|
||||||
|
*s = env
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringSlice(s *[]string, name string) {
|
||||||
|
if env := os.Getenv(name); len(env) > 0 {
|
||||||
|
parts := strings.Split(env, ",")
|
||||||
|
|
||||||
|
for i, p := range parts {
|
||||||
|
parts[i] = strings.TrimSpace(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
*s = parts
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
*s = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringSliceFile(s *[]string, filepath string) error {
|
||||||
|
if len(filepath) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Can't open file %s\n", filepath)
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
if str := scanner.Text(); len(str) != 0 && !strings.HasPrefix(str, "#") {
|
||||||
|
*s = append(*s, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return fmt.Errorf("Failed to read presets file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Bool(b *bool, name string) {
|
||||||
|
if env, err := strconv.ParseBool(os.Getenv(name)); err == nil {
|
||||||
|
*b = env
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ImageTypes(it *[]imagetype.Type, name string) error {
|
||||||
|
*it = []imagetype.Type{}
|
||||||
|
|
||||||
|
if env := os.Getenv(name); len(env) > 0 {
|
||||||
|
parts := strings.Split(env, ",")
|
||||||
|
|
||||||
|
for _, p := range parts {
|
||||||
|
pt := strings.TrimSpace(p)
|
||||||
|
if t, ok := imagetype.Types[pt]; ok {
|
||||||
|
*it = append(*it, t)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("Unknown image format to skip: %s", pt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ImageTypesQuality(m map[imagetype.Type]int, name string) error {
|
||||||
|
if env := os.Getenv(name); len(env) > 0 {
|
||||||
|
parts := strings.Split(env, ",")
|
||||||
|
|
||||||
|
for _, p := range parts {
|
||||||
|
i := strings.Index(p, "=")
|
||||||
|
if i < 0 {
|
||||||
|
return fmt.Errorf("Invalid format quality string: %s", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
imgtypeStr, qStr := strings.TrimSpace(p[:i]), strings.TrimSpace(p[i+1:])
|
||||||
|
|
||||||
|
imgtype, ok := imagetype.Types[imgtypeStr]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Invalid format: %s", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
q, err := strconv.Atoi(qStr)
|
||||||
|
if err != nil || q <= 0 || q > 100 {
|
||||||
|
return fmt.Errorf("Invalid quality: %s", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
m[imgtype] = q
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hex(b *[][]byte, name string) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if env := os.Getenv(name); len(env) > 0 {
|
||||||
|
parts := strings.Split(env, ",")
|
||||||
|
|
||||||
|
keys := make([][]byte, len(parts))
|
||||||
|
|
||||||
|
for i, part := range parts {
|
||||||
|
if keys[i], err = hex.DecodeString(part); err != nil {
|
||||||
|
return fmt.Errorf("%s expected to be hex-encoded strings. Invalid: %s\n", name, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*b = keys
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func HexFile(b *[][]byte, filepath string) error {
|
||||||
|
if len(filepath) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Can't open file %s\n", filepath)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := [][]byte{}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
part := scanner.Text()
|
||||||
|
|
||||||
|
if len(part) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if key, err := hex.DecodeString(part); err == nil {
|
||||||
|
keys = append(keys, key)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("%s expected to contain hex-encoded strings. Invalid: %s\n", filepath, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return fmt.Errorf("Failed to read file %s: %s", filepath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*b = keys
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
41
crypt.go
41
crypt.go
@@ -1,41 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errInvalidSignature = errors.New("Invalid signature")
|
|
||||||
errInvalidSignatureEncoding = errors.New("Invalid signature encoding")
|
|
||||||
)
|
|
||||||
|
|
||||||
type securityKey []byte
|
|
||||||
|
|
||||||
func validatePath(signature, path string) error {
|
|
||||||
messageMAC, err := base64.RawURLEncoding.DecodeString(signature)
|
|
||||||
if err != nil {
|
|
||||||
return errInvalidSignatureEncoding
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(conf.Keys); i++ {
|
|
||||||
if hmac.Equal(messageMAC, signatureFor(path, i)) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errInvalidSignature
|
|
||||||
}
|
|
||||||
|
|
||||||
func signatureFor(str string, pairInd int) []byte {
|
|
||||||
mac := hmac.New(sha256.New, conf.Keys[pairInd])
|
|
||||||
mac.Write(conf.Salts[pairInd])
|
|
||||||
mac.Write([]byte(str))
|
|
||||||
expectedMAC := mac.Sum(nil)
|
|
||||||
if conf.SignatureSize < 32 {
|
|
||||||
return expectedMAC[:conf.SignatureSize]
|
|
||||||
}
|
|
||||||
return expectedMAC
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CryptTestSuite struct{ MainTestSuite }
|
|
||||||
|
|
||||||
func (s *CryptTestSuite) SetupTest() {
|
|
||||||
s.MainTestSuite.SetupTest()
|
|
||||||
|
|
||||||
conf.Keys = []securityKey{securityKey("test-key")}
|
|
||||||
conf.Salts = []securityKey{securityKey("test-salt")}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CryptTestSuite) TestValidatePath() {
|
|
||||||
err := validatePath("dtLwhdnPPiu_epMl1LrzheLpvHas-4mwvY6L3Z8WwlY", "asd")
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CryptTestSuite) TestValidatePathTruncated() {
|
|
||||||
conf.SignatureSize = 8
|
|
||||||
|
|
||||||
err := validatePath("dtLwhdnPPis", "asd")
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CryptTestSuite) TestValidatePathInvalid() {
|
|
||||||
err := validatePath("dtLwhdnPPis", "asd")
|
|
||||||
assert.Error(s.T(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CryptTestSuite) TestValidatePathMultiplePairs() {
|
|
||||||
conf.Keys = append(conf.Keys, securityKey("test-key2"))
|
|
||||||
conf.Salts = append(conf.Salts, securityKey("test-salt2"))
|
|
||||||
|
|
||||||
err := validatePath("dtLwhdnPPiu_epMl1LrzheLpvHas-4mwvY6L3Z8WwlY", "asd")
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
|
|
||||||
err = validatePath("jbDffNPt1-XBgDccsaE-XJB9lx8JIJqdeYIZKgOqZpg", "asd")
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
|
|
||||||
err = validatePath("dtLwhdnPPis", "asd")
|
|
||||||
assert.Error(s.T(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCrypt(t *testing.T) {
|
|
||||||
suite.Run(t, new(CryptTestSuite))
|
|
||||||
}
|
|
||||||
91
datadog.go
91
datadog.go
@@ -1,91 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
|
|
||||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
dataDogSpanCtxKey = ctxKey("dataDogSpan")
|
|
||||||
)
|
|
||||||
|
|
||||||
func initDataDog() {
|
|
||||||
if !conf.DataDogEnable {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
name := os.Getenv("DD_SERVICE")
|
|
||||||
if len(name) == 0 {
|
|
||||||
name = "imgproxy"
|
|
||||||
}
|
|
||||||
|
|
||||||
tracer.Start(
|
|
||||||
tracer.WithService(name),
|
|
||||||
tracer.WithServiceVersion(version),
|
|
||||||
tracer.WithLogger(dataDogLogger{}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopDataDog() {
|
|
||||||
tracer.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func startDataDogRootSpan(ctx context.Context, rw http.ResponseWriter, r *http.Request) (context.Context, context.CancelFunc, http.ResponseWriter) {
|
|
||||||
span := tracer.StartSpan(
|
|
||||||
"request",
|
|
||||||
tracer.Measured(),
|
|
||||||
tracer.SpanType("web"),
|
|
||||||
tracer.Tag(ext.HTTPMethod, r.Method),
|
|
||||||
tracer.Tag(ext.HTTPURL, r.RequestURI),
|
|
||||||
)
|
|
||||||
cancel := func() { span.Finish() }
|
|
||||||
newRw := dataDogResponseWriter{rw, span}
|
|
||||||
|
|
||||||
return context.WithValue(ctx, dataDogSpanCtxKey, span), cancel, newRw
|
|
||||||
}
|
|
||||||
|
|
||||||
func startDataDogSpan(ctx context.Context, name string) context.CancelFunc {
|
|
||||||
rootSpan, _ := ctx.Value(dataDogSpanCtxKey).(tracer.Span)
|
|
||||||
span := tracer.StartSpan(name, tracer.Measured(), tracer.ChildOf(rootSpan.Context()))
|
|
||||||
return func() { span.Finish() }
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendErrorToDataDog(ctx context.Context, err error) {
|
|
||||||
rootSpan, _ := ctx.Value(dataDogSpanCtxKey).(tracer.Span)
|
|
||||||
rootSpan.Finish(tracer.WithError(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendTimeoutToDataDog(ctx context.Context, d time.Duration) {
|
|
||||||
rootSpan, _ := ctx.Value(dataDogSpanCtxKey).(tracer.Span)
|
|
||||||
rootSpan.SetTag("timeout_duration", d)
|
|
||||||
rootSpan.Finish(tracer.WithError(errors.New("Timeout")))
|
|
||||||
}
|
|
||||||
|
|
||||||
type dataDogLogger struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l dataDogLogger) Log(msg string) {
|
|
||||||
logNotice(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
type dataDogResponseWriter struct {
|
|
||||||
rw http.ResponseWriter
|
|
||||||
span tracer.Span
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ddrw dataDogResponseWriter) Header() http.Header {
|
|
||||||
return ddrw.rw.Header()
|
|
||||||
}
|
|
||||||
func (ddrw dataDogResponseWriter) Write(data []byte) (int, error) {
|
|
||||||
return ddrw.rw.Write(data)
|
|
||||||
}
|
|
||||||
func (ddrw dataDogResponseWriter) WriteHeader(statusCode int) {
|
|
||||||
ddrw.span.SetTag(ext.HTTPCode, statusCode)
|
|
||||||
ddrw.rw.WriteHeader(statusCode)
|
|
||||||
}
|
|
||||||
249
download.go
249
download.go
@@ -1,249 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"compress/gzip"
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v2/imagemeta"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
downloadClient *http.Client
|
|
||||||
|
|
||||||
imageDataCtxKey = ctxKey("imageData")
|
|
||||||
|
|
||||||
imageHeadersToStore = []string{
|
|
||||||
"Cache-Control",
|
|
||||||
"Expires",
|
|
||||||
}
|
|
||||||
|
|
||||||
errSourceResolutionTooBig = newError(422, "Source image resolution is too big", "Invalid source image")
|
|
||||||
errSourceFileTooBig = newError(422, "Source image file is too big", "Invalid source image")
|
|
||||||
errSourceImageTypeNotSupported = newError(422, "Source image type not supported", "Invalid source image")
|
|
||||||
)
|
|
||||||
|
|
||||||
const msgSourceImageIsUnreachable = "Source image is unreachable"
|
|
||||||
|
|
||||||
var downloadBufPool *bufPool
|
|
||||||
|
|
||||||
type hardLimitReader struct {
|
|
||||||
r io.Reader
|
|
||||||
left int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lr *hardLimitReader) Read(p []byte) (n int, err error) {
|
|
||||||
if lr.left <= 0 {
|
|
||||||
return 0, errSourceFileTooBig
|
|
||||||
}
|
|
||||||
if len(p) > lr.left {
|
|
||||||
p = p[0:lr.left]
|
|
||||||
}
|
|
||||||
n, err = lr.r.Read(p)
|
|
||||||
lr.left -= n
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDownloading() error {
|
|
||||||
transport := &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
MaxIdleConns: conf.Concurrency,
|
|
||||||
MaxIdleConnsPerHost: conf.Concurrency,
|
|
||||||
DisableCompression: true,
|
|
||||||
DialContext: (&net.Dialer{KeepAlive: 600 * time.Second}).DialContext,
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.IgnoreSslVerification {
|
|
||||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.LocalFileSystemRoot != "" {
|
|
||||||
transport.RegisterProtocol("local", newFsTransport())
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.S3Enabled {
|
|
||||||
if t, err := newS3Transport(); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
transport.RegisterProtocol("s3", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.GCSEnabled {
|
|
||||||
if t, err := newGCSTransport(); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
transport.RegisterProtocol("gs", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.ABSEnabled {
|
|
||||||
if t, err := newAzureTransport(); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
transport.RegisterProtocol("abs", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadClient = &http.Client{
|
|
||||||
Timeout: time.Duration(conf.DownloadTimeout) * time.Second,
|
|
||||||
Transport: transport,
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadBufPool = newBufPool("download", conf.Concurrency, conf.DownloadBufferSize)
|
|
||||||
|
|
||||||
imagemeta.SetMaxSvgCheckRead(conf.MaxSvgCheckBytes)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkDimensions(width, height int) error {
|
|
||||||
if width*height > conf.MaxSrcResolution {
|
|
||||||
return errSourceResolutionTooBig
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkTypeAndDimensions(r io.Reader) (imageType, error) {
|
|
||||||
meta, err := imagemeta.DecodeMeta(r)
|
|
||||||
if err == imagemeta.ErrFormat {
|
|
||||||
return imageTypeUnknown, errSourceImageTypeNotSupported
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return imageTypeUnknown, wrapError(err, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
imgtype, imgtypeOk := imageTypes[meta.Format()]
|
|
||||||
if !imgtypeOk || !imageTypeLoadSupport(imgtype) {
|
|
||||||
return imageTypeUnknown, errSourceImageTypeNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = checkDimensions(meta.Width(), meta.Height()); err != nil {
|
|
||||||
return imageTypeUnknown, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return imgtype, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readAndCheckImage(r io.Reader, contentLength int) (*imageData, error) {
|
|
||||||
if conf.MaxSrcFileSize > 0 && contentLength > conf.MaxSrcFileSize {
|
|
||||||
return nil, errSourceFileTooBig
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := downloadBufPool.Get(contentLength)
|
|
||||||
cancel := func() { downloadBufPool.Put(buf) }
|
|
||||||
|
|
||||||
if conf.MaxSrcFileSize > 0 {
|
|
||||||
r = &hardLimitReader{r: r, left: conf.MaxSrcFileSize}
|
|
||||||
}
|
|
||||||
|
|
||||||
br := newBufReader(r, buf)
|
|
||||||
|
|
||||||
imgtype, err := checkTypeAndDimensions(br)
|
|
||||||
if err != nil {
|
|
||||||
cancel()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = br.Flush(); err != nil {
|
|
||||||
cancel()
|
|
||||||
return nil, newError(404, err.Error(), msgSourceImageIsUnreachable).SetUnexpected(conf.ReportDownloadingErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &imageData{
|
|
||||||
Data: buf.Bytes(),
|
|
||||||
Type: imgtype,
|
|
||||||
cancel: cancel,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestImage(imageURL string) (*http.Response, error) {
|
|
||||||
req, err := http.NewRequest("GET", imageURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, newError(404, err.Error(), msgSourceImageIsUnreachable).SetUnexpected(conf.ReportDownloadingErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("User-Agent", conf.UserAgent)
|
|
||||||
|
|
||||||
res, err := downloadClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return res, newError(404, err.Error(), msgSourceImageIsUnreachable).SetUnexpected(conf.ReportDownloadingErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
body, _ := ioutil.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
|
|
||||||
msg := fmt.Sprintf("Can't download image; Status: %d; %s", res.StatusCode, string(body))
|
|
||||||
return res, newError(404, msg, msgSourceImageIsUnreachable).SetUnexpected(conf.ReportDownloadingErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func downloadImage(imageURL string) (*imageData, error) {
|
|
||||||
res, err := requestImage(imageURL)
|
|
||||||
if res != nil {
|
|
||||||
defer res.Body.Close()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
body := res.Body
|
|
||||||
contentLength := int(res.ContentLength)
|
|
||||||
|
|
||||||
if res.Header.Get("Content-Encoding") == "gzip" {
|
|
||||||
gzipBody, errGzip := gzip.NewReader(res.Body)
|
|
||||||
if gzipBody != nil {
|
|
||||||
defer gzipBody.Close()
|
|
||||||
}
|
|
||||||
if errGzip != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
body = gzipBody
|
|
||||||
contentLength = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
imgdata, err := readAndCheckImage(body, contentLength)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
imgdata.Headers = make(map[string]string)
|
|
||||||
for _, h := range imageHeadersToStore {
|
|
||||||
if val := res.Header.Get(h); len(val) != 0 {
|
|
||||||
imgdata.Headers[h] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return imgdata, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func downloadImageCtx(ctx context.Context) (context.Context, context.CancelFunc, error) {
|
|
||||||
imageURL := getImageURL(ctx)
|
|
||||||
|
|
||||||
defer startDataDogSpan(ctx, "downloading_image")()
|
|
||||||
defer startNewRelicSegment(ctx, "Downloading image")()
|
|
||||||
defer startPrometheusDuration(prometheusDownloadDuration)()
|
|
||||||
|
|
||||||
imgdata, err := downloadImage(imageURL)
|
|
||||||
if err != nil {
|
|
||||||
return ctx, func() {}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, imageDataCtxKey, imgdata)
|
|
||||||
|
|
||||||
return ctx, imgdata.Close, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getImageData(ctx context.Context) *imageData {
|
|
||||||
return ctx.Value(imageDataCtxKey).(*imageData)
|
|
||||||
}
|
|
||||||
26
errorreport/bugsnag/bugsnag.go
Normal file
26
errorreport/bugsnag/bugsnag.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package bugsnag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/bugsnag/bugsnag-go"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var enabled bool
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
if len(config.BugsnagKey) > 0 {
|
||||||
|
bugsnag.Configure(bugsnag.Configuration{
|
||||||
|
APIKey: config.BugsnagKey,
|
||||||
|
ReleaseStage: config.BugsnagStage,
|
||||||
|
})
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Report(err error, req *http.Request) {
|
||||||
|
if enabled {
|
||||||
|
bugsnag.Notify(err, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
errorreport/errorreport.go
Normal file
21
errorreport/errorreport.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package errorreport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/errorreport/bugsnag"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/errorreport/honeybadger"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/errorreport/sentry"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
bugsnag.Init()
|
||||||
|
honeybadger.Init()
|
||||||
|
sentry.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Report(err error, req *http.Request) {
|
||||||
|
bugsnag.Report(err, req)
|
||||||
|
honeybadger.Report(err, req)
|
||||||
|
sentry.Report(err, req)
|
||||||
|
}
|
||||||
38
errorreport/honeybadger/honeybadger.go
Normal file
38
errorreport/honeybadger/honeybadger.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package honeybadger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/honeybadger-io/honeybadger-go"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
enabled bool
|
||||||
|
|
||||||
|
headersReplacer = strings.NewReplacer("-", "_")
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
if len(config.HoneybadgerKey) > 0 {
|
||||||
|
honeybadger.Configure(honeybadger.Configuration{
|
||||||
|
APIKey: config.HoneybadgerKey,
|
||||||
|
Env: config.HoneybadgerEnv,
|
||||||
|
})
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Report(err error, req *http.Request) {
|
||||||
|
if enabled {
|
||||||
|
headers := make(honeybadger.CGIData)
|
||||||
|
|
||||||
|
for k, v := range req.Header {
|
||||||
|
key := "HTTP_" + headersReplacer.Replace(strings.ToUpper(k))
|
||||||
|
headers[key] = v[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
honeybadger.Notify(err, req.URL, headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
39
errorreport/sentry/sentry.go
Normal file
39
errorreport/sentry/sentry.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
enabled bool
|
||||||
|
|
||||||
|
timeout = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
if len(config.SentryDSN) > 0 {
|
||||||
|
sentry.Init(sentry.ClientOptions{
|
||||||
|
Dsn: config.SentryDSN,
|
||||||
|
Release: config.SentryRelease,
|
||||||
|
Environment: config.SentryEnvironment,
|
||||||
|
})
|
||||||
|
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Report(err error, req *http.Request) {
|
||||||
|
if enabled {
|
||||||
|
hub := sentry.CurrentHub().Clone()
|
||||||
|
hub.Scope().SetRequest(req)
|
||||||
|
hub.Scope().SetLevel(sentry.LevelError)
|
||||||
|
eventID := hub.CaptureException(err)
|
||||||
|
if eventID != nil {
|
||||||
|
hub.Flush(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bugsnag/bugsnag-go"
|
|
||||||
"github.com/getsentry/sentry-go"
|
|
||||||
"github.com/honeybadger-io/honeybadger-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
bugsnagEnabled bool
|
|
||||||
honeybadgerEnabled bool
|
|
||||||
sentryEnabled bool
|
|
||||||
|
|
||||||
headersReplacer = strings.NewReplacer("-", "_")
|
|
||||||
sentryTimeout = 5 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
func initErrorsReporting() {
|
|
||||||
if len(conf.BugsnagKey) > 0 {
|
|
||||||
bugsnag.Configure(bugsnag.Configuration{
|
|
||||||
APIKey: conf.BugsnagKey,
|
|
||||||
ReleaseStage: conf.BugsnagStage,
|
|
||||||
})
|
|
||||||
bugsnagEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.HoneybadgerKey) > 0 {
|
|
||||||
honeybadger.Configure(honeybadger.Configuration{
|
|
||||||
APIKey: conf.HoneybadgerKey,
|
|
||||||
Env: conf.HoneybadgerEnv,
|
|
||||||
})
|
|
||||||
honeybadgerEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.SentryDSN) > 0 {
|
|
||||||
sentry.Init(sentry.ClientOptions{
|
|
||||||
Dsn: conf.SentryDSN,
|
|
||||||
Release: conf.SentryRelease,
|
|
||||||
Environment: conf.SentryEnvironment,
|
|
||||||
})
|
|
||||||
|
|
||||||
sentryEnabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func reportError(err error, req *http.Request) {
|
|
||||||
if bugsnagEnabled {
|
|
||||||
bugsnag.Notify(err, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
if honeybadgerEnabled {
|
|
||||||
headers := make(honeybadger.CGIData)
|
|
||||||
|
|
||||||
for k, v := range req.Header {
|
|
||||||
key := "HTTP_" + headersReplacer.Replace(strings.ToUpper(k))
|
|
||||||
headers[key] = v[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
honeybadger.Notify(err, req.URL, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
if sentryEnabled {
|
|
||||||
hub := sentry.CurrentHub().Clone()
|
|
||||||
hub.Scope().SetRequest(req)
|
|
||||||
hub.Scope().SetLevel(sentry.LevelError)
|
|
||||||
eventID := hub.CaptureException(err)
|
|
||||||
if eventID != nil {
|
|
||||||
hub.Flush(sentryTimeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
13
etag.go
13
etag.go
@@ -7,6 +7,10 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"hash"
|
"hash"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
type eTagCalc struct {
|
type eTagCalc struct {
|
||||||
@@ -26,19 +30,18 @@ var eTagCalcPool = sync.Pool{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func calcETag(ctx context.Context) string {
|
func calcETag(ctx context.Context, imgdata *imagedata.ImageData, po *options.ProcessingOptions) string {
|
||||||
c := eTagCalcPool.Get().(*eTagCalc)
|
c := eTagCalcPool.Get().(*eTagCalc)
|
||||||
defer eTagCalcPool.Put(c)
|
defer eTagCalcPool.Put(c)
|
||||||
|
|
||||||
c.hash.Reset()
|
c.hash.Reset()
|
||||||
c.hash.Write(getImageData(ctx).Data)
|
c.hash.Write(imgdata.Data)
|
||||||
footprint := c.hash.Sum(nil)
|
footprint := c.hash.Sum(nil)
|
||||||
|
|
||||||
c.hash.Reset()
|
c.hash.Reset()
|
||||||
c.hash.Write(footprint)
|
c.hash.Write(footprint)
|
||||||
c.hash.Write([]byte(version))
|
c.hash.Write([]byte(version.Version()))
|
||||||
c.enc.Encode(conf)
|
c.enc.Encode(po)
|
||||||
c.enc.Encode(getProcessingOptions(ctx))
|
|
||||||
|
|
||||||
return hex.EncodeToString(c.hash.Sum(nil))
|
return hex.EncodeToString(c.hash.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,17 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config/configurators"
|
||||||
)
|
)
|
||||||
|
|
||||||
func healthcheck() int {
|
func healthcheck() int {
|
||||||
network := conf.Network
|
network := config.Network
|
||||||
bind := conf.Bind
|
bind := config.Bind
|
||||||
|
|
||||||
strEnvConfig(&network, "IMGPROXY_NETWORK")
|
configurators.String(&network, "IMGPROXY_NETWORK")
|
||||||
strEnvConfig(&bind, "IMGPROXY_BIND")
|
configurators.String(&bind, "IMGPROXY_BIND")
|
||||||
|
|
||||||
httpc := http.Client{
|
httpc := http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package ierrors
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type imgproxyError struct {
|
type Error struct {
|
||||||
StatusCode int
|
StatusCode int
|
||||||
Message string
|
Message string
|
||||||
PublicMessage string
|
PublicMessage string
|
||||||
@@ -15,11 +15,11 @@ type imgproxyError struct {
|
|||||||
stack []uintptr
|
stack []uintptr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *imgproxyError) Error() string {
|
func (e *Error) Error() string {
|
||||||
return e.Message
|
return e.Message
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *imgproxyError) FormatStack() string {
|
func (e *Error) FormatStack() string {
|
||||||
if e.stack == nil {
|
if e.stack == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -27,25 +27,25 @@ func (e *imgproxyError) FormatStack() string {
|
|||||||
return formatStack(e.stack)
|
return formatStack(e.stack)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *imgproxyError) StackTrace() []uintptr {
|
func (e *Error) StackTrace() []uintptr {
|
||||||
return e.stack
|
return e.stack
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *imgproxyError) SetUnexpected(u bool) *imgproxyError {
|
func (e *Error) SetUnexpected(u bool) *Error {
|
||||||
e.Unexpected = u
|
e.Unexpected = u
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
func newError(status int, msg string, pub string) *imgproxyError {
|
func New(status int, msg string, pub string) *Error {
|
||||||
return &imgproxyError{
|
return &Error{
|
||||||
StatusCode: status,
|
StatusCode: status,
|
||||||
Message: msg,
|
Message: msg,
|
||||||
PublicMessage: pub,
|
PublicMessage: pub,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUnexpectedError(msg string, skip int) *imgproxyError {
|
func NewUnexpected(msg string, skip int) *Error {
|
||||||
return &imgproxyError{
|
return &Error{
|
||||||
StatusCode: 500,
|
StatusCode: 500,
|
||||||
Message: msg,
|
Message: msg,
|
||||||
PublicMessage: "Internal error",
|
PublicMessage: "Internal error",
|
||||||
@@ -55,11 +55,11 @@ func newUnexpectedError(msg string, skip int) *imgproxyError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapError(err error, skip int) *imgproxyError {
|
func Wrap(err error, skip int) *Error {
|
||||||
if ierr, ok := err.(*imgproxyError); ok {
|
if ierr, ok := err.(*Error); ok {
|
||||||
return ierr
|
return ierr
|
||||||
}
|
}
|
||||||
return newUnexpectedError(err.Error(), skip+1)
|
return NewUnexpected(err.Error(), skip+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func callers(skip int) []uintptr {
|
func callers(skip int) []uintptr {
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type imageData struct {
|
|
||||||
Data []byte
|
|
||||||
Type imageType
|
|
||||||
Headers map[string]string
|
|
||||||
|
|
||||||
cancel context.CancelFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *imageData) Close() {
|
|
||||||
if d.cancel != nil {
|
|
||||||
d.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWatermarkData() (*imageData, error) {
|
|
||||||
if len(conf.WatermarkData) > 0 {
|
|
||||||
return base64ImageData(conf.WatermarkData, "watermark")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.WatermarkPath) > 0 {
|
|
||||||
return fileImageData(conf.WatermarkPath, "watermark")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.WatermarkURL) > 0 {
|
|
||||||
return remoteImageData(conf.WatermarkURL, "watermark")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFallbackImageData() (*imageData, error) {
|
|
||||||
if len(conf.FallbackImageData) > 0 {
|
|
||||||
return base64ImageData(conf.FallbackImageData, "fallback image")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.FallbackImagePath) > 0 {
|
|
||||||
return fileImageData(conf.FallbackImagePath, "fallback image")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.FallbackImageURL) > 0 {
|
|
||||||
return remoteImageData(conf.FallbackImageURL, "fallback image")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func base64ImageData(encoded, desc string) (*imageData, error) {
|
|
||||||
data, err := base64.StdEncoding.DecodeString(encoded)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Can't decode %s data: %s", desc, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
imgtype, err := checkTypeAndDimensions(bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Can't decode %s: %s", desc, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &imageData{Data: data, Type: imgtype}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileImageData(path, desc string) (*imageData, error) {
|
|
||||||
f, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Can't read %s: %s", desc, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fi, err := f.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Can't read %s: %s", desc, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
imgdata, err := readAndCheckImage(f, int(fi.Size()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Can't read %s: %s", desc, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return imgdata, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func remoteImageData(imageURL, desc string) (*imageData, error) {
|
|
||||||
imgdata, err := downloadImage(imageURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Can't download %s: %s", desc, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return imgdata, nil
|
|
||||||
}
|
|
||||||
133
image_type.go
133
image_type.go
@@ -1,133 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
/*
|
|
||||||
#cgo LDFLAGS: -s -w
|
|
||||||
#include "vips.h"
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type imageType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
imageTypeUnknown = imageType(C.UNKNOWN)
|
|
||||||
imageTypeJPEG = imageType(C.JPEG)
|
|
||||||
imageTypePNG = imageType(C.PNG)
|
|
||||||
imageTypeWEBP = imageType(C.WEBP)
|
|
||||||
imageTypeGIF = imageType(C.GIF)
|
|
||||||
imageTypeICO = imageType(C.ICO)
|
|
||||||
imageTypeSVG = imageType(C.SVG)
|
|
||||||
imageTypeHEIC = imageType(C.HEIC)
|
|
||||||
imageTypeAVIF = imageType(C.AVIF)
|
|
||||||
imageTypeBMP = imageType(C.BMP)
|
|
||||||
imageTypeTIFF = imageType(C.TIFF)
|
|
||||||
|
|
||||||
contentDispositionFilenameFallback = "image"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
imageTypes = map[string]imageType{
|
|
||||||
"jpeg": imageTypeJPEG,
|
|
||||||
"jpg": imageTypeJPEG,
|
|
||||||
"png": imageTypePNG,
|
|
||||||
"webp": imageTypeWEBP,
|
|
||||||
"gif": imageTypeGIF,
|
|
||||||
"ico": imageTypeICO,
|
|
||||||
"svg": imageTypeSVG,
|
|
||||||
"heic": imageTypeHEIC,
|
|
||||||
"avif": imageTypeAVIF,
|
|
||||||
"bmp": imageTypeBMP,
|
|
||||||
"tiff": imageTypeTIFF,
|
|
||||||
}
|
|
||||||
|
|
||||||
mimes = map[imageType]string{
|
|
||||||
imageTypeJPEG: "image/jpeg",
|
|
||||||
imageTypePNG: "image/png",
|
|
||||||
imageTypeWEBP: "image/webp",
|
|
||||||
imageTypeGIF: "image/gif",
|
|
||||||
imageTypeICO: "image/x-icon",
|
|
||||||
imageTypeSVG: "image/svg+xml",
|
|
||||||
imageTypeHEIC: "image/heif",
|
|
||||||
imageTypeAVIF: "image/avif",
|
|
||||||
imageTypeBMP: "image/bmp",
|
|
||||||
imageTypeTIFF: "image/tiff",
|
|
||||||
}
|
|
||||||
|
|
||||||
contentDispositionsFmt = map[imageType]string{
|
|
||||||
imageTypeJPEG: "inline; filename=\"%s.jpg\"",
|
|
||||||
imageTypePNG: "inline; filename=\"%s.png\"",
|
|
||||||
imageTypeWEBP: "inline; filename=\"%s.webp\"",
|
|
||||||
imageTypeGIF: "inline; filename=\"%s.gif\"",
|
|
||||||
imageTypeICO: "inline; filename=\"%s.ico\"",
|
|
||||||
imageTypeSVG: "inline; filename=\"%s.svg\"",
|
|
||||||
imageTypeHEIC: "inline; filename=\"%s.heic\"",
|
|
||||||
imageTypeAVIF: "inline; filename=\"%s.avif\"",
|
|
||||||
imageTypeBMP: "inline; filename=\"%s.bmp\"",
|
|
||||||
imageTypeTIFF: "inline; filename=\"%s.tiff\"",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (it imageType) String() string {
|
|
||||||
for k, v := range imageTypes {
|
|
||||||
if v == it {
|
|
||||||
return k
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it imageType) MarshalJSON() ([]byte, error) {
|
|
||||||
for k, v := range imageTypes {
|
|
||||||
if v == it {
|
|
||||||
return []byte(fmt.Sprintf("%q", k)), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return []byte("null"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it imageType) Mime() string {
|
|
||||||
if mime, ok := mimes[it]; ok {
|
|
||||||
return mime
|
|
||||||
}
|
|
||||||
|
|
||||||
return "application/octet-stream"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it imageType) ContentDisposition(filename string) string {
|
|
||||||
format, ok := contentDispositionsFmt[it]
|
|
||||||
if !ok {
|
|
||||||
return "inline"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(format, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it imageType) ContentDispositionFromURL(imageURL string) string {
|
|
||||||
url, err := url.Parse(imageURL)
|
|
||||||
if err != nil {
|
|
||||||
return it.ContentDisposition(contentDispositionFilenameFallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, filename := filepath.Split(url.Path)
|
|
||||||
if len(filename) == 0 {
|
|
||||||
return it.ContentDisposition(contentDispositionFilenameFallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
return it.ContentDisposition(strings.TrimSuffix(filename, filepath.Ext(filename)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it imageType) SupportsAlpha() bool {
|
|
||||||
return it != imageTypeJPEG && it != imageTypeBMP
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it imageType) SupportsColourProfile() bool {
|
|
||||||
return it == imageTypeJPEG ||
|
|
||||||
it == imageTypeWEBP ||
|
|
||||||
it == imageTypeAVIF
|
|
||||||
}
|
|
||||||
142
imagedata/download.go
Normal file
142
imagedata/download.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package imagedata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/ierrors"
|
||||||
|
|
||||||
|
azureTransport "github.com/imgproxy/imgproxy/v2/transport/azure"
|
||||||
|
fsTransport "github.com/imgproxy/imgproxy/v2/transport/fs"
|
||||||
|
gcsTransport "github.com/imgproxy/imgproxy/v2/transport/gcs"
|
||||||
|
s3Transport "github.com/imgproxy/imgproxy/v2/transport/s3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
downloadClient *http.Client
|
||||||
|
|
||||||
|
imageHeadersToStore = []string{
|
||||||
|
"Cache-Control",
|
||||||
|
"Expires",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const msgSourceImageIsUnreachable = "Source image is unreachable"
|
||||||
|
|
||||||
|
func initDownloading() error {
|
||||||
|
transport := &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
MaxIdleConns: config.Concurrency,
|
||||||
|
MaxIdleConnsPerHost: config.Concurrency,
|
||||||
|
DisableCompression: true,
|
||||||
|
DialContext: (&net.Dialer{KeepAlive: 600 * time.Second}).DialContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.IgnoreSslVerification {
|
||||||
|
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.LocalFileSystemRoot != "" {
|
||||||
|
transport.RegisterProtocol("local", fsTransport.New())
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.S3Enabled {
|
||||||
|
if t, err := s3Transport.New(); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
transport.RegisterProtocol("s3", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.GCSEnabled {
|
||||||
|
if t, err := gcsTransport.New(); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
transport.RegisterProtocol("gs", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ABSEnabled {
|
||||||
|
if t, err := azureTransport.New(); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
transport.RegisterProtocol("abs", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadClient = &http.Client{
|
||||||
|
Timeout: time.Duration(config.DownloadTimeout) * time.Second,
|
||||||
|
Transport: transport,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestImage(imageURL string) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest("GET", imageURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ierrors.New(404, err.Error(), msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", config.UserAgent)
|
||||||
|
|
||||||
|
res, err := downloadClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return res, ierrors.New(404, err.Error(), msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
body, _ := ioutil.ReadAll(res.Body)
|
||||||
|
res.Body.Close()
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("Can't download image; Status: %d; %s", res.StatusCode, string(body))
|
||||||
|
return res, ierrors.New(404, msg, msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func download(imageURL string) (*ImageData, error) {
|
||||||
|
res, err := requestImage(imageURL)
|
||||||
|
if res != nil {
|
||||||
|
defer res.Body.Close()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body := res.Body
|
||||||
|
contentLength := int(res.ContentLength)
|
||||||
|
|
||||||
|
if res.Header.Get("Content-Encoding") == "gzip" {
|
||||||
|
gzipBody, errGzip := gzip.NewReader(res.Body)
|
||||||
|
if gzipBody != nil {
|
||||||
|
defer gzipBody.Close()
|
||||||
|
}
|
||||||
|
if errGzip != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
body = gzipBody
|
||||||
|
contentLength = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
imgdata, err := readAndCheckImage(body, contentLength)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
imgdata.Headers = make(map[string]string)
|
||||||
|
for _, h := range imageHeadersToStore {
|
||||||
|
if val := res.Header.Get(h); len(val) != 0 {
|
||||||
|
imgdata.Headers[h] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return imgdata, nil
|
||||||
|
}
|
||||||
135
imagedata/image_data.go
Normal file
135
imagedata/image_data.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package imagedata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Watermark *ImageData
|
||||||
|
FallbackImage *ImageData
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImageData struct {
|
||||||
|
Type imagetype.Type
|
||||||
|
Data []byte
|
||||||
|
Headers map[string]string
|
||||||
|
|
||||||
|
cancel context.CancelFunc
|
||||||
|
cancelOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ImageData) Close() {
|
||||||
|
d.cancelOnce.Do(func() {
|
||||||
|
if d.cancel != nil {
|
||||||
|
d.cancel()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ImageData) SetCancel(cancel context.CancelFunc) {
|
||||||
|
d.cancel = cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
func Init() error {
|
||||||
|
initRead()
|
||||||
|
|
||||||
|
if err := initDownloading(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := loadWatermark(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := loadFallbackImage(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadWatermark() (err error) {
|
||||||
|
if len(config.WatermarkData) > 0 {
|
||||||
|
Watermark, err = FromBase64(config.WatermarkData, "watermark")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.WatermarkPath) > 0 {
|
||||||
|
Watermark, err = FromFile(config.WatermarkPath, "watermark")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.WatermarkURL) > 0 {
|
||||||
|
Watermark, err = Download(config.WatermarkURL, "watermark")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFallbackImage() (err error) {
|
||||||
|
if len(config.FallbackImageData) > 0 {
|
||||||
|
FallbackImage, err = FromBase64(config.FallbackImageData, "fallback image")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.FallbackImagePath) > 0 {
|
||||||
|
FallbackImage, err = FromFile(config.FallbackImagePath, "fallback image")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.FallbackImageURL) > 0 {
|
||||||
|
FallbackImage, err = Download(config.FallbackImageURL, "fallback image")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromBase64(encoded, desc string) (*ImageData, error) {
|
||||||
|
dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encoded))
|
||||||
|
size := 4 * (len(encoded)/3 + 1)
|
||||||
|
|
||||||
|
imgdata, err := readAndCheckImage(dec, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Can't decode %s: %s", desc, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return imgdata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromFile(path, desc string) (*ImageData, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Can't read %s: %s", desc, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fi, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Can't read %s: %s", desc, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
imgdata, err := readAndCheckImage(f, int(fi.Size()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Can't read %s: %s", desc, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return imgdata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Download(imageURL, desc string) (*ImageData, error) {
|
||||||
|
imgdata, err := download(imageURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Can't download %s: %s", desc, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return imgdata, nil
|
||||||
|
}
|
||||||
86
imagedata/read.go
Normal file
86
imagedata/read.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package imagedata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/bufpool"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/bufreader"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/ierrors"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagemeta"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/security"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrSourceFileTooBig = ierrors.New(422, "Source image file is too big", "Invalid source image")
|
||||||
|
ErrSourceImageTypeNotSupported = ierrors.New(422, "Source image type not supported", "Invalid source image")
|
||||||
|
)
|
||||||
|
|
||||||
|
var downloadBufPool *bufpool.Pool
|
||||||
|
|
||||||
|
func initRead() {
|
||||||
|
downloadBufPool = bufpool.New("download", config.Concurrency, config.DownloadBufferSize)
|
||||||
|
|
||||||
|
imagemeta.SetMaxSvgCheckRead(config.MaxSvgCheckBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
type hardLimitReader struct {
|
||||||
|
r io.Reader
|
||||||
|
left int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lr *hardLimitReader) Read(p []byte) (n int, err error) {
|
||||||
|
if lr.left <= 0 {
|
||||||
|
return 0, ErrSourceFileTooBig
|
||||||
|
}
|
||||||
|
if len(p) > lr.left {
|
||||||
|
p = p[0:lr.left]
|
||||||
|
}
|
||||||
|
n, err = lr.r.Read(p)
|
||||||
|
lr.left -= n
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAndCheckImage(r io.Reader, contentLength int) (*ImageData, error) {
|
||||||
|
if config.MaxSrcFileSize > 0 && contentLength > config.MaxSrcFileSize {
|
||||||
|
return nil, ErrSourceFileTooBig
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := downloadBufPool.Get(contentLength)
|
||||||
|
cancel := func() { downloadBufPool.Put(buf) }
|
||||||
|
|
||||||
|
if config.MaxSrcFileSize > 0 {
|
||||||
|
r = &hardLimitReader{r: r, left: config.MaxSrcFileSize}
|
||||||
|
}
|
||||||
|
|
||||||
|
br := bufreader.New(r, buf)
|
||||||
|
|
||||||
|
meta, err := imagemeta.DecodeMeta(br)
|
||||||
|
if err == imagemeta.ErrFormat {
|
||||||
|
return nil, ErrSourceImageTypeNotSupported
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, ierrors.Wrap(err, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
imgtype, imgtypeOk := imagetype.Types[meta.Format()]
|
||||||
|
if !imgtypeOk {
|
||||||
|
return nil, ErrSourceImageTypeNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = security.CheckDimensions(meta.Width(), meta.Height()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = br.Flush(); err != nil {
|
||||||
|
cancel()
|
||||||
|
return nil, ierrors.New(404, err.Error(), msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ImageData{
|
||||||
|
Data: buf.Bytes(),
|
||||||
|
Type: imgtype,
|
||||||
|
cancel: cancel,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
137
imagetype/imagetype.go
Normal file
137
imagetype/imagetype.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package imagetype
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo LDFLAGS: -s -w
|
||||||
|
#include "imagetype.h"
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Type int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Unknown = Type(C.UNKNOWN)
|
||||||
|
JPEG = Type(C.JPEG)
|
||||||
|
PNG = Type(C.PNG)
|
||||||
|
WEBP = Type(C.WEBP)
|
||||||
|
GIF = Type(C.GIF)
|
||||||
|
ICO = Type(C.ICO)
|
||||||
|
SVG = Type(C.SVG)
|
||||||
|
HEIC = Type(C.HEIC)
|
||||||
|
AVIF = Type(C.AVIF)
|
||||||
|
BMP = Type(C.BMP)
|
||||||
|
TIFF = Type(C.TIFF)
|
||||||
|
|
||||||
|
contentDispositionFilenameFallback = "image"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Types = map[string]Type{
|
||||||
|
"jpeg": JPEG,
|
||||||
|
"jpg": JPEG,
|
||||||
|
"png": PNG,
|
||||||
|
"webp": WEBP,
|
||||||
|
"gif": GIF,
|
||||||
|
"ico": ICO,
|
||||||
|
"svg": SVG,
|
||||||
|
"heic": HEIC,
|
||||||
|
"avif": AVIF,
|
||||||
|
"bmp": BMP,
|
||||||
|
"tiff": TIFF,
|
||||||
|
}
|
||||||
|
|
||||||
|
mimes = map[Type]string{
|
||||||
|
JPEG: "image/jpeg",
|
||||||
|
PNG: "image/png",
|
||||||
|
WEBP: "image/webp",
|
||||||
|
GIF: "image/gif",
|
||||||
|
ICO: "image/x-icon",
|
||||||
|
SVG: "image/svg+xml",
|
||||||
|
HEIC: "image/heif",
|
||||||
|
AVIF: "image/avif",
|
||||||
|
BMP: "image/bmp",
|
||||||
|
TIFF: "image/tiff",
|
||||||
|
}
|
||||||
|
|
||||||
|
contentDispositionsFmt = map[Type]string{
|
||||||
|
JPEG: "inline; filename=\"%s.jpg\"",
|
||||||
|
PNG: "inline; filename=\"%s.png\"",
|
||||||
|
WEBP: "inline; filename=\"%s.webp\"",
|
||||||
|
GIF: "inline; filename=\"%s.gif\"",
|
||||||
|
ICO: "inline; filename=\"%s.ico\"",
|
||||||
|
SVG: "inline; filename=\"%s.svg\"",
|
||||||
|
HEIC: "inline; filename=\"%s.heic\"",
|
||||||
|
AVIF: "inline; filename=\"%s.avif\"",
|
||||||
|
BMP: "inline; filename=\"%s.bmp\"",
|
||||||
|
TIFF: "inline; filename=\"%s.tiff\"",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (it Type) String() string {
|
||||||
|
for k, v := range Types {
|
||||||
|
if v == it {
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it Type) MarshalJSON() ([]byte, error) {
|
||||||
|
for k, v := range Types {
|
||||||
|
if v == it {
|
||||||
|
return []byte(fmt.Sprintf("%q", k)), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it Type) Mime() string {
|
||||||
|
if mime, ok := mimes[it]; ok {
|
||||||
|
return mime
|
||||||
|
}
|
||||||
|
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it Type) ContentDisposition(filename string) string {
|
||||||
|
format, ok := contentDispositionsFmt[it]
|
||||||
|
if !ok {
|
||||||
|
return "inline"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(format, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it Type) ContentDispositionFromURL(imageURL string) string {
|
||||||
|
url, err := url.Parse(imageURL)
|
||||||
|
if err != nil {
|
||||||
|
return it.ContentDisposition(contentDispositionFilenameFallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, filename := filepath.Split(url.Path)
|
||||||
|
if len(filename) == 0 {
|
||||||
|
return it.ContentDisposition(contentDispositionFilenameFallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
return it.ContentDisposition(strings.TrimSuffix(filename, filepath.Ext(filename)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it Type) SupportsAlpha() bool {
|
||||||
|
return it != JPEG && it != BMP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it Type) SupportsAnimation() bool {
|
||||||
|
return it == GIF || it == WEBP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it Type) SupportsColourProfile() bool {
|
||||||
|
return it == JPEG ||
|
||||||
|
it == WEBP ||
|
||||||
|
it == AVIF
|
||||||
|
}
|
||||||
13
imagetype/imagetype.h
Normal file
13
imagetype/imagetype.h
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
enum ImgproxyImageTypes {
|
||||||
|
UNKNOWN = 0,
|
||||||
|
JPEG,
|
||||||
|
PNG,
|
||||||
|
WEBP,
|
||||||
|
GIF,
|
||||||
|
ICO,
|
||||||
|
SVG,
|
||||||
|
HEIC,
|
||||||
|
AVIF,
|
||||||
|
BMP,
|
||||||
|
TIFF
|
||||||
|
};
|
||||||
40
imath/imath.go
Normal file
40
imath/imath.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package imath
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
func Max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func Min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func MinNonZero(a, b int) int {
|
||||||
|
switch {
|
||||||
|
case a == 0:
|
||||||
|
return b
|
||||||
|
case b == 0:
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
return Min(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Round(a float64) int {
|
||||||
|
return int(math.Round(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Scale(a int, scale float64) int {
|
||||||
|
if a == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return Round(float64(a) * scale)
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
// +build !linux,!darwin !go1.11
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func listenReuseport(network, address string) (net.Listener, error) {
|
|
||||||
if conf.SoReuseport {
|
|
||||||
logWarning("SO_REUSEPORT support is not implemented for your OS or Go version")
|
|
||||||
}
|
|
||||||
|
|
||||||
return net.Listen(network, address)
|
|
||||||
}
|
|
||||||
112
log.go
112
log.go
@@ -1,112 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
logrus "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initLog() error {
|
|
||||||
logFormat := "pretty"
|
|
||||||
strEnvConfig(&logFormat, "IMGPROXY_LOG_FORMAT")
|
|
||||||
|
|
||||||
switch logFormat {
|
|
||||||
case "structured":
|
|
||||||
logrus.SetFormatter(&logStructuredFormatter{})
|
|
||||||
case "json":
|
|
||||||
logrus.SetFormatter(&logrus.JSONFormatter{})
|
|
||||||
default:
|
|
||||||
logrus.SetFormatter(newLogPrettyFormatter())
|
|
||||||
}
|
|
||||||
|
|
||||||
logLevel := "info"
|
|
||||||
strEnvConfig(&logLevel, "IMGPROXY_LOG_LEVEL")
|
|
||||||
|
|
||||||
levelLogLevel, err := logrus.ParseLevel(logLevel)
|
|
||||||
if err != nil {
|
|
||||||
levelLogLevel = logrus.InfoLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.SetLevel(levelLogLevel)
|
|
||||||
|
|
||||||
if isSyslogEnabled() {
|
|
||||||
slHook, err := newSyslogHook()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Unable to connect to syslog daemon: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.AddHook(slHook)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func logRequest(reqID string, r *http.Request) {
|
|
||||||
path := r.RequestURI
|
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
|
||||||
"request_id": reqID,
|
|
||||||
"method": r.Method,
|
|
||||||
}).Infof("Started %s", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func logResponse(reqID string, r *http.Request, status int, err *imgproxyError, imageURL *string, po *processingOptions) {
|
|
||||||
var level logrus.Level
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case status >= 500:
|
|
||||||
level = logrus.ErrorLevel
|
|
||||||
case status >= 400:
|
|
||||||
level = logrus.WarnLevel
|
|
||||||
default:
|
|
||||||
level = logrus.InfoLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
fields := logrus.Fields{
|
|
||||||
"request_id": reqID,
|
|
||||||
"method": r.Method,
|
|
||||||
"status": status,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fields["error"] = err
|
|
||||||
|
|
||||||
if stack := err.FormatStack(); len(stack) > 0 {
|
|
||||||
fields["stack"] = stack
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if imageURL != nil {
|
|
||||||
fields["image_url"] = *imageURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if po != nil {
|
|
||||||
fields["processing_options"] = po
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.WithFields(fields).Logf(
|
|
||||||
level,
|
|
||||||
"Completed in %s %s", getTimerSince(r.Context()), r.RequestURI,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func logNotice(f string, args ...interface{}) {
|
|
||||||
logrus.Infof(f, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func logWarning(f string, args ...interface{}) {
|
|
||||||
logrus.Warnf(f, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func logError(f string, args ...interface{}) {
|
|
||||||
logrus.Errorf(f, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func logFatal(f string, args ...interface{}) {
|
|
||||||
logrus.Fatalf(f, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func logDebug(f string, args ...interface{}) {
|
|
||||||
logrus.Debugf(f, args...)
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -30,12 +30,12 @@ func (p logKeys) Len() int { return len(p) }
|
|||||||
func (p logKeys) Less(i, j int) bool { return logKeysPriorities[p[i]] > logKeysPriorities[p[j]] }
|
func (p logKeys) Less(i, j int) bool { return logKeysPriorities[p[i]] > logKeysPriorities[p[j]] }
|
||||||
func (p logKeys) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
func (p logKeys) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||||
|
|
||||||
type logPrettyFormatter struct {
|
type prettyFormatter struct {
|
||||||
levelFormat string
|
levelFormat string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLogPrettyFormatter() *logPrettyFormatter {
|
func newPrettyFormatter() *prettyFormatter {
|
||||||
f := new(logPrettyFormatter)
|
f := new(prettyFormatter)
|
||||||
|
|
||||||
levelLenMax := 0
|
levelLenMax := 0
|
||||||
for _, level := range logrus.AllLevels {
|
for _, level := range logrus.AllLevels {
|
||||||
@@ -50,7 +50,7 @@ func newLogPrettyFormatter() *logPrettyFormatter {
|
|||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *logPrettyFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
func (f *prettyFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||||
keys := make([]string, 0, len(entry.Data))
|
keys := make([]string, 0, len(entry.Data))
|
||||||
for k := range entry.Data {
|
for k := range entry.Data {
|
||||||
if k != "stack" {
|
if k != "stack" {
|
||||||
@@ -97,7 +97,7 @@ func (f *logPrettyFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
|||||||
return b.Bytes(), nil
|
return b.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *logPrettyFormatter) appendValue(b *bytes.Buffer, value interface{}) {
|
func (f *prettyFormatter) appendValue(b *bytes.Buffer, value interface{}) {
|
||||||
strValue, ok := value.(string)
|
strValue, ok := value.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
strValue = fmt.Sprint(value)
|
strValue = fmt.Sprint(value)
|
||||||
@@ -110,9 +110,9 @@ func (f *logPrettyFormatter) appendValue(b *bytes.Buffer, value interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type logStructuredFormatter struct{}
|
type structuredFormatter struct{}
|
||||||
|
|
||||||
func (f *logStructuredFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
func (f *structuredFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||||
keys := make([]string, 0, len(entry.Data))
|
keys := make([]string, 0, len(entry.Data))
|
||||||
for k := range entry.Data {
|
for k := range entry.Data {
|
||||||
keys = append(keys, k)
|
keys = append(keys, k)
|
||||||
@@ -141,7 +141,7 @@ func (f *logStructuredFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
|||||||
return b.Bytes(), nil
|
return b.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *logStructuredFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
|
func (f *structuredFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
|
||||||
if b.Len() != 0 {
|
if b.Len() != 0 {
|
||||||
b.WriteByte(' ')
|
b.WriteByte(' ')
|
||||||
}
|
}
|
||||||
48
logger/log.go
Normal file
48
logger/log.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
logrus "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config/configurators"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() error {
|
||||||
|
log.SetOutput(os.Stdout)
|
||||||
|
|
||||||
|
logFormat := "pretty"
|
||||||
|
logLevel := "info"
|
||||||
|
|
||||||
|
configurators.String(&logFormat, "IMGPROXY_LOG_FORMAT")
|
||||||
|
configurators.String(&logLevel, "IMGPROXY_LOG_LEVEL")
|
||||||
|
|
||||||
|
switch logFormat {
|
||||||
|
case "structured":
|
||||||
|
logrus.SetFormatter(&structuredFormatter{})
|
||||||
|
case "json":
|
||||||
|
logrus.SetFormatter(&logrus.JSONFormatter{})
|
||||||
|
default:
|
||||||
|
logrus.SetFormatter(newPrettyFormatter())
|
||||||
|
}
|
||||||
|
|
||||||
|
levelLogLevel, err := logrus.ParseLevel(logLevel)
|
||||||
|
if err != nil {
|
||||||
|
levelLogLevel = logrus.InfoLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.SetLevel(levelLogLevel)
|
||||||
|
|
||||||
|
if isSyslogEnabled() {
|
||||||
|
slHook, err := newSyslogHook()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to connect to syslog daemon: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.AddHook(slHook)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package main
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/syslog"
|
"log/syslog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config/configurators"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ type syslogHook struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isSyslogEnabled() (enabled bool) {
|
func isSyslogEnabled() (enabled bool) {
|
||||||
boolEnvConfig(&enabled, "IMGPROXY_SYSLOG_ENABLE")
|
configurators.Bool(&enabled, "IMGPROXY_SYSLOG_ENABLE")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,16 +38,16 @@ func newSyslogHook() (*syslogHook, error) {
|
|||||||
levelStr = "notice"
|
levelStr = "notice"
|
||||||
)
|
)
|
||||||
|
|
||||||
strEnvConfig(&network, "IMGPROXY_SYSLOG_NETWORK")
|
configurators.String(&network, "IMGPROXY_SYSLOG_NETWORK")
|
||||||
strEnvConfig(&addr, "IMGPROXY_SYSLOG_ADDRESS")
|
configurators.String(&addr, "IMGPROXY_SYSLOG_ADDRESS")
|
||||||
strEnvConfig(&tag, "IMGPROXY_SYSLOG_TAG")
|
configurators.String(&tag, "IMGPROXY_SYSLOG_TAG")
|
||||||
strEnvConfig(&levelStr, "IMGPROXY_SYSLOG_LEVEL")
|
configurators.String(&levelStr, "IMGPROXY_SYSLOG_LEVEL")
|
||||||
|
|
||||||
if l, ok := syslogLevels[levelStr]; ok {
|
if l, ok := syslogLevels[levelStr]; ok {
|
||||||
level = l
|
level = l
|
||||||
} else {
|
} else {
|
||||||
level = logrus.InfoLevel
|
level = logrus.InfoLevel
|
||||||
logWarning("Syslog level '%s' is invalid, 'info' is used", levelStr)
|
logrus.Warningf("Syslog level '%s' is invalid, 'info' is used", levelStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
w, err := syslog.Dial(network, addr, syslog.LOG_NOTICE, tag)
|
w, err := syslog.Dial(network, addr, syslog.LOG_NOTICE, tag)
|
||||||
@@ -54,7 +55,7 @@ func newSyslogHook() (*syslogHook, error) {
|
|||||||
return &syslogHook{
|
return &syslogHook{
|
||||||
writer: w,
|
writer: w,
|
||||||
levels: logrus.AllLevels[:int(level)+1],
|
levels: logrus.AllLevels[:int(level)+1],
|
||||||
formatter: &logStructuredFormatter{},
|
formatter: &structuredFormatter{},
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
71
main.go
71
main.go
@@ -3,81 +3,90 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"runtime"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/errorreport"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/logger"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/memory"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/metrics"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/metrics/prometheus"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/version"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "2.16.1"
|
|
||||||
|
|
||||||
type ctxKey string
|
|
||||||
|
|
||||||
func initialize() error {
|
func initialize() error {
|
||||||
log.SetOutput(os.Stdout)
|
if err := logger.Init(); err != nil {
|
||||||
|
|
||||||
if err := initLog(); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := configure(); err != nil {
|
if err := config.Configure(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := initNewrelic(); err != nil {
|
if err := metrics.Init(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
initDataDog()
|
if err := imagedata.Init(); err != nil {
|
||||||
|
|
||||||
initPrometheus()
|
|
||||||
|
|
||||||
if err := initDownloading(); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
initErrorsReporting()
|
initProcessingHandler()
|
||||||
|
|
||||||
if err := initVips(); err != nil {
|
errorreport.Init()
|
||||||
|
|
||||||
|
if err := vips.Init(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := checkPresets(conf.Presets); err != nil {
|
if err := options.ParsePresets(config.Presets); err != nil {
|
||||||
shutdownVips()
|
vips.Shutdown()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := options.ValidatePresets(); err != nil {
|
||||||
|
vips.Shutdown()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shutdown() {
|
||||||
|
vips.Shutdown()
|
||||||
|
metrics.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
func run() error {
|
func run() error {
|
||||||
if err := initialize(); err != nil {
|
if err := initialize(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer shutdownVips()
|
defer shutdown()
|
||||||
|
|
||||||
defer stopDataDog()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
var logMemStats = len(os.Getenv("IMGPROXY_LOG_MEM_STATS")) > 0
|
var logMemStats = len(os.Getenv("IMGPROXY_LOG_MEM_STATS")) > 0
|
||||||
|
|
||||||
for range time.Tick(time.Duration(conf.FreeMemoryInterval) * time.Second) {
|
for range time.Tick(time.Duration(config.FreeMemoryInterval) * time.Second) {
|
||||||
freeMemory()
|
memory.Free()
|
||||||
|
|
||||||
if logMemStats {
|
if logMemStats {
|
||||||
var m runtime.MemStats
|
memory.LogStats()
|
||||||
runtime.ReadMemStats(&m)
|
|
||||||
logDebug("MEMORY USAGE: Sys=%d HeapIdle=%d HeapInuse=%d", m.Sys/1024/1024, m.HeapIdle/1024/1024, m.HeapInuse/1024/1024)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
if err := startPrometheusServer(cancel); err != nil {
|
if err := prometheus.StartServer(cancel); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,12 +113,12 @@ func main() {
|
|||||||
case "health":
|
case "health":
|
||||||
os.Exit(healthcheck())
|
os.Exit(healthcheck())
|
||||||
case "version":
|
case "version":
|
||||||
fmt.Println(version)
|
fmt.Println(version.Version())
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := run(); err != nil {
|
if err := run(); err != nil {
|
||||||
logFatal(err.Error())
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
main_test.go
29
main_test.go
@@ -1,29 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MainTestSuite struct {
|
|
||||||
suite.Suite
|
|
||||||
|
|
||||||
oldConf config
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
initialize()
|
|
||||||
os.Exit(m.Run())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MainTestSuite) SetupTest() {
|
|
||||||
s.oldConf = conf
|
|
||||||
// Reset presets
|
|
||||||
conf.Presets = make(presets)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MainTestSuite) TearDownTest() {
|
|
||||||
conf = s.oldConf
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// +build !linux
|
// +build !linux
|
||||||
|
|
||||||
package main
|
package memory
|
||||||
|
|
||||||
import "runtime/debug"
|
import "runtime/debug"
|
||||||
|
|
||||||
func freeMemory() {
|
func Free() {
|
||||||
debug.FreeOSMemory()
|
debug.FreeOSMemory()
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// +build linux
|
// +build linux
|
||||||
|
|
||||||
package main
|
package memory
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#include <features.h>
|
#include <features.h>
|
||||||
@@ -13,7 +13,7 @@ void malloc_trim(size_t pad){}
|
|||||||
import "C"
|
import "C"
|
||||||
import "runtime/debug"
|
import "runtime/debug"
|
||||||
|
|
||||||
func freeMemory() {
|
func Free() {
|
||||||
debug.FreeOSMemory()
|
debug.FreeOSMemory()
|
||||||
|
|
||||||
C.malloc_trim(0)
|
C.malloc_trim(0)
|
||||||
23
memory/stats.go
Normal file
23
memory/stats.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LogStats() {
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
log.Debugf(
|
||||||
|
"GO MEMORY USAGE: Sys=%d HeapIdle=%d HeapInuse=%d",
|
||||||
|
m.Sys/1024/1024, m.HeapIdle/1024/1024, m.HeapInuse/1024/1024,
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debugf(
|
||||||
|
"VIPS MEMORY USAGE: Cur=%d Max=%d Allocs=%d",
|
||||||
|
int(vips.GetMem())/1024/1024, int(vips.GetMemHighwater())/1024/1024, int(vips.GetAllocs()),
|
||||||
|
)
|
||||||
|
}
|
||||||
118
metrics/datadog/datadog.go
Normal file
118
metrics/datadog/datadog.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package datadog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
|
||||||
|
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
type spanCtxKey struct{}
|
||||||
|
|
||||||
|
var enabled bool
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
if !config.DataDogEnable {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
name := os.Getenv("DD_SERVICE")
|
||||||
|
if len(name) == 0 {
|
||||||
|
name = "imgproxy"
|
||||||
|
}
|
||||||
|
|
||||||
|
tracer.Start(
|
||||||
|
tracer.WithService(name),
|
||||||
|
tracer.WithServiceVersion(version.Version()),
|
||||||
|
tracer.WithLogger(dataDogLogger{}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Stop() {
|
||||||
|
if enabled {
|
||||||
|
tracer.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartRootSpan(ctx context.Context, rw http.ResponseWriter, r *http.Request) (context.Context, context.CancelFunc, http.ResponseWriter) {
|
||||||
|
if !enabled {
|
||||||
|
return ctx, func() {}, rw
|
||||||
|
}
|
||||||
|
|
||||||
|
span := tracer.StartSpan(
|
||||||
|
"request",
|
||||||
|
tracer.Measured(),
|
||||||
|
tracer.SpanType("web"),
|
||||||
|
tracer.Tag(ext.HTTPMethod, r.Method),
|
||||||
|
tracer.Tag(ext.HTTPURL, r.RequestURI),
|
||||||
|
)
|
||||||
|
cancel := func() { span.Finish() }
|
||||||
|
newRw := dataDogResponseWriter{rw, span}
|
||||||
|
|
||||||
|
return context.WithValue(ctx, spanCtxKey{}, span), cancel, newRw
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartSpan(ctx context.Context, name string) context.CancelFunc {
|
||||||
|
if !enabled {
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rootSpan, ok := ctx.Value(spanCtxKey{}).(tracer.Span); ok {
|
||||||
|
span := tracer.StartSpan(name, tracer.Measured(), tracer.ChildOf(rootSpan.Context()))
|
||||||
|
return func() { span.Finish() }
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendError(ctx context.Context, err error) {
|
||||||
|
if !enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rootSpan, ok := ctx.Value(spanCtxKey{}).(tracer.Span); ok {
|
||||||
|
rootSpan.Finish(tracer.WithError(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendTimeout(ctx context.Context, d time.Duration) {
|
||||||
|
if !enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rootSpan, ok := ctx.Value(spanCtxKey{}).(tracer.Span); ok {
|
||||||
|
rootSpan.SetTag("timeout_duration", d)
|
||||||
|
rootSpan.Finish(tracer.WithError(errors.New("Timeout")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type dataDogLogger struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l dataDogLogger) Log(msg string) {
|
||||||
|
log.Info(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type dataDogResponseWriter struct {
|
||||||
|
rw http.ResponseWriter
|
||||||
|
span tracer.Span
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ddrw dataDogResponseWriter) Header() http.Header {
|
||||||
|
return ddrw.rw.Header()
|
||||||
|
}
|
||||||
|
func (ddrw dataDogResponseWriter) Write(data []byte) (int, error) {
|
||||||
|
return ddrw.rw.Write(data)
|
||||||
|
}
|
||||||
|
func (ddrw dataDogResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
ddrw.span.SetTag(ext.HTTPCode, statusCode)
|
||||||
|
ddrw.rw.WriteHeader(statusCode)
|
||||||
|
}
|
||||||
81
metrics/metrics.go
Normal file
81
metrics/metrics.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/metrics/datadog"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/metrics/newrelic"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/metrics/prometheus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() error {
|
||||||
|
prometheus.Init()
|
||||||
|
|
||||||
|
if err := newrelic.Init(); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
datadog.Init()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Stop() {
|
||||||
|
datadog.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartRequest(ctx context.Context, rw http.ResponseWriter, r *http.Request) (context.Context, context.CancelFunc, http.ResponseWriter) {
|
||||||
|
promCancel := prometheus.StartRequest()
|
||||||
|
ctx, nrCancel, rw := newrelic.StartTransaction(ctx, rw, r)
|
||||||
|
ctx, ddCancel, rw := datadog.StartRootSpan(ctx, rw, r)
|
||||||
|
|
||||||
|
cancel := func() {
|
||||||
|
promCancel()
|
||||||
|
nrCancel()
|
||||||
|
ddCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx, cancel, rw
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartDownloadingSegment(ctx context.Context) context.CancelFunc {
|
||||||
|
promCancel := prometheus.StartDownloadingSegment()
|
||||||
|
nrCancel := newrelic.StartSegment(ctx, "Downloading image")
|
||||||
|
ddCancel := datadog.StartSpan(ctx, "downloading_image")
|
||||||
|
|
||||||
|
cancel := func() {
|
||||||
|
promCancel()
|
||||||
|
nrCancel()
|
||||||
|
ddCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
return cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartProcessingSegment(ctx context.Context) context.CancelFunc {
|
||||||
|
promCancel := prometheus.StartProcessingSegment()
|
||||||
|
nrCancel := newrelic.StartSegment(ctx, "Processing image")
|
||||||
|
ddCancel := datadog.StartSpan(ctx, "processing_image")
|
||||||
|
|
||||||
|
cancel := func() {
|
||||||
|
promCancel()
|
||||||
|
nrCancel()
|
||||||
|
ddCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
return cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendError(ctx context.Context, errType string, err error) {
|
||||||
|
prometheus.IncrementErrorsTotal(errType)
|
||||||
|
newrelic.SendError(ctx, err)
|
||||||
|
datadog.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendTimeout(ctx context.Context, d time.Duration) {
|
||||||
|
prometheus.IncrementErrorsTotal("timeout")
|
||||||
|
newrelic.SendTimeout(ctx, d)
|
||||||
|
datadog.SendTimeout(ctx, d)
|
||||||
|
}
|
||||||
96
metrics/newrelic/newrelic.go
Normal file
96
metrics/newrelic/newrelic.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/newrelic/go-agent/v3/newrelic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type transactionCtxKey struct{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
enabled = false
|
||||||
|
|
||||||
|
newRelicApp *newrelic.Application
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() error {
|
||||||
|
if len(config.NewRelicKey) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
name := config.NewRelicAppName
|
||||||
|
if len(name) == 0 {
|
||||||
|
name = "imgproxy"
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
newRelicApp, err = newrelic.NewApplication(
|
||||||
|
newrelic.ConfigAppName(name),
|
||||||
|
newrelic.ConfigLicense(config.NewRelicKey),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Can't init New Relic agent: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartTransaction(ctx context.Context, rw http.ResponseWriter, r *http.Request) (context.Context, context.CancelFunc, http.ResponseWriter) {
|
||||||
|
if !enabled {
|
||||||
|
return ctx, func() {}, rw
|
||||||
|
}
|
||||||
|
|
||||||
|
txn := newRelicApp.StartTransaction("request")
|
||||||
|
txn.SetWebRequestHTTP(r)
|
||||||
|
newRw := txn.SetWebResponse(rw)
|
||||||
|
cancel := func() { txn.End() }
|
||||||
|
return context.WithValue(ctx, transactionCtxKey{}, txn), cancel, newRw
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartSegment(ctx context.Context, name string) context.CancelFunc {
|
||||||
|
if !enabled {
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if txn, ok := ctx.Value(transactionCtxKey{}).(*newrelic.Transaction); ok {
|
||||||
|
segment := txn.StartSegment(name)
|
||||||
|
return func() { segment.End() }
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendError(ctx context.Context, err error) {
|
||||||
|
if !enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if txn, ok := ctx.Value(transactionCtxKey{}).(*newrelic.Transaction); ok {
|
||||||
|
txn.NoticeError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendTimeout(ctx context.Context, d time.Duration) {
|
||||||
|
if !enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if txn, ok := ctx.Value(transactionCtxKey{}).(*newrelic.Transaction); ok {
|
||||||
|
txn.NoticeError(newrelic.Error{
|
||||||
|
Message: "Timeout",
|
||||||
|
Class: "Timeout",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"time": d.Seconds(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
181
metrics/prometheus/prometheus.go
Normal file
181
metrics/prometheus/prometheus.go
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package prometheus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/reuseport"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
enabled = false
|
||||||
|
|
||||||
|
requestsTotal prometheus.Counter
|
||||||
|
errorsTotal *prometheus.CounterVec
|
||||||
|
requestDuration prometheus.Histogram
|
||||||
|
downloadDuration prometheus.Histogram
|
||||||
|
processingDuration prometheus.Histogram
|
||||||
|
bufferSize *prometheus.HistogramVec
|
||||||
|
bufferDefaultSize *prometheus.GaugeVec
|
||||||
|
bufferMaxSize *prometheus.GaugeVec
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
if len(config.PrometheusBind) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestsTotal = prometheus.NewCounter(prometheus.CounterOpts{
|
||||||
|
Namespace: config.PrometheusNamespace,
|
||||||
|
Name: "requests_total",
|
||||||
|
Help: "A counter of the total number of HTTP requests imgproxy processed.",
|
||||||
|
})
|
||||||
|
|
||||||
|
errorsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: config.PrometheusNamespace,
|
||||||
|
Name: "errors_total",
|
||||||
|
Help: "A counter of the occurred errors separated by type.",
|
||||||
|
}, []string{"type"})
|
||||||
|
|
||||||
|
requestDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||||
|
Namespace: config.PrometheusNamespace,
|
||||||
|
Name: "request_duration_seconds",
|
||||||
|
Help: "A histogram of the response latency.",
|
||||||
|
})
|
||||||
|
|
||||||
|
downloadDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||||
|
Namespace: config.PrometheusNamespace,
|
||||||
|
Name: "download_duration_seconds",
|
||||||
|
Help: "A histogram of the source image downloading latency.",
|
||||||
|
})
|
||||||
|
|
||||||
|
processingDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||||
|
Namespace: config.PrometheusNamespace,
|
||||||
|
Name: "processing_duration_seconds",
|
||||||
|
Help: "A histogram of the image processing latency.",
|
||||||
|
})
|
||||||
|
|
||||||
|
bufferSize = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
|
Namespace: config.PrometheusNamespace,
|
||||||
|
Name: "buffer_size_bytes",
|
||||||
|
Help: "A histogram of the buffer size in bytes.",
|
||||||
|
Buckets: prometheus.ExponentialBuckets(1024, 2, 14),
|
||||||
|
}, []string{"type"})
|
||||||
|
|
||||||
|
bufferDefaultSize = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Namespace: config.PrometheusNamespace,
|
||||||
|
Name: "buffer_default_size_bytes",
|
||||||
|
Help: "A gauge of the buffer default size in bytes.",
|
||||||
|
}, []string{"type"})
|
||||||
|
|
||||||
|
bufferMaxSize = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Namespace: config.PrometheusNamespace,
|
||||||
|
Name: "buffer_max_size_bytes",
|
||||||
|
Help: "A gauge of the buffer max size in bytes.",
|
||||||
|
}, []string{"type"})
|
||||||
|
|
||||||
|
prometheus.MustRegister(
|
||||||
|
requestsTotal,
|
||||||
|
errorsTotal,
|
||||||
|
requestDuration,
|
||||||
|
downloadDuration,
|
||||||
|
processingDuration,
|
||||||
|
bufferSize,
|
||||||
|
bufferDefaultSize,
|
||||||
|
bufferMaxSize,
|
||||||
|
)
|
||||||
|
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartServer(cancel context.CancelFunc) error {
|
||||||
|
if !enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s := http.Server{Handler: promhttp.Handler()}
|
||||||
|
|
||||||
|
l, err := reuseport.Listen("tcp", config.PrometheusBind)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Can't start Prometheus metrics server: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Infof("Starting Prometheus server at %s", config.PrometheusBind)
|
||||||
|
if err := s.Serve(l); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartRequest() context.CancelFunc {
|
||||||
|
return startDuration(requestDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartDownloadingSegment() context.CancelFunc {
|
||||||
|
return startDuration(downloadDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartProcessingSegment() context.CancelFunc {
|
||||||
|
return startDuration(processingDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startDuration(m prometheus.Histogram) context.CancelFunc {
|
||||||
|
if !enabled {
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.Now()
|
||||||
|
return func() {
|
||||||
|
m.Observe(time.Since(t).Seconds())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IncrementErrorsTotal(t string) {
|
||||||
|
if enabled {
|
||||||
|
errorsTotal.With(prometheus.Labels{"type": t}).Inc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IncrementRequestsTotal() {
|
||||||
|
if enabled {
|
||||||
|
requestsTotal.Inc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ObserveBufferSize(t string, size int) {
|
||||||
|
if enabled {
|
||||||
|
bufferSize.With(prometheus.Labels{"type": t}).Observe(float64(size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetBufferDefaultSize(t string, size int) {
|
||||||
|
if enabled {
|
||||||
|
bufferDefaultSize.With(prometheus.Labels{"type": t}).Set(float64(size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetBufferMaxSize(t string, size int) {
|
||||||
|
if enabled {
|
||||||
|
bufferMaxSize.With(prometheus.Labels{"type": t}).Set(float64(size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddGaugeFunc(name, help string, f func() float64) {
|
||||||
|
gauge := prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
||||||
|
Namespace: config.PrometheusNamespace,
|
||||||
|
Name: name,
|
||||||
|
Help: help,
|
||||||
|
}, f)
|
||||||
|
prometheus.MustRegister(gauge)
|
||||||
|
}
|
||||||
88
newrelic.go
88
newrelic.go
@@ -1,88 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/newrelic/go-agent/v3/newrelic"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
newRelicTransactionCtxKey = ctxKey("newRelicTransaction")
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
newRelicEnabled = false
|
|
||||||
|
|
||||||
newRelicApp *newrelic.Application
|
|
||||||
)
|
|
||||||
|
|
||||||
func initNewrelic() error {
|
|
||||||
if len(conf.NewRelicKey) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
name := conf.NewRelicAppName
|
|
||||||
if len(name) == 0 {
|
|
||||||
name = "imgproxy"
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
newRelicApp, err = newrelic.NewApplication(
|
|
||||||
newrelic.ConfigAppName(name),
|
|
||||||
newrelic.ConfigLicense(conf.NewRelicKey),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Can't init New Relic agent: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newRelicEnabled = true
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func startNewRelicTransaction(ctx context.Context, rw http.ResponseWriter, r *http.Request) (context.Context, context.CancelFunc, http.ResponseWriter) {
|
|
||||||
if !newRelicEnabled {
|
|
||||||
return ctx, func() {}, rw
|
|
||||||
}
|
|
||||||
|
|
||||||
txn := newRelicApp.StartTransaction("request")
|
|
||||||
txn.SetWebRequestHTTP(r)
|
|
||||||
newRw := txn.SetWebResponse(rw)
|
|
||||||
cancel := func() { txn.End() }
|
|
||||||
return context.WithValue(ctx, newRelicTransactionCtxKey, txn), cancel, newRw
|
|
||||||
}
|
|
||||||
|
|
||||||
func startNewRelicSegment(ctx context.Context, name string) context.CancelFunc {
|
|
||||||
if !newRelicEnabled {
|
|
||||||
return func() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
txn := ctx.Value(newRelicTransactionCtxKey).(*newrelic.Transaction)
|
|
||||||
segment := txn.StartSegment(name)
|
|
||||||
return func() { segment.End() }
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendErrorToNewRelic(ctx context.Context, err error) {
|
|
||||||
if newRelicEnabled {
|
|
||||||
txn := ctx.Value(newRelicTransactionCtxKey).(*newrelic.Transaction)
|
|
||||||
txn.NoticeError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendTimeoutToNewRelic(ctx context.Context, d time.Duration) {
|
|
||||||
if newRelicEnabled {
|
|
||||||
txn := ctx.Value(newRelicTransactionCtxKey).(*newrelic.Transaction)
|
|
||||||
txn.NoticeError(newrelic.Error{
|
|
||||||
Message: "Timeout",
|
|
||||||
Class: "Timeout",
|
|
||||||
Attributes: map[string]interface{}{
|
|
||||||
"time": d.Seconds(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
52
options/gravity_type.go
Normal file
52
options/gravity_type.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type GravityType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
GravityUnknown GravityType = iota
|
||||||
|
GravityCenter
|
||||||
|
GravityNorth
|
||||||
|
GravityEast
|
||||||
|
GravitySouth
|
||||||
|
GravityWest
|
||||||
|
GravityNorthWest
|
||||||
|
GravityNorthEast
|
||||||
|
GravitySouthWest
|
||||||
|
GravitySouthEast
|
||||||
|
GravitySmart
|
||||||
|
GravityFocusPoint
|
||||||
|
)
|
||||||
|
|
||||||
|
var gravityTypes = map[string]GravityType{
|
||||||
|
"ce": GravityCenter,
|
||||||
|
"no": GravityNorth,
|
||||||
|
"ea": GravityEast,
|
||||||
|
"so": GravitySouth,
|
||||||
|
"we": GravityWest,
|
||||||
|
"nowe": GravityNorthWest,
|
||||||
|
"noea": GravityNorthEast,
|
||||||
|
"sowe": GravitySouthWest,
|
||||||
|
"soea": GravitySouthEast,
|
||||||
|
"sm": GravitySmart,
|
||||||
|
"fp": GravityFocusPoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gt GravityType) String() string {
|
||||||
|
for k, v := range gravityTypes {
|
||||||
|
if v == gt {
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gt GravityType) MarshalJSON() ([]byte, error) {
|
||||||
|
for k, v := range gravityTypes {
|
||||||
|
if v == gt {
|
||||||
|
return []byte(fmt.Sprintf("%q", k)), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
@@ -1,13 +1,23 @@
|
|||||||
package main
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type presets map[string]urlOptions
|
var presets map[string]urlOptions
|
||||||
|
|
||||||
func parsePreset(p presets, presetStr string) error {
|
func ParsePresets(presetStrs []string) error {
|
||||||
|
for _, presetStr := range presetStrs {
|
||||||
|
if err := parsePreset(presetStr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePreset(presetStr string) error {
|
||||||
presetStr = strings.Trim(presetStr, " ")
|
presetStr = strings.Trim(presetStr, " ")
|
||||||
|
|
||||||
if len(presetStr) == 0 || strings.HasPrefix(presetStr, "#") {
|
if len(presetStr) == 0 || strings.HasPrefix(presetStr, "#") {
|
||||||
@@ -38,16 +48,19 @@ func parsePreset(p presets, presetStr string) error {
|
|||||||
return fmt.Errorf("Invalid preset value: %s", presetStr)
|
return fmt.Errorf("Invalid preset value: %s", presetStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
p[name] = opts
|
if presets == nil {
|
||||||
|
presets = make(map[string]urlOptions)
|
||||||
|
}
|
||||||
|
presets[name] = opts
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkPresets(p presets) error {
|
func ValidatePresets() error {
|
||||||
var po processingOptions
|
var po ProcessingOptions
|
||||||
|
|
||||||
for name, opts := range p {
|
for name, opts := range presets {
|
||||||
if err := applyProcessingOptions(&po, opts); err != nil {
|
if err := applyURLOptions(&po, opts); err != nil {
|
||||||
return fmt.Errorf("Error in preset `%s`: %s", name, err)
|
return fmt.Errorf("Error in preset `%s`: %s", name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,109 +1,102 @@
|
|||||||
package main
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PresetsTestSuite struct{ MainTestSuite }
|
type PresetsTestSuite struct{ suite.Suite }
|
||||||
|
|
||||||
|
func (s *PresetsTestSuite) SetupTest() {
|
||||||
|
config.Reset()
|
||||||
|
// Reset presets
|
||||||
|
presets = make(map[string]urlOptions)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *PresetsTestSuite) TestParsePreset() {
|
func (s *PresetsTestSuite) TestParsePreset() {
|
||||||
p := make(presets)
|
err := parsePreset("test=resize:fit:100:200/sharpen:2")
|
||||||
|
|
||||||
err := parsePreset(p, "test=resize:fit:100:200/sharpen:2")
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
assert.Equal(s.T(), urlOptions{
|
assert.Equal(s.T(), urlOptions{
|
||||||
urlOption{Name: "resize", Args: []string{"fit", "100", "200"}},
|
urlOption{Name: "resize", Args: []string{"fit", "100", "200"}},
|
||||||
urlOption{Name: "sharpen", Args: []string{"2"}},
|
urlOption{Name: "sharpen", Args: []string{"2"}},
|
||||||
}, p["test"])
|
}, presets["test"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PresetsTestSuite) TestParsePresetInvalidString() {
|
func (s *PresetsTestSuite) TestParsePresetInvalidString() {
|
||||||
p := make(presets)
|
|
||||||
|
|
||||||
presetStr := "resize:fit:100:200/sharpen:2"
|
presetStr := "resize:fit:100:200/sharpen:2"
|
||||||
err := parsePreset(p, presetStr)
|
err := parsePreset(presetStr)
|
||||||
|
|
||||||
assert.Equal(s.T(), fmt.Errorf("Invalid preset string: %s", presetStr), err)
|
assert.Equal(s.T(), fmt.Errorf("Invalid preset string: %s", presetStr), err)
|
||||||
assert.Empty(s.T(), p)
|
assert.Empty(s.T(), presets)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PresetsTestSuite) TestParsePresetEmptyName() {
|
func (s *PresetsTestSuite) TestParsePresetEmptyName() {
|
||||||
p := make(presets)
|
|
||||||
|
|
||||||
presetStr := "=resize:fit:100:200/sharpen:2"
|
presetStr := "=resize:fit:100:200/sharpen:2"
|
||||||
err := parsePreset(p, presetStr)
|
err := parsePreset(presetStr)
|
||||||
|
|
||||||
assert.Equal(s.T(), fmt.Errorf("Empty preset name: %s", presetStr), err)
|
assert.Equal(s.T(), fmt.Errorf("Empty preset name: %s", presetStr), err)
|
||||||
assert.Empty(s.T(), p)
|
assert.Empty(s.T(), presets)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PresetsTestSuite) TestParsePresetEmptyValue() {
|
func (s *PresetsTestSuite) TestParsePresetEmptyValue() {
|
||||||
p := make(presets)
|
|
||||||
|
|
||||||
presetStr := "test="
|
presetStr := "test="
|
||||||
err := parsePreset(p, presetStr)
|
err := parsePreset(presetStr)
|
||||||
|
|
||||||
assert.Equal(s.T(), fmt.Errorf("Empty preset value: %s", presetStr), err)
|
assert.Equal(s.T(), fmt.Errorf("Empty preset value: %s", presetStr), err)
|
||||||
assert.Empty(s.T(), p)
|
assert.Empty(s.T(), presets)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PresetsTestSuite) TestParsePresetInvalidValue() {
|
func (s *PresetsTestSuite) TestParsePresetInvalidValue() {
|
||||||
p := make(presets)
|
|
||||||
|
|
||||||
presetStr := "test=resize:fit:100:200/sharpen:2/blur"
|
presetStr := "test=resize:fit:100:200/sharpen:2/blur"
|
||||||
err := parsePreset(p, presetStr)
|
err := parsePreset(presetStr)
|
||||||
|
|
||||||
assert.Equal(s.T(), fmt.Errorf("Invalid preset value: %s", presetStr), err)
|
assert.Equal(s.T(), fmt.Errorf("Invalid preset value: %s", presetStr), err)
|
||||||
assert.Empty(s.T(), p)
|
assert.Empty(s.T(), presets)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PresetsTestSuite) TestParsePresetEmptyString() {
|
func (s *PresetsTestSuite) TestParsePresetEmptyString() {
|
||||||
p := make(presets)
|
err := parsePreset(" ")
|
||||||
|
|
||||||
err := parsePreset(p, " ")
|
|
||||||
|
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
assert.Empty(s.T(), p)
|
assert.Empty(s.T(), presets)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PresetsTestSuite) TestParsePresetComment() {
|
func (s *PresetsTestSuite) TestParsePresetComment() {
|
||||||
p := make(presets)
|
err := parsePreset("# test=resize:fit:100:200/sharpen:2")
|
||||||
|
|
||||||
err := parsePreset(p, "# test=resize:fit:100:200/sharpen:2")
|
|
||||||
|
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
assert.Empty(s.T(), p)
|
assert.Empty(s.T(), presets)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PresetsTestSuite) TestCheckPresets() {
|
func (s *PresetsTestSuite) TestValidatePresets() {
|
||||||
p := presets{
|
presets = map[string]urlOptions{
|
||||||
"test": urlOptions{
|
"test": urlOptions{
|
||||||
urlOption{Name: "resize", Args: []string{"fit", "100", "200"}},
|
urlOption{Name: "resize", Args: []string{"fit", "100", "200"}},
|
||||||
urlOption{Name: "sharpen", Args: []string{"2"}},
|
urlOption{Name: "sharpen", Args: []string{"2"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := checkPresets(p)
|
err := ValidatePresets()
|
||||||
|
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PresetsTestSuite) TestCheckPresetsInvalid() {
|
func (s *PresetsTestSuite) TestValidatePresetsInvalid() {
|
||||||
p := presets{
|
presets = map[string]urlOptions{
|
||||||
"test": urlOptions{
|
"test": urlOptions{
|
||||||
urlOption{Name: "resize", Args: []string{"fit", "-1", "-2"}},
|
urlOption{Name: "resize", Args: []string{"fit", "-1", "-2"}},
|
||||||
urlOption{Name: "sharpen", Args: []string{"2"}},
|
urlOption{Name: "sharpen", Args: []string{"2"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := checkPresets(p)
|
err := ValidatePresets()
|
||||||
|
|
||||||
assert.Error(s.T(), err)
|
assert.Error(s.T(), err)
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
596
options/processing_options_test.go
Normal file
596
options/processing_options_test.go
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProcessingOptionsTestSuite struct{ suite.Suite }
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) SetupTest() {
|
||||||
|
config.Reset()
|
||||||
|
// Reset presets
|
||||||
|
presets = make(map[string]urlOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParseBase64URL() {
|
||||||
|
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
|
||||||
|
path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
|
||||||
|
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), originURL, imageURL)
|
||||||
|
assert.Equal(s.T(), imagetype.PNG, po.Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithoutExtension() {
|
||||||
|
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
|
||||||
|
path := fmt.Sprintf("/size:100:100/%s", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
|
||||||
|
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), originURL, imageURL)
|
||||||
|
assert.Equal(s.T(), imagetype.Unknown, po.Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithBase() {
|
||||||
|
config.BaseURL = "http://images.dev/"
|
||||||
|
|
||||||
|
originURL := "lorem/ipsum.jpg?param=value"
|
||||||
|
path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
|
||||||
|
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), fmt.Sprintf("%s%s", config.BaseURL, originURL), imageURL)
|
||||||
|
assert.Equal(s.T(), imagetype.PNG, po.Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePlainURL() {
|
||||||
|
originURL := "http://images.dev/lorem/ipsum.jpg"
|
||||||
|
path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
|
||||||
|
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), originURL, imageURL)
|
||||||
|
assert.Equal(s.T(), imagetype.PNG, po.Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithoutExtension() {
|
||||||
|
originURL := "http://images.dev/lorem/ipsum.jpg"
|
||||||
|
path := fmt.Sprintf("/size:100:100/plain/%s", originURL)
|
||||||
|
|
||||||
|
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), originURL, imageURL)
|
||||||
|
assert.Equal(s.T(), imagetype.Unknown, po.Format)
|
||||||
|
}
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscaped() {
|
||||||
|
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
|
||||||
|
path := fmt.Sprintf("/size:100:100/plain/%s@png", url.PathEscape(originURL))
|
||||||
|
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), originURL, imageURL)
|
||||||
|
assert.Equal(s.T(), imagetype.PNG, po.Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithBase() {
|
||||||
|
config.BaseURL = "http://images.dev/"
|
||||||
|
|
||||||
|
originURL := "lorem/ipsum.jpg"
|
||||||
|
path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
|
||||||
|
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), fmt.Sprintf("%s%s", config.BaseURL, originURL), imageURL)
|
||||||
|
assert.Equal(s.T(), imagetype.PNG, po.Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscapedWithBase() {
|
||||||
|
config.BaseURL = "http://images.dev/"
|
||||||
|
|
||||||
|
originURL := "lorem/ipsum.jpg?param=value"
|
||||||
|
path := fmt.Sprintf("/size:100:100/plain/%s@png", url.PathEscape(originURL))
|
||||||
|
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), fmt.Sprintf("%s%s", config.BaseURL, originURL), imageURL)
|
||||||
|
assert.Equal(s.T(), imagetype.PNG, po.Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (s *ProcessingOptionsTestSuite) TestParseURLAllowedSource() {
|
||||||
|
// config.AllowedSources = []string{"local://", "http://images.dev/"}
|
||||||
|
|
||||||
|
// path := "/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
// _, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
// require.Nil(s.T(), err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (s *ProcessingOptionsTestSuite) TestParseURLNotAllowedSource() {
|
||||||
|
// config.AllowedSources = []string{"local://", "http://images.dev/"}
|
||||||
|
|
||||||
|
// path := "/plain/s3://images/lorem/ipsum.jpg"
|
||||||
|
// _, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
// require.Error(s.T(), err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathFormat() {
|
||||||
|
path := "/format:webp/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), imagetype.WEBP, po.Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathResize() {
|
||||||
|
path := "/resize:fill:100:200:1/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), ResizeFill, po.ResizingType)
|
||||||
|
assert.Equal(s.T(), 100, po.Width)
|
||||||
|
assert.Equal(s.T(), 200, po.Height)
|
||||||
|
assert.True(s.T(), po.Enlarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathResizingType() {
|
||||||
|
path := "/resizing_type:fill/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), ResizeFill, po.ResizingType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathSize() {
|
||||||
|
path := "/size:100:200:1/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 100, po.Width)
|
||||||
|
assert.Equal(s.T(), 200, po.Height)
|
||||||
|
assert.True(s.T(), po.Enlarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathWidth() {
|
||||||
|
path := "/width:100/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 100, po.Width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathHeight() {
|
||||||
|
path := "/height:100/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 100, po.Height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathEnlarge() {
|
||||||
|
path := "/enlarge:1/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.True(s.T(), po.Enlarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathExtend() {
|
||||||
|
path := "/extend:1:so:10:20/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), true, po.Extend.Enabled)
|
||||||
|
assert.Equal(s.T(), GravitySouth, po.Extend.Gravity.Type)
|
||||||
|
assert.Equal(s.T(), 10.0, po.Extend.Gravity.X)
|
||||||
|
assert.Equal(s.T(), 20.0, po.Extend.Gravity.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathGravity() {
|
||||||
|
path := "/gravity:soea/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), GravitySouthEast, po.Gravity.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathGravityFocuspoint() {
|
||||||
|
path := "/gravity:fp:0.5:0.75/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), GravityFocusPoint, po.Gravity.Type)
|
||||||
|
assert.Equal(s.T(), 0.5, po.Gravity.X)
|
||||||
|
assert.Equal(s.T(), 0.75, po.Gravity.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathQuality() {
|
||||||
|
path := "/quality:55/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 55, po.Quality)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathBackground() {
|
||||||
|
path := "/background:128:129:130/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.True(s.T(), po.Flatten)
|
||||||
|
assert.Equal(s.T(), uint8(128), po.Background.R)
|
||||||
|
assert.Equal(s.T(), uint8(129), po.Background.G)
|
||||||
|
assert.Equal(s.T(), uint8(130), po.Background.B)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundHex() {
|
||||||
|
path := "/background:ffddee/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.True(s.T(), po.Flatten)
|
||||||
|
assert.Equal(s.T(), uint8(0xff), po.Background.R)
|
||||||
|
assert.Equal(s.T(), uint8(0xdd), po.Background.G)
|
||||||
|
assert.Equal(s.T(), uint8(0xee), po.Background.B)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundDisable() {
|
||||||
|
path := "/background:fff/background:/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.False(s.T(), po.Flatten)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathBlur() {
|
||||||
|
path := "/blur:0.2/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), float32(0.2), po.Blur)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathSharpen() {
|
||||||
|
path := "/sharpen:0.2/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), float32(0.2), po.Sharpen)
|
||||||
|
}
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathDpr() {
|
||||||
|
path := "/dpr:2/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 2.0, po.Dpr)
|
||||||
|
}
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathWatermark() {
|
||||||
|
path := "/watermark:0.5:soea:10:20:0.6/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.True(s.T(), po.Watermark.Enabled)
|
||||||
|
assert.Equal(s.T(), GravitySouthEast, po.Watermark.Gravity.Type)
|
||||||
|
assert.Equal(s.T(), 10.0, po.Watermark.Gravity.X)
|
||||||
|
assert.Equal(s.T(), 20.0, po.Watermark.Gravity.Y)
|
||||||
|
assert.Equal(s.T(), 0.6, po.Watermark.Scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathPreset() {
|
||||||
|
presets["test1"] = urlOptions{
|
||||||
|
urlOption{Name: "resizing_type", Args: []string{"fill"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
presets["test2"] = urlOptions{
|
||||||
|
urlOption{Name: "blur", Args: []string{"0.2"}},
|
||||||
|
urlOption{Name: "quality", Args: []string{"50"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
path := "/preset:test1:test2/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), ResizeFill, po.ResizingType)
|
||||||
|
assert.Equal(s.T(), float32(0.2), po.Blur)
|
||||||
|
assert.Equal(s.T(), 50, po.Quality)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathPresetDefault() {
|
||||||
|
presets["default"] = urlOptions{
|
||||||
|
urlOption{Name: "resizing_type", Args: []string{"fill"}},
|
||||||
|
urlOption{Name: "blur", Args: []string{"0.2"}},
|
||||||
|
urlOption{Name: "quality", Args: []string{"50"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
path := "/quality:70/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), ResizeFill, po.ResizingType)
|
||||||
|
assert.Equal(s.T(), float32(0.2), po.Blur)
|
||||||
|
assert.Equal(s.T(), 70, po.Quality)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathPresetLoopDetection() {
|
||||||
|
presets["test1"] = urlOptions{
|
||||||
|
urlOption{Name: "resizing_type", Args: []string{"fill"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
presets["test2"] = urlOptions{
|
||||||
|
urlOption{Name: "blur", Args: []string{"0.2"}},
|
||||||
|
urlOption{Name: "quality", Args: []string{"50"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
path := "/preset:test1:test2:test1/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
require.ElementsMatch(s.T(), po.UsedPresets, []string{"test1", "test2"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathCachebuster() {
|
||||||
|
path := "/cachebuster:123/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), "123", po.CacheBuster)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathStripMetadata() {
|
||||||
|
path := "/strip_metadata:true/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.True(s.T(), po.StripMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathWebpDetection() {
|
||||||
|
config.EnableWebpDetection = true
|
||||||
|
|
||||||
|
path := "/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
headers := http.Header{"Accept": []string{"image/webp"}}
|
||||||
|
po, _, err := ParsePath(path, headers)
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), true, po.PreferWebP)
|
||||||
|
assert.Equal(s.T(), false, po.EnforceWebP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathWebpEnforce() {
|
||||||
|
config.EnforceWebp = true
|
||||||
|
|
||||||
|
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||||
|
headers := http.Header{"Accept": []string{"image/webp"}}
|
||||||
|
po, _, err := ParsePath(path, headers)
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), true, po.PreferWebP)
|
||||||
|
assert.Equal(s.T(), true, po.EnforceWebP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeader() {
|
||||||
|
config.EnableClientHints = true
|
||||||
|
|
||||||
|
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||||
|
headers := http.Header{"Width": []string{"100"}}
|
||||||
|
po, _, err := ParsePath(path, headers)
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 100, po.Width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderDisabled() {
|
||||||
|
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||||
|
headers := http.Header{"Width": []string{"100"}}
|
||||||
|
po, _, err := ParsePath(path, headers)
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 0, po.Width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderRedefine() {
|
||||||
|
config.EnableClientHints = true
|
||||||
|
|
||||||
|
path := "/width:150/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||||
|
headers := http.Header{"Width": []string{"100"}}
|
||||||
|
po, _, err := ParsePath(path, headers)
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 150, po.Width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathViewportWidthHeader() {
|
||||||
|
config.EnableClientHints = true
|
||||||
|
|
||||||
|
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||||
|
headers := http.Header{"Viewport-Width": []string{"100"}}
|
||||||
|
po, _, err := ParsePath(path, headers)
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 100, po.Width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathViewportWidthHeaderDisabled() {
|
||||||
|
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||||
|
headers := http.Header{"Viewport-Width": []string{"100"}}
|
||||||
|
po, _, err := ParsePath(path, headers)
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 0, po.Width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathViewportWidthHeaderRedefine() {
|
||||||
|
config.EnableClientHints = true
|
||||||
|
|
||||||
|
path := "/width:150/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||||
|
headers := http.Header{"Viewport-Width": []string{"100"}}
|
||||||
|
po, _, err := ParsePath(path, headers)
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 150, po.Width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathDprHeader() {
|
||||||
|
config.EnableClientHints = true
|
||||||
|
|
||||||
|
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||||
|
headers := http.Header{"Dpr": []string{"2"}}
|
||||||
|
po, _, err := ParsePath(path, headers)
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 2.0, po.Dpr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathDprHeaderDisabled() {
|
||||||
|
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||||
|
headers := http.Header{"Dpr": []string{"2"}}
|
||||||
|
po, _, err := ParsePath(path, headers)
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 1.0, po.Dpr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (s *ProcessingOptionsTestSuite) TestParsePathSigned() {
|
||||||
|
// config.Keys = [][]byte{[]byte("test-key")}
|
||||||
|
// config.Salts = [][]byte{[]byte("test-salt")}
|
||||||
|
|
||||||
|
// path := "/HcvNognEV1bW6f8zRqxNYuOkV0IUf1xloRb57CzbT4g/width:150/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||||
|
// _, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
// require.Nil(s.T(), err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (s *ProcessingOptionsTestSuite) TestParsePathSignedInvalid() {
|
||||||
|
// config.Keys = [][]byte{[]byte("test-key")}
|
||||||
|
// config.Salts = [][]byte{[]byte("test-salt")}
|
||||||
|
|
||||||
|
// path := "/unsafe/width:150/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||||
|
// _, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
// require.Error(s.T(), err)
|
||||||
|
// assert.Equal(s.T(), signature.ErrInvalidSignature.Error(), err.Error())
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathOnlyPresets() {
|
||||||
|
config.OnlyPresets = true
|
||||||
|
presets["test1"] = urlOptions{
|
||||||
|
urlOption{Name: "blur", Args: []string{"0.2"}},
|
||||||
|
}
|
||||||
|
presets["test2"] = urlOptions{
|
||||||
|
urlOption{Name: "quality", Args: []string{"50"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
path := "/test1:test2/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), float32(0.2), po.Blur)
|
||||||
|
assert.Equal(s.T(), 50, po.Quality)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParseSkipProcessing() {
|
||||||
|
path := "/skp:jpg:png/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
|
||||||
|
po, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), []imagetype.Type{imagetype.JPEG, imagetype.PNG}, po.SkipProcessingFormats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParseSkipProcessingInvalid() {
|
||||||
|
path := "/skp:jpg:png:bad_format/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
|
||||||
|
_, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Error(s.T(), err)
|
||||||
|
assert.Equal(s.T(), "Invalid image format in skip processing: bad_format", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParseExpires() {
|
||||||
|
path := "/exp:32503669200/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
_, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParseExpiresExpired() {
|
||||||
|
path := "/exp:1609448400/plain/http://images.dev/lorem/ipsum.jpg"
|
||||||
|
_, _, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Error(s.T(), err)
|
||||||
|
assert.Equal(s.T(), errExpiredURL.Error(), err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParseBase64URLOnlyPresets() {
|
||||||
|
config.OnlyPresets = true
|
||||||
|
presets["test1"] = urlOptions{
|
||||||
|
urlOption{Name: "blur", Args: []string{"0.2"}},
|
||||||
|
}
|
||||||
|
presets["test2"] = urlOptions{
|
||||||
|
urlOption{Name: "quality", Args: []string{"50"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
|
||||||
|
path := fmt.Sprintf("/test1:test2/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
|
||||||
|
|
||||||
|
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), float32(0.2), po.Blur)
|
||||||
|
assert.Equal(s.T(), 50, po.Quality)
|
||||||
|
assert.Equal(s.T(), originURL, imageURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessingOptions(t *testing.T) {
|
||||||
|
suite.Run(t, new(ProcessingOptionsTestSuite))
|
||||||
|
}
|
||||||
39
options/resize_type.go
Normal file
39
options/resize_type.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type ResizeType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ResizeFit ResizeType = iota
|
||||||
|
ResizeFill
|
||||||
|
ResizeFillDown
|
||||||
|
ResizeForce
|
||||||
|
ResizeAuto
|
||||||
|
)
|
||||||
|
|
||||||
|
var resizeTypes = map[string]ResizeType{
|
||||||
|
"fit": ResizeFit,
|
||||||
|
"fill": ResizeFill,
|
||||||
|
"fill-down": ResizeFillDown,
|
||||||
|
"force": ResizeForce,
|
||||||
|
"auto": ResizeAuto,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rt ResizeType) String() string {
|
||||||
|
for k, v := range resizeTypes {
|
||||||
|
if v == rt {
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rt ResizeType) MarshalJSON() ([]byte, error) {
|
||||||
|
for k, v := range resizeTypes {
|
||||||
|
if v == rt {
|
||||||
|
return []byte(fmt.Sprintf("%q", k)), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
81
options/url.go
Normal file
81
options/url.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
const urlTokenPlain = "plain"
|
||||||
|
|
||||||
|
func decodeBase64URL(parts []string) (string, string, error) {
|
||||||
|
var format string
|
||||||
|
|
||||||
|
encoded := strings.Join(parts, "")
|
||||||
|
urlParts := strings.Split(encoded, ".")
|
||||||
|
|
||||||
|
if len(urlParts[0]) == 0 {
|
||||||
|
return "", "", errors.New("Image URL is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(urlParts) > 2 {
|
||||||
|
return "", "", fmt.Errorf("Multiple formats are specified: %s", encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(urlParts) == 2 && len(urlParts[1]) > 0 {
|
||||||
|
format = urlParts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
imageURL, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(urlParts[0], "="))
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("Invalid url encoding: %s", encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullURL := fmt.Sprintf("%s%s", config.BaseURL, string(imageURL))
|
||||||
|
|
||||||
|
return fullURL, format, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodePlainURL(parts []string) (string, string, error) {
|
||||||
|
var format string
|
||||||
|
|
||||||
|
encoded := strings.Join(parts, "/")
|
||||||
|
urlParts := strings.Split(encoded, "@")
|
||||||
|
|
||||||
|
if len(urlParts[0]) == 0 {
|
||||||
|
return "", "", errors.New("Image URL is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(urlParts) > 2 {
|
||||||
|
return "", "", fmt.Errorf("Multiple formats are specified: %s", encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(urlParts) == 2 && len(urlParts[1]) > 0 {
|
||||||
|
format = urlParts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
unescaped, err := url.PathUnescape(urlParts[0])
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("Invalid url encoding: %s", encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullURL := fmt.Sprintf("%s%s", config.BaseURL, unescaped)
|
||||||
|
|
||||||
|
return fullURL, format, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeURL(parts []string) (string, string, error) {
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "", "", errors.New("Image URL is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts[0] == urlTokenPlain && len(parts) > 1 {
|
||||||
|
return decodePlainURL(parts[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeBase64URL(parts)
|
||||||
|
}
|
||||||
36
options/url_options.go
Normal file
36
options/url_options.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
type urlOption struct {
|
||||||
|
Name string
|
||||||
|
Args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type urlOptions []urlOption
|
||||||
|
|
||||||
|
func parseURLOptions(opts []string) (urlOptions, []string) {
|
||||||
|
parsed := make(urlOptions, 0, len(opts))
|
||||||
|
urlStart := len(opts) + 1
|
||||||
|
|
||||||
|
for i, opt := range opts {
|
||||||
|
args := strings.Split(opt, ":")
|
||||||
|
|
||||||
|
if len(args) == 1 {
|
||||||
|
urlStart = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed = append(parsed, urlOption{Name: args[0], Args: args[1:]})
|
||||||
|
}
|
||||||
|
|
||||||
|
var rest []string
|
||||||
|
|
||||||
|
if urlStart < len(opts) {
|
||||||
|
rest = opts[urlStart:]
|
||||||
|
} else {
|
||||||
|
rest = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed, rest
|
||||||
|
}
|
||||||
896
process.go
896
process.go
@@ -1,896 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v2/imagemeta"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// https://chromium.googlesource.com/webm/libwebp/+/refs/heads/master/src/webp/encode.h#529
|
|
||||||
webpMaxDimension = 16383.0
|
|
||||||
)
|
|
||||||
|
|
||||||
var errConvertingNonSvgToSvg = newError(422, "Converting non-SVG images to SVG is not supported", "Converting non-SVG images to SVG is not supported")
|
|
||||||
|
|
||||||
func imageTypeLoadSupport(imgtype imageType) bool {
|
|
||||||
return imgtype == imageTypeSVG ||
|
|
||||||
imgtype == imageTypeICO ||
|
|
||||||
vipsTypeSupportLoad[imgtype]
|
|
||||||
}
|
|
||||||
|
|
||||||
func imageTypeSaveSupport(imgtype imageType) bool {
|
|
||||||
return imgtype == imageTypeSVG || vipsTypeSupportSave[imgtype]
|
|
||||||
}
|
|
||||||
|
|
||||||
func imageTypeGoodForWeb(imgtype imageType) bool {
|
|
||||||
return imgtype != imageTypeTIFF &&
|
|
||||||
imgtype != imageTypeBMP
|
|
||||||
}
|
|
||||||
|
|
||||||
func canSwitchFormat(src, dst, want imageType) bool {
|
|
||||||
return imageTypeSaveSupport(want) &&
|
|
||||||
(!vipsSupportAnimation(src) ||
|
|
||||||
(dst != imageTypeUnknown && !vipsSupportAnimation(dst)) ||
|
|
||||||
vipsSupportAnimation(want))
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractMeta(img *vipsImage, baseAngle int, useOrientation bool) (int, int, int, bool) {
|
|
||||||
width := img.Width()
|
|
||||||
height := img.Height()
|
|
||||||
|
|
||||||
angle := 0
|
|
||||||
flip := false
|
|
||||||
|
|
||||||
if useOrientation {
|
|
||||||
orientation := img.Orientation()
|
|
||||||
|
|
||||||
if orientation == 3 || orientation == 4 {
|
|
||||||
angle = 180
|
|
||||||
}
|
|
||||||
if orientation == 5 || orientation == 6 {
|
|
||||||
angle = 90
|
|
||||||
}
|
|
||||||
if orientation == 7 || orientation == 8 {
|
|
||||||
angle = 270
|
|
||||||
}
|
|
||||||
if orientation == 2 || orientation == 4 || orientation == 5 || orientation == 7 {
|
|
||||||
flip = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (angle+baseAngle)%180 != 0 {
|
|
||||||
width, height = height, width
|
|
||||||
}
|
|
||||||
|
|
||||||
return width, height, angle, flip
|
|
||||||
}
|
|
||||||
|
|
||||||
func calcScale(width, height int, po *processingOptions, imgtype imageType) (float64, float64) {
|
|
||||||
var wshrink, hshrink float64
|
|
||||||
|
|
||||||
srcW, srcH := float64(width), float64(height)
|
|
||||||
dstW, dstH := float64(po.Width), float64(po.Height)
|
|
||||||
|
|
||||||
if po.Width == 0 {
|
|
||||||
dstW = srcW
|
|
||||||
}
|
|
||||||
|
|
||||||
if dstW == srcW {
|
|
||||||
wshrink = 1
|
|
||||||
} else {
|
|
||||||
wshrink = srcW / dstW
|
|
||||||
}
|
|
||||||
|
|
||||||
if po.Height == 0 {
|
|
||||||
dstH = srcH
|
|
||||||
}
|
|
||||||
|
|
||||||
if dstH == srcH {
|
|
||||||
hshrink = 1
|
|
||||||
} else {
|
|
||||||
hshrink = srcH / dstH
|
|
||||||
}
|
|
||||||
|
|
||||||
if wshrink != 1 || hshrink != 1 {
|
|
||||||
rt := po.ResizingType
|
|
||||||
|
|
||||||
if rt == resizeAuto {
|
|
||||||
srcD := srcW - srcH
|
|
||||||
dstD := dstW - dstH
|
|
||||||
|
|
||||||
if (srcD >= 0 && dstD >= 0) || (srcD < 0 && dstD < 0) {
|
|
||||||
rt = resizeFill
|
|
||||||
} else {
|
|
||||||
rt = resizeFit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case po.Width == 0 && rt != resizeForce:
|
|
||||||
wshrink = hshrink
|
|
||||||
case po.Height == 0 && rt != resizeForce:
|
|
||||||
hshrink = wshrink
|
|
||||||
case rt == resizeFit:
|
|
||||||
wshrink = math.Max(wshrink, hshrink)
|
|
||||||
hshrink = wshrink
|
|
||||||
case rt == resizeFill || rt == resizeFillDown:
|
|
||||||
wshrink = math.Min(wshrink, hshrink)
|
|
||||||
hshrink = wshrink
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !po.Enlarge && imgtype != imageTypeSVG {
|
|
||||||
if wshrink < 1 {
|
|
||||||
hshrink /= wshrink
|
|
||||||
wshrink = 1
|
|
||||||
}
|
|
||||||
if hshrink < 1 {
|
|
||||||
wshrink /= hshrink
|
|
||||||
hshrink = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if po.MinWidth > 0 {
|
|
||||||
if minShrink := srcW / float64(po.MinWidth); minShrink < wshrink {
|
|
||||||
hshrink /= wshrink / minShrink
|
|
||||||
wshrink = minShrink
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if po.MinHeight > 0 {
|
|
||||||
if minShrink := srcH / float64(po.MinHeight); minShrink < hshrink {
|
|
||||||
wshrink /= hshrink / minShrink
|
|
||||||
hshrink = minShrink
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wshrink /= po.Dpr
|
|
||||||
hshrink /= po.Dpr
|
|
||||||
|
|
||||||
if wshrink > srcW {
|
|
||||||
wshrink = srcW
|
|
||||||
}
|
|
||||||
|
|
||||||
if hshrink > srcH {
|
|
||||||
hshrink = srcH
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1.0 / wshrink, 1.0 / hshrink
|
|
||||||
}
|
|
||||||
|
|
||||||
func canScaleOnLoad(imgtype imageType, scale float64) bool {
|
|
||||||
if imgtype == imageTypeSVG {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.DisableShrinkOnLoad || scale >= 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return imgtype == imageTypeJPEG || imgtype == imageTypeWEBP
|
|
||||||
}
|
|
||||||
|
|
||||||
func canFitToBytes(imgtype imageType) bool {
|
|
||||||
switch imgtype {
|
|
||||||
case imageTypeJPEG, imageTypeWEBP, imageTypeAVIF, imageTypeTIFF:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func calcJpegShink(scale float64, imgtype imageType) int {
|
|
||||||
shrink := int(1.0 / scale)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case shrink >= 8:
|
|
||||||
return 8
|
|
||||||
case shrink >= 4:
|
|
||||||
return 4
|
|
||||||
case shrink >= 2:
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func calcCropSize(orig int, crop float64) int {
|
|
||||||
switch {
|
|
||||||
case crop == 0.0:
|
|
||||||
return 0
|
|
||||||
case crop >= 1.0:
|
|
||||||
return int(crop)
|
|
||||||
default:
|
|
||||||
return maxInt(1, scaleInt(orig, crop))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func calcPosition(width, height, innerWidth, innerHeight int, gravity *gravityOptions, allowOverflow bool) (left, top int) {
|
|
||||||
if gravity.Type == gravityFocusPoint {
|
|
||||||
pointX := scaleInt(width, gravity.X)
|
|
||||||
pointY := scaleInt(height, gravity.Y)
|
|
||||||
|
|
||||||
left = pointX - innerWidth/2
|
|
||||||
top = pointY - innerHeight/2
|
|
||||||
} else {
|
|
||||||
offX, offY := int(gravity.X), int(gravity.Y)
|
|
||||||
|
|
||||||
left = (width-innerWidth+1)/2 + offX
|
|
||||||
top = (height-innerHeight+1)/2 + offY
|
|
||||||
|
|
||||||
if gravity.Type == gravityNorth || gravity.Type == gravityNorthEast || gravity.Type == gravityNorthWest {
|
|
||||||
top = 0 + offY
|
|
||||||
}
|
|
||||||
|
|
||||||
if gravity.Type == gravityEast || gravity.Type == gravityNorthEast || gravity.Type == gravitySouthEast {
|
|
||||||
left = width - innerWidth - offX
|
|
||||||
}
|
|
||||||
|
|
||||||
if gravity.Type == gravitySouth || gravity.Type == gravitySouthEast || gravity.Type == gravitySouthWest {
|
|
||||||
top = height - innerHeight - offY
|
|
||||||
}
|
|
||||||
|
|
||||||
if gravity.Type == gravityWest || gravity.Type == gravityNorthWest || gravity.Type == gravitySouthWest {
|
|
||||||
left = 0 + offX
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var minX, maxX, minY, maxY int
|
|
||||||
|
|
||||||
if allowOverflow {
|
|
||||||
minX, maxX = -innerWidth+1, width-1
|
|
||||||
minY, maxY = -innerHeight+1, height-1
|
|
||||||
} else {
|
|
||||||
minX, maxX = 0, width-innerWidth
|
|
||||||
minY, maxY = 0, height-innerHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
left = maxInt(minX, minInt(left, maxX))
|
|
||||||
top = maxInt(minY, minInt(top, maxY))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func cropImage(img *vipsImage, cropWidth, cropHeight int, gravity *gravityOptions) error {
|
|
||||||
if cropWidth == 0 && cropHeight == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
imgWidth, imgHeight := img.Width(), img.Height()
|
|
||||||
|
|
||||||
cropWidth = minNonZeroInt(cropWidth, imgWidth)
|
|
||||||
cropHeight = minNonZeroInt(cropHeight, imgHeight)
|
|
||||||
|
|
||||||
if cropWidth >= imgWidth && cropHeight >= imgHeight {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if gravity.Type == gravitySmart {
|
|
||||||
if err := img.CopyMemory(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := img.SmartCrop(cropWidth, cropHeight); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Applying additional modifications after smart crop causes SIGSEGV on Alpine
|
|
||||||
// so we have to copy memory after it
|
|
||||||
return img.CopyMemory()
|
|
||||||
}
|
|
||||||
|
|
||||||
left, top := calcPosition(imgWidth, imgHeight, cropWidth, cropHeight, gravity, false)
|
|
||||||
return img.Crop(left, top, cropWidth, cropHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareWatermark(wm *vipsImage, wmData *imageData, opts *watermarkOptions, imgWidth, imgHeight int) error {
|
|
||||||
if err := wm.Load(wmData.Data, wmData.Type, 1, 1.0, 1); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
po := newProcessingOptions()
|
|
||||||
po.ResizingType = resizeFit
|
|
||||||
po.Dpr = 1
|
|
||||||
po.Enlarge = true
|
|
||||||
po.Format = wmData.Type
|
|
||||||
|
|
||||||
if opts.Scale > 0 {
|
|
||||||
po.Width = maxInt(scaleInt(imgWidth, opts.Scale), 1)
|
|
||||||
po.Height = maxInt(scaleInt(imgHeight, opts.Scale), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := transformImage(context.Background(), wm, wmData.Data, po, wmData.Type); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := wm.EnsureAlpha(); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.Replicate {
|
|
||||||
return wm.Replicate(imgWidth, imgHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
left, top := calcPosition(imgWidth, imgHeight, wm.Width(), wm.Height(), &opts.Gravity, true)
|
|
||||||
|
|
||||||
return wm.Embed(imgWidth, imgHeight, left, top, rgbColor{0, 0, 0}, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyWatermark(img *vipsImage, wmData *imageData, opts *watermarkOptions, framesCount int) error {
|
|
||||||
if err := img.RgbColourspace(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := img.CopyMemory(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
wm := new(vipsImage)
|
|
||||||
defer wm.Clear()
|
|
||||||
|
|
||||||
width := img.Width()
|
|
||||||
height := img.Height()
|
|
||||||
|
|
||||||
if err := prepareWatermark(wm, wmData, opts, width, height/framesCount); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if framesCount > 1 {
|
|
||||||
if err := wm.Replicate(width, height); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
opacity := opts.Opacity * conf.WatermarkOpacity
|
|
||||||
|
|
||||||
return img.ApplyWatermark(wm, opacity)
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyMemoryAndCheckTimeout(ctx context.Context, img *vipsImage) error {
|
|
||||||
err := img.CopyMemory()
|
|
||||||
checkTimeout(ctx)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func transformImage(ctx context.Context, img *vipsImage, data []byte, po *processingOptions, imgtype imageType) error {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
trimmed bool
|
|
||||||
)
|
|
||||||
|
|
||||||
if po.Trim.Enabled {
|
|
||||||
if err = img.Trim(po.Trim.Threshold, po.Trim.Smart, po.Trim.Color, po.Trim.EqualHor, po.Trim.EqualVer); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
trimmed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
srcWidth, srcHeight, angle, flip := extractMeta(img, po.Rotate, po.AutoRotate)
|
|
||||||
|
|
||||||
cropWidth := calcCropSize(srcWidth, po.Crop.Width)
|
|
||||||
cropHeight := calcCropSize(srcHeight, po.Crop.Height)
|
|
||||||
|
|
||||||
cropGravity := po.Crop.Gravity
|
|
||||||
if cropGravity.Type == gravityUnknown {
|
|
||||||
cropGravity = po.Gravity
|
|
||||||
}
|
|
||||||
|
|
||||||
widthToScale := minNonZeroInt(cropWidth, srcWidth)
|
|
||||||
heightToScale := minNonZeroInt(cropHeight, srcHeight)
|
|
||||||
|
|
||||||
wscale, hscale := calcScale(widthToScale, heightToScale, po, imgtype)
|
|
||||||
|
|
||||||
if cropWidth > 0 {
|
|
||||||
cropWidth = maxInt(1, scaleInt(cropWidth, wscale))
|
|
||||||
}
|
|
||||||
if cropHeight > 0 {
|
|
||||||
cropHeight = maxInt(1, scaleInt(cropHeight, hscale))
|
|
||||||
}
|
|
||||||
if cropGravity.Type != gravityFocusPoint {
|
|
||||||
cropGravity.X *= wscale
|
|
||||||
cropGravity.Y *= hscale
|
|
||||||
}
|
|
||||||
|
|
||||||
prescale := math.Max(wscale, hscale)
|
|
||||||
|
|
||||||
if !trimmed && prescale != 1 && data != nil && canScaleOnLoad(imgtype, prescale) {
|
|
||||||
jpegShrink := calcJpegShink(prescale, imgtype)
|
|
||||||
|
|
||||||
if imgtype != imageTypeJPEG || jpegShrink != 1 {
|
|
||||||
// Do some scale-on-load
|
|
||||||
if err = img.Load(data, imgtype, jpegShrink, prescale, 1); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update scales after scale-on-load
|
|
||||||
newWidth, newHeight, _, _ := extractMeta(img, po.Rotate, po.AutoRotate)
|
|
||||||
|
|
||||||
wscale = float64(srcWidth) * wscale / float64(newWidth)
|
|
||||||
if srcWidth == scaleInt(srcWidth, wscale) {
|
|
||||||
wscale = 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
hscale = float64(srcHeight) * hscale / float64(newHeight)
|
|
||||||
if srcHeight == scaleInt(srcHeight, hscale) {
|
|
||||||
hscale = 1.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = img.Rad2Float(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
iccImported := false
|
|
||||||
convertToLinear := conf.UseLinearColorspace && (wscale != 1 || hscale != 1)
|
|
||||||
|
|
||||||
if convertToLinear || !img.IsSRGB() {
|
|
||||||
if err = img.ImportColourProfile(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
iccImported = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if convertToLinear {
|
|
||||||
if err = img.LinearColourspace(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err = img.RgbColourspace(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hasAlpha := img.HasAlpha()
|
|
||||||
|
|
||||||
if wscale != 1 || hscale != 1 {
|
|
||||||
if err = img.Resize(wscale, hscale, hasAlpha); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = img.Rotate(angle); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if flip {
|
|
||||||
if err = img.Flip(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = img.Rotate(po.Rotate); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = cropImage(img, cropWidth, cropHeight, &cropGravity); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crop image to the result size
|
|
||||||
resultWidth := scaleInt(po.Width, po.Dpr)
|
|
||||||
resultHeight := scaleInt(po.Height, po.Dpr)
|
|
||||||
|
|
||||||
if po.ResizingType == resizeFillDown {
|
|
||||||
if resultWidth > img.Width() {
|
|
||||||
resultHeight = scaleInt(resultHeight, float64(img.Width())/float64(resultWidth))
|
|
||||||
resultWidth = img.Width()
|
|
||||||
}
|
|
||||||
|
|
||||||
if resultHeight > img.Height() {
|
|
||||||
resultWidth = scaleInt(resultWidth, float64(img.Height())/float64(resultHeight))
|
|
||||||
resultHeight = img.Height()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = cropImage(img, resultWidth, resultHeight, &po.Gravity); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if po.Format == imageTypeWEBP {
|
|
||||||
webpLimitShrink := float64(maxInt(img.Width(), img.Height())) / webpMaxDimension
|
|
||||||
|
|
||||||
if webpLimitShrink > 1.0 {
|
|
||||||
scale := 1.0 / webpLimitShrink
|
|
||||||
if err = img.Resize(scale, scale, hasAlpha); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logWarning("WebP dimension size is limited to %d. The image is rescaled to %dx%d", int(webpMaxDimension), img.Width(), img.Height())
|
|
||||||
|
|
||||||
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keepProfile := !po.StripColorProfile && po.Format.SupportsColourProfile()
|
|
||||||
|
|
||||||
if iccImported {
|
|
||||||
if keepProfile {
|
|
||||||
// We imported ICC profile and want to keep it,
|
|
||||||
// so we need to export it
|
|
||||||
if err = img.ExportColourProfile(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// We imported ICC profile but don't want to keep it,
|
|
||||||
// so we need to export image to sRGB for maximum compatibility
|
|
||||||
if err = img.ExportColourProfileToSRGB(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if !keepProfile {
|
|
||||||
// We don't import ICC profile and don't want to keep it,
|
|
||||||
// so we need to transform it to sRGB for maximum compatibility
|
|
||||||
if err = img.TransformColourProfile(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = img.RgbColourspace(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !keepProfile {
|
|
||||||
if err = img.RemoveColourProfile(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transparentBg := po.Format.SupportsAlpha() && !po.Flatten
|
|
||||||
|
|
||||||
if hasAlpha && !transparentBg {
|
|
||||||
if err = img.Flatten(po.Background); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if po.Blur > 0 {
|
|
||||||
if err = img.Blur(po.Blur); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if po.Sharpen > 0 {
|
|
||||||
if err = img.Sharpen(po.Sharpen); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if po.Extend.Enabled && (resultWidth > img.Width() || resultHeight > img.Height()) {
|
|
||||||
offX, offY := calcPosition(resultWidth, resultHeight, img.Width(), img.Height(), &po.Extend.Gravity, false)
|
|
||||||
if err = img.Embed(resultWidth, resultHeight, offX, offY, po.Background, transparentBg); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if po.Padding.Enabled {
|
|
||||||
paddingTop := scaleInt(po.Padding.Top, po.Dpr)
|
|
||||||
paddingRight := scaleInt(po.Padding.Right, po.Dpr)
|
|
||||||
paddingBottom := scaleInt(po.Padding.Bottom, po.Dpr)
|
|
||||||
paddingLeft := scaleInt(po.Padding.Left, po.Dpr)
|
|
||||||
if err = img.Embed(
|
|
||||||
img.Width()+paddingLeft+paddingRight,
|
|
||||||
img.Height()+paddingTop+paddingBottom,
|
|
||||||
paddingLeft,
|
|
||||||
paddingTop,
|
|
||||||
po.Background,
|
|
||||||
transparentBg,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if po.Watermark.Enabled && watermark != nil {
|
|
||||||
if err = applyWatermark(img, watermark, &po.Watermark, 1); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = img.RgbColourspace(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := img.CastUchar(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if po.StripMetadata {
|
|
||||||
if err := img.Strip(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return copyMemoryAndCheckTimeout(ctx, img)
|
|
||||||
}
|
|
||||||
|
|
||||||
func transformAnimated(ctx context.Context, img *vipsImage, data []byte, po *processingOptions, imgtype imageType) error {
|
|
||||||
if po.Trim.Enabled {
|
|
||||||
logWarning("Trim is not supported for animated images")
|
|
||||||
po.Trim.Enabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
imgWidth := img.Width()
|
|
||||||
|
|
||||||
frameHeight, err := img.GetInt("page-height")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
framesCount := minInt(img.Height()/frameHeight, conf.MaxAnimationFrames)
|
|
||||||
|
|
||||||
// Double check dimensions because animated image has many frames
|
|
||||||
if err = checkDimensions(imgWidth, frameHeight*framesCount); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vips 8.8+ supports n-pages and doesn't load the whole animated image on header access
|
|
||||||
if nPages, _ := img.GetIntDefault("n-pages", 0); nPages > framesCount {
|
|
||||||
// Load only the needed frames
|
|
||||||
if err = img.Load(data, imgtype, 1, 1.0, framesCount); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delay, err := img.GetIntSliceDefault("delay", nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
loop, err := img.GetIntDefault("loop", 0)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy fields
|
|
||||||
// TODO: remove this in major update
|
|
||||||
gifLoop, err := img.GetIntDefault("gif-loop", -1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
gifDelay, err := img.GetIntDefault("gif-delay", -1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
watermarkEnabled := po.Watermark.Enabled
|
|
||||||
po.Watermark.Enabled = false
|
|
||||||
defer func() { po.Watermark.Enabled = watermarkEnabled }()
|
|
||||||
|
|
||||||
frames := make([]*vipsImage, framesCount)
|
|
||||||
defer func() {
|
|
||||||
for _, frame := range frames {
|
|
||||||
if frame != nil {
|
|
||||||
frame.Clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for i := 0; i < framesCount; i++ {
|
|
||||||
frame := new(vipsImage)
|
|
||||||
|
|
||||||
if err = img.Extract(frame, 0, i*frameHeight, imgWidth, frameHeight); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
frames[i] = frame
|
|
||||||
|
|
||||||
if err = transformImage(ctx, frame, nil, po, imgtype); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = copyMemoryAndCheckTimeout(ctx, frame); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = img.Arrayjoin(frames); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if watermarkEnabled && watermark != nil {
|
|
||||||
if err = applyWatermark(img, watermark, &po.Watermark, framesCount); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = img.CastUchar(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(delay) == 0 {
|
|
||||||
delay = make([]int, framesCount)
|
|
||||||
for i := range delay {
|
|
||||||
delay[i] = 40
|
|
||||||
}
|
|
||||||
} else if len(delay) > framesCount {
|
|
||||||
delay = delay[:framesCount]
|
|
||||||
}
|
|
||||||
|
|
||||||
img.SetInt("page-height", frames[0].Height())
|
|
||||||
img.SetIntSlice("delay", delay)
|
|
||||||
img.SetInt("loop", loop)
|
|
||||||
img.SetInt("n-pages", framesCount)
|
|
||||||
|
|
||||||
// Legacy fields
|
|
||||||
// TODO: remove this in major update
|
|
||||||
if gifLoop >= 0 {
|
|
||||||
img.SetInt("gif-loop", gifLoop)
|
|
||||||
}
|
|
||||||
if gifDelay >= 0 {
|
|
||||||
img.SetInt("gif-delay", gifDelay)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIcoData(imgdata *imageData) (*imageData, error) {
|
|
||||||
icoMeta, err := imagemeta.DecodeIcoMeta(bytes.NewReader(imgdata.Data))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
offset := icoMeta.BestImageOffset()
|
|
||||||
size := icoMeta.BestImageSize()
|
|
||||||
|
|
||||||
data := imgdata.Data[offset : offset+size]
|
|
||||||
|
|
||||||
var format string
|
|
||||||
|
|
||||||
meta, err := imagemeta.DecodeMeta(bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
// Looks like it's BMP with an incomplete header
|
|
||||||
if d, err := imagemeta.FixBmpHeader(data); err == nil {
|
|
||||||
format = "bmp"
|
|
||||||
data = d
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
format = meta.Format()
|
|
||||||
}
|
|
||||||
|
|
||||||
if imgtype, ok := imageTypes[format]; ok && vipsTypeSupportLoad[imgtype] {
|
|
||||||
return &imageData{
|
|
||||||
Data: data,
|
|
||||||
Type: imgtype,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("Can't load %s from ICO", meta.Format())
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveImageToFitBytes(ctx context.Context, po *processingOptions, img *vipsImage) ([]byte, context.CancelFunc, error) {
|
|
||||||
var diff float64
|
|
||||||
quality := po.getQuality()
|
|
||||||
|
|
||||||
for {
|
|
||||||
result, cancel, err := img.Save(po.Format, quality)
|
|
||||||
if len(result) <= po.MaxBytes || quality <= 10 || err != nil {
|
|
||||||
return result, cancel, err
|
|
||||||
}
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
checkTimeout(ctx)
|
|
||||||
|
|
||||||
delta := float64(len(result)) / float64(po.MaxBytes)
|
|
||||||
switch {
|
|
||||||
case delta > 3:
|
|
||||||
diff = 0.25
|
|
||||||
case delta > 1.5:
|
|
||||||
diff = 0.5
|
|
||||||
default:
|
|
||||||
diff = 0.75
|
|
||||||
}
|
|
||||||
quality = int(float64(quality) * diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func processImage(ctx context.Context) ([]byte, context.CancelFunc, error) {
|
|
||||||
runtime.LockOSThread()
|
|
||||||
defer runtime.UnlockOSThread()
|
|
||||||
|
|
||||||
defer startDataDogSpan(ctx, "processing_image")()
|
|
||||||
defer startNewRelicSegment(ctx, "Processing image")()
|
|
||||||
defer startPrometheusDuration(prometheusProcessingDuration)()
|
|
||||||
|
|
||||||
defer vipsCleanup()
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
imgdata := getImageData(ctx)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case po.Format == imageTypeUnknown:
|
|
||||||
switch {
|
|
||||||
case po.PreferAvif && canSwitchFormat(imgdata.Type, imageTypeUnknown, imageTypeAVIF):
|
|
||||||
po.Format = imageTypeAVIF
|
|
||||||
case po.PreferWebP && canSwitchFormat(imgdata.Type, imageTypeUnknown, imageTypeWEBP):
|
|
||||||
po.Format = imageTypeWEBP
|
|
||||||
case imageTypeSaveSupport(imgdata.Type) && imageTypeGoodForWeb(imgdata.Type):
|
|
||||||
po.Format = imgdata.Type
|
|
||||||
default:
|
|
||||||
po.Format = imageTypeJPEG
|
|
||||||
}
|
|
||||||
case po.EnforceAvif && canSwitchFormat(imgdata.Type, po.Format, imageTypeAVIF):
|
|
||||||
po.Format = imageTypeAVIF
|
|
||||||
case po.EnforceWebP && canSwitchFormat(imgdata.Type, po.Format, imageTypeWEBP):
|
|
||||||
po.Format = imageTypeWEBP
|
|
||||||
}
|
|
||||||
|
|
||||||
if po.Format == imageTypeSVG {
|
|
||||||
if imgdata.Type != imageTypeSVG {
|
|
||||||
return []byte{}, func() {}, errConvertingNonSvgToSvg
|
|
||||||
}
|
|
||||||
|
|
||||||
return imgdata.Data, func() {}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if imgdata.Type == imageTypeSVG && !vipsTypeSupportLoad[imageTypeSVG] {
|
|
||||||
return []byte{}, func() {}, errSourceImageTypeNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
if imgdata.Type == imageTypeICO {
|
|
||||||
icodata, err := getIcoData(imgdata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, func() {}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
imgdata = icodata
|
|
||||||
}
|
|
||||||
|
|
||||||
animationSupport := conf.MaxAnimationFrames > 1 && vipsSupportAnimation(imgdata.Type) && vipsSupportAnimation(po.Format)
|
|
||||||
|
|
||||||
pages := 1
|
|
||||||
if animationSupport {
|
|
||||||
pages = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
img := new(vipsImage)
|
|
||||||
defer img.Clear()
|
|
||||||
|
|
||||||
if err := img.Load(imgdata.Data, imgdata.Type, 1, 1.0, pages); err != nil {
|
|
||||||
return nil, func() {}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if animationSupport && img.IsAnimated() {
|
|
||||||
if err := transformAnimated(ctx, img, imgdata.Data, po, imgdata.Type); err != nil {
|
|
||||||
return nil, func() {}, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := transformImage(ctx, img, imgdata.Data, po, imgdata.Type); err != nil {
|
|
||||||
return nil, func() {}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
|
||||||
return nil, func() {}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if po.MaxBytes > 0 && canFitToBytes(po.Format) {
|
|
||||||
return saveImageToFitBytes(ctx, po, img)
|
|
||||||
}
|
|
||||||
|
|
||||||
return img.Save(po.Format, po.getQuality())
|
|
||||||
}
|
|
||||||
48
processing/apply_filters.go
Normal file
48
processing/apply_filters.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func applyFilters(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
if po.Blur == 0 && po.Sharpen == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := copyMemoryAndCheckTimeout(pctx.ctx, img); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := img.RgbColourspace(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// When image has alpha, we need to premultiply it to get rid of black edges
|
||||||
|
if err := img.Premultiply(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if po.Blur > 0 {
|
||||||
|
if err := img.Blur(po.Blur); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if po.Sharpen > 0 {
|
||||||
|
if err := img.Sharpen(po.Sharpen); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := img.Unpremultiply(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := img.CastUchar(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return copyMemoryAndCheckTimeout(pctx.ctx, img)
|
||||||
|
}
|
||||||
52
processing/calc_position.go
Normal file
52
processing/calc_position.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imath"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
func calcPosition(width, height, innerWidth, innerHeight int, gravity *options.GravityOptions, allowOverflow bool) (left, top int) {
|
||||||
|
if gravity.Type == options.GravityFocusPoint {
|
||||||
|
pointX := imath.Scale(width, gravity.X)
|
||||||
|
pointY := imath.Scale(height, gravity.Y)
|
||||||
|
|
||||||
|
left = pointX - innerWidth/2
|
||||||
|
top = pointY - innerHeight/2
|
||||||
|
} else {
|
||||||
|
offX, offY := int(gravity.X), int(gravity.Y)
|
||||||
|
|
||||||
|
left = (width-innerWidth+1)/2 + offX
|
||||||
|
top = (height-innerHeight+1)/2 + offY
|
||||||
|
|
||||||
|
if gravity.Type == options.GravityNorth || gravity.Type == options.GravityNorthEast || gravity.Type == options.GravityNorthWest {
|
||||||
|
top = 0 + offY
|
||||||
|
}
|
||||||
|
|
||||||
|
if gravity.Type == options.GravityEast || gravity.Type == options.GravityNorthEast || gravity.Type == options.GravitySouthEast {
|
||||||
|
left = width - innerWidth - offX
|
||||||
|
}
|
||||||
|
|
||||||
|
if gravity.Type == options.GravitySouth || gravity.Type == options.GravitySouthEast || gravity.Type == options.GravitySouthWest {
|
||||||
|
top = height - innerHeight - offY
|
||||||
|
}
|
||||||
|
|
||||||
|
if gravity.Type == options.GravityWest || gravity.Type == options.GravityNorthWest || gravity.Type == options.GravitySouthWest {
|
||||||
|
left = 0 + offX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var minX, maxX, minY, maxY int
|
||||||
|
|
||||||
|
if allowOverflow {
|
||||||
|
minX, maxX = -innerWidth+1, width-1
|
||||||
|
minY, maxY = -innerHeight+1, height-1
|
||||||
|
} else {
|
||||||
|
minX, maxX = 0, width-innerWidth
|
||||||
|
minY, maxY = 0, height-innerHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
left = imath.Max(minX, imath.Min(left, maxX))
|
||||||
|
top = imath.Max(minY, imath.Min(top, maxY))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
14
processing/copy_and_check_timeout.go
Normal file
14
processing/copy_and_check_timeout.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/router"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func copyMemoryAndCheckTimeout(ctx context.Context, img *vips.Image) error {
|
||||||
|
err := img.CopyMemory()
|
||||||
|
router.CheckTimeout(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
62
processing/crop.go
Normal file
62
processing/crop.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imath"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func cropImage(img *vips.Image, cropWidth, cropHeight int, gravity *options.GravityOptions) error {
|
||||||
|
if cropWidth == 0 && cropHeight == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
imgWidth, imgHeight := img.Width(), img.Height()
|
||||||
|
|
||||||
|
cropWidth = imath.MinNonZero(cropWidth, imgWidth)
|
||||||
|
cropHeight = imath.MinNonZero(cropHeight, imgHeight)
|
||||||
|
|
||||||
|
if cropWidth >= imgWidth && cropHeight >= imgHeight {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if gravity.Type == options.GravitySmart {
|
||||||
|
if err := img.CopyMemory(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := img.SmartCrop(cropWidth, cropHeight); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Applying additional modifications after smart crop causes SIGSEGV on Alpine
|
||||||
|
// so we have to copy memory after it
|
||||||
|
return img.CopyMemory()
|
||||||
|
}
|
||||||
|
|
||||||
|
left, top := calcPosition(imgWidth, imgHeight, cropWidth, cropHeight, gravity, false)
|
||||||
|
return img.Crop(left, top, cropWidth, cropHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
func crop(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
if err := cropImage(img, pctx.cropWidth, pctx.cropHeight, &pctx.cropGravity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crop image to the result size
|
||||||
|
resultWidth := imath.Scale(po.Width, po.Dpr)
|
||||||
|
resultHeight := imath.Scale(po.Height, po.Dpr)
|
||||||
|
|
||||||
|
if po.ResizingType == options.ResizeFillDown {
|
||||||
|
if resultWidth > img.Width() {
|
||||||
|
resultHeight = imath.Scale(resultHeight, float64(img.Width())/float64(resultWidth))
|
||||||
|
resultWidth = img.Width()
|
||||||
|
}
|
||||||
|
|
||||||
|
if resultHeight > img.Height() {
|
||||||
|
resultWidth = imath.Scale(resultWidth, float64(img.Height())/float64(resultHeight))
|
||||||
|
resultHeight = img.Height()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cropImage(img, resultWidth, resultHeight, &po.Gravity)
|
||||||
|
}
|
||||||
43
processing/export_color_profile.go
Normal file
43
processing/export_color_profile.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func exportColorProfile(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
keepProfile := !po.StripColorProfile && po.Format.SupportsColourProfile()
|
||||||
|
|
||||||
|
if pctx.iccImported {
|
||||||
|
if keepProfile {
|
||||||
|
// We imported ICC profile and want to keep it,
|
||||||
|
// so we need to export it
|
||||||
|
if err := img.ExportColourProfile(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We imported ICC profile but don't want to keep it,
|
||||||
|
// so we need to export image to sRGB for maximum compatibility
|
||||||
|
if err := img.ExportColourProfileToSRGB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !keepProfile {
|
||||||
|
// We don't import ICC profile and don't want to keep it,
|
||||||
|
// so we need to transform it to sRGB for maximum compatibility
|
||||||
|
if err := img.TransformColourProfile(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := img.RgbColourspace(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !keepProfile {
|
||||||
|
return img.RemoveColourProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
20
processing/extend.go
Normal file
20
processing/extend.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imath"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func extend(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
resultWidth := imath.Scale(po.Width, po.Dpr)
|
||||||
|
resultHeight := imath.Scale(po.Height, po.Dpr)
|
||||||
|
|
||||||
|
if !po.Extend.Enabled || (resultWidth <= img.Width() && resultHeight <= img.Height()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
offX, offY := calcPosition(resultWidth, resultHeight, img.Width(), img.Height(), &po.Extend.Gravity, false)
|
||||||
|
return img.Embed(resultWidth, resultHeight, offX, offY)
|
||||||
|
}
|
||||||
25
processing/finalize.go
Normal file
25
processing/finalize.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func finalize(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
if err := img.RgbColourspace(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := img.CastUchar(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if po.StripMetadata {
|
||||||
|
if err := img.Strip(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return copyMemoryAndCheckTimeout(pctx.ctx, img)
|
||||||
|
}
|
||||||
33
processing/fix_webp_size.go
Normal file
33
processing/fix_webp_size.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imath"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://chromium.googlesource.com/webm/libwebp/+/refs/heads/master/src/webp/encode.h#529
|
||||||
|
const webpMaxDimension = 16383.0
|
||||||
|
|
||||||
|
func fixWebpSize(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
if po.Format != imagetype.WEBP {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
webpLimitShrink := float64(imath.Max(img.Width(), img.Height())) / webpMaxDimension
|
||||||
|
|
||||||
|
if webpLimitShrink <= 1.0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scale := 1.0 / webpLimitShrink
|
||||||
|
if err := img.Resize(scale, scale); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warningf("WebP dimension size is limited to %d. The image is rescaled to %dx%d", int(webpMaxDimension), img.Width(), img.Height())
|
||||||
|
|
||||||
|
return copyMemoryAndCheckTimeout(pctx.ctx, img)
|
||||||
|
}
|
||||||
15
processing/flatten.go
Normal file
15
processing/flatten.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func flatten(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
if !po.Flatten && po.Format.SupportsAlpha() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return img.Flatten(po.Background)
|
||||||
|
}
|
||||||
29
processing/import_color_profile.go
Normal file
29
processing/import_color_profile.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func importColorProfile(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
if err := img.Rad2Float(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
convertToLinear := config.UseLinearColorspace && (pctx.wscale != 1 || pctx.hscale != 1)
|
||||||
|
|
||||||
|
if convertToLinear || !img.IsSRGB() {
|
||||||
|
if err := img.ImportColourProfile(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pctx.iccImported = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if convertToLinear {
|
||||||
|
return img.LinearColourspace()
|
||||||
|
}
|
||||||
|
|
||||||
|
return img.RgbColourspace()
|
||||||
|
}
|
||||||
26
processing/padding.go
Normal file
26
processing/padding.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imath"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func padding(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
if !po.Padding.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
paddingTop := imath.Scale(po.Padding.Top, po.Dpr)
|
||||||
|
paddingRight := imath.Scale(po.Padding.Right, po.Dpr)
|
||||||
|
paddingBottom := imath.Scale(po.Padding.Bottom, po.Dpr)
|
||||||
|
paddingLeft := imath.Scale(po.Padding.Left, po.Dpr)
|
||||||
|
|
||||||
|
return img.Embed(
|
||||||
|
img.Width()+paddingLeft+paddingRight,
|
||||||
|
img.Height()+paddingTop+paddingBottom,
|
||||||
|
paddingLeft,
|
||||||
|
paddingTop,
|
||||||
|
)
|
||||||
|
}
|
||||||
58
processing/pipeline.go
Normal file
58
processing/pipeline.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pipelineContext struct {
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
imgtype imagetype.Type
|
||||||
|
|
||||||
|
trimmed bool
|
||||||
|
|
||||||
|
srcWidth int
|
||||||
|
srcHeight int
|
||||||
|
angle int
|
||||||
|
flip bool
|
||||||
|
|
||||||
|
cropWidth int
|
||||||
|
cropHeight int
|
||||||
|
cropGravity options.GravityOptions
|
||||||
|
|
||||||
|
wscale float64
|
||||||
|
hscale float64
|
||||||
|
|
||||||
|
iccImported bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type pipelineStep func(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error
|
||||||
|
type pipeline []pipelineStep
|
||||||
|
|
||||||
|
func (p pipeline) Run(ctx context.Context, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
pctx := pipelineContext{
|
||||||
|
ctx: ctx,
|
||||||
|
|
||||||
|
wscale: 1.0,
|
||||||
|
hscale: 1.0,
|
||||||
|
|
||||||
|
cropGravity: po.Crop.Gravity,
|
||||||
|
}
|
||||||
|
|
||||||
|
if pctx.cropGravity.Type == options.GravityUnknown {
|
||||||
|
pctx.cropGravity = po.Gravity
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, step := range p {
|
||||||
|
if err := step(&pctx, img, po, imgdata); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
176
processing/prepare.go
Normal file
176
processing/prepare.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imath"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func extractMeta(img *vips.Image, baseAngle int, useOrientation bool) (int, int, int, bool) {
|
||||||
|
width := img.Width()
|
||||||
|
height := img.Height()
|
||||||
|
|
||||||
|
angle := 0
|
||||||
|
flip := false
|
||||||
|
|
||||||
|
if useOrientation {
|
||||||
|
orientation := img.Orientation()
|
||||||
|
|
||||||
|
if orientation == 3 || orientation == 4 {
|
||||||
|
angle = 180
|
||||||
|
}
|
||||||
|
if orientation == 5 || orientation == 6 {
|
||||||
|
angle = 90
|
||||||
|
}
|
||||||
|
if orientation == 7 || orientation == 8 {
|
||||||
|
angle = 270
|
||||||
|
}
|
||||||
|
if orientation == 2 || orientation == 4 || orientation == 5 || orientation == 7 {
|
||||||
|
flip = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (angle+baseAngle)%180 != 0 {
|
||||||
|
width, height = height, width
|
||||||
|
}
|
||||||
|
|
||||||
|
return width, height, angle, flip
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcScale(width, height int, po *options.ProcessingOptions, imgtype imagetype.Type) (float64, float64) {
|
||||||
|
var wshrink, hshrink float64
|
||||||
|
|
||||||
|
srcW, srcH := float64(width), float64(height)
|
||||||
|
dstW, dstH := float64(po.Width), float64(po.Height)
|
||||||
|
|
||||||
|
if po.Width == 0 {
|
||||||
|
dstW = srcW
|
||||||
|
}
|
||||||
|
|
||||||
|
if dstW == srcW {
|
||||||
|
wshrink = 1
|
||||||
|
} else {
|
||||||
|
wshrink = srcW / dstW
|
||||||
|
}
|
||||||
|
|
||||||
|
if po.Height == 0 {
|
||||||
|
dstH = srcH
|
||||||
|
}
|
||||||
|
|
||||||
|
if dstH == srcH {
|
||||||
|
hshrink = 1
|
||||||
|
} else {
|
||||||
|
hshrink = srcH / dstH
|
||||||
|
}
|
||||||
|
|
||||||
|
if wshrink != 1 || hshrink != 1 {
|
||||||
|
rt := po.ResizingType
|
||||||
|
|
||||||
|
if rt == options.ResizeAuto {
|
||||||
|
srcD := srcW - srcH
|
||||||
|
dstD := dstW - dstH
|
||||||
|
|
||||||
|
if (srcD >= 0 && dstD >= 0) || (srcD < 0 && dstD < 0) {
|
||||||
|
rt = options.ResizeFill
|
||||||
|
} else {
|
||||||
|
rt = options.ResizeFit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case po.Width == 0 && rt != options.ResizeForce:
|
||||||
|
wshrink = hshrink
|
||||||
|
case po.Height == 0 && rt != options.ResizeForce:
|
||||||
|
hshrink = wshrink
|
||||||
|
case rt == options.ResizeFit:
|
||||||
|
wshrink = math.Max(wshrink, hshrink)
|
||||||
|
hshrink = wshrink
|
||||||
|
case rt == options.ResizeFill || rt == options.ResizeFillDown:
|
||||||
|
wshrink = math.Min(wshrink, hshrink)
|
||||||
|
hshrink = wshrink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !po.Enlarge && imgtype != imagetype.SVG {
|
||||||
|
if wshrink < 1 {
|
||||||
|
hshrink /= wshrink
|
||||||
|
wshrink = 1
|
||||||
|
}
|
||||||
|
if hshrink < 1 {
|
||||||
|
wshrink /= hshrink
|
||||||
|
hshrink = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if po.MinWidth > 0 {
|
||||||
|
if minShrink := srcW / float64(po.MinWidth); minShrink < wshrink {
|
||||||
|
hshrink /= wshrink / minShrink
|
||||||
|
wshrink = minShrink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if po.MinHeight > 0 {
|
||||||
|
if minShrink := srcH / float64(po.MinHeight); minShrink < hshrink {
|
||||||
|
wshrink /= hshrink / minShrink
|
||||||
|
hshrink = minShrink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wshrink /= po.Dpr
|
||||||
|
hshrink /= po.Dpr
|
||||||
|
|
||||||
|
if wshrink > srcW {
|
||||||
|
wshrink = srcW
|
||||||
|
}
|
||||||
|
|
||||||
|
if hshrink > srcH {
|
||||||
|
hshrink = srcH
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1.0 / wshrink, 1.0 / hshrink
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcCropSize(orig int, crop float64) int {
|
||||||
|
switch {
|
||||||
|
case crop == 0.0:
|
||||||
|
return 0
|
||||||
|
case crop >= 1.0:
|
||||||
|
return int(crop)
|
||||||
|
default:
|
||||||
|
return imath.Max(1, imath.Scale(orig, crop))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepare(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
pctx.imgtype = imagetype.Unknown
|
||||||
|
if imgdata != nil {
|
||||||
|
pctx.imgtype = imgdata.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
pctx.srcWidth, pctx.srcHeight, pctx.angle, pctx.flip = extractMeta(img, po.Rotate, po.AutoRotate)
|
||||||
|
|
||||||
|
pctx.cropWidth = calcCropSize(pctx.srcWidth, po.Crop.Width)
|
||||||
|
pctx.cropHeight = calcCropSize(pctx.srcHeight, po.Crop.Height)
|
||||||
|
|
||||||
|
widthToScale := imath.MinNonZero(pctx.cropWidth, pctx.srcWidth)
|
||||||
|
heightToScale := imath.MinNonZero(pctx.cropHeight, pctx.srcHeight)
|
||||||
|
|
||||||
|
pctx.wscale, pctx.hscale = calcScale(widthToScale, heightToScale, po, pctx.imgtype)
|
||||||
|
|
||||||
|
if pctx.cropWidth > 0 {
|
||||||
|
pctx.cropWidth = imath.Max(1, imath.Scale(pctx.cropWidth, pctx.wscale))
|
||||||
|
}
|
||||||
|
if pctx.cropHeight > 0 {
|
||||||
|
pctx.cropHeight = imath.Max(1, imath.Scale(pctx.cropHeight, pctx.hscale))
|
||||||
|
}
|
||||||
|
if pctx.cropGravity.Type != options.GravityFocusPoint {
|
||||||
|
pctx.cropGravity.X *= pctx.wscale
|
||||||
|
pctx.cropGravity.Y *= pctx.hscale
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
273
processing/processing.go
Normal file
273
processing/processing.go
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imath"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/router"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/security"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mainPipeline = pipeline{
|
||||||
|
trim,
|
||||||
|
prepare,
|
||||||
|
scaleOnLoad,
|
||||||
|
importColorProfile,
|
||||||
|
scale,
|
||||||
|
rotateAndFlip,
|
||||||
|
crop,
|
||||||
|
fixWebpSize,
|
||||||
|
exportColorProfile,
|
||||||
|
applyFilters,
|
||||||
|
extend,
|
||||||
|
padding,
|
||||||
|
flatten,
|
||||||
|
watermark,
|
||||||
|
finalize,
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageTypeGoodForWeb(imgtype imagetype.Type) bool {
|
||||||
|
return imgtype != imagetype.TIFF &&
|
||||||
|
imgtype != imagetype.BMP
|
||||||
|
}
|
||||||
|
|
||||||
|
// src - the source image format
|
||||||
|
// dst - what the user specified
|
||||||
|
// want - what we want switch to
|
||||||
|
func canSwitchFormat(src, dst, want imagetype.Type) bool {
|
||||||
|
// If the format we want is not supported, we can't switch to it anyway
|
||||||
|
return vips.SupportsSave(want) &&
|
||||||
|
// if src format does't support animation, we can switch to whatever we want
|
||||||
|
(!src.SupportsAnimation() ||
|
||||||
|
// if user specified the format and it doesn't support animation, we can switch to whatever we want
|
||||||
|
(dst != imagetype.Unknown && !dst.SupportsAnimation()) ||
|
||||||
|
// if the format we want supports animation, we can switch in any case
|
||||||
|
want.SupportsAnimation())
|
||||||
|
}
|
||||||
|
|
||||||
|
func canFitToBytes(imgtype imagetype.Type) bool {
|
||||||
|
switch imgtype {
|
||||||
|
case imagetype.JPEG, imagetype.WEBP, imagetype.AVIF, imagetype.TIFF:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformAnimated(ctx context.Context, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
if po.Trim.Enabled {
|
||||||
|
log.Warning("Trim is not supported for animated images")
|
||||||
|
po.Trim.Enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
imgWidth := img.Width()
|
||||||
|
|
||||||
|
frameHeight, err := img.GetInt("page-height")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
framesCount := imath.Min(img.Height()/frameHeight, config.MaxAnimationFrames)
|
||||||
|
|
||||||
|
// Double check dimensions because animated image has many frames
|
||||||
|
if err = security.CheckDimensions(imgWidth, frameHeight*framesCount); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vips 8.8+ supports n-pages and doesn't load the whole animated image on header access
|
||||||
|
if nPages, _ := img.GetIntDefault("n-pages", 0); nPages > framesCount {
|
||||||
|
// Load only the needed frames
|
||||||
|
if err = img.Load(imgdata, 1, 1.0, framesCount); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delay, err := img.GetIntSliceDefault("delay", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
loop, err := img.GetIntDefault("loop", 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy fields
|
||||||
|
// TODO: remove this in major update
|
||||||
|
gifLoop, err := img.GetIntDefault("gif-loop", -1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gifDelay, err := img.GetIntDefault("gif-delay", -1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
watermarkEnabled := po.Watermark.Enabled
|
||||||
|
po.Watermark.Enabled = false
|
||||||
|
defer func() { po.Watermark.Enabled = watermarkEnabled }()
|
||||||
|
|
||||||
|
frames := make([]*vips.Image, framesCount)
|
||||||
|
defer func() {
|
||||||
|
for _, frame := range frames {
|
||||||
|
if frame != nil {
|
||||||
|
frame.Clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := 0; i < framesCount; i++ {
|
||||||
|
frame := new(vips.Image)
|
||||||
|
|
||||||
|
if err = img.Extract(frame, 0, i*frameHeight, imgWidth, frameHeight); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
frames[i] = frame
|
||||||
|
|
||||||
|
if err = mainPipeline.Run(ctx, frame, po, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = img.Arrayjoin(frames); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if watermarkEnabled && imagedata.Watermark != nil {
|
||||||
|
if err = applyWatermark(img, imagedata.Watermark, &po.Watermark, framesCount); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = img.CastUchar(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(delay) == 0 {
|
||||||
|
delay = make([]int, framesCount)
|
||||||
|
for i := range delay {
|
||||||
|
delay[i] = 40
|
||||||
|
}
|
||||||
|
} else if len(delay) > framesCount {
|
||||||
|
delay = delay[:framesCount]
|
||||||
|
}
|
||||||
|
|
||||||
|
img.SetInt("page-height", frames[0].Height())
|
||||||
|
img.SetIntSlice("delay", delay)
|
||||||
|
img.SetInt("loop", loop)
|
||||||
|
img.SetInt("n-pages", framesCount)
|
||||||
|
|
||||||
|
// Legacy fields
|
||||||
|
// TODO: remove this in major update
|
||||||
|
if gifLoop >= 0 {
|
||||||
|
img.SetInt("gif-loop", gifLoop)
|
||||||
|
}
|
||||||
|
if gifDelay >= 0 {
|
||||||
|
img.SetInt("gif-delay", gifDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveImageToFitBytes(ctx context.Context, po *options.ProcessingOptions, img *vips.Image) (*imagedata.ImageData, error) {
|
||||||
|
var diff float64
|
||||||
|
quality := po.GetQuality()
|
||||||
|
|
||||||
|
for {
|
||||||
|
imgdata, err := img.Save(po.Format, quality)
|
||||||
|
if len(imgdata.Data) <= po.MaxBytes || quality <= 10 || err != nil {
|
||||||
|
return imgdata, err
|
||||||
|
}
|
||||||
|
imgdata.Close()
|
||||||
|
|
||||||
|
router.CheckTimeout(ctx)
|
||||||
|
|
||||||
|
delta := float64(len(imgdata.Data)) / float64(po.MaxBytes)
|
||||||
|
switch {
|
||||||
|
case delta > 3:
|
||||||
|
diff = 0.25
|
||||||
|
case delta > 1.5:
|
||||||
|
diff = 0.5
|
||||||
|
default:
|
||||||
|
diff = 0.75
|
||||||
|
}
|
||||||
|
quality = int(float64(quality) * diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProcessImage(ctx context.Context, imgdata *imagedata.ImageData, po *options.ProcessingOptions) (*imagedata.ImageData, error) {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
|
defer vips.Cleanup()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case po.Format == imagetype.Unknown:
|
||||||
|
switch {
|
||||||
|
case po.PreferAvif && canSwitchFormat(imgdata.Type, imagetype.Unknown, imagetype.AVIF):
|
||||||
|
po.Format = imagetype.AVIF
|
||||||
|
case po.PreferWebP && canSwitchFormat(imgdata.Type, imagetype.Unknown, imagetype.WEBP):
|
||||||
|
po.Format = imagetype.WEBP
|
||||||
|
case vips.SupportsSave(imgdata.Type) && imageTypeGoodForWeb(imgdata.Type):
|
||||||
|
po.Format = imgdata.Type
|
||||||
|
default:
|
||||||
|
po.Format = imagetype.JPEG
|
||||||
|
}
|
||||||
|
case po.EnforceAvif && canSwitchFormat(imgdata.Type, po.Format, imagetype.AVIF):
|
||||||
|
po.Format = imagetype.AVIF
|
||||||
|
case po.EnforceWebP && canSwitchFormat(imgdata.Type, po.Format, imagetype.WEBP):
|
||||||
|
po.Format = imagetype.WEBP
|
||||||
|
}
|
||||||
|
|
||||||
|
if !vips.SupportsSave(po.Format) {
|
||||||
|
return nil, fmt.Errorf("Can't save %s, probably not supported by your libvips", po.Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
animationSupport := config.MaxAnimationFrames > 1 && imgdata.Type.SupportsAnimation() && po.Format.SupportsAnimation()
|
||||||
|
|
||||||
|
pages := 1
|
||||||
|
if animationSupport {
|
||||||
|
pages = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
img := new(vips.Image)
|
||||||
|
defer img.Clear()
|
||||||
|
|
||||||
|
if err := img.Load(imgdata, 1, 1.0, pages); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if animationSupport && img.IsAnimated() {
|
||||||
|
if err := transformAnimated(ctx, img, po, imgdata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := mainPipeline.Run(ctx, img, po, imgdata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if po.MaxBytes > 0 && canFitToBytes(po.Format) {
|
||||||
|
return saveImageToFitBytes(ctx, po, img)
|
||||||
|
}
|
||||||
|
|
||||||
|
return img.Save(po.Format, po.GetQuality())
|
||||||
|
}
|
||||||
21
processing/rotate_and_flip.go
Normal file
21
processing/rotate_and_flip.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func rotateAndFlip(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
if err := img.Rotate(pctx.angle); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pctx.flip {
|
||||||
|
if err := img.Flip(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return img.Rotate(po.Rotate)
|
||||||
|
}
|
||||||
17
processing/scale.go
Normal file
17
processing/scale.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func scale(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
if pctx.wscale != 1 || pctx.hscale != 1 {
|
||||||
|
if err := img.Resize(pctx.wscale, pctx.hscale); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return copyMemoryAndCheckTimeout(pctx.ctx, img)
|
||||||
|
}
|
||||||
72
processing/scale_on_load.go
Normal file
72
processing/scale_on_load.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imath"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func canScaleOnLoad(imgtype imagetype.Type, scale float64) bool {
|
||||||
|
if imgtype == imagetype.SVG {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DisableShrinkOnLoad || scale >= 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return imgtype == imagetype.JPEG || imgtype == imagetype.WEBP
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcJpegShink(scale float64, imgtype imagetype.Type) int {
|
||||||
|
shrink := int(1.0 / scale)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case shrink >= 8:
|
||||||
|
return 8
|
||||||
|
case shrink >= 4:
|
||||||
|
return 4
|
||||||
|
case shrink >= 2:
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func scaleOnLoad(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
prescale := math.Max(pctx.wscale, pctx.hscale)
|
||||||
|
|
||||||
|
if pctx.trimmed || prescale == 1 || imgdata == nil || !canScaleOnLoad(pctx.imgtype, prescale) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jpegShrink := calcJpegShink(prescale, pctx.imgtype)
|
||||||
|
|
||||||
|
if pctx.imgtype == imagetype.JPEG && jpegShrink == 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := img.Load(imgdata, jpegShrink, prescale, 1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update scales after scale-on-load
|
||||||
|
newWidth, newHeight, _, _ := extractMeta(img, po.Rotate, po.AutoRotate)
|
||||||
|
|
||||||
|
pctx.wscale = float64(pctx.srcWidth) * pctx.wscale / float64(newWidth)
|
||||||
|
if pctx.srcWidth == imath.Scale(pctx.srcWidth, pctx.wscale) {
|
||||||
|
pctx.wscale = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pctx.hscale = float64(pctx.srcHeight) * pctx.hscale / float64(newHeight)
|
||||||
|
if pctx.srcHeight == imath.Scale(pctx.srcHeight, pctx.hscale) {
|
||||||
|
pctx.hscale = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
24
processing/trim.go
Normal file
24
processing/trim.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func trim(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
if !po.Trim.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := img.Trim(po.Trim.Threshold, po.Trim.Smart, po.Trim.Color, po.Trim.EqualHor, po.Trim.EqualVer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := copyMemoryAndCheckTimeout(pctx.ctx, img); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pctx.trimmed = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
88
processing/watermark.go
Normal file
88
processing/watermark.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imath"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
var watermarkPipeline = pipeline{
|
||||||
|
prepare,
|
||||||
|
scaleOnLoad,
|
||||||
|
importColorProfile,
|
||||||
|
scale,
|
||||||
|
rotateAndFlip,
|
||||||
|
exportColorProfile,
|
||||||
|
finalize,
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareWatermark(wm *vips.Image, wmData *imagedata.ImageData, opts *options.WatermarkOptions, imgWidth, imgHeight int) error {
|
||||||
|
if err := wm.Load(wmData, 1, 1.0, 1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
po := options.NewProcessingOptions()
|
||||||
|
po.ResizingType = options.ResizeFit
|
||||||
|
po.Dpr = 1
|
||||||
|
po.Enlarge = true
|
||||||
|
po.Format = wmData.Type
|
||||||
|
|
||||||
|
if opts.Scale > 0 {
|
||||||
|
po.Width = imath.Max(imath.Scale(imgWidth, opts.Scale), 1)
|
||||||
|
po.Height = imath.Max(imath.Scale(imgHeight, opts.Scale), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := watermarkPipeline.Run(context.Background(), wm, po, wmData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Replicate {
|
||||||
|
return wm.Replicate(imgWidth, imgHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
left, top := calcPosition(imgWidth, imgHeight, wm.Width(), wm.Height(), &opts.Gravity, true)
|
||||||
|
|
||||||
|
return wm.Embed(imgWidth, imgHeight, left, top)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyWatermark(img *vips.Image, wmData *imagedata.ImageData, opts *options.WatermarkOptions, framesCount int) error {
|
||||||
|
if err := img.RgbColourspace(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := img.CopyMemory(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wm := new(vips.Image)
|
||||||
|
defer wm.Clear()
|
||||||
|
|
||||||
|
width := img.Width()
|
||||||
|
height := img.Height()
|
||||||
|
|
||||||
|
if err := prepareWatermark(wm, wmData, opts, width, height/framesCount); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if framesCount > 1 {
|
||||||
|
if err := wm.Replicate(width, height); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opacity := opts.Opacity * config.WatermarkOpacity
|
||||||
|
|
||||||
|
return img.ApplyWatermark(wm, opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func watermark(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||||
|
if !po.Watermark.Enabled || imagedata.Watermark == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return applyWatermark(img, imagedata.Watermark, &po.Watermark, 1)
|
||||||
|
}
|
||||||
@@ -7,79 +7,78 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/errorreport"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/ierrors"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/metrics"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/processing"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/router"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/security"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
processingSem chan struct{}
|
processingSem chan struct{}
|
||||||
|
|
||||||
headerVaryValue string
|
headerVaryValue string
|
||||||
fallbackImage *imageData
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
type fallbackImageUsedCtxKey struct{}
|
||||||
fallbackImageUsedCtxKey = ctxKey("fallbackImageUsed")
|
|
||||||
)
|
|
||||||
|
|
||||||
func initProcessingHandler() error {
|
func initProcessingHandler() {
|
||||||
var err error
|
processingSem = make(chan struct{}, config.Concurrency)
|
||||||
|
|
||||||
processingSem = make(chan struct{}, conf.Concurrency)
|
|
||||||
|
|
||||||
vary := make([]string, 0)
|
vary := make([]string, 0)
|
||||||
|
|
||||||
if conf.EnableWebpDetection || conf.EnforceWebp {
|
if config.EnableWebpDetection || config.EnforceWebp {
|
||||||
vary = append(vary, "Accept")
|
vary = append(vary, "Accept")
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.EnableClientHints {
|
if config.EnableClientHints {
|
||||||
vary = append(vary, "DPR", "Viewport-Width", "Width")
|
vary = append(vary, "DPR", "Viewport-Width", "Width")
|
||||||
}
|
}
|
||||||
|
|
||||||
headerVaryValue = strings.Join(vary, ", ")
|
headerVaryValue = strings.Join(vary, ", ")
|
||||||
|
|
||||||
if fallbackImage, err = getFallbackImageData(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func respondWithImage(ctx context.Context, reqID string, r *http.Request, rw http.ResponseWriter, data []byte) {
|
func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, resultData *imagedata.ImageData, po *options.ProcessingOptions, originURL string, originData *imagedata.ImageData) {
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
imgdata := getImageData(ctx)
|
|
||||||
|
|
||||||
var contentDisposition string
|
var contentDisposition string
|
||||||
if len(po.Filename) > 0 {
|
if len(po.Filename) > 0 {
|
||||||
contentDisposition = po.Format.ContentDisposition(po.Filename)
|
contentDisposition = resultData.Type.ContentDisposition(po.Filename)
|
||||||
} else {
|
} else {
|
||||||
contentDisposition = po.Format.ContentDispositionFromURL(getImageURL(ctx))
|
contentDisposition = resultData.Type.ContentDispositionFromURL(originURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
rw.Header().Set("Content-Type", po.Format.Mime())
|
rw.Header().Set("Content-Type", resultData.Type.Mime())
|
||||||
rw.Header().Set("Content-Disposition", contentDisposition)
|
rw.Header().Set("Content-Disposition", contentDisposition)
|
||||||
|
|
||||||
if conf.SetCanonicalHeader {
|
if config.SetCanonicalHeader {
|
||||||
origin := getImageURL(ctx)
|
if strings.HasPrefix(originURL, "https://") || strings.HasPrefix(originURL, "http://") {
|
||||||
if strings.HasPrefix(origin, "https://") || strings.HasPrefix(origin, "http://") {
|
linkHeader := fmt.Sprintf(`<%s>; rel="canonical"`, originURL)
|
||||||
linkHeader := fmt.Sprintf(`<%s>; rel="canonical"`, origin)
|
|
||||||
rw.Header().Set("Link", linkHeader)
|
rw.Header().Set("Link", linkHeader)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var cacheControl, expires string
|
var cacheControl, expires string
|
||||||
|
|
||||||
if conf.CacheControlPassthrough && imgdata.Headers != nil {
|
if config.CacheControlPassthrough && originData.Headers != nil {
|
||||||
if val, ok := imgdata.Headers["Cache-Control"]; ok {
|
if val, ok := originData.Headers["Cache-Control"]; ok {
|
||||||
cacheControl = val
|
cacheControl = val
|
||||||
}
|
}
|
||||||
if val, ok := imgdata.Headers["Expires"]; ok {
|
if val, ok := originData.Headers["Expires"]; ok {
|
||||||
expires = val
|
expires = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cacheControl) == 0 && len(expires) == 0 {
|
if len(cacheControl) == 0 && len(expires) == 0 {
|
||||||
cacheControl = fmt.Sprintf("max-age=%d, public", conf.TTL)
|
cacheControl = fmt.Sprintf("max-age=%d, public", config.TTL)
|
||||||
expires = time.Now().Add(time.Second * time.Duration(conf.TTL)).Format(http.TimeFormat)
|
expires = time.Now().Add(time.Second * time.Duration(config.TTL)).Format(http.TimeFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cacheControl) > 0 {
|
if len(cacheControl) > 0 {
|
||||||
@@ -93,128 +92,174 @@ func respondWithImage(ctx context.Context, reqID string, r *http.Request, rw htt
|
|||||||
rw.Header().Set("Vary", headerVaryValue)
|
rw.Header().Set("Vary", headerVaryValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.EnableDebugHeaders {
|
if config.EnableDebugHeaders {
|
||||||
rw.Header().Set("X-Origin-Content-Length", strconv.Itoa(len(imgdata.Data)))
|
rw.Header().Set("X-Origin-Content-Length", strconv.Itoa(len(originData.Data)))
|
||||||
}
|
}
|
||||||
|
|
||||||
rw.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
rw.Header().Set("Content-Length", strconv.Itoa(len(resultData.Data)))
|
||||||
statusCode := 200
|
statusCode := 200
|
||||||
if getFallbackImageUsed(ctx) {
|
if getFallbackImageUsed(r.Context()) {
|
||||||
statusCode = conf.FallbackImageHTTPCode
|
statusCode = config.FallbackImageHTTPCode
|
||||||
}
|
}
|
||||||
rw.WriteHeader(statusCode)
|
rw.WriteHeader(statusCode)
|
||||||
rw.Write(data)
|
rw.Write(resultData.Data)
|
||||||
|
|
||||||
imageURL := getImageURL(ctx)
|
router.LogResponse(
|
||||||
|
reqID, r, statusCode, nil,
|
||||||
logResponse(reqID, r, statusCode, nil, &imageURL, po)
|
log.Fields{
|
||||||
}
|
"image_url": originURL,
|
||||||
|
"processing_options": po,
|
||||||
func respondWithNotModified(ctx context.Context, reqID string, r *http.Request, rw http.ResponseWriter) {
|
},
|
||||||
rw.WriteHeader(304)
|
)
|
||||||
|
|
||||||
imageURL := getImageURL(ctx)
|
|
||||||
|
|
||||||
logResponse(reqID, r, 304, nil, &imageURL, getProcessingOptions(ctx))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx, timeoutCancel := context.WithTimeout(r.Context(), time.Duration(config.WriteTimeout)*time.Second)
|
||||||
|
|
||||||
var dataDogCancel context.CancelFunc
|
|
||||||
ctx, dataDogCancel, rw = startDataDogRootSpan(ctx, rw, r)
|
|
||||||
defer dataDogCancel()
|
|
||||||
|
|
||||||
var newRelicCancel context.CancelFunc
|
|
||||||
ctx, newRelicCancel, rw = startNewRelicTransaction(ctx, rw, r)
|
|
||||||
defer newRelicCancel()
|
|
||||||
|
|
||||||
incrementPrometheusRequestsTotal()
|
|
||||||
defer startPrometheusDuration(prometheusRequestDuration)()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case processingSem <- struct{}{}:
|
|
||||||
case <-ctx.Done():
|
|
||||||
panic(newError(499, "Request was cancelled before processing", "Cancelled"))
|
|
||||||
}
|
|
||||||
defer func() { <-processingSem }()
|
|
||||||
|
|
||||||
ctx, timeoutCancel := context.WithTimeout(ctx, time.Duration(conf.WriteTimeout)*time.Second)
|
|
||||||
defer timeoutCancel()
|
defer timeoutCancel()
|
||||||
|
|
||||||
ctx, err := parsePath(ctx, r)
|
var metricsCancel context.CancelFunc
|
||||||
|
ctx, metricsCancel, rw = metrics.StartRequest(ctx, rw, r)
|
||||||
|
defer metricsCancel()
|
||||||
|
|
||||||
|
path := r.RequestURI
|
||||||
|
if queryStart := strings.IndexByte(path, '?'); queryStart >= 0 {
|
||||||
|
path = path[:queryStart]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.PathPrefix) > 0 {
|
||||||
|
path = strings.TrimPrefix(path, config.PathPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
path = strings.TrimPrefix(path, "/")
|
||||||
|
signature := ""
|
||||||
|
|
||||||
|
if signatureEnd := strings.IndexByte(path, '/'); signatureEnd > 0 {
|
||||||
|
signature = path[:signatureEnd]
|
||||||
|
path = path[signatureEnd:]
|
||||||
|
} else {
|
||||||
|
panic(ierrors.New(404, fmt.Sprintf("Invalid path: %s", path), "Invalid URL"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := security.VerifySignature(signature, path); err != nil {
|
||||||
|
panic(ierrors.New(403, err.Error(), "Forbidden"))
|
||||||
|
}
|
||||||
|
|
||||||
|
po, imageURL, err := options.ParsePath(path, r.Header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, downloadcancel, err := downloadImageCtx(ctx)
|
if !security.VerifySourceURL(imageURL) {
|
||||||
defer downloadcancel()
|
panic(ierrors.New(404, fmt.Sprintf("Source URL is not allowed: %s", imageURL), "Invalid source"))
|
||||||
if err != nil {
|
}
|
||||||
sendErrorToDataDog(ctx, err)
|
|
||||||
sendErrorToNewRelic(ctx, err)
|
|
||||||
incrementPrometheusErrorsTotal("download")
|
|
||||||
|
|
||||||
if fallbackImage == nil {
|
// SVG is a special case. Though saving to svg is not supported, SVG->SVG is.
|
||||||
|
if !vips.SupportsSave(po.Format) && po.Format != imagetype.Unknown && po.Format != imagetype.SVG {
|
||||||
|
panic(ierrors.New(
|
||||||
|
422,
|
||||||
|
fmt.Sprintf("Resulting image format is not supported: %s", po.Format),
|
||||||
|
"Invalid URL",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The heavy part start here, so we need to restrict concurrency
|
||||||
|
select {
|
||||||
|
case processingSem <- struct{}{}:
|
||||||
|
case <-ctx.Done():
|
||||||
|
// We don't actually need to check timeout here,
|
||||||
|
// but it's an easy way to check if this is an actual timeout
|
||||||
|
// or the request was cancelled
|
||||||
|
router.CheckTimeout(ctx)
|
||||||
|
}
|
||||||
|
defer func() { <-processingSem }()
|
||||||
|
|
||||||
|
originData, err := func() (*imagedata.ImageData, error) {
|
||||||
|
defer metrics.StartDownloadingSegment(ctx)()
|
||||||
|
return imagedata.Download(imageURL, "source image")
|
||||||
|
}()
|
||||||
|
if err == nil {
|
||||||
|
defer originData.Close()
|
||||||
|
} else {
|
||||||
|
metrics.SendError(ctx, "download", err)
|
||||||
|
|
||||||
|
if imagedata.FallbackImage == nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ierr, ok := err.(*imgproxyError); !ok || ierr.Unexpected {
|
if ierr, ok := err.(*ierrors.Error); !ok || ierr.Unexpected {
|
||||||
reportError(err, r)
|
errorreport.Report(err, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
logWarning("Could not load image. Using fallback image: %s", err.Error())
|
log.Warningf("Could not load image. Using fallback image: %s", err.Error())
|
||||||
ctx = setFallbackImageUsedCtx(ctx)
|
r = r.WithContext(setFallbackImageUsedCtx(r.Context()))
|
||||||
ctx = context.WithValue(ctx, imageDataCtxKey, fallbackImage)
|
originData = imagedata.FallbackImage
|
||||||
}
|
}
|
||||||
|
|
||||||
checkTimeout(ctx)
|
router.CheckTimeout(ctx)
|
||||||
|
|
||||||
if conf.ETagEnabled && !getFallbackImageUsed(ctx) {
|
if config.ETagEnabled && !getFallbackImageUsed(ctx) {
|
||||||
eTag := calcETag(ctx)
|
eTag := calcETag(ctx, originData, po)
|
||||||
rw.Header().Set("ETag", eTag)
|
rw.Header().Set("ETag", eTag)
|
||||||
|
|
||||||
if eTag == r.Header.Get("If-None-Match") {
|
if eTag == r.Header.Get("If-None-Match") {
|
||||||
respondWithNotModified(ctx, reqID, r, rw)
|
rw.WriteHeader(304)
|
||||||
|
router.LogResponse(reqID, r, 304, nil, log.Fields{"image_url": imageURL})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkTimeout(ctx)
|
router.CheckTimeout(ctx)
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
if originData.Type == po.Format || po.Format == imagetype.Unknown {
|
||||||
if len(po.SkipProcessingFormats) > 0 {
|
// Don't process SVG
|
||||||
imgdata := getImageData(ctx)
|
if originData.Type == imagetype.SVG {
|
||||||
|
respondWithImage(reqID, r, rw, originData, po, imageURL, originData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if imgdata.Type == po.Format || po.Format == imageTypeUnknown {
|
if len(po.SkipProcessingFormats) > 0 {
|
||||||
for _, f := range po.SkipProcessingFormats {
|
for _, f := range po.SkipProcessingFormats {
|
||||||
if f == imgdata.Type {
|
if f == originData.Type {
|
||||||
po.Format = imgdata.Type
|
respondWithImage(reqID, r, rw, originData, po, imageURL, originData)
|
||||||
respondWithImage(ctx, reqID, r, rw, imgdata.Data)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
imageData, processcancel, err := processImage(ctx)
|
if !vips.SupportsLoad(originData.Type) {
|
||||||
defer processcancel()
|
panic(ierrors.New(
|
||||||
if err != nil {
|
422,
|
||||||
sendErrorToDataDog(ctx, err)
|
fmt.Sprintf("Source image format is not supported: %s", originData.Type),
|
||||||
sendErrorToNewRelic(ctx, err)
|
"Invalid URL",
|
||||||
incrementPrometheusErrorsTotal("processing")
|
))
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
checkTimeout(ctx)
|
// At this point we can't allow requested format to be SVG as we can't save SVGs
|
||||||
|
if po.Format == imagetype.SVG {
|
||||||
|
panic(ierrors.New(422, "Resulting image format is not supported: svg", "Invalid URL"))
|
||||||
|
}
|
||||||
|
|
||||||
respondWithImage(ctx, reqID, r, rw, imageData)
|
resultData, err := func() (*imagedata.ImageData, error) {
|
||||||
|
defer metrics.StartProcessingSegment(ctx)()
|
||||||
|
return processing.ProcessImage(ctx, originData, po)
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
metrics.SendError(ctx, "processing", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer resultData.Close()
|
||||||
|
|
||||||
|
router.CheckTimeout(ctx)
|
||||||
|
|
||||||
|
respondWithImage(reqID, r, rw, resultData, po, imageURL, originData)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setFallbackImageUsedCtx(ctx context.Context) context.Context {
|
func setFallbackImageUsedCtx(ctx context.Context) context.Context {
|
||||||
return context.WithValue(ctx, fallbackImageUsedCtxKey, true)
|
return context.WithValue(ctx, fallbackImageUsedCtxKey{}, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFallbackImageUsed(ctx context.Context) bool {
|
func getFallbackImageUsed(ctx context.Context) bool {
|
||||||
result, _ := ctx.Value(fallbackImageUsedCtxKey).(bool)
|
result, _ := ctx.Value(fallbackImageUsedCtxKey{}).(bool)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
281
processing_handler_test.go
Normal file
281
processing_handler_test.go
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagemeta"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/router"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/vips"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProcessingHandlerTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
router *router.Router
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) SetupSuite() {
|
||||||
|
config.Reset()
|
||||||
|
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
config.LocalFileSystemRoot = filepath.Join(wd, "/testdata")
|
||||||
|
|
||||||
|
logrus.SetOutput(io.Discard)
|
||||||
|
|
||||||
|
initialize()
|
||||||
|
|
||||||
|
s.router = buildRouter()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TeardownSuite() {
|
||||||
|
shutdown()
|
||||||
|
logrus.SetOutput(os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) SetupTest() {
|
||||||
|
// We don't need config.LocalFileSystemRoot anymore as it is used
|
||||||
|
// only during initialization
|
||||||
|
config.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) send(path string, header ...http.Header) *httptest.ResponseRecorder {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
|
||||||
|
if len(header) > 0 {
|
||||||
|
req.Header = header[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
s.router.ServeHTTP(rw, req)
|
||||||
|
|
||||||
|
return rw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) readTestFile(name string) []byte {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filepath.Join(wd, "testdata", name))
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) readBody(res *http.Response) []byte {
|
||||||
|
data, err := io.ReadAll(res.Body)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestRequest() {
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, res.StatusCode)
|
||||||
|
assert.Equal(s.T(), "image/png", res.Header.Get("Content-Type"))
|
||||||
|
|
||||||
|
meta, err := imagemeta.DecodeMeta(res.Body)
|
||||||
|
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), "png", meta.Format())
|
||||||
|
assert.Equal(s.T(), 4, meta.Width())
|
||||||
|
assert.Equal(s.T(), 4, meta.Height())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestSignatureValidationFailure() {
|
||||||
|
config.Keys = [][]byte{[]byte("test-key")}
|
||||||
|
config.Salts = [][]byte{[]byte("test-salt")}
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 403, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestSignatureValidationSuccess() {
|
||||||
|
config.Keys = [][]byte{[]byte("test-key")}
|
||||||
|
config.Salts = [][]byte{[]byte("test-salt")}
|
||||||
|
|
||||||
|
rw := s.send("/My9d3xq_PYpVHsPrCyww0Kh1w5KZeZhIlWhsa4az1TI/rs:fill:4:4/plain/local:///test1.png")
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestSourceValidationFailure() {
|
||||||
|
config.AllowedSources = []string{"https://"}
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestSourceValidationSuccess() {
|
||||||
|
config.AllowedSources = []string{"local:///"}
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestSourceFormatNotSupported() {
|
||||||
|
vips.DisableLoadSupport(imagetype.PNG)
|
||||||
|
defer vips.ResetLoadSupport()
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 422, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestResultingFormatNotSupported() {
|
||||||
|
vips.DisableSaveSupport(imagetype.PNG)
|
||||||
|
defer vips.ResetSaveSupport()
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 422, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestSkipProcessingConfig() {
|
||||||
|
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, res.StatusCode)
|
||||||
|
|
||||||
|
actual := s.readBody(res)
|
||||||
|
expected := s.readTestFile("test1.png")
|
||||||
|
|
||||||
|
assert.True(s.T(), bytes.Equal(expected, actual))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestSkipProcessingPO() {
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/skp:png/plain/local:///test1.png")
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, res.StatusCode)
|
||||||
|
|
||||||
|
actual := s.readBody(res)
|
||||||
|
expected := s.readTestFile("test1.png")
|
||||||
|
|
||||||
|
assert.True(s.T(), bytes.Equal(expected, actual))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
|
||||||
|
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, res.StatusCode)
|
||||||
|
|
||||||
|
actual := s.readBody(res)
|
||||||
|
expected := s.readTestFile("test1.png")
|
||||||
|
|
||||||
|
assert.True(s.T(), bytes.Equal(expected, actual))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestSkipProcessingDifferentFormat() {
|
||||||
|
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@jpg")
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, res.StatusCode)
|
||||||
|
|
||||||
|
actual := s.readBody(res)
|
||||||
|
expected := s.readTestFile("test1.png")
|
||||||
|
|
||||||
|
assert.False(s.T(), bytes.Equal(expected, actual))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestSkipProcessingSVG() {
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.svg")
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, res.StatusCode)
|
||||||
|
|
||||||
|
actual := s.readBody(res)
|
||||||
|
expected := s.readTestFile("test1.svg")
|
||||||
|
|
||||||
|
assert.True(s.T(), bytes.Equal(expected, actual))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestNotSkipProcessingSVGToJPG() {
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.svg@jpg")
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, res.StatusCode)
|
||||||
|
|
||||||
|
actual := s.readBody(res)
|
||||||
|
expected := s.readTestFile("test1.svg")
|
||||||
|
|
||||||
|
assert.False(s.T(), bytes.Equal(expected, actual))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestErrorSavingToSVG() {
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@svg")
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 422, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestCacheControlPassthrough() {
|
||||||
|
config.CacheControlPassthrough = true
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
data := s.readTestFile("test1.png")
|
||||||
|
rw.Header().Set("Cache-Control", "fake-cache-control")
|
||||||
|
rw.Header().Set("Expires", "fake-expires")
|
||||||
|
rw.WriteHeader(200)
|
||||||
|
rw.Write(data)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), "fake-cache-control", res.Header.Get("Cache-Control"))
|
||||||
|
assert.Equal(s.T(), "fake-expires", res.Header.Get("Expires"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughDisabled() {
|
||||||
|
config.CacheControlPassthrough = false
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
data := s.readTestFile("test1.png")
|
||||||
|
rw.Header().Set("Cache-Control", "fake-cache-control")
|
||||||
|
rw.Header().Set("Expires", "fake-expires")
|
||||||
|
rw.WriteHeader(200)
|
||||||
|
rw.Write(data)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.NotEqual(s.T(), "fake-cache-control", res.Header.Get("Cache-Control"))
|
||||||
|
assert.NotEqual(s.T(), "fake-expires", res.Header.Get("Expires"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessingHandler(t *testing.T) {
|
||||||
|
suite.Run(t, new(ProcessingHandlerTestSuite))
|
||||||
|
}
|
||||||
@@ -1,628 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProcessingOptionsTestSuite struct{ MainTestSuite }
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) getRequest(uri string) *http.Request {
|
|
||||||
return &http.Request{Method: "GET", RequestURI: uri, Header: make(http.Header)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParseBase64URL() {
|
|
||||||
imageURL := "http://images.dev/lorem/ipsum.jpg?param=value"
|
|
||||||
req := s.getRequest(fmt.Sprintf("/unsafe/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(imageURL))))
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
assert.Equal(s.T(), imageURL, getImageURL(ctx))
|
|
||||||
assert.Equal(s.T(), imageTypePNG, getProcessingOptions(ctx).Format)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithoutExtension() {
|
|
||||||
imageURL := "http://images.dev/lorem/ipsum.jpg?param=value"
|
|
||||||
req := s.getRequest(fmt.Sprintf("/unsafe/size:100:100/%s", base64.RawURLEncoding.EncodeToString([]byte(imageURL))))
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
assert.Equal(s.T(), imageURL, getImageURL(ctx))
|
|
||||||
assert.Equal(s.T(), imageTypeUnknown, getProcessingOptions(ctx).Format)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithBase() {
|
|
||||||
conf.BaseURL = "http://images.dev/"
|
|
||||||
|
|
||||||
imageURL := "lorem/ipsum.jpg?param=value"
|
|
||||||
req := s.getRequest(fmt.Sprintf("/unsafe/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(imageURL))))
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
assert.Equal(s.T(), fmt.Sprintf("%s%s", conf.BaseURL, imageURL), getImageURL(ctx))
|
|
||||||
assert.Equal(s.T(), imageTypePNG, getProcessingOptions(ctx).Format)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePlainURL() {
|
|
||||||
imageURL := "http://images.dev/lorem/ipsum.jpg"
|
|
||||||
req := s.getRequest(fmt.Sprintf("/unsafe/size:100:100/plain/%s@png", imageURL))
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
assert.Equal(s.T(), imageURL, getImageURL(ctx))
|
|
||||||
assert.Equal(s.T(), imageTypePNG, getProcessingOptions(ctx).Format)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithoutExtension() {
|
|
||||||
imageURL := "http://images.dev/lorem/ipsum.jpg"
|
|
||||||
req := s.getRequest(fmt.Sprintf("/unsafe/size:100:100/plain/%s", imageURL))
|
|
||||||
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
assert.Equal(s.T(), imageURL, getImageURL(ctx))
|
|
||||||
assert.Equal(s.T(), imageTypeUnknown, getProcessingOptions(ctx).Format)
|
|
||||||
}
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscaped() {
|
|
||||||
imageURL := "http://images.dev/lorem/ipsum.jpg?param=value"
|
|
||||||
req := s.getRequest(fmt.Sprintf("/unsafe/size:100:100/plain/%s@png", url.PathEscape(imageURL)))
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
assert.Equal(s.T(), imageURL, getImageURL(ctx))
|
|
||||||
assert.Equal(s.T(), imageTypePNG, getProcessingOptions(ctx).Format)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithBase() {
|
|
||||||
conf.BaseURL = "http://images.dev/"
|
|
||||||
|
|
||||||
imageURL := "lorem/ipsum.jpg"
|
|
||||||
req := s.getRequest(fmt.Sprintf("/unsafe/size:100:100/plain/%s@png", imageURL))
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
assert.Equal(s.T(), fmt.Sprintf("%s%s", conf.BaseURL, imageURL), getImageURL(ctx))
|
|
||||||
assert.Equal(s.T(), imageTypePNG, getProcessingOptions(ctx).Format)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscapedWithBase() {
|
|
||||||
conf.BaseURL = "http://images.dev/"
|
|
||||||
|
|
||||||
imageURL := "lorem/ipsum.jpg?param=value"
|
|
||||||
req := s.getRequest(fmt.Sprintf("/unsafe/size:100:100/plain/%s@png", url.PathEscape(imageURL)))
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
assert.Equal(s.T(), fmt.Sprintf("%s%s", conf.BaseURL, imageURL), getImageURL(ctx))
|
|
||||||
assert.Equal(s.T(), imageTypePNG, getProcessingOptions(ctx).Format)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParseURLAllowedSource() {
|
|
||||||
conf.AllowedSources = []string{"local://", "http://images.dev/"}
|
|
||||||
|
|
||||||
req := s.getRequest("/unsafe/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
_, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParseURLNotAllowedSource() {
|
|
||||||
conf.AllowedSources = []string{"local://", "http://images.dev/"}
|
|
||||||
|
|
||||||
req := s.getRequest("/unsafe/plain/s3://images/lorem/ipsum.jpg")
|
|
||||||
_, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Error(s.T(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathFormat() {
|
|
||||||
req := s.getRequest("/unsafe/format:webp/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), imageTypeWEBP, po.Format)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathResize() {
|
|
||||||
req := s.getRequest("/unsafe/resize:fill:100:200:1/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), resizeFill, po.ResizingType)
|
|
||||||
assert.Equal(s.T(), 100, po.Width)
|
|
||||||
assert.Equal(s.T(), 200, po.Height)
|
|
||||||
assert.True(s.T(), po.Enlarge)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathResizingType() {
|
|
||||||
req := s.getRequest("/unsafe/resizing_type:fill/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), resizeFill, po.ResizingType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathSize() {
|
|
||||||
req := s.getRequest("/unsafe/size:100:200:1/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), 100, po.Width)
|
|
||||||
assert.Equal(s.T(), 200, po.Height)
|
|
||||||
assert.True(s.T(), po.Enlarge)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathWidth() {
|
|
||||||
req := s.getRequest("/unsafe/width:100/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), 100, po.Width)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathHeight() {
|
|
||||||
req := s.getRequest("/unsafe/height:100/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), 100, po.Height)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathEnlarge() {
|
|
||||||
req := s.getRequest("/unsafe/enlarge:1/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.True(s.T(), po.Enlarge)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathExtend() {
|
|
||||||
req := s.getRequest("/unsafe/extend:1:so:10:20/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), true, po.Extend.Enabled)
|
|
||||||
assert.Equal(s.T(), gravitySouth, po.Extend.Gravity.Type)
|
|
||||||
assert.Equal(s.T(), 10.0, po.Extend.Gravity.X)
|
|
||||||
assert.Equal(s.T(), 20.0, po.Extend.Gravity.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathGravity() {
|
|
||||||
req := s.getRequest("/unsafe/gravity:soea/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), gravitySouthEast, po.Gravity.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathGravityFocuspoint() {
|
|
||||||
req := s.getRequest("/unsafe/gravity:fp:0.5:0.75/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), gravityFocusPoint, po.Gravity.Type)
|
|
||||||
assert.Equal(s.T(), 0.5, po.Gravity.X)
|
|
||||||
assert.Equal(s.T(), 0.75, po.Gravity.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathQuality() {
|
|
||||||
req := s.getRequest("/unsafe/quality:55/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), 55, po.Quality)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathBackground() {
|
|
||||||
req := s.getRequest("/unsafe/background:128:129:130/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.True(s.T(), po.Flatten)
|
|
||||||
assert.Equal(s.T(), uint8(128), po.Background.R)
|
|
||||||
assert.Equal(s.T(), uint8(129), po.Background.G)
|
|
||||||
assert.Equal(s.T(), uint8(130), po.Background.B)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundHex() {
|
|
||||||
req := s.getRequest("/unsafe/background:ffddee/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.True(s.T(), po.Flatten)
|
|
||||||
assert.Equal(s.T(), uint8(0xff), po.Background.R)
|
|
||||||
assert.Equal(s.T(), uint8(0xdd), po.Background.G)
|
|
||||||
assert.Equal(s.T(), uint8(0xee), po.Background.B)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundDisable() {
|
|
||||||
req := s.getRequest("/unsafe/background:fff/background:/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.False(s.T(), po.Flatten)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathBlur() {
|
|
||||||
req := s.getRequest("/unsafe/blur:0.2/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), float32(0.2), po.Blur)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathSharpen() {
|
|
||||||
req := s.getRequest("/unsafe/sharpen:0.2/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), float32(0.2), po.Sharpen)
|
|
||||||
}
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathDpr() {
|
|
||||||
req := s.getRequest("/unsafe/dpr:2/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), 2.0, po.Dpr)
|
|
||||||
}
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathWatermark() {
|
|
||||||
req := s.getRequest("/unsafe/watermark:0.5:soea:10:20:0.6/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.True(s.T(), po.Watermark.Enabled)
|
|
||||||
assert.Equal(s.T(), gravitySouthEast, po.Watermark.Gravity.Type)
|
|
||||||
assert.Equal(s.T(), 10.0, po.Watermark.Gravity.X)
|
|
||||||
assert.Equal(s.T(), 20.0, po.Watermark.Gravity.Y)
|
|
||||||
assert.Equal(s.T(), 0.6, po.Watermark.Scale)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathPreset() {
|
|
||||||
conf.Presets["test1"] = urlOptions{
|
|
||||||
urlOption{Name: "resizing_type", Args: []string{"fill"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.Presets["test2"] = urlOptions{
|
|
||||||
urlOption{Name: "blur", Args: []string{"0.2"}},
|
|
||||||
urlOption{Name: "quality", Args: []string{"50"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
req := s.getRequest("/unsafe/preset:test1:test2/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), resizeFill, po.ResizingType)
|
|
||||||
assert.Equal(s.T(), float32(0.2), po.Blur)
|
|
||||||
assert.Equal(s.T(), 50, po.Quality)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathPresetDefault() {
|
|
||||||
conf.Presets["default"] = urlOptions{
|
|
||||||
urlOption{Name: "resizing_type", Args: []string{"fill"}},
|
|
||||||
urlOption{Name: "blur", Args: []string{"0.2"}},
|
|
||||||
urlOption{Name: "quality", Args: []string{"50"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
req := s.getRequest("/unsafe/quality:70/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), resizeFill, po.ResizingType)
|
|
||||||
assert.Equal(s.T(), float32(0.2), po.Blur)
|
|
||||||
assert.Equal(s.T(), 70, po.Quality)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathPresetLoopDetection() {
|
|
||||||
conf.Presets["test1"] = urlOptions{
|
|
||||||
urlOption{Name: "resizing_type", Args: []string{"fill"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.Presets["test2"] = urlOptions{
|
|
||||||
urlOption{Name: "blur", Args: []string{"0.2"}},
|
|
||||||
urlOption{Name: "quality", Args: []string{"50"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
req := s.getRequest("/unsafe/preset:test1:test2:test1/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
require.ElementsMatch(s.T(), po.UsedPresets, []string{"test1", "test2"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathCachebuster() {
|
|
||||||
req := s.getRequest("/unsafe/cachebuster:123/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), "123", po.CacheBuster)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathStripMetadata() {
|
|
||||||
req := s.getRequest("/unsafe/strip_metadata:true/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.True(s.T(), po.StripMetadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathWebpDetection() {
|
|
||||||
conf.EnableWebpDetection = true
|
|
||||||
|
|
||||||
req := s.getRequest("/unsafe/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
req.Header.Set("Accept", "image/webp")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), true, po.PreferWebP)
|
|
||||||
assert.Equal(s.T(), false, po.EnforceWebP)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathWebpEnforce() {
|
|
||||||
conf.EnforceWebp = true
|
|
||||||
|
|
||||||
req := s.getRequest("/unsafe/plain/http://images.dev/lorem/ipsum.jpg@png")
|
|
||||||
req.Header.Set("Accept", "image/webp")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), true, po.PreferWebP)
|
|
||||||
assert.Equal(s.T(), true, po.EnforceWebP)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeader() {
|
|
||||||
conf.EnableClientHints = true
|
|
||||||
|
|
||||||
req := s.getRequest("/unsafe/plain/http://images.dev/lorem/ipsum.jpg@png")
|
|
||||||
req.Header.Set("Width", "100")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), 100, po.Width)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderDisabled() {
|
|
||||||
req := s.getRequest("/unsafe/plain/http://images.dev/lorem/ipsum.jpg@png")
|
|
||||||
req.Header.Set("Width", "100")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), 0, po.Width)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderRedefine() {
|
|
||||||
conf.EnableClientHints = true
|
|
||||||
|
|
||||||
req := s.getRequest("/unsafe/width:150/plain/http://images.dev/lorem/ipsum.jpg@png")
|
|
||||||
req.Header.Set("Width", "100")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), 150, po.Width)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathViewportWidthHeader() {
|
|
||||||
conf.EnableClientHints = true
|
|
||||||
|
|
||||||
req := s.getRequest("/unsafe/plain/http://images.dev/lorem/ipsum.jpg@png")
|
|
||||||
req.Header.Set("Viewport-Width", "100")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), 100, po.Width)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathViewportWidthHeaderDisabled() {
|
|
||||||
req := s.getRequest("/unsafe/plain/http://images.dev/lorem/ipsum.jpg@png")
|
|
||||||
req.Header.Set("Viewport-Width", "100")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), 0, po.Width)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathViewportWidthHeaderRedefine() {
|
|
||||||
conf.EnableClientHints = true
|
|
||||||
|
|
||||||
req := s.getRequest("/unsafe/width:150/plain/http://images.dev/lorem/ipsum.jpg@png")
|
|
||||||
req.Header.Set("Viewport-Width", "100")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), 150, po.Width)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathDprHeader() {
|
|
||||||
conf.EnableClientHints = true
|
|
||||||
|
|
||||||
req := s.getRequest("/unsafe/plain/http://images.dev/lorem/ipsum.jpg@png")
|
|
||||||
req.Header.Set("DPR", "2")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), 2.0, po.Dpr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathDprHeaderDisabled() {
|
|
||||||
req := s.getRequest("/unsafe/plain/http://images.dev/lorem/ipsum.jpg@png")
|
|
||||||
req.Header.Set("DPR", "2")
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), 1.0, po.Dpr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathSigned() {
|
|
||||||
conf.Keys = []securityKey{securityKey("test-key")}
|
|
||||||
conf.Salts = []securityKey{securityKey("test-salt")}
|
|
||||||
conf.AllowInsecure = false
|
|
||||||
|
|
||||||
req := s.getRequest("/HcvNognEV1bW6f8zRqxNYuOkV0IUf1xloRb57CzbT4g/width:150/plain/http://images.dev/lorem/ipsum.jpg@png")
|
|
||||||
_, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathSignedInvalid() {
|
|
||||||
conf.Keys = []securityKey{securityKey("test-key")}
|
|
||||||
conf.Salts = []securityKey{securityKey("test-salt")}
|
|
||||||
conf.AllowInsecure = false
|
|
||||||
|
|
||||||
req := s.getRequest("/unsafe/width:150/plain/http://images.dev/lorem/ipsum.jpg@png")
|
|
||||||
_, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Error(s.T(), err)
|
|
||||||
assert.Equal(s.T(), errInvalidSignature.Error(), err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathOnlyPresets() {
|
|
||||||
conf.OnlyPresets = true
|
|
||||||
conf.Presets["test1"] = urlOptions{
|
|
||||||
urlOption{Name: "blur", Args: []string{"0.2"}},
|
|
||||||
}
|
|
||||||
conf.Presets["test2"] = urlOptions{
|
|
||||||
urlOption{Name: "quality", Args: []string{"50"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
req := s.getRequest("/unsafe/test1:test2/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), float32(0.2), po.Blur)
|
|
||||||
assert.Equal(s.T(), 50, po.Quality)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParseSkipProcessing() {
|
|
||||||
req := s.getRequest("/unsafe/skp:jpg:png/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), []imageType{imageTypeJPEG, imageTypePNG}, po.SkipProcessingFormats)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParseSkipProcessingInvalid() {
|
|
||||||
req := s.getRequest("/unsafe/skp:jpg:png:bad_format/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
|
|
||||||
_, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Error(s.T(), err)
|
|
||||||
assert.Equal(s.T(), "Invalid image format in skip processing: bad_format", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParseExpires() {
|
|
||||||
req := s.getRequest("/unsafe/exp:32503669200/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
_, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParseExpiresExpired() {
|
|
||||||
req := s.getRequest("/unsafe/exp:1609448400/plain/http://images.dev/lorem/ipsum.jpg")
|
|
||||||
_, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Error(s.T(), err)
|
|
||||||
assert.Equal(s.T(), msgExpiredURL, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParseBase64URLOnlyPresets() {
|
|
||||||
conf.OnlyPresets = true
|
|
||||||
conf.Presets["test1"] = urlOptions{
|
|
||||||
urlOption{Name: "blur", Args: []string{"0.2"}},
|
|
||||||
}
|
|
||||||
conf.Presets["test2"] = urlOptions{
|
|
||||||
urlOption{Name: "quality", Args: []string{"50"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
imageURL := "http://images.dev/lorem/ipsum.jpg?param=value"
|
|
||||||
req := s.getRequest(fmt.Sprintf("/unsafe/test1:test2/%s.png", base64.RawURLEncoding.EncodeToString([]byte(imageURL))))
|
|
||||||
|
|
||||||
ctx, err := parsePath(context.Background(), req)
|
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
|
||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
|
||||||
assert.Equal(s.T(), float32(0.2), po.Blur)
|
|
||||||
assert.Equal(s.T(), 50, po.Quality)
|
|
||||||
}
|
|
||||||
func TestProcessingOptions(t *testing.T) {
|
|
||||||
suite.Run(t, new(ProcessingOptionsTestSuite))
|
|
||||||
}
|
|
||||||
180
prometheus.go
180
prometheus.go
@@ -1,180 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
prometheusEnabled = false
|
|
||||||
|
|
||||||
prometheusRequestsTotal prometheus.Counter
|
|
||||||
prometheusErrorsTotal *prometheus.CounterVec
|
|
||||||
prometheusRequestDuration prometheus.Histogram
|
|
||||||
prometheusDownloadDuration prometheus.Histogram
|
|
||||||
prometheusProcessingDuration prometheus.Histogram
|
|
||||||
prometheusBufferSize *prometheus.HistogramVec
|
|
||||||
prometheusBufferDefaultSize *prometheus.GaugeVec
|
|
||||||
prometheusBufferMaxSize *prometheus.GaugeVec
|
|
||||||
prometheusVipsMemory prometheus.GaugeFunc
|
|
||||||
prometheusVipsMaxMemory prometheus.GaugeFunc
|
|
||||||
prometheusVipsAllocs prometheus.GaugeFunc
|
|
||||||
)
|
|
||||||
|
|
||||||
func initPrometheus() {
|
|
||||||
if len(conf.PrometheusBind) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
prometheusRequestsTotal = prometheus.NewCounter(prometheus.CounterOpts{
|
|
||||||
Namespace: conf.PrometheusNamespace,
|
|
||||||
Name: "requests_total",
|
|
||||||
Help: "A counter of the total number of HTTP requests imgproxy processed.",
|
|
||||||
})
|
|
||||||
|
|
||||||
prometheusErrorsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
||||||
Namespace: conf.PrometheusNamespace,
|
|
||||||
Name: "errors_total",
|
|
||||||
Help: "A counter of the occurred errors separated by type.",
|
|
||||||
}, []string{"type"})
|
|
||||||
|
|
||||||
prometheusRequestDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
|
|
||||||
Namespace: conf.PrometheusNamespace,
|
|
||||||
Name: "request_duration_seconds",
|
|
||||||
Help: "A histogram of the response latency.",
|
|
||||||
})
|
|
||||||
|
|
||||||
prometheusDownloadDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
|
|
||||||
Namespace: conf.PrometheusNamespace,
|
|
||||||
Name: "download_duration_seconds",
|
|
||||||
Help: "A histogram of the source image downloading latency.",
|
|
||||||
})
|
|
||||||
|
|
||||||
prometheusProcessingDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
|
|
||||||
Namespace: conf.PrometheusNamespace,
|
|
||||||
Name: "processing_duration_seconds",
|
|
||||||
Help: "A histogram of the image processing latency.",
|
|
||||||
})
|
|
||||||
|
|
||||||
prometheusBufferSize = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
|
||||||
Namespace: conf.PrometheusNamespace,
|
|
||||||
Name: "buffer_size_bytes",
|
|
||||||
Help: "A histogram of the buffer size in bytes.",
|
|
||||||
Buckets: prometheus.ExponentialBuckets(1024, 2, 14),
|
|
||||||
}, []string{"type"})
|
|
||||||
|
|
||||||
prometheusBufferDefaultSize = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
|
||||||
Namespace: conf.PrometheusNamespace,
|
|
||||||
Name: "buffer_default_size_bytes",
|
|
||||||
Help: "A gauge of the buffer default size in bytes.",
|
|
||||||
}, []string{"type"})
|
|
||||||
|
|
||||||
prometheusBufferMaxSize = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
|
||||||
Namespace: conf.PrometheusNamespace,
|
|
||||||
Name: "buffer_max_size_bytes",
|
|
||||||
Help: "A gauge of the buffer max size in bytes.",
|
|
||||||
}, []string{"type"})
|
|
||||||
|
|
||||||
prometheusVipsMemory = prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
|
||||||
Namespace: conf.PrometheusNamespace,
|
|
||||||
Name: "vips_memory_bytes",
|
|
||||||
Help: "A gauge of the vips tracked memory usage in bytes.",
|
|
||||||
}, vipsGetMem)
|
|
||||||
|
|
||||||
prometheusVipsMaxMemory = prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
|
||||||
Namespace: conf.PrometheusNamespace,
|
|
||||||
Name: "vips_max_memory_bytes",
|
|
||||||
Help: "A gauge of the max vips tracked memory usage in bytes.",
|
|
||||||
}, vipsGetMemHighwater)
|
|
||||||
|
|
||||||
prometheusVipsAllocs = prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
|
||||||
Namespace: conf.PrometheusNamespace,
|
|
||||||
Name: "vips_allocs",
|
|
||||||
Help: "A gauge of the number of active vips allocations.",
|
|
||||||
}, vipsGetAllocs)
|
|
||||||
|
|
||||||
prometheus.MustRegister(
|
|
||||||
prometheusRequestsTotal,
|
|
||||||
prometheusErrorsTotal,
|
|
||||||
prometheusRequestDuration,
|
|
||||||
prometheusDownloadDuration,
|
|
||||||
prometheusProcessingDuration,
|
|
||||||
prometheusBufferSize,
|
|
||||||
prometheusBufferDefaultSize,
|
|
||||||
prometheusBufferMaxSize,
|
|
||||||
prometheusVipsMemory,
|
|
||||||
prometheusVipsMaxMemory,
|
|
||||||
prometheusVipsAllocs,
|
|
||||||
)
|
|
||||||
|
|
||||||
prometheusEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func startPrometheusServer(cancel context.CancelFunc) error {
|
|
||||||
if !prometheusEnabled {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
s := http.Server{Handler: promhttp.Handler()}
|
|
||||||
|
|
||||||
l, err := listenReuseport("tcp", conf.PrometheusBind)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Can't start Prometheus metrics server: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
logNotice("Starting Prometheus server at %s", conf.PrometheusBind)
|
|
||||||
if err := s.Serve(l); err != nil && err != http.ErrServerClosed {
|
|
||||||
logError(err.Error())
|
|
||||||
}
|
|
||||||
cancel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func startPrometheusDuration(m prometheus.Histogram) func() {
|
|
||||||
if !prometheusEnabled {
|
|
||||||
return func() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
t := time.Now()
|
|
||||||
return func() {
|
|
||||||
m.Observe(time.Since(t).Seconds())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func incrementPrometheusErrorsTotal(t string) {
|
|
||||||
if prometheusEnabled {
|
|
||||||
prometheusErrorsTotal.With(prometheus.Labels{"type": t}).Inc()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func incrementPrometheusRequestsTotal() {
|
|
||||||
if prometheusEnabled {
|
|
||||||
prometheusRequestsTotal.Inc()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func observePrometheusBufferSize(t string, size int) {
|
|
||||||
if prometheusEnabled {
|
|
||||||
prometheusBufferSize.With(prometheus.Labels{"type": t}).Observe(float64(size))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setPrometheusBufferDefaultSize(t string, size int) {
|
|
||||||
if prometheusEnabled {
|
|
||||||
prometheusBufferDefaultSize.With(prometheus.Labels{"type": t}).Set(float64(size))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setPrometheusBufferMaxSize(t string, size int) {
|
|
||||||
if prometheusEnabled {
|
|
||||||
prometheusBufferMaxSize.With(prometheus.Labels{"type": t}).Set(float64(size))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
15
reuseport/listen_no_reuseport.go
Normal file
15
reuseport/listen_no_reuseport.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// +build !linux,!darwin !go1.11
|
||||||
|
|
||||||
|
package reuseport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Listen(network, address string) (net.Listener, error) {
|
||||||
|
if config.SoReuseport {
|
||||||
|
log.Warning("SO_REUSEPORT support is not implemented for your OS or Go version")
|
||||||
|
}
|
||||||
|
|
||||||
|
return net.Listen(network, address)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// +build linux darwin
|
// +build linux darwin
|
||||||
// +build go1.11
|
// +build go1.11
|
||||||
|
|
||||||
package main
|
package reuseport
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -9,10 +9,12 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func listenReuseport(network, address string) (net.Listener, error) {
|
func Listen(network, address string) (net.Listener, error) {
|
||||||
if !conf.SoReuseport {
|
if !config.SoReuseport {
|
||||||
return net.Listen(network, address)
|
return net.Listen(network, address)
|
||||||
}
|
}
|
||||||
|
|
||||||
55
router/logging.go
Normal file
55
router/logging.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/ierrors"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LogRequest(reqID string, r *http.Request) {
|
||||||
|
path := r.RequestURI
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"request_id": reqID,
|
||||||
|
"method": r.Method,
|
||||||
|
}).Infof("Started %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogResponse(reqID string, r *http.Request, status int, err *ierrors.Error, additional ...log.Fields) {
|
||||||
|
var level log.Level
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case status >= 500:
|
||||||
|
level = log.ErrorLevel
|
||||||
|
case status >= 400:
|
||||||
|
level = log.WarnLevel
|
||||||
|
default:
|
||||||
|
level = log.InfoLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := log.Fields{
|
||||||
|
"request_id": reqID,
|
||||||
|
"method": r.Method,
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fields["error"] = err
|
||||||
|
|
||||||
|
if stack := err.FormatStack(); len(stack) > 0 {
|
||||||
|
fields["stack"] = stack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range additional {
|
||||||
|
for k, v := range f {
|
||||||
|
fields[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields(fields).Logf(
|
||||||
|
level,
|
||||||
|
"Completed in %s %s", ctxTime(r.Context()), r.RequestURI,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
nanoid "github.com/matoous/go-nanoid/v2"
|
nanoid "github.com/matoous/go-nanoid/v2"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -16,23 +17,23 @@ var (
|
|||||||
requestIDRe = regexp.MustCompile(`^[A-Za-z0-9_\-]+$`)
|
requestIDRe = regexp.MustCompile(`^[A-Za-z0-9_\-]+$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
type routeHandler func(string, http.ResponseWriter, *http.Request)
|
type RouteHandler func(string, http.ResponseWriter, *http.Request)
|
||||||
type panicHandler func(string, http.ResponseWriter, *http.Request, error)
|
type PanicHandler func(string, http.ResponseWriter, *http.Request, error)
|
||||||
|
|
||||||
type route struct {
|
type route struct {
|
||||||
Method string
|
Method string
|
||||||
Prefix string
|
Prefix string
|
||||||
Handler routeHandler
|
Handler RouteHandler
|
||||||
Exact bool
|
Exact bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type router struct {
|
type Router struct {
|
||||||
prefix string
|
prefix string
|
||||||
Routes []*route
|
Routes []*route
|
||||||
PanicHandler panicHandler
|
PanicHandler PanicHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *route) IsMatch(req *http.Request) bool {
|
func (r *route) isMatch(req *http.Request) bool {
|
||||||
if r.Method != req.Method {
|
if r.Method != req.Method {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -44,34 +45,34 @@ func (r *route) IsMatch(req *http.Request) bool {
|
|||||||
return strings.HasPrefix(req.URL.Path, r.Prefix)
|
return strings.HasPrefix(req.URL.Path, r.Prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRouter(prefix string) *router {
|
func New(prefix string) *Router {
|
||||||
return &router{
|
return &Router{
|
||||||
prefix: prefix,
|
prefix: prefix,
|
||||||
Routes: make([]*route, 0),
|
Routes: make([]*route, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) Add(method, prefix string, handler routeHandler, exact bool) {
|
func (r *Router) Add(method, prefix string, handler RouteHandler, exact bool) {
|
||||||
r.Routes = append(
|
r.Routes = append(
|
||||||
r.Routes,
|
r.Routes,
|
||||||
&route{Method: method, Prefix: r.prefix + prefix, Handler: handler, Exact: exact},
|
&route{Method: method, Prefix: r.prefix + prefix, Handler: handler, Exact: exact},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) GET(prefix string, handler routeHandler, exact bool) {
|
func (r *Router) GET(prefix string, handler RouteHandler, exact bool) {
|
||||||
r.Add(http.MethodGet, prefix, handler, exact)
|
r.Add(http.MethodGet, prefix, handler, exact)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) OPTIONS(prefix string, handler routeHandler, exact bool) {
|
func (r *Router) OPTIONS(prefix string, handler RouteHandler, exact bool) {
|
||||||
r.Add(http.MethodOptions, prefix, handler, exact)
|
r.Add(http.MethodOptions, prefix, handler, exact)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) HEAD(prefix string, handler routeHandler, exact bool) {
|
func (r *Router) HEAD(prefix string, handler RouteHandler, exact bool) {
|
||||||
r.Add(http.MethodHead, prefix, handler, exact)
|
r.Add(http.MethodHead, prefix, handler, exact)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
req = req.WithContext(setTimerSince(req.Context()))
|
req = setRequestTime(req)
|
||||||
|
|
||||||
reqID := req.Header.Get(xRequestIDHeader)
|
reqID := req.Header.Get(xRequestIDHeader)
|
||||||
|
|
||||||
@@ -92,16 +93,16 @@ func (r *router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
logRequest(reqID, req)
|
LogRequest(reqID, req)
|
||||||
|
|
||||||
for _, rr := range r.Routes {
|
for _, rr := range r.Routes {
|
||||||
if rr.IsMatch(req) {
|
if rr.isMatch(req) {
|
||||||
rr.Handler(reqID, rw, req)
|
rr.Handler(reqID, rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logWarning("Route for %s is not defined", req.URL.Path)
|
log.Warningf("Route for %s is not defined", req.URL.Path)
|
||||||
|
|
||||||
rw.WriteHeader(404)
|
rw.WriteHeader(404)
|
||||||
}
|
}
|
||||||
43
router/timer.go
Normal file
43
router/timer.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/ierrors"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
type timerSinceCtxKey = struct{}
|
||||||
|
|
||||||
|
func setRequestTime(r *http.Request) *http.Request {
|
||||||
|
return r.WithContext(
|
||||||
|
context.WithValue(r.Context(), timerSinceCtxKey{}, time.Now()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctxTime(ctx context.Context) time.Duration {
|
||||||
|
if t, ok := ctx.Value(timerSinceCtxKey{}).(time.Time); ok {
|
||||||
|
return time.Since(t)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckTimeout(ctx context.Context) {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
d := ctxTime(ctx)
|
||||||
|
|
||||||
|
if ctx.Err() != context.DeadlineExceeded {
|
||||||
|
panic(ierrors.New(499, fmt.Sprintf("Request was cancelled after %v", d), "Cancelled"))
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics.SendTimeout(ctx, d)
|
||||||
|
|
||||||
|
panic(ierrors.New(503, fmt.Sprintf("Timeout after %v", d), "Timeout"))
|
||||||
|
default:
|
||||||
|
// Go ahead
|
||||||
|
}
|
||||||
|
}
|
||||||
16
security/image_size.go
Normal file
16
security/image_size.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/ierrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrSourceResolutionTooBig = ierrors.New(422, "Source image resolution is too big", "Invalid source image")
|
||||||
|
|
||||||
|
func CheckDimensions(width, height int) error {
|
||||||
|
if width*height > config.MaxSrcResolution {
|
||||||
|
return ErrSourceResolutionTooBig
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
45
security/signature.go
Normal file
45
security/signature.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidSignature = errors.New("Invalid signature")
|
||||||
|
ErrInvalidSignatureEncoding = errors.New("Invalid signature encoding")
|
||||||
|
)
|
||||||
|
|
||||||
|
func VerifySignature(signature, path string) error {
|
||||||
|
if len(config.Keys) == 0 || len(config.Salts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
messageMAC, err := base64.RawURLEncoding.DecodeString(signature)
|
||||||
|
if err != nil {
|
||||||
|
return ErrInvalidSignatureEncoding
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(config.Keys); i++ {
|
||||||
|
if hmac.Equal(messageMAC, signatureFor(path, config.Keys[i], config.Salts[i], config.SignatureSize)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrInvalidSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
func signatureFor(str string, key, salt []byte, signatureSize int) []byte {
|
||||||
|
mac := hmac.New(sha256.New, key)
|
||||||
|
mac.Write(salt)
|
||||||
|
mac.Write([]byte(str))
|
||||||
|
expectedMAC := mac.Sum(nil)
|
||||||
|
if signatureSize < 32 {
|
||||||
|
return expectedMAC[:signatureSize]
|
||||||
|
}
|
||||||
|
return expectedMAC
|
||||||
|
}
|
||||||
56
security/signature_test.go
Normal file
56
security/signature_test.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SignatureTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignatureTestSuite) SetupTest() {
|
||||||
|
config.Reset()
|
||||||
|
|
||||||
|
config.Keys = [][]byte{[]byte("test-key")}
|
||||||
|
config.Salts = [][]byte{[]byte("test-salt")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignatureTestSuite) TestVerifySignature() {
|
||||||
|
err := VerifySignature("dtLwhdnPPiu_epMl1LrzheLpvHas-4mwvY6L3Z8WwlY", "asd")
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignatureTestSuite) TestVerifySignatureTruncated() {
|
||||||
|
config.SignatureSize = 8
|
||||||
|
|
||||||
|
err := VerifySignature("dtLwhdnPPis", "asd")
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignatureTestSuite) TestVerifySignatureInvalid() {
|
||||||
|
err := VerifySignature("dtLwhdnPPis", "asd")
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignatureTestSuite) TestVerifySignatureMultiplePairs() {
|
||||||
|
config.Keys = append(config.Keys, []byte("test-key2"))
|
||||||
|
config.Salts = append(config.Salts, []byte("test-salt2"))
|
||||||
|
|
||||||
|
err := VerifySignature("dtLwhdnPPiu_epMl1LrzheLpvHas-4mwvY6L3Z8WwlY", "asd")
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
err = VerifySignature("jbDffNPt1-XBgDccsaE-XJB9lx8JIJqdeYIZKgOqZpg", "asd")
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
err = VerifySignature("dtLwhdnPPis", "asd")
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignature(t *testing.T) {
|
||||||
|
suite.Run(t, new(SignatureTestSuite))
|
||||||
|
}
|
||||||
19
security/source.go
Normal file
19
security/source.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func VerifySourceURL(imageURL string) bool {
|
||||||
|
if len(config.AllowedSources) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, val := range config.AllowedSources {
|
||||||
|
if strings.HasPrefix(imageURL, string(val)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
59
server.go
59
server.go
@@ -7,17 +7,24 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/netutil"
|
"golang.org/x/net/netutil"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/errorreport"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/ierrors"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/reuseport"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
imgproxyIsRunningMsg = []byte("imgproxy is running")
|
imgproxyIsRunningMsg = []byte("imgproxy is running")
|
||||||
|
|
||||||
errInvalidSecret = newError(403, "Invalid secret", "Forbidden")
|
errInvalidSecret = ierrors.New(403, "Invalid secret", "Forbidden")
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildRouter() *router {
|
func buildRouter() *router.Router {
|
||||||
r := newRouter(conf.PathPrefix)
|
r := router.New(config.PathPrefix)
|
||||||
|
|
||||||
r.PanicHandler = handlePanic
|
r.PanicHandler = handlePanic
|
||||||
|
|
||||||
@@ -32,32 +39,28 @@ func buildRouter() *router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startServer(cancel context.CancelFunc) (*http.Server, error) {
|
func startServer(cancel context.CancelFunc) (*http.Server, error) {
|
||||||
l, err := listenReuseport(conf.Network, conf.Bind)
|
l, err := reuseport.Listen(config.Network, config.Bind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Can't start server: %s", err)
|
return nil, fmt.Errorf("Can't start server: %s", err)
|
||||||
}
|
}
|
||||||
l = netutil.LimitListener(l, conf.MaxClients)
|
l = netutil.LimitListener(l, config.MaxClients)
|
||||||
|
|
||||||
s := &http.Server{
|
s := &http.Server{
|
||||||
Handler: buildRouter(),
|
Handler: buildRouter(),
|
||||||
ReadTimeout: time.Duration(conf.ReadTimeout) * time.Second,
|
ReadTimeout: time.Duration(config.ReadTimeout) * time.Second,
|
||||||
MaxHeaderBytes: 1 << 20,
|
MaxHeaderBytes: 1 << 20,
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.KeepAliveTimeout > 0 {
|
if config.KeepAliveTimeout > 0 {
|
||||||
s.IdleTimeout = time.Duration(conf.KeepAliveTimeout) * time.Second
|
s.IdleTimeout = time.Duration(config.KeepAliveTimeout) * time.Second
|
||||||
} else {
|
} else {
|
||||||
s.SetKeepAlivesEnabled(false)
|
s.SetKeepAlivesEnabled(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := initProcessingHandler(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
logNotice("Starting server at %s", conf.Bind)
|
log.Infof("Starting server at %s", config.Bind)
|
||||||
if err := s.Serve(l); err != nil && err != http.ErrServerClosed {
|
if err := s.Serve(l); err != nil && err != http.ErrServerClosed {
|
||||||
logError(err.Error())
|
log.Error(err)
|
||||||
}
|
}
|
||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
@@ -66,7 +69,7 @@ func startServer(cancel context.CancelFunc) (*http.Server, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func shutdownServer(s *http.Server) {
|
func shutdownServer(s *http.Server) {
|
||||||
logNotice("Shutting down the server...")
|
log.Info("Shutting down the server...")
|
||||||
|
|
||||||
ctx, close := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, close := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer close()
|
defer close()
|
||||||
@@ -74,10 +77,10 @@ func shutdownServer(s *http.Server) {
|
|||||||
s.Shutdown(ctx)
|
s.Shutdown(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func withCORS(h routeHandler) routeHandler {
|
func withCORS(h router.RouteHandler) router.RouteHandler {
|
||||||
return func(reqID string, rw http.ResponseWriter, r *http.Request) {
|
return func(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||||
if len(conf.AllowOrigin) > 0 {
|
if len(config.AllowOrigin) > 0 {
|
||||||
rw.Header().Set("Access-Control-Allow-Origin", conf.AllowOrigin)
|
rw.Header().Set("Access-Control-Allow-Origin", config.AllowOrigin)
|
||||||
rw.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
rw.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,12 +88,12 @@ func withCORS(h routeHandler) routeHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func withSecret(h routeHandler) routeHandler {
|
func withSecret(h router.RouteHandler) router.RouteHandler {
|
||||||
if len(conf.Secret) == 0 {
|
if len(config.Secret) == 0 {
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
authHeader := []byte(fmt.Sprintf("Bearer %s", conf.Secret))
|
authHeader := []byte(fmt.Sprintf("Bearer %s", config.Secret))
|
||||||
|
|
||||||
return func(reqID string, rw http.ResponseWriter, r *http.Request) {
|
return func(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||||
if subtle.ConstantTimeCompare([]byte(r.Header.Get("Authorization")), authHeader) == 1 {
|
if subtle.ConstantTimeCompare([]byte(r.Header.Get("Authorization")), authHeader) == 1 {
|
||||||
@@ -102,17 +105,17 @@ func withSecret(h routeHandler) routeHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handlePanic(reqID string, rw http.ResponseWriter, r *http.Request, err error) {
|
func handlePanic(reqID string, rw http.ResponseWriter, r *http.Request, err error) {
|
||||||
ierr := wrapError(err, 3)
|
ierr := ierrors.Wrap(err, 3)
|
||||||
|
|
||||||
if ierr.Unexpected {
|
if ierr.Unexpected {
|
||||||
reportError(err, r)
|
errorreport.Report(err, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
logResponse(reqID, r, ierr.StatusCode, ierr, nil, nil)
|
router.LogResponse(reqID, r, ierr.StatusCode, ierr)
|
||||||
|
|
||||||
rw.WriteHeader(ierr.StatusCode)
|
rw.WriteHeader(ierr.StatusCode)
|
||||||
|
|
||||||
if conf.DevelopmentErrorsMode {
|
if config.DevelopmentErrorsMode {
|
||||||
rw.Write([]byte(ierr.Message))
|
rw.Write([]byte(ierr.Message))
|
||||||
} else {
|
} else {
|
||||||
rw.Write([]byte(ierr.PublicMessage))
|
rw.Write([]byte(ierr.PublicMessage))
|
||||||
@@ -120,18 +123,18 @@ func handlePanic(reqID string, rw http.ResponseWriter, r *http.Request, err erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleHealth(reqID string, rw http.ResponseWriter, r *http.Request) {
|
func handleHealth(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||||
logResponse(reqID, r, 200, nil, nil, nil)
|
router.LogResponse(reqID, r, 200, nil)
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
rw.Write(imgproxyIsRunningMsg)
|
rw.Write(imgproxyIsRunningMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleHead(reqID string, rw http.ResponseWriter, r *http.Request) {
|
func handleHead(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||||
logResponse(reqID, r, 200, nil, nil, nil)
|
router.LogResponse(reqID, r, 200, nil)
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleFavicon(reqID string, rw http.ResponseWriter, r *http.Request) {
|
func handleFavicon(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||||
logResponse(reqID, r, 200, nil, nil, nil)
|
router.LogResponse(reqID, r, 200, nil)
|
||||||
// TODO: Add a real favicon maybe?
|
// TODO: Add a real favicon maybe?
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
testdata/test1.png
vendored
Normal file
BIN
testdata/test1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
3
testdata/test1.svg
vendored
Normal file
3
testdata/test1.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="200" height="100">
|
||||||
|
<rect width="190" height="90" style="fill:rgb(0,0,0);stroke-width:5;stroke:rgb(255,255,255)" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 136 B |
36
timer.go
36
timer.go
@@ -1,36 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var timerSinceCtxKey = ctxKey("timerSince")
|
|
||||||
|
|
||||||
func setTimerSince(ctx context.Context) context.Context {
|
|
||||||
return context.WithValue(ctx, timerSinceCtxKey, time.Now())
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTimerSince(ctx context.Context) time.Duration {
|
|
||||||
return time.Since(ctx.Value(timerSinceCtxKey).(time.Time))
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkTimeout(ctx context.Context) {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
d := getTimerSince(ctx)
|
|
||||||
|
|
||||||
if ctx.Err() != context.DeadlineExceeded {
|
|
||||||
panic(newError(499, fmt.Sprintf("Request was cancelled after %v", d), "Cancelled"))
|
|
||||||
}
|
|
||||||
|
|
||||||
sendTimeoutToDataDog(ctx, d)
|
|
||||||
sendTimeoutToNewRelic(ctx, d)
|
|
||||||
incrementPrometheusErrorsTotal("timeout")
|
|
||||||
|
|
||||||
panic(newError(503, fmt.Sprintf("Timeout after %v", d), "Timeout"))
|
|
||||||
default:
|
|
||||||
// Go ahead
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package azure
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -8,23 +8,24 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type azureTransport struct {
|
type transport struct {
|
||||||
serviceURL *azblob.ServiceURL
|
serviceURL *azblob.ServiceURL
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAzureTransport() (http.RoundTripper, error) {
|
func New() (http.RoundTripper, error) {
|
||||||
credential, err := azblob.NewSharedKeyCredential(conf.ABSName, conf.ABSKey)
|
credential, err := azblob.NewSharedKeyCredential(config.ABSName, config.ABSKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pipeline := azblob.NewPipeline(credential, azblob.PipelineOptions{})
|
pipeline := azblob.NewPipeline(credential, azblob.PipelineOptions{})
|
||||||
|
|
||||||
endpoint := conf.ABSEndpoint
|
endpoint := config.ABSEndpoint
|
||||||
if len(endpoint) == 0 {
|
if len(endpoint) == 0 {
|
||||||
endpoint = fmt.Sprintf("https://%s.blob.core.windows.net", conf.ABSName)
|
endpoint = fmt.Sprintf("https://%s.blob.core.windows.net", config.ABSName)
|
||||||
}
|
}
|
||||||
endpointURL, err := url.Parse(endpoint)
|
endpointURL, err := url.Parse(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -33,10 +34,10 @@ func newAzureTransport() (http.RoundTripper, error) {
|
|||||||
|
|
||||||
serviceURL := azblob.NewServiceURL(*endpointURL, pipeline)
|
serviceURL := azblob.NewServiceURL(*endpointURL, pipeline)
|
||||||
|
|
||||||
return azureTransport{&serviceURL}, nil
|
return transport{&serviceURL}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t azureTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||||
containerURL := t.serviceURL.NewContainerURL(strings.ToLower(req.URL.Host))
|
containerURL := t.serviceURL.NewContainerURL(strings.ToLower(req.URL.Host))
|
||||||
blobURL := containerURL.NewBlockBlobURL(strings.TrimPrefix(req.URL.Path, "/"))
|
blobURL := containerURL.NewBlockBlobURL(strings.TrimPrefix(req.URL.Path, "/"))
|
||||||
|
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
package main
|
package fs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fsTransport struct {
|
type transport struct {
|
||||||
fs http.Dir
|
fs http.Dir
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFsTransport() fsTransport {
|
func New() transport {
|
||||||
return fsTransport{fs: http.Dir(conf.LocalFileSystemRoot)}
|
return transport{fs: http.Dir(config.LocalFileSystemRoot)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t fsTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||||
f, err := t.fs.Open(req.URL.Path)
|
f, err := t.fs.Open(req.URL.Path)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package gcs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -8,21 +8,22 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"cloud.google.com/go/storage"
|
"cloud.google.com/go/storage"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
"google.golang.org/api/option"
|
"google.golang.org/api/option"
|
||||||
)
|
)
|
||||||
|
|
||||||
type gcsTransport struct {
|
type transport struct {
|
||||||
client *storage.Client
|
client *storage.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGCSTransport() (http.RoundTripper, error) {
|
func New() (http.RoundTripper, error) {
|
||||||
var (
|
var (
|
||||||
client *storage.Client
|
client *storage.Client
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(conf.GCSKey) > 0 {
|
if len(config.GCSKey) > 0 {
|
||||||
client, err = storage.NewClient(context.Background(), option.WithCredentialsJSON([]byte(conf.GCSKey)))
|
client, err = storage.NewClient(context.Background(), option.WithCredentialsJSON([]byte(config.GCSKey)))
|
||||||
} else {
|
} else {
|
||||||
client, err = storage.NewClient(context.Background())
|
client, err = storage.NewClient(context.Background())
|
||||||
}
|
}
|
||||||
@@ -31,10 +32,10 @@ func newGCSTransport() (http.RoundTripper, error) {
|
|||||||
return nil, fmt.Errorf("Can't create GCS client: %s", err)
|
return nil, fmt.Errorf("Can't create GCS client: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return gcsTransport{client}, nil
|
return transport{client}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t gcsTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
func (t transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
bkt := t.client.Bucket(req.URL.Host)
|
bkt := t.client.Bucket(req.URL.Host)
|
||||||
obj := bkt.Object(strings.TrimPrefix(req.URL.Path, "/"))
|
obj := bkt.Object(strings.TrimPrefix(req.URL.Path, "/"))
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package s3
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -7,22 +7,24 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
"github.com/aws/aws-sdk-go/service/s3"
|
"github.com/aws/aws-sdk-go/service/s3"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// s3Transport implements RoundTripper for the 's3' protocol.
|
// transport implements RoundTripper for the 's3' protocol.
|
||||||
type s3Transport struct {
|
type transport struct {
|
||||||
svc *s3.S3
|
svc *s3.S3
|
||||||
}
|
}
|
||||||
|
|
||||||
func newS3Transport() (http.RoundTripper, error) {
|
func New() (http.RoundTripper, error) {
|
||||||
s3Conf := aws.NewConfig()
|
s3Conf := aws.NewConfig()
|
||||||
|
|
||||||
if len(conf.S3Region) != 0 {
|
if len(config.S3Region) != 0 {
|
||||||
s3Conf.Region = aws.String(conf.S3Region)
|
s3Conf.Region = aws.String(config.S3Region)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(conf.S3Endpoint) != 0 {
|
if len(config.S3Endpoint) != 0 {
|
||||||
s3Conf.Endpoint = aws.String(conf.S3Endpoint)
|
s3Conf.Endpoint = aws.String(config.S3Endpoint)
|
||||||
s3Conf.S3ForcePathStyle = aws.Bool(true)
|
s3Conf.S3ForcePathStyle = aws.Bool(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,10 +37,10 @@ func newS3Transport() (http.RoundTripper, error) {
|
|||||||
sess.Config.Region = aws.String("us-west-1")
|
sess.Config.Region = aws.String("us-west-1")
|
||||||
}
|
}
|
||||||
|
|
||||||
return s3Transport{s3.New(sess, s3Conf)}, nil
|
return transport{s3.New(sess, s3Conf)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t s3Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||||
input := &s3.GetObjectInput{
|
input := &s3.GetObjectInput{
|
||||||
Bucket: aws.String(req.URL.Host),
|
Bucket: aws.String(req.URL.Host),
|
||||||
Key: aws.String(req.URL.Path),
|
Key: aws.String(req.URL.Path),
|
||||||
56
utils.go
56
utils.go
@@ -1,56 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
"strings"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
func maxInt(a, b int) int {
|
|
||||||
if a > b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func minInt(a, b int) int {
|
|
||||||
if a < b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func minNonZeroInt(a, b int) int {
|
|
||||||
switch {
|
|
||||||
case a == 0:
|
|
||||||
return b
|
|
||||||
case b == 0:
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
return minInt(a, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func roundToInt(a float64) int {
|
|
||||||
return int(math.Round(a))
|
|
||||||
}
|
|
||||||
|
|
||||||
func scaleInt(a int, scale float64) int {
|
|
||||||
if a == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return roundToInt(float64(a) * scale)
|
|
||||||
}
|
|
||||||
|
|
||||||
func trimAfter(s string, sep byte) string {
|
|
||||||
i := strings.IndexByte(s, sep)
|
|
||||||
if i < 0 {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return s[:i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func ptrToBytes(ptr unsafe.Pointer, size int) []byte {
|
|
||||||
return (*[math.MaxInt32]byte)(ptr)[:int(size):int(size)]
|
|
||||||
}
|
|
||||||
7
version/version.go
Normal file
7
version/version.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
const version = "2.16.1"
|
||||||
|
|
||||||
|
func Version() string {
|
||||||
|
return version
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package vips
|
||||||
|
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
34
vips/color.go
Normal file
34
vips/color.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package vips
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var hexColorRegex = regexp.MustCompile("^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
|
||||||
|
|
||||||
|
const (
|
||||||
|
hexColorLongFormat = "%02x%02x%02x"
|
||||||
|
hexColorShortFormat = "%1x%1x%1x"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Color struct{ R, G, B uint8 }
|
||||||
|
|
||||||
|
func ColorFromHex(hexcolor string) (Color, error) {
|
||||||
|
c := Color{}
|
||||||
|
|
||||||
|
if !hexColorRegex.MatchString(hexcolor) {
|
||||||
|
return c, fmt.Errorf("Invalid hex color: %s", hexcolor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hexcolor) == 3 {
|
||||||
|
fmt.Sscanf(hexcolor, hexColorShortFormat, &c.R, &c.G, &c.B)
|
||||||
|
c.R *= 17
|
||||||
|
c.G *= 17
|
||||||
|
c.B *= 17
|
||||||
|
} else {
|
||||||
|
fmt.Sscanf(hexcolor, hexColorLongFormat, &c.R, &c.G, &c.B)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
132
vips/ico.go
Normal file
132
vips/ico.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package vips
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include "vips.h"
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagemeta"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (img *Image) loadIco(data []byte, shrink int, scale float64, pages int) error {
|
||||||
|
icoMeta, err := imagemeta.DecodeIcoMeta(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := icoMeta.BestImageOffset()
|
||||||
|
size := icoMeta.BestImageSize()
|
||||||
|
|
||||||
|
internalData := data[offset : offset+size]
|
||||||
|
|
||||||
|
var format string
|
||||||
|
|
||||||
|
meta, err := imagemeta.DecodeMeta(bytes.NewReader(internalData))
|
||||||
|
if err != nil {
|
||||||
|
// Looks like it's BMP with an incomplete header
|
||||||
|
if d, err := imagemeta.FixBmpHeader(internalData); err == nil {
|
||||||
|
format = "bmp"
|
||||||
|
internalData = d
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
format = meta.Format()
|
||||||
|
}
|
||||||
|
|
||||||
|
internalType, ok := imagetype.Types[format]
|
||||||
|
if !ok || internalType == imagetype.ICO || !SupportsLoad(internalType) {
|
||||||
|
return fmt.Errorf("Can't load %s from ICO", meta.Format())
|
||||||
|
}
|
||||||
|
|
||||||
|
imgdata := imagedata.ImageData{
|
||||||
|
Type: internalType,
|
||||||
|
Data: internalData,
|
||||||
|
}
|
||||||
|
|
||||||
|
return img.Load(&imgdata, shrink, scale, pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (img *Image) saveAsIco() (*imagedata.ImageData, error) {
|
||||||
|
if img.Width() > 256 || img.Height() > 256 {
|
||||||
|
return nil, errors.New("Image dimensions is too big. Max dimension size for ICO is 256")
|
||||||
|
}
|
||||||
|
|
||||||
|
var ptr unsafe.Pointer
|
||||||
|
imgsize := C.size_t(0)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
C.g_free_go(&ptr)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if C.vips_pngsave_go(img.VipsImage, &ptr, &imgsize, 0, 0, 256) != 0 {
|
||||||
|
return nil, Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
b := ptrToBytes(ptr, int(imgsize))
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.Grow(22 + int(imgsize))
|
||||||
|
|
||||||
|
// ICONDIR header
|
||||||
|
if _, err := buf.Write([]byte{0, 0, 1, 0, 1, 0}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ICONDIRENTRY
|
||||||
|
if _, err := buf.Write([]byte{
|
||||||
|
byte(img.Width() % 256),
|
||||||
|
byte(img.Height() % 256),
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Number of colors. Not supported in our case
|
||||||
|
if err := buf.WriteByte(0); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Reserved
|
||||||
|
if err := buf.WriteByte(0); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Color planes. Always 1 in our case
|
||||||
|
if _, err := buf.Write([]byte{1, 0}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Bits per pixel
|
||||||
|
if img.HasAlpha() {
|
||||||
|
if _, err := buf.Write([]byte{32, 0}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := buf.Write([]byte{24, 0}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Image data size
|
||||||
|
if err := binary.Write(buf, binary.LittleEndian, uint32(imgsize)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Image data offset. Always 22 in our case
|
||||||
|
if _, err := buf.Write([]byte{22, 0, 0, 0}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := buf.Write(b); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
imgdata := imagedata.ImageData{
|
||||||
|
Type: imagetype.ICO,
|
||||||
|
Data: buf.Bytes(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &imgdata, nil
|
||||||
|
}
|
||||||
19
vips/testing_helpers.go
Normal file
19
vips/testing_helpers.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package vips
|
||||||
|
|
||||||
|
import "github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
|
|
||||||
|
func DisableLoadSupport(it imagetype.Type) {
|
||||||
|
typeSupportLoad[it] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResetLoadSupport() {
|
||||||
|
typeSupportLoad = make(map[imagetype.Type]bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DisableSaveSupport(it imagetype.Type) {
|
||||||
|
typeSupportSave[it] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResetSaveSupport() {
|
||||||
|
typeSupportSave = make(map[imagetype.Type]bool)
|
||||||
|
}
|
||||||
@@ -200,16 +200,27 @@ vips_image_set_array_int_go(VipsImage *image, const char *name, const int *array
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
gboolean
|
|
||||||
vips_image_hasalpha_go(VipsImage * in) {
|
|
||||||
return vips_image_hasalpha(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
int
|
||||||
vips_addalpha_go(VipsImage *in, VipsImage **out) {
|
vips_addalpha_go(VipsImage *in, VipsImage **out) {
|
||||||
return vips_addalpha(in, out, NULL);
|
return vips_addalpha(in, out, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
vips_premultiply_go(VipsImage *in, VipsImage **out) {
|
||||||
|
if (!vips_image_hasalpha(in))
|
||||||
|
return vips_copy(in, out, NULL);
|
||||||
|
|
||||||
|
return vips_premultiply(in, out, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
vips_unpremultiply_go(VipsImage *in, VipsImage **out) {
|
||||||
|
if (!vips_image_hasalpha(in))
|
||||||
|
return vips_copy(in, out, NULL);
|
||||||
|
|
||||||
|
return vips_unpremultiply(in, out, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
vips_copy_go(VipsImage *in, VipsImage **out) {
|
vips_copy_go(VipsImage *in, VipsImage **out) {
|
||||||
return vips_copy(in, out, NULL);
|
return vips_copy(in, out, NULL);
|
||||||
@@ -227,37 +238,21 @@ vips_rad2float_go(VipsImage *in, VipsImage **out) {
|
|||||||
|
|
||||||
int
|
int
|
||||||
vips_resize_go(VipsImage *in, VipsImage **out, double wscale, double hscale) {
|
vips_resize_go(VipsImage *in, VipsImage **out, double wscale, double hscale) {
|
||||||
return vips_resize(in, out, wscale, "vscale", hscale, NULL);
|
if (!vips_image_hasalpha(in))
|
||||||
}
|
return vips_resize(in, out, wscale, "vscale", hscale, NULL);
|
||||||
|
|
||||||
int
|
VipsBandFormat format = vips_band_format(in);
|
||||||
vips_resize_with_premultiply(VipsImage *in, VipsImage **out, double wscale, double hscale) {
|
|
||||||
VipsBandFormat format;
|
|
||||||
VipsImage *tmp1, *tmp2;
|
|
||||||
|
|
||||||
format = vips_band_format(in);
|
VipsImage *base = vips_image_new();
|
||||||
|
VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 3);
|
||||||
|
|
||||||
if (vips_premultiply(in, &tmp1, NULL))
|
int res =
|
||||||
return 1;
|
vips_premultiply(in, &t[0], NULL) ||
|
||||||
|
vips_resize(t[0], &t[1], wscale, "vscale", hscale, NULL) ||
|
||||||
|
vips_unpremultiply(t[1], &t[2], NULL) ||
|
||||||
|
vips_cast(t[2], out, format, NULL);
|
||||||
|
|
||||||
if (vips_resize(tmp1, &tmp2, wscale, "vscale", hscale, NULL)) {
|
clear_image(&base);
|
||||||
clear_image(&tmp1);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
swap_and_clear(&tmp1, tmp2);
|
|
||||||
|
|
||||||
if (vips_unpremultiply(tmp1, &tmp2, NULL)) {
|
|
||||||
clear_image(&tmp1);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
swap_and_clear(&tmp1, tmp2);
|
|
||||||
|
|
||||||
if (vips_cast(tmp1, out, format, NULL)) {
|
|
||||||
clear_image(&tmp1);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
clear_image(&tmp1);
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -353,6 +348,9 @@ vips_sharpen_go(VipsImage *in, VipsImage **out, double sigma) {
|
|||||||
|
|
||||||
int
|
int
|
||||||
vips_flatten_go(VipsImage *in, VipsImage **out, double r, double g, double b) {
|
vips_flatten_go(VipsImage *in, VipsImage **out, double r, double g, double b) {
|
||||||
|
if (!vips_image_hasalpha(in))
|
||||||
|
return vips_copy(in, out, NULL);
|
||||||
|
|
||||||
VipsArrayDouble *bg = vips_array_double_newv(3, r, g, b);
|
VipsArrayDouble *bg = vips_array_double_newv(3, r, g, b);
|
||||||
int res = vips_flatten(in, out, "background", bg, NULL);
|
int res = vips_flatten(in, out, "background", bg, NULL);
|
||||||
vips_area_unref((VipsArea *)bg);
|
vips_area_unref((VipsArea *)bg);
|
||||||
@@ -453,23 +451,24 @@ vips_replicate_go(VipsImage *in, VipsImage **out, int width, int height) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
vips_embed_go(VipsImage *in, VipsImage **out, int x, int y, int width, int height, double *bg, int bgn) {
|
vips_embed_go(VipsImage *in, VipsImage **out, int x, int y, int width, int height) {
|
||||||
VipsArrayDouble *bga = vips_array_double_new(bg, bgn);
|
VipsImage *base = vips_image_new();
|
||||||
int ret = vips_embed(
|
VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 2);
|
||||||
in, out, x, y, width, height,
|
|
||||||
"extend", VIPS_EXTEND_BACKGROUND,
|
int ret =
|
||||||
"background", bga,
|
vips_colourspace(in, &t[0], VIPS_INTERPRETATION_sRGB, NULL) ||
|
||||||
NULL
|
vips_ensure_alpha(t[0], &t[1]) ||
|
||||||
);
|
vips_embed(t[1], out, x, y, width, height, "extend", VIPS_EXTEND_BLACK, NULL);
|
||||||
vips_area_unref((VipsArea *)bga);
|
|
||||||
|
clear_image(&base);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
vips_ensure_alpha(VipsImage *in, VipsImage **out) {
|
vips_ensure_alpha(VipsImage *in, VipsImage **out) {
|
||||||
if (vips_image_hasalpha_go(in)) {
|
if (vips_image_hasalpha(in))
|
||||||
return vips_copy(in, out, NULL);
|
return vips_copy(in, out, NULL);
|
||||||
}
|
|
||||||
|
|
||||||
return vips_bandjoin_const1(in, out, 255, NULL);
|
return vips_bandjoin_const1(in, out, 255, NULL);
|
||||||
}
|
}
|
||||||
@@ -477,28 +476,33 @@ vips_ensure_alpha(VipsImage *in, VipsImage **out) {
|
|||||||
int
|
int
|
||||||
vips_apply_watermark(VipsImage *in, VipsImage *watermark, VipsImage **out, double opacity) {
|
vips_apply_watermark(VipsImage *in, VipsImage *watermark, VipsImage **out, double opacity) {
|
||||||
VipsImage *base = vips_image_new();
|
VipsImage *base = vips_image_new();
|
||||||
VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 5);
|
VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 6);
|
||||||
|
|
||||||
|
if (vips_ensure_alpha(watermark, &t[0])) {
|
||||||
|
clear_image(&base);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (opacity < 1) {
|
if (opacity < 1) {
|
||||||
if (
|
if (
|
||||||
vips_extract_band(watermark, &t[0], 0, "n", watermark->Bands - 1, NULL) ||
|
vips_extract_band(t[0], &t[1], 0, "n", t[0]->Bands - 1, NULL) ||
|
||||||
vips_extract_band(watermark, &t[1], watermark->Bands - 1, "n", 1, NULL) ||
|
vips_extract_band(t[0], &t[2], t[0]->Bands - 1, "n", 1, NULL) ||
|
||||||
vips_linear1(t[1], &t[2], opacity, 0, NULL) ||
|
vips_linear1(t[2], &t[3], opacity, 0, NULL) ||
|
||||||
vips_bandjoin2(t[0], t[2], &t[3], NULL)
|
vips_bandjoin2(t[1], t[3], &t[4], NULL)
|
||||||
) {
|
) {
|
||||||
clear_image(&base);
|
clear_image(&base);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (vips_copy(watermark, &t[3], NULL)) {
|
if (vips_copy(t[0], &t[4], NULL)) {
|
||||||
clear_image(&base);
|
clear_image(&base);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int res =
|
int res =
|
||||||
vips_composite2(in, t[3], &t[4], VIPS_BLEND_MODE_OVER, "compositing_space", in->Type, NULL) ||
|
vips_composite2(in, t[4], &t[5], VIPS_BLEND_MODE_OVER, "compositing_space", in->Type, NULL) ||
|
||||||
vips_cast(t[4], out, vips_image_get_format(in), NULL);
|
vips_cast(t[5], out, vips_image_get_format(in), NULL);
|
||||||
|
|
||||||
clear_image(&base);
|
clear_image(&base);
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package vips
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#cgo pkg-config: vips
|
#cgo pkg-config: vips
|
||||||
@@ -8,26 +8,29 @@ package main
|
|||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/ierrors"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/metrics/prometheus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type vipsImage struct {
|
type Image struct {
|
||||||
VipsImage *C.VipsImage
|
VipsImage *C.VipsImage
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
vipsTypeSupportLoad = make(map[imageType]bool)
|
typeSupportLoad = make(map[imagetype.Type]bool)
|
||||||
vipsTypeSupportSave = make(map[imageType]bool)
|
typeSupportSave = make(map[imagetype.Type]bool)
|
||||||
|
|
||||||
watermark *imageData
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var vipsConf struct {
|
var vipsConf struct {
|
||||||
@@ -35,10 +38,9 @@ var vipsConf struct {
|
|||||||
PngInterlaced C.int
|
PngInterlaced C.int
|
||||||
PngQuantize C.int
|
PngQuantize C.int
|
||||||
PngQuantizationColors C.int
|
PngQuantizationColors C.int
|
||||||
WatermarkOpacity C.double
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initVips() error {
|
func Init() error {
|
||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
defer runtime.UnlockOSThread()
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
@@ -66,62 +68,87 @@ func initVips() error {
|
|||||||
C.vips_cache_set_trace(C.gboolean(1))
|
C.vips_cache_set_trace(C.gboolean(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, imgtype := range imageTypes {
|
vipsConf.JpegProgressive = gbool(config.JpegProgressive)
|
||||||
vipsTypeSupportLoad[imgtype] = int(C.vips_type_find_load_go(C.int(imgtype))) != 0
|
vipsConf.PngInterlaced = gbool(config.PngInterlaced)
|
||||||
vipsTypeSupportSave[imgtype] = int(C.vips_type_find_save_go(C.int(imgtype))) != 0
|
vipsConf.PngQuantize = gbool(config.PngQuantize)
|
||||||
}
|
vipsConf.PngQuantizationColors = C.int(config.PngQuantizationColors)
|
||||||
|
|
||||||
if conf.JpegProgressive {
|
prometheus.AddGaugeFunc(
|
||||||
vipsConf.JpegProgressive = C.int(1)
|
"vips_memory_bytes",
|
||||||
}
|
"A gauge of the vips tracked memory usage in bytes.",
|
||||||
|
GetMem,
|
||||||
if conf.PngInterlaced {
|
)
|
||||||
vipsConf.PngInterlaced = C.int(1)
|
prometheus.AddGaugeFunc(
|
||||||
}
|
"vips_max_memory_bytes",
|
||||||
|
"A gauge of the max vips tracked memory usage in bytes.",
|
||||||
if conf.PngQuantize {
|
GetMemHighwater,
|
||||||
vipsConf.PngQuantize = C.int(1)
|
)
|
||||||
}
|
prometheus.AddGaugeFunc(
|
||||||
|
"vips_allocs",
|
||||||
vipsConf.PngQuantizationColors = C.int(conf.PngQuantizationColors)
|
"A gauge of the number of active vips allocations.",
|
||||||
|
GetAllocs,
|
||||||
vipsConf.WatermarkOpacity = C.double(conf.WatermarkOpacity)
|
)
|
||||||
|
|
||||||
if err := vipsLoadWatermark(); err != nil {
|
|
||||||
C.vips_shutdown()
|
|
||||||
return fmt.Errorf("Can't load watermark: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func shutdownVips() {
|
func Shutdown() {
|
||||||
C.vips_shutdown()
|
C.vips_shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
func vipsGetMem() float64 {
|
func GetMem() float64 {
|
||||||
return float64(C.vips_tracked_get_mem())
|
return float64(C.vips_tracked_get_mem())
|
||||||
}
|
}
|
||||||
|
|
||||||
func vipsGetMemHighwater() float64 {
|
func GetMemHighwater() float64 {
|
||||||
return float64(C.vips_tracked_get_mem_highwater())
|
return float64(C.vips_tracked_get_mem_highwater())
|
||||||
}
|
}
|
||||||
|
|
||||||
func vipsGetAllocs() float64 {
|
func GetAllocs() float64 {
|
||||||
return float64(C.vips_tracked_get_allocs())
|
return float64(C.vips_tracked_get_allocs())
|
||||||
}
|
}
|
||||||
|
|
||||||
func vipsCleanup() {
|
func Cleanup() {
|
||||||
C.vips_cleanup()
|
C.vips_cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
func vipsError() error {
|
func Error() error {
|
||||||
return newUnexpectedError(C.GoString(C.vips_error_buffer()), 1)
|
return ierrors.NewUnexpected(C.GoString(C.vips_error_buffer()), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func vipsLoadWatermark() (err error) {
|
func SupportsLoad(it imagetype.Type) bool {
|
||||||
watermark, err = getWatermarkData()
|
if sup, ok := typeSupportLoad[it]; ok {
|
||||||
return
|
return sup
|
||||||
|
}
|
||||||
|
|
||||||
|
sup := false
|
||||||
|
if it == imagetype.ICO {
|
||||||
|
sup = true
|
||||||
|
} else {
|
||||||
|
sup = int(C.vips_type_find_load_go(C.int(it))) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
typeSupportLoad[it] = sup
|
||||||
|
|
||||||
|
return sup
|
||||||
|
}
|
||||||
|
|
||||||
|
func SupportsSave(it imagetype.Type) bool {
|
||||||
|
if sup, ok := typeSupportSave[it]; ok {
|
||||||
|
return sup
|
||||||
|
}
|
||||||
|
|
||||||
|
sup := false
|
||||||
|
if it == imagetype.ICO {
|
||||||
|
// We save ICO content as PNG so we need to check it
|
||||||
|
sup = int(C.vips_type_find_save_go(C.int(imagetype.PNG))) != 0
|
||||||
|
} else {
|
||||||
|
sup = int(C.vips_type_find_save_go(C.int(it))) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
typeSupportSave[it] = sup
|
||||||
|
|
||||||
|
return sup
|
||||||
}
|
}
|
||||||
|
|
||||||
func gbool(b bool) C.gboolean {
|
func gbool(b bool) C.gboolean {
|
||||||
@@ -131,39 +158,51 @@ func gbool(b bool) C.gboolean {
|
|||||||
return C.gboolean(0)
|
return C.gboolean(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Width() int {
|
func ptrToBytes(ptr unsafe.Pointer, size int) []byte {
|
||||||
|
return (*[math.MaxInt32]byte)(ptr)[:int(size):int(size)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (img *Image) Width() int {
|
||||||
return int(img.VipsImage.Xsize)
|
return int(img.VipsImage.Xsize)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Height() int {
|
func (img *Image) Height() int {
|
||||||
return int(img.VipsImage.Ysize)
|
return int(img.VipsImage.Ysize)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Load(data []byte, imgtype imageType, shrink int, scale float64, pages int) error {
|
func (img *Image) Load(imgdata *imagedata.ImageData, shrink int, scale float64, pages int) error {
|
||||||
|
if imgdata.Type == imagetype.ICO {
|
||||||
|
return img.loadIco(imgdata.Data, shrink, scale, pages)
|
||||||
|
}
|
||||||
|
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
|
data := unsafe.Pointer(&imgdata.Data[0])
|
||||||
|
dataSize := C.size_t(len(imgdata.Data))
|
||||||
err := C.int(0)
|
err := C.int(0)
|
||||||
|
|
||||||
switch imgtype {
|
switch imgdata.Type {
|
||||||
case imageTypeJPEG:
|
case imagetype.JPEG:
|
||||||
err = C.vips_jpegload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.int(shrink), &tmp)
|
err = C.vips_jpegload_go(data, dataSize, C.int(shrink), &tmp)
|
||||||
case imageTypePNG:
|
case imagetype.PNG:
|
||||||
err = C.vips_pngload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), &tmp)
|
err = C.vips_pngload_go(data, dataSize, &tmp)
|
||||||
case imageTypeWEBP:
|
case imagetype.WEBP:
|
||||||
err = C.vips_webpload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.double(scale), C.int(pages), &tmp)
|
err = C.vips_webpload_go(data, dataSize, C.double(scale), C.int(pages), &tmp)
|
||||||
case imageTypeGIF:
|
case imagetype.GIF:
|
||||||
err = C.vips_gifload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.int(pages), &tmp)
|
err = C.vips_gifload_go(data, dataSize, C.int(pages), &tmp)
|
||||||
case imageTypeSVG:
|
case imagetype.SVG:
|
||||||
err = C.vips_svgload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.double(scale), &tmp)
|
err = C.vips_svgload_go(data, dataSize, C.double(scale), &tmp)
|
||||||
case imageTypeHEIC, imageTypeAVIF:
|
case imagetype.HEIC, imagetype.AVIF:
|
||||||
err = C.vips_heifload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), &tmp)
|
err = C.vips_heifload_go(data, dataSize, &tmp)
|
||||||
case imageTypeBMP:
|
case imagetype.BMP:
|
||||||
err = C.vips_bmpload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), &tmp)
|
err = C.vips_bmpload_go(data, dataSize, &tmp)
|
||||||
case imageTypeTIFF:
|
case imagetype.TIFF:
|
||||||
err = C.vips_tiffload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), &tmp)
|
err = C.vips_tiffload_go(data, dataSize, &tmp)
|
||||||
|
default:
|
||||||
|
return errors.New("Usupported image type to load")
|
||||||
}
|
}
|
||||||
if err != 0 {
|
if err != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
@@ -171,126 +210,59 @@ func (img *vipsImage) Load(data []byte, imgtype imageType, shrink int, scale flo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Save(imgtype imageType, quality int) ([]byte, context.CancelFunc, error) {
|
func (img *Image) Save(imgtype imagetype.Type, quality int) (*imagedata.ImageData, error) {
|
||||||
if imgtype == imageTypeICO {
|
if imgtype == imagetype.ICO {
|
||||||
b, err := img.SaveAsIco()
|
return img.saveAsIco()
|
||||||
return b, func() {}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var ptr unsafe.Pointer
|
var ptr unsafe.Pointer
|
||||||
|
|
||||||
cancel := func() {
|
cancel := func() {
|
||||||
C.g_free_go(&ptr)
|
C.g_free_go(&ptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := C.int(0)
|
err := C.int(0)
|
||||||
|
|
||||||
imgsize := C.size_t(0)
|
imgsize := C.size_t(0)
|
||||||
|
|
||||||
switch imgtype {
|
switch imgtype {
|
||||||
case imageTypeJPEG:
|
case imagetype.JPEG:
|
||||||
err = C.vips_jpegsave_go(img.VipsImage, &ptr, &imgsize, C.int(quality), vipsConf.JpegProgressive)
|
err = C.vips_jpegsave_go(img.VipsImage, &ptr, &imgsize, C.int(quality), vipsConf.JpegProgressive)
|
||||||
case imageTypePNG:
|
case imagetype.PNG:
|
||||||
err = C.vips_pngsave_go(img.VipsImage, &ptr, &imgsize, vipsConf.PngInterlaced, vipsConf.PngQuantize, vipsConf.PngQuantizationColors)
|
err = C.vips_pngsave_go(img.VipsImage, &ptr, &imgsize, vipsConf.PngInterlaced, vipsConf.PngQuantize, vipsConf.PngQuantizationColors)
|
||||||
case imageTypeWEBP:
|
case imagetype.WEBP:
|
||||||
err = C.vips_webpsave_go(img.VipsImage, &ptr, &imgsize, C.int(quality))
|
err = C.vips_webpsave_go(img.VipsImage, &ptr, &imgsize, C.int(quality))
|
||||||
case imageTypeGIF:
|
case imagetype.GIF:
|
||||||
err = C.vips_gifsave_go(img.VipsImage, &ptr, &imgsize)
|
err = C.vips_gifsave_go(img.VipsImage, &ptr, &imgsize)
|
||||||
case imageTypeAVIF:
|
case imagetype.AVIF:
|
||||||
err = C.vips_avifsave_go(img.VipsImage, &ptr, &imgsize, C.int(quality))
|
err = C.vips_avifsave_go(img.VipsImage, &ptr, &imgsize, C.int(quality))
|
||||||
case imageTypeBMP:
|
case imagetype.BMP:
|
||||||
err = C.vips_bmpsave_go(img.VipsImage, &ptr, &imgsize)
|
err = C.vips_bmpsave_go(img.VipsImage, &ptr, &imgsize)
|
||||||
case imageTypeTIFF:
|
case imagetype.TIFF:
|
||||||
err = C.vips_tiffsave_go(img.VipsImage, &ptr, &imgsize, C.int(quality))
|
err = C.vips_tiffsave_go(img.VipsImage, &ptr, &imgsize, C.int(quality))
|
||||||
|
default:
|
||||||
|
return nil, errors.New("Usupported image type to save")
|
||||||
}
|
}
|
||||||
if err != 0 {
|
if err != 0 {
|
||||||
C.g_free_go(&ptr)
|
cancel()
|
||||||
return nil, cancel, vipsError()
|
return nil, Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
b := ptrToBytes(ptr, int(imgsize))
|
imgdata := imagedata.ImageData{
|
||||||
|
Type: imgtype,
|
||||||
|
Data: ptrToBytes(ptr, int(imgsize)),
|
||||||
|
}
|
||||||
|
|
||||||
return b, cancel, nil
|
imgdata.SetCancel(cancel)
|
||||||
|
|
||||||
|
return &imgdata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) SaveAsIco() ([]byte, error) {
|
func (img *Image) Clear() {
|
||||||
if img.Width() > 256 || img.Height() > 256 {
|
|
||||||
return nil, errors.New("Image dimensions is too big. Max dimension size for ICO is 256")
|
|
||||||
}
|
|
||||||
|
|
||||||
var ptr unsafe.Pointer
|
|
||||||
imgsize := C.size_t(0)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
C.g_free_go(&ptr)
|
|
||||||
}()
|
|
||||||
|
|
||||||
if C.vips_pngsave_go(img.VipsImage, &ptr, &imgsize, 0, 0, 256) != 0 {
|
|
||||||
return nil, vipsError()
|
|
||||||
}
|
|
||||||
|
|
||||||
b := ptrToBytes(ptr, int(imgsize))
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
buf.Grow(22 + int(imgsize))
|
|
||||||
|
|
||||||
// ICONDIR header
|
|
||||||
if _, err := buf.Write([]byte{0, 0, 1, 0, 1, 0}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ICONDIRENTRY
|
|
||||||
if _, err := buf.Write([]byte{
|
|
||||||
byte(img.Width() % 256),
|
|
||||||
byte(img.Height() % 256),
|
|
||||||
}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Number of colors. Not supported in our case
|
|
||||||
if err := buf.WriteByte(0); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Reserved
|
|
||||||
if err := buf.WriteByte(0); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Color planes. Always 1 in our case
|
|
||||||
if _, err := buf.Write([]byte{1, 0}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Bits per pixel
|
|
||||||
if img.HasAlpha() {
|
|
||||||
if _, err := buf.Write([]byte{32, 0}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if _, err := buf.Write([]byte{24, 0}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Image data size
|
|
||||||
if err := binary.Write(buf, binary.LittleEndian, uint32(imgsize)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Image data offset. Always 22 in our case
|
|
||||||
if _, err := buf.Write([]byte{22, 0, 0, 0}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := buf.Write(b); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (img *vipsImage) Clear() {
|
|
||||||
if img.VipsImage != nil {
|
if img.VipsImage != nil {
|
||||||
C.clear_image(&img.VipsImage)
|
C.clear_image(&img.VipsImage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Arrayjoin(in []*vipsImage) error {
|
func (img *Image) Arrayjoin(in []*Image) error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
arr := make([]*C.VipsImage, len(in))
|
arr := make([]*C.VipsImage, len(in))
|
||||||
@@ -299,35 +271,53 @@ func (img *vipsImage) Arrayjoin(in []*vipsImage) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if C.vips_arrayjoin_go(&arr[0], &tmp, C.int(len(arr))) != 0 {
|
if C.vips_arrayjoin_go(&arr[0], &tmp, C.int(len(arr))) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func vipsSupportAnimation(imgtype imageType) bool {
|
func (img *Image) IsAnimated() bool {
|
||||||
return imgtype == imageTypeGIF || imgtype == imageTypeWEBP
|
|
||||||
}
|
|
||||||
|
|
||||||
func (img *vipsImage) IsAnimated() bool {
|
|
||||||
return C.vips_is_animated(img.VipsImage) > 0
|
return C.vips_is_animated(img.VipsImage) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) HasAlpha() bool {
|
func (img *Image) HasAlpha() bool {
|
||||||
return C.vips_image_hasalpha_go(img.VipsImage) > 0
|
return C.vips_image_hasalpha(img.VipsImage) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) GetInt(name string) (int, error) {
|
func (img *Image) Premultiply() error {
|
||||||
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
|
if C.vips_premultiply_go(img.VipsImage, &tmp) != 0 {
|
||||||
|
return Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (img *Image) Unpremultiply() error {
|
||||||
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
|
if C.vips_unpremultiply_go(img.VipsImage, &tmp) != 0 {
|
||||||
|
return Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (img *Image) GetInt(name string) (int, error) {
|
||||||
var i C.int
|
var i C.int
|
||||||
|
|
||||||
if C.vips_image_get_int(img.VipsImage, cachedCString(name), &i) != 0 {
|
if C.vips_image_get_int(img.VipsImage, cachedCString(name), &i) != 0 {
|
||||||
return 0, vipsError()
|
return 0, Error()
|
||||||
}
|
}
|
||||||
return int(i), nil
|
return int(i), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) GetIntDefault(name string, def int) (int, error) {
|
func (img *Image) GetIntDefault(name string, def int) (int, error) {
|
||||||
if C.vips_image_get_typeof(img.VipsImage, cachedCString(name)) == 0 {
|
if C.vips_image_get_typeof(img.VipsImage, cachedCString(name)) == 0 {
|
||||||
return def, nil
|
return def, nil
|
||||||
}
|
}
|
||||||
@@ -335,12 +325,12 @@ func (img *vipsImage) GetIntDefault(name string, def int) (int, error) {
|
|||||||
return img.GetInt(name)
|
return img.GetInt(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) GetIntSlice(name string) ([]int, error) {
|
func (img *Image) GetIntSlice(name string) ([]int, error) {
|
||||||
var ptr unsafe.Pointer
|
var ptr unsafe.Pointer
|
||||||
size := C.int(0)
|
size := C.int(0)
|
||||||
|
|
||||||
if C.vips_image_get_array_int_go(img.VipsImage, cachedCString(name), (**C.int)(unsafe.Pointer(&ptr)), &size) != 0 {
|
if C.vips_image_get_array_int_go(img.VipsImage, cachedCString(name), (**C.int)(unsafe.Pointer(&ptr)), &size) != 0 {
|
||||||
return nil, vipsError()
|
return nil, Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
@@ -357,7 +347,7 @@ func (img *vipsImage) GetIntSlice(name string) ([]int, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) GetIntSliceDefault(name string, def []int) ([]int, error) {
|
func (img *Image) GetIntSliceDefault(name string, def []int) ([]int, error) {
|
||||||
if C.vips_image_get_typeof(img.VipsImage, cachedCString(name)) == 0 {
|
if C.vips_image_get_typeof(img.VipsImage, cachedCString(name)) == 0 {
|
||||||
return def, nil
|
return def, nil
|
||||||
}
|
}
|
||||||
@@ -365,11 +355,11 @@ func (img *vipsImage) GetIntSliceDefault(name string, def []int) ([]int, error)
|
|||||||
return img.GetIntSlice(name)
|
return img.GetIntSlice(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) SetInt(name string, value int) {
|
func (img *Image) SetInt(name string, value int) {
|
||||||
C.vips_image_set_int(img.VipsImage, cachedCString(name), C.int(value))
|
C.vips_image_set_int(img.VipsImage, cachedCString(name), C.int(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) SetIntSlice(name string, value []int) {
|
func (img *Image) SetIntSlice(name string, value []int) {
|
||||||
in := make([]C.int, len(value))
|
in := make([]C.int, len(value))
|
||||||
for i, el := range value {
|
for i, el := range value {
|
||||||
in[i] = C.int(el)
|
in[i] = C.int(el)
|
||||||
@@ -377,12 +367,12 @@ func (img *vipsImage) SetIntSlice(name string, value []int) {
|
|||||||
C.vips_image_set_array_int_go(img.VipsImage, cachedCString(name), &in[0], C.int(len(value)))
|
C.vips_image_set_array_int_go(img.VipsImage, cachedCString(name), &in[0], C.int(len(value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) CastUchar() error {
|
func (img *Image) CastUchar() error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if C.vips_image_get_format(img.VipsImage) != C.VIPS_FORMAT_UCHAR {
|
if C.vips_image_get_format(img.VipsImage) != C.VIPS_FORMAT_UCHAR {
|
||||||
if C.vips_cast_go(img.VipsImage, &tmp, C.VIPS_FORMAT_UCHAR) != 0 {
|
if C.vips_cast_go(img.VipsImage, &tmp, C.VIPS_FORMAT_UCHAR) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
}
|
}
|
||||||
@@ -390,12 +380,12 @@ func (img *vipsImage) CastUchar() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Rad2Float() error {
|
func (img *Image) Rad2Float() error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if C.vips_image_get_coding(img.VipsImage) == C.VIPS_CODING_RAD {
|
if C.vips_image_get_coding(img.VipsImage) == C.VIPS_CODING_RAD {
|
||||||
if C.vips_rad2float_go(img.VipsImage, &tmp) != 0 {
|
if C.vips_rad2float_go(img.VipsImage, &tmp) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
}
|
}
|
||||||
@@ -403,17 +393,11 @@ func (img *vipsImage) Rad2Float() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Resize(wscale, hscale float64, hasAlpa bool) error {
|
func (img *Image) Resize(wscale, hscale float64) error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if hasAlpa {
|
if C.vips_resize_go(img.VipsImage, &tmp, C.double(wscale), C.double(hscale)) != 0 {
|
||||||
if C.vips_resize_with_premultiply(img.VipsImage, &tmp, C.double(wscale), C.double(hscale)) != 0 {
|
return Error()
|
||||||
return vipsError()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if C.vips_resize_go(img.VipsImage, &tmp, C.double(wscale), C.double(hscale)) != 0 {
|
|
||||||
return vipsError()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
@@ -421,17 +405,17 @@ func (img *vipsImage) Resize(wscale, hscale float64, hasAlpa bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Orientation() C.int {
|
func (img *Image) Orientation() C.int {
|
||||||
return C.vips_get_orientation(img.VipsImage)
|
return C.vips_get_orientation(img.VipsImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Rotate(angle int) error {
|
func (img *Image) Rotate(angle int) error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
vipsAngle := (angle / 90) % 4
|
vipsAngle := (angle / 90) % 4
|
||||||
|
|
||||||
if C.vips_rot_go(img.VipsImage, &tmp, C.VipsAngle(vipsAngle)) != 0 {
|
if C.vips_rot_go(img.VipsImage, &tmp, C.VipsAngle(vipsAngle)) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
C.vips_autorot_remove_angle(tmp)
|
C.vips_autorot_remove_angle(tmp)
|
||||||
@@ -440,47 +424,47 @@ func (img *vipsImage) Rotate(angle int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Flip() error {
|
func (img *Image) Flip() error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if C.vips_flip_horizontal_go(img.VipsImage, &tmp) != 0 {
|
if C.vips_flip_horizontal_go(img.VipsImage, &tmp) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Crop(left, top, width, height int) error {
|
func (img *Image) Crop(left, top, width, height int) error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if C.vips_extract_area_go(img.VipsImage, &tmp, C.int(left), C.int(top), C.int(width), C.int(height)) != 0 {
|
if C.vips_extract_area_go(img.VipsImage, &tmp, C.int(left), C.int(top), C.int(width), C.int(height)) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Extract(out *vipsImage, left, top, width, height int) error {
|
func (img *Image) Extract(out *Image, left, top, width, height int) error {
|
||||||
if C.vips_extract_area_go(img.VipsImage, &out.VipsImage, C.int(left), C.int(top), C.int(width), C.int(height)) != 0 {
|
if C.vips_extract_area_go(img.VipsImage, &out.VipsImage, C.int(left), C.int(top), C.int(width), C.int(height)) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) SmartCrop(width, height int) error {
|
func (img *Image) SmartCrop(width, height int) error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if C.vips_smartcrop_go(img.VipsImage, &tmp, C.int(width), C.int(height)) != 0 {
|
if C.vips_smartcrop_go(img.VipsImage, &tmp, C.int(width), C.int(height)) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Trim(threshold float64, smart bool, color rgbColor, equalHor bool, equalVer bool) error {
|
func (img *Image) Trim(threshold float64, smart bool, color Color, equalHor bool, equalVer bool) error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if err := img.CopyMemory(); err != nil {
|
if err := img.CopyMemory(); err != nil {
|
||||||
@@ -490,58 +474,58 @@ func (img *vipsImage) Trim(threshold float64, smart bool, color rgbColor, equalH
|
|||||||
if C.vips_trim(img.VipsImage, &tmp, C.double(threshold),
|
if C.vips_trim(img.VipsImage, &tmp, C.double(threshold),
|
||||||
gbool(smart), C.double(color.R), C.double(color.G), C.double(color.B),
|
gbool(smart), C.double(color.R), C.double(color.G), C.double(color.B),
|
||||||
gbool(equalHor), gbool(equalVer)) != 0 {
|
gbool(equalHor), gbool(equalVer)) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) EnsureAlpha() error {
|
func (img *Image) EnsureAlpha() error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if C.vips_ensure_alpha(img.VipsImage, &tmp) != 0 {
|
if C.vips_ensure_alpha(img.VipsImage, &tmp) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Flatten(bg rgbColor) error {
|
func (img *Image) Flatten(bg Color) error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if C.vips_flatten_go(img.VipsImage, &tmp, C.double(bg.R), C.double(bg.G), C.double(bg.B)) != 0 {
|
if C.vips_flatten_go(img.VipsImage, &tmp, C.double(bg.R), C.double(bg.G), C.double(bg.B)) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Blur(sigma float32) error {
|
func (img *Image) Blur(sigma float32) error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if C.vips_gaussblur_go(img.VipsImage, &tmp, C.double(sigma)) != 0 {
|
if C.vips_gaussblur_go(img.VipsImage, &tmp, C.double(sigma)) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Sharpen(sigma float32) error {
|
func (img *Image) Sharpen(sigma float32) error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if C.vips_sharpen_go(img.VipsImage, &tmp, C.double(sigma)) != 0 {
|
if C.vips_sharpen_go(img.VipsImage, &tmp, C.double(sigma)) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) ImportColourProfile() error {
|
func (img *Image) ImportColourProfile() error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if img.VipsImage.Coding != C.VIPS_CODING_NONE {
|
if img.VipsImage.Coding != C.VIPS_CODING_NONE {
|
||||||
@@ -564,13 +548,13 @@ func (img *vipsImage) ImportColourProfile() error {
|
|||||||
if C.vips_icc_import_go(img.VipsImage, &tmp) == 0 {
|
if C.vips_icc_import_go(img.VipsImage, &tmp) == 0 {
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
} else {
|
} else {
|
||||||
logWarning("Can't import ICC profile: %s", vipsError())
|
log.Warningf("Can't import ICC profile: %s", Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) ExportColourProfile() error {
|
func (img *Image) ExportColourProfile() error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
// Don't export is there's no embedded profile or embedded profile is sRGB
|
// Don't export is there's no embedded profile or embedded profile is sRGB
|
||||||
@@ -581,13 +565,13 @@ func (img *vipsImage) ExportColourProfile() error {
|
|||||||
if C.vips_icc_export_go(img.VipsImage, &tmp) == 0 {
|
if C.vips_icc_export_go(img.VipsImage, &tmp) == 0 {
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
} else {
|
} else {
|
||||||
logWarning("Can't export ICC profile: %s", vipsError())
|
log.Warningf("Can't export ICC profile: %s", Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) ExportColourProfileToSRGB() error {
|
func (img *Image) ExportColourProfileToSRGB() error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
// Don't export is there's no embedded profile or embedded profile is sRGB
|
// Don't export is there's no embedded profile or embedded profile is sRGB
|
||||||
@@ -598,13 +582,13 @@ func (img *vipsImage) ExportColourProfileToSRGB() error {
|
|||||||
if C.vips_icc_export_srgb(img.VipsImage, &tmp) == 0 {
|
if C.vips_icc_export_srgb(img.VipsImage, &tmp) == 0 {
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
} else {
|
} else {
|
||||||
logWarning("Can't export ICC profile: %s", vipsError())
|
log.Warningf("Can't export ICC profile: %s", Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) TransformColourProfile() error {
|
func (img *Image) TransformColourProfile() error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
// Don't transform is there's no embedded profile or embedded profile is sRGB
|
// Don't transform is there's no embedded profile or embedded profile is sRGB
|
||||||
@@ -615,42 +599,42 @@ func (img *vipsImage) TransformColourProfile() error {
|
|||||||
if C.vips_icc_transform_go(img.VipsImage, &tmp) == 0 {
|
if C.vips_icc_transform_go(img.VipsImage, &tmp) == 0 {
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
} else {
|
} else {
|
||||||
logWarning("Can't transform ICC profile: %s", vipsError())
|
log.Warningf("Can't transform ICC profile: %s", Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) RemoveColourProfile() error {
|
func (img *Image) RemoveColourProfile() error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if C.vips_icc_remove(img.VipsImage, &tmp) == 0 {
|
if C.vips_icc_remove(img.VipsImage, &tmp) == 0 {
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
} else {
|
} else {
|
||||||
logWarning("Can't remove ICC profile: %s", vipsError())
|
log.Warningf("Can't remove ICC profile: %s", Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) IsSRGB() bool {
|
func (img *Image) IsSRGB() bool {
|
||||||
return img.VipsImage.Type == C.VIPS_INTERPRETATION_sRGB
|
return img.VipsImage.Type == C.VIPS_INTERPRETATION_sRGB
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) LinearColourspace() error {
|
func (img *Image) LinearColourspace() error {
|
||||||
return img.Colorspace(C.VIPS_INTERPRETATION_scRGB)
|
return img.Colorspace(C.VIPS_INTERPRETATION_scRGB)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) RgbColourspace() error {
|
func (img *Image) RgbColourspace() error {
|
||||||
return img.Colorspace(C.VIPS_INTERPRETATION_sRGB)
|
return img.Colorspace(C.VIPS_INTERPRETATION_sRGB)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Colorspace(colorspace C.VipsInterpretation) error {
|
func (img *Image) Colorspace(colorspace C.VipsInterpretation) error {
|
||||||
if img.VipsImage.Type != colorspace {
|
if img.VipsImage.Type != colorspace {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if C.vips_colourspace_go(img.VipsImage, &tmp, colorspace) != 0 {
|
if C.vips_colourspace_go(img.VipsImage, &tmp, colorspace) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
}
|
}
|
||||||
@@ -658,73 +642,53 @@ func (img *vipsImage) Colorspace(colorspace C.VipsInterpretation) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) CopyMemory() error {
|
func (img *Image) CopyMemory() error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
if tmp = C.vips_image_copy_memory(img.VipsImage); tmp == nil {
|
if tmp = C.vips_image_copy_memory(img.VipsImage); tmp == nil {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Replicate(width, height int) error {
|
func (img *Image) Replicate(width, height int) error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if C.vips_replicate_go(img.VipsImage, &tmp, C.int(width), C.int(height)) != 0 {
|
if C.vips_replicate_go(img.VipsImage, &tmp, C.int(width), C.int(height)) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Embed(width, height int, offX, offY int, bg rgbColor, transpBg bool) error {
|
func (img *Image) Embed(width, height int, offX, offY int) error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if err := img.RgbColourspace(); err != nil {
|
if C.vips_embed_go(img.VipsImage, &tmp, C.int(offX), C.int(offY), C.int(width), C.int(height)) != 0 {
|
||||||
return err
|
return Error()
|
||||||
}
|
|
||||||
|
|
||||||
var bgc []C.double
|
|
||||||
if transpBg {
|
|
||||||
if !img.HasAlpha() {
|
|
||||||
if C.vips_addalpha_go(img.VipsImage, &tmp) != 0 {
|
|
||||||
return vipsError()
|
|
||||||
}
|
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
|
||||||
}
|
|
||||||
|
|
||||||
bgc = []C.double{C.double(0)}
|
|
||||||
} else {
|
|
||||||
bgc = []C.double{C.double(bg.R), C.double(bg.G), C.double(bg.B), 1.0}
|
|
||||||
}
|
|
||||||
|
|
||||||
bgn := minInt(int(img.VipsImage.Bands), len(bgc))
|
|
||||||
|
|
||||||
if C.vips_embed_go(img.VipsImage, &tmp, C.int(offX), C.int(offY), C.int(width), C.int(height), &bgc[0], C.int(bgn)) != 0 {
|
|
||||||
return vipsError()
|
|
||||||
}
|
}
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) ApplyWatermark(wm *vipsImage, opacity float64) error {
|
func (img *Image) ApplyWatermark(wm *Image, opacity float64) error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if C.vips_apply_watermark(img.VipsImage, wm.VipsImage, &tmp, C.double(opacity)) != 0 {
|
if C.vips_apply_watermark(img.VipsImage, wm.VipsImage, &tmp, C.double(opacity)) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Strip() error {
|
func (img *Image) Strip() error {
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
if C.vips_strip(img.VipsImage, &tmp) != 0 {
|
if C.vips_strip(img.VipsImage, &tmp) != 0 {
|
||||||
return vipsError()
|
return Error()
|
||||||
}
|
}
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
|
|
||||||
@@ -4,19 +4,7 @@
|
|||||||
#include <vips/vips7compat.h>
|
#include <vips/vips7compat.h>
|
||||||
#include <vips/vector.h>
|
#include <vips/vector.h>
|
||||||
|
|
||||||
enum ImgproxyImageTypes {
|
#include "../imagetype/imagetype.h"
|
||||||
UNKNOWN = 0,
|
|
||||||
JPEG,
|
|
||||||
PNG,
|
|
||||||
WEBP,
|
|
||||||
GIF,
|
|
||||||
ICO,
|
|
||||||
SVG,
|
|
||||||
HEIC,
|
|
||||||
AVIF,
|
|
||||||
BMP,
|
|
||||||
TIFF
|
|
||||||
};
|
|
||||||
|
|
||||||
int vips_initialize();
|
int vips_initialize();
|
||||||
|
|
||||||
@@ -47,8 +35,9 @@ gboolean vips_is_animated(VipsImage * in);
|
|||||||
int vips_image_get_array_int_go(VipsImage *image, const char *name, int **out, int *n);
|
int vips_image_get_array_int_go(VipsImage *image, const char *name, int **out, int *n);
|
||||||
void vips_image_set_array_int_go(VipsImage *image, const char *name, const int *array, int n);
|
void vips_image_set_array_int_go(VipsImage *image, const char *name, const int *array, int n);
|
||||||
|
|
||||||
gboolean vips_image_hasalpha_go(VipsImage * in);
|
|
||||||
int vips_addalpha_go(VipsImage *in, VipsImage **out);
|
int vips_addalpha_go(VipsImage *in, VipsImage **out);
|
||||||
|
int vips_premultiply_go(VipsImage *in, VipsImage **out);
|
||||||
|
int vips_unpremultiply_go(VipsImage *in, VipsImage **out);
|
||||||
|
|
||||||
int vips_copy_go(VipsImage *in, VipsImage **out);
|
int vips_copy_go(VipsImage *in, VipsImage **out);
|
||||||
|
|
||||||
@@ -56,7 +45,6 @@ int vips_cast_go(VipsImage *in, VipsImage **out, VipsBandFormat format);
|
|||||||
int vips_rad2float_go(VipsImage *in, VipsImage **out);
|
int vips_rad2float_go(VipsImage *in, VipsImage **out);
|
||||||
|
|
||||||
int vips_resize_go(VipsImage *in, VipsImage **out, double wscale, double hscale);
|
int vips_resize_go(VipsImage *in, VipsImage **out, double wscale, double hscale);
|
||||||
int vips_resize_with_premultiply(VipsImage *in, VipsImage **out, double wscale, double hscale);
|
|
||||||
|
|
||||||
int vips_icc_is_srgb_iec61966(VipsImage *in);
|
int vips_icc_is_srgb_iec61966(VipsImage *in);
|
||||||
int vips_has_embedded_icc(VipsImage *in);
|
int vips_has_embedded_icc(VipsImage *in);
|
||||||
@@ -82,7 +70,7 @@ int vips_sharpen_go(VipsImage *in, VipsImage **out, double sigma);
|
|||||||
int vips_flatten_go(VipsImage *in, VipsImage **out, double r, double g, double b);
|
int vips_flatten_go(VipsImage *in, VipsImage **out, double r, double g, double b);
|
||||||
|
|
||||||
int vips_replicate_go(VipsImage *in, VipsImage **out, int across, int down);
|
int vips_replicate_go(VipsImage *in, VipsImage **out, int across, int down);
|
||||||
int vips_embed_go(VipsImage *in, VipsImage **out, int x, int y, int width, int height, double *bg, int bgn);
|
int vips_embed_go(VipsImage *in, VipsImage **out, int x, int y, int width, int height);
|
||||||
|
|
||||||
int vips_ensure_alpha(VipsImage *in, VipsImage **out);
|
int vips_ensure_alpha(VipsImage *in, VipsImage **out);
|
||||||
|
|
||||||
Reference in New Issue
Block a user