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

cshum / imagor / 4851669446

01 May 2023 01:52PM UTC coverage: 90.837% (-0.1%) from 90.949%
4851669446

Pull #359

github

Adrian Shum
feat(vips): pdf page selection support with page(n) filter
Pull Request #359: feat(vips): pdf page selection support with page(n) filter

53 of 53 new or added lines in 4 files covered. (100.0%)

5641 of 6210 relevant lines covered (90.84%)

1.07 hits per line

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

82.64
/vips/processor.go
1
package vips
2

3
import (
4
        "context"
5
        "github.com/cshum/imagor"
6
        "go.uber.org/zap"
7
        "math"
8
        "runtime"
9
        "strings"
10
        "sync"
11
)
12

13
// FilterFunc filter handler function
14
type FilterFunc func(ctx context.Context, img *Image, load imagor.LoadFunc, args ...string) (err error)
15

16
// FilterMap filter handler map
17
type FilterMap map[string]FilterFunc
18

19
var processorLock sync.RWMutex
20
var processorCount int
21

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

40
        disableFilters map[string]bool
41
}
42

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

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

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

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

171
func newThumbnailFromBlob(
172
        ctx context.Context, blob *imagor.Blob,
173
        width, height int, crop Interesting, size Size, params *ImportParams,
174
) (*Image, error) {
1✔
175
        if blob == nil || blob.IsEmpty() {
1✔
176
                return nil, imagor.ErrNotFound
×
177
        }
×
178
        reader, _, err := blob.NewReader()
1✔
179
        if err != nil {
1✔
180
                return nil, err
×
181
        }
×
182
        src := NewSource(reader)
1✔
183
        contextDefer(ctx, src.Close)
1✔
184
        return src.LoadThumbnail(width, height, crop, size, params)
1✔
185
}
186

187
// NewThumbnail creates new thumbnail with resize and crop from imagor.Blob
188
func (v *Processor) NewThumbnail(
189
        ctx context.Context, blob *imagor.Blob, width, height int, crop Interesting,
190
        size Size, n, page int,
191
) (*Image, error) {
1✔
192
        var params = NewImportParams()
1✔
193
        var err error
1✔
194
        var img *Image
1✔
195
        params.FailOnError.Set(false)
1✔
196
        if isMultiPage(blob, n, page) {
2✔
197
                if page < -1 {
1✔
198
                        params.Page.Set(-page - 1)
×
199
                } else if n < -1 {
2✔
200
                        params.NumPages.Set(-n)
1✔
201
                } else {
2✔
202
                        params.NumPages.Set(-1)
1✔
203
                }
1✔
204
                if crop == InterestingNone || size == SizeForce {
2✔
205
                        if img, err = newImageFromBlob(ctx, blob, params); err != nil {
1✔
206
                                return nil, WrapErr(err)
×
207
                        }
×
208
                        if n > 1 || page > 1 {
2✔
209
                                // reload image to restrict frames loaded
1✔
210
                                numPages := img.Pages()
1✔
211
                                img.Close()
1✔
212
                                if page > 1 && page > numPages {
1✔
213
                                        page = numPages
×
214
                                } else if n > 1 && n > numPages {
2✔
215
                                        n = numPages
1✔
216
                                }
1✔
217
                                return v.NewThumbnail(ctx, blob, width, height, crop, size, -n, -page)
1✔
218
                        }
219
                        if _, err = v.CheckResolution(img, nil); err != nil {
2✔
220
                                return nil, err
1✔
221
                        }
1✔
222
                        if err = img.ThumbnailWithSize(width, height, crop, size); err != nil {
1✔
223
                                img.Close()
×
224
                                return nil, WrapErr(err)
×
225
                        }
×
226
                } else {
1✔
227
                        if img, err = v.CheckResolution(newImageFromBlob(ctx, blob, params)); err != nil {
1✔
228
                                return nil, WrapErr(err)
×
229
                        }
×
230
                        if n > 1 || page > 1 {
2✔
231
                                // reload image to restrict frames loaded
1✔
232
                                numPages := img.Pages()
1✔
233
                                img.Close()
1✔
234
                                if page > 1 && page > numPages {
1✔
235
                                        page = numPages
×
236
                                } else if n > 1 && n > numPages {
2✔
237
                                        n = numPages
1✔
238
                                }
1✔
239
                                return v.NewThumbnail(ctx, blob, width, height, crop, size, -n, -page)
1✔
240
                        }
241
                        if err = v.animatedThumbnailWithCrop(img, width, height, crop, size); err != nil {
1✔
242
                                img.Close()
×
243
                                return nil, WrapErr(err)
×
244
                        }
×
245
                }
246
        } else {
1✔
247
                switch blob.BlobType() {
1✔
248
                case imagor.BlobTypeJPEG, imagor.BlobTypeGIF, imagor.BlobTypeWEBP:
1✔
249
                        // only allow real thumbnail for jpeg gif webp
1✔
250
                        img, err = newThumbnailFromBlob(ctx, blob, width, height, crop, size, params)
1✔
251
                default:
1✔
252
                        img, err = v.newThumbnailFallback(ctx, blob, width, height, crop, size, params)
1✔
253
                }
254
        }
255
        return v.CheckResolution(img, WrapErr(err))
1✔
256
}
257

258
func (v *Processor) newThumbnailFallback(
259
        ctx context.Context, blob *imagor.Blob, width, height int, crop Interesting, size Size, params *ImportParams,
260
) (img *Image, err error) {
1✔
261
        if img, err = v.CheckResolution(newImageFromBlob(ctx, blob, params)); err != nil {
2✔
262
                return
1✔
263
        }
1✔
264
        if err = img.ThumbnailWithSize(width, height, crop, size); err != nil {
1✔
265
                img.Close()
×
266
                return
×
267
        }
×
268
        return img, WrapErr(err)
1✔
269
}
270

271
// NewImage creates new Image from imagor.Blob
272
func (v *Processor) NewImage(ctx context.Context, blob *imagor.Blob, n, page int) (*Image, error) {
1✔
273
        var params = NewImportParams()
1✔
274
        params.FailOnError.Set(false)
1✔
275
        if isMultiPage(blob, n, page) {
2✔
276
                if page < -1 {
1✔
277
                        params.Page.Set(-page - 1)
×
278
                } else if n < -1 {
2✔
279
                        params.NumPages.Set(-n)
1✔
280
                } else {
2✔
281
                        params.NumPages.Set(-1)
1✔
282
                }
1✔
283
                img, err := v.CheckResolution(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
                        numPages := img.Pages()
1✔
290
                        img.Close()
1✔
291
                        if page > 1 && page > numPages {
1✔
292
                                page = numPages
×
293
                        } else if n > 1 && n > numPages {
2✔
294
                                n = numPages
1✔
295
                        }
1✔
296
                        return v.NewImage(ctx, blob, -n, -page)
1✔
297
                }
298
                return img, nil
1✔
299
        }
300
        img, err := v.CheckResolution(newImageFromBlob(ctx, blob, params))
1✔
301
        if err != nil {
1✔
302
                return nil, WrapErr(err)
×
303
        }
×
304
        return img, nil
1✔
305
}
306

307
// Thumbnail handles thumbnail operation
308
func (v *Processor) Thumbnail(
309
        img *Image, width, height int, crop Interesting, size Size,
310
) error {
1✔
311
        if crop == InterestingNone || size == SizeForce || img.Height() == img.PageHeight() {
2✔
312
                return img.ThumbnailWithSize(width, height, crop, size)
1✔
313
        }
1✔
314
        return v.animatedThumbnailWithCrop(img, width, height, crop, size)
1✔
315
}
316

317
// FocalThumbnail handles thumbnail with custom focal point
318
func (v *Processor) FocalThumbnail(img *Image, w, h int, fx, fy float64) (err error) {
1✔
319
        if float64(w)/float64(h) > float64(img.Width())/float64(img.PageHeight()) {
2✔
320
                if err = img.Thumbnail(w, v.MaxHeight, InterestingNone); err != nil {
1✔
321
                        return
×
322
                }
×
323
        } else {
1✔
324
                if err = img.Thumbnail(v.MaxWidth, h, InterestingNone); err != nil {
1✔
325
                        return
×
326
                }
×
327
        }
328
        var top, left float64
1✔
329
        left = float64(img.Width())*fx - float64(w)/2
1✔
330
        top = float64(img.PageHeight())*fy - float64(h)/2
1✔
331
        left = math.Max(0, math.Min(left, float64(img.Width()-w)))
1✔
332
        top = math.Max(0, math.Min(top, float64(img.PageHeight()-h)))
1✔
333
        return img.ExtractArea(int(left), int(top), w, h)
1✔
334
}
335

336
func (v *Processor) animatedThumbnailWithCrop(
337
        img *Image, w, h int, crop Interesting, size Size,
338
) (err error) {
1✔
339
        if size == SizeDown && img.Width() < w && img.PageHeight() < h {
1✔
340
                return
×
341
        }
×
342
        var top, left int
1✔
343
        if float64(w)/float64(h) > float64(img.Width())/float64(img.PageHeight()) {
2✔
344
                if err = img.ThumbnailWithSize(w, v.MaxHeight, InterestingNone, size); err != nil {
1✔
345
                        return
×
346
                }
×
347
        } else {
1✔
348
                if err = img.ThumbnailWithSize(v.MaxWidth, h, InterestingNone, size); err != nil {
1✔
349
                        return
×
350
                }
×
351
        }
352
        if crop == InterestingHigh {
2✔
353
                left = img.Width() - w
1✔
354
                top = img.PageHeight() - h
1✔
355
        } else if crop == InterestingCentre || crop == InterestingAttention {
3✔
356
                left = (img.Width() - w) / 2
1✔
357
                top = (img.PageHeight() - h) / 2
1✔
358
        }
1✔
359
        return img.ExtractArea(left, top, w, h)
1✔
360
}
361

362
// CheckResolution check image resolution for image bomb prevention
363
func (v *Processor) CheckResolution(img *Image, err error) (*Image, error) {
1✔
364
        if err != nil || img == nil {
2✔
365
                return img, err
1✔
366
        }
1✔
367
        if img.Width() > v.MaxWidth || img.PageHeight() > v.MaxHeight ||
1✔
368
                (img.Width()*img.Height()) > v.MaxResolution {
2✔
369
                img.Close()
1✔
370
                return nil, imagor.ErrMaxResolutionExceeded
1✔
371
        }
1✔
372
        return img, nil
1✔
373
}
374

375
func isMultiPage(blob *imagor.Blob, n, page int) bool {
1✔
376
        return blob != nil && (blob.SupportsAnimation() || blob.BlobType() == imagor.BlobTypePDF) && ((n != 1 && n != 0) || (page != 1 && page != 0))
1✔
377
}
1✔
378

379
// WrapErr wraps error to become imagor.Error
380
func WrapErr(err error) error {
1✔
381
        if err == nil {
2✔
382
                return nil
1✔
383
        }
1✔
384
        if e, ok := err.(imagor.Error); ok {
2✔
385
                return e
1✔
386
        }
1✔
387
        msg := strings.TrimSpace(err.Error())
1✔
388
        if strings.HasPrefix(msg, "VipsForeignLoad:") &&
1✔
389
                strings.HasSuffix(msg, "is not in a known format") {
1✔
390
                return imagor.ErrUnsupportedFormat
×
391
        }
×
392
        return imagor.NewError(msg, 406)
1✔
393
}
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