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
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
|
+
}
|