1 /*
2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *       http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.graphics.path
18 
19 import android.graphics.Bitmap
20 import android.graphics.Color
21 import android.graphics.Paint
22 import android.graphics.Path
23 import android.graphics.PointF
24 import android.graphics.RectF
25 import android.os.Build
26 import androidx.core.graphics.applyCanvas
27 import androidx.core.graphics.createBitmap
28 import androidx.test.ext.junit.runners.AndroidJUnit4
29 import androidx.test.filters.SmallTest
30 import kotlin.math.abs
31 import org.junit.Assert.assertEquals
32 import org.junit.Assert.assertFalse
33 import org.junit.Assert.assertTrue
34 import org.junit.Assert.fail
35 import org.junit.Test
36 import org.junit.runner.RunWith
37 
assertPointsEqualsnull38 private fun assertPointsEquals(p1: PointF, p2: PointF) {
39     assertEquals(p1.x, p2.x, 1e-6f)
40     assertEquals(p1.y, p2.y, 1e-6f)
41 }
42 
assertPointsEqualsnull43 private fun assertPointsEquals(p1: FloatArray, offset: Int, p2: PointF) {
44     assertEquals(p1[0 + offset * 2], p2.x, 1e-6f)
45     assertEquals(p1[1 + offset * 2], p2.y, 1e-6f)
46 }
47 
compareBitmapsnull48 private fun compareBitmaps(b1: Bitmap, b2: Bitmap) {
49     val epsilon: Int =
50         if (Build.VERSION.SDK_INT != 23) {
51             1
52         } else {
53             // There is more AA variability between conics and cubics on API 23, leading
54             // to failures on relatively small visual differences. Increase the error
55             // value for just this release to avoid erroneous bitmap comparison failures.
56             32
57         }
58 
59     assertEquals(b1.width, b2.width)
60     assertEquals(b1.height, b2.height)
61 
62     val p1 = IntArray(b1.width * b1.height)
63     b1.getPixels(p1, 0, b1.width, 0, 0, b1.width, b1.height)
64 
65     val p2 = IntArray(b2.width * b2.height)
66     b2.getPixels(p2, 0, b2.width, 0, 0, b2.width, b2.height)
67 
68     for (x in 0 until b1.width) {
69         for (y in 0 until b2.width) {
70             val index = y * b1.width + x
71 
72             val c1 = p1[index]
73             val c2 = p2[index]
74 
75             assertTrue(abs(Color.red(c1) - Color.red(c2)) <= epsilon)
76             assertTrue(abs(Color.green(c1) - Color.green(c2)) <= epsilon)
77             assertTrue(abs(Color.blue(c1) - Color.blue(c2)) <= epsilon)
78         }
79     }
80 }
81 
82 @SmallTest
83 @RunWith(AndroidJUnit4::class)
84 class PathIteratorTest {
85     @Test
emptyIteratornull86     fun emptyIterator() {
87         val path = Path()
88 
89         val iterator = path.iterator()
90         assertFalse(iterator.hasNext())
91         val firstSegment = iterator.next()
92         assertEquals(PathSegment.Type.Done, firstSegment.type)
93 
94         var count = 0
95         for (segment in path) {
96             count++
97         }
98 
99         assertEquals(0, count)
100     }
101 
102     @Test
emptyPeeknull103     fun emptyPeek() {
104         val path = Path()
105         val iterator = path.iterator()
106         assertEquals(PathSegment.Type.Done, iterator.peek())
107     }
108 
109     @Test
nonEmptyIteratornull110     fun nonEmptyIterator() {
111         val path =
112             Path().apply {
113                 moveTo(1.0f, 1.0f)
114                 lineTo(2.0f, 2.0f)
115                 close()
116             }
117 
118         val iterator = path.iterator()
119         assertTrue(iterator.hasNext())
120 
121         val types =
122             arrayOf(
123                 PathSegment.Type.Move,
124                 PathSegment.Type.Line,
125                 PathSegment.Type.Close,
126                 PathSegment.Type.Done
127             )
128         val points = arrayOf(PointF(1.0f, 1.0f), PointF(2.0f, 2.0f))
129 
130         var count = 0
131         for (segment in path) {
132             assertEquals(types[count], segment.type)
133             when (segment.type) {
134                 PathSegment.Type.Move -> {
135                     assertEquals(points[count], segment.points[0])
136                 }
137                 PathSegment.Type.Line -> {
138                     assertEquals(points[count - 1], segment.points[0])
139                     assertEquals(points[count], segment.points[1])
140                 }
141                 else -> {}
142             }
143             // TODO: remove condition and just auto-increment count when platform change is
144             // checked in which ignores DONE during iteration
145             if (segment.type != PathSegment.Type.Done) count++
146         }
147 
148         assertEquals(3, count)
149     }
150 
151     @Test
peeknull152     fun peek() {
153         val path =
154             Path().apply {
155                 moveTo(1.0f, 1.0f)
156                 lineTo(2.0f, 2.0f)
157                 close()
158             }
159 
160         val iterator = path.iterator()
161         assertEquals(PathSegment.Type.Move, iterator.peek())
162     }
163 
164     @Test
peekBeyondnull165     fun peekBeyond() {
166         val path = Path()
167         assertEquals(PathSegment.Type.Done, path.iterator().peek())
168 
169         path.apply {
170             moveTo(1.0f, 1.0f)
171             lineTo(2.0f, 2.0f)
172             close()
173         }
174 
175         val iterator = path.iterator()
176         while (iterator.hasNext()) iterator.next()
177         assertEquals(PathSegment.Type.Done, iterator.peek())
178     }
179 
180     @Test
iteratorStylesnull181     fun iteratorStyles() {
182         val path =
183             Path().apply {
184                 moveTo(1.0f, 1.0f)
185                 lineTo(2.0f, 2.0f)
186                 cubicTo(3.0f, 3.0f, 4.0f, 4.0f, 5.0f, 5.0f)
187                 quadTo(7.0f, 7.0f, 8.0f, 8.0f)
188                 moveTo(10.0f, 10.0f)
189                 // addRoundRect() will generate conic curves on certain API levels
190                 addRoundRect(RectF(12.0f, 12.0f, 36.0f, 36.0f), 8.0f, 8.0f, Path.Direction.CW)
191                 close()
192             }
193 
194         iteratorStylesImpl(path, PathIterator.ConicEvaluation.AsConic)
195         iteratorStylesImpl(path, PathIterator.ConicEvaluation.AsQuadratics)
196     }
197 
iteratorStylesImplnull198     private fun iteratorStylesImpl(path: Path, conicEvaluation: PathIterator.ConicEvaluation) {
199         val iterator1 = path.iterator(conicEvaluation)
200         val iterator2 = path.iterator(conicEvaluation)
201         val iterator3 = path.iterator(conicEvaluation)
202 
203         val points = FloatArray(8)
204         val points2 = FloatArray(16)
205 
206         while (iterator1.hasNext() || iterator2.hasNext() || iterator3.hasNext()) {
207             val segment = iterator1.next()
208             val type = iterator2.next(points)
209             val type2 = iterator3.next(points2, 8)
210 
211             assertEquals(type, segment.type)
212             assertEquals(type2, segment.type)
213 
214             when (type) {
215                 PathSegment.Type.Move -> {
216                     assertPointsEquals(points, 0, segment.points[0])
217                     assertPointsEquals(points2, 4, segment.points[0])
218                 }
219                 PathSegment.Type.Line -> {
220                     assertPointsEquals(points, 0, segment.points[0])
221                     assertPointsEquals(points, 1, segment.points[1])
222                     assertPointsEquals(points2, 4, segment.points[0])
223                     assertPointsEquals(points2, 5, segment.points[1])
224                 }
225                 PathSegment.Type.Quadratic -> {
226                     assertPointsEquals(points, 0, segment.points[0])
227                     assertPointsEquals(points, 1, segment.points[1])
228                     assertPointsEquals(points, 2, segment.points[2])
229                     assertPointsEquals(points2, 4, segment.points[0])
230                     assertPointsEquals(points2, 5, segment.points[1])
231                     assertPointsEquals(points2, 6, segment.points[2])
232                 }
233                 PathSegment.Type.Conic -> {
234                     assertPointsEquals(points, 0, segment.points[0])
235                     assertPointsEquals(points, 1, segment.points[1])
236                     assertPointsEquals(points, 2, segment.points[2])
237                     // Weight is stored after all of the points
238                     assertEquals(points[6], segment.weight)
239 
240                     assertPointsEquals(points2, 4, segment.points[0])
241                     assertPointsEquals(points2, 5, segment.points[1])
242                     assertPointsEquals(points2, 6, segment.points[2])
243                     // Weight is stored after all of the points
244                     assertEquals(points2[14], segment.weight)
245                 }
246                 PathSegment.Type.Cubic -> {
247                     assertPointsEquals(points, 0, segment.points[0])
248                     assertPointsEquals(points, 1, segment.points[1])
249                     assertPointsEquals(points, 2, segment.points[2])
250                     assertPointsEquals(points, 3, segment.points[3])
251 
252                     assertPointsEquals(points2, 4, segment.points[0])
253                     assertPointsEquals(points2, 5, segment.points[1])
254                     assertPointsEquals(points2, 6, segment.points[2])
255                     assertPointsEquals(points2, 7, segment.points[3])
256                 }
257                 PathSegment.Type.Close -> {}
258                 PathSegment.Type.Done -> {}
259             }
260         }
261     }
262 
263     @Test
donenull264     fun done() {
265         val path = Path().apply { close() }
266 
267         val segment = path.iterator().next()
268 
269         assertEquals(PathSegment.Type.Done, segment.type)
270         assertEquals(0, segment.points.size)
271         assertEquals(0.0f, segment.weight)
272     }
273 
274     @Test
closenull275     fun close() {
276         val path =
277             Path().apply {
278                 lineTo(10.0f, 12.0f)
279                 close()
280             }
281 
282         val iterator = path.iterator()
283         // Swallow the move
284         iterator.next()
285         // Swallow the line
286         iterator.next()
287 
288         val segment = iterator.next()
289 
290         assertEquals(PathSegment.Type.Close, segment.type)
291         assertEquals(0, segment.points.size)
292         assertEquals(0.0f, segment.weight)
293     }
294 
295     @Test
moveTonull296     fun moveTo() {
297         val path = Path().apply { moveTo(10.0f, 12.0f) }
298 
299         val segment = path.iterator().next()
300 
301         assertEquals(PathSegment.Type.Move, segment.type)
302         assertEquals(1, segment.points.size)
303         assertPointsEquals(PointF(10.0f, 12.0f), segment.points[0])
304         assertEquals(0.0f, segment.weight)
305     }
306 
307     @Test
lineTonull308     fun lineTo() {
309         val path =
310             Path().apply {
311                 moveTo(4.0f, 6.0f)
312                 lineTo(10.0f, 12.0f)
313             }
314 
315         val iterator = path.iterator()
316         // Swallow the move
317         iterator.next()
318 
319         val segment = iterator.next()
320 
321         assertEquals(PathSegment.Type.Line, segment.type)
322         assertEquals(2, segment.points.size)
323         assertPointsEquals(PointF(4.0f, 6.0f), segment.points[0])
324         assertPointsEquals(PointF(10.0f, 12.0f), segment.points[1])
325         assertEquals(0.0f, segment.weight)
326     }
327 
328     @Test
quadraticTonull329     fun quadraticTo() {
330         val path =
331             Path().apply {
332                 moveTo(4.0f, 6.0f)
333                 quadTo(10.0f, 12.0f, 20.0f, 24.0f)
334             }
335 
336         val iterator = path.iterator()
337         // Swallow the move
338         iterator.next()
339 
340         val segment = iterator.next()
341 
342         assertEquals(PathSegment.Type.Quadratic, segment.type)
343         assertEquals(3, segment.points.size)
344         assertPointsEquals(PointF(4.0f, 6.0f), segment.points[0])
345         assertPointsEquals(PointF(10.0f, 12.0f), segment.points[1])
346         assertPointsEquals(PointF(20.0f, 24.0f), segment.points[2])
347         assertEquals(0.0f, segment.weight)
348     }
349 
350     @Test
cubicTonull351     fun cubicTo() {
352         val path =
353             Path().apply {
354                 moveTo(4.0f, 6.0f)
355                 cubicTo(10.0f, 12.0f, 20.0f, 24.0f, 30.0f, 36.0f)
356             }
357 
358         val iterator = path.iterator()
359         // Swallow the move
360         iterator.next()
361 
362         val segment = iterator.next()
363 
364         assertEquals(PathSegment.Type.Cubic, segment.type)
365         assertEquals(4, segment.points.size)
366         assertPointsEquals(PointF(4.0f, 6.0f), segment.points[0])
367         assertPointsEquals(PointF(10.0f, 12.0f), segment.points[1])
368         assertPointsEquals(PointF(20.0f, 24.0f), segment.points[2])
369         assertPointsEquals(PointF(30.0f, 36.0f), segment.points[3])
370         assertEquals(0.0f, segment.weight)
371     }
372 
373     @Test
conicTonull374     fun conicTo() {
375         if (Build.VERSION.SDK_INT >= 25) {
376             val path =
377                 Path().apply {
378                     addRoundRect(RectF(12.0f, 12.0f, 24.0f, 24.0f), 8.0f, 8.0f, Path.Direction.CW)
379                 }
380 
381             val iterator = path.iterator(PathIterator.ConicEvaluation.AsConic)
382             // Swallow the move
383             iterator.next()
384 
385             val segment = iterator.next()
386 
387             assertEquals(PathSegment.Type.Conic, segment.type)
388             assertEquals(3, segment.points.size)
389 
390             assertPointsEquals(PointF(12.0f, 18.0f), segment.points[0])
391             assertPointsEquals(PointF(12.0f, 12.0f), segment.points[1])
392             assertPointsEquals(PointF(18.0f, 12.0f), segment.points[2])
393             assertEquals(0.70710677f, segment.weight)
394         }
395     }
396 
397     @Test
conicAsQuadraticsnull398     fun conicAsQuadratics() {
399         val path =
400             Path().apply {
401                 addRoundRect(RectF(12.0f, 12.0f, 24.0f, 24.0f), 8.0f, 8.0f, Path.Direction.CW)
402             }
403 
404         for (segment in path) {
405             if (segment.type == PathSegment.Type.Conic) fail("Found conic, none expected: $segment")
406         }
407     }
408 
409     @Test
convertedConicsnull410     fun convertedConics() {
411         val path1 =
412             Path().apply {
413                 addRoundRect(RectF(12.0f, 12.0f, 64.0f, 64.0f), 12.0f, 12.0f, Path.Direction.CW)
414             }
415 
416         val path2 = Path()
417         for (segment in path1) {
418             when (segment.type) {
419                 PathSegment.Type.Move -> path2.moveTo(segment.points[0].x, segment.points[0].y)
420                 PathSegment.Type.Line -> path2.lineTo(segment.points[1].x, segment.points[1].y)
421                 PathSegment.Type.Quadratic ->
422                     path2.quadTo(
423                         segment.points[1].x,
424                         segment.points[1].y,
425                         segment.points[2].x,
426                         segment.points[2].y
427                     )
428                 PathSegment.Type.Conic -> fail("Unexpected conic! $segment")
429                 PathSegment.Type.Cubic ->
430                     path2.cubicTo(
431                         segment.points[1].x,
432                         segment.points[1].y,
433                         segment.points[2].x,
434                         segment.points[2].y,
435                         segment.points[3].x,
436                         segment.points[3].y
437                     )
438                 PathSegment.Type.Close -> path2.close()
439                 PathSegment.Type.Done -> {}
440             }
441         }
442 
443         // Now with smaller error tolerance
444         val path3 = Path()
445         for (segment in
446             path1.iterator(conicEvaluation = PathIterator.ConicEvaluation.AsQuadratics, 0.001f)) {
447             when (segment.type) {
448                 PathSegment.Type.Move -> path3.moveTo(segment.points[0].x, segment.points[0].y)
449                 PathSegment.Type.Line -> path3.lineTo(segment.points[1].x, segment.points[1].y)
450                 PathSegment.Type.Quadratic ->
451                     path3.quadTo(
452                         segment.points[1].x,
453                         segment.points[1].y,
454                         segment.points[2].x,
455                         segment.points[2].y
456                     )
457                 PathSegment.Type.Conic -> fail("Unexpected conic! $segment")
458                 PathSegment.Type.Cubic ->
459                     path3.cubicTo(
460                         segment.points[1].x,
461                         segment.points[1].y,
462                         segment.points[2].x,
463                         segment.points[2].y,
464                         segment.points[3].x,
465                         segment.points[3].y
466                     )
467                 PathSegment.Type.Close -> path3.close()
468                 PathSegment.Type.Done -> {}
469             }
470         }
471 
472         val b1 =
473             createBitmap(76, 76).applyCanvas {
474                 drawARGB(255, 255, 255, 255)
475                 drawPath(
476                     path1,
477                     Paint().apply {
478                         color = argb(1.0f, 0.0f, 0.0f, 1.0f)
479                         strokeWidth = 2.0f
480                         isAntiAlias = true
481                         style = Paint.Style.STROKE
482                     }
483                 )
484             }
485 
486         val b2 =
487             createBitmap(76, 76).applyCanvas {
488                 drawARGB(255, 255, 255, 255)
489                 drawPath(
490                     path2,
491                     Paint().apply {
492                         color = argb(1.0f, 0.0f, 0.0f, 1.0f)
493                         strokeWidth = 2.0f
494                         isAntiAlias = true
495                         style = Paint.Style.STROKE
496                     }
497                 )
498             }
499 
500         compareBitmaps(b1, b2)
501         // Note: b1-vs-b3 is not a valid comparison; default Skia rendering does not use an
502         // error tolerance that low. The test for fine-precision in path3 was just to
503         // ensure that the system could handle the extra data and operations required
504     }
505 
506     @Test
sizesnull507     fun sizes() {
508         val path = Path()
509         var iterator: PathIterator = path.iterator()
510 
511         assertEquals(0, iterator.calculateSize())
512 
513         path.addRoundRect(RectF(12.0f, 12.0f, 64.0f, 64.0f), 8.0f, 8.0f, Path.Direction.CW)
514 
515         // Skia converted
516         if (Build.VERSION.SDK_INT > 22) {
517             // Preserve conics and count
518             iterator = path.iterator(PathIterator.ConicEvaluation.AsConic)
519             assertEquals(10, iterator.calculateSize())
520             assertEquals(iterator.calculateSize(false), iterator.calculateSize())
521         }
522 
523         // Convert conics and count
524         iterator = path.iterator(PathIterator.ConicEvaluation.AsQuadratics)
525         if (Build.VERSION.SDK_INT > 22) {
526             // simple size, not including conic conversion
527             assertEquals(10, iterator.calculateSize(false))
528         } else {
529             // round rects pre-API22 used line/quad/quad for each corner
530             assertEquals(14, iterator.calculateSize(false))
531         }
532         // now get the size with converted conics
533         assertEquals(14, iterator.calculateSize())
534     }
535 }
536 
argbnull537 fun argb(alpha: Float, red: Float, green: Float, blue: Float) =
538     ((alpha * 255.0f + 0.5f).toInt() shl 24) or
539         ((red * 255.0f + 0.5f).toInt() shl 16) or
540         ((green * 255.0f + 0.5f).toInt() shl 8) or
541         (blue * 255.0f + 0.5f).toInt()
542