11package com.kik.kikx.kikcodes.implementation
22
3- import android.R.attr.bitmap
43import android.content.Context
54import android.graphics.Bitmap
5+ import android.graphics.Rect
66import android.net.Uri
7+ import android.os.Environment
78import androidx.camera.core.ImageAnalysis
89import androidx.camera.core.ImageProxy
910import androidx.compose.runtime.Composable
1011import androidx.compose.runtime.remember
12+ import com.getcode.media.MediaScanner
13+ import com.getcode.util.save
1114import com.getcode.util.toByteArray
1215import com.getcode.util.uriToBitmap
1316import com.getcode.utils.ErrorUtils
@@ -17,7 +20,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext
1720import kotlinx.coroutines.CoroutineScope
1821import kotlinx.coroutines.Dispatchers
1922import 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
2129import javax.inject.Inject
2230
2331
@@ -43,6 +51,9 @@ class KikCodeAnalyzer @Inject constructor(
4351 var onCodeScanned: (ScannableKikCode ) -> Unit = { }
4452 var onNoCodeFound: () -> Unit = { }
4553
54+ @Inject
55+ internal lateinit var mediaScanner: MediaScanner
56+
4657 override fun analyze (imageProxy : ImageProxy ) {
4758 launch {
4859 scanner.scanKikCode(
@@ -67,22 +78,152 @@ class KikCodeAnalyzer @Inject constructor(
6778 launch {
6879 val bitmap = context.uriToBitmap(uri)
6980 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 ->
7588 onCodeScanned(result)
76- bitmap.recycle()
7789
7890 }.onFailure { error ->
7991 when (error) {
8092 is KikCodeScanner .NoKikCodeFoundException -> onNoCodeFound()
8193 else -> ErrorUtils .handleError(error)
8294 }
83- bitmap.recycle()
8495 }
8596 }
8697 }
8798 }
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