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

cshum / imagor / 21639592645

03 Feb 2026 05:00PM UTC coverage: 91.446% (-0.3%) from 91.772%
21639592645

Pull #722

github

cshum
feat: image filter
Pull Request #722: feat: image filter

104 of 143 new or added lines in 4 files covered. (72.73%)

60 existing lines in 1 file now uncovered.

5709 of 6243 relevant lines covered (91.45%)

1.09 hits per line

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

85.2
/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
func (v *Processor) image(ctx context.Context, img *vips.Image, load imagor.LoadFunc, args ...string) (err error) {
1✔
21
        ln := len(args)
1✔
22
        if ln < 1 {
1✔
NEW
23
                return
×
NEW
24
        }
×
25
        imagorPath := args[0]
1✔
26
        if unescape, e := url.QueryUnescape(args[0]); e == nil {
2✔
27
                imagorPath = unescape
1✔
28
        }
1✔
29
        params := imagorpath.Parse(imagorPath)
1✔
30
        var blob *imagor.Blob
1✔
31
        if blob, err = load(params.Image); err != nil {
1✔
NEW
32
                return
×
NEW
33
        }
×
34
        var overlay *vips.Image
1✔
35
        if overlay, err = v.loadAndProcess(ctx, blob, params, load); err != nil || overlay == nil {
1✔
NEW
36
                return
×
NEW
37
        }
×
38
        contextDefer(ctx, overlay.Close)
1✔
39

1✔
40
        // Ensure overlay has proper color space and alpha
1✔
41
        if overlay.Bands() < 3 {
1✔
NEW
42
                if err = overlay.Colourspace(vips.InterpretationSrgb, nil); err != nil {
×
NEW
43
                        return
×
NEW
44
                }
×
45
        }
46
        if !overlay.HasAlpha() {
1✔
NEW
47
                if err = overlay.Addalpha(); err != nil {
×
NEW
48
                        return
×
NEW
49
                }
×
50
        }
51

52
        var x, y int
1✔
53
        var across = 1
1✔
54
        var down = 1
1✔
55
        w := overlay.Width()
1✔
56
        h := overlay.PageHeight()
1✔
57

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

69
        // Parse x position (default to 0 if not provided)
70
        if ln >= 2 && args[1] != "" {
2✔
71
                if args[1] == "center" {
2✔
72
                        x = (img.Width() - overlay.Width()) / 2
1✔
73
                } else if args[1] == imagorpath.HAlignLeft {
2✔
NEW
74
                        x = 0
×
75
                } else if args[1] == imagorpath.HAlignRight {
1✔
NEW
76
                        x = img.Width() - overlay.Width()
×
77
                } else if args[1] == "repeat" {
2✔
78
                        x = 0
1✔
79
                        across = img.Width()/overlay.Width() + 1
1✔
80
                } else if strings.HasPrefix(strings.TrimPrefix(args[1], "-"), "0.") {
2✔
NEW
81
                        pec, _ := strconv.ParseFloat(args[1], 64)
×
NEW
82
                        x = int(pec * float64(img.Width()))
×
83
                } else if strings.HasSuffix(args[1], "p") {
1✔
NEW
84
                        x, _ = strconv.Atoi(strings.TrimSuffix(args[1], "p"))
×
NEW
85
                        x = x * img.Width() / 100
×
86
                } else {
1✔
87
                        x, _ = strconv.Atoi(args[1])
1✔
88
                }
1✔
89
        }
90

91
        // Parse y position (default to 0 if not provided)
92
        if ln >= 3 && args[2] != "" {
2✔
93
                if args[2] == "center" {
2✔
94
                        y = (img.PageHeight() - overlay.PageHeight()) / 2
1✔
95
                } else if args[2] == imagorpath.VAlignTop {
2✔
NEW
96
                        y = 0
×
97
                } else if args[2] == imagorpath.VAlignBottom {
1✔
NEW
98
                        y = img.PageHeight() - overlay.PageHeight()
×
99
                } else if args[2] == "repeat" {
2✔
100
                        y = 0
1✔
101
                        down = img.PageHeight()/overlay.PageHeight() + 1
1✔
102
                } else if strings.HasPrefix(strings.TrimPrefix(args[2], "-"), "0.") {
2✔
NEW
103
                        pec, _ := strconv.ParseFloat(args[2], 64)
×
NEW
104
                        y = int(pec * float64(img.PageHeight()))
×
105
                } else if strings.HasSuffix(args[2], "p") {
1✔
NEW
106
                        y, _ = strconv.Atoi(strings.TrimSuffix(args[2], "p"))
×
NEW
107
                        y = y * img.PageHeight() / 100
×
108
                } else {
1✔
109
                        y, _ = strconv.Atoi(args[2])
1✔
110
                }
1✔
111
        }
112

113
        // Handle negative positioning (from opposite edge)
114
        if x < 0 {
2✔
115
                x += img.Width() - overlay.Width()
1✔
116
        }
1✔
117
        if y < 0 {
2✔
118
                y += img.PageHeight() - overlay.PageHeight()
1✔
119
        }
1✔
120

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

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

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

148
        // Composite onto main image
149
        if err = img.Composite2(overlay, vips.BlendModeOver, nil); err != nil {
1✔
NEW
150
                return
×
NEW
151
        }
×
152
        return
1✔
153
}
154

155
func (v *Processor) watermark(ctx context.Context, img *vips.Image, load imagor.LoadFunc, args ...string) (err error) {
1✔
156
        ln := len(args)
1✔
157
        if ln < 1 {
2✔
158
                return
1✔
159
        }
1✔
160
        image := args[0]
1✔
161

1✔
162
        if unescape, e := url.QueryUnescape(args[0]); e == nil {
2✔
163
                image = unescape
1✔
164
        }
1✔
165

166
        if strings.HasPrefix(image, "b64:") {
2✔
167
                // if image URL starts with b64: prefix, Base64 decode it according to "base64url" in RFC 4648 (Section 5).
1✔
168
                result := make([]byte, base64.RawURLEncoding.DecodedLen(len(image[4:])))
1✔
169
                // in case decoding fails, use original image URL (possible that filename starts with b64: prefix, but as part of the file name)
1✔
170
                if _, e := base64.RawURLEncoding.Decode(result, []byte(image[4:])); e == nil {
2✔
171
                        image = string(result)
1✔
172
                }
1✔
173
        }
174

175
        var blob *imagor.Blob
1✔
176
        if blob, err = load(image); err != nil {
1✔
177
                return
×
178
        }
×
179
        var x, y, w, h int
1✔
180
        var across = 1
1✔
181
        var down = 1
1✔
182
        var overlay *vips.Image
1✔
183
        var n = 1
1✔
184
        if isAnimated(img) {
2✔
185
                n = -1
1✔
186
        }
1✔
187
        // w_ratio h_ratio
188
        if ln >= 6 {
2✔
189
                w = img.Width()
1✔
190
                h = img.PageHeight()
1✔
191
                if args[4] != "none" {
2✔
192
                        w, _ = strconv.Atoi(args[4])
1✔
193
                        w = img.Width() * w / 100
1✔
194
                }
1✔
195
                if args[5] != "none" {
2✔
196
                        h, _ = strconv.Atoi(args[5])
1✔
197
                        h = img.PageHeight() * h / 100
1✔
198
                }
1✔
199
                if overlay, err = v.NewThumbnail(
1✔
200
                        ctx, blob, w, h, vips.InterestingNone, vips.SizeBoth, n, 1, 0,
1✔
201
                ); err != nil {
1✔
202
                        return
×
203
                }
×
204
        } else {
1✔
205
                if overlay, err = v.NewThumbnail(
1✔
206
                        ctx, blob, v.MaxWidth, v.MaxHeight, vips.InterestingNone, vips.SizeDown, n, 1, 0,
1✔
207
                ); err != nil {
1✔
208
                        return
×
209
                }
×
210
        }
211
        var overlayN = overlay.Height() / overlay.PageHeight()
1✔
212
        contextDefer(ctx, overlay.Close)
1✔
213
        if overlay.Bands() < 3 {
2✔
214
                if err = overlay.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
UNCOV
215
                        return
×
UNCOV
216
                }
×
217
        }
218
        if !overlay.HasAlpha() {
2✔
219
                if err = overlay.Addalpha(); err != nil {
1✔
UNCOV
220
                        return
×
UNCOV
221
                }
×
222
        }
223
        w = overlay.Width()
1✔
224
        h = overlay.PageHeight()
1✔
225
        // alpha
1✔
226
        if ln >= 4 {
2✔
227
                alpha, _ := strconv.ParseFloat(args[3], 64)
1✔
228
                alpha = 1 - alpha/100
1✔
229
                if alpha != 1 {
2✔
230
                        if err = overlay.Linear([]float64{1, 1, 1, alpha}, []float64{0, 0, 0, 0}, nil); err != nil {
1✔
UNCOV
231
                                return
×
UNCOV
232
                        }
×
233
                }
234
        }
235
        // x y
236
        if ln >= 3 {
2✔
237
                if args[1] == "center" {
2✔
238
                        x = (img.Width() - overlay.Width()) / 2
1✔
239
                } else if args[1] == imagorpath.HAlignLeft {
3✔
240
                        x = 0
1✔
241
                } else if args[1] == imagorpath.HAlignRight {
3✔
242
                        x = img.Width() - overlay.Width()
1✔
243
                } else if args[1] == "repeat" {
3✔
244
                        x = 0
1✔
245
                        across = img.Width()/overlay.Width() + 1
1✔
246
                } else if strings.HasPrefix(strings.TrimPrefix(args[1], "-"), "0.") {
3✔
247
                        pec, _ := strconv.ParseFloat(args[1], 64)
1✔
248
                        x = int(pec * float64(img.Width()))
1✔
249
                } else if strings.HasSuffix(args[1], "p") {
3✔
250
                        x, _ = strconv.Atoi(strings.TrimSuffix(args[1], "p"))
1✔
251
                        x = x * img.Width() / 100
1✔
252
                } else {
2✔
253
                        x, _ = strconv.Atoi(args[1])
1✔
254
                }
1✔
255
                if args[2] == "center" {
2✔
256
                        y = (img.PageHeight() - overlay.PageHeight()) / 2
1✔
257
                } else if args[2] == imagorpath.VAlignTop {
3✔
258
                        y = 0
1✔
259
                } else if args[2] == imagorpath.VAlignBottom {
3✔
260
                        y = img.PageHeight() - overlay.PageHeight()
1✔
261
                } else if args[2] == "repeat" {
3✔
262
                        y = 0
1✔
263
                        down = img.PageHeight()/overlay.PageHeight() + 1
1✔
264
                } else if strings.HasPrefix(strings.TrimPrefix(args[2], "-"), "0.") {
3✔
265
                        pec, _ := strconv.ParseFloat(args[2], 64)
1✔
266
                        y = int(pec * float64(img.PageHeight()))
1✔
267
                } else if strings.HasSuffix(args[2], "p") {
3✔
268
                        y, _ = strconv.Atoi(strings.TrimSuffix(args[2], "p"))
1✔
269
                        y = y * img.PageHeight() / 100
1✔
270
                } else {
2✔
271
                        y, _ = strconv.Atoi(args[2])
1✔
272
                }
1✔
273
                if x < 0 {
2✔
274
                        x += img.Width() - overlay.Width()
1✔
275
                }
1✔
276
                if y < 0 {
2✔
277
                        y += img.PageHeight() - overlay.PageHeight()
1✔
278
                }
1✔
279
        }
280
        if across*down > 1 {
2✔
281
                if err = overlay.EmbedMultiPage(0, 0, across*w, down*h,
1✔
282
                        &vips.EmbedMultiPageOptions{Extend: vips.ExtendRepeat}); err != nil {
1✔
283
                        return
×
284
                }
×
285
        }
286
        if err = overlay.EmbedMultiPage(
1✔
287
                x, y, img.Width(), img.PageHeight(), nil,
1✔
288
        ); err != nil {
1✔
UNCOV
289
                return
×
UNCOV
290
        }
×
291
        if n := img.Height() / img.PageHeight(); n > overlayN {
2✔
292
                cnt := n / overlayN
1✔
293
                if n%overlayN > 0 {
2✔
294
                        cnt++
1✔
295
                }
1✔
296
                if err = overlay.Replicate(1, cnt); err != nil {
1✔
UNCOV
297
                        return
×
UNCOV
298
                }
×
299
        }
300
        if err = img.Composite2(overlay, vips.BlendModeOver, nil); err != nil {
1✔
UNCOV
301
                return
×
UNCOV
302
        }
×
303
        return
1✔
304
}
305

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

390
func roundCorner(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
391
        var rx, ry int
1✔
392
        var c []float64
1✔
393
        if len(args) == 0 {
2✔
394
                return
1✔
395
        }
1✔
396
        if a, e := url.QueryUnescape(args[0]); e == nil {
2✔
397
                args[0] = a
1✔
398
        }
1✔
399
        if len(args) == 3 {
2✔
400
                // rx,ry,color
1✔
401
                c = getColor(img, args[2])
1✔
402
                args = args[:2]
1✔
403
        }
1✔
404
        rx, _ = strconv.Atoi(args[0])
1✔
405
        ry = rx
1✔
406
        if len(args) > 1 {
2✔
407
                ry, _ = strconv.Atoi(args[1])
1✔
408
        }
1✔
409

410
        var rounded *vips.Image
1✔
411
        var w = img.Width()
1✔
412
        var h = img.PageHeight()
1✔
413
        if rounded, err = vips.NewSvgloadBuffer([]byte(fmt.Sprintf(`
1✔
414
                <svg viewBox="0 0 %d %d">
1✔
415
                        <rect rx="%d" ry="%d" 
1✔
416
                         x="0" y="0" width="%d" height="%d" 
1✔
417
                         fill="#fff"/>
1✔
418
                </svg>
1✔
419
        `, w, h, rx, ry, w, h)), nil); err != nil {
1✔
UNCOV
420
                return
×
UNCOV
421
        }
×
422
        contextDefer(ctx, rounded.Close)
1✔
423
        if n := img.Height() / img.PageHeight(); n > 1 {
2✔
424
                if err = rounded.Replicate(1, n); err != nil {
1✔
425
                        return
×
426
                }
×
427
        }
428
        if err = img.Composite2(rounded, vips.BlendModeDestIn, nil); err != nil {
1✔
UNCOV
429
                return
×
UNCOV
430
        }
×
431
        if c != nil {
2✔
432
                if err = img.Flatten(&vips.FlattenOptions{Background: c}); err != nil {
1✔
UNCOV
433
                        return
×
UNCOV
434
                }
×
435
        }
436
        return nil
1✔
437
}
438

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

532
func (v *Processor) padding(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
533
        ln := len(args)
1✔
534
        if ln < 2 {
2✔
535
                return nil
1✔
536
        }
1✔
537
        var (
1✔
538
                c       = args[0]
1✔
539
                left, _ = strconv.Atoi(args[1])
1✔
540
                top     = left
1✔
541
                right   = left
1✔
542
                bottom  = left
1✔
543
        )
1✔
544
        if ln > 2 {
2✔
545
                top, _ = strconv.Atoi(args[2])
1✔
546
                bottom = top
1✔
547
        }
1✔
548
        if ln > 4 {
2✔
549
                right, _ = strconv.Atoi(args[3])
1✔
550
                bottom, _ = strconv.Atoi(args[4])
1✔
551
        }
1✔
552
        return v.fill(ctx, img, img.Width(), img.PageHeight(), left, top, right, bottom, c)
1✔
553
}
554

555
func backgroundColor(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
556
        if len(args) == 0 {
2✔
557
                return
1✔
558
        }
1✔
559
        if !img.HasAlpha() {
2✔
560
                return
1✔
561
        }
1✔
562
        c := getColor(img, args[0])
1✔
563
        return img.Flatten(&vips.FlattenOptions{
1✔
564
                Background: c,
1✔
565
        })
1✔
566
}
567

568
func rotate(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
569
        if len(args) == 0 {
2✔
570
                return
1✔
571
        }
1✔
572
        if angle, _ := strconv.Atoi(args[0]); angle > 0 {
2✔
573
                switch angle {
1✔
574
                case 90, 270:
1✔
575
                        setRotate90(ctx)
1✔
576
                }
577
                if err = img.RotMultiPage(getAngle(angle)); err != nil {
1✔
UNCOV
578
                        return err
×
UNCOV
579
                }
×
580
        }
581
        return
1✔
582
}
583

584
func getAngle(angle int) vips.Angle {
1✔
585
        switch angle {
1✔
586
        case 90:
1✔
587
                return vips.AngleD270
1✔
588
        case 180:
1✔
589
                return vips.AngleD180
1✔
590
        case 270:
1✔
591
                return vips.AngleD90
1✔
UNCOV
592
        default:
×
UNCOV
593
                return vips.AngleD0
×
594
        }
595
}
596

597
func proportion(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
598
        if len(args) == 0 {
2✔
599
                return
1✔
600
        }
1✔
601
        scale, _ := strconv.ParseFloat(args[0], 64)
1✔
602
        if scale <= 0 {
2✔
603
                return // no ops
1✔
604
        }
1✔
605
        if scale > 100 {
2✔
606
                scale = 100
1✔
607
        }
1✔
608
        if scale > 1 {
2✔
609
                scale /= 100
1✔
610
        }
1✔
611
        width := int(float64(img.Width()) * scale)
1✔
612
        height := int(float64(img.PageHeight()) * scale)
1✔
613
        if width <= 0 || height <= 0 {
2✔
614
                return // op ops
1✔
615
        }
1✔
616
        return img.ThumbnailImage(width, &vips.ThumbnailImageOptions{
1✔
617
                Height: height,
1✔
618
                Crop:   vips.InterestingNone,
1✔
619
        })
1✔
620
}
621

622
func grayscale(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
623
        return img.Colourspace(vips.InterpretationBW, nil)
1✔
624
}
1✔
625

626
func brightness(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
627
        if len(args) == 0 {
2✔
628
                return
1✔
629
        }
1✔
630
        b, _ := strconv.ParseFloat(args[0], 64)
1✔
631
        b = b * 255 / 100
1✔
632
        return linearRGB(img, []float64{1, 1, 1}, []float64{b, b, b})
1✔
633
}
634

635
func contrast(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
636
        if len(args) == 0 {
2✔
637
                return
1✔
638
        }
1✔
639
        a, _ := strconv.ParseFloat(args[0], 64)
1✔
640
        a = a * 255 / 100
1✔
641
        a = math.Min(math.Max(a, -255), 255)
1✔
642
        a = (259 * (a + 255)) / (255 * (259 - a))
1✔
643
        b := 128 - a*128
1✔
644
        return linearRGB(img, []float64{a, a, a}, []float64{b, b, b})
1✔
645
}
646

647
func hue(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
648
        if len(args) == 0 {
2✔
649
                return
1✔
650
        }
1✔
651
        h, _ := strconv.ParseFloat(args[0], 64)
1✔
652
        return img.Modulate(1, 1, h)
1✔
653
}
654

655
func saturation(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
656
        if len(args) == 0 {
2✔
657
                return
1✔
658
        }
1✔
659
        s, _ := strconv.ParseFloat(args[0], 64)
1✔
660
        s = 1 + s/100
1✔
661
        return img.Modulate(1, s, 0)
1✔
662
}
663

664
func rgb(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
665
        if len(args) != 3 {
2✔
666
                return
1✔
667
        }
1✔
668
        r, _ := strconv.ParseFloat(args[0], 64)
1✔
669
        g, _ := strconv.ParseFloat(args[1], 64)
1✔
670
        b, _ := strconv.ParseFloat(args[2], 64)
1✔
671
        r = r * 255 / 100
1✔
672
        g = g * 255 / 100
1✔
673
        b = b * 255 / 100
1✔
674
        return linearRGB(img, []float64{1, 1, 1}, []float64{r, g, b})
1✔
675
}
676

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

689
func blur(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
690
        if isAnimated(img) {
2✔
691
                // skip animation support
1✔
692
                return
1✔
693
        }
1✔
694
        var sigma float64
1✔
695
        switch len(args) {
1✔
696
        case 2:
1✔
697
                sigma, _ = strconv.ParseFloat(args[1], 64)
1✔
698
                break
1✔
699
        case 1:
1✔
700
                sigma, _ = strconv.ParseFloat(args[0], 64)
1✔
701
                break
1✔
702
        }
703
        sigma /= 2
1✔
704
        if sigma > 0 {
2✔
705
                return img.Gaussblur(sigma, nil)
1✔
706
        }
1✔
UNCOV
707
        return
×
708
}
709

710
func sharpen(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
711
        if isAnimated(img) {
2✔
712
                // skip animation support
1✔
713
                return
1✔
714
        }
1✔
715
        var sigma float64
1✔
716
        switch len(args) {
1✔
717
        case 1:
1✔
718
                sigma, _ = strconv.ParseFloat(args[0], 64)
1✔
719
                break
1✔
720
        case 2, 3:
1✔
721
                sigma, _ = strconv.ParseFloat(args[1], 64)
1✔
722
                break
1✔
723
        }
724
        sigma = 1 + sigma*2
1✔
725
        if sigma > 0 {
2✔
726
                return img.Sharpen(&vips.SharpenOptions{
1✔
727
                        Sigma: sigma,
1✔
728
                        X1:    1,
1✔
729
                        M2:    2,
1✔
730
                })
1✔
731
        }
1✔
732
        return
1✔
733
}
734

735
func stripIcc(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
736
        if img.HasICCProfile() {
2✔
737
                opts := vips.DefaultIccTransformOptions()
1✔
738
                opts.Embedded = true
1✔
739
                opts.Intent = vips.IntentPerceptual
1✔
740
                if img.Interpretation() == vips.InterpretationRgb16 {
1✔
UNCOV
741
                        opts.Depth = 16
×
UNCOV
742
                }
×
743
                _ = img.IccTransform("srgb", opts)
1✔
744
        }
745
        return img.RemoveICCProfile()
1✔
746
}
747

748
func toColorspace(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) (err error) {
1✔
749
        profile := "srgb"
1✔
750
        if len(args) > 0 && args[0] != "" {
2✔
751
                profile = strings.ToLower(args[0])
1✔
752
        }
1✔
753
        if !img.HasICCProfile() {
2✔
754
                return nil
1✔
755
        }
1✔
756
        opts := vips.DefaultIccTransformOptions()
1✔
757
        opts.Embedded = true
1✔
758
        opts.Intent = vips.IntentPerceptual
1✔
759
        if img.Interpretation() == vips.InterpretationRgb16 {
1✔
UNCOV
760
                opts.Depth = 16
×
UNCOV
761
        }
×
762
        return img.IccTransform(profile, opts)
1✔
763
}
764

765
func stripExif(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
766
        return img.RemoveExif()
1✔
767
}
1✔
768

769
func trim(ctx context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
770
        var (
1✔
771
                ln        = len(args)
1✔
772
                pos       string
1✔
773
                tolerance int
1✔
774
        )
1✔
775
        if ln > 0 {
2✔
776
                tolerance, _ = strconv.Atoi(args[0])
1✔
777
        }
1✔
778
        if ln > 1 {
2✔
779
                pos = args[1]
1✔
780
        }
1✔
781
        if l, t, w, h, err := findTrim(ctx, img, pos, tolerance); err == nil {
2✔
782
                return img.ExtractAreaMultiPage(l, t, w, h)
1✔
783
        }
1✔
UNCOV
784
        return nil
×
785
}
786

787
func linearRGB(img *vips.Image, a, b []float64) error {
1✔
788
        if img.HasAlpha() {
2✔
789
                a = append(a, 1)
1✔
790
                b = append(b, 0)
1✔
791
        }
1✔
792
        return img.Linear(a, b, nil)
1✔
793
}
794

795
func isBlack(c []float64) bool {
1✔
796
        if len(c) < 3 {
1✔
UNCOV
797
                return false
×
UNCOV
798
        }
×
799
        return c[0] == 0x00 && c[1] == 0x00 && c[2] == 0x00
1✔
800
}
801

802
func isWhite(c []float64) bool {
1✔
803
        if len(c) < 3 {
1✔
UNCOV
804
                return false
×
UNCOV
805
        }
×
806
        return c[0] == 0xff && c[1] == 0xff && c[2] == 0xff
1✔
807
}
808

809
func getColor(img *vips.Image, color string) []float64 {
1✔
810
        var vc = make([]float64, 3)
1✔
811
        args := strings.Split(strings.ToLower(color), ",")
1✔
812
        mode := ""
1✔
813
        name := strings.TrimPrefix(args[0], "#")
1✔
814
        if len(args) > 1 {
2✔
815
                mode = args[1]
1✔
816
        }
1✔
817
        if name == "auto" {
2✔
818
                if img != nil {
2✔
819
                        x := 0
1✔
820
                        y := 0
1✔
821
                        if mode == "bottom-right" {
2✔
822
                                x = img.Width() - 1
1✔
823
                                y = img.PageHeight() - 1
1✔
824
                        }
1✔
825
                        p, _ := img.Getpoint(x, y, nil)
1✔
826
                        if len(p) >= 3 {
2✔
827
                                vc[0] = p[0]
1✔
828
                                vc[1] = p[1]
1✔
829
                                vc[2] = p[2]
1✔
830
                        }
1✔
831
                }
832
        } else if c, ok := colornames.Map[name]; ok {
2✔
833
                vc[0] = float64(c.R)
1✔
834
                vc[1] = float64(c.G)
1✔
835
                vc[2] = float64(c.B)
1✔
836
        } else if c, ok := parseHexColor(name); ok {
3✔
837
                vc[0] = float64(c.R)
1✔
838
                vc[1] = float64(c.G)
1✔
839
                vc[2] = float64(c.B)
1✔
840
        }
1✔
841
        return vc
1✔
842
}
843

844
func parseHexColor(s string) (c color.RGBA, ok bool) {
1✔
845
        c.A = 0xff
1✔
846
        switch len(s) {
1✔
847
        case 6:
1✔
848
                c.R = hexToByte(s[0])<<4 + hexToByte(s[1])
1✔
849
                c.G = hexToByte(s[2])<<4 + hexToByte(s[3])
1✔
850
                c.B = hexToByte(s[4])<<4 + hexToByte(s[5])
1✔
851
                ok = true
1✔
852
        case 3:
1✔
853
                c.R = hexToByte(s[0]) * 17
1✔
854
                c.G = hexToByte(s[1]) * 17
1✔
855
                c.B = hexToByte(s[2]) * 17
1✔
856
                ok = true
1✔
857
        }
858
        return
1✔
859
}
860

861
func hexToByte(b byte) byte {
1✔
862
        switch {
1✔
863
        case b >= '0' && b <= '9':
1✔
864
                return b - '0'
1✔
865
        case b >= 'a' && b <= 'f':
1✔
866
                return b - 'a' + 10
1✔
867
        }
868
        return 0
1✔
869
}
870

871
func crop(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
872
        if len(args) < 4 {
1✔
UNCOV
873
                return nil
×
UNCOV
874
        }
×
875

876
        // Parse arguments
877
        left, _ := strconv.ParseFloat(args[0], 64)
1✔
878
        top, _ := strconv.ParseFloat(args[1], 64)
1✔
879
        width, _ := strconv.ParseFloat(args[2], 64)
1✔
880
        height, _ := strconv.ParseFloat(args[3], 64)
1✔
881

1✔
882
        imgWidth := float64(img.Width())
1✔
883
        imgHeight := float64(img.PageHeight())
1✔
884

1✔
885
        // Convert relative (0-1) to absolute pixels
1✔
886
        if left > 0 && left < 1 {
2✔
887
                left = left * imgWidth
1✔
888
        }
1✔
889
        if top > 0 && top < 1 {
2✔
890
                top = top * imgHeight
1✔
891
        }
1✔
892
        if width > 0 && width < 1 {
2✔
893
                width = width * imgWidth
1✔
894
        }
1✔
895
        if height > 0 && height < 1 {
2✔
896
                height = height * imgHeight
1✔
897
        }
1✔
898

899
        // Clamp left and top to image bounds
900
        left = math.Max(0, math.Min(left, imgWidth))
1✔
901
        top = math.Max(0, math.Min(top, imgHeight))
1✔
902

1✔
903
        // Adjust width and height to not exceed image bounds
1✔
904
        width = math.Min(width, imgWidth-left)
1✔
905
        height = math.Min(height, imgHeight-top)
1✔
906

1✔
907
        // Skip if invalid crop area
1✔
908
        if width <= 0 || height <= 0 {
1✔
UNCOV
909
                return nil
×
UNCOV
910
        }
×
911

912
        return img.ExtractAreaMultiPage(int(left), int(top), int(width), int(height))
1✔
913
}
914

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