Added ability to scan barcode directly from image without opening camera

This commit is contained in:
T8RIN
2025-03-10 05:31:07 +03:00
parent eca6abf570
commit 89381b8a91
11 changed files with 320 additions and 54 deletions

View File

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

View File

@ -578,7 +578,8 @@ sealed class Screen(
@Serializable
data class ScanQrCode(
val qrCodeContent: String? = null
val qrCodeContent: String? = null,
val uriToAnalyze: Uri? = null
) : Screen(
id = 27,
title = R.string.qr_code,

View File

@ -87,7 +87,8 @@ fun BottomButtonsBlock(
showNullDataButtonAsContainer: Boolean = false,
columnarFab: (@Composable ColumnScope.() -> Unit)? = null,
actions: @Composable RowScope.() -> Unit,
isPrimaryButtonEnabled: Boolean = true
isPrimaryButtonEnabled: Boolean = true,
showColumnarFabInRow: Boolean = false,
) {
AnimatedContent(
targetState = targetState,
@ -97,9 +98,7 @@ fun BottomButtonsBlock(
) { (isNull, inside) ->
if (isNull) {
val button = @Composable {
EnhancedFloatingActionButton(
onClick = onSecondaryButtonClick,
onLongClick = onSecondaryButtonLongClick,
Row(
modifier = Modifier
.windowInsetsPadding(
WindowInsets.navigationBars.union(
@ -109,14 +108,24 @@ fun BottomButtonsBlock(
)
)
.padding(16.dp),
content = {
Spacer(Modifier.width(16.dp))
Icon(secondaryButtonIcon, null)
Spacer(Modifier.width(16.dp))
Text(secondaryButtonText)
Spacer(Modifier.width(16.dp))
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
EnhancedFloatingActionButton(
onClick = onSecondaryButtonClick,
onLongClick = onSecondaryButtonLongClick,
content = {
Spacer(Modifier.width(16.dp))
Icon(secondaryButtonIcon, null)
Spacer(Modifier.width(16.dp))
Text(secondaryButtonText)
Spacer(Modifier.width(16.dp))
}
)
if (showColumnarFabInRow && columnarFab != null) {
Column { columnarFab() }
}
)
}
}
if (showNullDataButtonAsContainer) {
Row(
@ -136,7 +145,9 @@ fun BottomButtonsBlock(
modifier = Modifier.drawHorizontalStroke(true),
actions = actions,
floatingActionButton = {
Row {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
AnimatedVisibility(visible = isSecondaryButtonVisible) {
EnhancedFloatingActionButton(
onClick = onSecondaryButtonClick,
@ -152,43 +163,45 @@ fun BottomButtonsBlock(
)
}
}
AnimatedVisibility(visible = showColumnarFabInRow) {
columnarFab?.let {
Column { it() }
}
}
AnimatedVisibility(visible = isPrimaryButtonVisible) {
Row {
Spacer(Modifier.width(8.dp))
EnhancedFloatingActionButton(
onClick = if (isPrimaryButtonEnabled) onPrimaryButtonClick
else null,
onLongClick = if (isPrimaryButtonEnabled) onPrimaryButtonLongClick
else null,
containerColor = takeColorFromScheme {
if (isPrimaryButtonEnabled) primaryContainer
else surfaceContainerHighest
},
contentColor = takeColorFromScheme {
if (isPrimaryButtonEnabled) onPrimaryContainer
else outline
}
) {
AnimatedContent(
targetState = primaryButtonIcon to primaryButtonText,
transitionSpec = { fadeIn() + scaleIn() togetherWith fadeOut() + scaleOut() }
) { (icon, text) ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
if (text.isNotEmpty()) {
Spacer(Modifier.width(16.dp))
}
Icon(
imageVector = icon,
contentDescription = null
)
if (text.isNotEmpty()) {
Spacer(Modifier.width(16.dp))
Text(text)
Spacer(Modifier.width(16.dp))
}
EnhancedFloatingActionButton(
onClick = if (isPrimaryButtonEnabled) onPrimaryButtonClick
else null,
onLongClick = if (isPrimaryButtonEnabled) onPrimaryButtonLongClick
else null,
containerColor = takeColorFromScheme {
if (isPrimaryButtonEnabled) primaryContainer
else surfaceContainerHighest
},
contentColor = takeColorFromScheme {
if (isPrimaryButtonEnabled) onPrimaryContainer
else outline
}
) {
AnimatedContent(
targetState = primaryButtonIcon to primaryButtonText,
transitionSpec = { fadeIn() + scaleIn() togetherWith fadeOut() + scaleOut() }
) { (icon, text) ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
if (text.isNotEmpty()) {
Spacer(Modifier.width(16.dp))
}
Icon(
imageVector = icon,
contentDescription = null
)
if (text.isNotEmpty()) {
Spacer(Modifier.width(16.dp))
Text(text)
Spacer(Modifier.width(16.dp))
}
}
}
@ -223,7 +236,10 @@ fun BottomButtonsBlock(
EnhancedFloatingActionButton(
onClick = onSecondaryButtonClick,
onLongClick = onSecondaryButtonLongClick,
containerColor = MaterialTheme.colorScheme.tertiaryContainer
containerColor = takeColorFromScheme {
if (isPrimaryButtonVisible) tertiaryContainer
else primaryContainer
}
) {
Icon(
imageVector = secondaryButtonIcon,

View File

@ -121,6 +121,7 @@ internal fun List<Uri>.screenList(
Screen.ImageStacking(uris),
Screen.ImageSplitting(uris.firstOrNull()),
Screen.ImageCutter(uris),
Screen.ScanQrCode(uriToAnalyze = uris.firstOrNull()),
Screen.GradientMaker(uris),
Screen.PdfTools(
Screen.PdfTools.Type.ImagesToPdf(uris)

View File

@ -52,7 +52,50 @@ import ru.tech.imageresizershrinker.feature.pdf_tools.presentation.screenLogic.P
import ru.tech.imageresizershrinker.feature.pick_color.presentation.screenLogic.PickColorFromImageComponent
import ru.tech.imageresizershrinker.feature.recognize.text.presentation.screenLogic.RecognizeTextComponent
import ru.tech.imageresizershrinker.feature.resize_convert.presentation.screenLogic.ResizeAndConvertComponent
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.*
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ApngTools
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Base64Tools
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ChecksumTools
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Cipher
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.CollageMaker
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ColorTools
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Compare
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Crop
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.DeleteExif
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.DocumentScanner
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Draw
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.EasterEgg
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.EditExif
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.EraseBackground
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Filter
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.FormatConversion
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.GeneratePalette
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.GifTools
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.GradientMaker
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ImageCutter
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ImagePreview
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ImageSplitting
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ImageStacking
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ImageStitching
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.JxlTools
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.LibrariesInfo
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.LimitResize
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.LoadNetImage
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Main
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.MarkupLayers
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.MeshGradients
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.NoiseGeneration
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.PdfTools
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.PickColorFromImage
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.RecognizeText
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ResizeAndConvert
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ScanQrCode
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Settings
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.SingleEdit
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.SvgMaker
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Watermarking
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.WebpTools
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.WeightResize
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Zip
import ru.tech.imageresizershrinker.feature.root.presentation.screenLogic.RootComponent
import ru.tech.imageresizershrinker.feature.scan_qr_code.presentation.screenLogic.ScanQrCodeComponent
import ru.tech.imageresizershrinker.feature.settings.presentation.screenLogic.SettingsComponent
@ -372,6 +415,7 @@ internal class ChildProvider @Inject constructor(
scanQrCodeComponentFactory(
componentContext = componentContext,
initialQrCodeContent = config.qrCodeContent,
uriToAnalyze = config.uriToAnalyze,
onGoBack = ::navigateBack
)
)

View File

@ -26,4 +26,6 @@ android.namespace = "ru.tech.imageresizershrinker.feature.scan_qr_code"
dependencies {
implementation(projects.core.filters)
"marketImplementation"(libs.quickie.bundled)
"fossImplementation"(libs.quickie.foss)
}

View File

@ -0,0 +1,64 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.feature.scan_qr_code.data
import android.graphics.Bitmap
import androidx.exifinterface.media.ExifInterface
import io.github.g00fy2.quickie.extensions.readQrCode
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
import ru.tech.imageresizershrinker.core.domain.image.ImageGetter
import ru.tech.imageresizershrinker.core.domain.resource.ResourceManager
import ru.tech.imageresizershrinker.core.resources.R
import ru.tech.imageresizershrinker.feature.scan_qr_code.domain.ImageBarcodeReader
import javax.inject.Inject
import kotlin.coroutines.resume
internal class AndroidImageBarcodeReader @Inject constructor(
private val imageGetter: ImageGetter<Bitmap, ExifInterface>,
resourceManager: ResourceManager,
dispatchersHolder: DispatchersHolder
) : ImageBarcodeReader, DispatchersHolder by dispatchersHolder, ResourceManager by resourceManager {
override suspend fun readBarcode(
image: Any
): Result<String> = withContext(defaultDispatcher) {
val bitmap = imageGetter.getImage(
data = image,
originalSize = false
)
if (bitmap == null) {
return@withContext Result.failure(NullPointerException(getString(R.string.something_went_wrong)))
}
suspendCancellableCoroutine { continuation ->
bitmap.readQrCode(
barcodeFormats = IntArray(0),
onSuccess = {
continuation.resume(Result.success(it))
},
onFailure = {
continuation.resume(Result.failure(it))
}
)
}
}
}

View File

@ -0,0 +1,40 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.feature.scan_qr_code.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ru.tech.imageresizershrinker.feature.scan_qr_code.data.AndroidImageBarcodeReader
import ru.tech.imageresizershrinker.feature.scan_qr_code.domain.ImageBarcodeReader
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ScanQrCodeModule {
@Binds
@Singleton
fun reader(
impl: AndroidImageBarcodeReader
): ImageBarcodeReader
}

View File

@ -0,0 +1,26 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.feature.scan_qr_code.domain
interface ImageBarcodeReader {
suspend fun readBarcode(
image: Any
): Result<String>
}

View File

@ -19,6 +19,7 @@ package ru.tech.imageresizershrinker.feature.scan_qr_code.presentation
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
@ -26,7 +27,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AutoFixHigh
import androidx.compose.material.icons.outlined.QrCodeScanner
import androidx.compose.material.icons.rounded.ImageSearch
import androidx.compose.material3.Badge
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -43,6 +46,9 @@ import androidx.compose.ui.unit.dp
import dev.shreyaspatil.capturable.controller.rememberCaptureController
import kotlinx.coroutines.launch
import ru.tech.imageresizershrinker.core.resources.R
import ru.tech.imageresizershrinker.core.ui.theme.takeColorFromScheme
import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.Picker
import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.rememberImagePicker
import ru.tech.imageresizershrinker.core.ui.utils.helper.asClip
import ru.tech.imageresizershrinker.core.ui.utils.helper.isLandscapeOrientationAsState
import ru.tech.imageresizershrinker.core.ui.utils.helper.rememberBarcodeScanner
@ -52,7 +58,9 @@ import ru.tech.imageresizershrinker.core.ui.widget.AdaptiveLayoutScreen
import ru.tech.imageresizershrinker.core.ui.widget.buttons.BottomButtonsBlock
import ru.tech.imageresizershrinker.core.ui.widget.buttons.ShareButton
import ru.tech.imageresizershrinker.core.ui.widget.dialogs.LoadingDialog
import ru.tech.imageresizershrinker.core.ui.widget.dialogs.OneTimeImagePickingDialog
import ru.tech.imageresizershrinker.core.ui.widget.dialogs.OneTimeSaveLocationSelectionDialog
import ru.tech.imageresizershrinker.core.ui.widget.enhanced.EnhancedFloatingActionButton
import ru.tech.imageresizershrinker.core.ui.widget.modifier.scaleOnTap
import ru.tech.imageresizershrinker.core.ui.widget.other.BarcodeType
import ru.tech.imageresizershrinker.core.ui.widget.other.TopAppBarEmoji
@ -82,6 +90,17 @@ fun ScanQrCodeContent(
)
}
val analyzerImagePicker = rememberImagePicker { uri: Uri ->
component.readBarcodeFromImage(
imageUri = uri,
onFailure = {
essentials.showFailureToast(
Throwable(context.getString(R.string.no_barcode_found), it)
)
}
)
}
LaunchedEffect(params.content) {
component.processFilterTemplateFromQrContent(
onSuccess = { filterName, filtersCount ->
@ -192,8 +211,11 @@ fun ScanQrCodeContent(
var showFolderSelectionDialog by rememberSaveable {
mutableStateOf(false)
}
var showOneTimeImagePickingDialog by rememberSaveable {
mutableStateOf(false)
}
BottomButtonsBlock(
targetState = (params.content.isEmpty()) to !isLandscape,
targetState = (params.content.isEmpty() && !isLandscape) to !isLandscape,
secondaryButtonIcon = Icons.Outlined.QrCodeScanner,
secondaryButtonText = stringResource(R.string.start_scanning),
onSecondaryButtonClick = scanner::scan,
@ -208,6 +230,25 @@ fun ScanQrCodeContent(
},
actions = {
if (!isLandscape) actions()
},
showColumnarFabInRow = true,
isPrimaryButtonVisible = !isLandscape || params.content.isNotEmpty(),
columnarFab = {
EnhancedFloatingActionButton(
onClick = analyzerImagePicker::pickImage,
onLongClick = {
showOneTimeImagePickingDialog = true
},
containerColor = takeColorFromScheme {
if (params.content.isEmpty()) tertiaryContainer
else secondaryContainer
}
) {
Icon(
imageVector = Icons.Rounded.ImageSearch,
contentDescription = null
)
}
}
)
OneTimeSaveLocationSelectionDialog(
@ -221,6 +262,12 @@ fun ScanQrCodeContent(
},
formatForFilenameSelection = component.getFormatForFilenameSelection()
)
OneTimeImagePickingDialog(
onDismiss = { showOneTimeImagePickingDialog = false },
picker = Picker.Single,
imagePicker = analyzerImagePicker,
visible = showOneTimeImagePickingDialog
)
},
canShowScreenData = true,
isPortrait = !isLandscape

View File

@ -48,16 +48,19 @@ import ru.tech.imageresizershrinker.core.settings.domain.model.SettingsState
import ru.tech.imageresizershrinker.core.settings.presentation.model.toUiFont
import ru.tech.imageresizershrinker.core.ui.utils.BaseComponent
import ru.tech.imageresizershrinker.core.ui.utils.state.update
import ru.tech.imageresizershrinker.feature.scan_qr_code.domain.ImageBarcodeReader
import ru.tech.imageresizershrinker.feature.scan_qr_code.presentation.components.QrPreviewParams
class ScanQrCodeComponent @AssistedInject internal constructor(
@Assisted componentContext: ComponentContext,
@Assisted initialQrCodeContent: String?,
@Assisted uriToAnalyze: Uri?,
@Assisted val onGoBack: () -> Unit,
private val fileController: FileController,
private val shareProvider: ShareProvider<Bitmap>,
private val imageCompressor: ImageCompressor<Bitmap>,
private val favoriteFiltersInteractor: FavoriteFiltersInteractor,
private val imageBarcodeReader: ImageBarcodeReader,
settingsProvider: SettingsProvider,
dispatchersHolder: DispatchersHolder
) : BaseComponent(dispatchersHolder, componentContext) {
@ -79,14 +82,16 @@ class ScanQrCodeComponent @AssistedInject internal constructor(
private var settingsState: SettingsState = SettingsState.Default
init {
settingsProvider.getSettingsStateFlow().onEach {
settingsState = it
settingsProvider.getSettingsStateFlow().onEach { state ->
settingsState = state
_params.update {
it.copy(
descriptionFont = settingsState.font.toUiFont()
)
}
}.launchIn(componentScope)
uriToAnalyze?.let(::readBarcodeFromImage)
}
fun saveBitmap(
@ -194,11 +199,30 @@ class ScanQrCodeComponent @AssistedInject internal constructor(
_params.update { params }
}
fun readBarcodeFromImage(
imageUri: Uri,
onFailure: (Throwable) -> Unit = {}
) {
componentScope.launch {
imageBarcodeReader
.readBarcode(imageUri)
.onSuccess {
updateParams(
params.copy(
content = it
)
)
}
.onFailure(onFailure)
}
}
@AssistedFactory
fun interface Factory {
operator fun invoke(
componentContext: ComponentContext,
initialQrCodeContent: String?,
uriToAnalyze: Uri?,
onGoBack: () -> Unit,
): ScanQrCodeComponent
}