-
Notifications
You must be signed in to change notification settings - Fork 207
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
222 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
apply plugin: 'org.jetbrains.kotlin.jvm' |
160 changes: 160 additions & 0 deletions
160
sample-cli/src/main/java/app/cash/paparazzi/sample/ImageComparer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
package app.cash.paparazzi.sample | ||
|
||
import java.awt.Color | ||
import java.awt.image.BufferedImage | ||
import java.awt.image.BufferedImage.TYPE_INT_ARGB | ||
import java.io.File | ||
import java.io.File.separatorChar | ||
import javax.imageio.ImageIO | ||
|
||
public fun main() { | ||
val image1 = "sample-cli/images1/app.cash.paparazzi.sample_RenderingIssuesTest_example_mac-arm-7ca1390_.png" | ||
val image2 = "sample-cli/images2/app.cash.paparazzi.sample_RenderingIssuesTest_example_mac-arm-7ca1390_.png" | ||
|
||
compareImages(image1, image2) | ||
} | ||
|
||
private fun readImage(image: String): BufferedImage { | ||
val file = File(image) | ||
println("Reading: ${file.absolutePath}") | ||
return ImageIO.read(file) | ||
} | ||
|
||
public fun compareImages(image1: String, image2: String) { | ||
val goldenImage: BufferedImage = readImage(image1) | ||
val image: BufferedImage = readImage(image2) | ||
|
||
val goldenImageWidth = goldenImage.width | ||
val goldenImageHeight = goldenImage.height | ||
|
||
val imageWidth = image.width | ||
val imageHeight = image.height | ||
|
||
val deltaWidth = Math.max(goldenImageWidth, imageWidth) | ||
val deltaHeight = Math.max(goldenImageHeight, imageHeight) | ||
|
||
// Blur the images to account for the scenarios where there are pixel | ||
// differences | ||
// in where a sharp edge occurs | ||
// goldenImage = blur(goldenImage, 6); | ||
// image = blur(image, 6); | ||
val width = goldenImageWidth + deltaWidth + imageWidth | ||
val deltaImage = BufferedImage(width, deltaHeight, TYPE_INT_ARGB) | ||
val g = deltaImage.graphics | ||
|
||
// Compute delta map | ||
var delta: Long = 0 | ||
for (y in 0 until deltaHeight) { | ||
for (x in 0 until deltaWidth) { | ||
val goldenRgb = if (x >= goldenImageWidth || y >= goldenImageHeight) { | ||
0x00808080 | ||
} else { | ||
goldenImage.getRGB(x, y) | ||
} | ||
|
||
val rgb = if (x >= imageWidth || y >= imageHeight) { | ||
0x00808080 | ||
} else { | ||
image.getRGB(x, y) | ||
} | ||
|
||
if (goldenRgb == rgb) { | ||
deltaImage.setRGB(goldenImageWidth + x, y, 0x00808080) | ||
continue | ||
} | ||
|
||
// If the pixels have no opacity, don't delta colors at all | ||
if (goldenRgb and -0x1000000 == 0 && rgb and -0x1000000 == 0) { | ||
deltaImage.setRGB(goldenImageWidth + x, y, 0x00808080) | ||
continue | ||
} | ||
|
||
val deltaR = (rgb and 0xFF0000).ushr(16) - (goldenRgb and 0xFF0000).ushr(16) | ||
val newR = 128 + deltaR and 0xFF | ||
val deltaG = (rgb and 0x00FF00).ushr(8) - (goldenRgb and 0x00FF00).ushr(8) | ||
val newG = 128 + deltaG and 0xFF | ||
val deltaB = (rgb and 0x0000FF) - (goldenRgb and 0x0000FF) | ||
val newB = 128 + deltaB and 0xFF | ||
|
||
val avgAlpha = | ||
((goldenRgb and -0x1000000).ushr(24) + (rgb and -0x1000000).ushr(24)) / 2 shl 24 | ||
|
||
val newRGB = avgAlpha or (newR shl 16) or (newG shl 8) or newB | ||
deltaImage.setRGB(goldenImageWidth + x, y, newRGB) | ||
|
||
delta += Math.abs(deltaR) | ||
.toLong() | ||
delta += Math.abs(deltaG) | ||
.toLong() | ||
delta += Math.abs(deltaB) | ||
.toLong() | ||
} | ||
} | ||
|
||
// 3 different colors, 256 color levels | ||
val total = deltaHeight.toLong() * deltaWidth.toLong() * 3L * 256L | ||
val percentDifference = (delta * 100 / total.toDouble()).toFloat() | ||
|
||
var error: String? = null | ||
val imageName = getName(image2) | ||
if (percentDifference > 0.0) { | ||
error = String.format("Images differ (by %f%%)", percentDifference) | ||
} else if (Math.abs(goldenImageWidth - imageWidth) >= 2) { | ||
error = "Widths differ too much for " + imageName + ": " + | ||
goldenImageWidth + "x" + goldenImageHeight + | ||
"vs" + imageWidth + "x" + imageHeight | ||
} else if (Math.abs(goldenImageHeight - imageHeight) >= 2) { | ||
error = "Heights differ too much for " + imageName + ": " + | ||
goldenImageWidth + "x" + goldenImageHeight + | ||
"vs" + imageWidth + "x" + imageHeight | ||
} | ||
|
||
if (error != null) { | ||
// Expected on the left | ||
// Golden on the right | ||
g.drawImage(goldenImage, 0, 0, null) | ||
g.drawImage(image, goldenImageWidth + deltaWidth, 0, null) | ||
|
||
// Labels | ||
if (deltaWidth > 80) { | ||
g.color = Color.RED | ||
g.drawString("Expected", 10, 20) | ||
g.drawString("Actual", goldenImageWidth + deltaWidth + 10, 20) | ||
} | ||
|
||
val output = File(".", "delta-$imageName") | ||
if (output.exists()) { | ||
val deleted = output.delete() | ||
println("existing output deleted?: $deleted") | ||
} | ||
ImageIO.write(deltaImage, "PNG", output) | ||
error += " - see details in file://" + output.path + "\n" | ||
error = run { | ||
var initialMessage = error | ||
val output1 = File(".", getName(image2)) | ||
if (output1.exists()) { | ||
val deleted = output1.delete() | ||
println("existing output deleted?: $deleted") | ||
} | ||
ImageIO.write(image, "PNG", output1) | ||
initialMessage += "Thumbnail for current rendering stored at file://" + output1.path | ||
// initialMessage += "\nRun the following command to accept the changes:\n"; | ||
// initialMessage += String.format("mv %1$s %2$s", output.getPath(), | ||
// ImageUtils.class.getResource(relativePath).getPath()); | ||
// The above has been commented out, since the destination path returned is in out dir | ||
// and it makes the tests pass without the code being actually checked in. | ||
initialMessage | ||
} | ||
println(error) | ||
throw AssertionError(error) | ||
} else { | ||
println("Images are identical!") | ||
} | ||
|
||
g.dispose() | ||
} | ||
|
||
private fun getName(relativePath: String): String { | ||
return relativePath.substring(relativePath.lastIndexOf(separatorChar) + 1) | ||
} | ||
|
53 changes: 53 additions & 0 deletions
53
sample/src/test/java/app/cash/paparazzi/sample/RenderingIssuesTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package app.cash.paparazzi.sample | ||
|
||
import androidx.compose.foundation.background | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.material.Text | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.graphics.Color | ||
import app.cash.paparazzi.Paparazzi | ||
import com.android.ide.common.rendering.api.SessionParams | ||
import org.junit.Rule | ||
import org.junit.Test | ||
|
||
class RenderingIssuesTest { | ||
@get:Rule | ||
val paparazzi = Paparazzi( | ||
renderingMode = SessionParams.RenderingMode.SHRINK, | ||
) | ||
|
||
@Test | ||
fun example() { | ||
paparazzi.snapshot("${osName()}-${gitShortSha()}") { | ||
Box( | ||
modifier = Modifier.background(Color(0xFF000033)) | ||
) { | ||
Text("ExampleText", color = Color.White) | ||
} | ||
} | ||
} | ||
|
||
fun osName(): String { | ||
val osName = System.getProperty("os.name")!!.lowercase() | ||
return when { | ||
osName.startsWith("windows") -> "win" | ||
osName.startsWith("mac") -> { | ||
val osArch = System.getProperty("os.arch")!!.lowercase() | ||
if (osArch.startsWith("x86")) "mac-x86" else "mac-arm" | ||
} | ||
|
||
else -> "linux" | ||
} | ||
} | ||
|
||
fun gitShortSha(): String { | ||
val command = "git rev-parse --short HEAD~" | ||
val p = ProcessBuilder().command(*command.split(" ").toTypedArray()).start() | ||
val exit = p.waitFor() | ||
if (exit == 0) { | ||
return p.inputStream.bufferedReader().readText() | ||
} else { | ||
throw Exception(p.errorStream.bufferedReader().readText()) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters