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

cshum / imagor / 22022082076

14 Feb 2026 06:18PM UTC coverage: 91.811% (-0.2%) from 91.968%
22022082076

Pull #713

github

mevinbabuc
feat: add in-memory watermark cache with ristretto

Adds an optional in-memory cache for processed watermark images using
dgraph-io/ristretto. When enabled via -imagor-watermark-cache-size,
watermarks are cached after resize, colorspace conversion, and alpha
application as vips.Image objects for fast copy-on-write retrieval.

Also adds singleflight deduplication for concurrent image loads to
prevent redundant fetches of the same watermark.
Pull Request #713: feat: add watermark caching for improved performance

81 of 99 new or added lines in 5 files covered. (81.82%)

2 existing lines in 2 files now uncovered.

5864 of 6387 relevant lines covered (91.81%)

1.1 hits per line

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

87.75
/processor/vipsprocessor/filter.go
1
package vipsprocessor
2

3
import (
4
        "context"
5
        "encoding/base64"
6
        "fmt"
7
        "math"
8
        "net/url"
9
        "strconv"
10
        "strings"
11

12
        "github.com/cshum/vipsgen/vips"
13

14
        "github.com/cshum/imagor"
15
        "github.com/cshum/imagor/imagorpath"
16
        "go.uber.org/zap"
17
)
18

19
type watermarkCacheKey struct {
20
        Image  string
21
        Width  int
22
        Height int
23
        Alpha  float64
24
        N      int
25
}
26

27
func (k watermarkCacheKey) String() string {
1✔
28
        return fmt.Sprintf("%s|%d|%d|%.4f|%d", k.Image, k.Width, k.Height, k.Alpha, k.N)
1✔
29
}
1✔
30

31
func (v *Processor) image(ctx context.Context, img *vips.Image, load imagor.LoadFunc, args ...string) (err error) {
1✔
32
        ln := len(args)
1✔
33
        if ln < 1 {
1✔
34
                return
×
35
        }
×
36
        imagorPath := args[0]
1✔
37
        if unescape, e := url.QueryUnescape(args[0]); e == nil {
2✔
38
                imagorPath = unescape
1✔
39
        }
1✔
40
        params := imagorpath.Parse(imagorPath)
1✔
41
        var blob *imagor.Blob
1✔
42
        if blob, err = load(params.Image); err != nil {
1✔
43
                return
×
44
        }
×
45
        var overlay *vips.Image
1✔
46
        // create fresh context for this processing level
1✔
47
        // while preserving parent resource tracking context
1✔
48
        ctx = withContext(ctx)
1✔
49
        if overlay, err = v.loadAndProcess(ctx, blob, params, load); err != nil || overlay == nil {
1✔
50
                return
×
51
        }
×
52
        contextDefer(ctx, overlay.Close)
1✔
53

1✔
54
        var xArg, yArg string
1✔
55
        var alpha float64
1✔
56
        var blendMode = vips.BlendModeOver // default to normal
1✔
57

1✔
58
        if ln >= 2 {
2✔
59
                xArg = args[1]
1✔
60
        }
1✔
61
        if ln >= 3 {
2✔
62
                yArg = args[2]
1✔
63
        }
1✔
64
        if ln >= 4 {
2✔
65
                alpha, _ = strconv.ParseFloat(args[3], 64)
1✔
66
        }
1✔
67
        if ln >= 5 {
2✔
68
                blendMode = getBlendMode(args[4])
1✔
69
        }
1✔
70

71
        return compositeOverlay(img, overlay, xArg, yArg, alpha, blendMode)
1✔
72
}
73

74
func (v *Processor) watermark(ctx context.Context, img *vips.Image, load imagor.LoadFunc, args ...string) (err error) {
1✔
75
        ln := len(args)
1✔
76
        if ln < 1 {
2✔
77
                return
1✔
78
        }
1✔
79
        image := args[0]
1✔
80

1✔
81
        if unescape, e := url.QueryUnescape(args[0]); e == nil {
2✔
82
                image = unescape
1✔
83
        }
1✔
84

85
        if strings.HasPrefix(image, "b64:") {
2✔
86
                result := make([]byte, base64.RawURLEncoding.DecodedLen(len(image[4:])))
1✔
87
                if _, e := base64.RawURLEncoding.Decode(result, []byte(image[4:])); e == nil {
2✔
88
                        image = string(result)
1✔
89
                }
1✔
90
        }
91

92
        var w, h int
1✔
93
        var overlay *vips.Image
1✔
94
        var n = 1
1✔
95
        if isAnimated(img) {
2✔
96
                n = -1
1✔
97
        }
1✔
98

99
        var alpha float64 = 1
1✔
100
        if ln >= 4 {
2✔
101
                alpha, _ = strconv.ParseFloat(args[3], 64)
1✔
102
                alpha = 1 - alpha/100
1✔
103
        }
1✔
104

105
        if ln >= 6 {
2✔
106
                w = img.Width()
1✔
107
                h = img.PageHeight()
1✔
108
                if args[4] != "none" {
2✔
109
                        w, _ = strconv.Atoi(args[4])
1✔
110
                        w = img.Width() * w / 100
1✔
111
                }
1✔
112
                if args[5] != "none" {
2✔
113
                        h, _ = strconv.Atoi(args[5])
1✔
114
                        h = img.PageHeight() * h / 100
1✔
115
                }
1✔
116
        } else {
1✔
117
                w = v.MaxWidth
1✔
118
                h = v.MaxHeight
1✔
119
        }
1✔
120

121
        cacheKey := watermarkCacheKey{Image: image, Width: w, Height: h, Alpha: alpha, N: n}
1✔
122
        var cacheHit bool
1✔
123

1✔
124
        if v.watermarkCache != nil {
2✔
125
                if cached, found := v.watermarkCache.Get(cacheKey.String()); found {
2✔
126
                        if cachedImg, ok := cached.(*vips.Image); ok {
2✔
127
                                overlay, err = cachedImg.Copy(nil)
1✔
128
                                if err != nil {
1✔
NEW
129
                                        return
×
NEW
130
                                }
×
131
                                cacheHit = true
1✔
132
                                if v.Debug {
2✔
133
                                        v.Logger.Debug("watermark cache hit", zap.String("image", image))
1✔
134
                                }
1✔
135
                        }
136
                }
137
        }
138

139
        if !cacheHit {
2✔
140
                var blob *imagor.Blob
1✔
141
                if blob, err = load(image); err != nil {
1✔
142
                        return
×
143
                }
×
144

145
                if ln >= 6 {
2✔
146
                        if overlay, err = v.NewThumbnail(
1✔
147
                                ctx, blob, w, h, vips.InterestingNone, vips.SizeBoth, n, 1, 0,
1✔
148
                        ); err != nil {
1✔
NEW
149
                                return
×
NEW
150
                        }
×
151
                } else {
1✔
152
                        if overlay, err = v.NewThumbnail(
1✔
153
                                ctx, blob, w, h, vips.InterestingNone, vips.SizeDown, n, 1, 0,
1✔
154
                        ); err != nil {
1✔
NEW
155
                                return
×
NEW
156
                        }
×
157
                }
158

159
                if overlay.Bands() < 3 {
2✔
160
                        if err = overlay.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
NEW
161
                                overlay.Close()
×
NEW
162
                                return
×
NEW
163
                        }
×
164
                }
165
                if !overlay.HasAlpha() {
2✔
166
                        if err = overlay.Addalpha(); err != nil {
1✔
NEW
167
                                overlay.Close()
×
NEW
168
                                return
×
NEW
169
                        }
×
170
                }
171

172
                if alpha != 1 {
2✔
173
                        if err = overlay.Linear([]float64{1, 1, 1, alpha}, []float64{0, 0, 0, 0}, nil); err != nil {
1✔
NEW
174
                                overlay.Close()
×
NEW
175
                                return
×
NEW
176
                        }
×
177
                }
178

179
                if v.watermarkCache != nil {
2✔
180
                        cachedImg, copyErr := overlay.Copy(nil)
1✔
181
                        if copyErr == nil {
2✔
182
                                cost := int64(cachedImg.Width() * cachedImg.Height() * cachedImg.Bands())
1✔
183
                                v.watermarkCache.Set(cacheKey.String(), cachedImg, cost)
1✔
184
                                if v.Debug {
2✔
185
                                        v.Logger.Debug("watermark cache store", zap.String("image", image), zap.Int64("cost", cost))
1✔
186
                                }
1✔
187
                        }
188
                }
189
        }
190
        contextDefer(ctx, overlay.Close)
1✔
191

1✔
192
        // Parse position arguments
1✔
193
        var xArg, yArg string
1✔
194
        if ln >= 3 {
2✔
195
                xArg = args[1]
1✔
196
                yArg = args[2]
1✔
197
        }
1✔
198

199
        // Use upstream compositeOverlay for positioning, embed, and composite
200
        // Pass alpha=0 since we already applied alpha above (before caching)
201
        return compositeOverlay(img, overlay, xArg, yArg, 0, vips.BlendModeOver)
1✔
202
}
203

204
func (v *Processor) fill(ctx context.Context, img *vips.Image, w, h int, pLeft, pTop, pRight, pBottom int, colour string) (err error) {
1✔
205
        if isRotate90(ctx) {
2✔
206
                tmpW := w
1✔
207
                w = h
1✔
208
                h = tmpW
1✔
209
                tmpPLeft := pLeft
1✔
210
                pLeft = pTop
1✔
211
                pTop = tmpPLeft
1✔
212
                tmpPRight := pRight
1✔
213
                pRight = pBottom
1✔
214
                pBottom = tmpPRight
1✔
215
        }
1✔
216
        c := getColor(img, colour)
1✔
217
        left := (w-img.Width())/2 + pLeft
1✔
218
        top := (h-img.PageHeight())/2 + pTop
1✔
219
        width := w + pLeft + pRight
1✔
220
        height := h + pTop + pBottom
1✔
221
        if colour != "blur" || v.DisableBlur || isAnimated(img) {
2✔
222
                // fill color
1✔
223
                isTransparent := colour == "none" || colour == "transparent"
1✔
224
                if img.HasAlpha() && !isTransparent {
2✔
225
                        c := getColor(img, colour)
1✔
226
                        if err = img.Flatten(&vips.FlattenOptions{Background: c}); err != nil {
1✔
227
                                return
×
228
                        }
×
229
                }
230
                if isTransparent {
2✔
231
                        if img.Bands() < 3 {
2✔
232
                                if err = img.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
233
                                        return
×
234
                                }
×
235
                        }
236
                        if !img.HasAlpha() {
2✔
237
                                if err = img.Addalpha(); err != nil {
1✔
238
                                        return
×
239
                                }
×
240
                        }
241
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{Extend: vips.ExtendBlack}); err != nil {
1✔
242
                                return
×
243
                        }
×
244
                } else if isBlack(c) {
2✔
245
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{Extend: vips.ExtendBlack}); err != nil {
1✔
246
                                return
×
247
                        }
×
248
                } else if isWhite(c) {
2✔
249
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{Extend: vips.ExtendWhite}); err != nil {
1✔
250
                                return
×
251
                        }
×
252
                } else {
1✔
253
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{
1✔
254
                                Extend:     vips.ExtendBackground,
1✔
255
                                Background: c,
1✔
256
                        }); err != nil {
1✔
257
                                return
×
258
                        }
×
259
                }
260
        } else {
1✔
261
                // fill blur
1✔
262
                var cp *vips.Image
1✔
263
                if cp, err = img.Copy(nil); err != nil {
1✔
264
                        return
×
265
                }
×
266
                contextDefer(ctx, cp.Close)
1✔
267
                if err = img.ThumbnailImage(
1✔
268
                        width, &vips.ThumbnailImageOptions{
1✔
269
                                Height: height,
1✔
270
                                Crop:   vips.InterestingNone,
1✔
271
                                Size:   vips.SizeForce,
1✔
272
                        },
1✔
273
                ); err != nil {
1✔
274
                        return
×
275
                }
×
276
                if err = img.Gaussblur(50, nil); err != nil {
1✔
277
                        return
×
278
                }
×
279
                if err = img.Composite2(
1✔
280
                        cp, vips.BlendModeOver,
1✔
281
                        &vips.Composite2Options{X: left, Y: top}); err != nil {
1✔
282
                        return
×
283
                }
×
284
        }
285
        return
1✔
286
}
287

288
func roundCorner(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
289
        var rx, ry int
1✔
290
        var c []float64
1✔
291
        if len(args) == 0 {
2✔
292
                return
1✔
293
        }
1✔
294
        if a, e := url.QueryUnescape(args[0]); e == nil {
2✔
295
                args[0] = a
1✔
296
        }
1✔
297
        if len(args) == 3 {
2✔
298
                // rx,ry,color
1✔
299
                c = getColor(img, args[2])
1✔
300
                args = args[:2]
1✔
301
        }
1✔
302
        rx, _ = strconv.Atoi(args[0])
1✔
303
        ry = rx
1✔
304
        if len(args) > 1 {
2✔
305
                ry, _ = strconv.Atoi(args[1])
1✔
306
        }
1✔
307

308
        var rounded *vips.Image
1✔
309
        var w = img.Width()
1✔
310
        var h = img.PageHeight()
1✔
311
        if rounded, err = vips.NewSvgloadBuffer([]byte(fmt.Sprintf(`
1✔
312
                <svg viewBox="0 0 %d %d">
1✔
313
                        <rect rx="%d" ry="%d" 
1✔
314
                         x="0" y="0" width="%d" height="%d" 
1✔
315
                         fill="#fff"/>
1✔
316
                </svg>
1✔
317
        `, w, h, rx, ry, w, h)), nil); err != nil {
1✔
318
                return
×
319
        }
×
320
        contextDefer(ctx, rounded.Close)
1✔
321
        if n := img.Height() / img.PageHeight(); n > 1 {
2✔
322
                if err = rounded.Replicate(1, n); err != nil {
1✔
323
                        return
×
324
                }
×
325
        }
326
        if err = img.Composite2(rounded, vips.BlendModeDestIn, nil); err != nil {
1✔
327
                return
×
328
        }
×
329
        if c != nil {
2✔
330
                if err = img.Flatten(&vips.FlattenOptions{Background: c}); err != nil {
1✔
331
                        return
×
332
                }
×
333
        }
334
        return nil
1✔
335
}
336

337
func label(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
338
        ln := len(args)
1✔
339
        if ln == 0 {
1✔
340
                return
×
341
        }
×
342
        if a, e := url.QueryUnescape(args[0]); e == nil {
2✔
343
                args[0] = a
1✔
344
        }
1✔
345
        var text = args[0]
1✔
346
        var font = "tahoma"
1✔
347
        var x, y int
1✔
348
        var c []float64
1✔
349
        var alpha float64
1✔
350
        var align = vips.AlignLow
1✔
351
        var size = 20
1✔
352
        var width = img.Width()
1✔
353
        if ln > 3 {
2✔
354
                size, _ = strconv.Atoi(args[3])
1✔
355
        }
1✔
356
        if ln > 1 {
2✔
357
                if args[1] == "center" {
2✔
358
                        align = vips.AlignCentre
1✔
359
                        x = width / 2
1✔
360
                } else if args[1] == imagorpath.HAlignRight {
3✔
361
                        align = vips.AlignHigh
1✔
362
                        x = width
1✔
363
                } else if strings.HasPrefix(strings.TrimPrefix(args[1], "-"), "0.") {
3✔
364
                        pec, _ := strconv.ParseFloat(args[1], 64)
1✔
365
                        x = int(pec * float64(width))
1✔
366
                } else if strings.HasSuffix(args[1], "p") {
3✔
367
                        x, _ = strconv.Atoi(strings.TrimSuffix(args[1], "p"))
1✔
368
                        x = x * width / 100
1✔
369
                } else {
2✔
370
                        x, _ = strconv.Atoi(args[1])
1✔
371
                }
1✔
372
                if x < 0 {
2✔
373
                        align = vips.AlignHigh
1✔
374
                        x += width
1✔
375
                }
1✔
376
        }
377
        if ln > 2 {
2✔
378
                if args[2] == "center" {
2✔
379
                        y = (img.PageHeight() - size) / 2
1✔
380
                } else if args[2] == imagorpath.VAlignTop {
3✔
381
                        y = 0
1✔
382
                } else if args[2] == imagorpath.VAlignBottom {
3✔
383
                        y = img.PageHeight() - size
1✔
384
                } else if strings.HasPrefix(strings.TrimPrefix(args[2], "-"), "0.") {
3✔
385
                        pec, _ := strconv.ParseFloat(args[2], 64)
1✔
386
                        y = int(pec * float64(img.PageHeight()))
1✔
387
                } else if strings.HasSuffix(args[2], "p") {
3✔
388
                        y, _ = strconv.Atoi(strings.TrimSuffix(args[2], "p"))
1✔
389
                        y = y * img.PageHeight() / 100
1✔
390
                } else {
2✔
391
                        y, _ = strconv.Atoi(args[2])
1✔
392
                }
1✔
393
                if y < 0 {
2✔
394
                        y += img.PageHeight() - size
1✔
395
                }
1✔
396
        }
397
        if ln > 4 {
2✔
398
                c = getColor(img, args[4])
1✔
399
        }
1✔
400
        if ln > 5 {
2✔
401
                alpha, _ = strconv.ParseFloat(args[5], 64)
1✔
402
                alpha /= 100
1✔
403
        }
1✔
404
        if ln > 6 {
2✔
405
                if a, e := url.QueryUnescape(args[6]); e == nil {
2✔
406
                        font = a
1✔
407
                } else {
1✔
408
                        font = args[6]
×
409
                }
×
410
        }
411
        if img.Bands() < 3 {
2✔
412
                if err = img.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
413
                        return
×
414
                }
×
415
        }
416
        if !img.HasAlpha() {
2✔
417
                if err = img.Addalpha(); err != nil {
1✔
418
                        return
×
419
                }
×
420
        }
421
        return img.Label(text, x, y, &vips.LabelOptions{
1✔
422
                Font:    font,
1✔
423
                Size:    size,
1✔
424
                Align:   align,
1✔
425
                Opacity: 1 - alpha,
1✔
426
                Color:   c,
1✔
427
        })
1✔
428
}
429

430
func (v *Processor) padding(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
431
        ln := len(args)
1✔
432
        if ln < 2 {
2✔
433
                return nil
1✔
434
        }
1✔
435
        var (
1✔
436
                c       = args[0]
1✔
437
                left, _ = strconv.Atoi(args[1])
1✔
438
                top     = left
1✔
439
                right   = left
1✔
440
                bottom  = left
1✔
441
        )
1✔
442
        if ln > 2 {
2✔
443
                top, _ = strconv.Atoi(args[2])
1✔
444
                bottom = top
1✔
445
        }
1✔
446
        if ln > 4 {
2✔
447
                right, _ = strconv.Atoi(args[3])
1✔
448
                bottom, _ = strconv.Atoi(args[4])
1✔
449
        }
1✔
450
        return v.fill(ctx, img, img.Width(), img.PageHeight(), left, top, right, bottom, c)
1✔
451
}
452

453
func backgroundColor(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
454
        if len(args) == 0 {
2✔
455
                return
1✔
456
        }
1✔
457
        if !img.HasAlpha() {
2✔
458
                return
1✔
459
        }
1✔
460
        c := getColor(img, args[0])
1✔
461
        return img.Flatten(&vips.FlattenOptions{
1✔
462
                Background: c,
1✔
463
        })
1✔
464
}
465

466
func rotate(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
467
        if len(args) == 0 {
2✔
468
                return
1✔
469
        }
1✔
470
        if angle, _ := strconv.Atoi(args[0]); angle > 0 {
2✔
471
                switch angle {
1✔
472
                case 90, 270:
1✔
473
                        setRotate90(ctx)
1✔
474
                }
475
                if err = img.RotMultiPage(getAngle(angle)); err != nil {
1✔
476
                        return err
×
477
                }
×
478
        }
479
        return
1✔
480
}
481

482
func proportion(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
483
        if len(args) == 0 {
2✔
484
                return
1✔
485
        }
1✔
486
        scale, _ := strconv.ParseFloat(args[0], 64)
1✔
487
        if scale <= 0 {
2✔
488
                return // no ops
1✔
489
        }
1✔
490
        if scale > 100 {
2✔
491
                scale = 100
1✔
492
        }
1✔
493
        if scale > 1 {
2✔
494
                scale /= 100
1✔
495
        }
1✔
496
        width := int(float64(img.Width()) * scale)
1✔
497
        height := int(float64(img.PageHeight()) * scale)
1✔
498
        if width <= 0 || height <= 0 {
2✔
499
                return // op ops
1✔
500
        }
1✔
501
        return img.ThumbnailImage(width, &vips.ThumbnailImageOptions{
1✔
502
                Height: height,
1✔
503
                Crop:   vips.InterestingNone,
1✔
504
        })
1✔
505
}
506

507
func grayscale(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
508
        return img.Colourspace(vips.InterpretationBW, nil)
1✔
509
}
1✔
510

511
func brightness(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
512
        if len(args) == 0 {
2✔
513
                return
1✔
514
        }
1✔
515
        b, _ := strconv.ParseFloat(args[0], 64)
1✔
516
        b = b * 255 / 100
1✔
517
        return linearRGB(img, []float64{1, 1, 1}, []float64{b, b, b})
1✔
518
}
519

520
func contrast(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
521
        if len(args) == 0 {
2✔
522
                return
1✔
523
        }
1✔
524
        a, _ := strconv.ParseFloat(args[0], 64)
1✔
525
        a = a * 255 / 100
1✔
526
        a = math.Min(math.Max(a, -255), 255)
1✔
527
        a = (259 * (a + 255)) / (255 * (259 - a))
1✔
528
        b := 128 - a*128
1✔
529
        return linearRGB(img, []float64{a, a, a}, []float64{b, b, b})
1✔
530
}
531

532
func hue(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
533
        if len(args) == 0 {
2✔
534
                return
1✔
535
        }
1✔
536
        h, _ := strconv.ParseFloat(args[0], 64)
1✔
537
        return img.Modulate(1, 1, h)
1✔
538
}
539

540
func saturation(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
541
        if len(args) == 0 {
2✔
542
                return
1✔
543
        }
1✔
544
        s, _ := strconv.ParseFloat(args[0], 64)
1✔
545
        s = 1 + s/100
1✔
546
        return img.Modulate(1, s, 0)
1✔
547
}
548

549
func rgb(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
550
        if len(args) != 3 {
2✔
551
                return
1✔
552
        }
1✔
553
        r, _ := strconv.ParseFloat(args[0], 64)
1✔
554
        g, _ := strconv.ParseFloat(args[1], 64)
1✔
555
        b, _ := strconv.ParseFloat(args[2], 64)
1✔
556
        r = r * 255 / 100
1✔
557
        g = g * 255 / 100
1✔
558
        b = b * 255 / 100
1✔
559
        return linearRGB(img, []float64{1, 1, 1}, []float64{r, g, b})
1✔
560
}
561

562
func modulate(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
563
        if len(args) != 3 {
2✔
564
                return
1✔
565
        }
1✔
566
        b, _ := strconv.ParseFloat(args[0], 64)
1✔
567
        s, _ := strconv.ParseFloat(args[1], 64)
1✔
568
        h, _ := strconv.ParseFloat(args[2], 64)
1✔
569
        b = 1 + b/100
1✔
570
        s = 1 + s/100
1✔
571
        return img.Modulate(b, s, h)
1✔
572
}
573

574
func blur(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
575
        if isAnimated(img) {
2✔
576
                // skip animation support
1✔
577
                return
1✔
578
        }
1✔
579
        var sigma float64
1✔
580
        switch len(args) {
1✔
581
        case 2:
1✔
582
                sigma, _ = strconv.ParseFloat(args[1], 64)
1✔
583
                break
1✔
584
        case 1:
1✔
585
                sigma, _ = strconv.ParseFloat(args[0], 64)
1✔
586
                break
1✔
587
        }
588
        sigma /= 2
1✔
589
        if sigma > 0 {
2✔
590
                return img.Gaussblur(sigma, nil)
1✔
591
        }
1✔
592
        return
×
593
}
594

595
func sharpen(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
596
        if isAnimated(img) {
2✔
597
                // skip animation support
1✔
598
                return
1✔
599
        }
1✔
600
        var sigma float64
1✔
601
        switch len(args) {
1✔
602
        case 1:
1✔
603
                sigma, _ = strconv.ParseFloat(args[0], 64)
1✔
604
                break
1✔
605
        case 2, 3:
1✔
606
                sigma, _ = strconv.ParseFloat(args[1], 64)
1✔
607
                break
1✔
608
        }
609
        sigma = 1 + sigma*2
1✔
610
        if sigma > 0 {
2✔
611
                return img.Sharpen(&vips.SharpenOptions{
1✔
612
                        Sigma: sigma,
1✔
613
                        X1:    1,
1✔
614
                        M2:    2,
1✔
615
                })
1✔
616
        }
1✔
617
        return
1✔
618
}
619

620
func stripIcc(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
621
        if img.HasICCProfile() {
2✔
622
                opts := vips.DefaultIccTransformOptions()
1✔
623
                opts.Embedded = true
1✔
624
                opts.Intent = vips.IntentPerceptual
1✔
625
                if img.Interpretation() == vips.InterpretationRgb16 {
1✔
626
                        opts.Depth = 16
×
627
                }
×
628
                _ = img.IccTransform("srgb", opts)
1✔
629
        }
630
        return img.RemoveICCProfile()
1✔
631
}
632

633
func toColorspace(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
634
        profile := "srgb"
1✔
635
        if len(args) > 0 && args[0] != "" {
2✔
636
                profile = strings.ToLower(args[0])
1✔
637
        }
1✔
638
        if !img.HasICCProfile() {
2✔
639
                return nil
1✔
640
        }
1✔
641
        opts := vips.DefaultIccTransformOptions()
1✔
642
        opts.Embedded = true
1✔
643
        opts.Intent = vips.IntentPerceptual
1✔
644
        if img.Interpretation() == vips.InterpretationRgb16 {
1✔
645
                opts.Depth = 16
×
646
        }
×
647
        return img.IccTransform(profile, opts)
1✔
648
}
649

650
func stripExif(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
651
        return img.RemoveExif()
1✔
652
}
1✔
653

654
func trim(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
655
        var (
1✔
656
                ln        = len(args)
1✔
657
                pos       string
1✔
658
                tolerance int
1✔
659
        )
1✔
660
        if ln > 0 {
2✔
661
                tolerance, _ = strconv.Atoi(args[0])
1✔
662
        }
1✔
663
        if ln > 1 {
2✔
664
                pos = args[1]
1✔
665
        }
1✔
666
        if l, t, w, h, err := findTrim(ctx, img, pos, tolerance); err == nil {
2✔
667
                return img.ExtractAreaMultiPage(l, t, w, h)
1✔
668
        }
1✔
669
        return nil
×
670
}
671

672
func crop(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
673
        if len(args) < 4 {
1✔
674
                return nil
×
675
        }
×
676

677
        // Parse arguments
678
        left, _ := strconv.ParseFloat(args[0], 64)
1✔
679
        top, _ := strconv.ParseFloat(args[1], 64)
1✔
680
        width, _ := strconv.ParseFloat(args[2], 64)
1✔
681
        height, _ := strconv.ParseFloat(args[3], 64)
1✔
682

1✔
683
        imgWidth := float64(img.Width())
1✔
684
        imgHeight := float64(img.PageHeight())
1✔
685

1✔
686
        // Convert relative (0-1) to absolute pixels
1✔
687
        if left > 0 && left < 1 {
2✔
688
                left = left * imgWidth
1✔
689
        }
1✔
690
        if top > 0 && top < 1 {
2✔
691
                top = top * imgHeight
1✔
692
        }
1✔
693
        if width > 0 && width < 1 {
2✔
694
                width = width * imgWidth
1✔
695
        }
1✔
696
        if height > 0 && height < 1 {
2✔
697
                height = height * imgHeight
1✔
698
        }
1✔
699

700
        // Clamp left and top to image bounds
701
        left = math.Max(0, math.Min(left, imgWidth))
1✔
702
        top = math.Max(0, math.Min(top, imgHeight))
1✔
703

1✔
704
        // Adjust width and height to not exceed image bounds
1✔
705
        width = math.Min(width, imgWidth-left)
1✔
706
        height = math.Min(height, imgHeight-top)
1✔
707

1✔
708
        // Skip if invalid crop area
1✔
709
        if width <= 0 || height <= 0 {
1✔
710
                return nil
×
711
        }
×
712

713
        return img.ExtractAreaMultiPage(int(left), int(top), int(width), int(height))
1✔
714
}
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