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
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anderson Souza
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
@@ -0,0 +1,20 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "LibyuvResizer"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/anderson-souza/react-native-libyuv-resizer.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
+ s.private_header_files = "ios/**/*.h"
18
+
19
+ install_modules_dependencies(s)
20
+ end
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # react-native-libyuv-resizer
2
+
3
+ High-performance image resizer for React Native using libyuv (Android).
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ yarn add react-native-libyuv-resizer
9
+ ```
10
+
11
+ ## API
12
+
13
+ ### `resize(filePath, targetWidth, targetHeight, quality, options?): Promise<ResizeResult>`
14
+
15
+ | Parameter | Type | Default | Description |
16
+ | -------------------- | --------------- | ----------- | --------------------------------------------------------------------------------- |
17
+ | `filePath` | `string` | — | Absolute path to source image |
18
+ | `targetWidth` | `number` | — | Output width in pixels |
19
+ | `targetHeight` | `number` | — | Output height in pixels |
20
+ | `quality` | `number` | — | Compression quality `1–100`. Controls JPEG and WebP lossy level. Ignored for PNG. When `format` is omitted: `100` → PNG, else → JPEG. |
21
+ | `options.rotation` | `RotationAngle` | `0` | Clockwise rotation before resize: `0 \| 90 \| 180 \| 270 \| -90 \| -180 \| -270` |
22
+ | `options.mode` | `ResizeMode` | `'contain'` | How the image fits the target box: `'contain' \| 'cover' \| 'stretch'` |
23
+ | `options.filterMode` | `FilterMode` | `'box'` | Scaling filter: `'none' \| 'linear' \| 'bilinear' \| 'box'` |
24
+ | `options.outputPath` | `string` | auto | Absolute directory path for the output file. Auto-generated in cache if omitted. |
25
+ | `options.keepMeta` | `boolean` | `false` | Copy EXIF metadata from source to output JPEG. Android only; no-op on iOS, PNG and WebP output. |
26
+ | `options.format` | `OutputFormat` | derived | Output format: `'jpeg' \| 'png' \| 'webp'`. When specified, takes precedence over the `quality`-based heuristic. WebP is Android only; iOS produces JPEG. |
27
+
28
+ ### Return value
29
+
30
+ ```ts
31
+ interface ResizeResult {
32
+ path: string; // absolute path to the resized file
33
+ uri: string; // file:// URI
34
+ size: number; // file size in bytes
35
+ name: string; // file name
36
+ width: number; // output width in pixels
37
+ height: number; // output height in pixels
38
+ }
39
+ ```
40
+
41
+ ### Types
42
+
43
+ ```ts
44
+ type RotationAngle = 0 | 90 | 180 | 270 | -90 | -180 | -270;
45
+ type ResizeMode = 'contain' | 'cover' | 'stretch';
46
+ type FilterMode = 'none' | 'linear' | 'bilinear' | 'box';
47
+ type OutputFormat = 'jpeg' | 'png' | 'webp';
48
+
49
+ interface ResizeOptions {
50
+ rotation?: RotationAngle;
51
+ mode?: ResizeMode;
52
+ filterMode?: FilterMode;
53
+ outputPath?: string;
54
+ keepMeta?: boolean;
55
+ format?: OutputFormat;
56
+ }
57
+ ```
58
+
59
+ ### Resize modes
60
+
61
+ | Mode | Behavior |
62
+ | --------- | -------------------------------------------------------------- |
63
+ | `contain` | Fits entirely within the target box, preserving aspect ratio |
64
+ | `cover` | Fills the target box, preserving aspect ratio (may crop) |
65
+ | `stretch` | Stretches to exact target dimensions, ignoring aspect ratio |
66
+
67
+ ### Filter modes
68
+
69
+ | Mode | Quality / Speed trade-off |
70
+ | ---------- | ------------------------------------------------------ |
71
+ | `none` | Nearest-neighbor — fastest, lowest quality |
72
+ | `linear` | Linear interpolation |
73
+ | `bilinear` | Bilinear interpolation |
74
+ | `box` | Box filter — best quality for downscaling *(default)* |
75
+
76
+ ## Usage
77
+
78
+ ```ts
79
+ import { resize } from 'react-native-libyuv-resizer';
80
+
81
+ // Basic resize
82
+ const result = await resize('/path/to/photo.jpg', 1280, 720, 85);
83
+ console.log(result.path, result.width, result.height);
84
+
85
+ // Resize with options
86
+ const result = await resize('/path/to/photo.jpg', 1280, 720, 85, {
87
+ rotation: 90,
88
+ mode: 'cover',
89
+ filterMode: 'bilinear',
90
+ });
91
+
92
+ // Preserve EXIF metadata (GPS, camera, date) — Android only
93
+ const result = await resize('/path/to/photo.jpg', 800, 600, 80, {
94
+ keepMeta: true,
95
+ });
96
+
97
+ // Custom output path
98
+ const result = await resize('/path/to/photo.jpg', 800, 600, 80, {
99
+ outputPath: '/path/to/output-dir',
100
+ });
101
+
102
+ // PNG output — explicit format
103
+ const result = await resize('/path/to/photo.jpg', 800, 600, 80, {
104
+ format: 'png',
105
+ });
106
+ console.log(result.path); // ends with .png
107
+
108
+ // WebP output — smaller files than JPEG at equivalent quality (Android only)
109
+ const result = await resize('/path/to/photo.jpg', 1280, 720, 85, {
110
+ format: 'webp',
111
+ });
112
+ console.log(result.path); // ends with .webp
113
+ ```
114
+
115
+ ### With react-native-image-picker
116
+
117
+ ```ts
118
+ import { launchImageLibrary } from 'react-native-image-picker';
119
+ import { resize } from 'react-native-libyuv-resizer';
120
+
121
+ const picked = await launchImageLibrary({ mediaType: 'photo' });
122
+ const asset = picked.assets?.[0];
123
+
124
+ if (asset?.uri) {
125
+ const result = await resize(asset.uri, 800, 600, 80, {
126
+ mode: 'cover',
127
+ keepMeta: true, // preserve GPS and camera tags
128
+ });
129
+ console.log('Resized image at:', result.path);
130
+ }
131
+ ```
132
+
133
+ ## format — output format
134
+
135
+ The `format` option explicitly controls the output container. When omitted, the library falls back to the legacy quality-based heuristic (`quality === 100` → PNG, otherwise JPEG).
136
+
137
+ | Value | Output | `quality` effect | Platform |
138
+ | -------- | ------ | ---------------- | -------- |
139
+ | `'jpeg'` | JPEG | Lossy compression level | Android, iOS |
140
+ | `'png'` | PNG | Ignored (lossless) | Android, iOS |
141
+ | `'webp'` | WebP lossy | Compression level | Android only; iOS produces JPEG |
142
+
143
+ **Performance note:** WebP encoding is significantly slower than JPEG (~5–15× on typical Android devices) because it runs in software with no dedicated hardware accelerator. Use `format: 'webp'` when file size matters more than encoding speed.
144
+
145
+ **Format vs quality precedence:**
146
+
147
+ ```ts
148
+ // format wins — output is JPEG even though quality=100
149
+ await resize(src, 800, 600, 100, { format: 'jpeg' });
150
+
151
+ // no format — quality=100 heuristic applies → PNG (backward compat)
152
+ await resize(src, 800, 600, 100);
153
+ ```
154
+
155
+ ## keepMeta — EXIF preservation
156
+
157
+ When `keepMeta: true`, all EXIF tags present in the source JPEG are copied to the output JPEG after encoding. This includes GPS coordinates, camera make/model, capture date, and any other tags supported by `androidx.exifinterface`.
158
+
159
+ **Behavior by scenario:**
160
+
161
+ | Scenario | Result |
162
+ | --- | --- |
163
+ | `keepMeta: true` + JPEG output | EXIF tags copied; `Orientation` reset to normal |
164
+ | `keepMeta: true` + PNG output | No-op — PNG has no standard EXIF |
165
+ | `keepMeta: true` + WebP output | No-op — consistent with PNG behaviour |
166
+ | `keepMeta: true` + iOS | No-op — iOS implementation pending |
167
+ | `keepMeta: false` (default) | No EXIF copy; identical to previous behavior |
168
+ | Source has no EXIF | Succeeds silently — output has no EXIF |
169
+
170
+ > **Note:** `Orientation` is always reset to `1` (normal) in the output, because the bitmap was decoded and re-encoded with the correct orientation already applied.
171
+
172
+ ## Platform notes
173
+
174
+ | Platform | Backend | keepMeta | WebP output |
175
+ | ----------- | ---------------------------- | -------- | ----------- |
176
+ | Android | libyuv (`ARGBScale`) via NDK | ✅ | ✅ (`WEBP_LOSSY` API 30+, `WEBP` fallback) |
177
+ | iOS | Not yet implemented | no-op | no-op (produces JPEG) |
178
+ | Web / other | Throws — native only | — | — |
179
+
180
+ ## Contributing
181
+
182
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
183
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
184
+ - [Code of conduct](CODE_OF_CONDUCT.md)
185
+
186
+ ## License
187
+
188
+ MIT
@@ -0,0 +1,30 @@
1
+ project(libyuvresizer)
2
+ cmake_minimum_required(VERSION 3.9.0)
3
+
4
+ set(PACKAGE_NAME "libyuvresizer")
5
+ set(CMAKE_CXX_STANDARD 17)
6
+
7
+ find_library(LOG_LIB log)
8
+
9
+ # libyuv
10
+ add_subdirectory(../libyuv libyuv)
11
+
12
+ add_library(
13
+ ${PACKAGE_NAME}
14
+ SHARED
15
+ src/main/cpp/LibyuvResizerModule.cpp
16
+ )
17
+
18
+ target_include_directories(
19
+ ${PACKAGE_NAME} PRIVATE
20
+ src/main/cpp
21
+ ../libyuv/include
22
+ )
23
+
24
+ target_link_libraries(
25
+ ${PACKAGE_NAME}
26
+ ${LOG_LIB}
27
+ android
28
+ jnigraphics
29
+ yuv
30
+ )
@@ -0,0 +1,100 @@
1
+ buildscript {
2
+ ext.LibyuvResizer = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36,
6
+ targetSdkVersion: 36
7
+ ]
8
+
9
+ ext.getExtOrDefault = { prop ->
10
+ if (rootProject.ext.has(prop)) {
11
+ return rootProject.ext.get(prop)
12
+ }
13
+
14
+ return LibyuvResizer[prop]
15
+ }
16
+
17
+ repositories {
18
+ google()
19
+ mavenCentral()
20
+ }
21
+
22
+ dependencies {
23
+ classpath "com.android.tools.build:gradle:8.7.2"
24
+ // noinspection DifferentKotlinGradleVersion
25
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
26
+ }
27
+ }
28
+
29
+
30
+ apply plugin: "com.android.library"
31
+ apply plugin: "kotlin-android"
32
+
33
+ apply plugin: "com.facebook.react"
34
+
35
+ android {
36
+ namespace "com.libyuvresizer"
37
+
38
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
39
+
40
+ defaultConfig {
41
+ minSdkVersion getExtOrDefault("minSdkVersion")
42
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
43
+
44
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
45
+
46
+ buildConfigField(
47
+ "boolean",
48
+ "IS_NEW_ARCHITECTURE_ENABLED",
49
+ project.hasProperty("newArchEnabled") ? project.getProperty("newArchEnabled") : "false"
50
+ )
51
+
52
+ externalNativeBuild {
53
+ cmake {
54
+ cppFlags "-O2 -frtti -fexceptions -Wall -fstack-protector-all"
55
+ abiFilters "armeabi-v7a", "x86", "x86_64", "arm64-v8a"
56
+ arguments "-DANDROID_STL=c++_shared",
57
+ "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
58
+ }
59
+ }
60
+ }
61
+
62
+ externalNativeBuild {
63
+ cmake {
64
+ path "CMakeLists.txt"
65
+ }
66
+ }
67
+
68
+ buildFeatures {
69
+ buildConfig true
70
+ }
71
+
72
+ buildTypes {
73
+ release {
74
+ minifyEnabled false
75
+ }
76
+ }
77
+
78
+ lint {
79
+ disable "GradleCompatible"
80
+ }
81
+
82
+ packagingOptions {
83
+ pickFirst 'lib/*/libc++_shared.so'
84
+ }
85
+
86
+ compileOptions {
87
+ sourceCompatibility JavaVersion.VERSION_1_8
88
+ targetCompatibility JavaVersion.VERSION_1_8
89
+ }
90
+ }
91
+
92
+ dependencies {
93
+ implementation "com.facebook.react:react-android"
94
+ implementation "androidx.exifinterface:exifinterface:1.3.7"
95
+ testImplementation "junit:junit:4.13.2"
96
+ androidTestImplementation "androidx.test:runner:1.6.2"
97
+ androidTestImplementation "androidx.test:rules:1.6.1"
98
+ androidTestImplementation "androidx.test.ext:junit:1.2.1"
99
+ androidTestImplementation "androidx.test:core:1.6.1"
100
+ }
@@ -0,0 +1,131 @@
1
+ package com.libyuvresizer
2
+
3
+ import androidx.exifinterface.media.ExifInterface
4
+ import androidx.test.ext.junit.runners.AndroidJUnit4
5
+ import androidx.test.platform.app.InstrumentationRegistry
6
+ import org.junit.After
7
+ import org.junit.Assert.assertEquals
8
+ import org.junit.Assert.assertNull
9
+ import org.junit.Before
10
+ import org.junit.Test
11
+ import org.junit.runner.RunWith
12
+ import java.io.File
13
+
14
+ @RunWith(AndroidJUnit4::class)
15
+ class ExifCopierTest {
16
+
17
+ private lateinit var cacheDir: File
18
+ private val tempFiles = mutableListOf<File>()
19
+
20
+ @Before
21
+ fun setUp() {
22
+ cacheDir = InstrumentationRegistry.getInstrumentation().targetContext.cacheDir
23
+ }
24
+
25
+ @After
26
+ fun tearDown() {
27
+ tempFiles.forEach { it.delete() }
28
+ tempFiles.clear()
29
+ }
30
+
31
+ private fun createJpegWithExif(name: String, block: ExifInterface.() -> Unit): String {
32
+ val path = TestFixtures.createJpeg(
33
+ InstrumentationRegistry.getInstrumentation().targetContext,
34
+ 100, 100, name
35
+ )
36
+ tempFiles += File(path)
37
+ ExifInterface(path).apply(block).saveAttributes()
38
+ return path
39
+ }
40
+
41
+ private fun tempDest(name: String): String {
42
+ val f = File(cacheDir, name)
43
+ tempFiles += f
44
+ return f.absolutePath
45
+ }
46
+
47
+ @Test
48
+ fun `copy GPS tags are preserved in destination`() {
49
+ val src = createJpegWithExif("exif_src_gps.jpg") {
50
+ setAttribute(ExifInterface.TAG_GPS_LATITUDE, "48/1,51/1,29/1")
51
+ setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, "N")
52
+ setAttribute(ExifInterface.TAG_GPS_LONGITUDE, "2/1,17/1,40/1")
53
+ setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, "E")
54
+ }
55
+ val dest = tempDest("exif_dst_gps.jpg")
56
+ TestFixtures.createJpeg(
57
+ InstrumentationRegistry.getInstrumentation().targetContext,
58
+ 50, 50, "exif_dst_gps.jpg"
59
+ ).also { File(it).copyTo(File(dest), overwrite = true) }
60
+
61
+ ExifCopier.copy(src, dest)
62
+
63
+ val result = ExifInterface(dest)
64
+ assertEquals("48/1,51/1,29/1", result.getAttribute(ExifInterface.TAG_GPS_LATITUDE))
65
+ assertEquals("N", result.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF))
66
+ assertEquals("2/1,17/1,40/1", result.getAttribute(ExifInterface.TAG_GPS_LONGITUDE))
67
+ assertEquals("E", result.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF))
68
+ }
69
+
70
+ @Test
71
+ fun `copy make and model are preserved`() {
72
+ val src = createJpegWithExif("exif_src_cam.jpg") {
73
+ setAttribute(ExifInterface.TAG_MAKE, "Google")
74
+ setAttribute(ExifInterface.TAG_MODEL, "Pixel 8")
75
+ setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, "2024:01:15 10:30:00")
76
+ }
77
+ val dest = tempDest("exif_dst_cam.jpg")
78
+ File(src).copyTo(File(dest), overwrite = true)
79
+
80
+ ExifCopier.copy(src, dest)
81
+
82
+ val result = ExifInterface(dest)
83
+ assertEquals("Google", result.getAttribute(ExifInterface.TAG_MAKE))
84
+ assertEquals("Pixel 8", result.getAttribute(ExifInterface.TAG_MODEL))
85
+ assertEquals("2024:01:15 10:30:00", result.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL))
86
+ }
87
+
88
+ @Test
89
+ fun `copy orientation is always reset to normal`() {
90
+ val src = createJpegWithExif("exif_src_rot.jpg") {
91
+ setAttribute(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_ROTATE_90.toString())
92
+ }
93
+ val dest = tempDest("exif_dst_rot.jpg")
94
+ File(src).copyTo(File(dest), overwrite = true)
95
+
96
+ ExifCopier.copy(src, dest)
97
+
98
+ val result = ExifInterface(dest)
99
+ assertEquals(
100
+ ExifInterface.ORIENTATION_NORMAL,
101
+ result.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)
102
+ )
103
+ }
104
+
105
+ @Test
106
+ fun `copy source with no EXIF succeeds without error`() {
107
+ val src = TestFixtures.createJpeg(
108
+ InstrumentationRegistry.getInstrumentation().targetContext,
109
+ 50, 50, "exif_src_noexif.jpg"
110
+ ).also { tempFiles += File(it) }
111
+ val dest = tempDest("exif_dst_noexif.jpg")
112
+ File(src).copyTo(File(dest), overwrite = true)
113
+
114
+ ExifCopier.copy(src, dest)
115
+
116
+ val result = ExifInterface(dest)
117
+ assertNull(result.getAttribute(ExifInterface.TAG_GPS_LATITUDE))
118
+ }
119
+
120
+ @Test
121
+ fun `copy source path does not exist returns silently`() {
122
+ val dest = tempDest("exif_dst_silent.jpg")
123
+ TestFixtures.createJpeg(
124
+ InstrumentationRegistry.getInstrumentation().targetContext,
125
+ 50, 50, "exif_dst_silent.jpg"
126
+ ).also { File(it).copyTo(File(dest), overwrite = true); tempFiles += File(it) }
127
+
128
+ // Must not throw
129
+ ExifCopier.copy("/nonexistent/path/source.jpg", dest)
130
+ }
131
+ }
@@ -0,0 +1,71 @@
1
+ package com.libyuvresizer
2
+
3
+ import com.facebook.react.bridge.Promise
4
+ import com.facebook.react.bridge.WritableMap
5
+
6
+ class FakePromise : Promise {
7
+ var result: Any? = null
8
+ private set
9
+ var errorCode: String? = null
10
+ private set
11
+ var errorMessage: String? = null
12
+ private set
13
+
14
+ private var _resolved = false
15
+ val resolved: Boolean get() = _resolved
16
+ val rejected: Boolean get() = errorCode != null
17
+
18
+ override fun resolve(value: Any?) {
19
+ result = value
20
+ _resolved = true
21
+ }
22
+
23
+ override fun reject(code: String?, message: String?) {
24
+ errorCode = code
25
+ errorMessage = message
26
+ }
27
+
28
+ override fun reject(code: String?, throwable: Throwable?) {
29
+ errorCode = code
30
+ errorMessage = throwable?.message
31
+ }
32
+
33
+ override fun reject(code: String?, message: String?, throwable: Throwable?) {
34
+ errorCode = code
35
+ errorMessage = message
36
+ }
37
+
38
+ override fun reject(code: String?, userInfo: WritableMap) {
39
+ errorCode = code
40
+ }
41
+
42
+ override fun reject(code: String?, throwable: Throwable?, userInfo: WritableMap) {
43
+ errorCode = code
44
+ errorMessage = throwable?.message
45
+ }
46
+
47
+ override fun reject(code: String?, message: String?, userInfo: WritableMap) {
48
+ errorCode = code
49
+ errorMessage = message
50
+ }
51
+
52
+ override fun reject(code: String?, message: String?, throwable: Throwable?, userInfo: WritableMap?) {
53
+ errorCode = code
54
+ errorMessage = message
55
+ }
56
+
57
+ override fun reject(throwable: Throwable) {
58
+ errorCode = "E_UNKNOWN"
59
+ errorMessage = throwable.message
60
+ }
61
+
62
+ override fun reject(throwable: Throwable, userInfo: WritableMap) {
63
+ errorCode = "E_UNKNOWN"
64
+ errorMessage = throwable.message
65
+ }
66
+
67
+ override fun reject(message: String) {
68
+ errorCode = "E_UNKNOWN"
69
+ errorMessage = message
70
+ }
71
+ }
@@ -0,0 +1,55 @@
1
+ package com.libyuvresizer
2
+
3
+ import android.content.Context
4
+ import com.facebook.react.bridge.Callback
5
+ import com.facebook.react.bridge.CatalystInstance
6
+ import com.facebook.react.bridge.JavaScriptContextHolder
7
+ import com.facebook.react.bridge.JavaScriptModule
8
+ import com.facebook.react.bridge.NativeModule
9
+ import com.facebook.react.bridge.ReactApplicationContext
10
+ import com.facebook.react.bridge.UIManager
11
+ import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder
12
+
13
+ @Suppress("UNCHECKED_CAST")
14
+ class FakeReactContext(context: Context) : ReactApplicationContext(context) {
15
+
16
+ override fun <T : JavaScriptModule> getJSModule(jsInterface: Class<T>): T =
17
+ throw UnsupportedOperationException()
18
+
19
+ override fun <T : NativeModule> hasNativeModule(nativeModuleInterface: Class<T>): Boolean = false
20
+
21
+ override fun getNativeModules(): MutableCollection<NativeModule> = mutableListOf()
22
+
23
+ override fun <T : NativeModule> getNativeModule(nativeModuleInterface: Class<T>): T? = null
24
+
25
+ override fun getNativeModule(moduleName: String): NativeModule? = null
26
+
27
+ override fun getCatalystInstance(): CatalystInstance =
28
+ throw UnsupportedOperationException()
29
+
30
+ override fun hasActiveCatalystInstance(): Boolean = false
31
+
32
+ override fun hasActiveReactInstance(): Boolean = false
33
+
34
+ override fun hasCatalystInstance(): Boolean = false
35
+
36
+ override fun hasReactInstance(): Boolean = false
37
+
38
+ override fun destroy() {}
39
+
40
+ override fun handleException(e: Exception) {
41
+ throw RuntimeException(e)
42
+ }
43
+
44
+ override fun isBridgeless(): Boolean = false
45
+
46
+ override fun getJavaScriptContextHolder(): JavaScriptContextHolder? = null
47
+
48
+ override fun getJSCallInvokerHolder(): CallInvokerHolder? = null
49
+
50
+ override fun getFabricUIManager(): UIManager? = null
51
+
52
+ override fun getSourceURL(): String? = null
53
+
54
+ override fun registerSegment(segmentId: Int, path: String, callback: Callback) {}
55
+ }