diff --git a/core/src/main/common/org/isoron/platform/gui/Canvas.kt b/core/src/main/common/org/isoron/platform/gui/Canvas.kt index b3e1264f9..b397febb4 100644 --- a/core/src/main/common/org/isoron/platform/gui/Canvas.kt +++ b/core/src/main/common/org/isoron/platform/gui/Canvas.kt @@ -47,4 +47,6 @@ interface Canvas { swipeAngle: Double) fun fillCircle(centerX: Double, centerY: Double, radius: Double) fun setTextAlign(align: TextAlign) -} \ No newline at end of file + fun toImage(): Image +} + diff --git a/core/src/main/common/org/isoron/platform/gui/Image.kt b/core/src/main/common/org/isoron/platform/gui/Image.kt new file mode 100644 index 000000000..4f9c41302 --- /dev/null +++ b/core/src/main/common/org/isoron/platform/gui/Image.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.gui + +import kotlin.math.* + +interface Image { + val width: Int + val height: Int + + fun getPixel(x: Int, y: Int): Color + fun setPixel(x: Int, y: Int, color: Color) + + suspend fun export(path: String) + + fun diff(other: Image) { + if (width != other.width) error("Width must match") + if (height != other.height) error("Height must match") + + for (x in 0 until width) { + for (y in 0 until height) { + val p1 = getPixel(x, y) + var l = 1.0 + for (dx in -2..2) { + if (x + dx < 0 || x + dx >= width) continue + for (dy in -2..2) { + if (y + dy < 0 || y + dy >= height) continue + val p2 = other.getPixel(x + dx, y + dy) + l = min(l, abs(p1.luminosity - p2.luminosity)) + } + } + setPixel(x, y, Color(l, l, l, 1.0)) + } + } + } + + val averageLuminosity: Double + get() { + var luminosity = 0.0 + for (x in 0 until width) { + for (y in 0 until height) { + luminosity += getPixel(x, y).luminosity + } + } + return luminosity / (width * height) + } +} \ No newline at end of file diff --git a/core/src/main/common/org/isoron/platform/io/Files.kt b/core/src/main/common/org/isoron/platform/io/Files.kt index e9fe8f92d..babecf082 100644 --- a/core/src/main/common/org/isoron/platform/io/Files.kt +++ b/core/src/main/common/org/isoron/platform/io/Files.kt @@ -19,6 +19,8 @@ package org.isoron.platform.io +import org.isoron.platform.gui.* + interface FileOpener { /** * Opens a file which was shipped bundled with the application, such as a @@ -88,4 +90,9 @@ interface ResourceFile { * Returns true if the file exists. */ suspend fun exists(): Boolean + + /** + * Loads resource file as an image. + */ + suspend fun toImage(): Image } \ No newline at end of file diff --git a/core/src/main/ios/org/isoron/platform/gui/IosCanvas.kt b/core/src/main/ios/org/isoron/platform/gui/IosCanvas.kt index 170589c31..c8f2e6871 100644 --- a/core/src/main/ios/org/isoron/platform/gui/IosCanvas.kt +++ b/core/src/main/ios/org/isoron/platform/gui/IosCanvas.kt @@ -28,8 +28,7 @@ val Color.uicolor: UIColor val Color.cgcolor: CGColorRef? get() = uicolor.CGColor -class IosCanvas() : Canvas { - val ctx = UIGraphicsGetCurrentContext() +class IosCanvas(val ctx: CGContextRef) : Canvas { var textColor = UIColor.blackColor override fun setColor(color: Color) { @@ -79,4 +78,8 @@ class IosCanvas() : Canvas { override fun setTextAlign(align: TextAlign) { } + + override fun toImage(): Image { + TODO() + } } \ No newline at end of file diff --git a/core/src/main/ios/org/isoron/platform/io/IosFiles.kt b/core/src/main/ios/org/isoron/platform/io/IosFiles.kt index c87c7b87b..ef428c9a0 100644 --- a/core/src/main/ios/org/isoron/platform/io/IosFiles.kt +++ b/core/src/main/ios/org/isoron/platform/io/IosFiles.kt @@ -21,6 +21,7 @@ package org.isoron.platform.io +import org.isoron.platform.gui.* import platform.Foundation.* class IosFileOpener : FileOpener { @@ -54,6 +55,11 @@ class IosFile(val path: String) : UserFile, ResourceFile { } override suspend fun copyTo(dest: UserFile) { - NSFileManager.defaultManager.copyItemAtPath(path, (dest as IosFile).path, null) + val manager = NSFileManager.defaultManager + manager.copyItemAtPath(path, (dest as IosFile).path, null) + } + + override suspend fun toImage(): Image { + TODO() } } \ No newline at end of file diff --git a/core/src/main/js/org/isoron/platform/gui/JsCanvas.kt b/core/src/main/js/org/isoron/platform/gui/JsCanvas.kt index 647bf7b97..33a42a7f5 100644 --- a/core/src/main/js/org/isoron/platform/gui/JsCanvas.kt +++ b/core/src/main/js/org/isoron/platform/gui/JsCanvas.kt @@ -27,6 +27,7 @@ import kotlin.math.* class JsCanvas(val element: HTMLCanvasElement, val pixelScale: Double) : Canvas { + val ctx = element.getContext("2d") as CanvasRenderingContext2D var fontSize = 12.0 var fontFamily = "NotoRegular" @@ -57,7 +58,7 @@ class JsCanvas(val element: HTMLCanvasElement, ctx.font = "${fontSize}px ${fontFamily}" ctx.textAlign = align ctx.textBaseline = CanvasTextBaseline.MIDDLE - ctx.fillText(text, toPixel(x), toPixel(y + fontSize * 0.05)) + ctx.fillText(text, toPixel(x), toPixel(y + fontSize * 0.025)) } override fun fillRect(x: Double, y: Double, width: Double, height: Double) { @@ -83,7 +84,7 @@ class JsCanvas(val element: HTMLCanvasElement, } override fun setFont(font: Font) { - fontFamily = when(font) { + fontFamily = when (font) { Font.REGULAR -> "NotoRegular" Font.BOLD -> "NotoBold" Font.FONT_AWESOME -> "FontAwesome" @@ -125,21 +126,18 @@ class JsCanvas(val element: HTMLCanvasElement, } override fun setTextAlign(align: TextAlign) { - this.align = when(align) { + this.align = when (align) { TextAlign.LEFT -> CanvasTextAlign.LEFT TextAlign.CENTER -> CanvasTextAlign.CENTER TextAlign.RIGHT -> CanvasTextAlign.RIGHT } } - suspend fun loadImage(src: String) { - Promise { resolve, reject -> - val img = Image() - img.onload = { - ctx.drawImage(img, 0.0, 0.0) - resolve(0) - } - img.src = src - }.await() + override fun toImage(): Image { + return JsImage(this, + ctx.getImageData(0.0, + 0.0, + element.width.toDouble(), + element.height.toDouble())) } } \ No newline at end of file diff --git a/core/src/main/js/org/isoron/platform/gui/JsImage.kt b/core/src/main/js/org/isoron/platform/gui/JsImage.kt new file mode 100644 index 000000000..66a60f1ef --- /dev/null +++ b/core/src/main/js/org/isoron/platform/gui/JsImage.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.gui + +import org.khronos.webgl.* +import org.w3c.dom.* +import kotlin.browser.* +import kotlin.math.* + +class JsImage(val canvas: JsCanvas, + val imageData: ImageData) : Image { + + override val width: Int + get() = imageData.width + + override val height: Int + get() = imageData.height + + val pixels = imageData.unsafeCast() + + init { + console.log(width, height, imageData.data.length) + } + + override suspend fun export(path: String) { + canvas.ctx.putImageData(imageData, 0.0, 0.0) + val container = document.createElement("div") + container.className = "export" + val title = document.createElement("div") + title.innerHTML = path + document.body?.appendChild(container) + container.appendChild(title) + container.appendChild(canvas.element) + } + + override fun getPixel(x: Int, y: Int): Color { + val offset = 4 * (y * width + x) + return Color(imageData.data[offset + 0] / 255.0, + imageData.data[offset + 1] / 255.0, + imageData.data[offset + 2] / 255.0, + imageData.data[offset + 3] / 255.0) + } + + override fun setPixel(x: Int, y: Int, color: Color) { + val offset = 4 * (y * width + x) + inline fun map(x: Double): Byte { + return (x * 255).roundToInt().unsafeCast() + } + imageData.data.set(offset + 0, map(color.red)) + imageData.data.set(offset + 1, map(color.green)) + imageData.data.set(offset + 2, map(color.blue)) + imageData.data.set(offset + 3, map(color.alpha)) + } + +} \ No newline at end of file diff --git a/core/src/main/js/org/isoron/platform/io/JsFiles.kt b/core/src/main/js/org/isoron/platform/io/JsFiles.kt index 36ba11cfb..7b8257856 100644 --- a/core/src/main/js/org/isoron/platform/io/JsFiles.kt +++ b/core/src/main/js/org/isoron/platform/io/JsFiles.kt @@ -20,7 +20,11 @@ package org.isoron.platform.io import kotlinx.coroutines.* +import org.isoron.platform.gui.* +import org.isoron.platform.gui.Image +import org.w3c.dom.* import org.w3c.xhr.* +import kotlin.browser.* import kotlin.js.* class JsFileStorage { @@ -142,4 +146,20 @@ class JsResourceFile(val filename: String) : ResourceFile { val fs = (dest as JsUserFile).fs fs.put(dest.filename, lines().joinToString("\n")) } + + override suspend fun toImage(): Image { + return Promise { resolve, reject -> + val img = org.w3c.dom.Image() + img.onload = { + val canvas = JsCanvas(document.createElement("canvas") as HTMLCanvasElement, 1.0) + canvas.element.width = img.naturalWidth + canvas.element.height = img.naturalHeight + canvas.setColor(Color(0xffffff)) + canvas.fillRect(0.0, 0.0, canvas.getWidth(), canvas.getHeight()) + canvas.ctx.drawImage(img, 0.0, 0.0) + resolve(canvas.toImage()) + } + img.src = "/assets/$filename" + }.await() + } } diff --git a/core/src/main/jvm/org/isoron/platform/gui/JavaCanvas.kt b/core/src/main/jvm/org/isoron/platform/gui/JavaCanvas.kt index 416ca5fd2..04740b3d3 100644 --- a/core/src/main/jvm/org/isoron/platform/gui/JavaCanvas.kt +++ b/core/src/main/jvm/org/isoron/platform/gui/JavaCanvas.kt @@ -30,6 +30,9 @@ import kotlin.math.* class JavaCanvas(val image: BufferedImage, val pixelScale: Double = 2.0) : Canvas { + override fun toImage(): Image { + return JavaImage(image) + } private val frc = FontRenderContext(null, true, true) private var fontSize = 12.0 diff --git a/core/src/main/jvm/org/isoron/platform/gui/JavaImage.kt b/core/src/main/jvm/org/isoron/platform/gui/JavaImage.kt new file mode 100644 index 000000000..db6d5cb5a --- /dev/null +++ b/core/src/main/jvm/org/isoron/platform/gui/JavaImage.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.gui + +import java.awt.image.* +import java.io.* +import javax.imageio.* + +class JavaImage(val bufferedImage: BufferedImage) : Image { + override fun setPixel(x: Int, y: Int, color: Color) { + bufferedImage.setRGB(x, y, java.awt.Color(color.red.toFloat(), + color.green.toFloat(), + color.blue.toFloat()).rgb) + } + + override suspend fun export(path: String) { + val file = File(path) + file.parentFile.mkdirs() + ImageIO.write(bufferedImage, "png", file) + } + + override val width: Int + get() = bufferedImage.width + + override val height: Int + get() = bufferedImage.height + + override fun getPixel(x: Int, y: Int): Color { + return Color(bufferedImage.getRGB(x, y)) + } +} \ No newline at end of file diff --git a/core/src/main/jvm/org/isoron/platform/io/JavaFiles.kt b/core/src/main/jvm/org/isoron/platform/io/JavaFiles.kt index 2092794d6..1bb3384e8 100644 --- a/core/src/main/jvm/org/isoron/platform/io/JavaFiles.kt +++ b/core/src/main/jvm/org/isoron/platform/io/JavaFiles.kt @@ -19,8 +19,10 @@ package org.isoron.platform.io +import org.isoron.platform.gui.* import java.io.* import java.nio.file.* +import javax.imageio.* class JavaResourceFile(val path: String) : ResourceFile { private val javaPath: Path @@ -49,6 +51,10 @@ class JavaResourceFile(val path: String) : ResourceFile { fun stream(): InputStream { return Files.newInputStream(javaPath) } + + override suspend fun toImage(): Image { + return JavaImage(ImageIO.read(stream())) + } } class JavaUserFile(val path: Path) : UserFile { diff --git a/core/src/test/common/org/isoron/DependencyResolver.kt b/core/src/test/common/org/isoron/DependencyResolver.kt index 289e99b11..4d445f251 100644 --- a/core/src/test/common/org/isoron/DependencyResolver.kt +++ b/core/src/test/common/org/isoron/DependencyResolver.kt @@ -27,17 +27,9 @@ enum class Locale { US, JAPAN } -interface CanvasHelper { - fun createCanvas(width: Int, height: Int): Canvas - suspend fun exportCanvas(canvas: Canvas, filename: String) - suspend fun compare(imageFile: ResourceFile, canvas: Canvas): Double -} - expect object DependencyResolver { - val supportsDatabaseTests: Boolean - val supportsCanvasTests: Boolean suspend fun getFileOpener(): FileOpener suspend fun getDatabase(): Database - fun getCanvasHelper(): CanvasHelper fun getDateFormatter(locale: Locale): LocalDateFormatter + fun createCanvas(width: Int, height: Int): Canvas } \ No newline at end of file diff --git a/core/src/test/common/org/isoron/platform/gui/CanvasTest.kt b/core/src/test/common/org/isoron/platform/gui/CanvasTest.kt index 2d4856885..1c2065cde 100644 --- a/core/src/test/common/org/isoron/platform/gui/CanvasTest.kt +++ b/core/src/test/common/org/isoron/platform/gui/CanvasTest.kt @@ -26,9 +26,7 @@ import kotlin.test.* class CanvasTest: BaseViewTest() { @Test fun run() = asyncTest{ - if (!DependencyResolver.supportsCanvasTests) return@asyncTest - val helper = DependencyResolver.getCanvasHelper() - val canvas = helper.createCanvas(500, 400) + val canvas = DependencyResolver.createCanvas(500, 400) canvas.setColor(Color(0x303030)) canvas.fillRect(0.0, 0.0, 500.0, 400.0) diff --git a/core/src/test/common/org/isoron/platform/io/DatabaseTest.kt b/core/src/test/common/org/isoron/platform/io/DatabaseTest.kt index dd5745549..533ab2489 100644 --- a/core/src/test/common/org/isoron/platform/io/DatabaseTest.kt +++ b/core/src/test/common/org/isoron/platform/io/DatabaseTest.kt @@ -26,7 +26,6 @@ class DatabaseTest { @Test fun testUsage() = asyncTest{ - if (!DependencyResolver.supportsDatabaseTests) return@asyncTest val db = DependencyResolver.getDatabase() db.setVersion(0) diff --git a/core/src/test/common/org/isoron/uhabits/BaseViewTest.kt b/core/src/test/common/org/isoron/uhabits/BaseViewTest.kt index 59c554eaf..8ba9d83a7 100644 --- a/core/src/test/common/org/isoron/uhabits/BaseViewTest.kt +++ b/core/src/test/common/org/isoron/uhabits/BaseViewTest.kt @@ -24,8 +24,6 @@ import org.isoron.platform.gui.* import org.isoron.uhabits.components.* import kotlin.test.* -var SIMILARITY_THRESHOLD = 5.0 - open class BaseViewTest { var theme = LightTheme() suspend fun assertRenders(width: Int, @@ -33,31 +31,34 @@ open class BaseViewTest { expectedPath: String, component: Component) { - val helper = DependencyResolver.getCanvasHelper() - val canvas = helper.createCanvas(width, height) + val canvas = DependencyResolver.createCanvas(width, height) component.draw(canvas) assertRenders(expectedPath, canvas) } - suspend fun assertRenders(expectedPath: String, + suspend fun assertRenders(path: String, canvas: Canvas) { - val helper = DependencyResolver.getCanvasHelper() + val actualImage = canvas.toImage() + val failedActualPath = "/tmp/failed/${path}" + val failedExpectedPath = failedActualPath.replace(".png", ".expected.png") + val failedDiffPath = failedActualPath.replace(".png", ".diff.png") val fileOpener = DependencyResolver.getFileOpener() - val expectedFile = fileOpener.openResourceFile(expectedPath) - val actualPath = "/failed/${expectedPath}" - + val expectedFile = fileOpener.openResourceFile(path) if (expectedFile.exists()) { - val d = helper.compare(expectedFile, canvas) - if (d >= SIMILARITY_THRESHOLD) { - helper.exportCanvas(canvas, actualPath) - val expectedCopy = expectedPath.replace(".png", ".expected.png") - expectedFile.copyTo(fileOpener.openUserFile("/failed/$expectedCopy")) - fail("Images differ (distance=${d}). Actual rendered saved to ${actualPath}.") + val expectedImage = expectedFile.toImage() + val diffImage = expectedFile.toImage() + diffImage.diff(actualImage) + val distance = diffImage.averageLuminosity * 100 + if (distance >= 1.0) { + expectedImage.export(failedExpectedPath) + actualImage.export(failedActualPath) + diffImage.export(failedDiffPath) + fail("Images differ (distance=${distance})") } } else { - helper.exportCanvas(canvas, actualPath) - fail("Expected file is missing. Actual render saved to $actualPath") + actualImage.export(failedActualPath) + fail("Expected image file is missing.") } } } \ No newline at end of file diff --git a/core/src/test/common/org/isoron/uhabits/models/CheckmarkRepositoryTest.kt b/core/src/test/common/org/isoron/uhabits/models/CheckmarkRepositoryTest.kt index af9c2c7c2..cdc97b17e 100644 --- a/core/src/test/common/org/isoron/uhabits/models/CheckmarkRepositoryTest.kt +++ b/core/src/test/common/org/isoron/uhabits/models/CheckmarkRepositoryTest.kt @@ -28,7 +28,6 @@ import kotlin.test.* class CheckmarkRepositoryTest() { @Test fun testCRUD() = asyncTest { - if (!DependencyResolver.supportsDatabaseTests) return@asyncTest val db = DependencyResolver.getDatabase() val habitA = 10 diff --git a/core/src/test/common/org/isoron/uhabits/models/HabitRepositoryTest.kt b/core/src/test/common/org/isoron/uhabits/models/HabitRepositoryTest.kt index 71a4582f3..faa234d78 100644 --- a/core/src/test/common/org/isoron/uhabits/models/HabitRepositoryTest.kt +++ b/core/src/test/common/org/isoron/uhabits/models/HabitRepositoryTest.kt @@ -26,7 +26,6 @@ import kotlin.test.* class HabitRepositoryTest() { @Test fun testCRUD() = asyncTest{ - if (!DependencyResolver.supportsDatabaseTests) return@asyncTest val db = DependencyResolver.getDatabase() val original0 = Habit(id = 0, name = "Wake up early", diff --git a/core/src/test/common/org/isoron/uhabits/models/PreferencesRepositoryTest.kt b/core/src/test/common/org/isoron/uhabits/models/PreferencesRepositoryTest.kt index dde14ac44..b24b65ecb 100644 --- a/core/src/test/common/org/isoron/uhabits/models/PreferencesRepositoryTest.kt +++ b/core/src/test/common/org/isoron/uhabits/models/PreferencesRepositoryTest.kt @@ -26,7 +26,6 @@ import kotlin.test.* class PreferencesRepositoryTest() { @Test fun testUsage() = asyncTest{ - if (!DependencyResolver.supportsDatabaseTests) return@asyncTest val db = DependencyResolver.getDatabase() val prefs = PreferencesRepository(db) assertEquals("default", prefs.getString("non_existing_key", "default")) diff --git a/core/src/test/ios/org/isoron/DependencyResolver.kt b/core/src/test/ios/org/isoron/DependencyResolver.kt index ecd47752c..4a5aa4304 100644 --- a/core/src/test/ios/org/isoron/DependencyResolver.kt +++ b/core/src/test/ios/org/isoron/DependencyResolver.kt @@ -19,23 +19,28 @@ package org.isoron +import org.isoron.platform.gui.* import org.isoron.platform.io.* import org.isoron.platform.time.* +import platform.CoreGraphics.* +import platform.UIKit.* actual object DependencyResolver { actual suspend fun getFileOpener(): FileOpener = IosFileOpener() actual fun getDateFormatter(locale: Locale): LocalDateFormatter { - return when(locale) { + return when (locale) { Locale.US -> IosLocalDateFormatter("en-US") Locale.JAPAN -> IosLocalDateFormatter("ja-JP") } } - // IosDatabase and IosCanvas are currently implemented in Swift, so we - // cannot test these classes here. The tests will be skipped. + actual fun createCanvas(width: Int, height: Int): Canvas { + UIGraphicsBeginImageContext(CGSizeMake(width.toDouble(), height.toDouble())) + val ctx = UIGraphicsGetCurrentContext()!! + return IosCanvas(ctx) + } + actual suspend fun getDatabase(): Database = TODO() - actual fun getCanvasHelper(): CanvasHelper = TODO() - actual val supportsDatabaseTests = false - actual val supportsCanvasTests = false + } \ No newline at end of file diff --git a/core/src/test/js/org/isoron/DependencyResolver.kt b/core/src/test/js/org/isoron/DependencyResolver.kt index 2775e8998..fa3e5e49c 100644 --- a/core/src/test/js/org/isoron/DependencyResolver.kt +++ b/core/src/test/js/org/isoron/DependencyResolver.kt @@ -23,14 +23,10 @@ import org.isoron.platform.gui.* import org.isoron.platform.io.* import org.isoron.platform.time.* import org.isoron.uhabits.* -import org.khronos.webgl.* import org.w3c.dom.* import kotlin.browser.* -import kotlin.math.* actual object DependencyResolver { - actual val supportsDatabaseTests = true - actual val supportsCanvasTests = true var fileOpener: JsFileOpener? = null actual suspend fun getFileOpener(): FileOpener { @@ -49,78 +45,22 @@ actual object DependencyResolver { return db } - actual fun getCanvasHelper(): CanvasHelper { - return JsCanvasHelper() - } - actual fun getDateFormatter(locale: Locale): LocalDateFormatter { return when (locale) { Locale.US -> JsDateFormatter("en-US") Locale.JAPAN -> JsDateFormatter("ja-JP") } } -} - -class JsCanvasHelper : CanvasHelper { - override suspend fun compare(imageFile: ResourceFile, - canvas: Canvas): Double { - canvas as JsCanvas - imageFile as JsResourceFile - val width = canvas.element.width - val height = canvas.element.height - - val expectedCanvasElement = document.createElement("canvas") as HTMLCanvasElement - expectedCanvasElement.width = width - expectedCanvasElement.height = height - expectedCanvasElement.style.width = canvas.element.style.width - expectedCanvasElement.style.height = canvas.element.style.height - expectedCanvasElement.className = "canvasTest" - document.body?.appendChild(expectedCanvasElement) - val expectedCanvas = JsCanvas(expectedCanvasElement, 1.0) - expectedCanvas.loadImage("../assets/${imageFile.filename}") - - val actualData = canvas.ctx.getImageData(0.0, - 0.0, - width.toDouble(), - height.toDouble()).data - val expectedData = expectedCanvas.ctx.getImageData(0.0, - 0.0, - width.toDouble(), - height.toDouble()).data - - var distance = 0.0; - for (x in 0 until width) { - for (y in 0 until height) { - val k = (y * width + x) * 4 - distance += abs(actualData[k] - expectedData[k]) - distance += abs(actualData[k + 1] - expectedData[k + 1]) - distance += abs(actualData[k + 2] - expectedData[k + 2]) - distance += abs(actualData[k + 3] - expectedData[k + 3]) - } - } - - val adjustedDistance = distance / 255.0 / 4 / 1000 - - if (adjustedDistance > SIMILARITY_THRESHOLD) { - expectedCanvasElement.style.display = "block" - canvas.element.style.display = "block" - } - - return adjustedDistance - } - - override fun createCanvas(width: Int, height: Int): Canvas { - val canvasElement = document.createElement("canvas") as HTMLCanvasElement - canvasElement.width = width * 2 - canvasElement.height = height * 2 - canvasElement.style.width = "${width}px" - canvasElement.style.height = "${height}px" - canvasElement.className = "canvasTest" - document.body?.appendChild(canvasElement) - return JsCanvas(canvasElement, 2.0) - } - override suspend fun exportCanvas(canvas: Canvas, filename: String) { - // do nothing + actual fun createCanvas(width: Int, height: Int): Canvas { + val element = document.createElement("canvas") as HTMLCanvasElement + element.width = 2 * width + element.height = 2 * height + element.style.width = "${2 * width}px" + element.style.height = "${2 * height}px" + val canvas = JsCanvas(element, 2.0) + canvas.setColor(Color(0xffffff)) + canvas.fillRect(0.0, 0.0, width.toDouble(), height.toDouble()) + return canvas } -} \ No newline at end of file +} diff --git a/core/src/test/jvm/org/isoron/DependencyResolver.kt b/core/src/test/jvm/org/isoron/DependencyResolver.kt index c0819ef20..7ea965d2e 100644 --- a/core/src/test/jvm/org/isoron/DependencyResolver.kt +++ b/core/src/test/jvm/org/isoron/DependencyResolver.kt @@ -30,11 +30,7 @@ import java.nio.file.* import javax.imageio.* actual object DependencyResolver { - actual val supportsDatabaseTests = true - actual val supportsCanvasTests = true - actual suspend fun getFileOpener(): FileOpener = JavaFileOpener() - actual fun getCanvasHelper(): CanvasHelper = JavaCanvasHelper() actual suspend fun getDatabase(): Database { val log = StandardLog() @@ -54,45 +50,13 @@ actual object DependencyResolver { Locale.JAPAN -> JavaLocalDateFormatter(java.util.Locale.JAPAN) } } -} - -class JavaCanvasHelper : CanvasHelper { - override suspend fun compare(imageFile: ResourceFile, canvas: Canvas): Double { - val actual = (canvas as JavaCanvas).image - val expected = ImageIO.read((imageFile as JavaResourceFile).stream()) - return compare(expected, actual) - } - private fun compare(expected: BufferedImage, - actual: BufferedImage): Double { - - if (actual.width != expected.width) return Double.POSITIVE_INFINITY - if (actual.height != expected.height) return Double.POSITIVE_INFINITY - - var distance = 0.0; - for (x in 0 until actual.width) { - for (y in 0 until actual.height) { - val p1 = Color(actual.getRGB(x, y)) - val p2 = Color(expected.getRGB(x, y)) - distance += abs(p1.red - p2.red) - distance += abs(p1.green - p2.green) - distance += abs(p1.blue - p2.blue) - } - } - - return distance / 4.0 - } - - override fun createCanvas(width: Int, height: Int): Canvas { + actual fun createCanvas(width: Int, height: Int): Canvas { val widthPx = width * 2 val heightPx = height * 2 - val image = BufferedImage(widthPx, heightPx, BufferedImage.TYPE_INT_ARGB) + val image = BufferedImage(widthPx, + heightPx, + BufferedImage.TYPE_INT_ARGB) return JavaCanvas(image, pixelScale = 2.0) } - - override suspend fun exportCanvas(canvas: Canvas, filename: String) { - val file = File("/tmp/$filename") - file.parentFile.mkdirs() - ImageIO.write((canvas as JavaCanvas).image, "png", file) - } -} \ No newline at end of file +} diff --git a/web/src/test/index.html b/web/src/test/index.html index 7db16233f..5bfeb0601 100644 --- a/web/src/test/index.html +++ b/web/src/test/index.html @@ -19,10 +19,12 @@ font-family: "NotoBold"; src: url(../assets/fonts/NotoSans-Bold.ttf) format("truetype"); } - .canvasTest { + .export { + text-align: center; + } + .export canvas { border: 1px solid #000; - display: none; - margin: 10px auto; + margin: 10px; }