From c603c730daeff2e55eae83b76b2f782f84917d1c Mon Sep 17 00:00:00 2001 From: T8RIN Date: Sat, 19 Apr 2025 02:37:25 +0300 Subject: [PATCH] Added Tone curves as filter by #1838 --- core/filters/build.gradle.kts | 1 + .../core/filters/domain/model/Filter.kt | 1 + .../filters/domain/model/ToneCurvesParams.kt | 13 +++ .../filters/presentation/model/UiFilter.kt | 3 +- .../presentation/model/UiToneCurvesFilter.kt | 16 ++++ .../presentation/widget/FilterItemContent.kt | 11 +++ .../widget/filterItem/ToneCurvesParamsItem.kt | 94 +++++++++++++++++++ feature/filters/build.gradle.kts | 1 + .../filters/data/AndroidFilterProvider.kt | 6 +- .../data/model/AutoRemoveRedEyesFilter.kt | 2 +- .../filters/data/model/CubeLutFilter.kt | 2 +- .../filters/data/model/ToneCurvesFilter.kt | 18 ++++ .../data/utils/serialization/Mappings.kt | 22 ++++- gradle/libs.versions.toml | 2 +- 14 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/domain/model/ToneCurvesParams.kt create mode 100644 core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/model/UiToneCurvesFilter.kt create mode 100644 core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/widget/filterItem/ToneCurvesParamsItem.kt create mode 100644 feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/model/ToneCurvesFilter.kt diff --git a/core/filters/build.gradle.kts b/core/filters/build.gradle.kts index 0065c6585..96b9589ab 100644 --- a/core/filters/build.gradle.kts +++ b/core/filters/build.gradle.kts @@ -28,4 +28,5 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.resources) implementation(libs.kotlin.reflect) + implementation(libs.toolbox.curves) } \ No newline at end of file diff --git a/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/domain/model/Filter.kt b/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/domain/model/Filter.kt index 3b5f77dcb..78e58cdb5 100644 --- a/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/domain/model/Filter.kt +++ b/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/domain/model/Filter.kt @@ -265,6 +265,7 @@ interface Filter : VisibilityOwner { interface LaplacianSimple : SimpleFilter interface MotionBlur : TripleFilter interface AutoRemoveRedEyes : Filter + interface ToneCurves : Filter } interface SimpleFilter : Filter diff --git a/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/domain/model/ToneCurvesParams.kt b/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/domain/model/ToneCurvesParams.kt new file mode 100644 index 000000000..af4e08d36 --- /dev/null +++ b/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/domain/model/ToneCurvesParams.kt @@ -0,0 +1,13 @@ +package ru.tech.imageresizershrinker.core.filters.domain.model + +import com.t8rin.curves.ImageCurvesEditorState + +data class ToneCurvesParams( + val controlPoints: List> +) { + companion object { + val Default by lazy { + ToneCurvesParams(ImageCurvesEditorState.Default.controlPoints) + } + } +} \ No newline at end of file diff --git a/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/model/UiFilter.kt b/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/model/UiFilter.kt index 58ffeacdf..f9294c4c9 100644 --- a/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/model/UiFilter.kt +++ b/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/model/UiFilter.kt @@ -144,7 +144,8 @@ sealed class UiFilter( UiPosterizeFilter(), UiColorPosterFilter(), UiTriToneFilter(), - UiPopArtFilter() + UiPopArtFilter(), + UiToneCurvesFilter() ), listOf( UiLUT512x512Filter(), diff --git a/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/model/UiToneCurvesFilter.kt b/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/model/UiToneCurvesFilter.kt new file mode 100644 index 000000000..2a7fcfe57 --- /dev/null +++ b/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/model/UiToneCurvesFilter.kt @@ -0,0 +1,16 @@ +package ru.tech.imageresizershrinker.core.filters.presentation.model + +import ru.tech.imageresizershrinker.core.filters.domain.model.Filter +import ru.tech.imageresizershrinker.core.filters.domain.model.FilterParam +import ru.tech.imageresizershrinker.core.filters.domain.model.ToneCurvesParams +import ru.tech.imageresizershrinker.core.resources.R + +class UiToneCurvesFilter( + override val value: ToneCurvesParams = ToneCurvesParams.Default +) : UiFilter( + title = R.string.tone_curves, + paramsInfo = listOf( + FilterParam(R.string.values, 0f..0f) + ), + value = value +), Filter.ToneCurves \ No newline at end of file diff --git a/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/widget/FilterItemContent.kt b/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/widget/FilterItemContent.kt index f508d5206..275e871ce 100644 --- a/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/widget/FilterItemContent.kt +++ b/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/widget/FilterItemContent.kt @@ -29,6 +29,7 @@ import ru.tech.imageresizershrinker.core.filters.domain.model.LinearGaussianPara import ru.tech.imageresizershrinker.core.filters.domain.model.LinearTiltShiftParams import ru.tech.imageresizershrinker.core.filters.domain.model.RadialTiltShiftParams import ru.tech.imageresizershrinker.core.filters.domain.model.SideFadeParams +import ru.tech.imageresizershrinker.core.filters.domain.model.ToneCurvesParams import ru.tech.imageresizershrinker.core.filters.domain.model.WaterParams import ru.tech.imageresizershrinker.core.filters.presentation.model.UiFilter import ru.tech.imageresizershrinker.core.filters.presentation.widget.filterItem.BooleanItem @@ -43,6 +44,7 @@ import ru.tech.imageresizershrinker.core.filters.presentation.widget.filterItem. import ru.tech.imageresizershrinker.core.filters.presentation.widget.filterItem.PairItem import ru.tech.imageresizershrinker.core.filters.presentation.widget.filterItem.RadialTiltShiftParamsItem import ru.tech.imageresizershrinker.core.filters.presentation.widget.filterItem.SideFadeRelativeItem +import ru.tech.imageresizershrinker.core.filters.presentation.widget.filterItem.ToneCurvesParamsItem import ru.tech.imageresizershrinker.core.filters.presentation.widget.filterItem.TripleItem import ru.tech.imageresizershrinker.core.filters.presentation.widget.filterItem.WaterParamsItem @@ -182,6 +184,15 @@ internal fun FilterItemContent( previewOnly = previewOnly ) } + + is ToneCurvesParams -> { + ToneCurvesParamsItem( + value = value, + filter = filter.cast(), + onFilterChange = onFilterChange, + previewOnly = previewOnly + ) + } } } } \ No newline at end of file diff --git a/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/widget/filterItem/ToneCurvesParamsItem.kt b/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/widget/filterItem/ToneCurvesParamsItem.kt new file mode 100644 index 000000000..0f12c8e57 --- /dev/null +++ b/core/filters/src/main/java/ru/tech/imageresizershrinker/core/filters/presentation/widget/filterItem/ToneCurvesParamsItem.kt @@ -0,0 +1,94 @@ +package ru.tech.imageresizershrinker.core.filters.presentation.widget.filterItem + +import android.graphics.Bitmap +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import coil3.imageLoader +import coil3.request.ImageRequest +import coil3.toBitmap +import com.t8rin.curves.ImageCurvesEditor +import com.t8rin.curves.ImageCurvesEditorState +import ru.tech.imageresizershrinker.core.filters.domain.model.ToneCurvesParams +import ru.tech.imageresizershrinker.core.filters.presentation.model.UiFilter +import ru.tech.imageresizershrinker.core.resources.R + +@Composable +internal fun ToneCurvesParamsItem( + value: ToneCurvesParams, + filter: UiFilter, + onFilterChange: (value: ToneCurvesParams) -> Unit, + previewOnly: Boolean +) { + val editorState: MutableState = + remember { mutableStateOf(ImageCurvesEditorState(value.controlPoints)) } + + Box( + modifier = Modifier.padding(8.dp) + ) { + var bitmap by remember { + mutableStateOf(null) + } + + val context = LocalContext.current + + LaunchedEffect(context) { + bitmap = context.imageLoader.execute( + ImageRequest.Builder(context) + .data(R.drawable.filter_preview_source) + .build() + ).image?.toBitmap() + } + + ImageCurvesEditor( + bitmap = bitmap, + state = editorState.value, + curvesSelectionText = { + Text( + text = when (it) { + 0 -> stringResource(R.string.all) + 1 -> stringResource(R.string.color_red) + 2 -> stringResource(R.string.color_green) + 3 -> stringResource(R.string.color_blue) + else -> "" + }, + style = MaterialTheme.typography.bodySmall + ) + }, + imageObtainingTrigger = false, + onImageObtained = { }, + //shape = RoundedCornerShape(4.dp), + containerModifier = Modifier.fillMaxWidth(), + onStateChange = { + onFilterChange( + ToneCurvesParams( + controlPoints = it.controlPoints + ) + ) + } + ) + + if (previewOnly) { + Surface( + modifier = Modifier.matchParentSize(), + color = Color.Transparent, + content = {} + ) + } + } +} \ No newline at end of file diff --git a/feature/filters/build.gradle.kts b/feature/filters/build.gradle.kts index 55d0ce15a..4a8657a7a 100644 --- a/feature/filters/build.gradle.kts +++ b/feature/filters/build.gradle.kts @@ -34,4 +34,5 @@ dependencies { implementation(libs.trickle) implementation(libs.toolbox.gpuimage) implementation(libs.toolbox.opencvTools) + implementation(libs.toolbox.curves) } \ No newline at end of file diff --git a/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/AndroidFilterProvider.kt b/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/AndroidFilterProvider.kt index 1d0e75e61..72c0ddd28 100644 --- a/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/AndroidFilterProvider.kt +++ b/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/AndroidFilterProvider.kt @@ -246,6 +246,7 @@ import ru.tech.imageresizershrinker.feature.filters.data.model.SunriseFilter import ru.tech.imageresizershrinker.feature.filters.data.model.SwirlDistortionFilter import ru.tech.imageresizershrinker.feature.filters.data.model.TentBlurFilter import ru.tech.imageresizershrinker.feature.filters.data.model.ThresholdFilter +import ru.tech.imageresizershrinker.feature.filters.data.model.ToneCurvesFilter import ru.tech.imageresizershrinker.feature.filters.data.model.ToonFilter import ru.tech.imageresizershrinker.feature.filters.data.model.TopHatFilter import ru.tech.imageresizershrinker.feature.filters.data.model.TriToneFilter @@ -490,7 +491,7 @@ internal class AndroidFilterProvider @Inject constructor( is Filter.SoftElegance -> SoftEleganceFilter(value, lutFilterFactory) is Filter.SoftEleganceVariant -> SoftEleganceVariantFilter(value, lutFilterFactory) is Filter.PaletteTransferVariant -> paletteTransferVariantFilterFactory(value) - is Filter.CubeLut -> CubeLutFilter(value, context) + is Filter.CubeLut -> CubeLutFilter(context, value) is Filter.BleachBypass -> BleachBypassFilter(value, lutFilterFactory) is Filter.Candlelight -> CandlelightFilter(value, lutFilterFactory) is Filter.DropBlues -> DropBluesFilter(value, lutFilterFactory) @@ -516,7 +517,8 @@ internal class AndroidFilterProvider @Inject constructor( is Filter.SobelSimple -> SobelSimpleFilter(value) is Filter.LaplacianSimple -> LaplacianSimpleFilter(value) is Filter.MotionBlur -> MotionBlurFilter(value) - is Filter.AutoRemoveRedEyes -> AutoRemoveRedEyesFilter(value, context) + is Filter.AutoRemoveRedEyes -> AutoRemoveRedEyesFilter(context, value) + is Filter.ToneCurves -> ToneCurvesFilter(context, value) else -> throw IllegalArgumentException("No filter implementation for interface ${filter::class.simpleName}") } diff --git a/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/model/AutoRemoveRedEyesFilter.kt b/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/model/AutoRemoveRedEyesFilter.kt index c621b9e42..a26220f9c 100644 --- a/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/model/AutoRemoveRedEyesFilter.kt +++ b/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/model/AutoRemoveRedEyesFilter.kt @@ -25,8 +25,8 @@ import ru.tech.imageresizershrinker.core.domain.transformation.Transformation import ru.tech.imageresizershrinker.core.filters.domain.model.Filter internal class AutoRemoveRedEyesFilter( + private val context: Context, override val value: Float = 150f, - private val context: Context ) : Transformation, Filter.AutoRemoveRedEyes { override val cacheKey: String diff --git a/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/model/CubeLutFilter.kt b/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/model/CubeLutFilter.kt index 00e09a985..77c1cffab 100644 --- a/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/model/CubeLutFilter.kt +++ b/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/model/CubeLutFilter.kt @@ -29,8 +29,8 @@ import ru.tech.imageresizershrinker.core.domain.transformation.Transformation import ru.tech.imageresizershrinker.core.filters.domain.model.Filter internal class CubeLutFilter @AssistedInject internal constructor( + private val context: Context, override val value: Pair = 1f to FileModel(""), - private val context: Context ) : Transformation, Filter.CubeLut { override val cacheKey: String diff --git a/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/model/ToneCurvesFilter.kt b/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/model/ToneCurvesFilter.kt new file mode 100644 index 000000000..e58205413 --- /dev/null +++ b/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/model/ToneCurvesFilter.kt @@ -0,0 +1,18 @@ +package ru.tech.imageresizershrinker.feature.filters.data.model + +import android.content.Context +import com.t8rin.curves.GPUImageToneCurveFilter +import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter +import ru.tech.imageresizershrinker.core.filters.domain.model.Filter +import ru.tech.imageresizershrinker.core.filters.domain.model.ToneCurvesParams + +internal class ToneCurvesFilter( + private val context: Context, + override val value: ToneCurvesParams = ToneCurvesParams.Default, +) : GPUFilterTransformation(context), Filter.ToneCurves { + + override val cacheKey: String + get() = (value to context).hashCode().toString() + + override fun createFilter(): GPUImageFilter = GPUImageToneCurveFilter(value.controlPoints) +} \ No newline at end of file diff --git a/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/utils/serialization/Mappings.kt b/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/utils/serialization/Mappings.kt index 9fcb57596..1bdad1c7b 100644 --- a/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/utils/serialization/Mappings.kt +++ b/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/data/utils/serialization/Mappings.kt @@ -30,6 +30,7 @@ import ru.tech.imageresizershrinker.core.filters.domain.model.LinearTiltShiftPar import ru.tech.imageresizershrinker.core.filters.domain.model.PopArtBlendingMode import ru.tech.imageresizershrinker.core.filters.domain.model.RadialTiltShiftParams import ru.tech.imageresizershrinker.core.filters.domain.model.SideFadeParams +import ru.tech.imageresizershrinker.core.filters.domain.model.ToneCurvesParams import ru.tech.imageresizershrinker.core.filters.domain.model.TransferFunc import ru.tech.imageresizershrinker.core.filters.domain.model.WaterParams import kotlin.collections.component1 @@ -151,6 +152,12 @@ internal fun Any.toPair(): Pair? { ).joinToString(PROPERTIES_SEPARATOR) } + is ToneCurvesParams -> { + ToneCurvesParams::class.simpleName!! to controlPoints.joinToString(PROPERTIES_SEPARATOR) { + it.joinToString(ADDITIONAL_PROPERTIES_SEPARATOR) + } + } + else -> null } } @@ -297,6 +304,18 @@ internal fun Pair.fromPair(): Any? { ) } + name == ToneCurvesParams::class.simpleName -> { + val controlPoints = value.split(PROPERTIES_SEPARATOR).map { valueString -> + valueString.split(ADDITIONAL_PROPERTIES_SEPARATOR).map { + it.toFloatOrNull() ?: 0f + } + } + + ToneCurvesParams( + controlPoints = controlPoints + ) + } + else -> null } } @@ -331,4 +350,5 @@ internal fun String.fromPart(type: String): Any { } } -private const val PROPERTIES_SEPARATOR = "$" \ No newline at end of file +private const val PROPERTIES_SEPARATOR = "$" +private const val ADDITIONAL_PROPERTIES_SEPARATOR = "*" \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ee3b7a7c..c2cf3a9ef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ versionCode = "173" jvmTarget = "17" -imageToolboxLibs = "3.5.6" +imageToolboxLibs = "3.5.7" trickle = "1.2.2" evaluator = "1.0.0" quickie = "1.14.0"