@@ -39,13 +39,22 @@ import org.jetbrains.kotlinx.dataframe.impl.io.SerializationKeys.METADATA
39
39
import org.jetbrains.kotlinx.dataframe.impl.io.SerializationKeys.NCOL
40
40
import org.jetbrains.kotlinx.dataframe.impl.io.SerializationKeys.NROW
41
41
import org.jetbrains.kotlinx.dataframe.impl.io.SerializationKeys.VERSION
42
+ import org.jetbrains.kotlinx.dataframe.impl.io.resizeKeepingAspectRatio
42
43
import org.jetbrains.kotlinx.dataframe.impl.nothingType
43
44
import org.jetbrains.kotlinx.dataframe.io.JSON.TypeClashTactic.ANY_COLUMNS
44
45
import org.jetbrains.kotlinx.dataframe.io.JSON.TypeClashTactic.ARRAY_AND_VALUE_COLUMNS
45
46
import org.jetbrains.kotlinx.dataframe.testJson
47
+ import org.jetbrains.kotlinx.dataframe.testResource
46
48
import org.jetbrains.kotlinx.dataframe.type
47
49
import org.jetbrains.kotlinx.dataframe.values
48
50
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
49
58
import kotlin.reflect.typeOf
50
59
51
60
class JsonTests {
@@ -1077,4 +1086,141 @@ class JsonTests {
1077
1086
val json = df.toJson()
1078
1087
DataFrame .readJsonStr(json) shouldBe df
1079
1088
}
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
+ }
1080
1226
}
0 commit comments