Skip to content

Commit 89381b8

Browse files
committed
Added ability to scan barcode directly from image without opening camera
1 parent eca6abf commit 89381b8

File tree

11 files changed

+320
-54
lines changed

11 files changed

+320
-54
lines changed

core/resources/src/main/res/values/strings.xml

+1
Original file line numberDiff line numberDiff line change
@@ -1600,4 +1600,5 @@
16001600
<string name="enforce_bw">Enforce B/W</string>
16011601
<string name="enforce_bw_sub">Barcode Image will be fully black and white and not colored by app\'s theme</string>
16021602
<string name="barcodes_sub">Scan any Barcode (QR, EAN, AZTEC, …) and get it\'s content or paste your text to generate new one</string>
1603+
<string name="no_barcode_found">No Barcode Found</string>
16031604
</resources>

core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/Screen.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,8 @@ sealed class Screen(
578578

579579
@Serializable
580580
data class ScanQrCode(
581-
val qrCodeContent: String? = null
581+
val qrCodeContent: String? = null,
582+
val uriToAnalyze: Uri? = null
582583
) : Screen(
583584
id = 27,
584585
title = R.string.qr_code,

core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/buttons/BottomButtonsBlock.kt

+65-49
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ fun BottomButtonsBlock(
8787
showNullDataButtonAsContainer: Boolean = false,
8888
columnarFab: (@Composable ColumnScope.() -> Unit)? = null,
8989
actions: @Composable RowScope.() -> Unit,
90-
isPrimaryButtonEnabled: Boolean = true
90+
isPrimaryButtonEnabled: Boolean = true,
91+
showColumnarFabInRow: Boolean = false,
9192
) {
9293
AnimatedContent(
9394
targetState = targetState,
@@ -97,9 +98,7 @@ fun BottomButtonsBlock(
9798
) { (isNull, inside) ->
9899
if (isNull) {
99100
val button = @Composable {
100-
EnhancedFloatingActionButton(
101-
onClick = onSecondaryButtonClick,
102-
onLongClick = onSecondaryButtonLongClick,
101+
Row(
103102
modifier = Modifier
104103
.windowInsetsPadding(
105104
WindowInsets.navigationBars.union(
@@ -109,14 +108,24 @@ fun BottomButtonsBlock(
109108
)
110109
)
111110
.padding(16.dp),
112-
content = {
113-
Spacer(Modifier.width(16.dp))
114-
Icon(secondaryButtonIcon, null)
115-
Spacer(Modifier.width(16.dp))
116-
Text(secondaryButtonText)
117-
Spacer(Modifier.width(16.dp))
111+
horizontalArrangement = Arrangement.spacedBy(8.dp),
112+
verticalAlignment = Alignment.CenterVertically
113+
) {
114+
EnhancedFloatingActionButton(
115+
onClick = onSecondaryButtonClick,
116+
onLongClick = onSecondaryButtonLongClick,
117+
content = {
118+
Spacer(Modifier.width(16.dp))
119+
Icon(secondaryButtonIcon, null)
120+
Spacer(Modifier.width(16.dp))
121+
Text(secondaryButtonText)
122+
Spacer(Modifier.width(16.dp))
123+
}
124+
)
125+
if (showColumnarFabInRow && columnarFab != null) {
126+
Column { columnarFab() }
118127
}
119-
)
128+
}
120129
}
121130
if (showNullDataButtonAsContainer) {
122131
Row(
@@ -136,7 +145,9 @@ fun BottomButtonsBlock(
136145
modifier = Modifier.drawHorizontalStroke(true),
137146
actions = actions,
138147
floatingActionButton = {
139-
Row {
148+
Row(
149+
horizontalArrangement = Arrangement.spacedBy(8.dp)
150+
) {
140151
AnimatedVisibility(visible = isSecondaryButtonVisible) {
141152
EnhancedFloatingActionButton(
142153
onClick = onSecondaryButtonClick,
@@ -152,43 +163,45 @@ fun BottomButtonsBlock(
152163
)
153164
}
154165
}
166+
AnimatedVisibility(visible = showColumnarFabInRow) {
167+
columnarFab?.let {
168+
Column { it() }
169+
}
170+
}
155171
AnimatedVisibility(visible = isPrimaryButtonVisible) {
156-
Row {
157-
Spacer(Modifier.width(8.dp))
158-
EnhancedFloatingActionButton(
159-
onClick = if (isPrimaryButtonEnabled) onPrimaryButtonClick
160-
else null,
161-
onLongClick = if (isPrimaryButtonEnabled) onPrimaryButtonLongClick
162-
else null,
163-
containerColor = takeColorFromScheme {
164-
if (isPrimaryButtonEnabled) primaryContainer
165-
else surfaceContainerHighest
166-
},
167-
contentColor = takeColorFromScheme {
168-
if (isPrimaryButtonEnabled) onPrimaryContainer
169-
else outline
170-
}
171-
) {
172-
AnimatedContent(
173-
targetState = primaryButtonIcon to primaryButtonText,
174-
transitionSpec = { fadeIn() + scaleIn() togetherWith fadeOut() + scaleOut() }
175-
) { (icon, text) ->
176-
Row(
177-
verticalAlignment = Alignment.CenterVertically,
178-
horizontalArrangement = Arrangement.Center
179-
) {
180-
if (text.isNotEmpty()) {
181-
Spacer(Modifier.width(16.dp))
182-
}
183-
Icon(
184-
imageVector = icon,
185-
contentDescription = null
186-
)
187-
if (text.isNotEmpty()) {
188-
Spacer(Modifier.width(16.dp))
189-
Text(text)
190-
Spacer(Modifier.width(16.dp))
191-
}
172+
EnhancedFloatingActionButton(
173+
onClick = if (isPrimaryButtonEnabled) onPrimaryButtonClick
174+
else null,
175+
onLongClick = if (isPrimaryButtonEnabled) onPrimaryButtonLongClick
176+
else null,
177+
containerColor = takeColorFromScheme {
178+
if (isPrimaryButtonEnabled) primaryContainer
179+
else surfaceContainerHighest
180+
},
181+
contentColor = takeColorFromScheme {
182+
if (isPrimaryButtonEnabled) onPrimaryContainer
183+
else outline
184+
}
185+
) {
186+
AnimatedContent(
187+
targetState = primaryButtonIcon to primaryButtonText,
188+
transitionSpec = { fadeIn() + scaleIn() togetherWith fadeOut() + scaleOut() }
189+
) { (icon, text) ->
190+
Row(
191+
verticalAlignment = Alignment.CenterVertically,
192+
horizontalArrangement = Arrangement.Center
193+
) {
194+
if (text.isNotEmpty()) {
195+
Spacer(Modifier.width(16.dp))
196+
}
197+
Icon(
198+
imageVector = icon,
199+
contentDescription = null
200+
)
201+
if (text.isNotEmpty()) {
202+
Spacer(Modifier.width(16.dp))
203+
Text(text)
204+
Spacer(Modifier.width(16.dp))
192205
}
193206
}
194207
}
@@ -223,7 +236,10 @@ fun BottomButtonsBlock(
223236
EnhancedFloatingActionButton(
224237
onClick = onSecondaryButtonClick,
225238
onLongClick = onSecondaryButtonLongClick,
226-
containerColor = MaterialTheme.colorScheme.tertiaryContainer
239+
containerColor = takeColorFromScheme {
240+
if (isPrimaryButtonVisible) tertiaryContainer
241+
else primaryContainer
242+
}
227243
) {
228244
Icon(
229245
imageVector = secondaryButtonIcon,

core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/utils/ScreenList.kt

+1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ internal fun List<Uri>.screenList(
121121
Screen.ImageStacking(uris),
122122
Screen.ImageSplitting(uris.firstOrNull()),
123123
Screen.ImageCutter(uris),
124+
Screen.ScanQrCode(uriToAnalyze = uris.firstOrNull()),
124125
Screen.GradientMaker(uris),
125126
Screen.PdfTools(
126127
Screen.PdfTools.Type.ImagesToPdf(uris)

feature/root/src/main/java/ru/tech/imageresizershrinker/feature/root/presentation/components/navigation/ChildProvider.kt

+45-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,50 @@ import ru.tech.imageresizershrinker.feature.pdf_tools.presentation.screenLogic.P
5252
import ru.tech.imageresizershrinker.feature.pick_color.presentation.screenLogic.PickColorFromImageComponent
5353
import ru.tech.imageresizershrinker.feature.recognize.text.presentation.screenLogic.RecognizeTextComponent
5454
import ru.tech.imageresizershrinker.feature.resize_convert.presentation.screenLogic.ResizeAndConvertComponent
55-
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.*
55+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ApngTools
56+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Base64Tools
57+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ChecksumTools
58+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Cipher
59+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.CollageMaker
60+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ColorTools
61+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Compare
62+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Crop
63+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.DeleteExif
64+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.DocumentScanner
65+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Draw
66+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.EasterEgg
67+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.EditExif
68+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.EraseBackground
69+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Filter
70+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.FormatConversion
71+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.GeneratePalette
72+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.GifTools
73+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.GradientMaker
74+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ImageCutter
75+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ImagePreview
76+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ImageSplitting
77+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ImageStacking
78+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ImageStitching
79+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.JxlTools
80+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.LibrariesInfo
81+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.LimitResize
82+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.LoadNetImage
83+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Main
84+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.MarkupLayers
85+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.MeshGradients
86+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.NoiseGeneration
87+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.PdfTools
88+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.PickColorFromImage
89+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.RecognizeText
90+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ResizeAndConvert
91+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ScanQrCode
92+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Settings
93+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.SingleEdit
94+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.SvgMaker
95+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Watermarking
96+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.WebpTools
97+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.WeightResize
98+
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Zip
5699
import ru.tech.imageresizershrinker.feature.root.presentation.screenLogic.RootComponent
57100
import ru.tech.imageresizershrinker.feature.scan_qr_code.presentation.screenLogic.ScanQrCodeComponent
58101
import ru.tech.imageresizershrinker.feature.settings.presentation.screenLogic.SettingsComponent
@@ -372,6 +415,7 @@ internal class ChildProvider @Inject constructor(
372415
scanQrCodeComponentFactory(
373416
componentContext = componentContext,
374417
initialQrCodeContent = config.qrCodeContent,
418+
uriToAnalyze = config.uriToAnalyze,
375419
onGoBack = ::navigateBack
376420
)
377421
)

feature/scan-qr-code/build.gradle.kts

+2
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,6 @@ android.namespace = "ru.tech.imageresizershrinker.feature.scan_qr_code"
2626

2727
dependencies {
2828
implementation(projects.core.filters)
29+
"marketImplementation"(libs.quickie.bundled)
30+
"fossImplementation"(libs.quickie.foss)
2931
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* ImageToolbox is an image editor for android
3+
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*
14+
* You should have received a copy of the Apache License
15+
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
16+
*/
17+
18+
package ru.tech.imageresizershrinker.feature.scan_qr_code.data
19+
20+
import android.graphics.Bitmap
21+
import androidx.exifinterface.media.ExifInterface
22+
import io.github.g00fy2.quickie.extensions.readQrCode
23+
import kotlinx.coroutines.suspendCancellableCoroutine
24+
import kotlinx.coroutines.withContext
25+
import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
26+
import ru.tech.imageresizershrinker.core.domain.image.ImageGetter
27+
import ru.tech.imageresizershrinker.core.domain.resource.ResourceManager
28+
import ru.tech.imageresizershrinker.core.resources.R
29+
import ru.tech.imageresizershrinker.feature.scan_qr_code.domain.ImageBarcodeReader
30+
import javax.inject.Inject
31+
import kotlin.coroutines.resume
32+
33+
internal class AndroidImageBarcodeReader @Inject constructor(
34+
private val imageGetter: ImageGetter<Bitmap, ExifInterface>,
35+
resourceManager: ResourceManager,
36+
dispatchersHolder: DispatchersHolder
37+
) : ImageBarcodeReader, DispatchersHolder by dispatchersHolder, ResourceManager by resourceManager {
38+
39+
override suspend fun readBarcode(
40+
image: Any
41+
): Result<String> = withContext(defaultDispatcher) {
42+
val bitmap = imageGetter.getImage(
43+
data = image,
44+
originalSize = false
45+
)
46+
47+
if (bitmap == null) {
48+
return@withContext Result.failure(NullPointerException(getString(R.string.something_went_wrong)))
49+
}
50+
51+
suspendCancellableCoroutine { continuation ->
52+
bitmap.readQrCode(
53+
barcodeFormats = IntArray(0),
54+
onSuccess = {
55+
continuation.resume(Result.success(it))
56+
},
57+
onFailure = {
58+
continuation.resume(Result.failure(it))
59+
}
60+
)
61+
}
62+
}
63+
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* ImageToolbox is an image editor for android
3+
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*
14+
* You should have received a copy of the Apache License
15+
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
16+
*/
17+
18+
package ru.tech.imageresizershrinker.feature.scan_qr_code.di
19+
20+
import dagger.Binds
21+
import dagger.Module
22+
import dagger.hilt.InstallIn
23+
import dagger.hilt.components.SingletonComponent
24+
import ru.tech.imageresizershrinker.feature.scan_qr_code.data.AndroidImageBarcodeReader
25+
import ru.tech.imageresizershrinker.feature.scan_qr_code.domain.ImageBarcodeReader
26+
import javax.inject.Singleton
27+
28+
29+
@Module
30+
@InstallIn(SingletonComponent::class)
31+
internal interface ScanQrCodeModule {
32+
33+
@Binds
34+
@Singleton
35+
fun reader(
36+
impl: AndroidImageBarcodeReader
37+
): ImageBarcodeReader
38+
39+
40+
}

0 commit comments

Comments
 (0)