Skip to content

Commit

Permalink
Introduce OffByTwo differ to relax image comparisons
Browse files Browse the repository at this point in the history
  • Loading branch information
jrodbx committed Jul 12, 2024
1 parent 11bc928 commit f3ac474
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package app.cash.paparazzi.gradle

import app.cash.paparazzi.gradle.ImageSubject.Companion.assertThat
import app.cash.paparazzi.gradle.ImageSubject.ImageAssert.Companion.DEFAULT_PERCENT_DIFFERENCE_THRESHOLD
import app.cash.paparazzi.gradle.PrepareResourcesTask.Config
import com.google.common.truth.Correspondence
import com.google.common.truth.Truth.assertThat
Expand Down Expand Up @@ -689,9 +688,7 @@ class PaparazziPluginTest {
assertThat(snapshots[0]).isSimilarTo(normal).withDefaultThreshold()
assertThat(snapshots[1]).isSimilarTo(horizontalScroll).withDefaultThreshold()
assertThat(snapshots[2]).isSimilarTo(verticalScroll).withDefaultThreshold()
// TODO: is this an issue with layoutlib or related to https://github.com/cashapp/paparazzi/issues/482?
assertThat(snapshots[3]).isSimilarTo(shrink)
.withThreshold(DEFAULT_PERCENT_DIFFERENCE_THRESHOLD + 0.007)
assertThat(snapshots[3]).isSimilarTo(shrink).withDefaultThreshold()
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ internal interface Differ {
val delta: BufferedImage
) : DiffResult

data class Similar(
val delta: BufferedImage,
val numSimilarPixels: Long
) : DiffResult

data class Different(
val delta: BufferedImage,
val percentDifference: Float
val percentDifference: Float,
val numDifferentPixels: Long
) : DiffResult
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

package app.cash.paparazzi.internal

import app.cash.paparazzi.internal.Differ.DiffResult
import app.cash.paparazzi.internal.Differ.DiffResult.Different
import app.cash.paparazzi.internal.Differ.DiffResult.Identical
import app.cash.paparazzi.internal.Differ.DiffResult.Similar
import java.awt.AlphaComposite
import java.awt.Color
import java.awt.Graphics2D
Expand Down Expand Up @@ -138,11 +140,12 @@ internal object ImageUtils {
throw IllegalStateException("expected:<$TYPE_INT_ARGB> but was:<${goldenImage.type}>")
}

val differ: Differ = PixelPerfect
val differ: Differ = OffByTwo
differ.compare(goldenImage, image).let { result ->
return when (result) {
is DiffResult.Identical -> result.delta to 0f
is DiffResult.Different -> result.delta to result.percentDifference
is Identical -> result.delta to 0f
is Similar -> result.delta to 0f
is Different -> result.delta to result.percentDifference
}
}
}
Expand Down
106 changes: 106 additions & 0 deletions paparazzi/src/main/java/app/cash/paparazzi/internal/OffByTwo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package app.cash.paparazzi.internal

import app.cash.paparazzi.internal.Differ.DiffResult
import java.awt.image.BufferedImage
import java.awt.image.BufferedImage.TYPE_INT_ARGB
import kotlin.math.abs
import kotlin.math.max

internal object OffByTwo : Differ {
override fun compare(expected: BufferedImage, actual: BufferedImage): DiffResult {
check(expected.width == actual.width && expected.height == actual.height) { "Images are different sizes" }

val expectedWidth = expected.width
val expectedHeight = expected.height

val actualWidth = actual.width
val actualHeight = actual.height

val maxWidth = max(expectedWidth, actualWidth)
val maxHeight = max(expectedHeight, actualHeight)

val deltaImage = BufferedImage(expectedWidth + maxWidth + actualWidth, maxHeight, TYPE_INT_ARGB)
val g = deltaImage.graphics

// Compute delta map
var delta: Long = 0
var similarPixels: Long = 0
var differentPixels: Long = 0
for (y in 0 until maxHeight) {
for (x in 0 until maxWidth) {
val expectedRgb = if (x >= expectedWidth || y >= expectedHeight) {
0x00808080
} else {
expected.getRGB(x, y)
}

val actualRgb = if (x >= actualWidth || y >= actualHeight) {
0x00808080
} else {
actual.getRGB(x, y)
}

if (expectedRgb == actualRgb) {
deltaImage.setRGB(expectedWidth + x, y, 0x00808080)
continue
}

// If the pixels have no opacity, don't delta colors at all
if (expectedRgb and -0x1000000 == 0 && actualRgb and -0x1000000 == 0) {
deltaImage.setRGB(expectedWidth + x, y, 0x00808080)
continue
}

val deltaR = (actualRgb and 0xFF0000).ushr(16) - (expectedRgb and 0xFF0000).ushr(16)
val deltaG = (actualRgb and 0x00FF00).ushr(8) - (expectedRgb and 0x00FF00).ushr(8)
val deltaB = (actualRgb and 0x0000FF) - (expectedRgb and 0x0000FF)

val newR = 128 + deltaR and 0xFF
val newG = 128 + deltaG and 0xFF
val newB = 128 + deltaB and 0xFF
val avgAlpha =
((expectedRgb and -0x1000000).ushr(24) + (actualRgb and -0x1000000).ushr(24)) / 2 shl 24
val newRGB = avgAlpha or (newR shl 16) or (newG shl 8) or newB

if (abs(deltaR) < 2 && abs(deltaG) < 2 && abs(deltaB) < 2) {
similarPixels++
deltaImage.setRGB(expectedWidth + x, y, newRGB)
continue
}

differentPixels++
deltaImage.setRGB(expectedWidth + x, y, newRGB)

delta += abs(deltaR).toLong()
delta += abs(deltaG).toLong()
delta += abs(deltaB).toLong()
}
}

// Expected on the left
// Actual on the right
g.drawImage(expected, 0, 0, null)
g.drawImage(actual, expectedWidth + maxWidth, 0, null)

g.dispose()

// 3 different colors, 256 color levels
val total = actualHeight.toLong() * actualWidth.toLong() * 3L * 256L
val percentDifference = (delta * 100 / total.toDouble()).toFloat()

return if (differentPixels > 0) {
DiffResult.Different(
delta = deltaImage,
percentDifference = percentDifference,
numDifferentPixels = differentPixels
)
} else if (similarPixels > 0) {
DiffResult.Similar(
delta = deltaImage,
numSimilarPixels = similarPixels
)
} else {
DiffResult.Identical(delta = deltaImage)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ internal object PixelPerfect : Differ {
} else {
DiffResult.Different(
delta = deltaImage,
percentDifference = percentDifference
percentDifference = percentDifference,
numDifferentPixels = differentPixels
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class ApngVerifierTest {
fail("Should have already failed")
} catch (e: AssertionError) {
assertThat(e.message).isEqualTo(
"4 frames differed by more than 0.0%\n" +
"4 frames differ\n" +
"Mismatched video fps expected: 1 actual: 3\n" +
" - see details in file://${deltaFile.path}\n\n"
)
Expand Down Expand Up @@ -105,7 +105,7 @@ class ApngVerifierTest {
fail("Should have already failed")
} catch (e: AssertionError) {
assertThat(e.message).isEqualTo(
"4 frames differed by more than 0.0%\n" +
"4 frames differ\n" +
"Mismatched video fps expected: 1 actual: 3\n" +
" - see details in file://${deltaFile.path}\n\n"
)
Expand Down Expand Up @@ -143,7 +143,7 @@ class ApngVerifierTest {
fail("Should have already failed")
} catch (e: AssertionError) {
assertThat(e.message).isEqualTo(
"1 frames differed by more than 0.0%\n" +
"1 frames differ\n" +
"Mismatched video fps expected: 1 actual: 3\n" +
" - see details in file://${deltaFile.path}\n\n"
)
Expand Down Expand Up @@ -178,7 +178,7 @@ class ApngVerifierTest {
fail("Should have already failed")
} catch (e: AssertionError) {
assertThat(e.message).isEqualTo(
"2 frames differed by more than 0.0%\n" +
"2 frames differ\n" +
" - see details in file://${deltaFile.path}\n\n"
)
}
Expand Down Expand Up @@ -214,7 +214,7 @@ class ApngVerifierTest {
fail("Should have already failed")
} catch (e: AssertionError) {
assertThat(e.message).isEqualTo(
"2 frames differed by more than 0.0%\n" +
"2 frames differ\n" +
"Mismatched frame count expected: 3 actual: 5\n" +
" - see details in file://${deltaFile.path}\n\n"
)
Expand Down Expand Up @@ -248,7 +248,7 @@ class ApngVerifierTest {
fail("Should have already failed")
} catch (e: AssertionError) {
assertThat(e.message).isEqualTo(
"1 frames differed by more than 0.0%\n" +
"1 frames differ\n" +
"Mismatched frame count expected: 3 actual: 2\n" +
" - see details in file://${deltaFile.path}\n\n"
)
Expand Down

0 comments on commit f3ac474

Please sign in to comment.