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.ui)
implementation(projects.core.resources) implementation(projects.core.resources)
implementation(libs.kotlin.reflect) implementation(libs.kotlin.reflect)
implementation(libs.toolbox.curves)
} }

View File

@ -265,6 +265,7 @@ interface Filter<Value> : VisibilityOwner {
interface LaplacianSimple : SimpleFilter interface LaplacianSimple : SimpleFilter
interface MotionBlur : TripleFilter<Int, Float, BlurEdgeMode> interface MotionBlur : TripleFilter<Int, Float, BlurEdgeMode>
interface AutoRemoveRedEyes : Filter<Float> interface AutoRemoveRedEyes : Filter<Float>
interface ToneCurves : Filter<ToneCurvesParams>
} }
interface SimpleFilter : Filter<Unit> 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(), UiPosterizeFilter(),
UiColorPosterFilter(), UiColorPosterFilter(),
UiTriToneFilter(), UiTriToneFilter(),
UiPopArtFilter() UiPopArtFilter(),
UiToneCurvesFilter()
), ),
listOf( listOf(
UiLUT512x512Filter(), 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.LinearTiltShiftParams
import ru.tech.imageresizershrinker.core.filters.domain.model.RadialTiltShiftParams 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.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.domain.model.WaterParams
import ru.tech.imageresizershrinker.core.filters.presentation.model.UiFilter import ru.tech.imageresizershrinker.core.filters.presentation.model.UiFilter
import ru.tech.imageresizershrinker.core.filters.presentation.widget.filterItem.BooleanItem 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.PairItem
import ru.tech.imageresizershrinker.core.filters.presentation.widget.filterItem.RadialTiltShiftParamsItem 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.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.TripleItem
import ru.tech.imageresizershrinker.core.filters.presentation.widget.filterItem.WaterParamsItem import ru.tech.imageresizershrinker.core.filters.presentation.widget.filterItem.WaterParamsItem
@ -182,6 +184,15 @@ internal fun <T> FilterItemContent(
previewOnly = previewOnly 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.trickle)
implementation(libs.toolbox.gpuimage) implementation(libs.toolbox.gpuimage)
implementation(libs.toolbox.opencvTools) 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.SwirlDistortionFilter
import ru.tech.imageresizershrinker.feature.filters.data.model.TentBlurFilter 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.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.ToonFilter
import ru.tech.imageresizershrinker.feature.filters.data.model.TopHatFilter import ru.tech.imageresizershrinker.feature.filters.data.model.TopHatFilter
import ru.tech.imageresizershrinker.feature.filters.data.model.TriToneFilter 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.SoftElegance -> SoftEleganceFilter(value, lutFilterFactory)
is Filter.SoftEleganceVariant -> SoftEleganceVariantFilter(value, lutFilterFactory) is Filter.SoftEleganceVariant -> SoftEleganceVariantFilter(value, lutFilterFactory)
is Filter.PaletteTransferVariant -> paletteTransferVariantFilterFactory(value) 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.BleachBypass -> BleachBypassFilter(value, lutFilterFactory)
is Filter.Candlelight -> CandlelightFilter(value, lutFilterFactory) is Filter.Candlelight -> CandlelightFilter(value, lutFilterFactory)
is Filter.DropBlues -> DropBluesFilter(value, lutFilterFactory) is Filter.DropBlues -> DropBluesFilter(value, lutFilterFactory)
@ -516,7 +517,8 @@ internal class AndroidFilterProvider @Inject constructor(
is Filter.SobelSimple -> SobelSimpleFilter(value) is Filter.SobelSimple -> SobelSimpleFilter(value)
is Filter.LaplacianSimple -> LaplacianSimpleFilter(value) is Filter.LaplacianSimple -> LaplacianSimpleFilter(value)
is Filter.MotionBlur -> MotionBlurFilter(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}") 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 import ru.tech.imageresizershrinker.core.filters.domain.model.Filter
internal class AutoRemoveRedEyesFilter( internal class AutoRemoveRedEyesFilter(
private val context: Context,
override val value: Float = 150f, override val value: Float = 150f,
private val context: Context
) : Transformation<Bitmap>, Filter.AutoRemoveRedEyes { ) : Transformation<Bitmap>, Filter.AutoRemoveRedEyes {
override val cacheKey: String 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 import ru.tech.imageresizershrinker.core.filters.domain.model.Filter
internal class CubeLutFilter @AssistedInject internal constructor( internal class CubeLutFilter @AssistedInject internal constructor(
private val context: Context,
override val value: Pair<Float, FileModel> = 1f to FileModel(""), override val value: Pair<Float, FileModel> = 1f to FileModel(""),
private val context: Context
) : Transformation<Bitmap>, Filter.CubeLut { ) : Transformation<Bitmap>, Filter.CubeLut {
override val cacheKey: String 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.PopArtBlendingMode
import ru.tech.imageresizershrinker.core.filters.domain.model.RadialTiltShiftParams 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.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.TransferFunc
import ru.tech.imageresizershrinker.core.filters.domain.model.WaterParams import ru.tech.imageresizershrinker.core.filters.domain.model.WaterParams
import kotlin.collections.component1 import kotlin.collections.component1
@ -151,6 +152,12 @@ internal fun Any.toPair(): Pair<String, String>? {
).joinToString(PROPERTIES_SEPARATOR) ).joinToString(PROPERTIES_SEPARATOR)
} }
is ToneCurvesParams -> {
ToneCurvesParams::class.simpleName!! to controlPoints.joinToString(PROPERTIES_SEPARATOR) {
it.joinToString(ADDITIONAL_PROPERTIES_SEPARATOR)
}
}
else -> null 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 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" jvmTarget = "17"
imageToolboxLibs = "3.5.6" imageToolboxLibs = "3.5.7"
trickle = "1.2.2" trickle = "1.2.2"
evaluator = "1.0.0" evaluator = "1.0.0"
quickie = "1.14.0" quickie = "1.14.0"