• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cshum / imagor / 3871661168

09 Jan 2023 07:33AM UTC coverage: 91.133% (-0.09%) from 91.221%
3871661168

Pull #294

github

Adrian Shum
feat(vips): fallback BMP support
Pull Request #294: feat(vips): bmp support

32 of 32 new or added lines in 3 files covered. (100.0%)

5447 of 5977 relevant lines covered (91.13%)

1.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.27
/vips/processor.go
1
package vips
2

3
import (
4
        "context"
5
        "github.com/cshum/imagor"
6
        "go.uber.org/zap"
7
        "math"
8
        "runtime"
9
        "strings"
10
        "sync"
11
)
12

13
// FilterFunc filter handler function
14
type FilterFunc func(ctx context.Context, img *Image, load imagor.LoadFunc, args ...string) (err error)
15

16
// FilterMap filter handler map
17
type FilterMap map[string]FilterFunc
18

19
var processorLock sync.RWMutex
20
var processorCount int
21

22
// Processor implements imagor.Processor interface
23
type Processor struct {
24
        Filters            FilterMap
25
        DisableBlur        bool
26
        DisableFilters     []string
27
        MaxFilterOps       int
28
        Logger             *zap.Logger
29
        Concurrency        int
30
        MaxCacheFiles      int
31
        MaxCacheMem        int
32
        MaxCacheSize       int
33
        MaxWidth           int
34
        MaxHeight          int
35
        MaxResolution      int
36
        MaxAnimationFrames int
37
        MozJPEG            bool
38
        Debug              bool
39

40
        disableFilters map[string]bool
41
}
42

43
// NewProcessor create Processor
44
func NewProcessor(options ...Option) *Processor {
1✔
45
        v := &Processor{
1✔
46
                MaxWidth:           9999,
1✔
47
                MaxHeight:          9999,
1✔
48
                MaxResolution:      81000000,
1✔
49
                Concurrency:        1,
1✔
50
                MaxFilterOps:       -1,
1✔
51
                MaxAnimationFrames: -1,
1✔
52
                Logger:             zap.NewNop(),
1✔
53
                disableFilters:     map[string]bool{},
1✔
54
        }
1✔
55
        v.Filters = FilterMap{
1✔
56
                "watermark":        v.watermark,
1✔
57
                "round_corner":     roundCorner,
1✔
58
                "rotate":           rotate,
1✔
59
                "label":            label,
1✔
60
                "grayscale":        grayscale,
1✔
61
                "brightness":       brightness,
1✔
62
                "background_color": backgroundColor,
1✔
63
                "contrast":         contrast,
1✔
64
                "modulate":         modulate,
1✔
65
                "hue":              hue,
1✔
66
                "saturation":       saturation,
1✔
67
                "rgb":              rgb,
1✔
68
                "blur":             blur,
1✔
69
                "sharpen":          sharpen,
1✔
70
                "strip_icc":        stripIcc,
1✔
71
                "strip_exif":       stripExif,
1✔
72
                "trim":             trim,
1✔
73
                "set_frames":       setFrames,
1✔
74
                "padding":          v.padding,
1✔
75
                "proportion":       proportion,
1✔
76
        }
1✔
77
        for _, option := range options {
2✔
78
                option(v)
1✔
79
        }
1✔
80
        if v.DisableBlur {
2✔
81
                v.DisableFilters = append(v.DisableFilters, "blur", "sharpen")
1✔
82
        }
1✔
83
        for _, name := range v.DisableFilters {
2✔
84
                v.disableFilters[name] = true
1✔
85
        }
1✔
86
        if v.Concurrency == -1 {
2✔
87
                v.Concurrency = runtime.NumCPU()
1✔
88
        }
1✔
89
        return v
1✔
90
}
91

92
// Startup implements imagor.Processor interface
93
func (v *Processor) Startup(_ context.Context) error {
1✔
94
        processorLock.Lock()
1✔
95
        defer processorLock.Unlock()
1✔
96
        processorCount++
1✔
97
        if processorCount > 1 {
2✔
98
                return nil
1✔
99
        }
1✔
100
        if v.Debug {
2✔
101
                SetLogging(func(domain string, level LogLevel, msg string) {
2✔
102
                        switch level {
1✔
103
                        case LogLevelDebug:
1✔
104
                                v.Logger.Debug(domain, zap.String("log", msg))
1✔
105
                        case LogLevelMessage, LogLevelInfo:
1✔
106
                                v.Logger.Info(domain, zap.String("log", msg))
1✔
107
                        case LogLevelWarning, LogLevelCritical, LogLevelError:
1✔
108
                                v.Logger.Warn(domain, zap.String("log", msg))
1✔
109
                        }
110
                }, LogLevelDebug)
111
        } else {
×
112
                SetLogging(func(domain string, level LogLevel, msg string) {
×
113
                        v.Logger.Warn(domain, zap.String("log", msg))
×
114
                }, LogLevelError)
×
115
        }
116
        Startup(&Config{
1✔
117
                MaxCacheFiles:    v.MaxCacheFiles,
1✔
118
                MaxCacheMem:      v.MaxCacheMem,
1✔
119
                MaxCacheSize:     v.MaxCacheSize,
1✔
120
                ConcurrencyLevel: v.Concurrency,
1✔
121
        })
1✔
122
        return nil
1✔
123
}
124

125
// Shutdown implements imagor.Processor interface
126
func (v *Processor) Shutdown(_ context.Context) error {
1✔
127
        processorLock.Lock()
1✔
128
        defer processorLock.Unlock()
1✔
129
        if processorCount <= 0 {
1✔
130
                return nil
×
131
        }
×
132
        processorCount--
1✔
133
        if processorCount == 0 {
2✔
134
                Shutdown()
1✔
135
        }
1✔
136
        return nil
1✔
137
}
138

139
func newImageFromBlob(
140
        ctx context.Context, blob *imagor.Blob, params *ImportParams,
141
) (*Image, error) {
1✔
142
        if blob == nil || blob.IsEmpty() {
1✔
143
                return nil, imagor.ErrNotFound
×
144
        }
×
145
        if blob.BlobType() == imagor.BlobTypeMemory {
2✔
146
                buf, width, height, bands, _ := blob.Memory()
1✔
147
                return LoadImageFromMemory(buf, width, height, bands)
1✔
148
        }
1✔
149
        reader, _, err := blob.NewReader()
1✔
150
        if err != nil {
1✔
151
                return nil, err
×
152
        }
×
153
        src := NewSource(reader)
1✔
154
        contextDefer(ctx, src.Close)
1✔
155
        img, err := src.LoadImage(params)
1✔
156
        if err != nil && blob.BlobType() == imagor.BlobTypeBMP {
2✔
157
                // fallback with Go BMP decoder if vips error on BMP
1✔
158
                src.Close()
1✔
159
                r, _, err := blob.NewReader()
1✔
160
                if err != nil {
1✔
161
                        return nil, err
×
162
                }
×
163
                defer func() {
2✔
164
                        _ = r.Close()
1✔
165
                }()
1✔
166
                return loadImageFromBMP(r)
1✔
167
        }
168
        return img, err
1✔
169
}
170

171
func newThumbnailFromBlob(
172
        ctx context.Context, blob *imagor.Blob,
173
        width, height int, crop Interesting, size Size, params *ImportParams,
174
) (*Image, error) {
1✔
175
        if blob == nil || blob.IsEmpty() {
1✔
176
                return nil, imagor.ErrNotFound
×
177
        }
×
178
        reader, _, err := blob.NewReader()
1✔
179
        if err != nil {
1✔
180
                return nil, err
×
181
        }
×
182
        src := NewSource(reader)
1✔
183
        contextDefer(ctx, src.Close)
1✔
184
        return src.LoadThumbnail(width, height, crop, size, params)
1✔
185
}
186

187
// NewThumbnail creates new thumbnail with resize and crop from imagor.Blob
188
func (v *Processor) NewThumbnail(
189
        ctx context.Context, blob *imagor.Blob, width, height int, crop Interesting, size Size, n int,
190
) (*Image, error) {
1✔
191
        var params *ImportParams
1✔
192
        var err error
1✔
193
        var img *Image
1✔
194
        if isBlobAnimated(blob, n) {
2✔
195
                params = NewImportParams()
1✔
196
                if n < -1 {
2✔
197
                        params.NumPages.Set(-n)
1✔
198
                } else {
2✔
199
                        params.NumPages.Set(-1)
1✔
200
                }
1✔
201
                if crop == InterestingNone || size == SizeForce {
2✔
202
                        if img, err = newImageFromBlob(ctx, blob, params); err != nil {
1✔
203
                                return nil, WrapErr(err)
×
204
                        }
×
205
                        if n > 1 && img.Pages() > n {
2✔
206
                                // reload image to restrict frames loaded
1✔
207
                                img.Close()
1✔
208
                                return v.NewThumbnail(ctx, blob, width, height, crop, size, -n)
1✔
209
                        }
1✔
210
                        if _, err = v.CheckResolution(img, nil); err != nil {
2✔
211
                                return nil, err
1✔
212
                        }
1✔
213
                        if err = img.ThumbnailWithSize(width, height, crop, size); err != nil {
1✔
214
                                img.Close()
×
215
                                return nil, WrapErr(err)
×
216
                        }
×
217
                } else {
1✔
218
                        if img, err = v.CheckResolution(newImageFromBlob(ctx, blob, params)); err != nil {
1✔
219
                                return nil, WrapErr(err)
×
220
                        }
×
221
                        if n > 1 && img.Pages() > n {
2✔
222
                                // reload image to restrict frames loaded
1✔
223
                                img.Close()
1✔
224
                                return v.NewThumbnail(ctx, blob, width, height, crop, size, -n)
1✔
225
                        }
1✔
226
                        if err = v.animatedThumbnailWithCrop(img, width, height, crop, size); err != nil {
1✔
227
                                img.Close()
×
228
                                return nil, WrapErr(err)
×
229
                        }
×
230
                }
231
        } else {
1✔
232
                switch blob.BlobType() {
1✔
233
                case imagor.BlobTypeJPEG, imagor.BlobTypeGIF, imagor.BlobTypeWEBP:
1✔
234
                        // only allow real thumbnail for jpeg gif webp
1✔
235
                        img, err = newThumbnailFromBlob(ctx, blob, width, height, crop, size, nil)
1✔
236
                default:
1✔
237
                        img, err = v.newThumbnailFallback(ctx, blob, width, height, crop, size, nil)
1✔
238
                }
239
        }
240
        return v.CheckResolution(img, WrapErr(err))
1✔
241
}
242

243
func (v *Processor) newThumbnailFallback(
244
        ctx context.Context, blob *imagor.Blob, width, height int, crop Interesting, size Size, params *ImportParams,
245
) (img *Image, err error) {
1✔
246
        if img, err = v.CheckResolution(newImageFromBlob(ctx, blob, params)); err != nil {
2✔
247
                return
1✔
248
        }
1✔
249
        if err = img.ThumbnailWithSize(width, height, crop, size); err != nil {
1✔
250
                img.Close()
×
251
                return
×
252
        }
×
253
        return img, WrapErr(err)
1✔
254
}
255

256
// NewImage creates new Image from imagor.Blob
257
func (v *Processor) NewImage(ctx context.Context, blob *imagor.Blob, n int) (*Image, error) {
1✔
258
        var params *ImportParams
1✔
259
        if isBlobAnimated(blob, n) {
2✔
260
                params = NewImportParams()
1✔
261
                if n < -1 {
2✔
262
                        params.NumPages.Set(-n)
1✔
263
                } else {
2✔
264
                        params.NumPages.Set(-1)
1✔
265
                }
1✔
266
                img, err := v.CheckResolution(newImageFromBlob(ctx, blob, params))
1✔
267
                if err != nil {
1✔
268
                        return nil, WrapErr(err)
×
269
                }
×
270
                // reload image to restrict frames loaded
271
                if n > 1 && img.Pages() > n {
2✔
272
                        img.Close()
1✔
273
                        return v.NewImage(ctx, blob, -n)
1✔
274
                }
1✔
275
                return img, nil
1✔
276
        }
277
        img, err := v.CheckResolution(newImageFromBlob(ctx, blob, params))
1✔
278
        if err != nil {
1✔
279
                return nil, WrapErr(err)
×
280
        }
×
281
        return img, nil
1✔
282
}
283

284
// Thumbnail handles thumbnail operation
285
func (v *Processor) Thumbnail(
286
        img *Image, width, height int, crop Interesting, size Size,
287
) error {
1✔
288
        if crop == InterestingNone || size == SizeForce || img.Height() == img.PageHeight() {
2✔
289
                return img.ThumbnailWithSize(width, height, crop, size)
1✔
290
        }
1✔
291
        return v.animatedThumbnailWithCrop(img, width, height, crop, size)
1✔
292
}
293

294
// FocalThumbnail handles thumbnail with custom focal point
295
func (v *Processor) FocalThumbnail(img *Image, w, h int, fx, fy float64) (err error) {
1✔
296
        if float64(w)/float64(h) > float64(img.Width())/float64(img.PageHeight()) {
2✔
297
                if err = img.Thumbnail(w, v.MaxHeight, InterestingNone); err != nil {
1✔
298
                        return
×
299
                }
×
300
        } else {
1✔
301
                if err = img.Thumbnail(v.MaxWidth, h, InterestingNone); err != nil {
1✔
302
                        return
×
303
                }
×
304
        }
305
        var top, left float64
1✔
306
        left = float64(img.Width())*fx - float64(w)/2
1✔
307
        top = float64(img.PageHeight())*fy - float64(h)/2
1✔
308
        left = math.Max(0, math.Min(left, float64(img.Width()-w)))
1✔
309
        top = math.Max(0, math.Min(top, float64(img.PageHeight()-h)))
1✔
310
        return img.ExtractArea(int(left), int(top), w, h)
1✔
311
}
312

313
func (v *Processor) animatedThumbnailWithCrop(
314
        img *Image, w, h int, crop Interesting, size Size,
315
) (err error) {
1✔
316
        if size == SizeDown && img.Width() < w && img.PageHeight() < h {
1✔
317
                return
×
318
        }
×
319
        var top, left int
1✔
320
        if float64(w)/float64(h) > float64(img.Width())/float64(img.PageHeight()) {
2✔
321
                if err = img.ThumbnailWithSize(w, v.MaxHeight, InterestingNone, size); err != nil {
1✔
322
                        return
×
323
                }
×
324
        } else {
1✔
325
                if err = img.ThumbnailWithSize(v.MaxWidth, h, InterestingNone, size); err != nil {
1✔
326
                        return
×
327
                }
×
328
        }
329
        if crop == InterestingHigh {
2✔
330
                left = img.Width() - w
1✔
331
                top = img.PageHeight() - h
1✔
332
        } else if crop == InterestingCentre || crop == InterestingAttention {
3✔
333
                left = (img.Width() - w) / 2
1✔
334
                top = (img.PageHeight() - h) / 2
1✔
335
        }
1✔
336
        return img.ExtractArea(left, top, w, h)
1✔
337
}
338

339
// CheckResolution check image resolution for image bomb prevention
340
func (v *Processor) CheckResolution(img *Image, err error) (*Image, error) {
1✔
341
        if err != nil || img == nil {
2✔
342
                return img, err
1✔
343
        }
1✔
344
        if img.Width() > v.MaxWidth || img.PageHeight() > v.MaxHeight ||
1✔
345
                (img.Width()*img.Height()) > v.MaxResolution {
2✔
346
                img.Close()
1✔
347
                return nil, imagor.ErrMaxResolutionExceeded
1✔
348
        }
1✔
349
        return img, nil
1✔
350
}
351

352
func isBlobAnimated(blob *imagor.Blob, n int) bool {
1✔
353
        return blob != nil && blob.SupportsAnimation() && n != 1 && n != 0
1✔
354
}
1✔
355

356
// WrapErr wraps error to become imagor.Error
357
func WrapErr(err error) error {
1✔
358
        if err == nil {
2✔
359
                return nil
1✔
360
        }
1✔
361
        if e, ok := err.(imagor.Error); ok {
2✔
362
                return e
1✔
363
        }
1✔
364
        msg := strings.TrimSpace(err.Error())
1✔
365
        if strings.HasPrefix(msg, "VipsForeignLoad:") &&
1✔
366
                strings.HasSuffix(msg, "is not in a known format") {
1✔
367
                return imagor.ErrUnsupportedFormat
×
368
        }
×
369
        return imagor.NewError(msg, 406)
1✔
370
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc