react-native-libyuv-resizer 0.2.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.
Files changed (50) hide show
  1. package/LICENSE +20 -0
  2. package/LibyuvResizer.podspec +20 -0
  3. package/README.md +188 -0
  4. package/android/CMakeLists.txt +30 -0
  5. package/android/build.gradle +100 -0
  6. package/android/src/androidTest/java/com/libyuvresizer/ExifCopierTest.kt +131 -0
  7. package/android/src/androidTest/java/com/libyuvresizer/FakePromise.kt +71 -0
  8. package/android/src/androidTest/java/com/libyuvresizer/FakeReactContext.kt +55 -0
  9. package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleErrorTest.kt +135 -0
  10. package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleExifTest.kt +140 -0
  11. package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleFilterModeTest.kt +85 -0
  12. package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleFormatTest.kt +146 -0
  13. package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleIntegrationTest.kt +157 -0
  14. package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleOutputPathTest.kt +96 -0
  15. package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleRotationTest.kt +120 -0
  16. package/android/src/androidTest/java/com/libyuvresizer/TestFixtures.kt +48 -0
  17. package/android/src/main/AndroidManifest.xml +2 -0
  18. package/android/src/main/cpp/LibyuvResizerModule.cpp +137 -0
  19. package/android/src/main/java/com/libyuvresizer/DimensionCalculator.kt +52 -0
  20. package/android/src/main/java/com/libyuvresizer/ExifCopier.kt +133 -0
  21. package/android/src/main/java/com/libyuvresizer/LibyuvResizerModule.kt +179 -0
  22. package/android/src/main/java/com/libyuvresizer/LibyuvResizerPackage.kt +30 -0
  23. package/android/src/main/java/com/libyuvresizer/ResizeValidator.kt +71 -0
  24. package/android/src/test/java/com/libyuvresizer/DimensionCalculatorTest.kt +181 -0
  25. package/android/src/test/java/com/libyuvresizer/ResizeValidatorTest.kt +203 -0
  26. package/ios/LibyuvResizer.h +6 -0
  27. package/ios/LibyuvResizer.mm +31 -0
  28. package/lib/module/NativeLibyuvResizer.js +28 -0
  29. package/lib/module/NativeLibyuvResizer.js.map +1 -0
  30. package/lib/module/index.js +20 -0
  31. package/lib/module/index.js.map +1 -0
  32. package/lib/module/package.json +1 -0
  33. package/lib/module/resizer.js +15 -0
  34. package/lib/module/resizer.js.map +1 -0
  35. package/lib/module/resizer.native.js +110 -0
  36. package/lib/module/resizer.native.js.map +1 -0
  37. package/lib/typescript/package.json +1 -0
  38. package/lib/typescript/src/NativeLibyuvResizer.d.ts +52 -0
  39. package/lib/typescript/src/NativeLibyuvResizer.d.ts.map +1 -0
  40. package/lib/typescript/src/index.d.ts +19 -0
  41. package/lib/typescript/src/index.d.ts.map +1 -0
  42. package/lib/typescript/src/resizer.d.ts +13 -0
  43. package/lib/typescript/src/resizer.d.ts.map +1 -0
  44. package/lib/typescript/src/resizer.native.d.ts +119 -0
  45. package/lib/typescript/src/resizer.native.d.ts.map +1 -0
  46. package/package.json +184 -0
  47. package/src/NativeLibyuvResizer.ts +81 -0
  48. package/src/index.tsx +23 -0
  49. package/src/resizer.native.tsx +175 -0
  50. package/src/resizer.tsx +31 -0
@@ -0,0 +1,96 @@
1
+ package com.libyuvresizer
2
+
3
+ import androidx.test.ext.junit.runners.AndroidJUnit4
4
+ import androidx.test.platform.app.InstrumentationRegistry
5
+ import org.junit.After
6
+ import org.junit.Assert.assertEquals
7
+ import org.junit.Assert.assertTrue
8
+ import org.junit.Before
9
+ import org.junit.Test
10
+ import org.junit.runner.RunWith
11
+ import com.facebook.react.bridge.ReadableMap
12
+ import java.io.File
13
+
14
+ @RunWith(AndroidJUnit4::class)
15
+ class LibyuvResizerModuleOutputPathTest {
16
+
17
+ private lateinit var module: LibyuvResizerModule
18
+ private lateinit var reactContext: FakeReactContext
19
+ private val createdFiles = mutableListOf<String>()
20
+ private lateinit var srcPath: String
21
+
22
+ @Before
23
+ fun setUp() {
24
+ val ctx = InstrumentationRegistry.getInstrumentation().targetContext
25
+ reactContext = FakeReactContext(ctx)
26
+ module = LibyuvResizerModule(reactContext)
27
+ srcPath = TestFixtures.createJpeg(reactContext, 120, 80, "output_path_src.jpg")
28
+ createdFiles += srcPath
29
+ }
30
+
31
+ @After
32
+ fun tearDown() {
33
+ createdFiles.forEach { TestFixtures.deleteIfExists(it) }
34
+ createdFiles.clear()
35
+ }
36
+
37
+ private fun resize(outputPath: String = "", quality: Double = 80.0, format: String = "jpeg"): FakePromise {
38
+ val promise = FakePromise()
39
+ module.resize(srcPath, 60.0, 60.0, quality, 0.0, "contain", outputPath, "box", false, format, promise)
40
+ return promise
41
+ }
42
+
43
+ @Test
44
+ fun resize_emptyOutputPath_writesToCacheDir() {
45
+ val promise = resize(outputPath = "")
46
+
47
+ assertTrue(promise.resolved)
48
+ val outPath = (promise.result as ReadableMap).getString("path")!!
49
+ createdFiles += outPath
50
+ assertTrue(
51
+ "output must be inside cacheDir",
52
+ outPath.startsWith(reactContext.cacheDir.absolutePath)
53
+ )
54
+ assertTrue(File(outPath).exists())
55
+ }
56
+
57
+ @Test
58
+ fun resize_customOutputPath_writesToProvidedDir() {
59
+ val destDir = reactContext.filesDir
60
+ val promise = resize(outputPath = destDir.absolutePath)
61
+
62
+ assertTrue(promise.resolved)
63
+ val outPath = (promise.result as ReadableMap).getString("path")!!
64
+ createdFiles += outPath
65
+ assertTrue(
66
+ "output must be inside filesDir",
67
+ outPath.startsWith(destDir.absolutePath)
68
+ )
69
+ assertTrue(File(outPath).exists())
70
+ }
71
+
72
+ @Test
73
+ fun resize_customOutputPath_keepsInputFilename() {
74
+ // resolveOutputFile uses File(inputFilePath).name → output filename = input filename.
75
+ // "output_path_src.jpg" is kept even when quality=100 (ext would be "png").
76
+ val destDir = reactContext.filesDir
77
+ val promise = resize(outputPath = destDir.absolutePath)
78
+
79
+ assertTrue(promise.resolved)
80
+ val outPath = (promise.result as ReadableMap).getString("path")!!
81
+ createdFiles += outPath
82
+ val outputFilename = File(outPath).name
83
+ assertEquals("output_path_src.jpg", outputFilename)
84
+ }
85
+
86
+ @Test
87
+ fun resize_formatPng_emptyOutputPath_usesPngExtension() {
88
+ val promise = resize(outputPath = "", format = "png")
89
+
90
+ assertTrue(promise.resolved)
91
+ val outPath = (promise.result as ReadableMap).getString("path")!!
92
+ createdFiles += outPath
93
+ assertTrue("output must end with .png", outPath.endsWith(".png"))
94
+ assertTrue(File(outPath).exists())
95
+ }
96
+ }
@@ -0,0 +1,120 @@
1
+ package com.libyuvresizer
2
+
3
+ import androidx.test.ext.junit.runners.AndroidJUnit4
4
+ import androidx.test.platform.app.InstrumentationRegistry
5
+ import org.junit.After
6
+ import org.junit.Assert.assertEquals
7
+ import org.junit.Assert.assertTrue
8
+ import org.junit.Before
9
+ import org.junit.Test
10
+ import org.junit.runner.RunWith
11
+ import com.facebook.react.bridge.ReadableMap
12
+ import java.io.File
13
+
14
+ @RunWith(AndroidJUnit4::class)
15
+ class LibyuvResizerModuleRotationTest {
16
+
17
+ private lateinit var module: LibyuvResizerModule
18
+ private lateinit var reactContext: FakeReactContext
19
+ private val createdFiles = mutableListOf<String>()
20
+
21
+ // 200x100 landscape source used in all rotation tests (no inSampleSize reduction at small targets)
22
+ private lateinit var landscapeSrc: String
23
+
24
+ @Before
25
+ fun setUp() {
26
+ val ctx = InstrumentationRegistry.getInstrumentation().targetContext
27
+ reactContext = FakeReactContext(ctx)
28
+ module = LibyuvResizerModule(reactContext)
29
+ landscapeSrc = TestFixtures.createJpeg(reactContext, 200, 100, "rotation_src.jpg")
30
+ createdFiles += landscapeSrc
31
+ }
32
+
33
+ @After
34
+ fun tearDown() {
35
+ createdFiles.forEach { TestFixtures.deleteIfExists(it) }
36
+ createdFiles.clear()
37
+ }
38
+
39
+ private fun resize(
40
+ filePath: String,
41
+ targetW: Double,
42
+ targetH: Double,
43
+ rotation: Double,
44
+ mode: String = "contain"
45
+ ): FakePromise {
46
+ val promise = FakePromise()
47
+ module.resize(filePath, targetW, targetH, 80.0, rotation, mode, "", "box", false, "jpeg", promise)
48
+ return promise
49
+ }
50
+
51
+ @Test
52
+ fun resize_rotation0_usesNativeResizeBranch() {
53
+ // rot == 0 → nativeResize path. 200x100 → 300x300 contain → 300x150
54
+ val promise = resize(landscapeSrc, 300.0, 300.0, 0.0)
55
+
56
+ assertTrue(promise.resolved)
57
+ val outPath = (promise.result as ReadableMap).getString("path")!!
58
+ createdFiles += outPath
59
+ assertTrue("output file must exist", File(outPath).exists())
60
+ val (w, h) = TestFixtures.decodeDimensions(outPath)
61
+ assertEquals(300, w)
62
+ assertEquals(150, h)
63
+ }
64
+
65
+ @Test
66
+ fun resize_rotation90_swapsDimensions() {
67
+ // rot=90 → nativeResizeAndRotate path.
68
+ // srcBitmap 200x100, effectiveW=srcH=100, effectiveH=srcW=200.
69
+ // contain: scale=min(300/100, 300/200)=min(3.0,1.5)=1.5 → (floor(150), floor(300)) = 150x300
70
+ val promise = resize(landscapeSrc, 300.0, 300.0, 90.0)
71
+
72
+ assertTrue(promise.resolved)
73
+ val outPath = (promise.result as ReadableMap).getString("path")!!
74
+ createdFiles += outPath
75
+ val (w, h) = TestFixtures.decodeDimensions(outPath)
76
+ assertEquals(150, w)
77
+ assertEquals(300, h)
78
+ }
79
+
80
+ @Test
81
+ fun resize_rotation180_doesNotSwapDimensions() {
82
+ // rot=180 → effectiveW=srcW=200, effectiveH=srcH=100 (no swap).
83
+ // contain: scale=min(300/200, 300/100)=1.5 → (floor(300), floor(150)) = 300x150 (same as rot=0)
84
+ val promise = resize(landscapeSrc, 300.0, 300.0, 180.0)
85
+
86
+ assertTrue(promise.resolved)
87
+ val outPath = (promise.result as ReadableMap).getString("path")!!
88
+ createdFiles += outPath
89
+ val (w, h) = TestFixtures.decodeDimensions(outPath)
90
+ assertEquals(300, w)
91
+ assertEquals(150, h)
92
+ }
93
+
94
+ @Test
95
+ fun resize_rotation270_swapsDimensions() {
96
+ // rot=270 → same swap logic as 90 → 150x300
97
+ val promise = resize(landscapeSrc, 300.0, 300.0, 270.0)
98
+
99
+ assertTrue(promise.resolved)
100
+ val outPath = (promise.result as ReadableMap).getString("path")!!
101
+ createdFiles += outPath
102
+ val (w, h) = TestFixtures.decodeDimensions(outPath)
103
+ assertEquals(150, w)
104
+ assertEquals(300, h)
105
+ }
106
+
107
+ @Test
108
+ fun resize_rotation90_stretch_targetDimsIgnoreRotation() {
109
+ // stretch with rotation: target dims are returned as-is (ignores rotation).
110
+ // 200x100 → target 120x80 stretch → output 120x80 regardless of rotation.
111
+ val promise = resize(landscapeSrc, 120.0, 80.0, 90.0, mode = "stretch")
112
+
113
+ assertTrue(promise.resolved)
114
+ val outPath = (promise.result as ReadableMap).getString("path")!!
115
+ createdFiles += outPath
116
+ val (w, h) = TestFixtures.decodeDimensions(outPath)
117
+ assertEquals(120, w)
118
+ assertEquals(80, h)
119
+ }
120
+ }
@@ -0,0 +1,48 @@
1
+ package com.libyuvresizer
2
+
3
+ import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import android.graphics.BitmapFactory
6
+ import android.graphics.Color
7
+ import androidx.exifinterface.media.ExifInterface
8
+ import java.io.File
9
+ import java.io.FileOutputStream
10
+
11
+ object TestFixtures {
12
+
13
+ fun createJpeg(context: Context, width: Int, height: Int, name: String): String {
14
+ val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
15
+ eraseColor(Color.BLUE)
16
+ }
17
+ val file = File(context.cacheDir, name)
18
+ FileOutputStream(file).use { bmp.compress(Bitmap.CompressFormat.JPEG, 80, it) }
19
+ bmp.recycle()
20
+ return file.absolutePath
21
+ }
22
+
23
+ fun createPng(context: Context, width: Int, height: Int, name: String): String {
24
+ val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
25
+ eraseColor(Color.GREEN)
26
+ }
27
+ val file = File(context.cacheDir, name)
28
+ FileOutputStream(file).use { bmp.compress(Bitmap.CompressFormat.PNG, 0, it) }
29
+ bmp.recycle()
30
+ return file.absolutePath
31
+ }
32
+
33
+ fun createCorruptFile(context: Context, name: String): String {
34
+ val file = File(context.cacheDir, name)
35
+ file.writeBytes(byteArrayOf(0xFF.toByte(), 0xD8.toByte(), 0x00, 0x01, 0x02))
36
+ return file.absolutePath
37
+ }
38
+
39
+ fun decodeDimensions(path: String): Pair<Int, Int> {
40
+ val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
41
+ BitmapFactory.decodeFile(path, opts)
42
+ return Pair(opts.outWidth, opts.outHeight)
43
+ }
44
+
45
+ fun deleteIfExists(path: String?) {
46
+ path?.let { File(it).delete() }
47
+ }
48
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,137 @@
1
+ #include <jni.h>
2
+ #include <android/bitmap.h>
3
+ #include <cstdlib>
4
+ #include "libyuv/scale_argb.h"
5
+ #include "libyuv/rotate_argb.h"
6
+
7
+ namespace {
8
+
9
+ struct MallocGuard {
10
+ uint8_t* ptr;
11
+ explicit MallocGuard(size_t n) : ptr(static_cast<uint8_t*>(std::malloc(n))) {}
12
+ ~MallocGuard() { std::free(ptr); }
13
+ MallocGuard(const MallocGuard&) = delete;
14
+ MallocGuard& operator=(const MallocGuard&) = delete;
15
+ };
16
+
17
+ } // namespace
18
+
19
+ extern "C" JNIEXPORT void JNICALL
20
+ Java_com_libyuvresizer_LibyuvResizerModule_nativeResize(
21
+ JNIEnv* env,
22
+ jobject /* thiz */,
23
+ jobject srcBitmap,
24
+ jobject dstBitmap,
25
+ jint filterMode
26
+ ) {
27
+ AndroidBitmapInfo srcInfo{}, dstInfo{};
28
+ AndroidBitmap_getInfo(env, srcBitmap, &srcInfo);
29
+ AndroidBitmap_getInfo(env, dstBitmap, &dstInfo);
30
+
31
+ void* srcPixels = nullptr;
32
+ void* dstPixels = nullptr;
33
+
34
+ if (AndroidBitmap_lockPixels(env, srcBitmap, &srcPixels) < 0) {
35
+ jclass ex = env->FindClass("java/lang/RuntimeException");
36
+ env->ThrowNew(ex, "AndroidBitmap_lockPixels failed (src)");
37
+ return;
38
+ }
39
+ if (AndroidBitmap_lockPixels(env, dstBitmap, &dstPixels) < 0) {
40
+ AndroidBitmap_unlockPixels(env, srcBitmap);
41
+ jclass ex = env->FindClass("java/lang/RuntimeException");
42
+ env->ThrowNew(ex, "AndroidBitmap_lockPixels failed (dst)");
43
+ return;
44
+ }
45
+
46
+ libyuv::ARGBScale(
47
+ static_cast<const uint8_t*>(srcPixels), static_cast<int>(srcInfo.stride),
48
+ static_cast<int>(srcInfo.width), static_cast<int>(srcInfo.height),
49
+ static_cast<uint8_t*>(dstPixels), static_cast<int>(dstInfo.stride),
50
+ static_cast<int>(dstInfo.width), static_cast<int>(dstInfo.height),
51
+ static_cast<libyuv::FilterModeEnum>(filterMode)
52
+ );
53
+
54
+ AndroidBitmap_unlockPixels(env, srcBitmap);
55
+ AndroidBitmap_unlockPixels(env, dstBitmap);
56
+ }
57
+
58
+ extern "C" JNIEXPORT void JNICALL
59
+ Java_com_libyuvresizer_LibyuvResizerModule_nativeResizeAndRotate(
60
+ JNIEnv* env,
61
+ jobject /* thiz */,
62
+ jobject srcBitmap,
63
+ jobject dstBitmap,
64
+ jint rotation,
65
+ jint filterMode
66
+ ) {
67
+ AndroidBitmapInfo srcInfo{}, dstInfo{};
68
+ AndroidBitmap_getInfo(env, srcBitmap, &srcInfo);
69
+ AndroidBitmap_getInfo(env, dstBitmap, &dstInfo);
70
+
71
+ // 90/270 rotations swap output dims — pre-rotation buffer has transposed size
72
+ const bool swap = (rotation == 90 || rotation == 270);
73
+ const uint32_t preW = swap ? dstInfo.height : dstInfo.width;
74
+ const uint32_t preH = swap ? dstInfo.width : dstInfo.height;
75
+ // use size_t to avoid uint32_t overflow on large images
76
+ const size_t preStride = static_cast<size_t>(preW) * 4;
77
+ const size_t allocSize = preStride * static_cast<size_t>(preH);
78
+
79
+ MallocGuard preBuf(allocSize);
80
+ if (!preBuf.ptr) {
81
+ jclass ex = env->FindClass("java/lang/OutOfMemoryError");
82
+ env->ThrowNew(ex, "nativeResizeAndRotate: malloc failed");
83
+ return;
84
+ }
85
+
86
+ void* srcPixels = nullptr;
87
+ void* dstPixels = nullptr;
88
+
89
+ if (AndroidBitmap_lockPixels(env, srcBitmap, &srcPixels) < 0) {
90
+ jclass ex = env->FindClass("java/lang/RuntimeException");
91
+ env->ThrowNew(ex, "AndroidBitmap_lockPixels failed (src)");
92
+ return; // preBuf freed by MallocGuard dtor
93
+ }
94
+ if (AndroidBitmap_lockPixels(env, dstBitmap, &dstPixels) < 0) {
95
+ AndroidBitmap_unlockPixels(env, srcBitmap);
96
+ jclass ex = env->FindClass("java/lang/RuntimeException");
97
+ env->ThrowNew(ex, "AndroidBitmap_lockPixels failed (dst)");
98
+ return; // preBuf freed by MallocGuard dtor
99
+ }
100
+
101
+ const int scaleResult = libyuv::ARGBScale(
102
+ static_cast<const uint8_t*>(srcPixels), static_cast<int>(srcInfo.stride),
103
+ static_cast<int>(srcInfo.width), static_cast<int>(srcInfo.height),
104
+ preBuf.ptr, static_cast<int>(preStride),
105
+ static_cast<int>(preW), static_cast<int>(preH),
106
+ static_cast<libyuv::FilterModeEnum>(filterMode)
107
+ );
108
+
109
+ if (scaleResult != 0) {
110
+ AndroidBitmap_unlockPixels(env, srcBitmap);
111
+ AndroidBitmap_unlockPixels(env, dstBitmap);
112
+ jclass ex = env->FindClass("java/lang/RuntimeException");
113
+ env->ThrowNew(ex, "ARGBScale failed");
114
+ return;
115
+ }
116
+
117
+ libyuv::RotationMode mode = libyuv::kRotate0;
118
+ if (rotation == 90) mode = libyuv::kRotate90;
119
+ if (rotation == 180) mode = libyuv::kRotate180;
120
+ if (rotation == 270) mode = libyuv::kRotate270;
121
+
122
+ const int rotateResult = libyuv::ARGBRotate(
123
+ preBuf.ptr, static_cast<int>(preStride),
124
+ static_cast<uint8_t*>(dstPixels), static_cast<int>(dstInfo.stride),
125
+ static_cast<int>(preW), static_cast<int>(preH),
126
+ mode
127
+ );
128
+
129
+ AndroidBitmap_unlockPixels(env, srcBitmap);
130
+ AndroidBitmap_unlockPixels(env, dstBitmap);
131
+
132
+ if (rotateResult != 0) {
133
+ jclass ex = env->FindClass("java/lang/RuntimeException");
134
+ env->ThrowNew(ex, "ARGBRotate failed");
135
+ }
136
+ // MallocGuard frees preBuf on scope exit
137
+ }
@@ -0,0 +1,52 @@
1
+ package com.libyuvresizer
2
+
3
+ import kotlin.math.roundToInt
4
+
5
+ object DimensionCalculator {
6
+ fun calculateInSampleSize(srcW: Int, srcH: Int, dstW: Int, dstH: Int): Int {
7
+ var sampleSize = 1
8
+ if (srcH > dstH || srcW > dstW) {
9
+ val halfH = srcH / 2
10
+ val halfW = srcW / 2
11
+ while (halfH / sampleSize >= dstH && halfW / sampleSize >= dstW) sampleSize *= 2
12
+ }
13
+ return sampleSize
14
+ }
15
+
16
+ // floor=true uses toInt() to ensure contain mode never exceeds target dimensions
17
+ fun scaleBy(scale: Double, w: Double, h: Double, floor: Boolean = false): Pair<Int, Int> {
18
+ val round: (Double) -> Int = if (floor) Double::toInt else Double::roundToInt
19
+ return Pair(maxOf(1, round(w * scale)), maxOf(1, round(h * scale)))
20
+ }
21
+
22
+ // For 90°/270°, srcW and srcH are the *raw bitmap* dims (before rotation).
23
+ // effectiveW/H are swapped to represent the post-rotation logical dims used for scale math.
24
+ fun computeDstDims(
25
+ srcW: Double,
26
+ srcH: Double,
27
+ targetW: Int,
28
+ targetH: Int,
29
+ rotation: Int,
30
+ mode: String
31
+ ): Pair<Int, Int> {
32
+ val (effectiveW, effectiveH) = if (rotation == 90 || rotation == 270)
33
+ Pair(srcH, srcW)
34
+ else
35
+ Pair(srcW, srcH)
36
+
37
+ return when (mode) {
38
+ "stretch" -> Pair(targetW, targetH)
39
+ "contain" -> scaleBy(
40
+ minOf(targetW / effectiveW, targetH / effectiveH),
41
+ effectiveW,
42
+ effectiveH,
43
+ floor = true
44
+ )
45
+ else -> scaleBy(
46
+ maxOf(targetW / effectiveW, targetH / effectiveH),
47
+ effectiveW,
48
+ effectiveH
49
+ )
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,133 @@
1
+ package com.libyuvresizer
2
+
3
+ import androidx.exifinterface.media.ExifInterface
4
+ import java.io.IOException
5
+
6
+ internal object ExifCopier {
7
+ fun copy(sourcePath: String, destPath: String) {
8
+ val src = try { ExifInterface(sourcePath) } catch (_: IOException) { return }
9
+ val dst = ExifInterface(destPath)
10
+ for (tag in TAGS) {
11
+ src.getAttribute(tag)?.let { dst.setAttribute(tag, it) }
12
+ }
13
+ dst.setAttribute(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL.toString())
14
+ dst.saveAttributes()
15
+ }
16
+
17
+ private val TAGS = listOf(
18
+ ExifInterface.TAG_APERTURE_VALUE,
19
+ ExifInterface.TAG_ARTIST,
20
+ ExifInterface.TAG_BITS_PER_SAMPLE,
21
+ ExifInterface.TAG_BRIGHTNESS_VALUE,
22
+ ExifInterface.TAG_CAMERA_OWNER_NAME,
23
+ ExifInterface.TAG_COLOR_SPACE,
24
+ ExifInterface.TAG_COMPONENTS_CONFIGURATION,
25
+ ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL,
26
+ ExifInterface.TAG_COMPRESSION,
27
+ ExifInterface.TAG_CONTRAST,
28
+ ExifInterface.TAG_COPYRIGHT,
29
+ ExifInterface.TAG_CUSTOM_RENDERED,
30
+ ExifInterface.TAG_DATETIME,
31
+ ExifInterface.TAG_DATETIME_DIGITIZED,
32
+ ExifInterface.TAG_DATETIME_ORIGINAL,
33
+ ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
34
+ ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
35
+ ExifInterface.TAG_DNG_VERSION,
36
+ ExifInterface.TAG_EXIF_VERSION,
37
+ ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
38
+ ExifInterface.TAG_EXPOSURE_INDEX,
39
+ ExifInterface.TAG_EXPOSURE_MODE,
40
+ ExifInterface.TAG_EXPOSURE_PROGRAM,
41
+ ExifInterface.TAG_EXPOSURE_TIME,
42
+ ExifInterface.TAG_F_NUMBER,
43
+ ExifInterface.TAG_FILE_SOURCE,
44
+ ExifInterface.TAG_FLASH,
45
+ ExifInterface.TAG_FLASH_ENERGY,
46
+ ExifInterface.TAG_FLASHPIX_VERSION,
47
+ ExifInterface.TAG_FOCAL_LENGTH,
48
+ ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
49
+ ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
50
+ ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
51
+ ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
52
+ ExifInterface.TAG_GAIN_CONTROL,
53
+ ExifInterface.TAG_GPS_ALTITUDE,
54
+ ExifInterface.TAG_GPS_ALTITUDE_REF,
55
+ ExifInterface.TAG_GPS_AREA_INFORMATION,
56
+ ExifInterface.TAG_GPS_DATESTAMP,
57
+ ExifInterface.TAG_GPS_DEST_BEARING,
58
+ ExifInterface.TAG_GPS_DEST_BEARING_REF,
59
+ ExifInterface.TAG_GPS_DEST_DISTANCE,
60
+ ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
61
+ ExifInterface.TAG_GPS_DEST_LATITUDE,
62
+ ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
63
+ ExifInterface.TAG_GPS_DEST_LONGITUDE,
64
+ ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
65
+ ExifInterface.TAG_GPS_DIFFERENTIAL,
66
+ ExifInterface.TAG_GPS_DOP,
67
+ ExifInterface.TAG_GPS_H_POSITIONING_ERROR,
68
+ ExifInterface.TAG_GPS_IMG_DIRECTION,
69
+ ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
70
+ ExifInterface.TAG_GPS_LATITUDE,
71
+ ExifInterface.TAG_GPS_LATITUDE_REF,
72
+ ExifInterface.TAG_GPS_LONGITUDE,
73
+ ExifInterface.TAG_GPS_LONGITUDE_REF,
74
+ ExifInterface.TAG_GPS_MAP_DATUM,
75
+ ExifInterface.TAG_GPS_MEASURE_MODE,
76
+ ExifInterface.TAG_GPS_PROCESSING_METHOD,
77
+ ExifInterface.TAG_GPS_SATELLITES,
78
+ ExifInterface.TAG_GPS_SPEED,
79
+ ExifInterface.TAG_GPS_SPEED_REF,
80
+ ExifInterface.TAG_GPS_STATUS,
81
+ ExifInterface.TAG_GPS_TIMESTAMP,
82
+ ExifInterface.TAG_GPS_TRACK,
83
+ ExifInterface.TAG_GPS_TRACK_REF,
84
+ ExifInterface.TAG_GPS_VERSION_ID,
85
+ ExifInterface.TAG_IMAGE_DESCRIPTION,
86
+ ExifInterface.TAG_IMAGE_UNIQUE_ID,
87
+ ExifInterface.TAG_INTEROPERABILITY_INDEX,
88
+ ExifInterface.TAG_ISO_SPEED,
89
+ ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
90
+ ExifInterface.TAG_LENS_MAKE,
91
+ ExifInterface.TAG_LENS_MODEL,
92
+ ExifInterface.TAG_LENS_SERIAL_NUMBER,
93
+ ExifInterface.TAG_LENS_SPECIFICATION,
94
+ ExifInterface.TAG_LIGHT_SOURCE,
95
+ ExifInterface.TAG_MAKE,
96
+ ExifInterface.TAG_MAKER_NOTE,
97
+ ExifInterface.TAG_MAX_APERTURE_VALUE,
98
+ ExifInterface.TAG_METERING_MODE,
99
+ ExifInterface.TAG_MODEL,
100
+ ExifInterface.TAG_OECF,
101
+ ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
102
+ ExifInterface.TAG_PIXEL_X_DIMENSION,
103
+ ExifInterface.TAG_PIXEL_Y_DIMENSION,
104
+ ExifInterface.TAG_RELATED_SOUND_FILE,
105
+ ExifInterface.TAG_RESOLUTION_UNIT,
106
+ ExifInterface.TAG_SATURATION,
107
+ ExifInterface.TAG_SCENE_CAPTURE_TYPE,
108
+ ExifInterface.TAG_SCENE_TYPE,
109
+ ExifInterface.TAG_SENSING_METHOD,
110
+ ExifInterface.TAG_SHARPNESS,
111
+ ExifInterface.TAG_SHUTTER_SPEED_VALUE,
112
+ ExifInterface.TAG_SOFTWARE,
113
+ ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE,
114
+ ExifInterface.TAG_SPECTRAL_SENSITIVITY,
115
+ ExifInterface.TAG_SUBJECT_AREA,
116
+ ExifInterface.TAG_SUBJECT_DISTANCE,
117
+ ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
118
+ ExifInterface.TAG_SUBJECT_LOCATION,
119
+ ExifInterface.TAG_SUBSEC_TIME,
120
+ ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
121
+ ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
122
+ ExifInterface.TAG_TRANSFER_FUNCTION,
123
+ ExifInterface.TAG_USER_COMMENT,
124
+ ExifInterface.TAG_WHITE_BALANCE,
125
+ ExifInterface.TAG_WHITE_POINT,
126
+ ExifInterface.TAG_X_RESOLUTION,
127
+ ExifInterface.TAG_Y_CB_CR_COEFFICIENTS,
128
+ ExifInterface.TAG_Y_CB_CR_POSITIONING,
129
+ ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING,
130
+ ExifInterface.TAG_Y_RESOLUTION,
131
+ // TAG_ORIENTATION excluded — reset to ORIENTATION_NORMAL after loop
132
+ )
133
+ }