1
2
3
4
5
6
7 package cmpimg
8
9 import (
10 "bytes"
11 "fmt"
12 "image"
13 "image/color"
14 "image/draw"
15 "math"
16 "reflect"
17 "strings"
18
19 "rsc.io/pdf"
20
21 _ "image/jpeg"
22 _ "image/png"
23
24 _ "golang.org/x/image/tiff"
25 )
26
27
28
29
30
31
32 func Equal(typ string, raw1, raw2 []byte) (bool, error) {
33 return EqualApprox(typ, raw1, raw2, 0)
34 }
35
36
37
38
39
40
41
42
43
44
45 func EqualApprox(typ string, raw1, raw2 []byte, delta float64) (bool, error) {
46 switch {
47 case delta < 0:
48 delta = 0
49 case delta > 1:
50 delta = 1
51 }
52
53 switch typ {
54 case "svg", "tex":
55 return bytes.Equal(raw1, raw2), nil
56
57 case "eps":
58 lines1, lines2 := strings.Split(string(raw1), "\n"), strings.Split(string(raw2), "\n")
59 if len(lines1) != len(lines2) {
60 return false, nil
61 }
62 for i, line1 := range lines1 {
63 if strings.Contains(line1, "CreationDate") {
64 continue
65 }
66 if line1 != lines2[i] {
67 return false, nil
68 }
69 }
70 return true, nil
71
72 case "pdf":
73 pdf1, err := pdf.NewReader(bytes.NewReader(raw1), int64(len(raw1)))
74 if err != nil {
75 return false, err
76 }
77
78 pdf2, err := pdf.NewReader(bytes.NewReader(raw2), int64(len(raw2)))
79 if err != nil {
80 return false, err
81 }
82
83 return cmpPdf(pdf1, pdf2), nil
84
85 case "jpeg", "jpg", "png", "tiff":
86 v1, _, err := image.Decode(bytes.NewReader(raw1))
87 if err != nil {
88 return false, err
89 }
90 v2, _, err := image.Decode(bytes.NewReader(raw2))
91 if err != nil {
92 return false, err
93 }
94 return cmpImg(v1, v2, delta), nil
95
96 default:
97 return false, fmt.Errorf("cmpimg: unknown image type %q", typ)
98 }
99 }
100
101 func cmpPdf(pdf1, pdf2 *pdf.Reader) bool {
102 n1 := pdf1.NumPage()
103 n2 := pdf2.NumPage()
104 if n1 != n2 {
105 return false
106 }
107
108 for i := 1; i <= n1; i++ {
109 p1 := pdf1.Page(i).Content()
110 p2 := pdf2.Page(i).Content()
111 if !reflect.DeepEqual(p1, p2) {
112 return false
113 }
114 }
115
116 t1 := pdf1.Trailer().String()
117 t2 := pdf2.Trailer().String()
118 return t1 == t2
119 }
120
121 func cmpImg(v1, v2 image.Image, delta float64) bool {
122 img1, ok := v1.(*image.RGBA)
123 if !ok {
124 img1 = newRGBAFrom(v1)
125 }
126
127 img2, ok := v2.(*image.RGBA)
128 if !ok {
129 img2 = newRGBAFrom(v2)
130 }
131
132 if len(img1.Pix) != len(img2.Pix) {
133 return false
134 }
135
136 max := delta * delta
137 bnd := img1.Bounds()
138 for x := bnd.Min.X; x < bnd.Max.X; x++ {
139 for y := bnd.Min.Y; y < bnd.Max.Y; y++ {
140 c1 := img1.RGBAAt(x, y)
141 c2 := img2.RGBAAt(x, y)
142 if !yiqEqApprox(c1, c2, max) {
143 return false
144 }
145 }
146 }
147
148 return true
149 }
150
151
152
153
154
155
156
157
158
159
160
161 func yiqEqApprox(c1, c2 color.RGBA, d2 float64) bool {
162 const max = 35215.0
163
164 var (
165 r1 = float64(c1.R)
166 g1 = float64(c1.G)
167 b1 = float64(c1.B)
168
169 r2 = float64(c2.R)
170 g2 = float64(c2.G)
171 b2 = float64(c2.B)
172
173 y1 = r1*0.29889531 + g1*0.58662247 + b1*0.11448223
174 i1 = r1*0.59597799 - g1*0.27417610 - b1*0.32180189
175 q1 = r1*0.21147017 - g1*0.52261711 + b1*0.31114694
176
177 y2 = r2*0.29889531 + g2*0.58662247 + b2*0.11448223
178 i2 = r2*0.59597799 - g2*0.27417610 - b2*0.32180189
179 q2 = r2*0.21147017 - g2*0.52261711 + b2*0.31114694
180
181 y = y1 - y2
182 i = i1 - i2
183 q = q1 - q2
184
185 diff = 0.5053*y*y + 0.299*i*i + 0.1957*q*q
186 )
187 return diff <= max*d2
188 }
189
190 func newRGBAFrom(src image.Image) *image.RGBA {
191 var (
192 bnds = src.Bounds()
193 dst = image.NewRGBA(bnds)
194 )
195 draw.Draw(dst, bnds, src, image.Point{}, draw.Src)
196 return dst
197 }
198
199
200
201
202
203
204
205
206
207
208 func Diff(dst draw.Image, a, b image.Image) image.Rectangle {
209 rect := dst.Bounds().Intersect(a.Bounds()).Intersect(b.Bounds())
210
211
212 min := uint16(math.MaxUint16)
213 max := uint16(0)
214 for x := rect.Min.X; x < rect.Max.X; x++ {
215 for y := rect.Min.Y; y < rect.Max.Y; y++ {
216 p := diffColor{a.At(x, y), b.At(x, y)}
217 g := color.Gray16Model.Convert(p).(color.Gray16)
218 if g.Y < min {
219 min = g.Y
220 }
221 if g.Y > max {
222 max = g.Y
223 }
224 }
225 }
226
227
228 for x := rect.Min.X; x < rect.Max.X; x++ {
229 for y := rect.Min.Y; y < rect.Max.Y; y++ {
230 dst.Set(x, y, scaledColor{
231 min: uint32(min), max: uint32(max),
232 c: diffColor{a.At(x, y), b.At(x, y)},
233 })
234 }
235 }
236
237 return rect
238 }
239
240 type diffColor struct {
241 a, b color.Color
242 }
243
244 func (c diffColor) RGBA() (r, g, b, a uint32) {
245 ra, ga, ba, _ := c.a.RGBA()
246 rb, gb, bb, _ := c.b.RGBA()
247 return diff(ra, rb), diff(ga, gb), diff(ba, bb), math.MaxUint16
248 }
249
250 func diff(a, b uint32) uint32 {
251 if a < b {
252 return b - a
253 }
254 return a - b
255 }
256
257 type scaledColor struct {
258 min, max uint32
259 c color.Color
260 }
261
262 func (c scaledColor) RGBA() (r, g, b, a uint32) {
263 if c.max == c.min {
264 return 0, 0, 0, 0
265 }
266 f := uint32(math.MaxUint16) / (c.max - c.min)
267 r, g, b, _ = c.c.RGBA()
268 r -= c.min
269 r *= f
270 g -= c.min
271 g *= f
272 b -= c.min
273 b *= f
274 return r, g, b, math.MaxUint16
275 }
276
View as plain text