Added ability to use unequal row and column sizes in Image Splitting by #1961

This commit is contained in:
T8RIN
2025-08-05 01:34:06 +03:00
parent f5bece16e6
commit 7dce08af63
6 changed files with 172 additions and 39 deletions

View File

@ -1742,4 +1742,5 @@
<string name="lens_correction">Lens Correction</string>
<string name="target_lens_profile">Target lens profile file in JSON format</string>
<string name="download_ready_lens_profiles">Download ready lens profiles</string>
<string name="part_percents">Part percents</string>
</resources>

View File

@ -39,8 +39,7 @@ internal class AndroidImageSplitter @Inject constructor(
override suspend fun split(
imageUri: String,
params: SplitParams,
onProgress: (Int) -> Unit
params: SplitParams
): List<String> = 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<Float> = emptyList(),
columnPercentages: List<Float> = emptyList()
): List<String> = withContext(defaultDispatcher) {
val cellHeight = image.height / rowsCount.toFloat()
val rowHeights = calculatePartSizes(image.height, rowPercentages, rowsCount)
val uris = mutableListOf<Deferred<List<String>>>()
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<Float> = emptyList()
): List<String> = withContext(defaultDispatcher) {
val cellHeight = image.height / count.toFloat()
val rowHeights = calculatePartSizes(image.height, rowPercentages, count)
val uris = mutableListOf<String?>()
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<Float> = emptyList()
): List<String> = withContext(defaultDispatcher) {
val cellWidth = image.width / count.toFloat()
val columnWidths = calculatePartSizes(image.width, columnPercentages, count)
val uris = mutableListOf<String?>()
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<Float>,
count: Int
): List<Int> {
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
}
}
}
}

View File

@ -21,8 +21,7 @@ interface ImageSplitter {
suspend fun split(
imageUri: String,
params: SplitParams,
onProgress: (Int) -> Unit
params: SplitParams
): List<String>
}

View File

@ -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<Float>,
val columnPercentages: List<Float>,
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()
)

View File

@ -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<Float>,
onValueChange: (List<Float>) -> 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)
)
}

View File

@ -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
}
}