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

cshum / imagor / 15284632253

27 May 2025 08:05PM UTC coverage: 92.02% (-0.4%) from 92.448%
15284632253

push

github

cshum
BufferFallback

20 of 31 new or added lines in 1 file covered. (64.52%)

19 existing lines in 2 files now uncovered.

4855 of 5276 relevant lines covered (92.02%)

1.1 hits per line

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

82.76
/processor/vipsprocessor/processor.go
1
package vipsprocessor
2

3
import (
4
        "context"
5
        "github.com/cshum/vipsgen/vips"
6
        "math"
7
        "runtime"
8
        "strings"
9
        "sync"
10

11
        "github.com/cshum/imagor"
12
        "go.uber.org/zap"
13
)
14

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

18
// FallbackFunc vips.Image fallback handler when vips.NewImageFromSource failed
19
type FallbackFunc func(blob *imagor.Blob, options *vips.LoadOptions) (*vips.Image, error)
20

21
// BufferFallback load image from buffer FallbackFunc
NEW
22
func BufferFallback(blob *imagor.Blob, options *vips.LoadOptions) (*vips.Image, error) {
×
NEW
23
        buf, err := blob.ReadAll()
×
NEW
24
        if err != nil {
×
NEW
25
                return nil, err
×
NEW
26
        }
×
NEW
27
        return vips.NewImageFromBuffer(buf, options)
×
28
}
29

30
// FilterMap filter handler map
31
type FilterMap map[string]FilterFunc
32

33
var processorLock sync.RWMutex
34
var processorCount int
35

36
// Processor implements imagor.Processor interface
37
type Processor struct {
38
        Filters            FilterMap
39
        FallbackFunc       FallbackFunc // TODO support slices of FallbackFunc
40
        DisableBlur        bool
41
        DisableFilters     []string
42
        MaxFilterOps       int
43
        Logger             *zap.Logger
44
        Concurrency        int
45
        MaxCacheFiles      int
46
        MaxCacheMem        int
47
        MaxCacheSize       int
48
        MaxWidth           int
49
        MaxHeight          int
50
        MaxResolution      int
51
        MaxAnimationFrames int
52
        MozJPEG            bool
53
        StripMetadata      bool
54
        AvifSpeed          int
55
        Debug              bool
56

57
        disableFilters map[string]bool
58
}
59

60
// NewProcessor create Processor
61
func NewProcessor(options ...Option) *Processor {
1✔
62
        v := &Processor{
1✔
63
                MaxWidth:           9999,
1✔
64
                MaxHeight:          9999,
1✔
65
                MaxResolution:      81000000,
1✔
66
                Concurrency:        1,
1✔
67
                MaxFilterOps:       -1,
1✔
68
                MaxAnimationFrames: -1,
1✔
69
                Logger:             zap.NewNop(),
1✔
70
                disableFilters:     map[string]bool{},
1✔
71
        }
1✔
72
        v.Filters = FilterMap{
1✔
73
                "watermark":        v.watermark,
1✔
74
                "round_corner":     roundCorner,
1✔
75
                "rotate":           rotate,
1✔
76
                "label":            label,
1✔
77
                "grayscale":        grayscale,
1✔
78
                "brightness":       brightness,
1✔
79
                "background_color": backgroundColor,
1✔
80
                "contrast":         contrast,
1✔
81
                "modulate":         modulate,
1✔
82
                "hue":              hue,
1✔
83
                "saturation":       saturation,
1✔
84
                "rgb":              rgb,
1✔
85
                "blur":             blur,
1✔
86
                "sharpen":          sharpen,
1✔
87
                "strip_icc":        stripIcc,
1✔
88
                "strip_exif":       stripExif,
1✔
89
                "trim":             trim,
1✔
90
                "padding":          v.padding,
1✔
91
                "proportion":       proportion,
1✔
92
        }
1✔
93
        for _, option := range options {
2✔
94
                option(v)
1✔
95
        }
1✔
96
        if v.DisableBlur {
2✔
97
                v.DisableFilters = append(v.DisableFilters, "blur", "sharpen")
1✔
98
        }
1✔
99
        for _, name := range v.DisableFilters {
2✔
100
                v.disableFilters[name] = true
1✔
101
        }
1✔
102
        if v.Concurrency == -1 {
2✔
103
                v.Concurrency = runtime.NumCPU()
1✔
104
        }
1✔
105
        return v
1✔
106
}
107

108
// Startup implements imagor.Processor interface
109
func (v *Processor) Startup(_ context.Context) error {
1✔
110
        processorLock.Lock()
1✔
111
        defer processorLock.Unlock()
1✔
112
        processorCount++
1✔
113
        if processorCount <= 1 {
2✔
114
                if v.Debug {
2✔
115
                        vips.SetLogging(func(domain string, level vips.LogLevel, msg string) {
2✔
116
                                switch level {
1✔
117
                                case vips.LogLevelDebug:
1✔
118
                                        v.Logger.Debug(domain, zap.String("log", msg))
1✔
119
                                case vips.LogLevelMessage, vips.LogLevelInfo:
1✔
120
                                        v.Logger.Info(domain, zap.String("log", msg))
1✔
121
                                case vips.LogLevelWarning, vips.LogLevelCritical, vips.LogLevelError:
1✔
122
                                        v.Logger.Warn(domain, zap.String("log", msg))
1✔
123
                                }
124
                        }, vips.LogLevelDebug)
NEW
125
                } else {
×
NEW
126
                        vips.SetLogging(func(domain string, level vips.LogLevel, msg string) {
×
UNCOV
127
                                v.Logger.Warn(domain, zap.String("log", msg))
×
NEW
128
                        }, vips.LogLevelError)
×
129
                }
130
                vips.Startup(&vips.Config{
1✔
131
                        MaxCacheFiles:    v.MaxCacheFiles,
1✔
132
                        MaxCacheMem:      v.MaxCacheMem,
1✔
133
                        MaxCacheSize:     v.MaxCacheSize,
1✔
134
                        ConcurrencyLevel: v.Concurrency,
1✔
135
                })
1✔
136
        }
137
        if vips.HasOperation("magickload_buffer") {
1✔
NEW
138
                v.FallbackFunc = BufferFallback
×
NEW
139
                v.Logger.Info("source fallback", zap.String("type", "magickload_buffer"))
×
140
        } else {
1✔
141
                v.FallbackFunc = BmpFallbackFunc
1✔
142
                v.Logger.Info("source fallback", zap.String("type", "bmp"))
1✔
143
        }
1✔
144
        return nil
1✔
145
}
146

147
// Shutdown implements imagor.Processor interface
148
func (v *Processor) Shutdown(_ context.Context) error {
1✔
149
        processorLock.Lock()
1✔
150
        defer processorLock.Unlock()
1✔
151
        if processorCount <= 0 {
1✔
152
                return nil
×
153
        }
×
154
        processorCount--
1✔
155
        if processorCount == 0 {
2✔
156
                vips.Shutdown()
1✔
157
        }
1✔
158
        return nil
1✔
159
}
160

161
func (v *Processor) newImageFromBlob(
162
        ctx context.Context, blob *imagor.Blob, options *vips.LoadOptions,
163
) (*vips.Image, error) {
1✔
164
        if blob == nil || blob.IsEmpty() {
1✔
165
                return nil, imagor.ErrNotFound
×
166
        }
×
167
        if blob.BlobType() == imagor.BlobTypeMemory {
2✔
168
                buf, width, height, bands, _ := blob.Memory()
1✔
169
                return vips.NewImageFromMemory(buf, width, height, bands)
1✔
170
        }
1✔
171
        reader, _, err := blob.NewReader()
1✔
172
        if err != nil {
1✔
173
                return nil, err
×
174
        }
×
175
        src := vips.NewSource(reader)
1✔
176
        contextDefer(ctx, src.Close)
1✔
177
        img, err := vips.NewImageFromSource(src, options)
1✔
178
        if err != nil && v.FallbackFunc != nil {
2✔
179
                src.Close()
1✔
180
                return v.FallbackFunc(blob, options)
1✔
181
        }
1✔
182
        return img, err
1✔
183
}
184

185
func newThumbnailFromBlob(
186
        ctx context.Context, blob *imagor.Blob,
187
        width, height int, crop vips.Interesting, size vips.Size, options *vips.LoadOptions,
188
) (*vips.Image, error) {
1✔
189
        if blob == nil || blob.IsEmpty() {
1✔
190
                return nil, imagor.ErrNotFound
×
191
        }
×
192
        reader, _, err := blob.NewReader()
1✔
193
        if err != nil {
1✔
194
                return nil, err
×
195
        }
×
196
        src := vips.NewSource(reader)
1✔
197
        contextDefer(ctx, src.Close)
1✔
198
        var optionString string
1✔
199
        if options != nil {
2✔
200
                optionString = options.OptionString()
1✔
201
        }
1✔
202
        return vips.NewThumbnailSource(src, width, &vips.ThumbnailSourceOptions{
1✔
203
                Height:       height,
1✔
204
                Crop:         crop,
1✔
205
                Size:         size,
1✔
206
                OptionString: optionString,
1✔
207
        })
1✔
208
}
209

210
// NewThumbnail creates new thumbnail with resize and crop from imagor.Blob
211
func (v *Processor) NewThumbnail(
212
        ctx context.Context, blob *imagor.Blob, width, height int, crop vips.Interesting,
213
        size vips.Size, n, page int, dpi int,
214
) (*vips.Image, error) {
1✔
215
        var options = &vips.LoadOptions{}
1✔
216
        if dpi > 0 {
1✔
217
                options.Dpi = dpi
×
218
        }
×
219
        var err error
1✔
220
        var img *vips.Image
1✔
221
        if isMultiPage(blob, n, page) {
2✔
222
                applyMultiPageOptions(options, n, page)
1✔
223
                if crop == vips.InterestingNone || size == vips.SizeForce {
2✔
224
                        if img, err = v.newImageFromBlob(ctx, blob, options); err != nil {
1✔
225
                                return nil, WrapErr(err)
×
226
                        }
×
227
                        if n > 1 || page > 1 {
2✔
228
                                // reload image to restrict frames loaded
1✔
229
                                n, page = recalculateImage(img, n, page)
1✔
230
                                return v.NewThumbnail(ctx, blob, width, height, crop, size, -n, -page, dpi)
1✔
231
                        }
1✔
232
                        if _, err = v.CheckResolution(img, nil); err != nil {
2✔
233
                                return nil, err
1✔
234
                        }
1✔
235
                        if err = img.ThumbnailImage(width, &vips.ThumbnailImageOptions{
1✔
236
                                Height: height, Size: size, Crop: crop,
1✔
237
                        }); err != nil {
1✔
238
                                img.Close()
×
239
                                return nil, WrapErr(err)
×
240
                        }
×
241
                } else {
1✔
242
                        if img, err = v.CheckResolution(v.newImageFromBlob(ctx, blob, options)); err != nil {
1✔
243
                                return nil, WrapErr(err)
×
244
                        }
×
245
                        if n > 1 || page > 1 {
2✔
246
                                // reload image to restrict frames loaded
1✔
247
                                n, page = recalculateImage(img, n, page)
1✔
248
                                return v.NewThumbnail(ctx, blob, width, height, crop, size, -n, -page, dpi)
1✔
249
                        }
1✔
250
                        if err = v.animatedThumbnailWithCrop(img, width, height, crop, size); err != nil {
1✔
251
                                img.Close()
×
252
                                return nil, WrapErr(err)
×
253
                        }
×
254
                }
255
        } else {
1✔
256
                switch blob.BlobType() {
1✔
257
                case imagor.BlobTypeJPEG, imagor.BlobTypeGIF, imagor.BlobTypeWEBP:
1✔
258
                        // only allow real thumbnail for jpeg gif webp
1✔
259
                        img, err = newThumbnailFromBlob(ctx, blob, width, height, crop, size, options)
1✔
260
                default:
1✔
261
                        img, err = v.newThumbnailFallback(ctx, blob, width, height, crop, size, options)
1✔
262
                }
263
        }
264
        return v.CheckResolution(img, WrapErr(err))
1✔
265
}
266

267
func (v *Processor) newThumbnailFallback(
268
        ctx context.Context, blob *imagor.Blob, width, height int, crop vips.Interesting, size vips.Size, options *vips.LoadOptions,
269
) (img *vips.Image, err error) {
1✔
270
        if img, err = v.CheckResolution(v.newImageFromBlob(ctx, blob, options)); err != nil {
2✔
271
                return
1✔
272
        }
1✔
273
        if err = img.ThumbnailImage(width, &vips.ThumbnailImageOptions{
1✔
274
                Height: height, Size: size, Crop: crop,
1✔
275
        }); err != nil {
1✔
276
                img.Close()
×
277
                return
×
278
        }
×
279
        return img, WrapErr(err)
1✔
280
}
281

282
// NewImage creates new Image from imagor.Blob
283
func (v *Processor) NewImage(ctx context.Context, blob *imagor.Blob, n, page int, dpi int) (*vips.Image, error) {
1✔
284
        var params = &vips.LoadOptions{}
1✔
285
        if dpi > 0 {
1✔
286
                params.Dpi = dpi
×
287
        }
×
288
        params.FailOnError = false
1✔
289
        if isMultiPage(blob, n, page) {
2✔
290
                applyMultiPageOptions(params, n, page)
1✔
291
                img, err := v.CheckResolution(v.newImageFromBlob(ctx, blob, params))
1✔
292
                if err != nil {
1✔
293
                        return nil, WrapErr(err)
×
294
                }
×
295
                // reload image to restrict frames loaded
296
                if n > 1 || page > 1 {
2✔
297
                        n, page = recalculateImage(img, n, page)
1✔
298
                        return v.NewImage(ctx, blob, -n, -page, dpi)
1✔
299
                }
1✔
300
                return img, nil
1✔
301
        }
302
        img, err := v.CheckResolution(v.newImageFromBlob(ctx, blob, params))
1✔
303
        if err != nil {
1✔
304
                return nil, WrapErr(err)
×
305
        }
×
306
        return img, nil
1✔
307
}
308

309
// Thumbnail handles thumbnail operation
310
func (v *Processor) Thumbnail(
311
        img *vips.Image, width, height int, crop vips.Interesting, size vips.Size,
312
) error {
1✔
313
        if crop == vips.InterestingNone || size == vips.SizeForce || img.Height() == img.PageHeight() {
2✔
314
                return img.ThumbnailImage(width, &vips.ThumbnailImageOptions{
1✔
315
                        Height: height, Size: size, Crop: crop,
1✔
316
                })
1✔
317
        }
1✔
318
        return v.animatedThumbnailWithCrop(img, width, height, crop, size)
1✔
319
}
320

321
// FocalThumbnail handles thumbnail with custom focal point
322
func (v *Processor) FocalThumbnail(img *vips.Image, w, h int, fx, fy float64) (err error) {
1✔
323
        var imageWidth, imageHeight float64
1✔
324
        // exif orientation greater 5-8 are 90 or 270 degrees, w and h swapped
1✔
325
        if img.Orientation() > 4 {
2✔
326
                imageWidth = float64(img.PageHeight())
1✔
327
                imageHeight = float64(img.Width())
1✔
328
        } else {
2✔
329
                imageWidth = float64(img.Width())
1✔
330
                imageHeight = float64(img.PageHeight())
1✔
331
        }
1✔
332

333
        if float64(w)/float64(h) > float64(imageWidth)/float64(imageHeight) {
2✔
334
                if err = img.ThumbnailImage(w, &vips.ThumbnailImageOptions{
1✔
335
                        Height: v.MaxHeight, Crop: vips.InterestingNone,
1✔
336
                }); err != nil {
1✔
337
                        return
×
338
                }
×
339
        } else {
1✔
340
                if err = img.ThumbnailImage(v.MaxWidth, &vips.ThumbnailImageOptions{
1✔
341
                        Height: h, Crop: vips.InterestingNone,
1✔
342
                }); err != nil {
1✔
343
                        return
×
344
                }
×
345
        }
346
        var top, left float64
1✔
347
        left = float64(img.Width())*fx - float64(w)/2
1✔
348
        top = float64(img.PageHeight())*fy - float64(h)/2
1✔
349
        left = math.Max(0, math.Min(left, float64(img.Width()-w)))
1✔
350
        top = math.Max(0, math.Min(top, float64(img.PageHeight()-h)))
1✔
351
        return img.ExtractAreaMultiPage(int(left), int(top), w, h)
1✔
352
}
353

354
func (v *Processor) animatedThumbnailWithCrop(
355
        img *vips.Image, w, h int, crop vips.Interesting, size vips.Size,
356
) (err error) {
1✔
357
        if size == vips.SizeDown && img.Width() < w && img.PageHeight() < h {
1✔
358
                return
×
359
        }
×
360
        var top, left int
1✔
361
        if float64(w)/float64(h) > float64(img.Width())/float64(img.PageHeight()) {
2✔
362
                if err = img.ThumbnailImage(w, &vips.ThumbnailImageOptions{
1✔
363
                        Height: v.MaxHeight, Crop: vips.InterestingNone, Size: size,
1✔
364
                }); err != nil {
1✔
365
                        return
×
366
                }
×
367
        } else {
1✔
368
                if err = img.ThumbnailImage(v.MaxWidth, &vips.ThumbnailImageOptions{
1✔
369
                        Height: h, Crop: vips.InterestingNone, Size: size,
1✔
370
                }); err != nil {
1✔
371
                        return
×
372
                }
×
373
        }
374
        if crop == vips.InterestingHigh {
2✔
375
                left = img.Width() - w
1✔
376
                top = img.PageHeight() - h
1✔
377
        } else if crop == vips.InterestingCentre || crop == vips.InterestingAttention {
3✔
378
                left = (img.Width() - w) / 2
1✔
379
                top = (img.PageHeight() - h) / 2
1✔
380
        }
1✔
381
        return img.ExtractAreaMultiPage(left, top, w, h)
1✔
382
}
383

384
// CheckResolution check image resolution for image bomb prevention
385
func (v *Processor) CheckResolution(img *vips.Image, err error) (*vips.Image, error) {
1✔
386
        if err != nil || img == nil {
2✔
387
                return img, err
1✔
388
        }
1✔
389
        if img.Width() > v.MaxWidth || img.PageHeight() > v.MaxHeight ||
1✔
390
                (img.Width()*img.Height()) > v.MaxResolution {
2✔
391
                img.Close()
1✔
392
                return nil, imagor.ErrMaxResolutionExceeded
1✔
393
        }
1✔
394
        return img, nil
1✔
395
}
396

397
func isMultiPage(blob *imagor.Blob, n, page int) bool {
1✔
398
        return blob != nil && (blob.SupportsAnimation() || blob.BlobType() == imagor.BlobTypePDF) && ((n != 1 && n != 0) || (page != 1 && page != 0))
1✔
399
}
1✔
400

401
func applyMultiPageOptions(params *vips.LoadOptions, n, page int) {
1✔
402
        if page < -1 {
2✔
403
                params.Page = -page - 1
1✔
404
        } else if n < -1 {
3✔
405
                params.N = -n
1✔
406
        } else {
2✔
407
                params.N = -1
1✔
408
        }
1✔
409
}
410

411
func recalculateImage(img *vips.Image, n, page int) (int, int) {
1✔
412
        // reload image to restrict frames loaded
1✔
413
        numPages := img.Pages()
1✔
414
        img.Close()
1✔
415
        if page > 1 && page > numPages {
2✔
416
                page = numPages
1✔
417
        } else if n > 1 && n > numPages {
3✔
418
                n = numPages
1✔
419
        }
1✔
420
        return n, page
1✔
421
}
422

423
// WrapErr wraps error to become imagor.Error
424
func WrapErr(err error) error {
1✔
425
        if err == nil {
2✔
426
                return nil
1✔
427
        }
1✔
428
        if e, ok := err.(imagor.Error); ok {
2✔
429
                return e
1✔
430
        }
1✔
431
        msg := strings.TrimSpace(err.Error())
1✔
432
        if strings.HasPrefix(msg, "VipsForeignLoad:") &&
1✔
433
                strings.HasSuffix(msg, "is not in a known format") {
1✔
434
                return imagor.ErrUnsupportedFormat
×
435
        }
×
436
        return imagor.NewError(msg, 406)
1✔
437
}
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