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

cshum / imagor / 15317751800

29 May 2025 06:25AM UTC coverage: 92.343% (-0.07%) from 92.412%
15317751800

Pull #549

github

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

51 of 67 new or added lines in 3 files covered. (76.12%)

1 existing line in 1 file now uncovered.

4872 of 5276 relevant lines covered (92.34%)

1.11 hits per line

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

84.79
/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
        disableFilters map[string]bool
46
}
47

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

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

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

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

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

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

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

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

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

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

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

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

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

384
func isMultiPage(blob *imagor.Blob, n, page int) bool {
1✔
385
        return blob != nil && (blob.SupportsAnimation() || blob.BlobType() == imagor.BlobTypePDF) && ((n != 1 && n != 0) || (page != 1 && page != 0))
1✔
386
}
1✔
387

388
func applyMultiPageOptions(params *vips.LoadOptions, n, page int) {
1✔
389
        if page < -1 {
2✔
390
                params.Page = -page - 1
1✔
391
        } else if n < -1 {
3✔
392
                params.N = -n
1✔
393
        } else {
2✔
394
                params.N = -1
1✔
395
        }
1✔
396
}
397

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

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