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.
- package/LICENSE +20 -0
- package/LibyuvResizer.podspec +20 -0
- package/README.md +188 -0
- package/android/CMakeLists.txt +30 -0
- package/android/build.gradle +100 -0
- package/android/src/androidTest/java/com/libyuvresizer/ExifCopierTest.kt +131 -0
- package/android/src/androidTest/java/com/libyuvresizer/FakePromise.kt +71 -0
- package/android/src/androidTest/java/com/libyuvresizer/FakeReactContext.kt +55 -0
- package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleErrorTest.kt +135 -0
- package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleExifTest.kt +140 -0
- package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleFilterModeTest.kt +85 -0
- package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleFormatTest.kt +146 -0
- package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleIntegrationTest.kt +157 -0
- package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleOutputPathTest.kt +96 -0
- package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleRotationTest.kt +120 -0
- package/android/src/androidTest/java/com/libyuvresizer/TestFixtures.kt +48 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/LibyuvResizerModule.cpp +137 -0
- package/android/src/main/java/com/libyuvresizer/DimensionCalculator.kt +52 -0
- package/android/src/main/java/com/libyuvresizer/ExifCopier.kt +133 -0
- package/android/src/main/java/com/libyuvresizer/LibyuvResizerModule.kt +179 -0
- package/android/src/main/java/com/libyuvresizer/LibyuvResizerPackage.kt +30 -0
- package/android/src/main/java/com/libyuvresizer/ResizeValidator.kt +71 -0
- package/android/src/test/java/com/libyuvresizer/DimensionCalculatorTest.kt +181 -0
- package/android/src/test/java/com/libyuvresizer/ResizeValidatorTest.kt +203 -0
- package/ios/LibyuvResizer.h +6 -0
- package/ios/LibyuvResizer.mm +31 -0
- package/lib/module/NativeLibyuvResizer.js +28 -0
- package/lib/module/NativeLibyuvResizer.js.map +1 -0
- package/lib/module/index.js +20 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/resizer.js +15 -0
- package/lib/module/resizer.js.map +1 -0
- package/lib/module/resizer.native.js +110 -0
- package/lib/module/resizer.native.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeLibyuvResizer.d.ts +52 -0
- package/lib/typescript/src/NativeLibyuvResizer.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +19 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/resizer.d.ts +13 -0
- package/lib/typescript/src/resizer.d.ts.map +1 -0
- package/lib/typescript/src/resizer.native.d.ts +119 -0
- package/lib/typescript/src/resizer.native.d.ts.map +1 -0
- package/package.json +184 -0
- package/src/NativeLibyuvResizer.ts +81 -0
- package/src/index.tsx +23 -0
- package/src/resizer.native.tsx +175 -0
- 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,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
|
+
}
|