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

cshum / imagor / 21640688306

03 Feb 2026 05:30PM UTC coverage: 91.927% (+0.2%) from 91.772%
21640688306

Pull #722

github

cshum
feat: image filter
Pull Request #722: feat(vipsprocessor): add image() filter with nested path parsing

147 of 165 new or added lines in 4 files covered. (89.09%)

44 existing lines in 1 file now uncovered.

5682 of 6181 relevant lines covered (91.93%)

1.1 hits per line

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

88.81
/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
        "golang.org/x/image/colornames"
18
)
19

20
// compositeOverlay prepares and composites an overlay image onto the base image
21
// Handles color space, alpha channel, positioning, repeat patterns, and animation frames
22
func compositeOverlay(img *vips.Image, overlay *vips.Image, xArg, yArg string, alpha float64) error {
1✔
23
        // Ensure overlay has proper color space and alpha
1✔
24
        if overlay.Bands() < 3 {
2✔
25
                if err := overlay.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
NEW
26
                        return err
×
NEW
27
                }
×
28
        }
29
        if !overlay.HasAlpha() {
2✔
30
                if err := overlay.Addalpha(); err != nil {
1✔
NEW
31
                        return err
×
NEW
32
                }
×
33
        }
34

35
        w := overlay.Width()
1✔
36
        h := overlay.PageHeight()
1✔
37

1✔
38
        // Apply alpha if provided
1✔
39
        if alpha > 0 {
2✔
40
                alphaMultiplier := 1 - alpha/100
1✔
41
                if alphaMultiplier != 1 {
2✔
42
                        if err := overlay.Linear([]float64{1, 1, 1, alphaMultiplier}, []float64{0, 0, 0, 0}, nil); err != nil {
1✔
NEW
43
                                return err
×
NEW
44
                        }
×
45
                }
46
        }
47

48
        // Parse position
49
        var x, y int
1✔
50
        across := 1
1✔
51
        down := 1
1✔
52
        overlayWidth := overlay.Width()
1✔
53
        overlayHeight := overlay.PageHeight()
1✔
54

1✔
55
        if xArg != "" {
2✔
56
                if xArg == "center" {
2✔
57
                        x = (img.Width() - overlayWidth) / 2
1✔
58
                } else if xArg == imagorpath.HAlignLeft {
3✔
59
                        x = 0
1✔
60
                } else if xArg == imagorpath.HAlignRight {
3✔
61
                        x = img.Width() - overlayWidth
1✔
62
                } else if xArg == "repeat" {
3✔
63
                        x = 0
1✔
64
                        across = img.Width()/overlayWidth + 1
1✔
65
                } else if strings.HasPrefix(strings.TrimPrefix(xArg, "-"), "0.") {
3✔
66
                        pec, _ := strconv.ParseFloat(xArg, 64)
1✔
67
                        x = int(pec * float64(img.Width()))
1✔
68
                } else if strings.HasSuffix(xArg, "p") {
3✔
69
                        x, _ = strconv.Atoi(strings.TrimSuffix(xArg, "p"))
1✔
70
                        x = x * img.Width() / 100
1✔
71
                } else {
2✔
72
                        x, _ = strconv.Atoi(xArg)
1✔
73
                }
1✔
74
                if x < 0 {
2✔
75
                        x += img.Width() - overlayWidth
1✔
76
                }
1✔
77
        }
78

79
        if yArg != "" {
2✔
80
                if yArg == "center" {
2✔
81
                        y = (img.PageHeight() - overlayHeight) / 2
1✔
82
                } else if yArg == imagorpath.VAlignTop {
3✔
83
                        y = 0
1✔
84
                } else if yArg == imagorpath.VAlignBottom {
3✔
85
                        y = img.PageHeight() - overlayHeight
1✔
86
                } else if yArg == "repeat" {
3✔
87
                        y = 0
1✔
88
                        down = img.PageHeight()/overlayHeight + 1
1✔
89
                } else if strings.HasPrefix(strings.TrimPrefix(yArg, "-"), "0.") {
3✔
90
                        pec, _ := strconv.ParseFloat(yArg, 64)
1✔
91
                        y = int(pec * float64(img.PageHeight()))
1✔
92
                } else if strings.HasSuffix(yArg, "p") {
3✔
93
                        y, _ = strconv.Atoi(strings.TrimSuffix(yArg, "p"))
1✔
94
                        y = y * img.PageHeight() / 100
1✔
95
                } else {
2✔
96
                        y, _ = strconv.Atoi(yArg)
1✔
97
                }
1✔
98
                if y < 0 {
2✔
99
                        y += img.PageHeight() - overlayHeight
1✔
100
                }
1✔
101
        }
102

103
        // Handle repeat pattern
104
        if across*down > 1 {
2✔
105
                if err := overlay.EmbedMultiPage(0, 0, across*w, down*h,
1✔
106
                        &vips.EmbedMultiPageOptions{Extend: vips.ExtendRepeat}); err != nil {
1✔
NEW
107
                        return err
×
NEW
108
                }
×
109
        }
110

111
        // Position overlay on canvas
112
        if err := overlay.EmbedMultiPage(
1✔
113
                x, y, img.Width(), img.PageHeight(), nil,
1✔
114
        ); err != nil {
1✔
NEW
115
                return err
×
NEW
116
        }
×
117

118
        // Handle animation frames
119
        overlayN := overlay.Height() / overlay.PageHeight()
1✔
120
        if n := img.Height() / img.PageHeight(); n > overlayN {
2✔
121
                cnt := n / overlayN
1✔
122
                if n%overlayN > 0 {
2✔
123
                        cnt++
1✔
124
                }
1✔
125
                if err := overlay.Replicate(1, cnt); err != nil {
1✔
NEW
126
                        return err
×
NEW
127
                }
×
128
        }
129

130
        // Composite onto main image
131
        return img.Composite2(overlay, vips.BlendModeOver, nil)
1✔
132
}
133

134
func (v *Processor) image(ctx context.Context, img *vips.Image, load imagor.LoadFunc, args ...string) (err error) {
1✔
135
        ln := len(args)
1✔
136
        if ln < 1 {
1✔
NEW
137
                return
×
NEW
138
        }
×
139
        imagorPath := args[0]
1✔
140
        if unescape, e := url.QueryUnescape(args[0]); e == nil {
2✔
141
                imagorPath = unescape
1✔
142
        }
1✔
143
        params := imagorpath.Parse(imagorPath)
1✔
144
        var blob *imagor.Blob
1✔
145
        if blob, err = load(params.Image); err != nil {
1✔
NEW
146
                return
×
NEW
147
        }
×
148
        var overlay *vips.Image
1✔
149
        if overlay, err = v.loadAndProcess(ctx, blob, params, load); err != nil || overlay == nil {
1✔
NEW
150
                return
×
NEW
151
        }
×
152
        contextDefer(ctx, overlay.Close)
1✔
153

1✔
154
        var xArg, yArg string
1✔
155
        var alpha float64
1✔
156
        if ln >= 2 {
2✔
157
                xArg = args[1]
1✔
158
        }
1✔
159
        if ln >= 3 {
2✔
160
                yArg = args[2]
1✔
161
        }
1✔
162
        if ln >= 4 {
2✔
163
                alpha, _ = strconv.ParseFloat(args[3], 64)
1✔
164
        }
1✔
165

166
        return compositeOverlay(img, overlay, xArg, yArg, alpha)
1✔
167
}
168

169
func (v *Processor) watermark(ctx context.Context, img *vips.Image, load imagor.LoadFunc, args ...string) (err error) {
1✔
170
        ln := len(args)
1✔
171
        if ln < 1 {
2✔
172
                return
1✔
173
        }
1✔
174
        image := args[0]
1✔
175

1✔
176
        if unescape, e := url.QueryUnescape(args[0]); e == nil {
2✔
177
                image = unescape
1✔
178
        }
1✔
179

180
        if strings.HasPrefix(image, "b64:") {
2✔
181
                // if image URL starts with b64: prefix, Base64 decode it according to "base64url" in RFC 4648 (Section 5).
1✔
182
                result := make([]byte, base64.RawURLEncoding.DecodedLen(len(image[4:])))
1✔
183
                // in case decoding fails, use original image URL (possible that filename starts with b64: prefix, but as part of the file name)
1✔
184
                if _, e := base64.RawURLEncoding.Decode(result, []byte(image[4:])); e == nil {
2✔
185
                        image = string(result)
1✔
186
                }
1✔
187
        }
188

189
        var blob *imagor.Blob
1✔
190
        if blob, err = load(image); err != nil {
1✔
191
                return
×
192
        }
×
193
        var w, h int
1✔
194
        var overlay *vips.Image
1✔
195
        var n = 1
1✔
196
        if isAnimated(img) {
2✔
197
                n = -1
1✔
198
        }
1✔
199
        // w_ratio h_ratio
200
        if ln >= 6 {
2✔
201
                w = img.Width()
1✔
202
                h = img.PageHeight()
1✔
203
                if args[4] != "none" {
2✔
204
                        w, _ = strconv.Atoi(args[4])
1✔
205
                        w = img.Width() * w / 100
1✔
206
                }
1✔
207
                if args[5] != "none" {
2✔
208
                        h, _ = strconv.Atoi(args[5])
1✔
209
                        h = img.PageHeight() * h / 100
1✔
210
                }
1✔
211
                if overlay, err = v.NewThumbnail(
1✔
212
                        ctx, blob, w, h, vips.InterestingNone, vips.SizeBoth, n, 1, 0,
1✔
213
                ); err != nil {
1✔
UNCOV
214
                        return
×
UNCOV
215
                }
×
216
        } else {
1✔
217
                if overlay, err = v.NewThumbnail(
1✔
218
                        ctx, blob, v.MaxWidth, v.MaxHeight, vips.InterestingNone, vips.SizeDown, n, 1, 0,
1✔
219
                ); err != nil {
1✔
UNCOV
220
                        return
×
UNCOV
221
                }
×
222
        }
223
        contextDefer(ctx, overlay.Close)
1✔
224

1✔
225
        // Parse arguments
1✔
226
        var xArg, yArg string
1✔
227
        var alpha float64
1✔
228
        if ln >= 3 {
2✔
229
                xArg = args[1]
1✔
230
                yArg = args[2]
1✔
231
        }
1✔
232
        if ln >= 4 {
2✔
233
                alpha, _ = strconv.ParseFloat(args[3], 64)
1✔
234
        }
1✔
235

236
        return compositeOverlay(img, overlay, xArg, yArg, alpha)
1✔
237
}
238

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

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

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

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

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

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

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

517
func getAngle(angle int) vips.Angle {
1✔
518
        switch angle {
1✔
519
        case 90:
1✔
520
                return vips.AngleD270
1✔
521
        case 180:
1✔
522
                return vips.AngleD180
1✔
523
        case 270:
1✔
524
                return vips.AngleD90
1✔
UNCOV
525
        default:
×
UNCOV
526
                return vips.AngleD0
×
527
        }
528
}
529

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

555
func grayscale(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
556
        return img.Colourspace(vips.InterpretationBW, nil)
1✔
557
}
1✔
558

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

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

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

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

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

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

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

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

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

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

698
func stripExif(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
699
        return img.RemoveExif()
1✔
700
}
1✔
701

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

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

728
func isBlack(c []float64) bool {
1✔
729
        if len(c) < 3 {
1✔
UNCOV
730
                return false
×
UNCOV
731
        }
×
732
        return c[0] == 0x00 && c[1] == 0x00 && c[2] == 0x00
1✔
733
}
734

735
func isWhite(c []float64) bool {
1✔
736
        if len(c) < 3 {
1✔
UNCOV
737
                return false
×
UNCOV
738
        }
×
739
        return c[0] == 0xff && c[1] == 0xff && c[2] == 0xff
1✔
740
}
741

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

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

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

804
func crop(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
805
        if len(args) < 4 {
1✔
UNCOV
806
                return nil
×
UNCOV
807
        }
×
808

809
        // Parse arguments
810
        left, _ := strconv.ParseFloat(args[0], 64)
1✔
811
        top, _ := strconv.ParseFloat(args[1], 64)
1✔
812
        width, _ := strconv.ParseFloat(args[2], 64)
1✔
813
        height, _ := strconv.ParseFloat(args[3], 64)
1✔
814

1✔
815
        imgWidth := float64(img.Width())
1✔
816
        imgHeight := float64(img.PageHeight())
1✔
817

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

832
        // Clamp left and top to image bounds
833
        left = math.Max(0, math.Min(left, imgWidth))
1✔
834
        top = math.Max(0, math.Min(top, imgHeight))
1✔
835

1✔
836
        // Adjust width and height to not exceed image bounds
1✔
837
        width = math.Min(width, imgWidth-left)
1✔
838
        height = math.Min(height, imgHeight-top)
1✔
839

1✔
840
        // Skip if invalid crop area
1✔
841
        if width <= 0 || height <= 0 {
1✔
UNCOV
842
                return nil
×
UNCOV
843
        }
×
844

845
        return img.ExtractAreaMultiPage(int(left), int(top), int(width), int(height))
1✔
846
}
847

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