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

cshum / imagor / 21752953553

06 Feb 2026 01:52PM UTC coverage: 91.837% (-0.06%) from 91.894%
21752953553

push

github

web-flow
fix: image overlay complete out of bound fix (#730)

* fix: image overlay complete out of bound fix

* test: update golden files

---------

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

5 of 5 new or added lines in 1 file covered. (100.0%)

4 existing lines in 1 file now uncovered.

5715 of 6223 relevant lines covered (91.84%)

1.1 hits per line

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

83.74
/processor/vipsprocessor/overlay.go
1
package vipsprocessor
2

3
import (
4
        "strconv"
5
        "strings"
6

7
        "github.com/cshum/imagor/imagorpath"
8
        "github.com/cshum/vipsgen/vips"
9
)
10

11
// blendModeMap maps blend mode names to vips.BlendMode constants
12
var blendModeMap = map[string]vips.BlendMode{
13
        "normal":      vips.BlendModeOver,
14
        "multiply":    vips.BlendModeMultiply,
15
        "color-burn":  vips.BlendModeColourBurn,
16
        "darken":      vips.BlendModeDarken,
17
        "screen":      vips.BlendModeScreen,
18
        "color-dodge": vips.BlendModeColourDodge,
19
        "lighten":     vips.BlendModeLighten,
20
        "add":         vips.BlendModeAdd,
21
        "overlay":     vips.BlendModeOverlay,
22
        "soft-light":  vips.BlendModeSoftLight,
23
        "hard-light":  vips.BlendModeHardLight,
24
        "difference":  vips.BlendModeDifference,
25
        "exclusion":   vips.BlendModeExclusion,
26
        "mask":        vips.BlendModeDestIn,
27
        "mask-out":    vips.BlendModeDestOut,
28
}
29

30
// parseOverlayPosition parses position argument and returns position value and repeat count
31
func parseOverlayPosition(arg string, canvasSize, overlaySize int, hAlign, vAlign string) (pos int, repeat int) {
1✔
32
        repeat = 1
1✔
33
        if arg == "" {
2✔
34
                return 0, 1
1✔
35
        }
1✔
36

37
        if arg == "center" {
2✔
38
                return (canvasSize - overlaySize) / 2, 1
1✔
39
        } else if arg == hAlign || arg == vAlign {
3✔
40
                if arg == imagorpath.HAlignRight || arg == imagorpath.VAlignBottom {
2✔
41
                        return canvasSize - overlaySize, 1
1✔
42
                }
1✔
43
                return 0, 1
1✔
44
        } else if arg == "repeat" {
2✔
45
                return 0, canvasSize/overlaySize + 1
1✔
46
        } else if strings.HasPrefix(strings.TrimPrefix(arg, "-"), "0.") {
3✔
47
                pec, _ := strconv.ParseFloat(arg, 64)
1✔
48
                return int(pec * float64(canvasSize)), 1
1✔
49
        } else if strings.HasSuffix(arg, "p") {
3✔
50
                val, _ := strconv.Atoi(strings.TrimSuffix(arg, "p"))
1✔
51
                return val * canvasSize / 100, 1
1✔
52
        }
1✔
53

54
        pos, _ = strconv.Atoi(arg)
1✔
55
        return pos, 1
1✔
56
}
57

58
// compositeOverlay transforms and composites overlay image onto the base image
59
// Handles color space, alpha channel, positioning, repeat patterns, cropping, and animation frames
60
// Returns early without compositing if overlay is completely outside canvas bounds
61
func compositeOverlay(img *vips.Image, overlay *vips.Image, xArg, yArg string, alpha float64, blendMode vips.BlendMode) error {
1✔
62
        // Ensure overlay has proper color space and alpha
1✔
63
        if overlay.Bands() < 3 {
2✔
64
                if err := overlay.Colourspace(vips.InterpretationSrgb, nil); err != nil {
1✔
65
                        return err
×
66
                }
×
67
        }
68
        if !overlay.HasAlpha() {
2✔
69
                if err := overlay.Addalpha(); err != nil {
1✔
70
                        return err
×
71
                }
×
72
        }
73

74
        // Apply alpha if provided
75
        if alpha > 0 {
2✔
76
                alphaMultiplier := 1 - alpha/100
1✔
77
                if alphaMultiplier != 1 {
2✔
78
                        if err := overlay.Linear([]float64{1, 1, 1, alphaMultiplier}, []float64{0, 0, 0, 0}, nil); err != nil {
1✔
79
                                return err
×
80
                        }
×
81
                }
82
        }
83

84
        // Parse position
85
        overlayWidth := overlay.Width()
1✔
86
        overlayHeight := overlay.PageHeight()
1✔
87

1✔
88
        x, across := parseOverlayPosition(xArg, img.Width(), overlayWidth, imagorpath.HAlignLeft, imagorpath.HAlignRight)
1✔
89
        y, down := parseOverlayPosition(yArg, img.PageHeight(), overlayHeight, imagorpath.VAlignTop, imagorpath.VAlignBottom)
1✔
90

1✔
91
        // Apply negative adjustment for all cases EXCEPT center
1✔
92
        if x < 0 && xArg != "center" {
2✔
93
                x += img.Width() - overlayWidth
1✔
94
        }
1✔
95
        if y < 0 && yArg != "center" {
2✔
96
                y += img.PageHeight() - overlayHeight
1✔
97
        }
1✔
98

99
        // Handle repeat pattern
100
        if across*down > 1 {
2✔
101
                if err := overlay.EmbedMultiPage(0, 0, across*overlayWidth, down*overlayHeight,
1✔
102
                        &vips.EmbedMultiPageOptions{Extend: vips.ExtendRepeat}); err != nil {
1✔
103
                        return err
×
104
                }
×
105
                // Update dimensions after repeat
106
                overlayWidth = overlay.Width()
1✔
107
                overlayHeight = overlay.PageHeight()
1✔
108
        }
109

110
        // Check if overlay is completely outside canvas bounds
111
        // Skip compositing if there's no intersection with the canvas
112
        if x >= img.Width() || y >= img.PageHeight() ||
1✔
113
                x+overlayWidth <= 0 || y+overlayHeight <= 0 {
2✔
114
                // Overlay is completely outside canvas bounds, skip it
1✔
115
                return nil
1✔
116
        }
1✔
117

118
        // Position overlay on canvas
119
        // Crop overlay to only the visible portion within canvas bounds
120
        visibleLeft := 0
1✔
121
        visibleTop := 0
1✔
122
        visibleWidth := overlayWidth
1✔
123
        visibleHeight := overlayHeight
1✔
124
        embedX := x
1✔
125
        embedY := y
1✔
126

1✔
127
        // Handle overlay extending beyond right/bottom edges
1✔
128
        if x+overlayWidth > img.Width() {
2✔
129
                visibleWidth = img.Width() - x
1✔
130
        }
1✔
131
        if y+overlayHeight > img.PageHeight() {
2✔
132
                visibleHeight = img.PageHeight() - y
1✔
133
        }
1✔
134

135
        // Handle overlay starting before left/top edges (negative positions)
136
        if x < 0 {
2✔
137
                visibleLeft = -x
1✔
138
                visibleWidth = overlayWidth + x // reduce width
1✔
139
                embedX = 0
1✔
140
        }
1✔
141
        if y < 0 {
2✔
142
                visibleTop = -y
1✔
143
                visibleHeight = overlayHeight + y // reduce height
1✔
144
                embedY = 0
1✔
145
        }
1✔
146

147
        // Crop overlay to visible portion if needed
148
        if visibleLeft > 0 || visibleTop > 0 ||
1✔
149
                visibleWidth < overlayWidth || visibleHeight < overlayHeight {
2✔
150
                if visibleWidth > 0 && visibleHeight > 0 {
2✔
151
                        if err := overlay.ExtractAreaMultiPage(
1✔
152
                                visibleLeft, visibleTop, visibleWidth, visibleHeight,
1✔
153
                        ); err != nil {
1✔
154
                                return err
×
155
                        }
×
UNCOV
156
                } else {
×
UNCOV
157
                        // Overlay is completely outside canvas bounds, skip it
×
UNCOV
158
                        return nil
×
UNCOV
159
                }
×
160
        }
161

162
        // Embed the cropped overlay at adjusted position
163
        if err := overlay.EmbedMultiPage(
1✔
164
                embedX, embedY, img.Width(), img.PageHeight(), nil,
1✔
165
        ); err != nil {
1✔
166
                return err
×
167
        }
×
168

169
        // Handle animation frames
170
        overlayN := overlay.Height() / overlay.PageHeight()
1✔
171
        if n := img.Height() / img.PageHeight(); n > overlayN {
2✔
172
                cnt := n / overlayN
1✔
173
                if n%overlayN > 0 {
2✔
174
                        cnt++
1✔
175
                }
1✔
176
                if err := overlay.Replicate(1, cnt); err != nil {
1✔
177
                        return err
×
178
                }
×
179
        }
180

181
        // Composite overlay onto image with specified blend mode
182
        return img.Composite2(overlay, blendMode, nil)
1✔
183
}
184

185
// getBlendMode returns the vips.BlendMode for a given mode string
186
// Defaults to BlendModeOver (normal) if mode is empty or invalid
187
func getBlendMode(mode string) vips.BlendMode {
1✔
188
        if mode == "" {
1✔
189
                return vips.BlendModeOver
×
190
        }
×
191
        if blendMode, ok := blendModeMap[strings.ToLower(mode)]; ok {
2✔
192
                return blendMode
1✔
193
        }
1✔
194
        // Default to normal if invalid mode
195
        return vips.BlendModeOver
1✔
196
}
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