1
1
package com.kik.kikx.kikcodes.implementation
2
2
3
- import android.R.attr.bitmap
4
3
import android.content.Context
5
4
import android.graphics.Bitmap
5
+ import android.graphics.Rect
6
6
import android.net.Uri
7
+ import android.os.Environment
7
8
import androidx.camera.core.ImageAnalysis
8
9
import androidx.camera.core.ImageProxy
9
10
import androidx.compose.runtime.Composable
10
11
import androidx.compose.runtime.remember
12
+ import com.getcode.media.MediaScanner
13
+ import com.getcode.util.save
11
14
import com.getcode.util.toByteArray
12
15
import com.getcode.util.uriToBitmap
13
16
import com.getcode.utils.ErrorUtils
@@ -17,7 +20,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext
17
20
import kotlinx.coroutines.CoroutineScope
18
21
import kotlinx.coroutines.Dispatchers
19
22
import kotlinx.coroutines.launch
20
- import java.nio.ByteBuffer
23
+ import kotlinx.coroutines.withContext
24
+ import java.io.File
25
+ import java.text.DateFormat
26
+ import java.text.SimpleDateFormat
27
+ import java.util.Date
28
+ import java.util.Locale
21
29
import javax.inject.Inject
22
30
23
31
@@ -43,6 +51,9 @@ class KikCodeAnalyzer @Inject constructor(
43
51
var onCodeScanned: (ScannableKikCode ) -> Unit = { }
44
52
var onNoCodeFound: () -> Unit = { }
45
53
54
+ @Inject
55
+ internal lateinit var mediaScanner: MediaScanner
56
+
46
57
override fun analyze (imageProxy : ImageProxy ) {
47
58
launch {
48
59
scanner.scanKikCode(
@@ -67,22 +78,152 @@ class KikCodeAnalyzer @Inject constructor(
67
78
launch {
68
79
val bitmap = context.uriToBitmap(uri)
69
80
if (bitmap != null ) {
70
- scanner.scanKikCode(
71
- bitmap.toByteArray(),
72
- bitmap.width,
73
- bitmap.height,
74
- ).onSuccess { result ->
81
+ detectCodeInImage(bitmap) {
82
+ scanner.scanKikCode(
83
+ it.toByteArray(),
84
+ it.width,
85
+ it.height,
86
+ )
87
+ }.onSuccess { result ->
75
88
onCodeScanned(result)
76
- bitmap.recycle()
77
89
78
90
}.onFailure { error ->
79
91
when (error) {
80
92
is KikCodeScanner .NoKikCodeFoundException -> onNoCodeFound()
81
93
else -> ErrorUtils .handleError(error)
82
94
}
83
- bitmap.recycle()
84
95
}
85
96
}
86
97
}
87
98
}
88
- }
99
+
100
+ private suspend fun detectCodeInImage (
101
+ bitmap : Bitmap ,
102
+ minSectionSize : Int = 100,
103
+ scan : suspend (Bitmap ) -> Result <ScannableKikCode >
104
+ ): Result <ScannableKikCode > = withContext(Dispatchers .Default ) {
105
+ val destinationRoot =
106
+ Environment .getExternalStoragePublicDirectory(Environment .DIRECTORY_PICTURES )
107
+ val date: DateFormat = SimpleDateFormat (" yyyy-MM-dd-H-mm" , Locale .CANADA )
108
+ val destination = File (destinationRoot, date.format(Date ()))
109
+ if (! destination.exists()) {
110
+ destination.mkdirs()
111
+ }
112
+
113
+ // Start the recursive division and scanning process
114
+ return @withContext divideAndScan(bitmap, destination, minSectionSize, scan)
115
+ }
116
+
117
+ private suspend fun divideAndScan (
118
+ bitmap : Bitmap ,
119
+ destination : File ,
120
+ minSectionSize : Int ,
121
+ scan : suspend (Bitmap ) -> Result <ScannableKikCode >,
122
+ ): Result <ScannableKikCode > {
123
+ val zoomLevels = listOf (1.0 , 2.0 , 5.0 , 10.0 )
124
+
125
+ return processBitmapRecursively(bitmap, destination, minSectionSize, scan, zoomLevels)
126
+ }
127
+
128
+ private suspend fun processBitmapRecursively (
129
+ bitmap : Bitmap ,
130
+ destination : File ,
131
+ minSectionSize : Int ,
132
+ scan : suspend (Bitmap ) -> Result <ScannableKikCode >,
133
+ zoomLevels : List <Double >
134
+ ): Result <ScannableKikCode > {
135
+ val width = bitmap.width
136
+ val height = bitmap.height
137
+
138
+ // Base case: If the bitmap is smaller than the minimum section size, process it directly
139
+ if (width <= minSectionSize || height <= minSectionSize) {
140
+ return scanWithZoomLevels(bitmap, destination, scan, zoomLevels)
141
+ }
142
+
143
+ // Scan the center section first
144
+ val centerRect = calculateCenterRect(width, height)
145
+ val centerBitmap = Bitmap .createBitmap(bitmap, centerRect.left, centerRect.top, centerRect.width(), centerRect.height())
146
+
147
+ val centerResult = scanWithZoomLevels(centerBitmap, destination, scan, zoomLevels)
148
+ centerBitmap.recycle()
149
+
150
+ if (centerResult.isSuccess) {
151
+ return centerResult
152
+ }
153
+
154
+ // Divide the bitmap into left and right halves and process recursively
155
+ val leftHalf = Bitmap .createBitmap(bitmap, 0 , 0 , width / 2 , height)
156
+ val rightHalf = Bitmap .createBitmap(bitmap, width / 2 , 0 , width / 2 , height)
157
+
158
+ val leftResult = processBitmapRecursively(leftHalf, destination, minSectionSize, scan, zoomLevels)
159
+ leftHalf.recycle()
160
+
161
+ if (leftResult.isSuccess) {
162
+ rightHalf.recycle()
163
+ return leftResult
164
+ }
165
+
166
+ val rightResult = processBitmapRecursively(rightHalf, destination, minSectionSize, scan, zoomLevels)
167
+ rightHalf.recycle()
168
+
169
+ return rightResult
170
+ }
171
+
172
+ private suspend fun scanWithZoomLevels (
173
+ bitmap : Bitmap ,
174
+ destination : File ,
175
+ scan : suspend (Bitmap ) -> Result <ScannableKikCode >,
176
+ zoomLevels : List <Double >
177
+ ): Result <ScannableKikCode > {
178
+ for (zoomLevel in zoomLevels) {
179
+ val zoomedBitmap = zoomBitmap(bitmap, zoomLevel)
180
+ saveSegment(zoomedBitmap, destination) {
181
+ " section_${zoomedBitmap.width} x${zoomedBitmap.height} _zoom${zoomLevel} .png"
182
+ }
183
+ val result = scan(zoomedBitmap)
184
+
185
+ zoomedBitmap.recycle()
186
+
187
+ if (result.isSuccess) {
188
+ return result
189
+ }
190
+ }
191
+
192
+ return Result .failure(Exception (" No successful scan" ))
193
+ }
194
+
195
+ private fun saveSegment (bitmap : Bitmap , destination : File , name : () -> String ) {
196
+ if (DEBUG ) {
197
+ bitmap.save(destination, name)
198
+ }
199
+ }
200
+
201
+ private fun zoomBitmap (bitmap : Bitmap , zoomLevel : Double ): Bitmap {
202
+ // If zoomLevel is 1.0, just return a copy of the original bitmap (to prevent recycling issues)
203
+ if (zoomLevel == 1.0 ) return Bitmap .createBitmap(bitmap)
204
+
205
+ val cropWidth = (bitmap.width / zoomLevel).toInt()
206
+ val cropHeight = (bitmap.height / zoomLevel).toInt()
207
+ val xOffset = (bitmap.width - cropWidth) / 2
208
+ val yOffset = (bitmap.height - cropHeight) / 2
209
+
210
+ val croppedBitmap = Bitmap .createBitmap(bitmap, xOffset, yOffset, cropWidth, cropHeight)
211
+ val scaledBitmap = Bitmap .createScaledBitmap(croppedBitmap, bitmap.width, bitmap.height, true )
212
+
213
+ croppedBitmap.recycle()
214
+ return scaledBitmap
215
+ }
216
+
217
+ private fun calculateCenterRect (width : Int , height : Int ): Rect {
218
+ val centerWidth = width / 2
219
+ val centerHeight = height / 2
220
+ return Rect (
221
+ centerWidth / 2 ,
222
+ centerHeight / 2 ,
223
+ centerWidth + centerWidth / 2 ,
224
+ centerHeight + centerHeight / 2
225
+ )
226
+ }
227
+ }
228
+
229
+ private const val DEBUG = false
0 commit comments