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

cshum / imagor / 15488577405

06 Jun 2025 10:35AM UTC coverage: 90.768% (-1.7%) from 92.43%
15488577405

Pull #562

github

cshum
reset arm64 golden
Pull Request #562: build: libvips 8.17.0, vipsgen 1.1.1

4808 of 5297 relevant lines covered (90.77%)

1.09 hits per line

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

83.49
/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
        Unlimited          bool
44
        Debug              bool
45

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
        return v
1✔
95
}
96

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

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

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

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

201
// NewThumbnail creates new thumbnail with resize and crop from imagor.Blob
202
func (v *Processor) NewThumbnail(
203
        ctx context.Context, blob *imagor.Blob, width, height int, crop vips.Interesting,
204
        size vips.Size, n, page int, dpi int,
205
) (*vips.Image, error) {
1✔
206
        var options = &vips.LoadOptions{}
1✔
207
        if dpi > 0 {
1✔
208
                options.Dpi = dpi
×
209
        }
×
210
        options.Unlimited = v.Unlimited
1✔
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 options = &vips.LoadOptions{}
1✔
277
        if dpi > 0 {
1✔
278
                options.Dpi = dpi
×
279
        }
×
280
        options.Unlimited = v.Unlimited
1✔
281
        if isMultiPage(blob, n, page) {
2✔
282
                applyMultiPageOptions(options, n, page)
1✔
283
                img, err := v.CheckResolution(v.newImageFromBlob(ctx, blob, options))
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 {
1✔
289
                        n, page = recalculateImage(img, n, page)
×
290
                        return v.NewImage(ctx, blob, -n, -page, dpi)
×
291
                }
×
292
                return img, nil
1✔
293
        }
294
        img, err := v.CheckResolution(v.newImageFromBlob(ctx, blob, options))
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 !v.Unlimited && (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