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

cshum / imagor / 15233893740

25 May 2025 03:47AM UTC coverage: 92.118% (+2.4%) from 89.678%
15233893740

Pull #544

github

cshum
update exif extraction
Pull Request #544: feat: introducing vipsgen

308 of 346 new or added lines in 6 files covered. (89.02%)

6 existing lines in 1 file now uncovered.

4862 of 5278 relevant lines covered (92.12%)

1.1 hits per line

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

84.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
        DisableBlur        bool
28
        DisableFilters     []string
29
        MaxFilterOps       int
30
        Logger             *zap.Logger
31
        Concurrency        int
32
        MaxCacheFiles      int
33
        MaxCacheMem        int
34
        MaxCacheSize       int
35
        MaxWidth           int
36
        MaxHeight          int
37
        MaxResolution      int
38
        MaxAnimationFrames int
39
        MozJPEG            bool
40
        StripMetadata      bool
41
        AvifSpeed          int
42
        Debug              bool
43

44
        disableFilters map[string]bool
45
}
46

47
// NewProcessor create Processor
48
func NewProcessor(options ...Option) *Processor {
1✔
49
        v := &Processor{
1✔
50
                MaxWidth:           9999,
1✔
51
                MaxHeight:          9999,
1✔
52
                MaxResolution:      81000000,
1✔
53
                Concurrency:        1,
1✔
54
                MaxFilterOps:       -1,
1✔
55
                MaxAnimationFrames: -1,
1✔
56
                Logger:             zap.NewNop(),
1✔
57
                disableFilters:     map[string]bool{},
1✔
58
        }
1✔
59
        v.Filters = FilterMap{
1✔
60
                "watermark":        v.watermark,
1✔
61
                "round_corner":     roundCorner,
1✔
62
                "rotate":           rotate,
1✔
63
                "label":            label,
1✔
64
                "grayscale":        grayscale,
1✔
65
                "brightness":       brightness,
1✔
66
                "background_color": backgroundColor,
1✔
67
                "contrast":         contrast,
1✔
68
                "modulate":         modulate,
1✔
69
                "hue":              hue,
1✔
70
                "saturation":       saturation,
1✔
71
                "rgb":              rgb,
1✔
72
                "blur":             blur,
1✔
73
                "sharpen":          sharpen,
1✔
74
                "strip_icc":        stripIcc,
1✔
75
                "strip_exif":       stripExif,
1✔
76
                "trim":             trim,
1✔
77
                "set_frames":       setFrames,
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
                return nil
1✔
103
        }
1✔
104
        if v.Debug {
2✔
105
                vips.SetLogging(func(domain string, level vips.LogLevel, msg string) {
2✔
106
                        switch level {
1✔
107
                        case vips.LogLevelDebug:
1✔
108
                                v.Logger.Debug(domain, zap.String("log", msg))
1✔
109
                        case vips.LogLevelMessage, vips.LogLevelInfo:
1✔
110
                                v.Logger.Info(domain, zap.String("log", msg))
1✔
111
                        case vips.LogLevelWarning, vips.LogLevelCritical, vips.LogLevelError:
1✔
112
                                v.Logger.Warn(domain, zap.String("log", msg))
1✔
113
                        }
114
                }, vips.LogLevelDebug)
115
        } else {
×
NEW
116
                vips.SetLogging(func(domain string, level vips.LogLevel, msg string) {
×
117
                        v.Logger.Warn(domain, zap.String("log", msg))
×
NEW
118
                }, vips.LogLevelError)
×
119
        }
120
        vips.Startup(&vips.Config{
1✔
121
                MaxCacheFiles:    v.MaxCacheFiles,
1✔
122
                MaxCacheMem:      v.MaxCacheMem,
1✔
123
                MaxCacheSize:     v.MaxCacheSize,
1✔
124
                ConcurrencyLevel: v.Concurrency,
1✔
125
        })
1✔
126
        return nil
1✔
127
}
128

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

143
func newImageFromBlob(
144
        ctx context.Context, blob *imagor.Blob, options *vips.LoadOptions,
145
) (*vips.Image, error) {
1✔
146
        if blob == nil || blob.IsEmpty() {
1✔
147
                return nil, imagor.ErrNotFound
×
148
        }
×
149
        if blob.BlobType() == imagor.BlobTypeMemory {
2✔
150
                buf, width, height, bands, _ := blob.Memory()
1✔
151
                return vips.NewImageFromMemory(buf, width, height, bands)
1✔
152
        }
1✔
153
        reader, _, err := blob.NewReader()
1✔
154
        if err != nil {
1✔
155
                return nil, err
×
156
        }
×
157
        src := vips.NewSource(reader)
1✔
158
        contextDefer(ctx, src.Close)
1✔
159
        img, err := vips.NewImageFromSource(src, options)
1✔
160
        if err != nil && blob.BlobType() == imagor.BlobTypeBMP {
2✔
161
                // fallback with Go BMP decoder if vips error on BMP
1✔
162
                src.Close()
1✔
163
                r, _, err := blob.NewReader()
1✔
164
                if err != nil {
1✔
165
                        return nil, err
×
166
                }
×
167
                defer func() {
2✔
168
                        _ = r.Close()
1✔
169
                }()
1✔
170
                return loadImageFromBMP(r)
1✔
171
        }
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✔
NEW
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 = 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(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(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✔
NEW
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(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(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