diff --git a/core/resources/src/main/res/values/strings.xml b/core/resources/src/main/res/values/strings.xml index 1d78af7f4..314b4d154 100644 --- a/core/resources/src/main/res/values/strings.xml +++ b/core/resources/src/main/res/values/strings.xml @@ -1742,4 +1742,5 @@ Lens Correction Target lens profile file in JSON format Download ready lens profiles + Part percents diff --git a/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/data/AndroidImageSplitter.kt b/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/data/AndroidImageSplitter.kt index dedd0bb25..08ab3ee0c 100644 --- a/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/data/AndroidImageSplitter.kt +++ b/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/data/AndroidImageSplitter.kt @@ -39,8 +39,7 @@ internal class AndroidImageSplitter @Inject constructor( override suspend fun split( imageUri: String, - params: SplitParams, - onProgress: (Int) -> Unit + params: SplitParams ): List = withContext(defaultDispatcher) { if (params.columnsCount <= 1 && params.rowsCount <= 1) { return@withContext listOf(imageUri) @@ -56,14 +55,16 @@ internal class AndroidImageSplitter @Inject constructor( image = image, count = params.columnsCount, imageFormat = params.imageFormat, - quality = params.quality + quality = params.quality, + columnPercentages = params.columnPercentages, ) } else if (params.columnsCount <= 1) { splitForRows( image = image, count = params.rowsCount, imageFormat = params.imageFormat, - quality = params.quality + quality = params.quality, + rowPercentages = params.rowPercentages ) } else { splitBoth( @@ -71,7 +72,9 @@ internal class AndroidImageSplitter @Inject constructor( rowsCount = params.rowsCount, columnsCount = params.columnsCount, imageFormat = params.imageFormat, - quality = params.quality + quality = params.quality, + rowPercentages = params.rowPercentages, + columnPercentages = params.columnPercentages ) } } @@ -81,28 +84,29 @@ internal class AndroidImageSplitter @Inject constructor( rowsCount: Int, columnsCount: Int, imageFormat: ImageFormat, - quality: Quality + quality: Quality, + rowPercentages: List = emptyList(), + columnPercentages: List = emptyList() ): List = withContext(defaultDispatcher) { - val cellHeight = image.height / rowsCount.toFloat() + val rowHeights = calculatePartSizes(image.height, rowPercentages, rowsCount) val uris = mutableListOf>>() + var currentY = 0 for (row in 0 until rowsCount) { - val y = (row * cellHeight).toInt() - val height = if (y + cellHeight.toInt() > image.height) { - image.height - y - } else cellHeight.toInt() - - val rowBitmap = Bitmap.createBitmap(image, 0, y, image.width, height) + val height = rowHeights[row] + val rowBitmap = Bitmap.createBitmap(image, 0, currentY, image.width, height) val rowUris = async { splitForColumns( image = rowBitmap, count = columnsCount, imageFormat = imageFormat, - quality = quality + quality = quality, + columnPercentages = columnPercentages ) } uris.add(rowUris) + currentY += height } uris.flatMap { it.await() } @@ -112,19 +116,16 @@ internal class AndroidImageSplitter @Inject constructor( image: Bitmap, count: Int, imageFormat: ImageFormat, - quality: Quality + quality: Quality, + rowPercentages: List = emptyList() ): List = withContext(defaultDispatcher) { - val cellHeight = image.height / count.toFloat() - + val rowHeights = calculatePartSizes(image.height, rowPercentages, count) val uris = mutableListOf() + var currentY = 0 for (i in 0 until count) { - val y = (i * cellHeight).toInt() - val height = if (y + cellHeight.toInt() > image.height) { - image.height - y - } else cellHeight.toInt() - - val cell = Bitmap.createBitmap(image, 0, y, image.width, height) + val height = rowHeights[i] + val cell = Bitmap.createBitmap(image, 0, currentY, image.width, height) uris.add( shareProvider.cacheImage( @@ -137,6 +138,7 @@ internal class AndroidImageSplitter @Inject constructor( ) ) ) + currentY += height } uris.filterNotNull() @@ -146,19 +148,16 @@ internal class AndroidImageSplitter @Inject constructor( image: Bitmap, count: Int, imageFormat: ImageFormat, - quality: Quality + quality: Quality, + columnPercentages: List = emptyList() ): List = withContext(defaultDispatcher) { - val cellWidth = image.width / count.toFloat() - + val columnWidths = calculatePartSizes(image.width, columnPercentages, count) val uris = mutableListOf() + var currentX = 0 for (i in 0 until count) { - val x = (i * cellWidth).toInt() - val width = if (x + cellWidth.toInt() > image.width) { - image.width - x - } else cellWidth.toInt() - - val cell = Bitmap.createBitmap(image, x, 0, width, image.height) + val width = columnWidths[i] + val cell = Bitmap.createBitmap(image, currentX, 0, width, image.height) uris.add( shareProvider.cacheImage( @@ -171,8 +170,51 @@ internal class AndroidImageSplitter @Inject constructor( ) ) ) + currentX += width } uris.filterNotNull() } + private fun calculatePartSizes( + totalSize: Int, + percentages: List, + count: Int + ): List { + if (percentages.isEmpty()) { + val partSize = totalSize / count + return List(count) { index -> + if (index == count - 1) { + totalSize - (partSize * (count - 1)) + } else { + partSize + } + } + } + + val normalizedPercentages = if (percentages.size < count) { + val remainingPercentage = 100f - percentages.sum() + val remainingParts = count - percentages.size + val equalPercentage = remainingPercentage / remainingParts + percentages + List(remainingParts) { equalPercentage } + } else if (percentages.size > count) { + percentages.take(count) + } else { + percentages + } + + val totalPercentage = normalizedPercentages.sum() + val normalized = normalizedPercentages.map { it / totalPercentage } + + return normalized.map { percentage -> + (totalSize * percentage).toInt() + }.let { sizes -> + val calculatedTotal = sizes.sum() + if (calculatedTotal != totalSize) { + sizes.dropLast(1) + (totalSize - sizes.dropLast(1).sum()) + } else { + sizes + } + } + } + } \ No newline at end of file diff --git a/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/domain/ImageSplitter.kt b/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/domain/ImageSplitter.kt index aa0c17067..25bb1aa88 100644 --- a/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/domain/ImageSplitter.kt +++ b/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/domain/ImageSplitter.kt @@ -21,8 +21,7 @@ interface ImageSplitter { suspend fun split( imageUri: String, - params: SplitParams, - onProgress: (Int) -> Unit + params: SplitParams ): List } \ No newline at end of file diff --git a/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/domain/SplitParams.kt b/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/domain/SplitParams.kt index b161de04d..8b07f35c1 100644 --- a/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/domain/SplitParams.kt +++ b/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/domain/SplitParams.kt @@ -23,14 +23,18 @@ import com.t8rin.imagetoolbox.core.domain.image.model.Quality data class SplitParams( val rowsCount: Int, val columnsCount: Int, + val rowPercentages: List, + val columnPercentages: List, val imageFormat: ImageFormat, - val quality: Quality + val quality: Quality, ) { companion object { val Default by lazy { SplitParams( rowsCount = 2, columnsCount = 2, + rowPercentages = emptyList(), + columnPercentages = emptyList(), imageFormat = ImageFormat.Default, quality = Quality.Base() ) diff --git a/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/presentation/components/SplitParamsSelector.kt b/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/presentation/components/SplitParamsSelector.kt index 64590a6d7..ea723dea1 100644 --- a/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/presentation/components/SplitParamsSelector.kt +++ b/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/presentation/components/SplitParamsSelector.kt @@ -23,19 +23,25 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.TableRows import androidx.compose.material.icons.rounded.ViewColumn +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.smarttoolfactory.extendedcolors.util.roundToTwoDigits import com.t8rin.imagetoolbox.core.resources.R import com.t8rin.imagetoolbox.core.ui.widget.controls.selection.ImageFormatSelector import com.t8rin.imagetoolbox.core.ui.widget.controls.selection.QualitySelector import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedSliderItem import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults +import com.t8rin.imagetoolbox.core.ui.widget.text.RoundedTextField import com.t8rin.imagetoolbox.image_splitting.domain.SplitParams import kotlin.math.roundToInt @@ -71,7 +77,22 @@ internal fun SplitParamsSelector( rowsCount = it.roundToInt() }, onValueChange = {}, - shape = ShapeDefaults.top + shape = ShapeDefaults.top, + additionalContent = if (rowsCount > 1) { + { + PercentagesField( + totalSize = rowsCount, + percentageValues = value.rowPercentages, + onValueChange = { + onValueChange( + value.copy( + rowPercentages = it + ) + ) + } + ) + } + } else null ) Spacer(Modifier.height(4.dp)) @@ -102,7 +123,22 @@ internal fun SplitParamsSelector( columnsCount = it.roundToInt() }, onValueChange = {}, - shape = ShapeDefaults.bottom + shape = ShapeDefaults.bottom, + additionalContent = if (columnsCount > 1) { + { + PercentagesField( + totalSize = columnsCount, + percentageValues = value.columnPercentages, + onValueChange = { + onValueChange( + value.copy( + columnPercentages = it + ) + ) + } + ) + } + } else null ) if (value.imageFormat.canChangeCompressionValue) { Spacer(Modifier.height(8.dp)) @@ -125,4 +161,54 @@ internal fun SplitParamsSelector( ) } ) +} + +@Composable +private fun PercentagesField( + totalSize: Int, + percentageValues: List, + onValueChange: (List) -> Unit +) { + val default by remember(totalSize) { + derivedStateOf { + List(totalSize) { 1f / totalSize }.joinToString("/") { + it.roundToTwoDigits().toString() + } + } + } + var percentages by remember(default) { + mutableStateOf( + percentageValues.joinToString("/") { + it.roundToTwoDigits().toString() + }.ifEmpty { default } + ) + } + + LaunchedEffect(percentageValues, totalSize) { + if (percentageValues.size > totalSize) { + percentages = percentageValues.take(totalSize).joinToString("/") { + it.roundToTwoDigits().toString() + } + } + } + + LaunchedEffect(percentages) { + onValueChange( + percentages.split("/").mapNotNull { it.toFloatOrNull() } + ) + } + + RoundedTextField( + value = percentages, + onValueChange = { + percentages = it + }, + label = { + Text(text = stringResource(R.string.part_percents)) + }, + hint = { + Text(text = default) + }, + modifier = Modifier.padding(8.dp) + ) } \ No newline at end of file diff --git a/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/presentation/screenLogic/ImageSplitterComponent.kt b/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/presentation/screenLogic/ImageSplitterComponent.kt index db0a66bb4..51a4091d8 100644 --- a/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/presentation/screenLogic/ImageSplitterComponent.kt +++ b/feature/image-splitting/src/main/java/com/t8rin/imagetoolbox/image_splitting/presentation/screenLogic/ImageSplitterComponent.kt @@ -82,8 +82,7 @@ class ImageSplitterComponent @AssistedInject internal constructor( _uris.update { imageSplitter.split( imageUri = uri!!.toString(), - params = params, - onProgress = {} + params = params ).map { it.toUri() } } } @@ -145,10 +144,12 @@ class ImageSplitterComponent @AssistedInject internal constructor( fun shareBitmaps(onComplete: () -> Unit) { savingJob = componentScope.launch { _isSaving.value = true + _done.value = 0 shareProvider.shareUris( uris = uris.map { it.toString() } ) onComplete() + _isSaving.value = false } }