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

cshum / imagor / 15327829984

29 May 2025 03:46PM UTC coverage: 92.318% (-0.09%) from 92.412%
15327829984

Pull #550

github

cshum
WithForceBmpFallback WithForceBufferFallback
Pull Request #550: feat: fallback with BMP or Buffer on Magick installation

56 of 75 new or added lines in 3 files covered. (74.67%)

1 existing line in 1 file now uncovered.

4879 of 5285 relevant lines covered (92.32%)

1.11 hits per line

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

84.39
/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
        FallbackFunc       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
        if v.FallbackFunc == nil {
2✔
126
                if vips.HasOperation("magickload_buffer") {
1✔
NEW
127
                        v.FallbackFunc = BufferFallbackFunc
×
NEW
128
                        v.Logger.Debug("source fallback", zap.String("fallback", "magickload_buffer"))
×
129
                } else {
1✔
130
                        v.FallbackFunc = BmpFallbackFunc
1✔
131
                        v.Logger.Debug("source fallback", zap.String("fallback", "bmp"))
1✔
132
                }
1✔
133
        }
134
        return nil
1✔
135
}
136

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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