react-native-image-compression-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +562 -0
- package/android/build.gradle +63 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/imagecompressionkit/ImageCompressionKitModule.kt +1127 -0
- package/android/src/main/java/com/imagecompressionkit/ImageCompressionKitPackage.kt +33 -0
- package/android/src/main/java/com/imagecompressionkit/ImageCompressionOutput.kt +396 -0
- package/android/src/main/java/com/imagecompressionkit/JpegExifMetadata.kt +233 -0
- package/ios/RCTImageCompressionKit.h +10 -0
- package/ios/RCTImageCompressionKit.mm +72 -0
- package/lib/NativeImageCompressionKit.d.ts +55 -0
- package/lib/NativeImageCompressionKit.d.ts.map +1 -0
- package/lib/NativeImageCompressionKit.js +5 -0
- package/lib/NativeImageCompressionKit.js.map +1 -0
- package/lib/api.d.ts +4 -0
- package/lib/api.d.ts.map +1 -0
- package/lib/api.js +25 -0
- package/lib/api.js.map +1 -0
- package/lib/errors.d.ts +9 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +53 -0
- package/lib/errors.js.map +1 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +14 -0
- package/lib/index.js.map +1 -0
- package/lib/nativeModule.d.ts +7 -0
- package/lib/nativeModule.d.ts.map +1 -0
- package/lib/nativeModule.js +73 -0
- package/lib/nativeModule.js.map +1 -0
- package/lib/types.d.ts +59 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +23 -0
- package/lib/types.js.map +1 -0
- package/lib/validation.d.ts +3 -0
- package/lib/validation.d.ts.map +1 -0
- package/lib/validation.js +108 -0
- package/lib/validation.js.map +1 -0
- package/package.json +106 -0
- package/react-native-image-compression-kit.podspec +21 -0
- package/react-native.config.js +12 -0
- package/src/NativeImageCompressionKit.ts +81 -0
- package/src/api.ts +28 -0
- package/src/errors.ts +91 -0
- package/src/index.ts +25 -0
- package/src/nativeModule.ts +130 -0
- package/src/types.ts +88 -0
- package/src/validation.ts +181 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.imagecompressionkit
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
|
|
9
|
+
class ImageCompressionKitPackage : BaseReactPackage() {
|
|
10
|
+
override fun getModule(
|
|
11
|
+
name: String,
|
|
12
|
+
reactContext: ReactApplicationContext
|
|
13
|
+
): NativeModule? =
|
|
14
|
+
if (name == ImageCompressionKitModule.NAME) {
|
|
15
|
+
ImageCompressionKitModule(reactContext)
|
|
16
|
+
} else {
|
|
17
|
+
null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
|
|
21
|
+
ReactModuleInfoProvider {
|
|
22
|
+
mapOf(
|
|
23
|
+
ImageCompressionKitModule.NAME to ReactModuleInfo(
|
|
24
|
+
ImageCompressionKitModule.NAME,
|
|
25
|
+
ImageCompressionKitModule.NAME,
|
|
26
|
+
false,
|
|
27
|
+
false,
|
|
28
|
+
false,
|
|
29
|
+
true
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
package com.imagecompressionkit
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.net.Uri
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import java.io.File
|
|
7
|
+
import java.io.FileOutputStream
|
|
8
|
+
import java.util.UUID
|
|
9
|
+
|
|
10
|
+
internal data class CompressionOutputDimensions(
|
|
11
|
+
val width: Int,
|
|
12
|
+
val height: Int
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
internal data class CompressionOutputResultMetadata(
|
|
16
|
+
val uri: String,
|
|
17
|
+
val format: String,
|
|
18
|
+
val width: Int,
|
|
19
|
+
val height: Int,
|
|
20
|
+
val byteSize: Long,
|
|
21
|
+
val originalByteSize: Long,
|
|
22
|
+
val compressionRatio: Double
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
internal data class CompressionFormatCapability(
|
|
26
|
+
val format: String,
|
|
27
|
+
val input: Boolean,
|
|
28
|
+
val output: Boolean,
|
|
29
|
+
val supportsAlpha: Boolean,
|
|
30
|
+
val supportsAnimation: Boolean,
|
|
31
|
+
val notes: List<String>
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
internal enum class OutputFormat(
|
|
35
|
+
val value: String,
|
|
36
|
+
val fileExtension: String,
|
|
37
|
+
val supportsTargetSizeCompression: Boolean,
|
|
38
|
+
val supportsJpegExifMetadata: Boolean
|
|
39
|
+
) {
|
|
40
|
+
JPEG(
|
|
41
|
+
value = "jpeg",
|
|
42
|
+
fileExtension = "jpg",
|
|
43
|
+
supportsTargetSizeCompression = true,
|
|
44
|
+
supportsJpegExifMetadata = true
|
|
45
|
+
),
|
|
46
|
+
PNG(
|
|
47
|
+
value = "png",
|
|
48
|
+
fileExtension = "png",
|
|
49
|
+
supportsTargetSizeCompression = false,
|
|
50
|
+
supportsJpegExifMetadata = false
|
|
51
|
+
),
|
|
52
|
+
WEBP(
|
|
53
|
+
value = "webp",
|
|
54
|
+
fileExtension = "webp",
|
|
55
|
+
supportsTargetSizeCompression = true,
|
|
56
|
+
supportsJpegExifMetadata = false
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
val compressFormat: Bitmap.CompressFormat
|
|
60
|
+
get() = when (this) {
|
|
61
|
+
JPEG -> Bitmap.CompressFormat.JPEG
|
|
62
|
+
PNG -> Bitmap.CompressFormat.PNG
|
|
63
|
+
WEBP -> webpCompressFormat()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fun compressionQuality(quality: Int): Int =
|
|
67
|
+
if (this == PNG) {
|
|
68
|
+
100
|
|
69
|
+
} else {
|
|
70
|
+
quality
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Suppress("DEPRECATION")
|
|
74
|
+
private fun webpCompressFormat(): Bitmap.CompressFormat =
|
|
75
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
76
|
+
Bitmap.CompressFormat.WEBP_LOSSY
|
|
77
|
+
} else {
|
|
78
|
+
Bitmap.CompressFormat.WEBP
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
companion object {
|
|
82
|
+
fun fromValue(value: String?): OutputFormat? =
|
|
83
|
+
values().firstOrNull { it.value == value }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
internal object ImageCompressionOutput {
|
|
88
|
+
const val MAX_BYTES_UNSUPPORTED_MESSAGE =
|
|
89
|
+
"Android MVP supports output.maxBytes for JPEG and WebP output only."
|
|
90
|
+
|
|
91
|
+
val FORMAT_VALUES = arrayOf(
|
|
92
|
+
JPEG_FORMAT,
|
|
93
|
+
PNG_FORMAT,
|
|
94
|
+
WEBP_FORMAT,
|
|
95
|
+
HEIC_FORMAT,
|
|
96
|
+
HEIF_FORMAT,
|
|
97
|
+
"avif",
|
|
98
|
+
GIF_FORMAT
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
fun fromValue(value: String?): OutputFormat? =
|
|
102
|
+
OutputFormat.fromValue(value)
|
|
103
|
+
|
|
104
|
+
fun maxBytesValidationError(
|
|
105
|
+
outputFormat: OutputFormat,
|
|
106
|
+
maxBytes: Long?
|
|
107
|
+
): String? =
|
|
108
|
+
if (maxBytes != null && !outputFormat.supportsTargetSizeCompression) {
|
|
109
|
+
MAX_BYTES_UNSUPPORTED_MESSAGE
|
|
110
|
+
} else {
|
|
111
|
+
null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fun createOutputFile(cacheDir: File, outputFormat: OutputFormat): File {
|
|
115
|
+
val outputDir = File(cacheDir, OUTPUT_DIRECTORY_NAME)
|
|
116
|
+
if (!outputDir.exists()) {
|
|
117
|
+
outputDir.mkdirs()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return File(
|
|
121
|
+
outputDir,
|
|
122
|
+
"compressed-${System.currentTimeMillis()}-${UUID.randomUUID()}.${outputFormat.fileExtension}"
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fun encodeBitmap(
|
|
127
|
+
bitmap: Bitmap,
|
|
128
|
+
outputFile: File,
|
|
129
|
+
outputFormat: OutputFormat,
|
|
130
|
+
quality: Int,
|
|
131
|
+
maxBytes: Long?,
|
|
132
|
+
copiedExifMetadata: CopiedExifMetadata? = null
|
|
133
|
+
): Boolean =
|
|
134
|
+
if (maxBytes == null) {
|
|
135
|
+
encodeBitmapAtQuality(
|
|
136
|
+
bitmap,
|
|
137
|
+
outputFile,
|
|
138
|
+
outputFormat,
|
|
139
|
+
quality,
|
|
140
|
+
copiedExifMetadata
|
|
141
|
+
)
|
|
142
|
+
} else {
|
|
143
|
+
encodeBitmapToTargetSize(
|
|
144
|
+
bitmap,
|
|
145
|
+
outputFile,
|
|
146
|
+
outputFormat,
|
|
147
|
+
quality,
|
|
148
|
+
maxBytes,
|
|
149
|
+
copiedExifMetadata
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fun createResultMetadata(
|
|
154
|
+
originalByteSize: Long,
|
|
155
|
+
outputFile: File,
|
|
156
|
+
dimensions: CompressionOutputDimensions,
|
|
157
|
+
outputFormat: OutputFormat
|
|
158
|
+
): CompressionOutputResultMetadata {
|
|
159
|
+
val byteSize = outputFile.length()
|
|
160
|
+
|
|
161
|
+
return CompressionOutputResultMetadata(
|
|
162
|
+
uri = Uri.fromFile(outputFile).toString(),
|
|
163
|
+
format = outputFormat.value,
|
|
164
|
+
width = dimensions.width,
|
|
165
|
+
height = dimensions.height,
|
|
166
|
+
byteSize = byteSize,
|
|
167
|
+
originalByteSize = originalByteSize,
|
|
168
|
+
compressionRatio = if (originalByteSize > 0L) {
|
|
169
|
+
byteSize.toDouble() / originalByteSize.toDouble()
|
|
170
|
+
} else {
|
|
171
|
+
1.0
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fun createFormatCapability(format: String): CompressionFormatCapability {
|
|
177
|
+
val outputFormat = OutputFormat.fromValue(format)
|
|
178
|
+
val isSupportedInput = format in SUPPORTED_INPUT_FORMATS
|
|
179
|
+
|
|
180
|
+
return CompressionFormatCapability(
|
|
181
|
+
format = format,
|
|
182
|
+
input = isSupportedInput,
|
|
183
|
+
output = outputFormat != null,
|
|
184
|
+
supportsAlpha = false,
|
|
185
|
+
supportsAnimation = false,
|
|
186
|
+
notes = when (outputFormat) {
|
|
187
|
+
OutputFormat.JPEG -> jpegFormatNotes()
|
|
188
|
+
OutputFormat.PNG -> pngFormatNotes()
|
|
189
|
+
OutputFormat.WEBP -> webpFormatNotes()
|
|
190
|
+
null -> when (format) {
|
|
191
|
+
HEIC_FORMAT -> heicHeifFormatNotes("HEIC")
|
|
192
|
+
HEIF_FORMAT -> heicHeifFormatNotes("HEIF")
|
|
193
|
+
AVIF_FORMAT -> avifFormatNotes()
|
|
194
|
+
GIF_FORMAT -> gifFormatNotes()
|
|
195
|
+
else -> notImplementedNotes()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private fun notImplementedNotes(): List<String> =
|
|
202
|
+
listOf("Native codec support has not been implemented yet.")
|
|
203
|
+
|
|
204
|
+
private fun heicHeifFormatNotes(formatLabel: String): List<String> =
|
|
205
|
+
listOf(
|
|
206
|
+
"$formatLabel input is supported on Android 8.0+ when device HEIF decode codecs are present.",
|
|
207
|
+
"Android API 28+ uses ImageDecoder for $formatLabel input.",
|
|
208
|
+
"Android API 26-27 attempts a guarded BitmapFactory HEIF decode fallback.",
|
|
209
|
+
"$formatLabel inputs are decoded without copying EXIF metadata.",
|
|
210
|
+
"$formatLabel output is not implemented."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
private fun avifFormatNotes(): List<String> =
|
|
214
|
+
listOf(
|
|
215
|
+
"AVIF input is supported on Android 14+ for baseline still images.",
|
|
216
|
+
"Android API 34+ uses ImageDecoder for AVIF input.",
|
|
217
|
+
"AVIF inputs are decoded without copying EXIF metadata.",
|
|
218
|
+
"Animated AVIF preservation is not implemented.",
|
|
219
|
+
"AVIF output is not implemented."
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
private fun encodeBitmapToTargetSize(
|
|
223
|
+
bitmap: Bitmap,
|
|
224
|
+
outputFile: File,
|
|
225
|
+
outputFormat: OutputFormat,
|
|
226
|
+
qualityCap: Int,
|
|
227
|
+
maxBytes: Long,
|
|
228
|
+
copiedExifMetadata: CopiedExifMetadata?
|
|
229
|
+
): Boolean {
|
|
230
|
+
var currentQuality = qualityCap
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
!encodeBitmapAtQuality(
|
|
234
|
+
bitmap,
|
|
235
|
+
outputFile,
|
|
236
|
+
outputFormat,
|
|
237
|
+
currentQuality,
|
|
238
|
+
copiedExifMetadata
|
|
239
|
+
)
|
|
240
|
+
) {
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (outputFile.length() <= maxBytes) {
|
|
245
|
+
return true
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
var lowestAboveTargetQuality = currentQuality
|
|
249
|
+
var lowestAboveTargetSize = outputFile.length()
|
|
250
|
+
var bestWithinTargetQuality: Int? = null
|
|
251
|
+
var low = MIN_QUALITY
|
|
252
|
+
var high = qualityCap - 1
|
|
253
|
+
|
|
254
|
+
while (low <= high) {
|
|
255
|
+
currentQuality = (low + high) / 2
|
|
256
|
+
|
|
257
|
+
if (
|
|
258
|
+
!encodeBitmapAtQuality(
|
|
259
|
+
bitmap,
|
|
260
|
+
outputFile,
|
|
261
|
+
outputFormat,
|
|
262
|
+
currentQuality,
|
|
263
|
+
copiedExifMetadata
|
|
264
|
+
)
|
|
265
|
+
) {
|
|
266
|
+
return false
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
val byteSize = outputFile.length()
|
|
270
|
+
|
|
271
|
+
if (byteSize <= maxBytes) {
|
|
272
|
+
bestWithinTargetQuality = currentQuality
|
|
273
|
+
low = currentQuality + 1
|
|
274
|
+
} else {
|
|
275
|
+
if (byteSize < lowestAboveTargetSize) {
|
|
276
|
+
lowestAboveTargetQuality = currentQuality
|
|
277
|
+
lowestAboveTargetSize = byteSize
|
|
278
|
+
}
|
|
279
|
+
high = currentQuality - 1
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
val finalQuality = bestWithinTargetQuality ?: lowestAboveTargetQuality
|
|
284
|
+
|
|
285
|
+
return if (currentQuality == finalQuality) {
|
|
286
|
+
true
|
|
287
|
+
} else {
|
|
288
|
+
encodeBitmapAtQuality(
|
|
289
|
+
bitmap,
|
|
290
|
+
outputFile,
|
|
291
|
+
outputFormat,
|
|
292
|
+
finalQuality,
|
|
293
|
+
copiedExifMetadata
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private fun encodeBitmapAtQuality(
|
|
299
|
+
bitmap: Bitmap,
|
|
300
|
+
outputFile: File,
|
|
301
|
+
outputFormat: OutputFormat,
|
|
302
|
+
quality: Int,
|
|
303
|
+
copiedExifMetadata: CopiedExifMetadata?
|
|
304
|
+
): Boolean {
|
|
305
|
+
val encoded = FileOutputStream(outputFile).use { outputStream ->
|
|
306
|
+
bitmap.compress(
|
|
307
|
+
outputFormat.compressFormat,
|
|
308
|
+
outputFormat.compressionQuality(quality),
|
|
309
|
+
outputStream
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!encoded) {
|
|
314
|
+
return false
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (outputFormat.supportsJpegExifMetadata) {
|
|
318
|
+
writeCopiedExifMetadata(copiedExifMetadata, outputFile)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return true
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private fun writeCopiedExifMetadata(
|
|
325
|
+
copiedExifMetadata: CopiedExifMetadata?,
|
|
326
|
+
outputFile: File
|
|
327
|
+
) {
|
|
328
|
+
try {
|
|
329
|
+
JpegExifMetadata.write(copiedExifMetadata, outputFile)
|
|
330
|
+
} catch (error: Exception) {
|
|
331
|
+
throw IllegalStateException(
|
|
332
|
+
"Android MVP could not write preserved EXIF metadata.",
|
|
333
|
+
error
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private fun jpegFormatNotes(): List<String> =
|
|
339
|
+
listOf(
|
|
340
|
+
"Android MVP supports JPEG file:// and content:// sources.",
|
|
341
|
+
"EXIF orientation is applied before resize and selected output encoding.",
|
|
342
|
+
"Resize supports contain, cover, and stretch modes with maxWidth and maxHeight.",
|
|
343
|
+
"Target-size compression supports maxBytes by adjusting JPEG quality.",
|
|
344
|
+
"Metadata preserve copies supported JPEG source EXIF attributes into JPEG output.",
|
|
345
|
+
"Metadata safe copies privacy-filtered JPEG source EXIF attributes.",
|
|
346
|
+
"Metadata safe excludes GPS/location, owner/serial, maker note, user comment, and XMP.",
|
|
347
|
+
"Metadata preserve normalizes output EXIF orientation after pixels are transformed.",
|
|
348
|
+
"Metadata strip re-encodes JPEG output without preserving source metadata.",
|
|
349
|
+
"PNG, WebP, GIF, HEIC, HEIF, and AVIF sources are decoded without copying EXIF metadata."
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
private fun pngFormatNotes(): List<String> =
|
|
353
|
+
listOf(
|
|
354
|
+
"Android MVP supports PNG file:// and content:// sources.",
|
|
355
|
+
"Android can encode decoded JPEG, PNG, WebP, or GIF input to PNG output.",
|
|
356
|
+
"PNG output ignores quality and does not support target-size maxBytes.",
|
|
357
|
+
"Non-JPEG output does not preserve source EXIF metadata."
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
private fun webpFormatNotes(): List<String> =
|
|
361
|
+
listOf(
|
|
362
|
+
"Android MVP supports WebP file:// and content:// sources.",
|
|
363
|
+
"Android can encode decoded JPEG, PNG, WebP, or GIF input to WebP output.",
|
|
364
|
+
"WebP target-size compression supports maxBytes by adjusting WebP quality.",
|
|
365
|
+
"Non-JPEG output does not preserve source EXIF metadata.",
|
|
366
|
+
"Animated WebP input or output is not implemented."
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
private fun gifFormatNotes(): List<String> =
|
|
370
|
+
listOf(
|
|
371
|
+
"Android MVP decodes GIF file:// and content:// sources as a static first frame.",
|
|
372
|
+
"Animated GIF preservation is not implemented.",
|
|
373
|
+
"GIF output is not implemented.",
|
|
374
|
+
"GIF sources are decoded without copying EXIF metadata."
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
private val SUPPORTED_INPUT_FORMATS = setOf(
|
|
378
|
+
JPEG_FORMAT,
|
|
379
|
+
PNG_FORMAT,
|
|
380
|
+
WEBP_FORMAT,
|
|
381
|
+
HEIC_FORMAT,
|
|
382
|
+
HEIF_FORMAT,
|
|
383
|
+
AVIF_FORMAT,
|
|
384
|
+
GIF_FORMAT
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
private const val JPEG_FORMAT = "jpeg"
|
|
388
|
+
private const val PNG_FORMAT = "png"
|
|
389
|
+
private const val WEBP_FORMAT = "webp"
|
|
390
|
+
private const val HEIC_FORMAT = "heic"
|
|
391
|
+
private const val HEIF_FORMAT = "heif"
|
|
392
|
+
private const val AVIF_FORMAT = "avif"
|
|
393
|
+
private const val GIF_FORMAT = "gif"
|
|
394
|
+
private const val OUTPUT_DIRECTORY_NAME = "image-compression-kit"
|
|
395
|
+
private const val MIN_QUALITY = 0
|
|
396
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
package com.imagecompressionkit
|
|
2
|
+
|
|
3
|
+
import androidx.exifinterface.media.ExifInterface
|
|
4
|
+
import java.io.File
|
|
5
|
+
import java.io.InputStream
|
|
6
|
+
|
|
7
|
+
internal data class CopiedExifMetadata(
|
|
8
|
+
val attributes: Map<String, String>,
|
|
9
|
+
val width: Int,
|
|
10
|
+
val height: Int
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
internal object JpegExifMetadata {
|
|
14
|
+
val SAFE_EXIF_TAGS = arrayOf(
|
|
15
|
+
ExifInterface.TAG_APERTURE_VALUE,
|
|
16
|
+
ExifInterface.TAG_BRIGHTNESS_VALUE,
|
|
17
|
+
ExifInterface.TAG_CFA_PATTERN,
|
|
18
|
+
ExifInterface.TAG_COLOR_SPACE,
|
|
19
|
+
ExifInterface.TAG_COMPONENTS_CONFIGURATION,
|
|
20
|
+
ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL,
|
|
21
|
+
ExifInterface.TAG_CONTRAST,
|
|
22
|
+
ExifInterface.TAG_CUSTOM_RENDERED,
|
|
23
|
+
ExifInterface.TAG_DATETIME,
|
|
24
|
+
ExifInterface.TAG_DATETIME_DIGITIZED,
|
|
25
|
+
ExifInterface.TAG_DATETIME_ORIGINAL,
|
|
26
|
+
ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
|
|
27
|
+
ExifInterface.TAG_EXIF_VERSION,
|
|
28
|
+
ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
|
|
29
|
+
ExifInterface.TAG_EXPOSURE_INDEX,
|
|
30
|
+
ExifInterface.TAG_EXPOSURE_MODE,
|
|
31
|
+
ExifInterface.TAG_EXPOSURE_PROGRAM,
|
|
32
|
+
ExifInterface.TAG_EXPOSURE_TIME,
|
|
33
|
+
ExifInterface.TAG_F_NUMBER,
|
|
34
|
+
ExifInterface.TAG_FILE_SOURCE,
|
|
35
|
+
ExifInterface.TAG_FLASH,
|
|
36
|
+
ExifInterface.TAG_FLASHPIX_VERSION,
|
|
37
|
+
ExifInterface.TAG_FOCAL_LENGTH,
|
|
38
|
+
ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
|
|
39
|
+
ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
|
|
40
|
+
ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
|
|
41
|
+
ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
|
|
42
|
+
ExifInterface.TAG_GAIN_CONTROL,
|
|
43
|
+
ExifInterface.TAG_GAMMA,
|
|
44
|
+
ExifInterface.TAG_ISO_SPEED,
|
|
45
|
+
ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY,
|
|
46
|
+
ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ,
|
|
47
|
+
ExifInterface.TAG_LENS_MAKE,
|
|
48
|
+
ExifInterface.TAG_LENS_MODEL,
|
|
49
|
+
ExifInterface.TAG_LENS_SPECIFICATION,
|
|
50
|
+
ExifInterface.TAG_LIGHT_SOURCE,
|
|
51
|
+
ExifInterface.TAG_MAKE,
|
|
52
|
+
ExifInterface.TAG_MAX_APERTURE_VALUE,
|
|
53
|
+
ExifInterface.TAG_METERING_MODE,
|
|
54
|
+
ExifInterface.TAG_MODEL,
|
|
55
|
+
ExifInterface.TAG_OFFSET_TIME,
|
|
56
|
+
ExifInterface.TAG_OFFSET_TIME_DIGITIZED,
|
|
57
|
+
ExifInterface.TAG_OFFSET_TIME_ORIGINAL,
|
|
58
|
+
ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
|
|
59
|
+
ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX,
|
|
60
|
+
ExifInterface.TAG_SATURATION,
|
|
61
|
+
ExifInterface.TAG_SCENE_CAPTURE_TYPE,
|
|
62
|
+
ExifInterface.TAG_SCENE_TYPE,
|
|
63
|
+
ExifInterface.TAG_SENSING_METHOD,
|
|
64
|
+
ExifInterface.TAG_SENSITIVITY_TYPE,
|
|
65
|
+
ExifInterface.TAG_SHARPNESS,
|
|
66
|
+
ExifInterface.TAG_SHUTTER_SPEED_VALUE,
|
|
67
|
+
ExifInterface.TAG_SOFTWARE,
|
|
68
|
+
ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY,
|
|
69
|
+
ExifInterface.TAG_SUBSEC_TIME,
|
|
70
|
+
ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
|
|
71
|
+
ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
|
|
72
|
+
ExifInterface.TAG_WHITE_BALANCE
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
val PRESERVED_EXIF_TAGS = arrayOf(
|
|
76
|
+
ExifInterface.TAG_ARTIST,
|
|
77
|
+
ExifInterface.TAG_BODY_SERIAL_NUMBER,
|
|
78
|
+
ExifInterface.TAG_BRIGHTNESS_VALUE,
|
|
79
|
+
ExifInterface.TAG_CAMERA_OWNER_NAME,
|
|
80
|
+
ExifInterface.TAG_CFA_PATTERN,
|
|
81
|
+
ExifInterface.TAG_COLOR_SPACE,
|
|
82
|
+
ExifInterface.TAG_COMPONENTS_CONFIGURATION,
|
|
83
|
+
ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL,
|
|
84
|
+
ExifInterface.TAG_CONTRAST,
|
|
85
|
+
ExifInterface.TAG_COPYRIGHT,
|
|
86
|
+
ExifInterface.TAG_CUSTOM_RENDERED,
|
|
87
|
+
ExifInterface.TAG_DATETIME,
|
|
88
|
+
ExifInterface.TAG_DATETIME_DIGITIZED,
|
|
89
|
+
ExifInterface.TAG_DATETIME_ORIGINAL,
|
|
90
|
+
ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
|
|
91
|
+
ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
|
|
92
|
+
ExifInterface.TAG_EXIF_VERSION,
|
|
93
|
+
ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
|
|
94
|
+
ExifInterface.TAG_EXPOSURE_INDEX,
|
|
95
|
+
ExifInterface.TAG_EXPOSURE_MODE,
|
|
96
|
+
ExifInterface.TAG_EXPOSURE_PROGRAM,
|
|
97
|
+
ExifInterface.TAG_EXPOSURE_TIME,
|
|
98
|
+
ExifInterface.TAG_F_NUMBER,
|
|
99
|
+
ExifInterface.TAG_FILE_SOURCE,
|
|
100
|
+
ExifInterface.TAG_FLASH,
|
|
101
|
+
ExifInterface.TAG_FLASH_ENERGY,
|
|
102
|
+
ExifInterface.TAG_FLASHPIX_VERSION,
|
|
103
|
+
ExifInterface.TAG_FOCAL_LENGTH,
|
|
104
|
+
ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
|
|
105
|
+
ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
|
|
106
|
+
ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
|
|
107
|
+
ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
|
|
108
|
+
ExifInterface.TAG_GAIN_CONTROL,
|
|
109
|
+
ExifInterface.TAG_GAMMA,
|
|
110
|
+
ExifInterface.TAG_GPS_ALTITUDE,
|
|
111
|
+
ExifInterface.TAG_GPS_ALTITUDE_REF,
|
|
112
|
+
ExifInterface.TAG_GPS_AREA_INFORMATION,
|
|
113
|
+
ExifInterface.TAG_GPS_DATESTAMP,
|
|
114
|
+
ExifInterface.TAG_GPS_DEST_BEARING,
|
|
115
|
+
ExifInterface.TAG_GPS_DEST_BEARING_REF,
|
|
116
|
+
ExifInterface.TAG_GPS_DEST_DISTANCE,
|
|
117
|
+
ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
|
|
118
|
+
ExifInterface.TAG_GPS_DEST_LATITUDE,
|
|
119
|
+
ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
|
|
120
|
+
ExifInterface.TAG_GPS_DEST_LONGITUDE,
|
|
121
|
+
ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
|
|
122
|
+
ExifInterface.TAG_GPS_DIFFERENTIAL,
|
|
123
|
+
ExifInterface.TAG_GPS_DOP,
|
|
124
|
+
ExifInterface.TAG_GPS_H_POSITIONING_ERROR,
|
|
125
|
+
ExifInterface.TAG_GPS_IMG_DIRECTION,
|
|
126
|
+
ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
|
|
127
|
+
ExifInterface.TAG_GPS_LATITUDE,
|
|
128
|
+
ExifInterface.TAG_GPS_LATITUDE_REF,
|
|
129
|
+
ExifInterface.TAG_GPS_LONGITUDE,
|
|
130
|
+
ExifInterface.TAG_GPS_LONGITUDE_REF,
|
|
131
|
+
ExifInterface.TAG_GPS_MAP_DATUM,
|
|
132
|
+
ExifInterface.TAG_GPS_MEASURE_MODE,
|
|
133
|
+
ExifInterface.TAG_GPS_PROCESSING_METHOD,
|
|
134
|
+
ExifInterface.TAG_GPS_SATELLITES,
|
|
135
|
+
ExifInterface.TAG_GPS_SPEED,
|
|
136
|
+
ExifInterface.TAG_GPS_SPEED_REF,
|
|
137
|
+
ExifInterface.TAG_GPS_STATUS,
|
|
138
|
+
ExifInterface.TAG_GPS_TIMESTAMP,
|
|
139
|
+
ExifInterface.TAG_GPS_TRACK,
|
|
140
|
+
ExifInterface.TAG_GPS_TRACK_REF,
|
|
141
|
+
ExifInterface.TAG_GPS_VERSION_ID,
|
|
142
|
+
ExifInterface.TAG_IMAGE_DESCRIPTION,
|
|
143
|
+
ExifInterface.TAG_IMAGE_UNIQUE_ID,
|
|
144
|
+
ExifInterface.TAG_INTEROPERABILITY_INDEX,
|
|
145
|
+
ExifInterface.TAG_ISO_SPEED,
|
|
146
|
+
ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY,
|
|
147
|
+
ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ,
|
|
148
|
+
ExifInterface.TAG_LENS_MAKE,
|
|
149
|
+
ExifInterface.TAG_LENS_MODEL,
|
|
150
|
+
ExifInterface.TAG_LENS_SERIAL_NUMBER,
|
|
151
|
+
ExifInterface.TAG_LENS_SPECIFICATION,
|
|
152
|
+
ExifInterface.TAG_LIGHT_SOURCE,
|
|
153
|
+
ExifInterface.TAG_MAKE,
|
|
154
|
+
ExifInterface.TAG_MAKER_NOTE,
|
|
155
|
+
ExifInterface.TAG_MAX_APERTURE_VALUE,
|
|
156
|
+
ExifInterface.TAG_METERING_MODE,
|
|
157
|
+
ExifInterface.TAG_MODEL,
|
|
158
|
+
ExifInterface.TAG_OECF,
|
|
159
|
+
ExifInterface.TAG_OFFSET_TIME,
|
|
160
|
+
ExifInterface.TAG_OFFSET_TIME_DIGITIZED,
|
|
161
|
+
ExifInterface.TAG_OFFSET_TIME_ORIGINAL,
|
|
162
|
+
ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
|
|
163
|
+
ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX,
|
|
164
|
+
ExifInterface.TAG_RELATED_SOUND_FILE,
|
|
165
|
+
ExifInterface.TAG_SATURATION,
|
|
166
|
+
ExifInterface.TAG_SCENE_CAPTURE_TYPE,
|
|
167
|
+
ExifInterface.TAG_SCENE_TYPE,
|
|
168
|
+
ExifInterface.TAG_SENSING_METHOD,
|
|
169
|
+
ExifInterface.TAG_SENSITIVITY_TYPE,
|
|
170
|
+
ExifInterface.TAG_SHARPNESS,
|
|
171
|
+
ExifInterface.TAG_SHUTTER_SPEED_VALUE,
|
|
172
|
+
ExifInterface.TAG_SOFTWARE,
|
|
173
|
+
ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE,
|
|
174
|
+
ExifInterface.TAG_SPECTRAL_SENSITIVITY,
|
|
175
|
+
ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY,
|
|
176
|
+
ExifInterface.TAG_SUBJECT_AREA,
|
|
177
|
+
ExifInterface.TAG_SUBJECT_DISTANCE,
|
|
178
|
+
ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
|
|
179
|
+
ExifInterface.TAG_SUBJECT_LOCATION,
|
|
180
|
+
ExifInterface.TAG_SUBSEC_TIME,
|
|
181
|
+
ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
|
|
182
|
+
ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
|
|
183
|
+
ExifInterface.TAG_USER_COMMENT,
|
|
184
|
+
ExifInterface.TAG_WHITE_BALANCE,
|
|
185
|
+
ExifInterface.TAG_XMP
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
fun read(
|
|
189
|
+
inputStream: InputStream,
|
|
190
|
+
exifTags: Array<String>,
|
|
191
|
+
width: Int,
|
|
192
|
+
height: Int
|
|
193
|
+
): CopiedExifMetadata {
|
|
194
|
+
val sourceExif = ExifInterface(inputStream)
|
|
195
|
+
val attributes = linkedMapOf<String, String>()
|
|
196
|
+
|
|
197
|
+
exifTags.forEach { tag ->
|
|
198
|
+
val value = sourceExif.getAttribute(tag)
|
|
199
|
+
|
|
200
|
+
if (value != null) {
|
|
201
|
+
attributes[tag] = value
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return CopiedExifMetadata(
|
|
206
|
+
attributes = attributes,
|
|
207
|
+
width = width,
|
|
208
|
+
height = height
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fun write(metadata: CopiedExifMetadata?, outputFile: File) {
|
|
213
|
+
if (metadata == null) {
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
val outputExif = ExifInterface(outputFile.absolutePath)
|
|
218
|
+
|
|
219
|
+
metadata.attributes.forEach { (tag, value) ->
|
|
220
|
+
outputExif.setAttribute(tag, value)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
outputExif.setAttribute(
|
|
224
|
+
ExifInterface.TAG_ORIENTATION,
|
|
225
|
+
ExifInterface.ORIENTATION_NORMAL.toString()
|
|
226
|
+
)
|
|
227
|
+
outputExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, metadata.width.toString())
|
|
228
|
+
outputExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, metadata.height.toString())
|
|
229
|
+
outputExif.setAttribute(ExifInterface.TAG_PIXEL_X_DIMENSION, metadata.width.toString())
|
|
230
|
+
outputExif.setAttribute(ExifInterface.TAG_PIXEL_Y_DIMENSION, metadata.height.toString())
|
|
231
|
+
outputExif.saveAttributes()
|
|
232
|
+
}
|
|
233
|
+
}
|