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

cshum / imagor / 21858410493

10 Feb 2026 09:03AM UTC coverage: 91.819% (-0.1%) from 91.926%
21858410493

push

github

cshum
Merge remote-tracking branch 'origin/master'

35 of 37 new or added lines in 4 files covered. (94.59%)

124 existing lines in 4 files now uncovered.

5791 of 6307 relevant lines covered (91.82%)

1.1 hits per line

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

88.33
/processor/vipsprocessor/process.go
1
package vipsprocessor
2

3
import (
4
        "context"
5
        "math"
6
        "strconv"
7
        "strings"
8
        "time"
9

10
        "github.com/cshum/imagor"
11
        "github.com/cshum/imagor/imagorpath"
12
        "github.com/cshum/vipsgen/vips"
13
        "go.uber.org/zap"
14
)
15

16
var imageTypeMap = map[string]vips.ImageType{
17
        "gif":  vips.ImageTypeGif,
18
        "jpeg": vips.ImageTypeJpeg,
19
        "jpg":  vips.ImageTypeJpeg,
20
        "pdf":  vips.ImageTypePdf,
21
        "png":  vips.ImageTypePng,
22
        "svg":  vips.ImageTypeSvg,
23
        "tiff": vips.ImageTypeTiff,
24
        "webp": vips.ImageTypeWebp,
25
        "heif": vips.ImageTypeHeif,
26
        "bmp":  vips.ImageTypeBmp,
27
        "avif": vips.ImageTypeAvif,
28
        "jp2":  vips.ImageTypeJp2k,
29
        "jxl":  vips.ImageTypeJxl,
30
}
31

32
// IsAnimationSupported indicates if image type supports animation
33
func IsAnimationSupported(imageType vips.ImageType) bool {
1✔
34
        return imageType == vips.ImageTypeGif || imageType == vips.ImageTypeWebp
1✔
35
}
1✔
36

37
// exportParams holds parameters needed for image export
38
type exportParams struct {
39
        format        vips.ImageType
40
        quality       int
41
        compression   int
42
        bitdepth      int
43
        palette       bool
44
        stripMetadata bool
45
        maxBytes      int
46
}
47

48
// Process implements imagor.Processor interface
49
func (v *Processor) Process(
50
        ctx context.Context, blob *imagor.Blob, p imagorpath.Params, load imagor.LoadFunc,
51
) (*imagor.Blob, error) {
1✔
52
        ctx = withContext(ctx)
1✔
53
        defer contextDone(ctx)
1✔
54

1✔
55
        // Load and process the image
1✔
56
        img, err := v.loadAndProcess(ctx, blob, p, load)
1✔
57
        if err != nil {
2✔
58
                return nil, err
1✔
59
        }
1✔
60
        defer img.Close()
1✔
61

1✔
62
        // Extract export parameters
1✔
63
        params := v.extractExportParams(p, blob, img)
1✔
64

1✔
65
        // Handle metadata response
1✔
66
        if p.Meta {
2✔
67
                stripExif := false
1✔
68
                for _, f := range p.Filters {
2✔
69
                        if f.Name == "strip_exif" {
2✔
70
                                stripExif = true
1✔
71
                                break
1✔
72
                        }
73
                }
74
                return imagor.NewBlobFromJsonMarshal(metadata(img, params.format, stripExif)), nil
1✔
75
        }
76

77
        // Export with max_bytes retry loop
78
        params.format = supportedSaveFormat(params.format)
1✔
79
        for {
2✔
80
                buf, err := v.export(img, params.format, params.compression, params.quality, params.palette, params.bitdepth, params.stripMetadata)
1✔
81
                if err != nil {
1✔
82
                        return nil, WrapErr(err)
×
83
                }
×
84
                if params.maxBytes > 0 && (params.quality > 10 || params.quality == 0) && params.format != vips.ImageTypePng {
2✔
85
                        ln := len(buf)
1✔
86
                        if v.Debug {
2✔
87
                                v.Logger.Debug("max_bytes",
1✔
88
                                        zap.Int("bytes", ln),
1✔
89
                                        zap.Int("quality", params.quality),
1✔
90
                                )
1✔
91
                        }
1✔
92
                        if ln > params.maxBytes {
2✔
93
                                if params.quality == 0 {
2✔
94
                                        params.quality = 80
1✔
95
                                }
1✔
96
                                delta := float64(ln) / float64(params.maxBytes)
1✔
97
                                switch {
1✔
98
                                case delta > 3:
1✔
99
                                        params.quality = params.quality * 25 / 100
1✔
100
                                case delta > 1.5:
1✔
101
                                        params.quality = params.quality * 50 / 100
1✔
102
                                default:
×
103
                                        params.quality = params.quality * 75 / 100
×
104
                                }
105
                                if err := ctx.Err(); err != nil {
1✔
106
                                        return nil, WrapErr(err)
×
107
                                }
×
108
                                continue
1✔
109
                        }
110
                }
111
                blob := imagor.NewBlobFromBytes(buf)
1✔
112
                if typ, ok := params.format.MimeType(); ok {
2✔
113
                        blob.SetContentType(typ)
1✔
114
                }
1✔
115
                return blob, nil
1✔
116
        }
117
}
118

119
// extractExportParams extracts export-related parameters from filters
120
func (v *Processor) extractExportParams(p imagorpath.Params, blob *imagor.Blob, img *vips.Image) *exportParams {
1✔
121
        var (
1✔
122
                quality       int
1✔
123
                bitdepth      int
1✔
124
                compression   int
1✔
125
                palette       bool
1✔
126
                stripMetadata = v.StripMetadata
1✔
127
                maxBytes      int
1✔
128
                format        = vips.ImageTypeUnknown
1✔
129
        )
1✔
130

1✔
131
        // Extract export parameters from filters
1✔
132
        for _, f := range p.Filters {
2✔
133
                if v.disableFilters[f.Name] {
2✔
134
                        continue
1✔
135
                }
136
                switch f.Name {
1✔
137
                case "format":
1✔
138
                        if imageType, ok := imageTypeMap[f.Args]; ok {
2✔
139
                                format = supportedSaveFormat(imageType)
1✔
140
                        }
1✔
141
                case "quality":
1✔
142
                        quality, _ = strconv.Atoi(f.Args)
1✔
143
                case "autojpg":
1✔
144
                        format = vips.ImageTypeJpeg
1✔
145
                case "palette":
1✔
146
                        palette = true
1✔
147
                case "bitdepth":
1✔
148
                        bitdepth, _ = strconv.Atoi(f.Args)
1✔
149
                case "compression":
1✔
150
                        compression, _ = strconv.Atoi(f.Args)
1✔
151
                case "max_bytes":
1✔
152
                        if n, _ := strconv.Atoi(f.Args); n > 0 {
2✔
153
                                maxBytes = n
1✔
154
                        }
1✔
155
                case "strip_metadata":
1✔
156
                        stripMetadata = true
1✔
157
                }
158
        }
159

160
        // Determine format if not specified
161
        if format == vips.ImageTypeUnknown {
2✔
162
                if blob.BlobType() == imagor.BlobTypeAVIF {
2✔
163
                        format = vips.ImageTypeAvif
1✔
164
                } else {
2✔
165
                        format = img.Format()
1✔
166
                }
1✔
167
        }
168

169
        return &exportParams{
1✔
170
                format:        format,
1✔
171
                quality:       quality,
1✔
172
                compression:   compression,
1✔
173
                bitdepth:      bitdepth,
1✔
174
                palette:       palette,
1✔
175
                stripMetadata: stripMetadata,
1✔
176
                maxBytes:      maxBytes,
1✔
177
        }
1✔
178
}
179

180
// loadAndProcess loads the image from blob and applies all transformations
181
func (v *Processor) loadAndProcess(
182
        ctx context.Context, blob *imagor.Blob, p imagorpath.Params, load imagor.LoadFunc,
183
) (*vips.Image, error) {
1✔
184
        var (
1✔
185
                thumbnailNotSupported bool
1✔
186
                upscale               = true
1✔
187
                stretch               = p.Stretch
1✔
188
                thumbnail             = false
1✔
189
                orient                int
1✔
190
                img                   *vips.Image
1✔
191
                maxN                  = v.MaxAnimationFrames
1✔
192
                page                  = 1
1✔
193
                dpi                   = 0
1✔
194
                err                   error
1✔
195
        )
1✔
196
        if p.Trim || p.VFlip || p.FullFitIn || p.AdaptiveFitIn {
2✔
197
                thumbnailNotSupported = true
1✔
198
        }
1✔
199
        if p.FitIn {
2✔
200
                upscale = false
1✔
201
        }
1✔
202
        if maxN == 0 || maxN < -1 {
2✔
203
                maxN = 1
1✔
204
        }
1✔
205
        if blob != nil && !blob.SupportsAnimation() {
2✔
206
                maxN = 1
1✔
207
        }
1✔
208
        for _, f := range p.Filters {
2✔
209
                if v.disableFilters[f.Name] {
2✔
210
                        continue
1✔
211
                }
212
                switch f.Name {
1✔
213
                case "format":
1✔
214
                        if imageType, ok := imageTypeMap[f.Args]; ok {
2✔
215
                                format := supportedSaveFormat(imageType)
1✔
216
                                if !IsAnimationSupported(format) {
2✔
217
                                        // no frames if export format not support animation
1✔
218
                                        maxN = 1
1✔
219
                                }
1✔
220
                        }
221
                case "max_frames":
1✔
222
                        if n, _ := strconv.Atoi(f.Args); n > 0 && (maxN == -1 || n < maxN) {
2✔
223
                                maxN = n
1✔
224
                        }
1✔
225
                case "stretch":
1✔
226
                        stretch = true
1✔
227
                case "upscale":
1✔
228
                        upscale = true
1✔
229
                case "no_upscale":
1✔
230
                        upscale = false
1✔
231
                case "fill", "background_color":
1✔
232
                        if args := strings.Split(f.Args, ","); args[0] == "auto" {
2✔
233
                                thumbnailNotSupported = true
1✔
234
                        }
1✔
235
                case "page":
1✔
236
                        if n, _ := strconv.Atoi(f.Args); n > 0 {
2✔
237
                                page = n
1✔
238
                        }
1✔
239
                case "dpi":
×
240
                        if n, _ := strconv.Atoi(f.Args); n > 0 {
×
241
                                dpi = n
×
242
                        }
×
243
                case "orient":
1✔
244
                        if n, _ := strconv.Atoi(f.Args); n > 0 {
2✔
245
                                orient = n
1✔
246
                                thumbnailNotSupported = true
1✔
247
                        }
1✔
248
                case "max_bytes":
1✔
249
                        if n, _ := strconv.Atoi(f.Args); n > 0 {
2✔
250
                                thumbnailNotSupported = true
1✔
251
                        }
1✔
252
                case "trim", "focal", "rotate":
1✔
253
                        thumbnailNotSupported = true
1✔
254
                }
255
        }
256

257
        // Force fallback for nested images >= 1000px to avoid VIPS thumbnail bug
258
        if isNestedImage(ctx) && (p.Width >= 1000 || p.Height >= 1000) {
1✔
UNCOV
259
                thumbnailNotSupported = true
×
UNCOV
260
        }
×
261

262
        if !thumbnailNotSupported &&
1✔
263
                p.CropBottom == 0.0 && p.CropTop == 0.0 && p.CropLeft == 0.0 && p.CropRight == 0.0 {
2✔
264
                // apply shrink-on-load where possible
1✔
265
                if p.FitIn {
2✔
266
                        if p.Width > 0 || p.Height > 0 {
2✔
267
                                w := p.Width
1✔
268
                                h := p.Height
1✔
269
                                if w == 0 {
2✔
270
                                        w = v.MaxWidth
1✔
271
                                }
1✔
272
                                if h == 0 {
2✔
273
                                        h = v.MaxHeight
1✔
274
                                }
1✔
275
                                size := vips.SizeDown
1✔
276
                                if upscale {
2✔
277
                                        size = vips.SizeBoth
1✔
278
                                }
1✔
279
                                if img, err = v.NewThumbnail(
1✔
280
                                        ctx, blob, w, h, vips.InterestingNone, size, maxN, page, dpi,
1✔
281
                                ); err != nil {
1✔
UNCOV
282
                                        return nil, err
×
UNCOV
283
                                }
×
284
                                thumbnail = true
1✔
285
                        }
286
                } else if stretch {
2✔
287
                        if p.Width > 0 && p.Height > 0 {
2✔
288
                                if img, err = v.NewThumbnail(
1✔
289
                                        ctx, blob, p.Width, p.Height,
1✔
290
                                        vips.InterestingNone, vips.SizeForce, maxN, page, dpi,
1✔
291
                                ); err != nil {
1✔
UNCOV
292
                                        return nil, err
×
UNCOV
293
                                }
×
294
                                thumbnail = true
1✔
295
                        }
296
                } else {
1✔
297
                        if p.Width > 0 && p.Height > 0 {
2✔
298
                                interest := vips.InterestingNone
1✔
299
                                if p.Smart {
2✔
300
                                        interest = vips.InterestingAttention
1✔
301
                                        thumbnail = true
1✔
302
                                } else if (p.VAlign == imagorpath.VAlignTop && p.HAlign == "") ||
2✔
303
                                        (p.HAlign == imagorpath.HAlignLeft && p.VAlign == "") {
2✔
304
                                        interest = vips.InterestingLow
1✔
305
                                        thumbnail = true
1✔
306
                                } else if (p.VAlign == imagorpath.VAlignBottom && p.HAlign == "") ||
2✔
307
                                        (p.HAlign == imagorpath.HAlignRight && p.VAlign == "") {
2✔
308
                                        interest = vips.InterestingHigh
1✔
309
                                        thumbnail = true
1✔
310
                                } else if (p.VAlign == "" || p.VAlign == "middle") &&
2✔
311
                                        (p.HAlign == "" || p.HAlign == "center") {
2✔
312
                                        interest = vips.InterestingCentre
1✔
313
                                        thumbnail = true
1✔
314
                                }
1✔
315
                                if thumbnail {
2✔
316
                                        if img, err = v.NewThumbnail(
1✔
317
                                                ctx, blob, p.Width, p.Height,
1✔
318
                                                interest, vips.SizeBoth, maxN, page, dpi,
1✔
319
                                        ); err != nil {
2✔
320
                                                return nil, err
1✔
321
                                        }
1✔
322
                                }
323
                        } else if p.Width > 0 && p.Height == 0 {
2✔
324
                                if img, err = v.NewThumbnail(
1✔
325
                                        ctx, blob, p.Width, v.MaxHeight,
1✔
326
                                        vips.InterestingNone, vips.SizeBoth, maxN, page, dpi,
1✔
327
                                ); err != nil {
2✔
328
                                        return nil, err
1✔
329
                                }
1✔
330
                                thumbnail = true
1✔
331
                        } else if p.Height > 0 && p.Width == 0 {
2✔
332
                                if img, err = v.NewThumbnail(
1✔
333
                                        ctx, blob, v.MaxWidth, p.Height,
1✔
334
                                        vips.InterestingNone, vips.SizeBoth, maxN, page, dpi,
1✔
335
                                ); err != nil {
1✔
UNCOV
336
                                        return nil, err
×
UNCOV
337
                                }
×
338
                                thumbnail = true
1✔
339
                        }
340
                }
341
        }
342
        if !thumbnail {
2✔
343
                if thumbnailNotSupported {
2✔
344
                        if img, err = v.NewImage(ctx, blob, maxN, page, dpi); err != nil {
1✔
UNCOV
345
                                return nil, err
×
UNCOV
346
                        }
×
347
                } else {
1✔
348
                        if img, err = v.NewThumbnail(
1✔
349
                                ctx, blob, v.MaxWidth, v.MaxHeight,
1✔
350
                                vips.InterestingNone, vips.SizeDown, maxN, page, dpi,
1✔
351
                        ); err != nil {
2✔
352
                                return nil, err
1✔
353
                        }
1✔
354
                }
355
        }
356

357
        if orient > 0 {
2✔
358
                // orient rotate before resize
1✔
359
                if err = img.RotMultiPage(getAngle(orient)); err != nil {
1✔
UNCOV
360
                        return nil, err
×
UNCOV
361
                }
×
362
        }
363

364
        var (
1✔
365
                origWidth  = float64(img.Width())
1✔
366
                origHeight = float64(img.PageHeight())
1✔
367
        )
1✔
368
        if v.Debug {
2✔
369
                v.Logger.Debug("image",
1✔
370
                        zap.Int("width", img.Width()),
1✔
371
                        zap.Int("height", img.Height()),
1✔
372
                        zap.Int("page_height", img.PageHeight()))
1✔
373
        }
1✔
374

375
        // Extract focal points for transformation
376
        var focalRects []focal
1✔
377
        for _, f := range p.Filters {
2✔
378
                if v.disableFilters[f.Name] {
2✔
379
                        continue
1✔
380
                }
381
                if f.Name == "focal" {
2✔
382
                        args := strings.FieldsFunc(f.Args, argSplit)
1✔
383
                        switch len(args) {
1✔
384
                        case 4:
1✔
385
                                rect := focal{}
1✔
386
                                rect.Left, _ = strconv.ParseFloat(args[0], 64)
1✔
387
                                rect.Top, _ = strconv.ParseFloat(args[1], 64)
1✔
388
                                rect.Right, _ = strconv.ParseFloat(args[2], 64)
1✔
389
                                rect.Bottom, _ = strconv.ParseFloat(args[3], 64)
1✔
390
                                if rect.Left < 1 && rect.Top < 1 && rect.Right <= 1 && rect.Bottom <= 1 {
2✔
391
                                        rect.Left *= origWidth
1✔
392
                                        rect.Right *= origWidth
1✔
393
                                        rect.Top *= origHeight
1✔
394
                                        rect.Bottom *= origHeight
1✔
395
                                }
1✔
396
                                if rect.Right > rect.Left && rect.Bottom > rect.Top {
2✔
397
                                        focalRects = append(focalRects, rect)
1✔
398
                                }
1✔
399
                        case 2:
1✔
400
                                rect := focal{}
1✔
401
                                rect.Left, _ = strconv.ParseFloat(args[0], 64)
1✔
402
                                rect.Top, _ = strconv.ParseFloat(args[1], 64)
1✔
403
                                if rect.Left < 1 && rect.Top < 1 {
2✔
404
                                        rect.Left *= origWidth
1✔
405
                                        rect.Top *= origHeight
1✔
406
                                }
1✔
407
                                rect.Right = rect.Left + 1
1✔
408
                                rect.Bottom = rect.Top + 1
1✔
409
                                focalRects = append(focalRects, rect)
1✔
410
                        }
411
                }
412
        }
413
        // Apply transformations
414
        if err := v.applyTransformations(ctx, img, p, load, thumbnail, stretch, upscale, focalRects); err != nil {
1✔
UNCOV
415
                return nil, WrapErr(err)
×
UNCOV
416
        }
×
417

418
        return img, nil
1✔
419
}
420

421
// applyTransformations applies all image transformations (crop, resize, flip, filters)
422
func (v *Processor) applyTransformations(
423
        ctx context.Context, img *vips.Image, p imagorpath.Params, load imagor.LoadFunc, thumbnail, stretch, upscale bool, focalRects []focal,
424
) error {
1✔
425
        var (
1✔
426
                origWidth  = float64(img.Width())
1✔
427
                origHeight = float64(img.PageHeight())
1✔
428
                cropLeft,
1✔
429
                cropTop,
1✔
430
                cropRight,
1✔
431
                cropBottom float64
1✔
432
        )
1✔
433
        if p.CropRight > 0 || p.CropLeft > 0 || p.CropBottom > 0 || p.CropTop > 0 {
2✔
434
                // percentage
1✔
435
                cropLeft = math.Max(p.CropLeft, 0)
1✔
436
                cropTop = math.Max(p.CropTop, 0)
1✔
437
                cropRight = p.CropRight
1✔
438
                cropBottom = p.CropBottom
1✔
439
                if p.CropLeft < 1 && p.CropTop < 1 && p.CropRight <= 1 && p.CropBottom <= 1 {
2✔
440
                        cropLeft = math.Round(cropLeft * origWidth)
1✔
441
                        cropTop = math.Round(cropTop * origHeight)
1✔
442
                        cropRight = math.Round(cropRight * origWidth)
1✔
443
                        cropBottom = math.Round(cropBottom * origHeight)
1✔
444
                }
1✔
445
                if cropRight == 0 {
2✔
446
                        cropRight = origWidth - 1
1✔
447
                }
1✔
448
                if cropBottom == 0 {
2✔
449
                        cropBottom = origHeight - 1
1✔
450
                }
1✔
451
                cropRight = math.Min(cropRight, origWidth-1)
1✔
452
                cropBottom = math.Min(cropBottom, origHeight-1)
1✔
453
        }
454
        if p.Trim {
2✔
455
                if l, t, w, h, err := findTrim(ctx, img, p.TrimBy, p.TrimTolerance); err == nil {
2✔
456
                        cropLeft = math.Max(cropLeft, float64(l))
1✔
457
                        cropTop = math.Max(cropTop, float64(t))
1✔
458
                        if cropRight > 0 {
2✔
459
                                cropRight = math.Min(cropRight, float64(l+w))
1✔
460
                        } else {
2✔
461
                                cropRight = float64(l + w)
1✔
462
                        }
1✔
463
                        if cropBottom > 0 {
2✔
464
                                cropBottom = math.Min(cropBottom, float64(t+h))
1✔
465
                        } else {
2✔
466
                                cropBottom = float64(t + h)
1✔
467
                        }
1✔
468
                }
469
        }
470
        if cropRight > cropLeft && cropBottom > cropTop {
2✔
471
                if err := img.ExtractAreaMultiPage(
1✔
472
                        int(cropLeft), int(cropTop), int(cropRight-cropLeft), int(cropBottom-cropTop),
1✔
473
                ); err != nil {
1✔
UNCOV
474
                        return err
×
UNCOV
475
                }
×
476
        }
477
        var (
1✔
478
                w = p.Width
1✔
479
                h = p.Height
1✔
480
        )
1✔
481

1✔
482
        // Apply adaptive fit-in: swap dimensions if it would get better image definition
1✔
483
        if p.AdaptiveFitIn && w > 0 && h > 0 {
2✔
484
                imgAspect := float64(img.Width()) / float64(img.PageHeight())
1✔
485
                boxAspect := float64(w) / float64(h)
1✔
486
                // If orientations differ (one portrait, one landscape), swap dimensions
1✔
487
                if (imgAspect > 1) != (boxAspect > 1) {
2✔
488
                        w, h = h, w
1✔
489
                }
1✔
490
        }
491

492
        if w == 0 && h == 0 {
2✔
493
                w = img.Width()
1✔
494
                h = img.PageHeight()
1✔
495
        } else if w == 0 {
3✔
496
                w = img.Width() * h / img.PageHeight()
1✔
497
                if !upscale && w > img.Width() {
1✔
498
                        w = img.Width()
×
499
                }
×
500
        } else if h == 0 {
2✔
501
                h = img.PageHeight() * w / img.Width()
1✔
502
                if !upscale && h > img.PageHeight() {
1✔
UNCOV
503
                        h = img.PageHeight()
×
UNCOV
504
                }
×
505
        }
506
        if !thumbnail {
2✔
507
                if p.FitIn {
2✔
508
                        // Calculate dimensions for full-fit-in
1✔
509
                        if p.FullFitIn && w > 0 && h > 0 {
2✔
510
                                imgAspect := float64(img.Width()) / float64(img.PageHeight())
1✔
511
                                boxAspect := float64(w) / float64(h)
1✔
512

1✔
513
                                if imgAspect < boxAspect {
2✔
514
                                        // Image is taller (portrait) - use width as constraint, height will exceed box
1✔
515
                                        h = int(float64(w) / imgAspect)
1✔
516
                                } else {
2✔
517
                                        // Image is wider (landscape) - use height as constraint, width will exceed box
1✔
518
                                        w = int(float64(h) * imgAspect)
1✔
519
                                }
1✔
520
                        }
521

522
                        if upscale || w < img.Width() || h < img.PageHeight() {
2✔
523
                                opts := &vips.ThumbnailImageOptions{Height: h, Crop: vips.InterestingNone}
1✔
524
                                if err := img.ThumbnailImage(w, opts); err != nil {
1✔
UNCOV
525
                                        return err
×
UNCOV
526
                                }
×
527
                        }
528
                } else if stretch {
2✔
529
                        if upscale || (w < img.Width() && h < img.PageHeight()) {
2✔
530
                                if err := img.ThumbnailImage(
1✔
531
                                        w, &vips.ThumbnailImageOptions{Height: h, Crop: vips.InterestingNone, Size: vips.SizeForce},
1✔
532
                                ); err != nil {
1✔
UNCOV
533
                                        return err
×
534
                                }
×
535
                        }
536
                } else if upscale || w < img.Width() || h < img.PageHeight() {
2✔
537
                        interest := vips.InterestingCentre
1✔
538
                        if p.Smart {
1✔
UNCOV
539
                                interest = vips.InterestingAttention
×
540
                        } else if float64(w)/float64(h) > float64(img.Width())/float64(img.PageHeight()) {
2✔
541
                                if p.VAlign == imagorpath.VAlignTop {
2✔
542
                                        interest = vips.InterestingLow
1✔
543
                                } else if p.VAlign == imagorpath.VAlignBottom {
3✔
544
                                        interest = vips.InterestingHigh
1✔
545
                                }
1✔
546
                        } else {
1✔
547
                                if p.HAlign == imagorpath.HAlignLeft {
2✔
548
                                        interest = vips.InterestingLow
1✔
549
                                } else if p.HAlign == imagorpath.HAlignRight {
3✔
550
                                        interest = vips.InterestingHigh
1✔
551
                                }
1✔
552
                        }
553
                        if len(focalRects) > 0 {
2✔
554
                                focalX, focalY := parseFocalPoint(focalRects...)
1✔
555
                                if err := v.FocalThumbnail(
1✔
556
                                        img, w, h,
1✔
557
                                        (focalX-cropLeft)/float64(img.Width()),
1✔
558
                                        (focalY-cropTop)/float64(img.PageHeight()),
1✔
559
                                ); err != nil {
1✔
560
                                        return err
×
UNCOV
561
                                }
×
562
                        } else {
1✔
563
                                if err := v.Thumbnail(img, w, h, interest, vips.SizeBoth); err != nil {
1✔
564
                                        return err
×
UNCOV
565
                                }
×
566
                        }
567
                        if _, err := v.CheckResolution(img, nil); err != nil {
1✔
UNCOV
568
                                return err
×
569
                        }
×
570
                }
571
        }
572
        if p.HFlip {
2✔
573
                if err := img.Flip(vips.DirectionHorizontal); err != nil {
1✔
574
                        return err
×
575
                }
×
576
        }
577
        if p.VFlip {
2✔
578
                if err := img.Flip(vips.DirectionVertical); err != nil {
1✔
579
                        return err
×
580
                }
×
581
        }
582
        for i, filter := range p.Filters {
2✔
583
                if err := ctx.Err(); err != nil {
1✔
UNCOV
584
                        return err
×
UNCOV
585
                }
×
586
                if v.disableFilters[filter.Name] {
2✔
587
                        continue
1✔
588
                }
589
                if v.MaxFilterOps > 0 && i >= v.MaxFilterOps {
2✔
590
                        if v.Debug {
2✔
591
                                v.Logger.Debug("max-filter-ops-exceeded",
1✔
592
                                        zap.String("name", filter.Name), zap.String("args", filter.Args))
1✔
593
                        }
1✔
594
                        break
1✔
595
                }
596
                start := time.Now()
1✔
597
                var args []string
1✔
598
                if filter.Args != "" {
2✔
599
                        args = imagorpath.SplitArgs(filter.Args)
1✔
600
                }
1✔
601
                if fn := v.Filters[filter.Name]; fn != nil {
2✔
602
                        if err := fn(ctx, img, load, args...); err != nil {
1✔
UNCOV
603
                                return err
×
604
                        }
×
605
                } else if filter.Name == "fill" {
2✔
606
                        if err := v.fill(ctx, img, w, h,
1✔
607
                                p.PaddingLeft, p.PaddingTop, p.PaddingRight, p.PaddingBottom,
1✔
608
                                filter.Args); err != nil {
1✔
UNCOV
609
                                return err
×
UNCOV
610
                        }
×
611
                }
612
                if v.Debug {
2✔
613
                        v.Logger.Debug("filter",
1✔
614
                                zap.String("name", filter.Name), zap.String("args", filter.Args),
1✔
615
                                zap.Duration("took", time.Since(start)))
1✔
616
                }
1✔
617
        }
618
        return nil
1✔
619
}
620

621
// Metadata image attributes
622
type Metadata struct {
623
        Format      string            `json:"format"`
624
        ContentType string            `json:"content_type"`
625
        Width       int               `json:"width"`
626
        Height      int               `json:"height"`
627
        Orientation int               `json:"orientation"`
628
        Pages       int               `json:"pages"`
629
        Bands       int               `json:"bands"`
630
        Exif        map[string]string `json:"exif"`
631
}
632

633
func metadata(img *vips.Image, format vips.ImageType, stripExif bool) *Metadata {
1✔
634
        pages := 1
1✔
635
        if IsAnimationSupported(format) {
2✔
636
                pages = img.Height() / img.PageHeight()
1✔
637
        }
1✔
638
        if format == vips.ImageTypePdf {
2✔
639
                pages = img.Pages()
1✔
640
        }
1✔
641
        exif := map[string]string{}
1✔
642
        if !stripExif {
2✔
643
                exif = extractExif(img.Exif())
1✔
644
        }
1✔
645
        mimeType, _ := format.MimeType()
1✔
646
        return &Metadata{
1✔
647
                Format:      string(format),
1✔
648
                ContentType: mimeType,
1✔
649
                Width:       img.Width(),
1✔
650
                Height:      img.PageHeight(),
1✔
651
                Pages:       pages,
1✔
652
                Bands:       img.Bands(),
1✔
653
                Orientation: img.Orientation(),
1✔
654
                Exif:        exif,
1✔
655
        }
1✔
656
}
657

658
func supportedSaveFormat(format vips.ImageType) vips.ImageType {
1✔
659
        switch format {
1✔
660
        case vips.ImageTypePng, vips.ImageTypeWebp, vips.ImageTypeTiff, vips.ImageTypeGif, vips.ImageTypeAvif, vips.ImageTypeHeif, vips.ImageTypeJp2k, vips.ImageTypeJxl:
1✔
661
                return format
1✔
662
        }
663
        return vips.ImageTypeJpeg
1✔
664
}
665

666
func (v *Processor) export(
667
        image *vips.Image, format vips.ImageType, compression int, quality int, palette bool, bitdepth int, stripMetadata bool,
668
) ([]byte, error) {
1✔
669
        // check resolution before export
1✔
670
        if _, err := v.CheckResolution(image, nil); err != nil {
1✔
UNCOV
671
                return nil, err
×
UNCOV
672
        }
×
673
        switch format {
1✔
674
        case vips.ImageTypePng:
1✔
675
                opts := &vips.PngsaveBufferOptions{
1✔
676
                        Q:           quality,
1✔
677
                        Palette:     palette,
1✔
678
                        Bitdepth:    bitdepth,
1✔
679
                        Compression: compression,
1✔
680
                }
1✔
681
                if stripMetadata {
2✔
682
                        opts.Keep = vips.KeepNone
1✔
683
                } else {
2✔
684
                        opts.Keep = vips.KeepAll
1✔
685
                }
1✔
686
                return image.PngsaveBuffer(opts)
1✔
687
        case vips.ImageTypeWebp:
1✔
688
                opts := &vips.WebpsaveBufferOptions{
1✔
689
                        Q: quality,
1✔
690
                }
1✔
691
                if stripMetadata {
2✔
692
                        opts.Keep = vips.KeepNone
1✔
693
                } else {
2✔
694
                        opts.Keep = vips.KeepAll
1✔
695
                }
1✔
696
                return image.WebpsaveBuffer(opts)
1✔
697
        case vips.ImageTypeJxl:
1✔
698
                opts := &vips.JxlsaveBufferOptions{
1✔
699
                        Q: quality,
1✔
700
                }
1✔
701
                if stripMetadata {
1✔
UNCOV
702
                        opts.Keep = vips.KeepNone
×
703
                } else {
1✔
704
                        opts.Keep = vips.KeepAll
1✔
705
                }
1✔
706
                return image.JxlsaveBuffer(opts)
1✔
707
        case vips.ImageTypeTiff:
1✔
708
                opts := &vips.TiffsaveBufferOptions{
1✔
709
                        Q: quality,
1✔
710
                }
1✔
711
                if stripMetadata {
2✔
712
                        opts.Keep = vips.KeepNone
1✔
713
                } else {
2✔
714
                        opts.Keep = vips.KeepAll
1✔
715
                }
1✔
716
                return image.TiffsaveBuffer(opts)
1✔
717
        case vips.ImageTypeGif:
1✔
718
                opts := &vips.GifsaveBufferOptions{}
1✔
719
                if stripMetadata {
2✔
720
                        opts.Keep = vips.KeepNone
1✔
721
                } else {
2✔
722
                        opts.Keep = vips.KeepAll
1✔
723
                }
1✔
724
                return image.GifsaveBuffer(opts)
1✔
725
        case vips.ImageTypeAvif:
1✔
726
                opts := &vips.HeifsaveBufferOptions{
1✔
727
                        Q:           quality,
1✔
728
                        Compression: vips.HeifCompressionAv1,
1✔
729
                }
1✔
730
                if stripMetadata {
2✔
731
                        opts.Keep = vips.KeepNone
1✔
732
                } else {
2✔
733
                        opts.Keep = vips.KeepAll
1✔
734
                }
1✔
735
                opts.Effort = 9 - v.AvifSpeed
1✔
736
                return image.HeifsaveBuffer(opts)
1✔
737
        case vips.ImageTypeHeif:
1✔
738
                opts := &vips.HeifsaveBufferOptions{
1✔
739
                        Q: quality,
1✔
740
                }
1✔
741
                if stripMetadata {
1✔
UNCOV
742
                        opts.Keep = vips.KeepNone
×
743
                } else {
1✔
744
                        opts.Keep = vips.KeepAll
1✔
745
                }
1✔
746
                return image.HeifsaveBuffer(opts)
1✔
UNCOV
747
        case vips.ImageTypeJp2k:
×
UNCOV
748
                opts := &vips.Jp2ksaveBufferOptions{
×
UNCOV
749
                        Q: quality,
×
UNCOV
750
                }
×
751
                if stripMetadata {
×
UNCOV
752
                        opts.Keep = vips.KeepNone
×
NEW
753
                } else {
×
NEW
754
                        opts.Keep = vips.KeepAll
×
UNCOV
755
                }
×
UNCOV
756
                return image.Jp2ksaveBuffer(opts)
×
757
        default:
1✔
758
                opts := &vips.JpegsaveBufferOptions{}
1✔
759
                if v.MozJPEG {
1✔
760
                        opts.Q = 75
×
761
                        opts.Keep = vips.KeepNone
×
762
                        opts.OptimizeCoding = true
×
763
                        opts.Interlace = true
×
764
                        opts.OptimizeScans = true
×
765
                        opts.TrellisQuant = true
×
766
                        opts.QuantTable = 3
×
767
                }
×
768
                if quality > 0 {
2✔
769
                        opts.Q = quality
1✔
770
                }
1✔
771
                if stripMetadata {
2✔
772
                        opts.Keep = vips.KeepNone
1✔
773
                } else if !v.MozJPEG {
3✔
774
                        opts.Keep = vips.KeepAll
1✔
775
                }
1✔
776
                return image.JpegsaveBuffer(opts)
1✔
777
        }
778
}
779

780
func argSplit(r rune) bool {
1✔
781
        return r == 'x' || r == ',' || r == ':'
1✔
782
}
1✔
783

784
type focal struct {
785
        Left   float64
786
        Right  float64
787
        Top    float64
788
        Bottom float64
789
}
790

791
func parseFocalPoint(focalRects ...focal) (focalX, focalY float64) {
1✔
792
        var sumWeight float64
1✔
793
        for _, f := range focalRects {
2✔
794
                sumWeight += (f.Right - f.Left) * (f.Bottom - f.Top)
1✔
795
        }
1✔
796
        for _, f := range focalRects {
2✔
797
                r := (f.Right - f.Left) * (f.Bottom - f.Top) / sumWeight
1✔
798
                focalX += (f.Left + f.Right) / 2 * r
1✔
799
                focalY += (f.Top + f.Bottom) / 2 * r
1✔
800
        }
1✔
801
        return
1✔
802
}
803

804
func findTrim(
805
        _ context.Context, img *vips.Image, pos string, tolerance int,
806
) (l, t, w, h int, err error) {
1✔
807
        if isAnimated(img) {
2✔
808
                // skip animation support
1✔
809
                return
1✔
810
        }
1✔
811
        tmp, err := img.Copy(&vips.CopyOptions{Interpretation: vips.InterpretationSrgb})
1✔
812
        if err != nil {
1✔
UNCOV
813
                return
×
UNCOV
814
        }
×
815
        defer tmp.Close()
1✔
816
        if tmp.HasAlpha() {
2✔
817
                if err = tmp.Flatten(&vips.FlattenOptions{Background: []float64{255, 0, 255}}); err != nil {
1✔
UNCOV
818
                        return
×
UNCOV
819
                }
×
820
        }
821
        var x, y int
1✔
822
        if pos == imagorpath.TrimByBottomRight {
2✔
823
                x = tmp.Width() - 1
1✔
824
                y = tmp.PageHeight() - 1
1✔
825
        }
1✔
826
        if tolerance == 0 {
2✔
827
                tolerance = 1
1✔
828
        }
1✔
829
        background, err := tmp.Getpoint(x, y, nil)
1✔
830
        if err != nil {
1✔
831
                return
×
832
        }
×
833
        l, t, w, h, err = tmp.FindTrim(&vips.FindTrimOptions{
1✔
834
                Threshold:  float64(tolerance),
1✔
835
                Background: background,
1✔
836
        })
1✔
837
        return
1✔
838
}
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

© 2026 Coveralls, Inc