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

cshum / imagor / 21662828254

04 Feb 2026 07:39AM UTC coverage: 91.84% (-0.05%) from 91.887%
21662828254

push

github

web-flow
feat(vipsprocessor): add image() filter with nested path parsing (#722)

* feat: image filter

* feat: image filter

* feat: image filter

* feat: image filter

* feat: image filter

* test: update golden files

* feat: image filter

* feat: image filter

* test: reset golden

* test: update golden files

* feat: image filter

---------

Co-authored-by: cshum <293790+cshum@users.noreply.github.com>

161 of 183 new or added lines in 4 files covered. (87.98%)

1 existing line in 1 file now uncovered.

5695 of 6201 relevant lines covered (91.84%)

1.1 hits per line

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

88.3
/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
// blendModeMap maps blend mode names to vips.BlendMode constants
21
var blendModeMap = map[string]vips.BlendMode{
22
        "normal":      vips.BlendModeOver,
23
        "multiply":    vips.BlendModeMultiply,
24
        "color-burn":  vips.BlendModeColourBurn,
25
        "darken":      vips.BlendModeDarken,
26
        "screen":      vips.BlendModeScreen,
27
        "color-dodge": vips.BlendModeColourDodge,
28
        "lighten":     vips.BlendModeLighten,
29
        "add":         vips.BlendModeAdd,
30
        "overlay":     vips.BlendModeOverlay,
31
        "soft-light":  vips.BlendModeSoftLight,
32
        "hard-light":  vips.BlendModeHardLight,
33
        "difference":  vips.BlendModeDifference,
34
        "exclusion":   vips.BlendModeExclusion,
35
        "mask":        vips.BlendModeDestIn,
36
        "mask-out":    vips.BlendModeDestOut,
37
}
38

39
// prepareOverlay prepares an overlay image for compositing
40
// Handles color space, alpha channel, positioning, repeat patterns, and animation frames
41
// Returns the prepared overlay ready for compositing with Composite2()
42
func prepareOverlay(img *vips.Image, overlay *vips.Image, xArg, yArg string, alpha float64) error {
1✔
43
        // Ensure overlay has proper color space and alpha
1✔
44
        if overlay.Bands() < 3 {
2✔
45
                if err := overlay.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
NEW
46
                        return err
×
NEW
47
                }
×
48
        }
49
        if !overlay.HasAlpha() {
2✔
50
                if err := overlay.Addalpha(); err != nil {
1✔
NEW
51
                        return err
×
NEW
52
                }
×
53
        }
54

55
        w := overlay.Width()
1✔
56
        h := overlay.PageHeight()
1✔
57

1✔
58
        // Apply alpha if provided
1✔
59
        if alpha > 0 {
2✔
60
                alphaMultiplier := 1 - alpha/100
1✔
61
                if alphaMultiplier != 1 {
2✔
62
                        if err := overlay.Linear([]float64{1, 1, 1, alphaMultiplier}, []float64{0, 0, 0, 0}, nil); err != nil {
1✔
NEW
63
                                return err
×
NEW
64
                        }
×
65
                }
66
        }
67

68
        // Parse position
69
        var x, y int
1✔
70
        across := 1
1✔
71
        down := 1
1✔
72
        overlayWidth := overlay.Width()
1✔
73
        overlayHeight := overlay.PageHeight()
1✔
74

1✔
75
        if xArg != "" {
2✔
76
                if xArg == "center" {
2✔
77
                        x = (img.Width() - overlayWidth) / 2
1✔
78
                } else if xArg == imagorpath.HAlignLeft {
3✔
79
                        x = 0
1✔
80
                } else if xArg == imagorpath.HAlignRight {
3✔
81
                        x = img.Width() - overlayWidth
1✔
82
                } else if xArg == "repeat" {
3✔
83
                        x = 0
1✔
84
                        across = img.Width()/overlayWidth + 1
1✔
85
                } else if strings.HasPrefix(strings.TrimPrefix(xArg, "-"), "0.") {
3✔
86
                        pec, _ := strconv.ParseFloat(xArg, 64)
1✔
87
                        x = int(pec * float64(img.Width()))
1✔
88
                } else if strings.HasSuffix(xArg, "p") {
3✔
89
                        x, _ = strconv.Atoi(strings.TrimSuffix(xArg, "p"))
1✔
90
                        x = x * img.Width() / 100
1✔
91
                } else {
2✔
92
                        x, _ = strconv.Atoi(xArg)
1✔
93
                }
1✔
94
                if x < 0 {
2✔
95
                        x += img.Width() - overlayWidth
1✔
96
                }
1✔
97
        }
98

99
        if yArg != "" {
2✔
100
                if yArg == "center" {
2✔
101
                        y = (img.PageHeight() - overlayHeight) / 2
1✔
102
                } else if yArg == imagorpath.VAlignTop {
3✔
103
                        y = 0
1✔
104
                } else if yArg == imagorpath.VAlignBottom {
3✔
105
                        y = img.PageHeight() - overlayHeight
1✔
106
                } else if yArg == "repeat" {
3✔
107
                        y = 0
1✔
108
                        down = img.PageHeight()/overlayHeight + 1
1✔
109
                } else if strings.HasPrefix(strings.TrimPrefix(yArg, "-"), "0.") {
3✔
110
                        pec, _ := strconv.ParseFloat(yArg, 64)
1✔
111
                        y = int(pec * float64(img.PageHeight()))
1✔
112
                } else if strings.HasSuffix(yArg, "p") {
3✔
113
                        y, _ = strconv.Atoi(strings.TrimSuffix(yArg, "p"))
1✔
114
                        y = y * img.PageHeight() / 100
1✔
115
                } else {
2✔
116
                        y, _ = strconv.Atoi(yArg)
1✔
117
                }
1✔
118
                if y < 0 {
2✔
119
                        y += img.PageHeight() - overlayHeight
1✔
120
                }
1✔
121
        }
122

123
        // Handle repeat pattern
124
        if across*down > 1 {
2✔
125
                if err := overlay.EmbedMultiPage(0, 0, across*w, down*h,
1✔
126
                        &vips.EmbedMultiPageOptions{Extend: vips.ExtendRepeat}); err != nil {
1✔
NEW
127
                        return err
×
NEW
128
                }
×
129
        }
130

131
        // Position overlay on canvas
132
        if err := overlay.EmbedMultiPage(
1✔
133
                x, y, img.Width(), img.PageHeight(), nil,
1✔
134
        ); err != nil {
1✔
NEW
135
                return err
×
NEW
136
        }
×
137

138
        // Handle animation frames
139
        overlayN := overlay.Height() / overlay.PageHeight()
1✔
140
        if n := img.Height() / img.PageHeight(); n > overlayN {
2✔
141
                cnt := n / overlayN
1✔
142
                if n%overlayN > 0 {
2✔
143
                        cnt++
1✔
144
                }
1✔
145
                if err := overlay.Replicate(1, cnt); err != nil {
1✔
NEW
146
                        return err
×
NEW
147
                }
×
148
        }
149

150
        return nil
1✔
151
}
152

153
// getBlendMode returns the vips.BlendMode for a given mode string
154
// Defaults to BlendModeOver (normal) if mode is empty or invalid
155
func getBlendMode(mode string) vips.BlendMode {
1✔
156
        if mode == "" {
1✔
NEW
157
                return vips.BlendModeOver
×
NEW
158
        }
×
159
        if blendMode, ok := blendModeMap[strings.ToLower(mode)]; ok {
2✔
160
                return blendMode
1✔
161
        }
1✔
162
        // Default to normal if invalid mode
163
        return vips.BlendModeOver
1✔
164
}
165

166
func (v *Processor) image(ctx context.Context, img *vips.Image, load imagor.LoadFunc, args ...string) (err error) {
1✔
167
        ln := len(args)
1✔
168
        if ln < 1 {
1✔
NEW
169
                return
×
NEW
170
        }
×
171
        imagorPath := args[0]
1✔
172
        if unescape, e := url.QueryUnescape(args[0]); e == nil {
2✔
173
                imagorPath = unescape
1✔
174
        }
1✔
175
        params := imagorpath.Parse(imagorPath)
1✔
176
        var blob *imagor.Blob
1✔
177
        if blob, err = load(params.Image); err != nil {
1✔
NEW
178
                return
×
NEW
179
        }
×
180
        var overlay *vips.Image
1✔
181
        if overlay, err = v.loadAndProcess(ctx, blob, params, load); err != nil || overlay == nil {
1✔
NEW
182
                return
×
NEW
183
        }
×
184
        contextDefer(ctx, overlay.Close)
1✔
185

1✔
186
        var xArg, yArg string
1✔
187
        var alpha float64
1✔
188
        var blendMode = vips.BlendModeOver // default to normal
1✔
189

1✔
190
        if ln >= 2 {
2✔
191
                xArg = args[1]
1✔
192
        }
1✔
193
        if ln >= 3 {
2✔
194
                yArg = args[2]
1✔
195
        }
1✔
196
        if ln >= 4 {
2✔
197
                alpha, _ = strconv.ParseFloat(args[3], 64)
1✔
198
        }
1✔
199
        if ln >= 5 {
2✔
200
                // Parse blend mode (5th parameter)
1✔
201
                blendMode = getBlendMode(args[4])
1✔
202
        }
1✔
203

204
        // Prepare overlay for compositing
205
        if err = prepareOverlay(img, overlay, xArg, yArg, alpha); err != nil {
1✔
NEW
206
                return
×
NEW
207
        }
×
208

209
        // Composite overlay onto image with specified blend mode
210
        return img.Composite2(overlay, blendMode, nil)
1✔
211
}
212

213
func (v *Processor) watermark(ctx context.Context, img *vips.Image, load imagor.LoadFunc, args ...string) (err error) {
1✔
214
        ln := len(args)
1✔
215
        if ln < 1 {
2✔
216
                return
1✔
217
        }
1✔
218
        image := args[0]
1✔
219

1✔
220
        if unescape, e := url.QueryUnescape(args[0]); e == nil {
2✔
221
                image = unescape
1✔
222
        }
1✔
223

224
        if strings.HasPrefix(image, "b64:") {
2✔
225
                // if image URL starts with b64: prefix, Base64 decode it according to "base64url" in RFC 4648 (Section 5).
1✔
226
                result := make([]byte, base64.RawURLEncoding.DecodedLen(len(image[4:])))
1✔
227
                // in case decoding fails, use original image URL (possible that filename starts with b64: prefix, but as part of the file name)
1✔
228
                if _, e := base64.RawURLEncoding.Decode(result, []byte(image[4:])); e == nil {
2✔
229
                        image = string(result)
1✔
230
                }
1✔
231
        }
232

233
        var blob *imagor.Blob
1✔
234
        if blob, err = load(image); err != nil {
1✔
235
                return
×
236
        }
×
237
        var w, h int
1✔
238
        var overlay *vips.Image
1✔
239
        var n = 1
1✔
240
        if isAnimated(img) {
2✔
241
                n = -1
1✔
242
        }
1✔
243
        // w_ratio h_ratio
244
        if ln >= 6 {
2✔
245
                w = img.Width()
1✔
246
                h = img.PageHeight()
1✔
247
                if args[4] != "none" {
2✔
248
                        w, _ = strconv.Atoi(args[4])
1✔
249
                        w = img.Width() * w / 100
1✔
250
                }
1✔
251
                if args[5] != "none" {
2✔
252
                        h, _ = strconv.Atoi(args[5])
1✔
253
                        h = img.PageHeight() * h / 100
1✔
254
                }
1✔
255
                if overlay, err = v.NewThumbnail(
1✔
256
                        ctx, blob, w, h, vips.InterestingNone, vips.SizeBoth, n, 1, 0,
1✔
257
                ); err != nil {
1✔
258
                        return
×
259
                }
×
260
        } else {
1✔
261
                if overlay, err = v.NewThumbnail(
1✔
262
                        ctx, blob, v.MaxWidth, v.MaxHeight, vips.InterestingNone, vips.SizeDown, n, 1, 0,
1✔
263
                ); err != nil {
1✔
264
                        return
×
265
                }
×
266
        }
267
        contextDefer(ctx, overlay.Close)
1✔
268

1✔
269
        // Parse arguments
1✔
270
        var xArg, yArg string
1✔
271
        var alpha float64
1✔
272
        if ln >= 3 {
2✔
273
                xArg = args[1]
1✔
274
                yArg = args[2]
1✔
275
        }
1✔
276
        if ln >= 4 {
2✔
277
                alpha, _ = strconv.ParseFloat(args[3], 64)
1✔
278
        }
1✔
279

280
        // Prepare overlay for compositing
281
        if err = prepareOverlay(img, overlay, xArg, yArg, alpha); err != nil {
1✔
282
                return
×
283
        }
×
284

285
        // Composite overlay onto image
286
        return img.Composite2(overlay, vips.BlendModeOver, nil)
1✔
287
}
288

289
func (v *Processor) fill(ctx context.Context, img *vips.Image, w, h int, pLeft, pTop, pRight, pBottom int, colour string) (err error) {
1✔
290
        if isRotate90(ctx) {
2✔
291
                tmpW := w
1✔
292
                w = h
1✔
293
                h = tmpW
1✔
294
                tmpPLeft := pLeft
1✔
295
                pLeft = pTop
1✔
296
                pTop = tmpPLeft
1✔
297
                tmpPRight := pRight
1✔
298
                pRight = pBottom
1✔
299
                pBottom = tmpPRight
1✔
300
        }
1✔
301
        c := getColor(img, colour)
1✔
302
        left := (w-img.Width())/2 + pLeft
1✔
303
        top := (h-img.PageHeight())/2 + pTop
1✔
304
        width := w + pLeft + pRight
1✔
305
        height := h + pTop + pBottom
1✔
306
        if colour != "blur" || v.DisableBlur || isAnimated(img) {
2✔
307
                // fill color
1✔
308
                isTransparent := colour == "none" || colour == "transparent"
1✔
309
                if img.HasAlpha() && !isTransparent {
2✔
310
                        c := getColor(img, colour)
1✔
311
                        if err = img.Flatten(&vips.FlattenOptions{Background: c}); err != nil {
1✔
312
                                return
×
313
                        }
×
314
                }
315
                if isTransparent {
2✔
316
                        if img.Bands() < 3 {
2✔
317
                                if err = img.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
318
                                        return
×
319
                                }
×
320
                        }
321
                        if !img.HasAlpha() {
2✔
322
                                if err = img.Addalpha(); err != nil {
1✔
323
                                        return
×
324
                                }
×
325
                        }
326
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{Extend: vips.ExtendBlack}); err != nil {
1✔
327
                                return
×
328
                        }
×
329
                } else if isBlack(c) {
2✔
330
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{Extend: vips.ExtendBlack}); err != nil {
1✔
331
                                return
×
332
                        }
×
333
                } else if isWhite(c) {
2✔
334
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{Extend: vips.ExtendWhite}); err != nil {
1✔
335
                                return
×
336
                        }
×
337
                } else {
1✔
338
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{
1✔
339
                                Extend:     vips.ExtendBackground,
1✔
340
                                Background: c,
1✔
341
                        }); err != nil {
1✔
342
                                return
×
343
                        }
×
344
                }
345
        } else {
1✔
346
                // fill blur
1✔
347
                var cp *vips.Image
1✔
348
                if cp, err = img.Copy(nil); err != nil {
1✔
349
                        return
×
350
                }
×
351
                contextDefer(ctx, cp.Close)
1✔
352
                if err = img.ThumbnailImage(
1✔
353
                        width, &vips.ThumbnailImageOptions{
1✔
354
                                Height: height,
1✔
355
                                Crop:   vips.InterestingNone,
1✔
356
                                Size:   vips.SizeForce,
1✔
357
                        },
1✔
358
                ); err != nil {
1✔
359
                        return
×
360
                }
×
361
                if err = img.Gaussblur(50, nil); err != nil {
1✔
362
                        return
×
363
                }
×
364
                if err = img.Composite2(
1✔
365
                        cp, vips.BlendModeOver,
1✔
366
                        &vips.Composite2Options{X: left, Y: top}); err != nil {
1✔
367
                        return
×
368
                }
×
369
        }
370
        return
1✔
371
}
372

373
func roundCorner(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
374
        var rx, ry int
1✔
375
        var c []float64
1✔
376
        if len(args) == 0 {
2✔
377
                return
1✔
378
        }
1✔
379
        if a, e := url.QueryUnescape(args[0]); e == nil {
2✔
380
                args[0] = a
1✔
381
        }
1✔
382
        if len(args) == 3 {
2✔
383
                // rx,ry,color
1✔
384
                c = getColor(img, args[2])
1✔
385
                args = args[:2]
1✔
386
        }
1✔
387
        rx, _ = strconv.Atoi(args[0])
1✔
388
        ry = rx
1✔
389
        if len(args) > 1 {
2✔
390
                ry, _ = strconv.Atoi(args[1])
1✔
391
        }
1✔
392

393
        var rounded *vips.Image
1✔
394
        var w = img.Width()
1✔
395
        var h = img.PageHeight()
1✔
396
        if rounded, err = vips.NewSvgloadBuffer([]byte(fmt.Sprintf(`
1✔
397
                <svg viewBox="0 0 %d %d">
1✔
398
                        <rect rx="%d" ry="%d" 
1✔
399
                         x="0" y="0" width="%d" height="%d" 
1✔
400
                         fill="#fff"/>
1✔
401
                </svg>
1✔
402
        `, w, h, rx, ry, w, h)), nil); err != nil {
1✔
403
                return
×
404
        }
×
405
        contextDefer(ctx, rounded.Close)
1✔
406
        if n := img.Height() / img.PageHeight(); n > 1 {
2✔
407
                if err = rounded.Replicate(1, n); err != nil {
1✔
408
                        return
×
409
                }
×
410
        }
411
        if err = img.Composite2(rounded, vips.BlendModeDestIn, nil); err != nil {
1✔
412
                return
×
413
        }
×
414
        if c != nil {
2✔
415
                if err = img.Flatten(&vips.FlattenOptions{Background: c}); err != nil {
1✔
416
                        return
×
417
                }
×
418
        }
419
        return nil
1✔
420
}
421

422
func label(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
423
        ln := len(args)
1✔
424
        if ln == 0 {
1✔
425
                return
×
426
        }
×
427
        if a, e := url.QueryUnescape(args[0]); e == nil {
2✔
428
                args[0] = a
1✔
429
        }
1✔
430
        var text = args[0]
1✔
431
        var font = "tahoma"
1✔
432
        var x, y int
1✔
433
        var c []float64
1✔
434
        var alpha float64
1✔
435
        var align = vips.AlignLow
1✔
436
        var size = 20
1✔
437
        var width = img.Width()
1✔
438
        if ln > 3 {
2✔
439
                size, _ = strconv.Atoi(args[3])
1✔
440
        }
1✔
441
        if ln > 1 {
2✔
442
                if args[1] == "center" {
2✔
443
                        align = vips.AlignCentre
1✔
444
                        x = width / 2
1✔
445
                } else if args[1] == imagorpath.HAlignRight {
3✔
446
                        align = vips.AlignHigh
1✔
447
                        x = width
1✔
448
                } else if strings.HasPrefix(strings.TrimPrefix(args[1], "-"), "0.") {
3✔
449
                        pec, _ := strconv.ParseFloat(args[1], 64)
1✔
450
                        x = int(pec * float64(width))
1✔
451
                } else if strings.HasSuffix(args[1], "p") {
3✔
452
                        x, _ = strconv.Atoi(strings.TrimSuffix(args[1], "p"))
1✔
453
                        x = x * width / 100
1✔
454
                } else {
2✔
455
                        x, _ = strconv.Atoi(args[1])
1✔
456
                }
1✔
457
                if x < 0 {
2✔
458
                        align = vips.AlignHigh
1✔
459
                        x += width
1✔
460
                }
1✔
461
        }
462
        if ln > 2 {
2✔
463
                if args[2] == "center" {
2✔
464
                        y = (img.PageHeight() - size) / 2
1✔
465
                } else if args[2] == imagorpath.VAlignTop {
3✔
466
                        y = 0
1✔
467
                } else if args[2] == imagorpath.VAlignBottom {
3✔
468
                        y = img.PageHeight() - size
1✔
469
                } else if strings.HasPrefix(strings.TrimPrefix(args[2], "-"), "0.") {
3✔
470
                        pec, _ := strconv.ParseFloat(args[2], 64)
1✔
471
                        y = int(pec * float64(img.PageHeight()))
1✔
472
                } else if strings.HasSuffix(args[2], "p") {
3✔
473
                        y, _ = strconv.Atoi(strings.TrimSuffix(args[2], "p"))
1✔
474
                        y = y * img.PageHeight() / 100
1✔
475
                } else {
2✔
476
                        y, _ = strconv.Atoi(args[2])
1✔
477
                }
1✔
478
                if y < 0 {
2✔
479
                        y += img.PageHeight() - size
1✔
480
                }
1✔
481
        }
482
        if ln > 4 {
2✔
483
                c = getColor(img, args[4])
1✔
484
        }
1✔
485
        if ln > 5 {
2✔
486
                alpha, _ = strconv.ParseFloat(args[5], 64)
1✔
487
                alpha /= 100
1✔
488
        }
1✔
489
        if ln > 6 {
2✔
490
                if a, e := url.QueryUnescape(args[6]); e == nil {
2✔
491
                        font = a
1✔
492
                } else {
1✔
493
                        font = args[6]
×
494
                }
×
495
        }
496
        if img.Bands() < 3 {
2✔
497
                if err = img.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
498
                        return
×
499
                }
×
500
        }
501
        if !img.HasAlpha() {
2✔
502
                if err = img.Addalpha(); err != nil {
1✔
503
                        return
×
504
                }
×
505
        }
506
        return img.Label(text, x, y, &vips.LabelOptions{
1✔
507
                Font:    font,
1✔
508
                Size:    size,
1✔
509
                Align:   align,
1✔
510
                Opacity: 1 - alpha,
1✔
511
                Color:   c,
1✔
512
        })
1✔
513
}
514

515
func (v *Processor) padding(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
516
        ln := len(args)
1✔
517
        if ln < 2 {
2✔
518
                return nil
1✔
519
        }
1✔
520
        var (
1✔
521
                c       = args[0]
1✔
522
                left, _ = strconv.Atoi(args[1])
1✔
523
                top     = left
1✔
524
                right   = left
1✔
525
                bottom  = left
1✔
526
        )
1✔
527
        if ln > 2 {
2✔
528
                top, _ = strconv.Atoi(args[2])
1✔
529
                bottom = top
1✔
530
        }
1✔
531
        if ln > 4 {
2✔
532
                right, _ = strconv.Atoi(args[3])
1✔
533
                bottom, _ = strconv.Atoi(args[4])
1✔
534
        }
1✔
535
        return v.fill(ctx, img, img.Width(), img.PageHeight(), left, top, right, bottom, c)
1✔
536
}
537

538
func backgroundColor(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
539
        if len(args) == 0 {
2✔
540
                return
1✔
541
        }
1✔
542
        if !img.HasAlpha() {
2✔
543
                return
1✔
544
        }
1✔
545
        c := getColor(img, args[0])
1✔
546
        return img.Flatten(&vips.FlattenOptions{
1✔
547
                Background: c,
1✔
548
        })
1✔
549
}
550

551
func rotate(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
552
        if len(args) == 0 {
2✔
553
                return
1✔
554
        }
1✔
555
        if angle, _ := strconv.Atoi(args[0]); angle > 0 {
2✔
556
                switch angle {
1✔
557
                case 90, 270:
1✔
558
                        setRotate90(ctx)
1✔
559
                }
560
                if err = img.RotMultiPage(getAngle(angle)); err != nil {
1✔
561
                        return err
×
562
                }
×
563
        }
564
        return
1✔
565
}
566

567
func getAngle(angle int) vips.Angle {
1✔
568
        switch angle {
1✔
569
        case 90:
1✔
570
                return vips.AngleD270
1✔
571
        case 180:
1✔
572
                return vips.AngleD180
1✔
573
        case 270:
1✔
574
                return vips.AngleD90
1✔
575
        default:
×
576
                return vips.AngleD0
×
577
        }
578
}
579

580
func proportion(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
581
        if len(args) == 0 {
2✔
582
                return
1✔
583
        }
1✔
584
        scale, _ := strconv.ParseFloat(args[0], 64)
1✔
585
        if scale <= 0 {
2✔
586
                return // no ops
1✔
587
        }
1✔
588
        if scale > 100 {
2✔
589
                scale = 100
1✔
590
        }
1✔
591
        if scale > 1 {
2✔
592
                scale /= 100
1✔
593
        }
1✔
594
        width := int(float64(img.Width()) * scale)
1✔
595
        height := int(float64(img.PageHeight()) * scale)
1✔
596
        if width <= 0 || height <= 0 {
2✔
597
                return // op ops
1✔
598
        }
1✔
599
        return img.ThumbnailImage(width, &vips.ThumbnailImageOptions{
1✔
600
                Height: height,
1✔
601
                Crop:   vips.InterestingNone,
1✔
602
        })
1✔
603
}
604

605
func grayscale(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
606
        return img.Colourspace(vips.InterpretationBW, nil)
1✔
607
}
1✔
608

609
func brightness(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
610
        if len(args) == 0 {
2✔
611
                return
1✔
612
        }
1✔
613
        b, _ := strconv.ParseFloat(args[0], 64)
1✔
614
        b = b * 255 / 100
1✔
615
        return linearRGB(img, []float64{1, 1, 1}, []float64{b, b, b})
1✔
616
}
617

618
func contrast(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
619
        if len(args) == 0 {
2✔
620
                return
1✔
621
        }
1✔
622
        a, _ := strconv.ParseFloat(args[0], 64)
1✔
623
        a = a * 255 / 100
1✔
624
        a = math.Min(math.Max(a, -255), 255)
1✔
625
        a = (259 * (a + 255)) / (255 * (259 - a))
1✔
626
        b := 128 - a*128
1✔
627
        return linearRGB(img, []float64{a, a, a}, []float64{b, b, b})
1✔
628
}
629

630
func hue(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
631
        if len(args) == 0 {
2✔
632
                return
1✔
633
        }
1✔
634
        h, _ := strconv.ParseFloat(args[0], 64)
1✔
635
        return img.Modulate(1, 1, h)
1✔
636
}
637

638
func saturation(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
639
        if len(args) == 0 {
2✔
640
                return
1✔
641
        }
1✔
642
        s, _ := strconv.ParseFloat(args[0], 64)
1✔
643
        s = 1 + s/100
1✔
644
        return img.Modulate(1, s, 0)
1✔
645
}
646

647
func rgb(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
648
        if len(args) != 3 {
2✔
649
                return
1✔
650
        }
1✔
651
        r, _ := strconv.ParseFloat(args[0], 64)
1✔
652
        g, _ := strconv.ParseFloat(args[1], 64)
1✔
653
        b, _ := strconv.ParseFloat(args[2], 64)
1✔
654
        r = r * 255 / 100
1✔
655
        g = g * 255 / 100
1✔
656
        b = b * 255 / 100
1✔
657
        return linearRGB(img, []float64{1, 1, 1}, []float64{r, g, b})
1✔
658
}
659

660
func modulate(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
661
        if len(args) != 3 {
2✔
662
                return
1✔
663
        }
1✔
664
        b, _ := strconv.ParseFloat(args[0], 64)
1✔
665
        s, _ := strconv.ParseFloat(args[1], 64)
1✔
666
        h, _ := strconv.ParseFloat(args[2], 64)
1✔
667
        b = 1 + b/100
1✔
668
        s = 1 + s/100
1✔
669
        return img.Modulate(b, s, h)
1✔
670
}
671

672
func blur(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
673
        if isAnimated(img) {
2✔
674
                // skip animation support
1✔
675
                return
1✔
676
        }
1✔
677
        var sigma float64
1✔
678
        switch len(args) {
1✔
679
        case 2:
1✔
680
                sigma, _ = strconv.ParseFloat(args[1], 64)
1✔
681
                break
1✔
682
        case 1:
1✔
683
                sigma, _ = strconv.ParseFloat(args[0], 64)
1✔
684
                break
1✔
685
        }
686
        sigma /= 2
1✔
687
        if sigma > 0 {
2✔
688
                return img.Gaussblur(sigma, nil)
1✔
689
        }
1✔
690
        return
×
691
}
692

693
func sharpen(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
694
        if isAnimated(img) {
2✔
695
                // skip animation support
1✔
696
                return
1✔
697
        }
1✔
698
        var sigma float64
1✔
699
        switch len(args) {
1✔
700
        case 1:
1✔
701
                sigma, _ = strconv.ParseFloat(args[0], 64)
1✔
702
                break
1✔
703
        case 2, 3:
1✔
704
                sigma, _ = strconv.ParseFloat(args[1], 64)
1✔
705
                break
1✔
706
        }
707
        sigma = 1 + sigma*2
1✔
708
        if sigma > 0 {
2✔
709
                return img.Sharpen(&vips.SharpenOptions{
1✔
710
                        Sigma: sigma,
1✔
711
                        X1:    1,
1✔
712
                        M2:    2,
1✔
713
                })
1✔
714
        }
1✔
715
        return
1✔
716
}
717

718
func stripIcc(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
719
        if img.HasICCProfile() {
2✔
720
                opts := vips.DefaultIccTransformOptions()
1✔
721
                opts.Embedded = true
1✔
722
                opts.Intent = vips.IntentPerceptual
1✔
723
                if img.Interpretation() == vips.InterpretationRgb16 {
1✔
724
                        opts.Depth = 16
×
725
                }
×
726
                _ = img.IccTransform("srgb", opts)
1✔
727
        }
728
        return img.RemoveICCProfile()
1✔
729
}
730

731
func toColorspace(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
732
        profile := "srgb"
1✔
733
        if len(args) > 0 && args[0] != "" {
2✔
734
                profile = strings.ToLower(args[0])
1✔
735
        }
1✔
736
        if !img.HasICCProfile() {
2✔
737
                return nil
1✔
738
        }
1✔
739
        opts := vips.DefaultIccTransformOptions()
1✔
740
        opts.Embedded = true
1✔
741
        opts.Intent = vips.IntentPerceptual
1✔
742
        if img.Interpretation() == vips.InterpretationRgb16 {
1✔
743
                opts.Depth = 16
×
744
        }
×
745
        return img.IccTransform(profile, opts)
1✔
746
}
747

748
func stripExif(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
749
        return img.RemoveExif()
1✔
750
}
1✔
751

752
func trim(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
753
        var (
1✔
754
                ln        = len(args)
1✔
755
                pos       string
1✔
756
                tolerance int
1✔
757
        )
1✔
758
        if ln > 0 {
2✔
759
                tolerance, _ = strconv.Atoi(args[0])
1✔
760
        }
1✔
761
        if ln > 1 {
2✔
762
                pos = args[1]
1✔
763
        }
1✔
764
        if l, t, w, h, err := findTrim(ctx, img, pos, tolerance); err == nil {
2✔
765
                return img.ExtractAreaMultiPage(l, t, w, h)
1✔
766
        }
1✔
767
        return nil
×
768
}
769

770
func linearRGB(img *vips.Image, a, b []float64) error {
1✔
771
        if img.HasAlpha() {
2✔
772
                a = append(a, 1)
1✔
773
                b = append(b, 0)
1✔
774
        }
1✔
775
        return img.Linear(a, b, nil)
1✔
776
}
777

778
func isBlack(c []float64) bool {
1✔
779
        if len(c) < 3 {
1✔
780
                return false
×
781
        }
×
782
        return c[0] == 0x00 && c[1] == 0x00 && c[2] == 0x00
1✔
783
}
784

785
func isWhite(c []float64) bool {
1✔
786
        if len(c) < 3 {
1✔
787
                return false
×
788
        }
×
789
        return c[0] == 0xff && c[1] == 0xff && c[2] == 0xff
1✔
790
}
791

792
func getColor(img *vips.Image, color string) []float64 {
1✔
793
        var vc = make([]float64, 3)
1✔
794
        args := strings.Split(strings.ToLower(color), ",")
1✔
795
        mode := ""
1✔
796
        name := strings.TrimPrefix(args[0], "#")
1✔
797
        if len(args) > 1 {
2✔
798
                mode = args[1]
1✔
799
        }
1✔
800
        if name == "auto" {
2✔
801
                if img != nil {
2✔
802
                        x := 0
1✔
803
                        y := 0
1✔
804
                        if mode == "bottom-right" {
2✔
805
                                x = img.Width() - 1
1✔
806
                                y = img.PageHeight() - 1
1✔
807
                        }
1✔
808
                        p, _ := img.Getpoint(x, y, nil)
1✔
809
                        if len(p) >= 3 {
2✔
810
                                vc[0] = p[0]
1✔
811
                                vc[1] = p[1]
1✔
812
                                vc[2] = p[2]
1✔
813
                        }
1✔
814
                }
815
        } else if c, ok := colornames.Map[name]; ok {
2✔
816
                vc[0] = float64(c.R)
1✔
817
                vc[1] = float64(c.G)
1✔
818
                vc[2] = float64(c.B)
1✔
819
        } else if c, ok := parseHexColor(name); ok {
3✔
820
                vc[0] = float64(c.R)
1✔
821
                vc[1] = float64(c.G)
1✔
822
                vc[2] = float64(c.B)
1✔
823
        }
1✔
824
        return vc
1✔
825
}
826

827
func parseHexColor(s string) (c color.RGBA, ok bool) {
1✔
828
        c.A = 0xff
1✔
829
        switch len(s) {
1✔
830
        case 6:
1✔
831
                c.R = hexToByte(s[0])<<4 + hexToByte(s[1])
1✔
832
                c.G = hexToByte(s[2])<<4 + hexToByte(s[3])
1✔
833
                c.B = hexToByte(s[4])<<4 + hexToByte(s[5])
1✔
834
                ok = true
1✔
835
        case 3:
1✔
836
                c.R = hexToByte(s[0]) * 17
1✔
837
                c.G = hexToByte(s[1]) * 17
1✔
838
                c.B = hexToByte(s[2]) * 17
1✔
839
                ok = true
1✔
840
        }
841
        return
1✔
842
}
843

844
func hexToByte(b byte) byte {
1✔
845
        switch {
1✔
846
        case b >= '0' && b <= '9':
1✔
847
                return b - '0'
1✔
848
        case b >= 'a' && b <= 'f':
1✔
849
                return b - 'a' + 10
1✔
850
        }
851
        return 0
1✔
852
}
853

854
func crop(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
855
        if len(args) < 4 {
1✔
856
                return nil
×
857
        }
×
858

859
        // Parse arguments
860
        left, _ := strconv.ParseFloat(args[0], 64)
1✔
861
        top, _ := strconv.ParseFloat(args[1], 64)
1✔
862
        width, _ := strconv.ParseFloat(args[2], 64)
1✔
863
        height, _ := strconv.ParseFloat(args[3], 64)
1✔
864

1✔
865
        imgWidth := float64(img.Width())
1✔
866
        imgHeight := float64(img.PageHeight())
1✔
867

1✔
868
        // Convert relative (0-1) to absolute pixels
1✔
869
        if left > 0 && left < 1 {
2✔
870
                left = left * imgWidth
1✔
871
        }
1✔
872
        if top > 0 && top < 1 {
2✔
873
                top = top * imgHeight
1✔
874
        }
1✔
875
        if width > 0 && width < 1 {
2✔
876
                width = width * imgWidth
1✔
877
        }
1✔
878
        if height > 0 && height < 1 {
2✔
879
                height = height * imgHeight
1✔
880
        }
1✔
881

882
        // Clamp left and top to image bounds
883
        left = math.Max(0, math.Min(left, imgWidth))
1✔
884
        top = math.Max(0, math.Min(top, imgHeight))
1✔
885

1✔
886
        // Adjust width and height to not exceed image bounds
1✔
887
        width = math.Min(width, imgWidth-left)
1✔
888
        height = math.Min(height, imgHeight-top)
1✔
889

1✔
890
        // Skip if invalid crop area
1✔
891
        if width <= 0 || height <= 0 {
1✔
892
                return nil
×
893
        }
×
894

895
        return img.ExtractAreaMultiPage(int(left), int(top), int(width), int(height))
1✔
896
}
897

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