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,179 @@
1
+ package com.libyuvresizer
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.BitmapFactory
5
+ import android.net.Uri
6
+ import android.os.Build
7
+ import com.facebook.react.bridge.Arguments
8
+ import com.facebook.react.bridge.Promise
9
+ import com.facebook.react.bridge.ReactApplicationContext
10
+ import com.facebook.react.module.annotations.ReactModule
11
+ import java.io.File
12
+ import java.io.FileOutputStream
13
+ import java.io.IOException
14
+ import java.util.UUID
15
+ import androidx.core.graphics.createBitmap
16
+
17
+ @ReactModule(name = LibyuvResizerModule.NAME)
18
+ class LibyuvResizerModule(reactContext: ReactApplicationContext) :
19
+ NativeLibyuvResizerSpec(reactContext) {
20
+
21
+ companion object {
22
+ const val NAME = NativeLibyuvResizerSpec.NAME
23
+
24
+ private val FILTER_MODE_MAP = mapOf("none" to 0, "linear" to 1, "bilinear" to 2, "box" to 3)
25
+
26
+ @Suppress("DEPRECATION")
27
+ fun formatToExtAndCompressFormat(format: String): Pair<String, Bitmap.CompressFormat> =
28
+ when (format) {
29
+ "png" -> "png" to Bitmap.CompressFormat.PNG
30
+ "webp" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
31
+ "webp" to Bitmap.CompressFormat.WEBP_LOSSY
32
+ } else {
33
+ "webp" to Bitmap.CompressFormat.WEBP
34
+ }
35
+
36
+ else -> "jpg" to Bitmap.CompressFormat.JPEG
37
+ }
38
+
39
+ init {
40
+ System.loadLibrary("libyuvresizer")
41
+ }
42
+ }
43
+
44
+ private external fun nativeResize(srcBitmap: Bitmap, dstBitmap: Bitmap, filterMode: Int)
45
+ private external fun nativeResizeAndRotate(
46
+ srcBitmap: Bitmap,
47
+ dstBitmap: Bitmap,
48
+ rotation: Int,
49
+ filterMode: Int
50
+ )
51
+
52
+ override fun resize(
53
+ filePath: String,
54
+ targetWidth: Double,
55
+ targetHeight: Double,
56
+ quality: Double,
57
+ rotation: Double,
58
+ mode: String,
59
+ outputPath: String,
60
+ filterMode: String,
61
+ keepMeta: Boolean,
62
+ format: String,
63
+ promise: Promise
64
+ ) {
65
+ try {
66
+ val targetW = targetWidth.toInt()
67
+ val targetH = targetHeight.toInt()
68
+ val q = quality.toInt()
69
+ val rot = rotation.toInt()
70
+
71
+ val params = ResizeParams(
72
+ filePath,
73
+ targetW,
74
+ targetH,
75
+ q,
76
+ rot,
77
+ mode,
78
+ outputPath,
79
+ filterMode,
80
+ keepMeta,
81
+ format
82
+ )
83
+ when (val result = ResizeValidator.validate(params)) {
84
+ is ValidationResult.Invalid -> {
85
+ promise.reject(result.code, result.message)
86
+ return
87
+ }
88
+
89
+ is ValidationResult.Valid -> Unit
90
+ }
91
+
92
+ val bitmapConfig = Bitmap.Config.ARGB_8888
93
+
94
+ val boundsOpts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
95
+ BitmapFactory.decodeFile(filePath, boundsOpts)
96
+
97
+ val decodeOpts = BitmapFactory.Options().apply {
98
+ inSampleSize =
99
+ DimensionCalculator.calculateInSampleSize(
100
+ boundsOpts.outWidth,
101
+ boundsOpts.outHeight,
102
+ targetW,
103
+ targetH
104
+ )
105
+ inPreferredConfig = bitmapConfig
106
+ }
107
+ val srcBitmap = BitmapFactory.decodeFile(filePath, decodeOpts)
108
+ ?: run {
109
+ promise.reject("E_DECODE_FAILED", "Failed to decode image")
110
+ return
111
+ }
112
+
113
+ // srcBitmap must be recycled even if dstBitmap allocation throws OOM
114
+ try {
115
+ val srcW = srcBitmap.width.toDouble()
116
+ val srcH = srcBitmap.height.toDouble()
117
+
118
+ // cover scales to fill target on both axes — no cropping applied by design
119
+ val (dstW, dstH) = DimensionCalculator.computeDstDims(
120
+ srcW,
121
+ srcH,
122
+ targetW,
123
+ targetH,
124
+ rot,
125
+ mode
126
+ )
127
+
128
+ val dstBitmap = createBitmap(dstW, dstH, bitmapConfig)
129
+ try {
130
+ val filterModeInt = FILTER_MODE_MAP.getValue(filterMode)
131
+ if (rot == 0) {
132
+ nativeResize(srcBitmap, dstBitmap, filterModeInt)
133
+ } else {
134
+ nativeResizeAndRotate(srcBitmap, dstBitmap, rot, filterModeInt)
135
+ }
136
+
137
+ val (ext, compressFmt) = formatToExtAndCompressFormat(params.format)
138
+ val outFile = resolveOutputFile(filePath, outputPath, ext)
139
+ FileOutputStream(outFile).use { fos ->
140
+ dstBitmap.compress(compressFmt, q, fos)
141
+ }
142
+
143
+ if (params.keepMeta && params.format == "jpeg") {
144
+ try {
145
+ ExifCopier.copy(filePath, outFile.absolutePath)
146
+ } catch (e: IOException) {
147
+ promise.reject("E_EXIF_WRITE_FAILED", e.message ?: "Failed to write EXIF metadata")
148
+ return
149
+ }
150
+ }
151
+
152
+ val result = Arguments.createMap().apply {
153
+ putString("path", outFile.absolutePath)
154
+ putString("uri", Uri.fromFile(outFile).toString())
155
+ putDouble("size", outFile.length().toDouble())
156
+ putString("name", outFile.name)
157
+ putInt("width", dstBitmap.width)
158
+ putInt("height", dstBitmap.height)
159
+ }
160
+ promise.resolve(result)
161
+ } finally {
162
+ dstBitmap.recycle()
163
+ }
164
+ } finally {
165
+ srcBitmap.recycle()
166
+ }
167
+ } catch (e: Exception) {
168
+ promise.reject("E_UNKNOWN", e.message ?: "Unknown error")
169
+ }
170
+ }
171
+
172
+ private fun resolveOutputFile(inputFilePath: String, outputPath: String, ext: String): File {
173
+ if (outputPath.isEmpty()) {
174
+ return File(reactApplicationContext.cacheDir, "${UUID.randomUUID()}.$ext")
175
+ }
176
+ return File(outputPath, File(inputFilePath).name)
177
+ }
178
+
179
+ }
@@ -0,0 +1,30 @@
1
+ package com.libyuvresizer
2
+
3
+ import com.facebook.react.TurboReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+
9
+ class LibyuvResizerPackage : TurboReactPackage() {
10
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
11
+ return if (name == LibyuvResizerModule.NAME) {
12
+ LibyuvResizerModule(reactContext)
13
+ } else {
14
+ null
15
+ }
16
+ }
17
+
18
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
19
+ mapOf(
20
+ LibyuvResizerModule.NAME to ReactModuleInfo(
21
+ name = LibyuvResizerModule.NAME,
22
+ className = LibyuvResizerModule.NAME,
23
+ canOverrideExistingModule = false,
24
+ needsEagerInit = false,
25
+ isCxxModule = false,
26
+ isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
27
+ )
28
+ )
29
+ }
30
+ }
@@ -0,0 +1,71 @@
1
+ package com.libyuvresizer
2
+
3
+ import java.io.File
4
+
5
+ data class ResizeParams(
6
+ val filePath: String,
7
+ val targetWidth: Int,
8
+ val targetHeight: Int,
9
+ val quality: Int,
10
+ val rotation: Int,
11
+ val mode: String,
12
+ val outputPath: String,
13
+ val filterMode: String,
14
+ val keepMeta: Boolean = false,
15
+ val format: String = "jpeg"
16
+ )
17
+
18
+ sealed class ValidationResult {
19
+ object Valid : ValidationResult()
20
+ data class Invalid(val code: String, val message: String) : ValidationResult()
21
+ }
22
+
23
+ object ResizeValidator {
24
+ private val VALID_MODES = setOf("contain", "cover", "stretch")
25
+ private val VALID_FILTER_MODES = setOf("none", "linear", "bilinear", "box")
26
+ private val VALID_ROTATIONS = setOf(0, 90, 180, 270)
27
+ private val VALID_FORMATS = setOf("jpeg", "png", "webp")
28
+
29
+ fun validate(params: ResizeParams): ValidationResult {
30
+ if (!File(params.filePath).exists())
31
+ return ValidationResult.Invalid("E_FILE_NOT_FOUND", "File not found: ${params.filePath}")
32
+ if (params.targetWidth <= 0 || params.targetHeight <= 0)
33
+ return ValidationResult.Invalid("E_INVALID_DIMS", "Invalid dimensions")
34
+ if (params.quality !in 1..100)
35
+ return ValidationResult.Invalid("E_INVALID_QUALITY", "Quality must be between 1 and 100")
36
+ if (params.rotation !in VALID_ROTATIONS)
37
+ return ValidationResult.Invalid(
38
+ "E_INVALID_ROTATION",
39
+ "rotation must be 0, 90, 180 or 270, got: ${params.rotation}"
40
+ )
41
+ if (params.mode !in VALID_MODES)
42
+ return ValidationResult.Invalid(
43
+ "E_INVALID_MODE",
44
+ "mode must be contain, cover, or stretch, got: ${params.mode}"
45
+ )
46
+ if (params.filterMode !in VALID_FILTER_MODES)
47
+ return ValidationResult.Invalid(
48
+ "E_INVALID_FILTER_MODE",
49
+ "filterMode must be none, linear, bilinear, or box, got: ${params.filterMode}"
50
+ )
51
+ if (params.format !in VALID_FORMATS)
52
+ return ValidationResult.Invalid(
53
+ "E_INVALID_FORMAT",
54
+ "format must be jpeg, png, or webp, got: ${params.format}"
55
+ )
56
+ if (params.outputPath.isNotEmpty()) {
57
+ val dir = File(params.outputPath)
58
+ if (!dir.exists())
59
+ return ValidationResult.Invalid(
60
+ "E_INVALID_OUTPUT_PATH",
61
+ "Output directory does not exist: ${params.outputPath}"
62
+ )
63
+ if (!dir.isDirectory)
64
+ return ValidationResult.Invalid(
65
+ "E_INVALID_OUTPUT_PATH",
66
+ "outputPath must be a directory, not a file: ${params.outputPath}"
67
+ )
68
+ }
69
+ return ValidationResult.Valid
70
+ }
71
+ }
@@ -0,0 +1,181 @@
1
+ package com.libyuvresizer
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertTrue
5
+ import org.junit.Test
6
+
7
+ class DimensionCalculatorTest {
8
+
9
+ // --- calculateInSampleSize ---
10
+
11
+ @Test
12
+ fun `returns 1 when src smaller than dst`() {
13
+ assertEquals(1, DimensionCalculator.calculateInSampleSize(100, 100, 200, 200))
14
+ }
15
+
16
+ @Test
17
+ fun `returns 1 for equal dimensions`() {
18
+ assertEquals(1, DimensionCalculator.calculateInSampleSize(200, 200, 200, 200))
19
+ }
20
+
21
+ @Test
22
+ fun `returns 2 when src is double dst`() {
23
+ assertEquals(2, DimensionCalculator.calculateInSampleSize(400, 400, 200, 200))
24
+ }
25
+
26
+ @Test
27
+ fun `returns 4 when src is 4x dst`() {
28
+ assertEquals(4, DimensionCalculator.calculateInSampleSize(800, 800, 200, 200))
29
+ }
30
+
31
+ @Test
32
+ fun `returns 1 when only one axis exceeds but loop condition requires both`() {
33
+ // srcW=400 > dstW=200, but srcH=100 < dstH=200 — loop exits at sampleSize=1
34
+ assertEquals(1, DimensionCalculator.calculateInSampleSize(400, 100, 200, 200))
35
+ }
36
+
37
+ @Test
38
+ fun `returns correct sampleSize for odd source dimensions`() {
39
+ // srcW=401 srcH=401: halfW=halfH=200 — same as 400x400 case
40
+ assertEquals(2, DimensionCalculator.calculateInSampleSize(401, 401, 200, 200))
41
+ }
42
+
43
+ // --- scaleBy ---
44
+
45
+ @Test
46
+ fun `scaleBy 2x doubles dimensions`() {
47
+ assertEquals(Pair(200, 100), DimensionCalculator.scaleBy(2.0, 100.0, 50.0))
48
+ }
49
+
50
+ @Test
51
+ fun `scaleBy 0_5 halves dimensions`() {
52
+ assertEquals(Pair(50, 25), DimensionCalculator.scaleBy(0.5, 100.0, 50.0))
53
+ }
54
+
55
+ @Test
56
+ fun `scaleBy minimum output is 1`() {
57
+ assertEquals(Pair(1, 1), DimensionCalculator.scaleBy(0.0001, 1.0, 1.0))
58
+ }
59
+
60
+ @Test
61
+ fun `scaleBy rounds correctly`() {
62
+ // 100 * 0.056 = 5.6 -> 6
63
+ assertEquals(Pair(6, 6), DimensionCalculator.scaleBy(0.056, 100.0, 100.0))
64
+ }
65
+
66
+ // --- computeDstDims: stretch ---
67
+
68
+ @Test
69
+ fun `stretch returns exact target dims`() {
70
+ assertEquals(Pair(100, 50), DimensionCalculator.computeDstDims(1920.0, 1080.0, 100, 50, 0, "stretch"))
71
+ }
72
+
73
+ @Test
74
+ fun `stretch ignores src dimensions`() {
75
+ assertEquals(Pair(300, 200), DimensionCalculator.computeDstDims(10.0, 10.0, 300, 200, 0, "stretch"))
76
+ }
77
+
78
+ // --- computeDstDims: contain ---
79
+
80
+ @Test
81
+ fun `contain 16x9 src in square box constrains width`() {
82
+ // 1920x1080 contain in 100x100: scale = min(100/1920, 100/1080) = 0.052... (width-limited)
83
+ val (w, h) = DimensionCalculator.computeDstDims(1920.0, 1080.0, 100, 100, 0, "contain")
84
+ assertEquals(100, w)
85
+ assertEquals(56, h) // 1080 * (100/1920) ≈ 56.25 -> 56
86
+ }
87
+
88
+ @Test
89
+ fun `contain portrait src in landscape box constrains height`() {
90
+ // 100x200 contain in 200x100: scale = min(200/100, 100/200) = 0.5 (height-limited)
91
+ val (w, h) = DimensionCalculator.computeDstDims(100.0, 200.0, 200, 100, 0, "contain")
92
+ assertEquals(50, w) // 100 * 0.5
93
+ assertEquals(100, h) // 200 * 0.5
94
+ }
95
+
96
+ @Test
97
+ fun `contain output never exceeds target on either axis`() {
98
+ val (w, h) = DimensionCalculator.computeDstDims(1920.0, 1080.0, 100, 100, 0, "contain")
99
+ assertTrue("w=$w exceeds target", w <= 100)
100
+ assertTrue("h=$h exceeds target", h <= 100)
101
+ }
102
+
103
+ @Test
104
+ fun `contain never exceeds target for small integer dimensions`() {
105
+ // exhaustive check over small src and target combos — previously broken with roundToInt
106
+ for (sw in 1..20) for (sh in 1..20) for (tw in 1..20) for (th in 1..20) {
107
+ val (w, h) = DimensionCalculator.computeDstDims(sw.toDouble(), sh.toDouble(), tw, th, 0, "contain")
108
+ assertTrue("src=${sw}x${sh} target=${tw}x${th} → w=$w > tw=$tw", w <= tw)
109
+ assertTrue("src=${sw}x${sh} target=${tw}x${th} → h=$h > th=$th", h <= th)
110
+ }
111
+ }
112
+
113
+ // --- computeDstDims: cover ---
114
+
115
+ @Test
116
+ fun `cover 16x9 src in square box fills on both axes`() {
117
+ // 1920x1080 cover 100x100: scale = max(100/1920, 100/1080) = 0.0926... (height-driven)
118
+ val (w, h) = DimensionCalculator.computeDstDims(1920.0, 1080.0, 100, 100, 0, "cover")
119
+ assertEquals(100, h)
120
+ assertTrue("w=$w should exceed or equal target", w >= 100)
121
+ }
122
+
123
+ @Test
124
+ fun `cover output meets target on at least one axis`() {
125
+ val (w, h) = DimensionCalculator.computeDstDims(1920.0, 1080.0, 100, 100, 0, "cover")
126
+ assertTrue("neither axis meets target: w=$w h=$h", w >= 100 || h >= 100)
127
+ }
128
+
129
+ @Test
130
+ fun `cover 16x9 src in square box exact output width`() {
131
+ // scale = max(100/1920, 100/1080) = 100/1080 = 0.09259...
132
+ // w = round(1920 * 0.09259...) = round(177.77...) = 178
133
+ // h = round(1080 * 0.09259...) = round(100.0) = 100
134
+ val (w, h) = DimensionCalculator.computeDstDims(1920.0, 1080.0, 100, 100, 0, "cover")
135
+ assertEquals(178, w)
136
+ assertEquals(100, h)
137
+ }
138
+
139
+ // --- computeDstDims: rotation ---
140
+
141
+ @Test
142
+ fun `rotation 90 swaps effective dims for contain`() {
143
+ // src 100x200, rotation=90 → effectiveW=200, effectiveH=100
144
+ // contain in 100x100: scale = min(100/200, 100/100) = 0.5
145
+ // result: scaleBy(0.5, 200, 100) = (100, 50)
146
+ val (w, h) = DimensionCalculator.computeDstDims(100.0, 200.0, 100, 100, 90, "contain")
147
+ assertEquals(100, w)
148
+ assertEquals(50, h)
149
+ }
150
+
151
+ @Test
152
+ fun `rotation 270 swaps effective dims same as 90`() {
153
+ val result90 = DimensionCalculator.computeDstDims(100.0, 200.0, 100, 100, 90, "contain")
154
+ val result270 = DimensionCalculator.computeDstDims(100.0, 200.0, 100, 100, 270, "contain")
155
+ assertEquals(result90, result270)
156
+ }
157
+
158
+ @Test
159
+ fun `rotation 0 does not swap dims for contain`() {
160
+ // src 100x200, rotation=0 → effectiveW=100, effectiveH=200
161
+ // contain in 100x100: scale = min(100/100, 100/200) = 0.5
162
+ // result: scaleBy(0.5, 100, 200) = (50, 100)
163
+ val (w, h) = DimensionCalculator.computeDstDims(100.0, 200.0, 100, 100, 0, "contain")
164
+ assertEquals(50, w)
165
+ assertEquals(100, h)
166
+ }
167
+
168
+ @Test
169
+ fun `rotation 180 does not swap dims`() {
170
+ val result0 = DimensionCalculator.computeDstDims(100.0, 200.0, 100, 100, 0, "contain")
171
+ val result180 = DimensionCalculator.computeDstDims(100.0, 200.0, 100, 100, 180, "contain")
172
+ assertEquals(result0, result180)
173
+ }
174
+
175
+ @Test
176
+ fun `stretch ignores rotation`() {
177
+ val result0 = DimensionCalculator.computeDstDims(1920.0, 1080.0, 100, 50, 0, "stretch")
178
+ val result90 = DimensionCalculator.computeDstDims(1920.0, 1080.0, 100, 50, 90, "stretch")
179
+ assertEquals(result0, result90)
180
+ }
181
+ }
@@ -0,0 +1,203 @@
1
+ package com.libyuvresizer
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertTrue
5
+ import org.junit.Rule
6
+ import org.junit.Test
7
+ import org.junit.rules.TemporaryFolder
8
+
9
+ class ResizeValidatorTest {
10
+
11
+ @get:Rule
12
+ val tmp = TemporaryFolder()
13
+
14
+ private fun validParams(filePath: String, outputPath: String = "") = ResizeParams(
15
+ filePath = filePath,
16
+ targetWidth = 100,
17
+ targetHeight = 100,
18
+ quality = 80,
19
+ rotation = 0,
20
+ mode = "contain",
21
+ outputPath = outputPath,
22
+ filterMode = "bilinear"
23
+ )
24
+
25
+ // --- happy path ---
26
+
27
+ @Test
28
+ fun `valid params with empty outputPath returns Valid`() {
29
+ val file = tmp.newFile("image.jpg")
30
+ assertEquals(ValidationResult.Valid, ResizeValidator.validate(validParams(file.absolutePath)))
31
+ }
32
+
33
+ @Test
34
+ fun `valid params with directory outputPath returns Valid`() {
35
+ val file = tmp.newFile("image.jpg")
36
+ val dir = tmp.newFolder("out")
37
+ assertEquals(ValidationResult.Valid, ResizeValidator.validate(validParams(file.absolutePath, dir.absolutePath)))
38
+ }
39
+
40
+ @Test
41
+ fun `all valid rotations accepted`() {
42
+ val file = tmp.newFile("image.jpg")
43
+ for (rot in listOf(0, 90, 180, 270)) {
44
+ val result = ResizeValidator.validate(validParams(file.absolutePath).copy(rotation = rot))
45
+ assertEquals("rotation $rot should be Valid", ValidationResult.Valid, result)
46
+ }
47
+ }
48
+
49
+ @Test
50
+ fun `all valid modes accepted`() {
51
+ val file = tmp.newFile("image.jpg")
52
+ for (mode in listOf("contain", "cover", "stretch")) {
53
+ val result = ResizeValidator.validate(validParams(file.absolutePath).copy(mode = mode))
54
+ assertEquals("mode $mode should be Valid", ValidationResult.Valid, result)
55
+ }
56
+ }
57
+
58
+ @Test
59
+ fun `all valid filterModes accepted`() {
60
+ val file = tmp.newFile("image.jpg")
61
+ for (fm in listOf("none", "linear", "bilinear", "box")) {
62
+ val result = ResizeValidator.validate(validParams(file.absolutePath).copy(filterMode = fm))
63
+ assertEquals("filterMode $fm should be Valid", ValidationResult.Valid, result)
64
+ }
65
+ }
66
+
67
+ @Test
68
+ fun `all valid formats accepted`() {
69
+ val file = tmp.newFile("image.jpg")
70
+ for (fmt in listOf("jpeg", "png", "webp")) {
71
+ val result = ResizeValidator.validate(validParams(file.absolutePath).copy(format = fmt))
72
+ assertEquals("format $fmt should be Valid", ValidationResult.Valid, result)
73
+ }
74
+ }
75
+
76
+ @Test
77
+ fun `quality boundary 1 accepted`() {
78
+ val file = tmp.newFile("image.jpg")
79
+ assertEquals(ValidationResult.Valid, ResizeValidator.validate(validParams(file.absolutePath).copy(quality = 1)))
80
+ }
81
+
82
+ @Test
83
+ fun `quality boundary 100 accepted`() {
84
+ val file = tmp.newFile("image.jpg")
85
+ assertEquals(ValidationResult.Valid, ResizeValidator.validate(validParams(file.absolutePath).copy(quality = 100)))
86
+ }
87
+
88
+ // --- error codes ---
89
+
90
+ private fun assertCode(expected: String, params: ResizeParams) {
91
+ val result = ResizeValidator.validate(params)
92
+ assertTrue("expected Invalid, got $result", result is ValidationResult.Invalid)
93
+ assertEquals(expected, (result as ValidationResult.Invalid).code)
94
+ }
95
+
96
+ @Test
97
+ fun `missing file returns E_FILE_NOT_FOUND`() {
98
+ assertCode("E_FILE_NOT_FOUND", validParams("/no/such/file.jpg"))
99
+ }
100
+
101
+ @Test
102
+ fun `zero width returns E_INVALID_DIMS`() {
103
+ val file = tmp.newFile("image.jpg")
104
+ assertCode("E_INVALID_DIMS", validParams(file.absolutePath).copy(targetWidth = 0))
105
+ }
106
+
107
+ @Test
108
+ fun `negative height returns E_INVALID_DIMS`() {
109
+ val file = tmp.newFile("image.jpg")
110
+ assertCode("E_INVALID_DIMS", validParams(file.absolutePath).copy(targetHeight = -1))
111
+ }
112
+
113
+ @Test
114
+ fun `quality 0 returns E_INVALID_QUALITY`() {
115
+ val file = tmp.newFile("image.jpg")
116
+ assertCode("E_INVALID_QUALITY", validParams(file.absolutePath).copy(quality = 0))
117
+ }
118
+
119
+ @Test
120
+ fun `quality 101 returns E_INVALID_QUALITY`() {
121
+ val file = tmp.newFile("image.jpg")
122
+ assertCode("E_INVALID_QUALITY", validParams(file.absolutePath).copy(quality = 101))
123
+ }
124
+
125
+ @Test
126
+ fun `rotation 45 returns E_INVALID_ROTATION`() {
127
+ val file = tmp.newFile("image.jpg")
128
+ assertCode("E_INVALID_ROTATION", validParams(file.absolutePath).copy(rotation = 45))
129
+ }
130
+
131
+ @Test
132
+ fun `invalid mode returns E_INVALID_MODE`() {
133
+ val file = tmp.newFile("image.jpg")
134
+ assertCode("E_INVALID_MODE", validParams(file.absolutePath).copy(mode = "fill"))
135
+ }
136
+
137
+ @Test
138
+ fun `invalid filterMode returns E_INVALID_FILTER_MODE`() {
139
+ val file = tmp.newFile("image.jpg")
140
+ assertCode("E_INVALID_FILTER_MODE", validParams(file.absolutePath).copy(filterMode = "cubic"))
141
+ }
142
+
143
+ @Test
144
+ fun `invalid format returns E_INVALID_FORMAT`() {
145
+ val file = tmp.newFile("image.jpg")
146
+ assertCode("E_INVALID_FORMAT", validParams(file.absolutePath).copy(format = "gif"))
147
+ }
148
+
149
+ @Test
150
+ fun `empty format returns E_INVALID_FORMAT`() {
151
+ val file = tmp.newFile("image.jpg")
152
+ assertCode("E_INVALID_FORMAT", validParams(file.absolutePath).copy(format = ""))
153
+ }
154
+
155
+ @Test
156
+ fun `nonexistent outputPath returns E_INVALID_OUTPUT_PATH`() {
157
+ val file = tmp.newFile("image.jpg")
158
+ assertCode("E_INVALID_OUTPUT_PATH", validParams(file.absolutePath, "/nonexistent/dir"))
159
+ }
160
+
161
+ @Test
162
+ fun `outputPath pointing to file returns E_INVALID_OUTPUT_PATH`() {
163
+ val file = tmp.newFile("image.jpg")
164
+ val notDir = tmp.newFile("output.jpg")
165
+ assertCode("E_INVALID_OUTPUT_PATH", validParams(file.absolutePath, notDir.absolutePath))
166
+ }
167
+
168
+ // --- error message content ---
169
+
170
+ @Test
171
+ fun `E_FILE_NOT_FOUND message includes path`() {
172
+ val result = ResizeValidator.validate(validParams("/bad/path.jpg")) as ValidationResult.Invalid
173
+ assertTrue(result.message.contains("/bad/path.jpg"))
174
+ }
175
+
176
+ @Test
177
+ fun `E_INVALID_ROTATION message includes got value`() {
178
+ val file = tmp.newFile("image.jpg")
179
+ val result = ResizeValidator.validate(validParams(file.absolutePath).copy(rotation = 45)) as ValidationResult.Invalid
180
+ assertTrue(result.message.contains("45"))
181
+ }
182
+
183
+ @Test
184
+ fun `E_INVALID_MODE message includes got value`() {
185
+ val file = tmp.newFile("image.jpg")
186
+ val result = ResizeValidator.validate(validParams(file.absolutePath).copy(mode = "fill")) as ValidationResult.Invalid
187
+ assertTrue(result.message.contains("fill"))
188
+ }
189
+
190
+ @Test
191
+ fun `E_INVALID_FILTER_MODE message includes got value`() {
192
+ val file = tmp.newFile("image.jpg")
193
+ val result = ResizeValidator.validate(validParams(file.absolutePath).copy(filterMode = "cubic")) as ValidationResult.Invalid
194
+ assertTrue(result.message.contains("cubic"))
195
+ }
196
+
197
+ @Test
198
+ fun `E_INVALID_FORMAT message includes got value`() {
199
+ val file = tmp.newFile("image.jpg")
200
+ val result = ResizeValidator.validate(validParams(file.absolutePath).copy(format = "gif")) as ValidationResult.Invalid
201
+ assertTrue(result.message.contains("gif"))
202
+ }
203
+ }
@@ -0,0 +1,6 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <LibyuvResizerSpec/LibyuvResizerSpec.h>
3
+
4
+ @interface LibyuvResizer : NSObject <NativeLibyuvResizerSpec, RCTBridgeModule>
5
+
6
+ @end