mirror of
https://github.com/T8RIN/ImageToolbox.git
synced 2025-08-24 00:29:32 +08:00
Added ability to use unequal row and column sizes in Image Splitting by #1961
This commit is contained in:
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -21,8 +21,7 @@ interface ImageSplitter {
|
||||
|
||||
suspend fun split(
|
||||
imageUri: String,
|
||||
params: SplitParams,
|
||||
onProgress: (Int) -> Unit
|
||||
params: SplitParams
|
||||
): List<String>
|
||||
|
||||
}
|
@ -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()
|
||||
)
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user