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

cshum / imagor / 21673761649

04 Feb 2026 01:42PM UTC coverage: 91.731% (-0.1%) from 91.86%
21673761649

push

github

web-flow
fix: image filter to crop overlay to only the visible portion (#724)

* fix: image filter to crop overlay to only the visible portion

* test cases

* test: update golden files

---------

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

27 of 37 new or added lines in 1 file covered. (72.97%)

1 existing line in 1 file now uncovered.

5724 of 6240 relevant lines covered (91.73%)

1.1 hits per line

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

87.55
/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
// transformOverlay transform overlay image for compositing
40
// Handles color space, alpha channel, positioning, repeat patterns, and animation frames
41
func transformOverlay(img *vips.Image, overlay *vips.Image, xArg, yArg string, alpha float64) error {
1✔
42
        // Ensure overlay has proper color space and alpha
1✔
43
        if overlay.Bands() < 3 {
2✔
44
                if err := overlay.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
45
                        return err
×
46
                }
×
47
        }
48
        if !overlay.HasAlpha() {
2✔
49
                if err := overlay.Addalpha(); err != nil {
1✔
50
                        return err
×
51
                }
×
52
        }
53

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

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

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

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

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

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

130
        // Position overlay on canvas
131
        // Crop overlay to only the visible portion within canvas bounds
132
        visibleLeft := 0
1✔
133
        visibleTop := 0
1✔
134
        visibleWidth := overlayWidth
1✔
135
        visibleHeight := overlayHeight
1✔
136
        embedX := x
1✔
137
        embedY := y
1✔
138

1✔
139
        // Handle overlay extending beyond right/bottom edges
1✔
140
        if x+overlayWidth > img.Width() {
2✔
141
                visibleWidth = img.Width() - x
1✔
142
        }
1✔
143
        if y+overlayHeight > img.PageHeight() {
2✔
144
                visibleHeight = img.PageHeight() - y
1✔
145
        }
1✔
146

147
        // Handle overlay starting before left/top edges (negative positions)
148
        if x < 0 {
1✔
NEW
149
                visibleLeft = -x
×
NEW
150
                visibleWidth = overlayWidth + x // reduce width
×
NEW
151
                embedX = 0
×
NEW
152
        }
×
153
        if y < 0 {
1✔
NEW
154
                visibleTop = -y
×
NEW
155
                visibleHeight = overlayHeight + y // reduce height
×
NEW
156
                embedY = 0
×
NEW
157
        }
×
158

159
        // Crop overlay to visible portion if needed
160
        if visibleLeft > 0 || visibleTop > 0 ||
1✔
161
                visibleWidth < overlayWidth || visibleHeight < overlayHeight {
2✔
162
                if visibleWidth > 0 && visibleHeight > 0 {
2✔
163
                        if err := overlay.ExtractAreaMultiPage(
1✔
164
                                visibleLeft, visibleTop, visibleWidth, visibleHeight,
1✔
165
                        ); err != nil {
1✔
NEW
166
                                return err
×
NEW
167
                        }
×
168
                } else {
1✔
169
                        // Overlay is completely outside canvas bounds, skip it
1✔
170
                        return nil
1✔
171
                }
1✔
172
        }
173

174
        // Embed the cropped overlay at adjusted position
175
        if err := overlay.EmbedMultiPage(
1✔
176
                embedX, embedY, img.Width(), img.PageHeight(), nil,
1✔
177
        ); err != nil {
1✔
178
                return err
×
179
        }
×
180

181
        // Handle animation frames
182
        overlayN := overlay.Height() / overlay.PageHeight()
1✔
183
        if n := img.Height() / img.PageHeight(); n > overlayN {
2✔
184
                cnt := n / overlayN
1✔
185
                if n%overlayN > 0 {
2✔
186
                        cnt++
1✔
187
                }
1✔
188
                if err := overlay.Replicate(1, cnt); err != nil {
1✔
189
                        return err
×
190
                }
×
191
        }
192

193
        return nil
1✔
194
}
195

196
// getBlendMode returns the vips.BlendMode for a given mode string
197
// Defaults to BlendModeOver (normal) if mode is empty or invalid
198
func getBlendMode(mode string) vips.BlendMode {
1✔
199
        if mode == "" {
1✔
200
                return vips.BlendModeOver
×
201
        }
×
202
        if blendMode, ok := blendModeMap[strings.ToLower(mode)]; ok {
2✔
203
                return blendMode
1✔
204
        }
1✔
205
        // Default to normal if invalid mode
206
        return vips.BlendModeOver
1✔
207
}
208

209
func (v *Processor) image(ctx context.Context, img *vips.Image, load imagor.LoadFunc, args ...string) (err error) {
1✔
210
        ln := len(args)
1✔
211
        if ln < 1 {
1✔
212
                return
×
213
        }
×
214
        imagorPath := args[0]
1✔
215
        if unescape, e := url.QueryUnescape(args[0]); e == nil {
2✔
216
                imagorPath = unescape
1✔
217
        }
1✔
218
        params := imagorpath.Parse(imagorPath)
1✔
219
        var blob *imagor.Blob
1✔
220
        if blob, err = load(params.Image); err != nil {
1✔
221
                return
×
222
        }
×
223
        var overlay *vips.Image
1✔
224
        if overlay, err = v.loadAndProcess(ctx, blob, params, load); err != nil || overlay == nil {
1✔
225
                return
×
226
        }
×
227
        contextDefer(ctx, overlay.Close)
1✔
228

1✔
229
        var xArg, yArg string
1✔
230
        var alpha float64
1✔
231
        var blendMode = vips.BlendModeOver // default to normal
1✔
232

1✔
233
        if ln >= 2 {
2✔
234
                xArg = args[1]
1✔
235
        }
1✔
236
        if ln >= 3 {
2✔
237
                yArg = args[2]
1✔
238
        }
1✔
239
        if ln >= 4 {
2✔
240
                alpha, _ = strconv.ParseFloat(args[3], 64)
1✔
241
        }
1✔
242
        if ln >= 5 {
2✔
243
                // Parse blend mode (5th parameter)
1✔
244
                blendMode = getBlendMode(args[4])
1✔
245
        }
1✔
246

247
        // Prepare overlay for compositing
248
        if err = transformOverlay(img, overlay, xArg, yArg, alpha); err != nil {
1✔
249
                return
×
250
        }
×
251

252
        // Composite overlay onto image with specified blend mode
253
        return img.Composite2(overlay, blendMode, nil)
1✔
254
}
255

256
func (v *Processor) watermark(ctx context.Context, img *vips.Image, load imagor.LoadFunc, args ...string) (err error) {
1✔
257
        ln := len(args)
1✔
258
        if ln < 1 {
2✔
259
                return
1✔
260
        }
1✔
261
        image := args[0]
1✔
262

1✔
263
        if unescape, e := url.QueryUnescape(args[0]); e == nil {
2✔
264
                image = unescape
1✔
265
        }
1✔
266

267
        if strings.HasPrefix(image, "b64:") {
2✔
268
                // if image URL starts with b64: prefix, Base64 decode it according to "base64url" in RFC 4648 (Section 5).
1✔
269
                result := make([]byte, base64.RawURLEncoding.DecodedLen(len(image[4:])))
1✔
270
                // in case decoding fails, use original image URL (possible that filename starts with b64: prefix, but as part of the file name)
1✔
271
                if _, e := base64.RawURLEncoding.Decode(result, []byte(image[4:])); e == nil {
2✔
272
                        image = string(result)
1✔
273
                }
1✔
274
        }
275

276
        var blob *imagor.Blob
1✔
277
        if blob, err = load(image); err != nil {
1✔
278
                return
×
279
        }
×
280
        var w, h int
1✔
281
        var overlay *vips.Image
1✔
282
        var n = 1
1✔
283
        if isAnimated(img) {
2✔
284
                n = -1
1✔
285
        }
1✔
286
        // w_ratio h_ratio
287
        if ln >= 6 {
2✔
288
                w = img.Width()
1✔
289
                h = img.PageHeight()
1✔
290
                if args[4] != "none" {
2✔
291
                        w, _ = strconv.Atoi(args[4])
1✔
292
                        w = img.Width() * w / 100
1✔
293
                }
1✔
294
                if args[5] != "none" {
2✔
295
                        h, _ = strconv.Atoi(args[5])
1✔
296
                        h = img.PageHeight() * h / 100
1✔
297
                }
1✔
298
                if overlay, err = v.NewThumbnail(
1✔
299
                        ctx, blob, w, h, vips.InterestingNone, vips.SizeBoth, n, 1, 0,
1✔
300
                ); err != nil {
1✔
301
                        return
×
302
                }
×
303
        } else {
1✔
304
                if overlay, err = v.NewThumbnail(
1✔
305
                        ctx, blob, v.MaxWidth, v.MaxHeight, vips.InterestingNone, vips.SizeDown, n, 1, 0,
1✔
306
                ); err != nil {
1✔
307
                        return
×
308
                }
×
309
        }
310
        contextDefer(ctx, overlay.Close)
1✔
311

1✔
312
        // Parse arguments
1✔
313
        var xArg, yArg string
1✔
314
        var alpha float64
1✔
315
        if ln >= 3 {
2✔
316
                xArg = args[1]
1✔
317
                yArg = args[2]
1✔
318
        }
1✔
319
        if ln >= 4 {
2✔
320
                alpha, _ = strconv.ParseFloat(args[3], 64)
1✔
321
        }
1✔
322

323
        // Prepare overlay for compositing
324
        if err = transformOverlay(img, overlay, xArg, yArg, alpha); err != nil {
1✔
325
                return
×
326
        }
×
327

328
        // Composite overlay onto image
329
        return img.Composite2(overlay, vips.BlendModeOver, nil)
1✔
330
}
331

332
func (v *Processor) fill(ctx context.Context, img *vips.Image, w, h int, pLeft, pTop, pRight, pBottom int, colour string) (err error) {
1✔
333
        if isRotate90(ctx) {
2✔
334
                tmpW := w
1✔
335
                w = h
1✔
336
                h = tmpW
1✔
337
                tmpPLeft := pLeft
1✔
338
                pLeft = pTop
1✔
339
                pTop = tmpPLeft
1✔
340
                tmpPRight := pRight
1✔
341
                pRight = pBottom
1✔
342
                pBottom = tmpPRight
1✔
343
        }
1✔
344
        c := getColor(img, colour)
1✔
345
        left := (w-img.Width())/2 + pLeft
1✔
346
        top := (h-img.PageHeight())/2 + pTop
1✔
347
        width := w + pLeft + pRight
1✔
348
        height := h + pTop + pBottom
1✔
349
        if colour != "blur" || v.DisableBlur || isAnimated(img) {
2✔
350
                // fill color
1✔
351
                isTransparent := colour == "none" || colour == "transparent"
1✔
352
                if img.HasAlpha() && !isTransparent {
2✔
353
                        c := getColor(img, colour)
1✔
354
                        if err = img.Flatten(&vips.FlattenOptions{Background: c}); err != nil {
1✔
355
                                return
×
356
                        }
×
357
                }
358
                if isTransparent {
2✔
359
                        if img.Bands() < 3 {
2✔
360
                                if err = img.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
361
                                        return
×
362
                                }
×
363
                        }
364
                        if !img.HasAlpha() {
2✔
365
                                if err = img.Addalpha(); err != nil {
1✔
366
                                        return
×
367
                                }
×
368
                        }
369
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{Extend: vips.ExtendBlack}); err != nil {
1✔
370
                                return
×
371
                        }
×
372
                } else if isBlack(c) {
2✔
373
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{Extend: vips.ExtendBlack}); err != nil {
1✔
374
                                return
×
375
                        }
×
376
                } else if isWhite(c) {
2✔
377
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{Extend: vips.ExtendWhite}); err != nil {
1✔
378
                                return
×
379
                        }
×
380
                } else {
1✔
381
                        if err = img.EmbedMultiPage(left, top, width, height, &vips.EmbedMultiPageOptions{
1✔
382
                                Extend:     vips.ExtendBackground,
1✔
383
                                Background: c,
1✔
384
                        }); err != nil {
1✔
385
                                return
×
386
                        }
×
387
                }
388
        } else {
1✔
389
                // fill blur
1✔
390
                var cp *vips.Image
1✔
391
                if cp, err = img.Copy(nil); err != nil {
1✔
392
                        return
×
393
                }
×
394
                contextDefer(ctx, cp.Close)
1✔
395
                if err = img.ThumbnailImage(
1✔
396
                        width, &vips.ThumbnailImageOptions{
1✔
397
                                Height: height,
1✔
398
                                Crop:   vips.InterestingNone,
1✔
399
                                Size:   vips.SizeForce,
1✔
400
                        },
1✔
401
                ); err != nil {
1✔
402
                        return
×
403
                }
×
404
                if err = img.Gaussblur(50, nil); err != nil {
1✔
405
                        return
×
406
                }
×
407
                if err = img.Composite2(
1✔
408
                        cp, vips.BlendModeOver,
1✔
409
                        &vips.Composite2Options{X: left, Y: top}); err != nil {
1✔
410
                        return
×
411
                }
×
412
        }
413
        return
1✔
414
}
415

416
func roundCorner(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
417
        var rx, ry int
1✔
418
        var c []float64
1✔
419
        if len(args) == 0 {
2✔
420
                return
1✔
421
        }
1✔
422
        if a, e := url.QueryUnescape(args[0]); e == nil {
2✔
423
                args[0] = a
1✔
424
        }
1✔
425
        if len(args) == 3 {
2✔
426
                // rx,ry,color
1✔
427
                c = getColor(img, args[2])
1✔
428
                args = args[:2]
1✔
429
        }
1✔
430
        rx, _ = strconv.Atoi(args[0])
1✔
431
        ry = rx
1✔
432
        if len(args) > 1 {
2✔
433
                ry, _ = strconv.Atoi(args[1])
1✔
434
        }
1✔
435

436
        var rounded *vips.Image
1✔
437
        var w = img.Width()
1✔
438
        var h = img.PageHeight()
1✔
439
        if rounded, err = vips.NewSvgloadBuffer([]byte(fmt.Sprintf(`
1✔
440
                <svg viewBox="0 0 %d %d">
1✔
441
                        <rect rx="%d" ry="%d" 
1✔
442
                         x="0" y="0" width="%d" height="%d" 
1✔
443
                         fill="#fff"/>
1✔
444
                </svg>
1✔
445
        `, w, h, rx, ry, w, h)), nil); err != nil {
1✔
446
                return
×
447
        }
×
448
        contextDefer(ctx, rounded.Close)
1✔
449
        if n := img.Height() / img.PageHeight(); n > 1 {
2✔
450
                if err = rounded.Replicate(1, n); err != nil {
1✔
451
                        return
×
452
                }
×
453
        }
454
        if err = img.Composite2(rounded, vips.BlendModeDestIn, nil); err != nil {
1✔
455
                return
×
456
        }
×
457
        if c != nil {
2✔
458
                if err = img.Flatten(&vips.FlattenOptions{Background: c}); err != nil {
1✔
459
                        return
×
460
                }
×
461
        }
462
        return nil
1✔
463
}
464

465
func label(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
466
        ln := len(args)
1✔
467
        if ln == 0 {
1✔
468
                return
×
469
        }
×
470
        if a, e := url.QueryUnescape(args[0]); e == nil {
2✔
471
                args[0] = a
1✔
472
        }
1✔
473
        var text = args[0]
1✔
474
        var font = "tahoma"
1✔
475
        var x, y int
1✔
476
        var c []float64
1✔
477
        var alpha float64
1✔
478
        var align = vips.AlignLow
1✔
479
        var size = 20
1✔
480
        var width = img.Width()
1✔
481
        if ln > 3 {
2✔
482
                size, _ = strconv.Atoi(args[3])
1✔
483
        }
1✔
484
        if ln > 1 {
2✔
485
                if args[1] == "center" {
2✔
486
                        align = vips.AlignCentre
1✔
487
                        x = width / 2
1✔
488
                } else if args[1] == imagorpath.HAlignRight {
3✔
489
                        align = vips.AlignHigh
1✔
490
                        x = width
1✔
491
                } else if strings.HasPrefix(strings.TrimPrefix(args[1], "-"), "0.") {
3✔
492
                        pec, _ := strconv.ParseFloat(args[1], 64)
1✔
493
                        x = int(pec * float64(width))
1✔
494
                } else if strings.HasSuffix(args[1], "p") {
3✔
495
                        x, _ = strconv.Atoi(strings.TrimSuffix(args[1], "p"))
1✔
496
                        x = x * width / 100
1✔
497
                } else {
2✔
498
                        x, _ = strconv.Atoi(args[1])
1✔
499
                }
1✔
500
                if x < 0 {
2✔
501
                        align = vips.AlignHigh
1✔
502
                        x += width
1✔
503
                }
1✔
504
        }
505
        if ln > 2 {
2✔
506
                if args[2] == "center" {
2✔
507
                        y = (img.PageHeight() - size) / 2
1✔
508
                } else if args[2] == imagorpath.VAlignTop {
3✔
509
                        y = 0
1✔
510
                } else if args[2] == imagorpath.VAlignBottom {
3✔
511
                        y = img.PageHeight() - size
1✔
512
                } else if strings.HasPrefix(strings.TrimPrefix(args[2], "-"), "0.") {
3✔
513
                        pec, _ := strconv.ParseFloat(args[2], 64)
1✔
514
                        y = int(pec * float64(img.PageHeight()))
1✔
515
                } else if strings.HasSuffix(args[2], "p") {
3✔
516
                        y, _ = strconv.Atoi(strings.TrimSuffix(args[2], "p"))
1✔
517
                        y = y * img.PageHeight() / 100
1✔
518
                } else {
2✔
519
                        y, _ = strconv.Atoi(args[2])
1✔
520
                }
1✔
521
                if y < 0 {
2✔
522
                        y += img.PageHeight() - size
1✔
523
                }
1✔
524
        }
525
        if ln > 4 {
2✔
526
                c = getColor(img, args[4])
1✔
527
        }
1✔
528
        if ln > 5 {
2✔
529
                alpha, _ = strconv.ParseFloat(args[5], 64)
1✔
530
                alpha /= 100
1✔
531
        }
1✔
532
        if ln > 6 {
2✔
533
                if a, e := url.QueryUnescape(args[6]); e == nil {
2✔
534
                        font = a
1✔
535
                } else {
1✔
536
                        font = args[6]
×
537
                }
×
538
        }
539
        if img.Bands() < 3 {
2✔
540
                if err = img.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
541
                        return
×
542
                }
×
543
        }
544
        if !img.HasAlpha() {
2✔
545
                if err = img.Addalpha(); err != nil {
1✔
546
                        return
×
547
                }
×
548
        }
549
        return img.Label(text, x, y, &vips.LabelOptions{
1✔
550
                Font:    font,
1✔
551
                Size:    size,
1✔
552
                Align:   align,
1✔
553
                Opacity: 1 - alpha,
1✔
554
                Color:   c,
1✔
555
        })
1✔
556
}
557

558
func (v *Processor) padding(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
559
        ln := len(args)
1✔
560
        if ln < 2 {
2✔
561
                return nil
1✔
562
        }
1✔
563
        var (
1✔
564
                c       = args[0]
1✔
565
                left, _ = strconv.Atoi(args[1])
1✔
566
                top     = left
1✔
567
                right   = left
1✔
568
                bottom  = left
1✔
569
        )
1✔
570
        if ln > 2 {
2✔
571
                top, _ = strconv.Atoi(args[2])
1✔
572
                bottom = top
1✔
573
        }
1✔
574
        if ln > 4 {
2✔
575
                right, _ = strconv.Atoi(args[3])
1✔
576
                bottom, _ = strconv.Atoi(args[4])
1✔
577
        }
1✔
578
        return v.fill(ctx, img, img.Width(), img.PageHeight(), left, top, right, bottom, c)
1✔
579
}
580

581
func backgroundColor(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
582
        if len(args) == 0 {
2✔
583
                return
1✔
584
        }
1✔
585
        if !img.HasAlpha() {
2✔
586
                return
1✔
587
        }
1✔
588
        c := getColor(img, args[0])
1✔
589
        return img.Flatten(&vips.FlattenOptions{
1✔
590
                Background: c,
1✔
591
        })
1✔
592
}
593

594
func rotate(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
595
        if len(args) == 0 {
2✔
596
                return
1✔
597
        }
1✔
598
        if angle, _ := strconv.Atoi(args[0]); angle > 0 {
2✔
599
                switch angle {
1✔
600
                case 90, 270:
1✔
601
                        setRotate90(ctx)
1✔
602
                }
603
                if err = img.RotMultiPage(getAngle(angle)); err != nil {
1✔
604
                        return err
×
605
                }
×
606
        }
607
        return
1✔
608
}
609

610
func getAngle(angle int) vips.Angle {
1✔
611
        switch angle {
1✔
612
        case 90:
1✔
613
                return vips.AngleD270
1✔
614
        case 180:
1✔
615
                return vips.AngleD180
1✔
616
        case 270:
1✔
617
                return vips.AngleD90
1✔
618
        default:
×
619
                return vips.AngleD0
×
620
        }
621
}
622

623
func proportion(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
624
        if len(args) == 0 {
2✔
625
                return
1✔
626
        }
1✔
627
        scale, _ := strconv.ParseFloat(args[0], 64)
1✔
628
        if scale <= 0 {
2✔
629
                return // no ops
1✔
630
        }
1✔
631
        if scale > 100 {
2✔
632
                scale = 100
1✔
633
        }
1✔
634
        if scale > 1 {
2✔
635
                scale /= 100
1✔
636
        }
1✔
637
        width := int(float64(img.Width()) * scale)
1✔
638
        height := int(float64(img.PageHeight()) * scale)
1✔
639
        if width <= 0 || height <= 0 {
2✔
640
                return // op ops
1✔
641
        }
1✔
642
        return img.ThumbnailImage(width, &vips.ThumbnailImageOptions{
1✔
643
                Height: height,
1✔
644
                Crop:   vips.InterestingNone,
1✔
645
        })
1✔
646
}
647

648
func grayscale(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
649
        return img.Colourspace(vips.InterpretationBW, nil)
1✔
650
}
1✔
651

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

661
func contrast(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
662
        if len(args) == 0 {
2✔
663
                return
1✔
664
        }
1✔
665
        a, _ := strconv.ParseFloat(args[0], 64)
1✔
666
        a = a * 255 / 100
1✔
667
        a = math.Min(math.Max(a, -255), 255)
1✔
668
        a = (259 * (a + 255)) / (255 * (259 - a))
1✔
669
        b := 128 - a*128
1✔
670
        return linearRGB(img, []float64{a, a, a}, []float64{b, b, b})
1✔
671
}
672

673
func hue(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
674
        if len(args) == 0 {
2✔
675
                return
1✔
676
        }
1✔
677
        h, _ := strconv.ParseFloat(args[0], 64)
1✔
678
        return img.Modulate(1, 1, h)
1✔
679
}
680

681
func saturation(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
682
        if len(args) == 0 {
2✔
683
                return
1✔
684
        }
1✔
685
        s, _ := strconv.ParseFloat(args[0], 64)
1✔
686
        s = 1 + s/100
1✔
687
        return img.Modulate(1, s, 0)
1✔
688
}
689

690
func rgb(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
691
        if len(args) != 3 {
2✔
692
                return
1✔
693
        }
1✔
694
        r, _ := strconv.ParseFloat(args[0], 64)
1✔
695
        g, _ := strconv.ParseFloat(args[1], 64)
1✔
696
        b, _ := strconv.ParseFloat(args[2], 64)
1✔
697
        r = r * 255 / 100
1✔
698
        g = g * 255 / 100
1✔
699
        b = b * 255 / 100
1✔
700
        return linearRGB(img, []float64{1, 1, 1}, []float64{r, g, b})
1✔
701
}
702

703
func modulate(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
704
        if len(args) != 3 {
2✔
705
                return
1✔
706
        }
1✔
707
        b, _ := strconv.ParseFloat(args[0], 64)
1✔
708
        s, _ := strconv.ParseFloat(args[1], 64)
1✔
709
        h, _ := strconv.ParseFloat(args[2], 64)
1✔
710
        b = 1 + b/100
1✔
711
        s = 1 + s/100
1✔
712
        return img.Modulate(b, s, h)
1✔
713
}
714

715
func blur(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
716
        if isAnimated(img) {
2✔
717
                // skip animation support
1✔
718
                return
1✔
719
        }
1✔
720
        var sigma float64
1✔
721
        switch len(args) {
1✔
722
        case 2:
1✔
723
                sigma, _ = strconv.ParseFloat(args[1], 64)
1✔
724
                break
1✔
725
        case 1:
1✔
726
                sigma, _ = strconv.ParseFloat(args[0], 64)
1✔
727
                break
1✔
728
        }
729
        sigma /= 2
1✔
730
        if sigma > 0 {
2✔
731
                return img.Gaussblur(sigma, nil)
1✔
732
        }
1✔
733
        return
×
734
}
735

736
func sharpen(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
737
        if isAnimated(img) {
2✔
738
                // skip animation support
1✔
739
                return
1✔
740
        }
1✔
741
        var sigma float64
1✔
742
        switch len(args) {
1✔
743
        case 1:
1✔
744
                sigma, _ = strconv.ParseFloat(args[0], 64)
1✔
745
                break
1✔
746
        case 2, 3:
1✔
747
                sigma, _ = strconv.ParseFloat(args[1], 64)
1✔
748
                break
1✔
749
        }
750
        sigma = 1 + sigma*2
1✔
751
        if sigma > 0 {
2✔
752
                return img.Sharpen(&vips.SharpenOptions{
1✔
753
                        Sigma: sigma,
1✔
754
                        X1:    1,
1✔
755
                        M2:    2,
1✔
756
                })
1✔
757
        }
1✔
758
        return
1✔
759
}
760

761
func stripIcc(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
762
        if img.HasICCProfile() {
2✔
763
                opts := vips.DefaultIccTransformOptions()
1✔
764
                opts.Embedded = true
1✔
765
                opts.Intent = vips.IntentPerceptual
1✔
766
                if img.Interpretation() == vips.InterpretationRgb16 {
1✔
767
                        opts.Depth = 16
×
768
                }
×
769
                _ = img.IccTransform("srgb", opts)
1✔
770
        }
771
        return img.RemoveICCProfile()
1✔
772
}
773

774
func toColorspace(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
775
        profile := "srgb"
1✔
776
        if len(args) > 0 && args[0] != "" {
2✔
777
                profile = strings.ToLower(args[0])
1✔
778
        }
1✔
779
        if !img.HasICCProfile() {
2✔
780
                return nil
1✔
781
        }
1✔
782
        opts := vips.DefaultIccTransformOptions()
1✔
783
        opts.Embedded = true
1✔
784
        opts.Intent = vips.IntentPerceptual
1✔
785
        if img.Interpretation() == vips.InterpretationRgb16 {
1✔
786
                opts.Depth = 16
×
787
        }
×
788
        return img.IccTransform(profile, opts)
1✔
789
}
790

791
func stripExif(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
792
        return img.RemoveExif()
1✔
793
}
1✔
794

795
func trim(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
796
        var (
1✔
797
                ln        = len(args)
1✔
798
                pos       string
1✔
799
                tolerance int
1✔
800
        )
1✔
801
        if ln > 0 {
2✔
802
                tolerance, _ = strconv.Atoi(args[0])
1✔
803
        }
1✔
804
        if ln > 1 {
2✔
805
                pos = args[1]
1✔
806
        }
1✔
807
        if l, t, w, h, err := findTrim(ctx, img, pos, tolerance); err == nil {
2✔
808
                return img.ExtractAreaMultiPage(l, t, w, h)
1✔
809
        }
1✔
810
        return nil
×
811
}
812

813
func linearRGB(img *vips.Image, a, b []float64) error {
1✔
814
        if img.HasAlpha() {
2✔
815
                a = append(a, 1)
1✔
816
                b = append(b, 0)
1✔
817
        }
1✔
818
        return img.Linear(a, b, nil)
1✔
819
}
820

821
func isBlack(c []float64) bool {
1✔
822
        if len(c) < 3 {
1✔
823
                return false
×
824
        }
×
825
        return c[0] == 0x00 && c[1] == 0x00 && c[2] == 0x00
1✔
826
}
827

828
func isWhite(c []float64) bool {
1✔
829
        if len(c) < 3 {
1✔
830
                return false
×
831
        }
×
832
        return c[0] == 0xff && c[1] == 0xff && c[2] == 0xff
1✔
833
}
834

835
func getColor(img *vips.Image, color string) []float64 {
1✔
836
        var vc = make([]float64, 3)
1✔
837
        args := strings.Split(strings.ToLower(color), ",")
1✔
838
        mode := ""
1✔
839
        name := strings.TrimPrefix(args[0], "#")
1✔
840
        if len(args) > 1 {
2✔
841
                mode = args[1]
1✔
842
        }
1✔
843
        if name == "auto" {
2✔
844
                if img != nil {
2✔
845
                        x := 0
1✔
846
                        y := 0
1✔
847
                        if mode == "bottom-right" {
2✔
848
                                x = img.Width() - 1
1✔
849
                                y = img.PageHeight() - 1
1✔
850
                        }
1✔
851
                        p, _ := img.Getpoint(x, y, nil)
1✔
852
                        if len(p) >= 3 {
2✔
853
                                vc[0] = p[0]
1✔
854
                                vc[1] = p[1]
1✔
855
                                vc[2] = p[2]
1✔
856
                        }
1✔
857
                }
858
        } else if c, ok := colornames.Map[name]; ok {
2✔
859
                vc[0] = float64(c.R)
1✔
860
                vc[1] = float64(c.G)
1✔
861
                vc[2] = float64(c.B)
1✔
862
        } else if c, ok := parseHexColor(name); ok {
3✔
863
                vc[0] = float64(c.R)
1✔
864
                vc[1] = float64(c.G)
1✔
865
                vc[2] = float64(c.B)
1✔
866
        }
1✔
867
        return vc
1✔
868
}
869

870
func parseHexColor(s string) (c color.RGBA, ok bool) {
1✔
871
        c.A = 0xff
1✔
872
        switch len(s) {
1✔
873
        case 6:
1✔
874
                c.R = hexToByte(s[0])<<4 + hexToByte(s[1])
1✔
875
                c.G = hexToByte(s[2])<<4 + hexToByte(s[3])
1✔
876
                c.B = hexToByte(s[4])<<4 + hexToByte(s[5])
1✔
877
                ok = true
1✔
878
        case 3:
1✔
879
                c.R = hexToByte(s[0]) * 17
1✔
880
                c.G = hexToByte(s[1]) * 17
1✔
881
                c.B = hexToByte(s[2]) * 17
1✔
882
                ok = true
1✔
883
        }
884
        return
1✔
885
}
886

887
func hexToByte(b byte) byte {
1✔
888
        switch {
1✔
889
        case b >= '0' && b <= '9':
1✔
890
                return b - '0'
1✔
891
        case b >= 'a' && b <= 'f':
1✔
892
                return b - 'a' + 10
1✔
893
        }
894
        return 0
1✔
895
}
896

897
func crop(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
898
        if len(args) < 4 {
1✔
899
                return nil
×
900
        }
×
901

902
        // Parse arguments
903
        left, _ := strconv.ParseFloat(args[0], 64)
1✔
904
        top, _ := strconv.ParseFloat(args[1], 64)
1✔
905
        width, _ := strconv.ParseFloat(args[2], 64)
1✔
906
        height, _ := strconv.ParseFloat(args[3], 64)
1✔
907

1✔
908
        imgWidth := float64(img.Width())
1✔
909
        imgHeight := float64(img.PageHeight())
1✔
910

1✔
911
        // Convert relative (0-1) to absolute pixels
1✔
912
        if left > 0 && left < 1 {
2✔
913
                left = left * imgWidth
1✔
914
        }
1✔
915
        if top > 0 && top < 1 {
2✔
916
                top = top * imgHeight
1✔
917
        }
1✔
918
        if width > 0 && width < 1 {
2✔
919
                width = width * imgWidth
1✔
920
        }
1✔
921
        if height > 0 && height < 1 {
2✔
922
                height = height * imgHeight
1✔
923
        }
1✔
924

925
        // Clamp left and top to image bounds
926
        left = math.Max(0, math.Min(left, imgWidth))
1✔
927
        top = math.Max(0, math.Min(top, imgHeight))
1✔
928

1✔
929
        // Adjust width and height to not exceed image bounds
1✔
930
        width = math.Min(width, imgWidth-left)
1✔
931
        height = math.Min(height, imgHeight-top)
1✔
932

1✔
933
        // Skip if invalid crop area
1✔
934
        if width <= 0 || height <= 0 {
1✔
935
                return nil
×
936
        }
×
937

938
        return img.ExtractAreaMultiPage(int(left), int(top), int(width), int(height))
1✔
939
}
940

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