mirror of
https://github.com/T8RIN/ImageToolbox.git
synced 2025-08-25 00:51:59 +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="lens_correction">Lens Correction</string>
|
||||||
<string name="target_lens_profile">Target lens profile file in JSON format</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="download_ready_lens_profiles">Download ready lens profiles</string>
|
||||||
|
<string name="part_percents">Part percents</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -39,8 +39,7 @@ internal class AndroidImageSplitter @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun split(
|
override suspend fun split(
|
||||||
imageUri: String,
|
imageUri: String,
|
||||||
params: SplitParams,
|
params: SplitParams
|
||||||
onProgress: (Int) -> Unit
|
|
||||||
): List<String> = withContext(defaultDispatcher) {
|
): List<String> = withContext(defaultDispatcher) {
|
||||||
if (params.columnsCount <= 1 && params.rowsCount <= 1) {
|
if (params.columnsCount <= 1 && params.rowsCount <= 1) {
|
||||||
return@withContext listOf(imageUri)
|
return@withContext listOf(imageUri)
|
||||||
@ -56,14 +55,16 @@ internal class AndroidImageSplitter @Inject constructor(
|
|||||||
image = image,
|
image = image,
|
||||||
count = params.columnsCount,
|
count = params.columnsCount,
|
||||||
imageFormat = params.imageFormat,
|
imageFormat = params.imageFormat,
|
||||||
quality = params.quality
|
quality = params.quality,
|
||||||
|
columnPercentages = params.columnPercentages,
|
||||||
)
|
)
|
||||||
} else if (params.columnsCount <= 1) {
|
} else if (params.columnsCount <= 1) {
|
||||||
splitForRows(
|
splitForRows(
|
||||||
image = image,
|
image = image,
|
||||||
count = params.rowsCount,
|
count = params.rowsCount,
|
||||||
imageFormat = params.imageFormat,
|
imageFormat = params.imageFormat,
|
||||||
quality = params.quality
|
quality = params.quality,
|
||||||
|
rowPercentages = params.rowPercentages
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
splitBoth(
|
splitBoth(
|
||||||
@ -71,7 +72,9 @@ internal class AndroidImageSplitter @Inject constructor(
|
|||||||
rowsCount = params.rowsCount,
|
rowsCount = params.rowsCount,
|
||||||
columnsCount = params.columnsCount,
|
columnsCount = params.columnsCount,
|
||||||
imageFormat = params.imageFormat,
|
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,
|
rowsCount: Int,
|
||||||
columnsCount: Int,
|
columnsCount: Int,
|
||||||
imageFormat: ImageFormat,
|
imageFormat: ImageFormat,
|
||||||
quality: Quality
|
quality: Quality,
|
||||||
|
rowPercentages: List<Float> = emptyList(),
|
||||||
|
columnPercentages: List<Float> = emptyList()
|
||||||
): List<String> = withContext(defaultDispatcher) {
|
): List<String> = withContext(defaultDispatcher) {
|
||||||
val cellHeight = image.height / rowsCount.toFloat()
|
val rowHeights = calculatePartSizes(image.height, rowPercentages, rowsCount)
|
||||||
val uris = mutableListOf<Deferred<List<String>>>()
|
val uris = mutableListOf<Deferred<List<String>>>()
|
||||||
|
|
||||||
|
var currentY = 0
|
||||||
for (row in 0 until rowsCount) {
|
for (row in 0 until rowsCount) {
|
||||||
val y = (row * cellHeight).toInt()
|
val height = rowHeights[row]
|
||||||
val height = if (y + cellHeight.toInt() > image.height) {
|
val rowBitmap = Bitmap.createBitmap(image, 0, currentY, image.width, height)
|
||||||
image.height - y
|
|
||||||
} else cellHeight.toInt()
|
|
||||||
|
|
||||||
val rowBitmap = Bitmap.createBitmap(image, 0, y, image.width, height)
|
|
||||||
|
|
||||||
val rowUris = async {
|
val rowUris = async {
|
||||||
splitForColumns(
|
splitForColumns(
|
||||||
image = rowBitmap,
|
image = rowBitmap,
|
||||||
count = columnsCount,
|
count = columnsCount,
|
||||||
imageFormat = imageFormat,
|
imageFormat = imageFormat,
|
||||||
quality = quality
|
quality = quality,
|
||||||
|
columnPercentages = columnPercentages
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
uris.add(rowUris)
|
uris.add(rowUris)
|
||||||
|
currentY += height
|
||||||
}
|
}
|
||||||
|
|
||||||
uris.flatMap { it.await() }
|
uris.flatMap { it.await() }
|
||||||
@ -112,19 +116,16 @@ internal class AndroidImageSplitter @Inject constructor(
|
|||||||
image: Bitmap,
|
image: Bitmap,
|
||||||
count: Int,
|
count: Int,
|
||||||
imageFormat: ImageFormat,
|
imageFormat: ImageFormat,
|
||||||
quality: Quality
|
quality: Quality,
|
||||||
|
rowPercentages: List<Float> = emptyList()
|
||||||
): List<String> = withContext(defaultDispatcher) {
|
): List<String> = withContext(defaultDispatcher) {
|
||||||
val cellHeight = image.height / count.toFloat()
|
val rowHeights = calculatePartSizes(image.height, rowPercentages, count)
|
||||||
|
|
||||||
val uris = mutableListOf<String?>()
|
val uris = mutableListOf<String?>()
|
||||||
|
|
||||||
|
var currentY = 0
|
||||||
for (i in 0 until count) {
|
for (i in 0 until count) {
|
||||||
val y = (i * cellHeight).toInt()
|
val height = rowHeights[i]
|
||||||
val height = if (y + cellHeight.toInt() > image.height) {
|
val cell = Bitmap.createBitmap(image, 0, currentY, image.width, height)
|
||||||
image.height - y
|
|
||||||
} else cellHeight.toInt()
|
|
||||||
|
|
||||||
val cell = Bitmap.createBitmap(image, 0, y, image.width, height)
|
|
||||||
|
|
||||||
uris.add(
|
uris.add(
|
||||||
shareProvider.cacheImage(
|
shareProvider.cacheImage(
|
||||||
@ -137,6 +138,7 @@ internal class AndroidImageSplitter @Inject constructor(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
currentY += height
|
||||||
}
|
}
|
||||||
|
|
||||||
uris.filterNotNull()
|
uris.filterNotNull()
|
||||||
@ -146,19 +148,16 @@ internal class AndroidImageSplitter @Inject constructor(
|
|||||||
image: Bitmap,
|
image: Bitmap,
|
||||||
count: Int,
|
count: Int,
|
||||||
imageFormat: ImageFormat,
|
imageFormat: ImageFormat,
|
||||||
quality: Quality
|
quality: Quality,
|
||||||
|
columnPercentages: List<Float> = emptyList()
|
||||||
): List<String> = withContext(defaultDispatcher) {
|
): List<String> = withContext(defaultDispatcher) {
|
||||||
val cellWidth = image.width / count.toFloat()
|
val columnWidths = calculatePartSizes(image.width, columnPercentages, count)
|
||||||
|
|
||||||
val uris = mutableListOf<String?>()
|
val uris = mutableListOf<String?>()
|
||||||
|
|
||||||
|
var currentX = 0
|
||||||
for (i in 0 until count) {
|
for (i in 0 until count) {
|
||||||
val x = (i * cellWidth).toInt()
|
val width = columnWidths[i]
|
||||||
val width = if (x + cellWidth.toInt() > image.width) {
|
val cell = Bitmap.createBitmap(image, currentX, 0, width, image.height)
|
||||||
image.width - x
|
|
||||||
} else cellWidth.toInt()
|
|
||||||
|
|
||||||
val cell = Bitmap.createBitmap(image, x, 0, width, image.height)
|
|
||||||
|
|
||||||
uris.add(
|
uris.add(
|
||||||
shareProvider.cacheImage(
|
shareProvider.cacheImage(
|
||||||
@ -171,8 +170,51 @@ internal class AndroidImageSplitter @Inject constructor(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
currentX += width
|
||||||
}
|
}
|
||||||
uris.filterNotNull()
|
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(
|
suspend fun split(
|
||||||
imageUri: String,
|
imageUri: String,
|
||||||
params: SplitParams,
|
params: SplitParams
|
||||||
onProgress: (Int) -> Unit
|
|
||||||
): List<String>
|
): List<String>
|
||||||
|
|
||||||
}
|
}
|
@ -23,14 +23,18 @@ import com.t8rin.imagetoolbox.core.domain.image.model.Quality
|
|||||||
data class SplitParams(
|
data class SplitParams(
|
||||||
val rowsCount: Int,
|
val rowsCount: Int,
|
||||||
val columnsCount: Int,
|
val columnsCount: Int,
|
||||||
|
val rowPercentages: List<Float>,
|
||||||
|
val columnPercentages: List<Float>,
|
||||||
val imageFormat: ImageFormat,
|
val imageFormat: ImageFormat,
|
||||||
val quality: Quality
|
val quality: Quality,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val Default by lazy {
|
val Default by lazy {
|
||||||
SplitParams(
|
SplitParams(
|
||||||
rowsCount = 2,
|
rowsCount = 2,
|
||||||
columnsCount = 2,
|
columnsCount = 2,
|
||||||
|
rowPercentages = emptyList(),
|
||||||
|
columnPercentages = emptyList(),
|
||||||
imageFormat = ImageFormat.Default,
|
imageFormat = ImageFormat.Default,
|
||||||
quality = Quality.Base()
|
quality = Quality.Base()
|
||||||
)
|
)
|
||||||
|
@ -23,19 +23,25 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.TableRows
|
import androidx.compose.material.icons.rounded.TableRows
|
||||||
import androidx.compose.material.icons.rounded.ViewColumn
|
import androidx.compose.material.icons.rounded.ViewColumn
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.smarttoolfactory.extendedcolors.util.roundToTwoDigits
|
||||||
import com.t8rin.imagetoolbox.core.resources.R
|
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.ImageFormatSelector
|
||||||
import com.t8rin.imagetoolbox.core.ui.widget.controls.selection.QualitySelector
|
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.enhanced.EnhancedSliderItem
|
||||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults
|
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 com.t8rin.imagetoolbox.image_splitting.domain.SplitParams
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@ -71,7 +77,22 @@ internal fun SplitParamsSelector(
|
|||||||
rowsCount = it.roundToInt()
|
rowsCount = it.roundToInt()
|
||||||
},
|
},
|
||||||
onValueChange = {},
|
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))
|
Spacer(Modifier.height(4.dp))
|
||||||
|
|
||||||
@ -102,7 +123,22 @@ internal fun SplitParamsSelector(
|
|||||||
columnsCount = it.roundToInt()
|
columnsCount = it.roundToInt()
|
||||||
},
|
},
|
||||||
onValueChange = {},
|
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) {
|
if (value.imageFormat.canChangeCompressionValue) {
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
@ -126,3 +162,53 @@ 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 {
|
_uris.update {
|
||||||
imageSplitter.split(
|
imageSplitter.split(
|
||||||
imageUri = uri!!.toString(),
|
imageUri = uri!!.toString(),
|
||||||
params = params,
|
params = params
|
||||||
onProgress = {}
|
|
||||||
).map { it.toUri() }
|
).map { it.toUri() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,10 +144,12 @@ class ImageSplitterComponent @AssistedInject internal constructor(
|
|||||||
fun shareBitmaps(onComplete: () -> Unit) {
|
fun shareBitmaps(onComplete: () -> Unit) {
|
||||||
savingJob = componentScope.launch {
|
savingJob = componentScope.launch {
|
||||||
_isSaving.value = true
|
_isSaving.value = true
|
||||||
|
_done.value = 0
|
||||||
shareProvider.shareUris(
|
shareProvider.shareUris(
|
||||||
uris = uris.map { it.toString() }
|
uris = uris.map { it.toString() }
|
||||||
)
|
)
|
||||||
onComplete()
|
onComplete()
|
||||||
|
_isSaving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user