Skip to content

Commit 02ae27a

Browse files
committed
Extract image serialization tests and utility functions
This commit introduces ImageSerializationTests for validating image base64 encoding. Additionally, a utility function has been added to Utils.kt to parse string into JsonObject for tests.
1 parent 245d690 commit 02ae27a

File tree

6 files changed

+362
-292
lines changed

6 files changed

+362
-292
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.jetbrains.kotlinx.dataframe
22

3+
import com.beust.klaxon.JsonObject
4+
import com.beust.klaxon.Parser
35
import org.jetbrains.kotlinx.dataframe.api.print
46
import org.jetbrains.kotlinx.dataframe.api.schema
57
import org.jetbrains.kotlinx.dataframe.io.renderToString
@@ -24,3 +26,8 @@ fun <T : DataFrame<*>> T.alsoDebug(println: String? = null, rowsLimit: Int = 20)
2426
print(borders = true, title = true, columnTypes = true, valueLimit = -1, rowsLimit = rowsLimit)
2527
schema().print()
2628
}
29+
30+
fun parseJsonStr(jsonStr: String): JsonObject {
31+
val parser = Parser.default()
32+
return parser.parse(StringBuilder(jsonStr)) as JsonObject
33+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package org.jetbrains.kotlinx.dataframe.io
2+
3+
import com.beust.klaxon.JsonArray
4+
import com.beust.klaxon.JsonObject
5+
import io.kotest.matchers.shouldBe
6+
import io.kotest.matchers.string.shouldContain
7+
import org.jetbrains.kotlinx.dataframe.api.dataFrameOf
8+
import org.jetbrains.kotlinx.dataframe.impl.io.SerializationKeys.KOTLIN_DATAFRAME
9+
import org.jetbrains.kotlinx.dataframe.impl.io.resizeKeepingAspectRatio
10+
import org.jetbrains.kotlinx.dataframe.io.Base64ImageEncodingOptions.Companion.ALL_OFF
11+
import org.jetbrains.kotlinx.dataframe.io.Base64ImageEncodingOptions.Companion.GZIP_ON
12+
import org.jetbrains.kotlinx.dataframe.io.Base64ImageEncodingOptions.Companion.LIMIT_SIZE_ON
13+
import org.jetbrains.kotlinx.dataframe.parseJsonStr
14+
import org.jetbrains.kotlinx.dataframe.testResource
15+
import org.junit.Test
16+
import org.junit.runner.RunWith
17+
import org.junit.runners.Parameterized
18+
import java.awt.image.BufferedImage
19+
import java.io.ByteArrayInputStream
20+
import java.io.ByteArrayOutputStream
21+
import java.io.File
22+
import java.util.*
23+
import java.util.zip.GZIPInputStream
24+
import javax.imageio.ImageIO
25+
26+
@RunWith(Parameterized::class)
27+
class ImageSerializationTests(private val encodingOptions: Base64ImageEncodingOptions?) {
28+
@Test
29+
fun `serialize images as base64`() {
30+
val images = readImagesFromResources()
31+
val json = encodeImagesAsJson(images, encodingOptions)
32+
33+
if (encodingOptions == DISABLED) {
34+
checkImagesEncodedAsStrings(json, images.size)
35+
return
36+
}
37+
38+
val decodedImages = decodeImagesFromJson(json, images.size, encodingOptions!!)
39+
40+
for ((decodedImage, original) in decodedImages.zip(images)) {
41+
val expectedImage = resizeIfNeeded(original, encodingOptions)
42+
isImagesIdentical(decodedImage, expectedImage, 2) shouldBe true
43+
}
44+
}
45+
46+
private fun readImagesFromResources(): List<BufferedImage> {
47+
val dir = File(testResource("imgs").path)
48+
49+
return dir.listFiles()?.map { file ->
50+
try {
51+
ImageIO.read(file)
52+
} catch (ex: Exception) {
53+
throw IllegalArgumentException("Error reading ${file.name}: ${ex.message}")
54+
}
55+
} ?: emptyList()
56+
}
57+
58+
private fun encodeImagesAsJson(
59+
images: List<BufferedImage>,
60+
encodingOptions: Base64ImageEncodingOptions?
61+
): JsonObject {
62+
val df = dataFrameOf(listOf("imgs"), images)
63+
val jsonStr = df.toJsonWithMetadata(20, nestedRowLimit = 20, imageEncodingOptions = encodingOptions)
64+
65+
return parseJsonStr(jsonStr)
66+
}
67+
68+
private fun checkImagesEncodedAsStrings(json: JsonObject, numImgs: Int) {
69+
for (i in 0..<numImgs) {
70+
val row = (json[KOTLIN_DATAFRAME] as JsonArray<*>)[i] as JsonObject
71+
val img = row["imgs"] as String
72+
73+
img shouldContain "BufferedImage"
74+
}
75+
}
76+
77+
private fun decodeImagesFromJson(
78+
json: JsonObject,
79+
imgsNum: Int,
80+
encodingOptions: Base64ImageEncodingOptions
81+
): List<BufferedImage> {
82+
val result = mutableListOf<BufferedImage>()
83+
for (i in 0..<imgsNum) {
84+
val row = (json[KOTLIN_DATAFRAME] as JsonArray<*>)[i] as JsonObject
85+
val imgString = row["imgs"] as String
86+
87+
val bytes = decodeBase64Image(imgString, encodingOptions)
88+
val decodedImage = createImageFromBytes(bytes)
89+
90+
result.add(decodedImage)
91+
}
92+
93+
return result
94+
}
95+
96+
private fun decodeBase64Image(imgString: String, encodingOptions: Base64ImageEncodingOptions): ByteArray =
97+
when {
98+
encodingOptions.isGzipOn -> decompressGzip(Base64.getDecoder().decode(imgString))
99+
else -> Base64.getDecoder().decode(imgString)
100+
}
101+
102+
private fun decompressGzip(input: ByteArray): ByteArray {
103+
return ByteArrayOutputStream().use { byteArrayOutputStream ->
104+
GZIPInputStream(input.inputStream()).use { inputStream ->
105+
inputStream.copyTo(byteArrayOutputStream)
106+
}
107+
byteArrayOutputStream.toByteArray()
108+
}
109+
}
110+
111+
private fun resizeIfNeeded(image: BufferedImage, encodingOptions: Base64ImageEncodingOptions): BufferedImage =
112+
when {
113+
!encodingOptions.isLimitSizeOn -> image
114+
else -> image.resizeKeepingAspectRatio(encodingOptions.imageSizeLimit)
115+
}
116+
117+
private fun createImageFromBytes(bytes: ByteArray): BufferedImage {
118+
val bais = ByteArrayInputStream(bytes)
119+
return ImageIO.read(bais)
120+
}
121+
122+
private fun isImagesIdentical(img1: BufferedImage, img2: BufferedImage, allowedDelta: Int): Boolean {
123+
// First check dimensions
124+
if (img1.width != img2.width || img1.height != img2.height) {
125+
return false
126+
}
127+
128+
// Then check each pixel
129+
for (y in 0 until img1.height) {
130+
for (x in 0 until img1.width) {
131+
val rgb1 = img1.getRGB(x, y)
132+
val rgb2 = img2.getRGB(x, y)
133+
134+
val r1 = (rgb1 shr 16) and 0xFF
135+
val g1 = (rgb1 shr 8) and 0xFF
136+
val b1 = rgb1 and 0xFF
137+
138+
val r2 = (rgb2 shr 16) and 0xFF
139+
val g2 = (rgb2 shr 8) and 0xFF
140+
val b2 = rgb2 and 0xFF
141+
142+
val diff = kotlin.math.abs(r1 - r2) + kotlin.math.abs(g1 - g2) + kotlin.math.abs(b1 - b2)
143+
144+
// If the difference in color components exceed our allowance return false
145+
if (diff > allowedDelta) {
146+
return false
147+
}
148+
}
149+
}
150+
151+
// If no exceeding difference was found, the images are identical within our allowedDelta
152+
return true
153+
}
154+
155+
companion object {
156+
private val DEFAULT = Base64ImageEncodingOptions()
157+
private val GZIP_ON_RESIZE_OFF = Base64ImageEncodingOptions(options = GZIP_ON)
158+
private val GZIP_OFF_RESIZE_OFF = Base64ImageEncodingOptions(options = ALL_OFF)
159+
private val GZIP_ON_RESIZE_TO_700 = Base64ImageEncodingOptions(imageSizeLimit = 700, options = GZIP_ON or LIMIT_SIZE_ON)
160+
private val DISABLED = null
161+
162+
@JvmStatic
163+
@Parameterized.Parameters
164+
fun imageEncodingOptionsToTest(): Collection<Base64ImageEncodingOptions?> {
165+
return listOf(
166+
DEFAULT,
167+
GZIP_ON_RESIZE_OFF,
168+
GZIP_OFF_RESIZE_OFF,
169+
GZIP_ON_RESIZE_TO_700,
170+
null,
171+
)
172+
}
173+
}
174+
}

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

Lines changed: 0 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,13 @@ 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
4342
import org.jetbrains.kotlinx.dataframe.impl.nothingType
4443
import org.jetbrains.kotlinx.dataframe.io.JSON.TypeClashTactic.ANY_COLUMNS
4544
import org.jetbrains.kotlinx.dataframe.io.JSON.TypeClashTactic.ARRAY_AND_VALUE_COLUMNS
4645
import org.jetbrains.kotlinx.dataframe.testJson
47-
import org.jetbrains.kotlinx.dataframe.testResource
4846
import org.jetbrains.kotlinx.dataframe.type
4947
import org.jetbrains.kotlinx.dataframe.values
5048
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
5849
import kotlin.reflect.typeOf
5950

6051
class JsonTests {
@@ -1086,141 +1077,4 @@ class JsonTests {
10861077
val json = df.toJson()
10871078
DataFrame.readJsonStr(json) shouldBe df
10881079
}
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-
}
12261080
}

core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/Utils.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.jetbrains.kotlinx.dataframe
22

3+
import com.beust.klaxon.JsonObject
4+
import com.beust.klaxon.Parser
35
import org.jetbrains.kotlinx.dataframe.api.print
46
import org.jetbrains.kotlinx.dataframe.api.schema
57
import org.jetbrains.kotlinx.dataframe.io.renderToString
@@ -24,3 +26,8 @@ fun <T : DataFrame<*>> T.alsoDebug(println: String? = null, rowsLimit: Int = 20)
2426
print(borders = true, title = true, columnTypes = true, valueLimit = -1, rowsLimit = rowsLimit)
2527
schema().print()
2628
}
29+
30+
fun parseJsonStr(jsonStr: String): JsonObject {
31+
val parser = Parser.default()
32+
return parser.parse(StringBuilder(jsonStr)) as JsonObject
33+
}

0 commit comments

Comments
 (0)