Added Tone curves as filter by #1838

This commit is contained in:
T8RIN
2025-04-19 02:37:25 +03:00
parent f60c7c855a
commit c603c730da
14 changed files with 185 additions and 7 deletions

View File

@ -28,4 +28,5 @@ dependencies {
implementation(projects.core.ui)
implementation(projects.core.resources)
implementation(libs.kotlin.reflect)
implementation(libs.toolbox.curves)
}

View File

@ -265,6 +265,7 @@ interface Filter<Value> : VisibilityOwner {
interface LaplacianSimple : SimpleFilter
interface MotionBlur : TripleFilter<Int, Float, BlurEdgeMode>
interface AutoRemoveRedEyes : Filter<Float>
interface ToneCurves : Filter<ToneCurvesParams>
}
interface SimpleFilter : Filter<Unit>

View File

@ -0,0 +1,13 @@
package ru.tech.imageresizershrinker.core.filters.domain.model
import com.t8rin.curves.ImageCurvesEditorState
data class ToneCurvesParams(
val controlPoints: List<List<Float>>
) {
companion object {
val Default by lazy {
ToneCurvesParams(ImageCurvesEditorState.Default.controlPoints)
}
}
}

View File

@ -144,7 +144,8 @@ sealed class UiFilter<T>(
UiPosterizeFilter(),
UiColorPosterFilter(),
UiTriToneFilter(),
UiPopArtFilter()
UiPopArtFilter(),
UiToneCurvesFilter()
),
listOf(
UiLUT512x512Filter(),

View File

@ -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<ToneCurvesParams>(
title = R.string.tone_curves,
paramsInfo = listOf(
FilterParam(R.string.values, 0f..0f)
),
value = value
), Filter.ToneCurves

View File

@ -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 <T> FilterItemContent(
previewOnly = previewOnly
)
}
is ToneCurvesParams -> {
ToneCurvesParamsItem(
value = value,
filter = filter.cast(),
onFilterChange = onFilterChange,
previewOnly = previewOnly
)
}
}
}
}

View File

@ -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<ToneCurvesParams>,
onFilterChange: (value: ToneCurvesParams) -> Unit,
previewOnly: Boolean
) {
val editorState: MutableState<ImageCurvesEditorState> =
remember { mutableStateOf(ImageCurvesEditorState(value.controlPoints)) }
Box(
modifier = Modifier.padding(8.dp)
) {
var bitmap by remember {
mutableStateOf<Bitmap?>(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 = {}
)
}
}
}

View File

@ -34,4 +34,5 @@ dependencies {
implementation(libs.trickle)
implementation(libs.toolbox.gpuimage)
implementation(libs.toolbox.opencvTools)
implementation(libs.toolbox.curves)
}

View File

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

View File

@ -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<Bitmap>, Filter.AutoRemoveRedEyes {
override val cacheKey: String

View File

@ -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<Float, FileModel> = 1f to FileModel(""),
private val context: Context
) : Transformation<Bitmap>, Filter.CubeLut {
override val cacheKey: String

View File

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

View File

@ -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<String, String>? {
).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<String, String>.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 = "$"
private const val PROPERTIES_SEPARATOR = "$"
private const val ADDITIONAL_PROPERTIES_SEPARATOR = "*"

View File

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