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

cshum / imagor / 21662332045

04 Feb 2026 07:19AM UTC coverage: 91.878% (+0.1%) from 91.772%
21662332045

Pull #722

github

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

190 of 212 new or added lines in 4 files covered. (89.62%)

54 existing lines in 1 file now uncovered.

5724 of 6230 relevant lines covered (91.88%)

1.1 hits per line

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

88.74
/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
// prepareOverlay prepares an overlay image for compositing
21
// Handles color space, alpha channel, positioning, repeat patterns, and animation frames
22
// Returns the prepared overlay ready for compositing with Composite2()
23
func prepareOverlay(img *vips.Image, overlay *vips.Image, xArg, yArg string, alpha float64) error {
1✔
24
        // Ensure overlay has proper color space and alpha
1✔
25
        if overlay.Bands() < 3 {
2✔
26
                if err := overlay.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
NEW
27
                        return err
×
NEW
28
                }
×
29
        }
30
        if !overlay.HasAlpha() {
2✔
31
                if err := overlay.Addalpha(); err != nil {
1✔
NEW
32
                        return err
×
NEW
33
                }
×
34
        }
35

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

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

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

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

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

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

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

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

131
        return nil
1✔
132
}
133

134
// getBlendMode returns the vips.BlendMode for a given mode string
135
// Defaults to BlendModeOver (normal) if mode is empty or invalid
136
func getBlendMode(mode string) vips.BlendMode {
1✔
137
        if mode == "" {
1✔
NEW
138
                return vips.BlendModeOver
×
NEW
139
        }
×
140

141
        blendModeMap := map[string]vips.BlendMode{
1✔
142
                // Default
1✔
143
                "normal": vips.BlendModeOver,
1✔
144

1✔
145
                // Darken group
1✔
146
                "multiply":   vips.BlendModeMultiply,
1✔
147
                "color-burn": vips.BlendModeColourBurn,
1✔
148
                "darken":     vips.BlendModeDarken,
1✔
149

1✔
150
                // Lighten group
1✔
151
                "screen":      vips.BlendModeScreen,
1✔
152
                "color-dodge": vips.BlendModeColourDodge,
1✔
153
                "lighten":     vips.BlendModeLighten,
1✔
154
                "add":         vips.BlendModeAdd,
1✔
155

1✔
156
                // Contrast group
1✔
157
                "overlay":    vips.BlendModeOverlay,
1✔
158
                "soft-light": vips.BlendModeSoftLight,
1✔
159
                "hard-light": vips.BlendModeHardLight,
1✔
160

1✔
161
                // Inversion group
1✔
162
                "difference": vips.BlendModeDifference,
1✔
163
                "exclusion":  vips.BlendModeExclusion,
1✔
164

1✔
165
                // Masking
1✔
166
                "mask":     vips.BlendModeDestIn,
1✔
167
                "mask-out": vips.BlendModeDestOut,
1✔
168
        }
1✔
169

1✔
170
        if blendMode, ok := blendModeMap[strings.ToLower(mode)]; ok {
2✔
171
                return blendMode
1✔
172
        }
1✔
173

174
        // Default to normal if invalid mode
175
        return vips.BlendModeOver
1✔
176
}
177

178
func (v *Processor) image(ctx context.Context, img *vips.Image, load imagor.LoadFunc, args ...string) (err error) {
1✔
179
        ln := len(args)
1✔
180
        if ln < 1 {
1✔
NEW
181
                return
×
NEW
182
        }
×
183
        imagorPath := args[0]
1✔
184
        if unescape, e := url.QueryUnescape(args[0]); e == nil {
2✔
185
                imagorPath = unescape
1✔
186
        }
1✔
187
        params := imagorpath.Parse(imagorPath)
1✔
188
        var blob *imagor.Blob
1✔
189
        if blob, err = load(params.Image); err != nil {
1✔
NEW
190
                return
×
NEW
191
        }
×
192
        var overlay *vips.Image
1✔
193
        if overlay, err = v.loadAndProcess(ctx, blob, params, load); err != nil || overlay == nil {
1✔
NEW
194
                return
×
NEW
195
        }
×
196
        contextDefer(ctx, overlay.Close)
1✔
197

1✔
198
        var xArg, yArg string
1✔
199
        var alpha float64
1✔
200
        var blendMode = vips.BlendModeOver // default to normal
1✔
201

1✔
202
        if ln >= 2 {
2✔
203
                xArg = args[1]
1✔
204
        }
1✔
205
        if ln >= 3 {
2✔
206
                yArg = args[2]
1✔
207
        }
1✔
208
        if ln >= 4 {
2✔
209
                alpha, _ = strconv.ParseFloat(args[3], 64)
1✔
210
        }
1✔
211
        if ln >= 5 {
2✔
212
                // Parse blend mode (5th parameter)
1✔
213
                blendMode = getBlendMode(args[4])
1✔
214
        }
1✔
215

216
        // Prepare overlay for compositing
217
        if err = prepareOverlay(img, overlay, xArg, yArg, alpha); err != nil {
1✔
NEW
UNCOV
218
                return
×
NEW
UNCOV
219
        }
×
220

221
        // Composite overlay onto image with specified blend mode
222
        return img.Composite2(overlay, blendMode, nil)
1✔
223
}
224

225
func (v *Processor) watermark(ctx context.Context, img *vips.Image, load imagor.LoadFunc, args ...string) (err error) {
1✔
226
        ln := len(args)
1✔
227
        if ln < 1 {
2✔
228
                return
1✔
229
        }
1✔
230
        image := args[0]
1✔
231

1✔
232
        if unescape, e := url.QueryUnescape(args[0]); e == nil {
2✔
233
                image = unescape
1✔
234
        }
1✔
235

236
        if strings.HasPrefix(image, "b64:") {
2✔
237
                // if image URL starts with b64: prefix, Base64 decode it according to "base64url" in RFC 4648 (Section 5).
1✔
238
                result := make([]byte, base64.RawURLEncoding.DecodedLen(len(image[4:])))
1✔
239
                // in case decoding fails, use original image URL (possible that filename starts with b64: prefix, but as part of the file name)
1✔
240
                if _, e := base64.RawURLEncoding.Decode(result, []byte(image[4:])); e == nil {
2✔
241
                        image = string(result)
1✔
242
                }
1✔
243
        }
244

245
        var blob *imagor.Blob
1✔
246
        if blob, err = load(image); err != nil {
1✔
UNCOV
247
                return
×
UNCOV
248
        }
×
249
        var w, h int
1✔
250
        var overlay *vips.Image
1✔
251
        var n = 1
1✔
252
        if isAnimated(img) {
2✔
253
                n = -1
1✔
254
        }
1✔
255
        // w_ratio h_ratio
256
        if ln >= 6 {
2✔
257
                w = img.Width()
1✔
258
                h = img.PageHeight()
1✔
259
                if args[4] != "none" {
2✔
260
                        w, _ = strconv.Atoi(args[4])
1✔
261
                        w = img.Width() * w / 100
1✔
262
                }
1✔
263
                if args[5] != "none" {
2✔
264
                        h, _ = strconv.Atoi(args[5])
1✔
265
                        h = img.PageHeight() * h / 100
1✔
266
                }
1✔
267
                if overlay, err = v.NewThumbnail(
1✔
268
                        ctx, blob, w, h, vips.InterestingNone, vips.SizeBoth, n, 1, 0,
1✔
269
                ); err != nil {
1✔
270
                        return
×
271
                }
×
272
        } else {
1✔
273
                if overlay, err = v.NewThumbnail(
1✔
274
                        ctx, blob, v.MaxWidth, v.MaxHeight, vips.InterestingNone, vips.SizeDown, n, 1, 0,
1✔
275
                ); err != nil {
1✔
276
                        return
×
277
                }
×
278
        }
279
        contextDefer(ctx, overlay.Close)
1✔
280

1✔
281
        // Parse arguments
1✔
282
        var xArg, yArg string
1✔
283
        var alpha float64
1✔
284
        if ln >= 3 {
2✔
285
                xArg = args[1]
1✔
286
                yArg = args[2]
1✔
287
        }
1✔
288
        if ln >= 4 {
2✔
289
                alpha, _ = strconv.ParseFloat(args[3], 64)
1✔
290
        }
1✔
291

292
        // Prepare overlay for compositing
293
        if err = prepareOverlay(img, overlay, xArg, yArg, alpha); err != nil {
1✔
UNCOV
294
                return
×
UNCOV
295
        }
×
296

297
        // Composite overlay onto image
298
        return img.Composite2(overlay, vips.BlendModeOver, nil)
1✔
299
}
300

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

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

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

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

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

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

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

579
func getAngle(angle int) vips.Angle {
1✔
580
        switch angle {
1✔
581
        case 90:
1✔
582
                return vips.AngleD270
1✔
583
        case 180:
1✔
584
                return vips.AngleD180
1✔
585
        case 270:
1✔
586
                return vips.AngleD90
1✔
UNCOV
587
        default:
×
UNCOV
588
                return vips.AngleD0
×
589
        }
590
}
591

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

617
func grayscale(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
618
        return img.Colourspace(vips.InterpretationBW, nil)
1✔
619
}
1✔
620

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

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

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

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

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

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

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

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

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

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

760
func stripExif(_ context.Context, img *vips.Image, _ imagor.LoadFunc, _ ...string) (err error) {
1✔
761
        return img.RemoveExif()
1✔
762
}
1✔
763

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

782
func linearRGB(img *vips.Image, a, b []float64) error {
1✔
783
        if img.HasAlpha() {
2✔
784
                a = append(a, 1)
1✔
785
                b = append(b, 0)
1✔
786
        }
1✔
787
        return img.Linear(a, b, nil)
1✔
788
}
789

790
func isBlack(c []float64) bool {
1✔
791
        if len(c) < 3 {
1✔
UNCOV
792
                return false
×
UNCOV
793
        }
×
794
        return c[0] == 0x00 && c[1] == 0x00 && c[2] == 0x00
1✔
795
}
796

797
func isWhite(c []float64) bool {
1✔
798
        if len(c) < 3 {
1✔
UNCOV
799
                return false
×
UNCOV
800
        }
×
801
        return c[0] == 0xff && c[1] == 0xff && c[2] == 0xff
1✔
802
}
803

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

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

856
func hexToByte(b byte) byte {
1✔
857
        switch {
1✔
858
        case b >= '0' && b <= '9':
1✔
859
                return b - '0'
1✔
860
        case b >= 'a' && b <= 'f':
1✔
861
                return b - 'a' + 10
1✔
862
        }
863
        return 0
1✔
864
}
865

866
func crop(_ context.Context, img *vips.Image, _ imagor.LoadFunc, args ...string) error {
1✔
867
        if len(args) < 4 {
1✔
UNCOV
868
                return nil
×
UNCOV
869
        }
×
870

871
        // Parse arguments
872
        left, _ := strconv.ParseFloat(args[0], 64)
1✔
873
        top, _ := strconv.ParseFloat(args[1], 64)
1✔
874
        width, _ := strconv.ParseFloat(args[2], 64)
1✔
875
        height, _ := strconv.ParseFloat(args[3], 64)
1✔
876

1✔
877
        imgWidth := float64(img.Width())
1✔
878
        imgHeight := float64(img.PageHeight())
1✔
879

1✔
880
        // Convert relative (0-1) to absolute pixels
1✔
881
        if left > 0 && left < 1 {
2✔
882
                left = left * imgWidth
1✔
883
        }
1✔
884
        if top > 0 && top < 1 {
2✔
885
                top = top * imgHeight
1✔
886
        }
1✔
887
        if width > 0 && width < 1 {
2✔
888
                width = width * imgWidth
1✔
889
        }
1✔
890
        if height > 0 && height < 1 {
2✔
891
                height = height * imgHeight
1✔
892
        }
1✔
893

894
        // Clamp left and top to image bounds
895
        left = math.Max(0, math.Min(left, imgWidth))
1✔
896
        top = math.Max(0, math.Min(top, imgHeight))
1✔
897

1✔
898
        // Adjust width and height to not exceed image bounds
1✔
899
        width = math.Min(width, imgWidth-left)
1✔
900
        height = math.Min(height, imgHeight-top)
1✔
901

1✔
902
        // Skip if invalid crop area
1✔
903
        if width <= 0 || height <= 0 {
1✔
UNCOV
904
                return nil
×
UNCOV
905
        }
×
906

907
        return img.ExtractAreaMultiPage(int(left), int(top), int(width), int(height))
1✔
908
}
909

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