Unverified Commit 8d260f5a authored by Igor Demin's avatar Igor Demin Committed by GitHub

Fix CubicResampler (#176)

Made similar to https://github.com/JetBrains/skija/blob/8057d11ea2b34e92c36188236200cfef58bf8d76/shared/java/CubicResampler.java#L31

BufferedImage.toBitmap() is copied from Compose
parent 9fcf9c10
package org.jetbrains.skia package org.jetbrains.skia
import org.jetbrains.skia.impl.NativePointer
/** /**
* *
...@@ -25,7 +24,7 @@ import org.jetbrains.skia.impl.NativePointer ...@@ -25,7 +24,7 @@ import org.jetbrains.skia.impl.NativePointer
*/ */
class CubicResampler(internal val b: Float, internal val c: Float) : SamplingMode { class CubicResampler(internal val b: Float, internal val c: Float) : SamplingMode {
override fun _pack(): Long = ((b.toBits().toULong() shl 32) or c.toBits().toULong()).toLong() override fun _pack(): Long = (0x8L shl 60) or ((b.toBits().toULong() shl 32) or c.toBits().toULong()).toLong()
override fun equals(o: Any?): Boolean { override fun equals(o: Any?): Boolean {
if (o === this) return true if (o === this) return true
......
package org.jetbrains.skia package org.jetbrains.skia
import org.jetbrains.skia.impl.NativePointer
class FilterMipmap constructor( class FilterMipmap constructor(
internal val filterMode: FilterMode, internal val filterMode: FilterMode,
internal val mipmapMode: MipmapMode = MipmapMode.NONE internal val mipmapMode: MipmapMode = MipmapMode.NONE
......
package org.jetbrains.skiko package org.jetbrains.skiko
import org.jetbrains.skia.Bitmap import org.jetbrains.skia.Bitmap
import org.jetbrains.skia.ColorAlphaType
import org.jetbrains.skia.ColorType import org.jetbrains.skia.ColorType
import org.jetbrains.skia.ImageInfo
import org.jetbrains.skia.Image import org.jetbrains.skia.Image
import java.awt.Transparency import java.awt.Transparency
import java.awt.color.ColorSpace import java.awt.color.ColorSpace
import java.awt.image.BufferedImage import java.awt.image.*
import java.awt.image.ComponentColorModel
import java.awt.image.DataBuffer
import java.awt.image.Raster
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
import javax.imageio.ImageIO
private class DirectDataBuffer(val backing: ByteBuffer): DataBuffer(TYPE_BYTE, backing.limit()) { private class DirectDataBuffer(val backing: ByteBuffer): DataBuffer(TYPE_BYTE, backing.limit()) {
override fun getElem(bank: Int, index: Int): Int { override fun getElem(bank: Int, index: Int): Int {
...@@ -49,12 +46,30 @@ fun Bitmap.toBufferedImage(): BufferedImage { ...@@ -49,12 +46,30 @@ fun Bitmap.toBufferedImage(): BufferedImage {
} }
fun BufferedImage.toBitmap(): Bitmap { fun BufferedImage.toBitmap(): Bitmap {
return Bitmap.makeFromImage(this.toImage()) val bytesPerPixel = 4
val pixels = ByteArray(width * height * bytesPerPixel)
var k = 0
for (y in 0 until height) {
for (x in 0 until width) {
val argb = getRGB(x, y)
val a = (argb shr 24) and 0xff
val r = (argb shr 16) and 0xff
val g = (argb shr 8) and 0xff
val b = (argb shr 0) and 0xff
pixels[k++] = b.toByte()
pixels[k++] = g.toByte()
pixels[k++] = r.toByte()
pixels[k++] = a.toByte()
}
}
val bitmap = Bitmap()
bitmap.allocPixels(ImageInfo.makeS32(width, height, ColorAlphaType.UNPREMUL))
bitmap.installPixels(pixels)
return bitmap
} }
fun BufferedImage.toImage(): Image { fun BufferedImage.toImage(): Image {
val bos = ByteArrayOutputStream() return Image.makeFromBitmap(toBitmap())
ImageIO.write(this, "png", bos)
val data = bos.toByteArray()
return Image.makeFromEncoded(data)
} }
\ No newline at end of file
...@@ -6,8 +6,14 @@ enum class OS(val id: String) { ...@@ -6,8 +6,14 @@ enum class OS(val id: String) {
MacOS("macos") MacOS("macos")
; ;
val isLinux
get() = this == Linux
val isWindows val isWindows
get() = this == Windows get() = this == Windows
val isMacOS
get() = this == MacOS
} }
enum class Arch(val id: String) { enum class Arch(val id: String) {
......
package org.jetbrains.skiko
import org.jetbrains.skia.*
import org.jetbrains.skiko.util.ScreenshotTestRule
import org.jetbrains.skiko.util.loadResourceImage
import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
class PaintTest {
@get:Rule
val screenshots = ScreenshotTestRule()
@Test
fun filterQuality() {
// macOs has different results
assumeTrue(hostOs.isWindows || hostOs.isLinux)
val surface = Surface.makeRasterN32Premul(16, 16)
surface.canvas.drawImageRect(
image = loadResourceImage("test.png"),
src = Rect.makeXYWH(0f, 2f, 2f, 4f),
dst = Rect.makeXYWH(0f, 4f, 4f, 12f),
samplingMode = FilterMipmap(FilterMode.NEAREST, MipmapMode.NONE),
Paint(),
true
)
surface.canvas.drawImageRect(
image = loadResourceImage("test.png"),
src = Rect.makeXYWH(0f, 2f, 2f, 4f),
dst = Rect.makeXYWH(4f, 4f, 4f, 12f),
samplingMode = FilterMipmap(FilterMode.LINEAR, MipmapMode.NONE),
Paint(),
true
)
surface.canvas.drawImageRect(
image = loadResourceImage("test.png"),
src = Rect.makeXYWH(0f, 2f, 2f, 4f),
dst = Rect.makeXYWH(8f, 4f, 4f, 12f),
samplingMode = CubicResampler(1 / 3.0f, 1 / 3.0f),
Paint(),
true
)
screenshots.assert(surface.makeImageSnapshot())
}
}
\ No newline at end of file
package org.jetbrains.skiko.util package org.jetbrains.skiko.util
import org.jetbrains.skia.Image
import org.jetbrains.skia.Pixmap
import java.awt.Color import java.awt.Color
import java.awt.image.BufferedImage import java.io.InputStream
import kotlin.math.abs import kotlin.math.abs
fun isContentSame(img1: BufferedImage, img2: BufferedImage, sensitivity: Double): Boolean { fun isContentSame(img1: Image, img2: Image, sensitivity: Double): Boolean {
require(sensitivity in 0.0..1.0) require(sensitivity in 0.0..1.0)
val sensitivity255 = (sensitivity * 255).toInt() val sensitivity255 = (sensitivity * 255).toInt()
if (img1.width == img2.width && img1.height == img2.height) { if (img1.width == img2.width && img1.height == img2.height) {
for (x in 0 until img1.width) { val pixMap1 = Pixmap()
for (y in 0 until img1.height) { val pixMap2 = Pixmap()
val color1 = Color(img1.getRGB(x, y)) img1.readPixels(pixMap1, 0, 0, false)
val color2 = Color(img2.getRGB(x, y)) img2.readPixels(pixMap2, 0, 0, false)
for (y in 0 until img1.height) {
for (x in 0 until img1.width) {
val color1 = Color(pixMap1.getColor(x, y))
val color2 = Color(pixMap2.getColor(x, y))
if (abs(color1.red - color2.red) > sensitivity255) { if (abs(color1.red - color2.red) > sensitivity255) {
return false return false
} }
...@@ -30,4 +36,21 @@ fun isContentSame(img1: BufferedImage, img2: BufferedImage, sensitivity: Double) ...@@ -30,4 +36,21 @@ fun isContentSame(img1: BufferedImage, img2: BufferedImage, sensitivity: Double)
return false return false
} }
return true return true
} }
\ No newline at end of file
fun loadResourceImage(path: String) = useResource(path, ::loadImage)
inline fun <T> useResource(
resourcePath: String,
block: (InputStream) -> T
): T = openResource(resourcePath).use(block)
fun openResource(resourcePath: String): InputStream {
val classLoader = Thread.currentThread().contextClassLoader!!
return requireNotNull(classLoader.getResourceAsStream(resourcePath)) {
"Resource $resourcePath not found"
}
}
fun loadImage(inputStream: InputStream): Image =
Image.makeFromEncoded(inputStream.readAllBytes())
package org.jetbrains.skiko.util package org.jetbrains.skiko.util
import org.jetbrains.skia.Image
import org.jetbrains.skiko.OS import org.jetbrains.skiko.OS
import org.jetbrains.skiko.hostOs import org.jetbrains.skiko.hostOs
import org.jetbrains.skiko.toImage
import org.junit.rules.TestRule import org.junit.rules.TestRule
import org.junit.runner.Description import org.junit.runner.Description
import org.junit.runners.model.Statement import org.junit.runners.model.Statement
...@@ -41,6 +43,10 @@ class ScreenshotTestRule : TestRule { ...@@ -41,6 +43,10 @@ class ScreenshotTestRule : TestRule {
fun assert(rectangle: Rectangle, id: String = "") { fun assert(rectangle: Rectangle, id: String = "") {
val actual = robot.createScreenCapture(rectangle) val actual = robot.createScreenCapture(rectangle)
assert(actual.toImage(), id)
}
fun assert(actual: Image, id: String = "") {
val name = if (id.isNotEmpty()) "${testIdentifier}_$id" else testIdentifier val name = if (id.isNotEmpty()) "${testIdentifier}_$id" else testIdentifier
val actualFile = File(screenshotsDir, "${name}_actual.png") val actualFile = File(screenshotsDir, "${name}_actual.png")
val expectedFile = File(screenshotsDir, "$name.png") val expectedFile = File(screenshotsDir, "$name.png")
...@@ -48,16 +54,16 @@ class ScreenshotTestRule : TestRule { ...@@ -48,16 +54,16 @@ class ScreenshotTestRule : TestRule {
actualFile.delete() actualFile.delete()
} }
if (expectedFile.exists()) { if (expectedFile.exists()) {
val expected = ImageIO.read(expectedFile) val expected = Image.makeFromEncoded(expectedFile.readBytes())
// macOs screenshots can have different color on different configurations // macOs screenshots can have different color on different configurations
if (!isContentSame(expected, actual, sensitivity = 0.25)) { if (!isContentSame(expected, actual, sensitivity = 0.25)) {
ImageIO.write(actual, "png", actualFile) actualFile.writeBytes(actual.encodeToData()!!.bytes)
throw AssertionError( throw AssertionError(
"Image mismatch! Expected image ${expectedFile.absolutePath}, actual: ${actualFile.absolutePath}" "Image mismatch! Expected image ${expectedFile.absolutePath}, actual: ${actualFile.absolutePath}"
) )
} }
} else { } else {
ImageIO.write(actual, "png", actualFile) actualFile.writeBytes(actual.encodeToData()!!.bytes)
throw AssertionError( throw AssertionError(
"Missing screenshot image " + "Missing screenshot image " +
"${actualFile.absolutePath}. " + "${actualFile.absolutePath}. " +
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment