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

cshum / imagor / 21472786449

29 Jan 2026 09:28AM UTC coverage: 91.734% (-0.1%) from 91.871%
21472786449

Pull #713

github

mevinbabuc
docs: add watermark cache documentation
Pull Request #713: feat: add watermark caching for improved performance

79 of 100 new or added lines in 5 files covered. (79.0%)

1 existing line in 1 file now uncovered.

5638 of 6146 relevant lines covered (91.73%)

1.1 hits per line

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

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

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

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

15
        "github.com/cshum/imagor"
16
        "github.com/cshum/imagor/imagorpath"
17
        "go.uber.org/zap"
18
        "golang.org/x/image/colornames"
19
)
20

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

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

33
func (v *Processor) watermark(ctx context.Context, img *vips.Image, load imagor.LoadFunc, args ...string) (err error) {
1✔
34
        ln := len(args)
1✔
35
        if ln < 1 {
2✔
36
                return
1✔
37
        }
1✔
38
        image := args[0]
1✔
39

1✔
40
        if unescape, e := url.QueryUnescape(args[0]); e == nil {
2✔
41
                image = unescape
1✔
42
        }
1✔
43

44
        if strings.HasPrefix(image, "b64:") {
2✔
45
                result := make([]byte, base64.RawURLEncoding.DecodedLen(len(image[4:])))
1✔
46
                if _, e := base64.RawURLEncoding.Decode(result, []byte(image[4:])); e == nil {
2✔
47
                        image = string(result)
1✔
48
                }
1✔
49
        }
50

51
        var x, y, w, h int
1✔
52
        var across = 1
1✔
53
        var down = 1
1✔
54
        var overlay *vips.Image
1✔
55
        var n = 1
1✔
56
        if isAnimated(img) {
2✔
57
                n = -1
1✔
58
        }
1✔
59

60
        var alpha float64 = 1
1✔
61
        if ln >= 4 {
2✔
62
                alpha, _ = strconv.ParseFloat(args[3], 64)
1✔
63
                alpha = 1 - alpha/100
1✔
64
        }
1✔
65

66
        if ln >= 6 {
2✔
67
                w = img.Width()
1✔
68
                h = img.PageHeight()
1✔
69
                if args[4] != "none" {
2✔
70
                        w, _ = strconv.Atoi(args[4])
1✔
71
                        w = img.Width() * w / 100
1✔
72
                }
1✔
73
                if args[5] != "none" {
2✔
74
                        h, _ = strconv.Atoi(args[5])
1✔
75
                        h = img.PageHeight() * h / 100
1✔
76
                }
1✔
77
        } else {
1✔
78
                w = v.MaxWidth
1✔
79
                h = v.MaxHeight
1✔
80
        }
1✔
81

82
        cacheKey := watermarkCacheKey{Image: image, Width: w, Height: h, Alpha: alpha, N: n}
1✔
83
        var overlayBytes []byte
1✔
84
        var cacheHit bool
1✔
85

1✔
86
        if v.watermarkCache != nil {
2✔
87
                if cached, found := v.watermarkCache.Get(cacheKey.String()); found {
2✔
88
                        overlayBytes = cached.([]byte)
1✔
89
                        cacheHit = true
1✔
90
                        if v.Debug {
2✔
91
                                v.Logger.Debug("watermark cache hit", zap.String("image", image))
1✔
92
                        }
1✔
93
                }
94
        }
95

96
        if !cacheHit {
2✔
97
                var blob *imagor.Blob
1✔
98
                if blob, err = load(image); err != nil {
1✔
99
                        return
×
100
                }
×
101

102
                if ln >= 6 {
2✔
103
                        if overlay, err = v.NewThumbnail(
1✔
104
                                ctx, blob, w, h, vips.InterestingNone, vips.SizeBoth, n, 1, 0,
1✔
105
                        ); err != nil {
1✔
NEW
106
                                return
×
NEW
107
                        }
×
108
                } else {
1✔
109
                        if overlay, err = v.NewThumbnail(
1✔
110
                                ctx, blob, w, h, vips.InterestingNone, vips.SizeDown, n, 1, 0,
1✔
111
                        ); err != nil {
1✔
NEW
112
                                return
×
NEW
113
                        }
×
114
                }
115

116
                if overlay.Bands() < 3 {
2✔
117
                        if err = overlay.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
NEW
118
                                overlay.Close()
×
NEW
119
                                return
×
NEW
120
                        }
×
121
                }
122
                if !overlay.HasAlpha() {
2✔
123
                        if err = overlay.Addalpha(); err != nil {
1✔
NEW
124
                                overlay.Close()
×
NEW
125
                                return
×
NEW
126
                        }
×
127
                }
128

129
                if alpha != 1 {
2✔
130
                        if err = overlay.Linear([]float64{1, 1, 1, alpha}, []float64{0, 0, 0, 0}, nil); err != nil {
1✔
NEW
131
                                overlay.Close()
×
NEW
132
                                return
×
NEW
133
                        }
×
134
                }
135

136
                if v.watermarkCache != nil {
2✔
137
                        overlayBytes, err = overlay.WebpsaveBuffer(&vips.WebpsaveBufferOptions{Lossless: true})
1✔
138
                        if err != nil {
1✔
NEW
139
                                overlay.Close()
×
NEW
140
                                return
×
NEW
141
                        }
×
142
                        v.watermarkCache.Set(cacheKey.String(), overlayBytes, int64(len(overlayBytes)))
1✔
143
                        if v.Debug {
2✔
144
                                v.Logger.Debug("watermark cache store", zap.String("image", image), zap.Int("bytes", len(overlayBytes)))
1✔
145
                        }
1✔
146
                        overlay.Close()
1✔
147

1✔
148
                        overlay, err = vips.NewImageFromBuffer(overlayBytes, nil)
1✔
149
                        if err != nil {
1✔
150
                                return
×
151
                        }
×
152
                }
153
        } else {
1✔
154
                overlay, err = vips.NewImageFromBuffer(overlayBytes, nil)
1✔
155
                if err != nil {
1✔
NEW
156
                        return
×
NEW
157
                }
×
158
        }
159

160
        var overlayN = overlay.Height() / overlay.PageHeight()
1✔
161
        contextDefer(ctx, overlay.Close)
1✔
162
        w = overlay.Width()
1✔
163
        h = overlay.PageHeight()
1✔
164
        // x y
1✔
165
        if ln >= 3 {
2✔
166
                if args[1] == "center" {
2✔
167
                        x = (img.Width() - overlay.Width()) / 2
1✔
168
                } else if args[1] == imagorpath.HAlignLeft {
3✔
169
                        x = 0
1✔
170
                } else if args[1] == imagorpath.HAlignRight {
3✔
171
                        x = img.Width() - overlay.Width()
1✔
172
                } else if args[1] == "repeat" {
3✔
173
                        x = 0
1✔
174
                        across = img.Width()/overlay.Width() + 1
1✔
175
                } else if strings.HasPrefix(strings.TrimPrefix(args[1], "-"), "0.") {
3✔
176
                        pec, _ := strconv.ParseFloat(args[1], 64)
1✔
177
                        x = int(pec * float64(img.Width()))
1✔
178
                } else if strings.HasSuffix(args[1], "p") {
3✔
179
                        x, _ = strconv.Atoi(strings.TrimSuffix(args[1], "p"))
1✔
180
                        x = x * img.Width() / 100
1✔
181
                } else {
2✔
182
                        x, _ = strconv.Atoi(args[1])
1✔
183
                }
1✔
184
                if args[2] == "center" {
2✔
185
                        y = (img.PageHeight() - overlay.PageHeight()) / 2
1✔
186
                } else if args[2] == imagorpath.VAlignTop {
3✔
187
                        y = 0
1✔
188
                } else if args[2] == imagorpath.VAlignBottom {
3✔
189
                        y = img.PageHeight() - overlay.PageHeight()
1✔
190
                } else if args[2] == "repeat" {
3✔
191
                        y = 0
1✔
192
                        down = img.PageHeight()/overlay.PageHeight() + 1
1✔
193
                } else if strings.HasPrefix(strings.TrimPrefix(args[2], "-"), "0.") {
3✔
194
                        pec, _ := strconv.ParseFloat(args[2], 64)
1✔
195
                        y = int(pec * float64(img.PageHeight()))
1✔
196
                } else if strings.HasSuffix(args[2], "p") {
3✔
197
                        y, _ = strconv.Atoi(strings.TrimSuffix(args[2], "p"))
1✔
198
                        y = y * img.PageHeight() / 100
1✔
199
                } else {
2✔
200
                        y, _ = strconv.Atoi(args[2])
1✔
201
                }
1✔
202
                if x < 0 {
2✔
203
                        x += img.Width() - overlay.Width()
1✔
204
                }
1✔
205
                if y < 0 {
2✔
206
                        y += img.PageHeight() - overlay.PageHeight()
1✔
207
                }
1✔
208
        }
209
        if across*down > 1 {
2✔
210
                if err = overlay.EmbedMultiPage(0, 0, across*w, down*h,
1✔
211
                        &vips.EmbedMultiPageOptions{Extend: vips.ExtendRepeat}); err != nil {
1✔
212
                        return
×
213
                }
×
214
        }
215
        if err = overlay.EmbedMultiPage(
1✔
216
                x, y, img.Width(), img.PageHeight(), nil,
1✔
217
        ); err != nil {
1✔
218
                return
×
219
        }
×
220
        if n := img.Height() / img.PageHeight(); n > overlayN {
2✔
221
                cnt := n / overlayN
1✔
222
                if n%overlayN > 0 {
2✔
223
                        cnt++
1✔
224
                }
1✔
225
                if err = overlay.Replicate(1, cnt); err != nil {
1✔
226
                        return
×
227
                }
×
228
        }
229
        if err = img.Composite2(overlay, vips.BlendModeOver, nil); err != nil {
1✔
230
                return
×
231
        }
×
232
        return
1✔
233
}
234

235
func (v *Processor) fill(ctx context.Context, img *vips.Image, w, h int, pLeft, pTop, pRight, pBottom int, colour string) (err error) {
1✔
236
        if isRotate90(ctx) {
2✔
237
                tmpW := w
1✔
238
                w = h
1✔
239
                h = tmpW
1✔
240
                tmpPLeft := pLeft
1✔
241
                pLeft = pTop
1✔
242
                pTop = tmpPLeft
1✔
243
                tmpPRight := pRight
1✔
244
                pRight = pBottom
1✔
245
                pBottom = tmpPRight
1✔
246
        }
1✔
247
        c := getColor(img, colour)
1✔
248
        left := (w-img.Width())/2 + pLeft
1✔
249
        top := (h-img.PageHeight())/2 + pTop
1✔
250
        width := w + pLeft + pRight
1✔
251
        height := h + pTop + pBottom
1✔
252
        if colour != "blur" || v.DisableBlur || isAnimated(img) {
2✔
253
                // fill color
1✔
254
                isTransparent := colour == "none" || colour == "transparent"
1✔
255
                if img.HasAlpha() && !isTransparent {
2✔
256
                        c := getColor(img, colour)
1✔
257
                        if err = img.Flatten(&vips.FlattenOptions{Background: c}); err != nil {
1✔
258
                                return
×
259
                        }
×
260
                }
261
                if isTransparent {
2✔
262
                        if img.Bands() < 3 {
2✔
263
                                if err = img.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
264
                                        return
×
265
                                }
×
266
                        }
267
                        if !img.HasAlpha() {
2✔
268
                                if err = img.Addalpha(); err != nil {
1✔
269
                                        return
×
270
                                }
×
271
                        }
272
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{Extend: vips.ExtendBlack}); err != nil {
1✔
273
                                return
×
274
                        }
×
275
                } else if isBlack(c) {
2✔
276
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{Extend: vips.ExtendBlack}); err != nil {
1✔
277
                                return
×
278
                        }
×
279
                } else if isWhite(c) {
2✔
280
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{Extend: vips.ExtendWhite}); err != nil {
1✔
281
                                return
×
282
                        }
×
283
                } else {
1✔
284
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{
1✔
285
                                Extend:     vips.ExtendBackground,
1✔
286
                                Background: c,
1✔
287
                        }); err != nil {
1✔
288
                                return
×
289
                        }
×
290
                }
291
        } else {
1✔
292
                // fill blur
1✔
293
                var cp *vips.Image
1✔
294
                if cp, err = img.Copy(nil); err != nil {
1✔
295
                        return
×
296
                }
×
297
                contextDefer(ctx, cp.Close)
1✔
298
                if err = img.ThumbnailImage(
1✔
299
                        width, &vips.ThumbnailImageOptions{
1✔
300
                                Height: height,
1✔
301
                                Crop:   vips.InterestingNone,
1✔
302
                                Size:   vips.SizeForce,
1✔
303
                        },
1✔
304
                ); err != nil {
1✔
305
                        return
×
306
                }
×
307
                if err = img.Gaussblur(50, nil); err != nil {
1✔
308
                        return
×
309
                }
×
310
                if err = img.Composite2(
1✔
311
                        cp, vips.BlendModeOver,
1✔
312
                        &vips.Composite2Options{X: left, Y: top}); err != nil {
1✔
313
                        return
×
314
                }
×
315
        }
316
        return
1✔
317
}
318

319
func roundCorner(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
320
        var rx, ry int
1✔
321
        var c []float64
1✔
322
        if len(args) == 0 {
2✔
323
                return
1✔
324
        }
1✔
325
        if a, e := url.QueryUnescape(args[0]); e == nil {
2✔
326
                args[0] = a
1✔
327
        }
1✔
328
        if len(args) == 3 {
2✔
329
                // rx,ry,color
1✔
330
                c = getColor(img, args[2])
1✔
331
                args = args[:2]
1✔
332
        }
1✔
333
        rx, _ = strconv.Atoi(args[0])
1✔
334
        ry = rx
1✔
335
        if len(args) > 1 {
2✔
336
                ry, _ = strconv.Atoi(args[1])
1✔
337
        }
1✔
338

339
        var rounded *vips.Image
1✔
340
        var w = img.Width()
1✔
341
        var h = img.PageHeight()
1✔
342
        if rounded, err = vips.NewSvgloadBuffer([]byte(fmt.Sprintf(`
1✔
343
                <svg viewBox="0 0 %d %d">
1✔
344
                        <rect rx="%d" ry="%d" 
1✔
345
                         x="0" y="0" width="%d" height="%d" 
1✔
346
                         fill="#fff"/>
1✔
347
                </svg>
1✔
348
        `, w, h, rx, ry, w, h)), nil); err != nil {
1✔
349
                return
×
350
        }
×
351
        contextDefer(ctx, rounded.Close)
1✔
352
        if n := img.Height() / img.PageHeight(); n > 1 {
2✔
353
                if err = rounded.Replicate(1, n); err != nil {
1✔
354
                        return
×
355
                }
×
356
        }
357
        if err = img.Composite2(rounded, vips.BlendModeDestIn, nil); err != nil {
1✔
358
                return
×
359
        }
×
360
        if c != nil {
2✔
361
                if err = img.Flatten(&vips.FlattenOptions{Background: c}); err != nil {
1✔
362
                        return
×
363
                }
×
364
        }
365
        return nil
1✔
366
}
367

368
func label(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
369
        ln := len(args)
1✔
370
        if ln == 0 {
1✔
371
                return
×
372
        }
×
373
        if a, e := url.QueryUnescape(args[0]); e == nil {
2✔
374
                args[0] = a
1✔
375
        }
1✔
376
        var text = args[0]
1✔
377
        var font = "tahoma"
1✔
378
        var x, y int
1✔
379
        var c []float64
1✔
380
        var alpha float64
1✔
381
        var align = vips.AlignLow
1✔
382
        var size = 20
1✔
383
        var width = img.Width()
1✔
384
        if ln > 3 {
2✔
385
                size, _ = strconv.Atoi(args[3])
1✔
386
        }
1✔
387
        if ln > 1 {
2✔
388
                if args[1] == "center" {
2✔
389
                        align = vips.AlignCentre
1✔
390
                        x = width / 2
1✔
391
                } else if args[1] == imagorpath.HAlignRight {
3✔
392
                        align = vips.AlignHigh
1✔
393
                        x = width
1✔
394
                } else if strings.HasPrefix(strings.TrimPrefix(args[1], "-"), "0.") {
3✔
395
                        pec, _ := strconv.ParseFloat(args[1], 64)
1✔
396
                        x = int(pec * float64(width))
1✔
397
                } else if strings.HasSuffix(args[1], "p") {
3✔
398
                        x, _ = strconv.Atoi(strings.TrimSuffix(args[1], "p"))
1✔
399
                        x = x * width / 100
1✔
400
                } else {
2✔
401
                        x, _ = strconv.Atoi(args[1])
1✔
402
                }
1✔
403
                if x < 0 {
2✔
404
                        align = vips.AlignHigh
1✔
405
                        x += width
1✔
406
                }
1✔
407
        }
408
        if ln > 2 {
2✔
409
                if args[2] == "center" {
2✔
410
                        y = (img.PageHeight() - size) / 2
1✔
411
                } else if args[2] == imagorpath.VAlignTop {
3✔
412
                        y = 0
1✔
413
                } else if args[2] == imagorpath.VAlignBottom {
3✔
414
                        y = img.PageHeight() - size
1✔
415
                } else if strings.HasPrefix(strings.TrimPrefix(args[2], "-"), "0.") {
3✔
416
                        pec, _ := strconv.ParseFloat(args[2], 64)
1✔
417
                        y = int(pec * float64(img.PageHeight()))
1✔
418
                } else if strings.HasSuffix(args[2], "p") {
3✔
419
                        y, _ = strconv.Atoi(strings.TrimSuffix(args[2], "p"))
1✔
420
                        y = y * img.PageHeight() / 100
1✔
421
                } else {
2✔
422
                        y, _ = strconv.Atoi(args[2])
1✔
423
                }
1✔
424
                if y < 0 {
2✔
425
                        y += img.PageHeight() - size
1✔
426
                }
1✔
427
        }
428
        if ln > 4 {
2✔
429
                c = getColor(img, args[4])
1✔
430
        }
1✔
431
        if ln > 5 {
2✔
432
                alpha, _ = strconv.ParseFloat(args[5], 64)
1✔
433
                alpha /= 100
1✔
434
        }
1✔
435
        if ln > 6 {
2✔
436
                if a, e := url.QueryUnescape(args[6]); e == nil {
2✔
437
                        font = a
1✔
438
                } else {
1✔
439
                        font = args[6]
×
440
                }
×
441
        }
442
        if img.Bands() < 3 {
2✔
443
                if err = img.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
444
                        return
×
445
                }
×
446
        }
447
        if !img.HasAlpha() {
2✔
448
                if err = img.Addalpha(); err != nil {
1✔
449
                        return
×
450
                }
×
451
        }
452
        return img.Label(text, x, y, &vips.LabelOptions{
1✔
453
                Font:    font,
1✔
454
                Size:    size,
1✔
455
                Align:   align,
1✔
456
                Opacity: 1 - alpha,
1✔
457
                Color:   c,
1✔
458
        })
1✔
459
}
460

461
func (v *Processor) padding(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
462
        ln := len(args)
1✔
463
        if ln < 2 {
2✔
464
                return nil
1✔
465
        }
1✔
466
        var (
1✔
467
                c       = args[0]
1✔
468
                left, _ = strconv.Atoi(args[1])
1✔
469
                top     = left
1✔
470
                right   = left
1✔
471
                bottom  = left
1✔
472
        )
1✔
473
        if ln > 2 {
2✔
474
                top, _ = strconv.Atoi(args[2])
1✔
475
                bottom = top
1✔
476
        }
1✔
477
        if ln > 4 {
2✔
478
                right, _ = strconv.Atoi(args[3])
1✔
479
                bottom, _ = strconv.Atoi(args[4])
1✔
480
        }
1✔
481
        return v.fill(ctx, img, img.Width(), img.PageHeight(), left, top, right, bottom, c)
1✔
482
}
483

484
func backgroundColor(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
485
        if len(args) == 0 {
2✔
486
                return
1✔
487
        }
1✔
488
        if !img.HasAlpha() {
2✔
489
                return
1✔
490
        }
1✔
491
        c := getColor(img, args[0])
1✔
492
        return img.Flatten(&vips.FlattenOptions{
1✔
493
                Background: c,
1✔
494
        })
1✔
495
}
496

497
func rotate(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
498
        if len(args) == 0 {
2✔
499
                return
1✔
500
        }
1✔
501
        if angle, _ := strconv.Atoi(args[0]); angle > 0 {
2✔
502
                switch angle {
1✔
503
                case 90, 270:
1✔
504
                        setRotate90(ctx)
1✔
505
                }
506
                if err = img.RotMultiPage(getAngle(angle)); err != nil {
1✔
507
                        return err
×
508
                }
×
509
        }
510
        return
1✔
511
}
512

513
func getAngle(angle int) vips.Angle {
1✔
514
        switch angle {
1✔
515
        case 90:
1✔
516
                return vips.AngleD270
1✔
517
        case 180:
1✔
518
                return vips.AngleD180
1✔
519
        case 270:
1✔
520
                return vips.AngleD90
1✔
521
        default:
×
522
                return vips.AngleD0
×
523
        }
524
}
525

526
func proportion(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
527
        if len(args) == 0 {
2✔
528
                return
1✔
529
        }
1✔
530
        scale, _ := strconv.ParseFloat(args[0], 64)
1✔
531
        if scale <= 0 {
2✔
532
                return // no ops
1✔
533
        }
1✔
534
        if scale > 100 {
2✔
535
                scale = 100
1✔
536
        }
1✔
537
        if scale > 1 {
2✔
538
                scale /= 100
1✔
539
        }
1✔
540
        width := int(float64(img.Width()) * scale)
1✔
541
        height := int(float64(img.PageHeight()) * scale)
1✔
542
        if width <= 0 || height <= 0 {
2✔
543
                return // op ops
1✔
544
        }
1✔
545
        return img.ThumbnailImage(width, &vips.ThumbnailImageOptions{
1✔
546
                Height: height,
1✔
547
                Crop:   vips.InterestingNone,
1✔
548
        })
1✔
549
}
550

551
func grayscale(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
552
        return img.Colourspace(vips.InterpretationBW, nil)
1✔
553
}
1✔
554

555
func brightness(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
556
        if len(args) == 0 {
2✔
557
                return
1✔
558
        }
1✔
559
        b, _ := strconv.ParseFloat(args[0], 64)
1✔
560
        b = b * 255 / 100
1✔
561
        return linearRGB(img, []float64{1, 1, 1}, []float64{b, b, b})
1✔
562
}
563

564
func contrast(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
565
        if len(args) == 0 {
2✔
566
                return
1✔
567
        }
1✔
568
        a, _ := strconv.ParseFloat(args[0], 64)
1✔
569
        a = a * 255 / 100
1✔
570
        a = math.Min(math.Max(a, -255), 255)
1✔
571
        a = (259 * (a + 255)) / (255 * (259 - a))
1✔
572
        b := 128 - a*128
1✔
573
        return linearRGB(img, []float64{a, a, a}, []float64{b, b, b})
1✔
574
}
575

576
func hue(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
577
        if len(args) == 0 {
2✔
578
                return
1✔
579
        }
1✔
580
        h, _ := strconv.ParseFloat(args[0], 64)
1✔
581
        return img.Modulate(1, 1, h)
1✔
582
}
583

584
func saturation(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
585
        if len(args) == 0 {
2✔
586
                return
1✔
587
        }
1✔
588
        s, _ := strconv.ParseFloat(args[0], 64)
1✔
589
        s = 1 + s/100
1✔
590
        return img.Modulate(1, s, 0)
1✔
591
}
592

593
func rgb(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
594
        if len(args) != 3 {
2✔
595
                return
1✔
596
        }
1✔
597
        r, _ := strconv.ParseFloat(args[0], 64)
1✔
598
        g, _ := strconv.ParseFloat(args[1], 64)
1✔
599
        b, _ := strconv.ParseFloat(args[2], 64)
1✔
600
        r = r * 255 / 100
1✔
601
        g = g * 255 / 100
1✔
602
        b = b * 255 / 100
1✔
603
        return linearRGB(img, []float64{1, 1, 1}, []float64{r, g, b})
1✔
604
}
605

606
func modulate(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
607
        if len(args) != 3 {
2✔
608
                return
1✔
609
        }
1✔
610
        b, _ := strconv.ParseFloat(args[0], 64)
1✔
611
        s, _ := strconv.ParseFloat(args[1], 64)
1✔
612
        h, _ := strconv.ParseFloat(args[2], 64)
1✔
613
        b = 1 + b/100
1✔
614
        s = 1 + s/100
1✔
615
        return img.Modulate(b, s, h)
1✔
616
}
617

618
func blur(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
619
        if isAnimated(img) {
2✔
620
                // skip animation support
1✔
621
                return
1✔
622
        }
1✔
623
        var sigma float64
1✔
624
        switch len(args) {
1✔
625
        case 2:
1✔
626
                sigma, _ = strconv.ParseFloat(args[1], 64)
1✔
627
                break
1✔
628
        case 1:
1✔
629
                sigma, _ = strconv.ParseFloat(args[0], 64)
1✔
630
                break
1✔
631
        }
632
        sigma /= 2
1✔
633
        if sigma > 0 {
2✔
634
                return img.Gaussblur(sigma, nil)
1✔
635
        }
1✔
636
        return
×
637
}
638

639
func sharpen(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
640
        if isAnimated(img) {
2✔
641
                // skip animation support
1✔
642
                return
1✔
643
        }
1✔
644
        var sigma float64
1✔
645
        switch len(args) {
1✔
646
        case 1:
1✔
647
                sigma, _ = strconv.ParseFloat(args[0], 64)
1✔
648
                break
1✔
649
        case 2, 3:
1✔
650
                sigma, _ = strconv.ParseFloat(args[1], 64)
1✔
651
                break
1✔
652
        }
653
        sigma = 1 + sigma*2
1✔
654
        if sigma > 0 {
2✔
655
                return img.Sharpen(&vips.SharpenOptions{
1✔
656
                        Sigma: sigma,
1✔
657
                        X1:    1,
1✔
658
                        M2:    2,
1✔
659
                })
1✔
660
        }
1✔
661
        return
1✔
662
}
663

664
func stripIcc(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
665
        if img.HasICCProfile() {
2✔
666
                opts := vips.DefaultIccTransformOptions()
1✔
667
                opts.Embedded = true
1✔
668
                opts.Intent = vips.IntentPerceptual
1✔
669
                if img.Interpretation() == vips.InterpretationRgb16 {
1✔
670
                        opts.Depth = 16
×
671
                }
×
672
                _ = img.IccTransform("srgb", opts)
1✔
673
        }
674
        return img.RemoveICCProfile()
1✔
675
}
676

677
func toColorspace(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
678
        profile := "srgb"
1✔
679
        if len(args) > 0 && args[0] != "" {
2✔
680
                profile = strings.ToLower(args[0])
1✔
681
        }
1✔
682
        if !img.HasICCProfile() {
2✔
683
                return nil
1✔
684
        }
1✔
685
        opts := vips.DefaultIccTransformOptions()
1✔
686
        opts.Embedded = true
1✔
687
        opts.Intent = vips.IntentPerceptual
1✔
688
        if img.Interpretation() == vips.InterpretationRgb16 {
1✔
689
                opts.Depth = 16
×
690
        }
×
691
        return img.IccTransform(profile, opts)
1✔
692
}
693

694
func stripExif(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
695
        return img.RemoveExif()
1✔
696
}
1✔
697

698
func trim(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
699
        var (
1✔
700
                ln        = len(args)
1✔
701
                pos       string
1✔
702
                tolerance int
1✔
703
        )
1✔
704
        if ln > 0 {
2✔
705
                tolerance, _ = strconv.Atoi(args[0])
1✔
706
        }
1✔
707
        if ln > 1 {
2✔
708
                pos = args[1]
1✔
709
        }
1✔
710
        if l, t, w, h, err := findTrim(ctx, img, pos, tolerance); err == nil {
2✔
711
                return img.ExtractAreaMultiPage(l, t, w, h)
1✔
712
        }
1✔
713
        return nil
×
714
}
715

716
func linearRGB(img *vips.Image, a, b []float64) error {
1✔
717
        if img.HasAlpha() {
2✔
718
                a = append(a, 1)
1✔
719
                b = append(b, 0)
1✔
720
        }
1✔
721
        return img.Linear(a, b, nil)
1✔
722
}
723

724
func isBlack(c []float64) bool {
1✔
725
        if len(c) < 3 {
1✔
726
                return false
×
727
        }
×
728
        return c[0] == 0x00 && c[1] == 0x00 && c[2] == 0x00
1✔
729
}
730

731
func isWhite(c []float64) bool {
1✔
732
        if len(c) < 3 {
1✔
733
                return false
×
734
        }
×
735
        return c[0] == 0xff && c[1] == 0xff && c[2] == 0xff
1✔
736
}
737

738
func getColor(img *vips.Image, color string) []float64 {
1✔
739
        var vc = make([]float64, 3)
1✔
740
        args := strings.Split(strings.ToLower(color), ",")
1✔
741
        mode := ""
1✔
742
        name := strings.TrimPrefix(args[0], "#")
1✔
743
        if len(args) > 1 {
2✔
744
                mode = args[1]
1✔
745
        }
1✔
746
        if name == "auto" {
2✔
747
                if img != nil {
2✔
748
                        x := 0
1✔
749
                        y := 0
1✔
750
                        if mode == "bottom-right" {
2✔
751
                                x = img.Width() - 1
1✔
752
                                y = img.PageHeight() - 1
1✔
753
                        }
1✔
754
                        p, _ := img.Getpoint(x, y, nil)
1✔
755
                        if len(p) >= 3 {
2✔
756
                                vc[0] = p[0]
1✔
757
                                vc[1] = p[1]
1✔
758
                                vc[2] = p[2]
1✔
759
                        }
1✔
760
                }
761
        } else if c, ok := colornames.Map[name]; ok {
2✔
762
                vc[0] = float64(c.R)
1✔
763
                vc[1] = float64(c.G)
1✔
764
                vc[2] = float64(c.B)
1✔
765
        } else if c, ok := parseHexColor(name); ok {
3✔
766
                vc[0] = float64(c.R)
1✔
767
                vc[1] = float64(c.G)
1✔
768
                vc[2] = float64(c.B)
1✔
769
        }
1✔
770
        return vc
1✔
771
}
772

773
func parseHexColor(s string) (c color.RGBA, ok bool) {
1✔
774
        c.A = 0xff
1✔
775
        switch len(s) {
1✔
776
        case 6:
1✔
777
                c.R = hexToByte(s[0])<<4 + hexToByte(s[1])
1✔
778
                c.G = hexToByte(s[2])<<4 + hexToByte(s[3])
1✔
779
                c.B = hexToByte(s[4])<<4 + hexToByte(s[5])
1✔
780
                ok = true
1✔
781
        case 3:
1✔
782
                c.R = hexToByte(s[0]) * 17
1✔
783
                c.G = hexToByte(s[1]) * 17
1✔
784
                c.B = hexToByte(s[2]) * 17
1✔
785
                ok = true
1✔
786
        }
787
        return
1✔
788
}
789

790
func hexToByte(b byte) byte {
1✔
791
        switch {
1✔
792
        case b >= '0' && b <= '9':
1✔
793
                return b - '0'
1✔
794
        case b >= 'a' && b <= 'f':
1✔
795
                return b - 'a' + 10
1✔
796
        }
797
        return 0
1✔
798
}
799

800
func crop(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
801
        if len(args) < 4 {
1✔
802
                return nil
×
803
        }
×
804

805
        // Parse arguments
806
        left, _ := strconv.ParseFloat(args[0], 64)
1✔
807
        top, _ := strconv.ParseFloat(args[1], 64)
1✔
808
        width, _ := strconv.ParseFloat(args[2], 64)
1✔
809
        height, _ := strconv.ParseFloat(args[3], 64)
1✔
810

1✔
811
        imgWidth := float64(img.Width())
1✔
812
        imgHeight := float64(img.PageHeight())
1✔
813

1✔
814
        // Convert relative (0-1) to absolute pixels
1✔
815
        if left > 0 && left < 1 {
2✔
816
                left = left * imgWidth
1✔
817
        }
1✔
818
        if top > 0 && top < 1 {
2✔
819
                top = top * imgHeight
1✔
820
        }
1✔
821
        if width > 0 && width < 1 {
2✔
822
                width = width * imgWidth
1✔
823
        }
1✔
824
        if height > 0 && height < 1 {
2✔
825
                height = height * imgHeight
1✔
826
        }
1✔
827

828
        // Clamp left and top to image bounds
829
        left = math.Max(0, math.Min(left, imgWidth))
1✔
830
        top = math.Max(0, math.Min(top, imgHeight))
1✔
831

1✔
832
        // Adjust width and height to not exceed image bounds
1✔
833
        width = math.Min(width, imgWidth-left)
1✔
834
        height = math.Min(height, imgHeight-top)
1✔
835

1✔
836
        // Skip if invalid crop area
1✔
837
        if width <= 0 || height <= 0 {
1✔
838
                return nil
×
839
        }
×
840

841
        return img.ExtractAreaMultiPage(int(left), int(top), int(width), int(height))
1✔
842
}
843

844
func isAnimated(img *vips.Image) bool {
1✔
845
        return img.Height() > img.PageHeight()
1✔
846
}
1✔
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