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

cshum / imagor / 15317608139

29 May 2025 06:15AM UTC coverage: 92.443% (+0.03%) from 92.412%
15317608139

Pull #549

github

cshum
FallbackFuncs
Pull Request #549: feat(vips): buffer and BMP fallback

59 of 71 new or added lines in 3 files covered. (83.1%)

1 existing line in 1 file now uncovered.

4881 of 5280 relevant lines covered (92.44%)

1.11 hits per line

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

84.98
/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
// FilterMap filter handler map
19
type FilterMap map[string]FilterFunc
20

21
var processorLock sync.RWMutex
22
var processorCount int
23

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

45
        BmpFallback    bool
46
        disableFilters map[string]bool
47
}
48

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

101
// Startup implements imagor.Processor interface
102
func (v *Processor) Startup(_ context.Context) error {
1✔
103
        processorLock.Lock()
1✔
104
        defer processorLock.Unlock()
1✔
105
        processorCount++
1✔
106
        if processorCount <= 1 {
2✔
107
                if v.Debug {
2✔
108
                        vips.SetLogging(func(domain string, level vips.LogLevel, msg string) {
2✔
109
                                switch level {
1✔
110
                                case vips.LogLevelDebug:
1✔
111
                                        v.Logger.Debug(domain, zap.String("log", msg))
1✔
112
                                case vips.LogLevelMessage, vips.LogLevelInfo:
1✔
113
                                        v.Logger.Info(domain, zap.String("log", msg))
1✔
114
                                case vips.LogLevelWarning, vips.LogLevelCritical, vips.LogLevelError:
1✔
115
                                        v.Logger.Warn(domain, zap.String("log", msg))
1✔
116
                                }
117
                        }, vips.LogLevelDebug)
NEW
118
                } else {
×
NEW
119
                        vips.SetLogging(func(domain string, level vips.LogLevel, msg string) {
×
UNCOV
120
                                v.Logger.Warn(domain, zap.String("log", msg))
×
NEW
121
                        }, vips.LogLevelError)
×
122
                }
123
                vips.Startup(&vips.Config{
1✔
124
                        MaxCacheFiles:    v.MaxCacheFiles,
1✔
125
                        MaxCacheMem:      v.MaxCacheMem,
1✔
126
                        MaxCacheSize:     v.MaxCacheSize,
1✔
127
                        ConcurrencyLevel: v.Concurrency,
1✔
128
                })
1✔
129
        }
130
        return nil
1✔
131
}
132

133
// Shutdown implements imagor.Processor interface
134
func (v *Processor) Shutdown(_ context.Context) error {
1✔
135
        processorLock.Lock()
1✔
136
        defer processorLock.Unlock()
1✔
137
        if processorCount <= 0 {
1✔
138
                return nil
×
139
        }
×
140
        processorCount--
1✔
141
        if processorCount == 0 {
2✔
142
                vips.Shutdown()
1✔
143
        }
1✔
144
        return nil
1✔
145
}
146

147
func (v *Processor) newImageFromBlob(
148
        ctx context.Context, blob *imagor.Blob, options *vips.LoadOptions,
149
) (*vips.Image, error) {
1✔
150
        if blob == nil || blob.IsEmpty() {
1✔
151
                return nil, imagor.ErrNotFound
×
152
        }
×
153
        if blob.BlobType() == imagor.BlobTypeMemory {
2✔
154
                buf, width, height, bands, _ := blob.Memory()
1✔
155
                return vips.NewImageFromMemory(buf, width, height, bands)
1✔
156
        }
1✔
157
        reader, _, err := blob.NewReader()
1✔
158
        if err != nil {
1✔
159
                return nil, err
×
160
        }
×
161
        src := vips.NewSource(reader)
1✔
162
        contextDefer(ctx, src.Close)
1✔
163
        img, err := vips.NewImageFromSource(src, options)
1✔
164
        if err != nil {
2✔
165
                src.Close()
1✔
166
                if len(v.FallbackFuncs) > 0 {
2✔
167
                        for _, fallbackFunc := range v.FallbackFuncs {
2✔
168
                                if img2, err2 := fallbackFunc(blob, options); err2 == nil {
2✔
169
                                        return img2, nil
1✔
170
                                }
1✔
171
                        }
172
                }
173
        }
174
        return img, err
1✔
175
}
176

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

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

259
func (v *Processor) newThumbnailFallback(
260
        ctx context.Context, blob *imagor.Blob, width, height int, crop vips.Interesting, size vips.Size, options *vips.LoadOptions,
261
) (img *vips.Image, err error) {
1✔
262
        if img, err = v.CheckResolution(v.newImageFromBlob(ctx, blob, options)); err != nil {
2✔
263
                return
1✔
264
        }
1✔
265
        if err = img.ThumbnailImage(width, &vips.ThumbnailImageOptions{
1✔
266
                Height: height, Size: size, Crop: crop,
1✔
267
        }); err != nil {
1✔
268
                img.Close()
×
269
                return
×
270
        }
×
271
        return img, WrapErr(err)
1✔
272
}
273

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

301
// Thumbnail handles thumbnail operation
302
func (v *Processor) Thumbnail(
303
        img *vips.Image, width, height int, crop vips.Interesting, size vips.Size,
304
) error {
1✔
305
        if crop == vips.InterestingNone || size == vips.SizeForce || img.Height() == img.PageHeight() {
2✔
306
                return img.ThumbnailImage(width, &vips.ThumbnailImageOptions{
1✔
307
                        Height: height, Size: size, Crop: crop,
1✔
308
                })
1✔
309
        }
1✔
310
        return v.animatedThumbnailWithCrop(img, width, height, crop, size)
1✔
311
}
312

313
// FocalThumbnail handles thumbnail with custom focal point
314
func (v *Processor) FocalThumbnail(img *vips.Image, w, h int, fx, fy float64) (err error) {
1✔
315
        var imageWidth, imageHeight float64
1✔
316
        // exif orientation greater 5-8 are 90 or 270 degrees, w and h swapped
1✔
317
        if img.Orientation() > 4 {
2✔
318
                imageWidth = float64(img.PageHeight())
1✔
319
                imageHeight = float64(img.Width())
1✔
320
        } else {
2✔
321
                imageWidth = float64(img.Width())
1✔
322
                imageHeight = float64(img.PageHeight())
1✔
323
        }
1✔
324

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

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

376
// CheckResolution check image resolution for image bomb prevention
377
func (v *Processor) CheckResolution(img *vips.Image, err error) (*vips.Image, error) {
1✔
378
        if err != nil || img == nil {
2✔
379
                return img, err
1✔
380
        }
1✔
381
        if img.Width() > v.MaxWidth || img.PageHeight() > v.MaxHeight ||
1✔
382
                (img.Width()*img.Height()) > v.MaxResolution {
2✔
383
                img.Close()
1✔
384
                return nil, imagor.ErrMaxResolutionExceeded
1✔
385
        }
1✔
386
        return img, nil
1✔
387
}
388

389
func isMultiPage(blob *imagor.Blob, n, page int) bool {
1✔
390
        return blob != nil && (blob.SupportsAnimation() || blob.BlobType() == imagor.BlobTypePDF) && ((n != 1 && n != 0) || (page != 1 && page != 0))
1✔
391
}
1✔
392

393
func applyMultiPageOptions(params *vips.LoadOptions, n, page int) {
1✔
394
        if page < -1 {
2✔
395
                params.Page = -page - 1
1✔
396
        } else if n < -1 {
3✔
397
                params.N = -n
1✔
398
        } else {
2✔
399
                params.N = -1
1✔
400
        }
1✔
401
}
402

403
func recalculateImage(img *vips.Image, n, page int) (int, int) {
1✔
404
        // reload image to restrict frames loaded
1✔
405
        numPages := img.Pages()
1✔
406
        img.Close()
1✔
407
        if page > 1 && page > numPages {
2✔
408
                page = numPages
1✔
409
        } else if n > 1 && n > numPages {
3✔
410
                n = numPages
1✔
411
        }
1✔
412
        return n, page
1✔
413
}
414

415
// WrapErr wraps error to become imagor.Error
416
func WrapErr(err error) error {
1✔
417
        if err == nil {
2✔
418
                return nil
1✔
419
        }
1✔
420
        if e, ok := err.(imagor.Error); ok {
2✔
421
                return e
1✔
422
        }
1✔
423
        msg := strings.TrimSpace(err.Error())
1✔
424
        if strings.HasPrefix(msg, "VipsForeignLoad:") &&
1✔
425
                strings.HasSuffix(msg, "is not in a known format") {
1✔
426
                return imagor.ErrUnsupportedFormat
×
427
        }
×
428
        return imagor.NewError(msg, 406)
1✔
429
}
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

© 2025 Coveralls, Inc