Skip to content

Commit 1bd5a3d

Browse files
committed
Add image serialization tests to JsonTests
1 parent b9f7e53 commit 1bd5a3d

File tree

5 files changed

+292
-0
lines changed

5 files changed

+292
-0
lines changed

core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/json.kt

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,22 @@ import org.jetbrains.kotlinx.dataframe.impl.io.SerializationKeys.METADATA
3939
import org.jetbrains.kotlinx.dataframe.impl.io.SerializationKeys.NCOL
4040
import org.jetbrains.kotlinx.dataframe.impl.io.SerializationKeys.NROW
4141
import org.jetbrains.kotlinx.dataframe.impl.io.SerializationKeys.VERSION
42+
import org.jetbrains.kotlinx.dataframe.impl.io.resizeKeepingAspectRatio
4243
import org.jetbrains.kotlinx.dataframe.impl.nothingType
4344
import org.jetbrains.kotlinx.dataframe.io.JSON.TypeClashTactic.ANY_COLUMNS
4445
import org.jetbrains.kotlinx.dataframe.io.JSON.TypeClashTactic.ARRAY_AND_VALUE_COLUMNS
4546
import org.jetbrains.kotlinx.dataframe.testJson
47+
import org.jetbrains.kotlinx.dataframe.testResource
4648
import org.jetbrains.kotlinx.dataframe.type
4749
import org.jetbrains.kotlinx.dataframe.values
4850
import org.junit.Test
51+
import java.awt.image.BufferedImage
52+
import java.io.ByteArrayInputStream
53+
import java.io.ByteArrayOutputStream
54+
import java.io.File
55+
import java.util.*
56+
import java.util.zip.GZIPInputStream
57+
import javax.imageio.ImageIO
4958
import kotlin.reflect.typeOf
5059

5160
class JsonTests {
@@ -1077,4 +1086,141 @@ class JsonTests {
10771086
val json = df.toJson()
10781087
DataFrame.readJsonStr(json) shouldBe df
10791088
}
1089+
1090+
@Test
1091+
fun `serialize images as base64 default`() {
1092+
testSerializeImagesAsBase64()
1093+
}
1094+
1095+
@Test
1096+
fun `serialize images as base64 gzip on resize off`() {
1097+
testSerializeImagesAsBase64(ImageEncodingOptions(encodeAsBase64 = true, options = ImageEncodingOptions.GZIP_ON))
1098+
}
1099+
1100+
@Test
1101+
fun `serialize images as base64 gzip off resize off`() {
1102+
testSerializeImagesAsBase64(ImageEncodingOptions(encodeAsBase64 = true, options = 0))
1103+
}
1104+
1105+
@Test
1106+
fun `serialize images as base64 gzip on resize 700`() {
1107+
testSerializeImagesAsBase64(
1108+
ImageEncodingOptions(
1109+
encodeAsBase64 = true,
1110+
imageSizeLimit = 700,
1111+
options = ImageEncodingOptions.GZIP_ON or ImageEncodingOptions.LIMIT_SIZE_ON
1112+
)
1113+
)
1114+
}
1115+
1116+
@Test
1117+
fun `serialize images with toString`() {
1118+
val images = readImagesFromResources()
1119+
1120+
val df = dataFrameOf(listOf("imgs"), images)
1121+
val jsonStr = df.toJsonWithMetadata(
1122+
20,
1123+
nestedRowLimit = 20,
1124+
imageEncodingOptions = ImageEncodingOptions(encodeAsBase64 = false)
1125+
)
1126+
1127+
val json = parseJsonStr(jsonStr)
1128+
1129+
for (i in images.indices) {
1130+
val row = (json[KOTLIN_DATAFRAME] as JsonArray<*>)[i] as JsonObject
1131+
val img = row["imgs"] as String
1132+
1133+
img shouldContain "BufferedImage"
1134+
}
1135+
}
1136+
1137+
private fun testSerializeImagesAsBase64(encodingOptions: ImageEncodingOptions? = null) {
1138+
val images = readImagesFromResources()
1139+
1140+
val df = dataFrameOf(listOf("imgs"), images)
1141+
val jsonStr = if (encodingOptions == null) {
1142+
df.toJsonWithMetadata(20, nestedRowLimit = 20)
1143+
} else {
1144+
df.toJsonWithMetadata(20, nestedRowLimit = 20, imageEncodingOptions = encodingOptions)
1145+
}
1146+
1147+
val json = parseJsonStr(jsonStr)
1148+
1149+
for (i in images.indices) {
1150+
val row = (json[KOTLIN_DATAFRAME] as JsonArray<*>)[i] as JsonObject
1151+
val imgString = row["imgs"] as String
1152+
val bytes = when {
1153+
encodingOptions == null -> decompressGzip(Base64.getDecoder().decode(imgString))
1154+
encodingOptions.isGzipOn -> decompressGzip(Base64.getDecoder().decode(imgString))
1155+
else -> Base64.getDecoder().decode(imgString)
1156+
}
1157+
1158+
val decodedImage = createImageFromBytes(bytes)!!
1159+
val expectedImage = when {
1160+
encodingOptions != null && !encodingOptions.isLimitSizeOn -> images[i]
1161+
else -> images[i].resizeKeepingAspectRatio(encodingOptions?.imageSizeLimit ?: 600)
1162+
}
1163+
1164+
compareImagesWithDelta(decodedImage, expectedImage, 2) shouldBe true
1165+
}
1166+
}
1167+
1168+
private fun readImagesFromResources(): List<BufferedImage> {
1169+
val dir = File(testResource("imgs").path)
1170+
1171+
return dir.listFiles()?.map { file ->
1172+
try {
1173+
ImageIO.read(file)
1174+
} catch (ex: Exception) {
1175+
throw IllegalArgumentException("Error reading ${file.name}: ${ex.message}")
1176+
}
1177+
} ?: emptyList()
1178+
}
1179+
1180+
private fun decompressGzip(input: ByteArray): ByteArray {
1181+
return ByteArrayOutputStream().use { byteArrayOutputStream ->
1182+
GZIPInputStream(input.inputStream()).use { inputStream ->
1183+
inputStream.copyTo(byteArrayOutputStream)
1184+
}
1185+
byteArrayOutputStream.toByteArray()
1186+
}
1187+
}
1188+
1189+
private fun createImageFromBytes(bytes: ByteArray): BufferedImage? {
1190+
val bais = ByteArrayInputStream(bytes)
1191+
return ImageIO.read(bais)
1192+
}
1193+
1194+
private fun compareImagesWithDelta(img1: BufferedImage, img2: BufferedImage, allowedDelta: Int): Boolean {
1195+
// First check dimensions
1196+
if (img1.width != img2.width || img1.height != img2.height) {
1197+
return false
1198+
}
1199+
1200+
// Then check each pixel
1201+
for (y in 0 until img1.height) {
1202+
for (x in 0 until img1.width) {
1203+
val rgb1 = img1.getRGB(x, y)
1204+
val rgb2 = img2.getRGB(x, y)
1205+
1206+
val r1 = (rgb1 shr 16) and 0xFF
1207+
val g1 = (rgb1 shr 8) and 0xFF
1208+
val b1 = rgb1 and 0xFF
1209+
1210+
val r2 = (rgb2 shr 16) and 0xFF
1211+
val g2 = (rgb2 shr 8) and 0xFF
1212+
val b2 = rgb2 and 0xFF
1213+
1214+
val diff = kotlin.math.abs(r1 - r2) + kotlin.math.abs(g1 - g2) + kotlin.math.abs(b1 - b2)
1215+
1216+
// If the difference in color components exceed our allowance return false
1217+
if (diff > allowedDelta) {
1218+
return false
1219+
}
1220+
}
1221+
}
1222+
1223+
// If no exceeding difference was found, the images are identical within our allowedDelta
1224+
return true
1225+
}
10801226
}

core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/json.kt

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,22 @@ import org.jetbrains.kotlinx.dataframe.impl.io.SerializationKeys.METADATA
3939
import org.jetbrains.kotlinx.dataframe.impl.io.SerializationKeys.NCOL
4040
import org.jetbrains.kotlinx.dataframe.impl.io.SerializationKeys.NROW
4141
import org.jetbrains.kotlinx.dataframe.impl.io.SerializationKeys.VERSION
42+
import org.jetbrains.kotlinx.dataframe.impl.io.resizeKeepingAspectRatio
4243
import org.jetbrains.kotlinx.dataframe.impl.nothingType
4344
import org.jetbrains.kotlinx.dataframe.io.JSON.TypeClashTactic.ANY_COLUMNS
4445
import org.jetbrains.kotlinx.dataframe.io.JSON.TypeClashTactic.ARRAY_AND_VALUE_COLUMNS
4546
import org.jetbrains.kotlinx.dataframe.testJson
47+
import org.jetbrains.kotlinx.dataframe.testResource
4648
import org.jetbrains.kotlinx.dataframe.type
4749
import org.jetbrains.kotlinx.dataframe.values
4850
import org.junit.Test
51+
import java.awt.image.BufferedImage
52+
import java.io.ByteArrayInputStream
53+
import java.io.ByteArrayOutputStream
54+
import java.io.File
55+
import java.util.*
56+
import java.util.zip.GZIPInputStream
57+
import javax.imageio.ImageIO
4958
import kotlin.reflect.typeOf
5059

5160
class JsonTests {
@@ -1077,4 +1086,141 @@ class JsonTests {
10771086
val json = df.toJson()
10781087
DataFrame.readJsonStr(json) shouldBe df
10791088
}
1089+
1090+
@Test
1091+
fun `serialize images as base64 default`() {
1092+
testSerializeImagesAsBase64()
1093+
}
1094+
1095+
@Test
1096+
fun `serialize images as base64 gzip on resize off`() {
1097+
testSerializeImagesAsBase64(ImageEncodingOptions(encodeAsBase64 = true, options = ImageEncodingOptions.GZIP_ON))
1098+
}
1099+
1100+
@Test
1101+
fun `serialize images as base64 gzip off resize off`() {
1102+
testSerializeImagesAsBase64(ImageEncodingOptions(encodeAsBase64 = true, options = 0))
1103+
}
1104+
1105+
@Test
1106+
fun `serialize images as base64 gzip on resize 700`() {
1107+
testSerializeImagesAsBase64(
1108+
ImageEncodingOptions(
1109+
encodeAsBase64 = true,
1110+
imageSizeLimit = 700,
1111+
options = ImageEncodingOptions.GZIP_ON or ImageEncodingOptions.LIMIT_SIZE_ON
1112+
)
1113+
)
1114+
}
1115+
1116+
@Test
1117+
fun `serialize images with toString`() {
1118+
val images = readImagesFromResources()
1119+
1120+
val df = dataFrameOf(listOf("imgs"), images)
1121+
val jsonStr = df.toJsonWithMetadata(
1122+
20,
1123+
nestedRowLimit = 20,
1124+
imageEncodingOptions = ImageEncodingOptions(encodeAsBase64 = false)
1125+
)
1126+
1127+
val json = parseJsonStr(jsonStr)
1128+
1129+
for (i in images.indices) {
1130+
val row = (json[KOTLIN_DATAFRAME] as JsonArray<*>)[i] as JsonObject
1131+
val img = row["imgs"] as String
1132+
1133+
img shouldContain "BufferedImage"
1134+
}
1135+
}
1136+
1137+
private fun testSerializeImagesAsBase64(encodingOptions: ImageEncodingOptions? = null) {
1138+
val images = readImagesFromResources()
1139+
1140+
val df = dataFrameOf(listOf("imgs"), images)
1141+
val jsonStr = if (encodingOptions == null) {
1142+
df.toJsonWithMetadata(20, nestedRowLimit = 20)
1143+
} else {
1144+
df.toJsonWithMetadata(20, nestedRowLimit = 20, imageEncodingOptions = encodingOptions)
1145+
}
1146+
1147+
val json = parseJsonStr(jsonStr)
1148+
1149+
for (i in images.indices) {
1150+
val row = (json[KOTLIN_DATAFRAME] as JsonArray<*>)[i] as JsonObject
1151+
val imgString = row["imgs"] as String
1152+
val bytes = when {
1153+
encodingOptions == null -> decompressGzip(Base64.getDecoder().decode(imgString))
1154+
encodingOptions.isGzipOn -> decompressGzip(Base64.getDecoder().decode(imgString))
1155+
else -> Base64.getDecoder().decode(imgString)
1156+
}
1157+
1158+
val decodedImage = createImageFromBytes(bytes)!!
1159+
val expectedImage = when {
1160+
encodingOptions != null && !encodingOptions.isLimitSizeOn -> images[i]
1161+
else -> images[i].resizeKeepingAspectRatio(encodingOptions?.imageSizeLimit ?: 600)
1162+
}
1163+
1164+
compareImagesWithDelta(decodedImage, expectedImage, 2) shouldBe true
1165+
}
1166+
}
1167+
1168+
private fun readImagesFromResources(): List<BufferedImage> {
1169+
val dir = File(testResource("imgs").path)
1170+
1171+
return dir.listFiles()?.map { file ->
1172+
try {
1173+
ImageIO.read(file)
1174+
} catch (ex: Exception) {
1175+
throw IllegalArgumentException("Error reading ${file.name}: ${ex.message}")
1176+
}
1177+
} ?: emptyList()
1178+
}
1179+
1180+
private fun decompressGzip(input: ByteArray): ByteArray {
1181+
return ByteArrayOutputStream().use { byteArrayOutputStream ->
1182+
GZIPInputStream(input.inputStream()).use { inputStream ->
1183+
inputStream.copyTo(byteArrayOutputStream)
1184+
}
1185+
byteArrayOutputStream.toByteArray()
1186+
}
1187+
}
1188+
1189+
private fun createImageFromBytes(bytes: ByteArray): BufferedImage? {
1190+
val bais = ByteArrayInputStream(bytes)
1191+
return ImageIO.read(bais)
1192+
}
1193+
1194+
private fun compareImagesWithDelta(img1: BufferedImage, img2: BufferedImage, allowedDelta: Int): Boolean {
1195+
// First check dimensions
1196+
if (img1.width != img2.width || img1.height != img2.height) {
1197+
return false
1198+
}
1199+
1200+
// Then check each pixel
1201+
for (y in 0 until img1.height) {
1202+
for (x in 0 until img1.width) {
1203+
val rgb1 = img1.getRGB(x, y)
1204+
val rgb2 = img2.getRGB(x, y)
1205+
1206+
val r1 = (rgb1 shr 16) and 0xFF
1207+
val g1 = (rgb1 shr 8) and 0xFF
1208+
val b1 = rgb1 and 0xFF
1209+
1210+
val r2 = (rgb2 shr 16) and 0xFF
1211+
val g2 = (rgb2 shr 8) and 0xFF
1212+
val b2 = rgb2 and 0xFF
1213+
1214+
val diff = kotlin.math.abs(r1 - r2) + kotlin.math.abs(g1 - g2) + kotlin.math.abs(b1 - b2)
1215+
1216+
// If the difference in color components exceed our allowance return false
1217+
if (diff > allowedDelta) {
1218+
return false
1219+
}
1220+
}
1221+
}
1222+
1223+
// If no exceeding difference was found, the images are identical within our allowedDelta
1224+
return true
1225+
}
10801226
}
3.9 MB
Loading
1.99 MB
Loading
2.09 MB
Loading

0 commit comments

Comments
 (0)