Added Noise generator by #948

This commit is contained in:
T8RIN
2024-09-12 20:22:09 +03:00
parent 7bc9ad192e
commit 851e5531d1
37 changed files with 3447 additions and 26 deletions

View File

@ -21,7 +21,7 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ru.tech.imageresizershrinker.core.data.dispatchers.DispatchersHolderImpl
import ru.tech.imageresizershrinker.core.data.dispatchers.AndroidDispatchersHolder
import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
import javax.inject.Singleton
@ -32,7 +32,7 @@ internal interface DispatchersModule {
@Binds
@Singleton
fun dispatchersHolder(
dispatchers: DispatchersHolderImpl
dispatchers: AndroidDispatchersHolder
): DispatchersHolder
}

View File

@ -26,7 +26,7 @@ import ru.tech.imageresizershrinker.core.di.UiDispatcher
import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
import javax.inject.Inject
internal data class DispatchersHolderImpl @Inject constructor(
internal data class AndroidDispatchersHolder @Inject constructor(
@UiDispatcher override val uiDispatcher: CoroutineDispatcher,
@IoDispatcher override val ioDispatcher: CoroutineDispatcher,
@EncodingDispatcher override val encodingDispatcher: CoroutineDispatcher,

View File

@ -31,6 +31,11 @@ data class IntegerSize(
value
}.getOrNull() ?: 1f
val safeAspectRatio: Float
get() = aspectRatio
.coerceAtLeast(0.005f)
.coerceAtMost(1000f)
operator fun times(i: Float): IntegerSize = IntegerSize(
width = (width * i).toInt(),
height = (height * i).toInt()

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.core.filters.domain.model
package ru.tech.imageresizershrinker.core.domain.utils
import kotlin.math.pow
import kotlin.math.roundToInt

View File

@ -17,10 +17,10 @@
package ru.tech.imageresizershrinker.core.filters.presentation.model
import ru.tech.imageresizershrinker.core.domain.utils.NEAREST_ODD_ROUNDING
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.LinearGaussianParams
import ru.tech.imageresizershrinker.core.filters.domain.model.NEAREST_ODD_ROUNDING
import ru.tech.imageresizershrinker.core.resources.R
class UiLinearGaussianBlurFilter(

View File

@ -17,9 +17,9 @@
package ru.tech.imageresizershrinker.core.filters.presentation.model
import ru.tech.imageresizershrinker.core.domain.utils.NEAREST_ODD_ROUNDING
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.NEAREST_ODD_ROUNDING
import ru.tech.imageresizershrinker.core.filters.domain.model.TransferFunc
import ru.tech.imageresizershrinker.core.resources.R

View File

@ -17,9 +17,9 @@
package ru.tech.imageresizershrinker.core.filters.presentation.model
import ru.tech.imageresizershrinker.core.domain.utils.NEAREST_ODD_ROUNDING
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.NEAREST_ODD_ROUNDING
import ru.tech.imageresizershrinker.core.resources.R
class UiTentBlurFilter(

View File

@ -49,6 +49,7 @@ import androidx.compose.ui.unit.dp
import ru.tech.imageresizershrinker.core.domain.model.ColorModel
import ru.tech.imageresizershrinker.core.domain.model.FileModel
import ru.tech.imageresizershrinker.core.domain.model.ImageModel
import ru.tech.imageresizershrinker.core.domain.utils.roundTo
import ru.tech.imageresizershrinker.core.filters.domain.model.BlurEdgeMode
import ru.tech.imageresizershrinker.core.filters.domain.model.BokehParams
import ru.tech.imageresizershrinker.core.filters.domain.model.ClaheParams
@ -65,7 +66,6 @@ import ru.tech.imageresizershrinker.core.filters.domain.model.RadialTiltShiftPar
import ru.tech.imageresizershrinker.core.filters.domain.model.SideFadeParams
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.roundTo
import ru.tech.imageresizershrinker.core.filters.domain.model.wrap
import ru.tech.imageresizershrinker.core.filters.presentation.model.UiColorFilter
import ru.tech.imageresizershrinker.core.filters.presentation.model.UiFilter

View File

@ -1387,4 +1387,19 @@
<string name="show_all">Show All</string>
<string name="hide_nav_bar">Hide Nav Bar</string>
<string name="hide_status_bar">Hide Status Bar</string>
<string name="noise_generation">Noise Generation</string>
<string name="noise_generation_sub">Generate different noises like Perlin or other types</string>
<string name="frequency">Frequency</string>
<string name="noise_type">Noise Type</string>
<string name="rotation_type">Rotation Type</string>
<string name="fractal_type">Fractal Type</string>
<string name="octaves">Octaves</string>
<string name="lacunarity">Lacunarity</string>
<string name="gain">Gain</string>
<string name="weighted_strength">Weighted Strength</string>
<string name="ping_pong_strength">Ping Pong Strength</string>
<string name="distance_function">Distance Function</string>
<string name="return_type">Return Type</string>
<string name="jitter">Jitter</string>
<string name="domain_warp">Domain Warp</string>
</resources>

View File

@ -36,6 +36,7 @@ import androidx.compose.material.icons.outlined.FilterHdr
import androidx.compose.material.icons.outlined.FolderZip
import androidx.compose.material.icons.outlined.GifBox
import androidx.compose.material.icons.outlined.Gradient
import androidx.compose.material.icons.outlined.Grain
import androidx.compose.material.icons.outlined.Photo
import androidx.compose.material.icons.outlined.PictureAsPdf
import androidx.compose.material.icons.outlined.QrCode
@ -116,6 +117,7 @@ sealed class Screen(
is ImageSplitting -> "Image_Splitting"
is ColorTools -> "Color_Tools"
is WebpTools -> "WEBP_Tools"
is NoiseGeneration -> "Noise_Generation"
}
val icon: ImageVector?
@ -156,6 +158,7 @@ sealed class Screen(
is ImageSplitting -> Icons.Outlined.ContentCut
ColorTools -> Icons.Outlined.ColorLens
is WebpTools -> Icons.Rounded.WebpBox
NoiseGeneration -> Icons.Outlined.Grain
}
data object Settings : Screen(
@ -698,6 +701,12 @@ sealed class Screen(
}
}
data object NoiseGeneration : Screen(
id = 32,
title = R.string.noise_generation,
subtitle = R.string.noise_generation_sub
)
companion object {
val typedEntries by lazy {
listOf(
@ -750,6 +759,7 @@ sealed class Screen(
JxlTools(),
ApngTools(),
Cipher(),
NoiseGeneration,
Zip(),
WebpTools()
) to Triple(
@ -763,6 +773,6 @@ sealed class Screen(
typedEntries.flatMap { it.first }.sortedBy { it.id }
}
const val FEATURES_COUNT = 51
const val FEATURES_COUNT = 52
}
}

View File

@ -51,7 +51,7 @@ import ru.tech.imageresizershrinker.feature.filters.di.FilterInteractorDataStore
import javax.inject.Inject
import kotlin.reflect.full.primaryConstructor
internal class FavoriteFiltersInteractorImpl @Inject constructor(
internal class AndroidFavoriteFiltersInteractor @Inject constructor(
@ApplicationContext private val context: Context,
@FilterInteractorDataStore private val dataStore: DataStore<Preferences>,
private val fileController: FileController

View File

@ -21,10 +21,10 @@ import android.graphics.Bitmap
import com.awxkee.aire.Aire
import ru.tech.imageresizershrinker.core.domain.model.IntegerSize
import ru.tech.imageresizershrinker.core.domain.transformation.Transformation
import ru.tech.imageresizershrinker.core.domain.utils.NEAREST_ODD_ROUNDING
import ru.tech.imageresizershrinker.core.domain.utils.roundTo
import ru.tech.imageresizershrinker.core.filters.domain.model.Filter
import ru.tech.imageresizershrinker.core.filters.domain.model.LinearGaussianParams
import ru.tech.imageresizershrinker.core.filters.domain.model.NEAREST_ODD_ROUNDING
import ru.tech.imageresizershrinker.core.filters.domain.model.roundTo
import ru.tech.imageresizershrinker.feature.filters.data.utils.toEdgeMode
import ru.tech.imageresizershrinker.feature.filters.data.utils.toFunc

View File

@ -33,9 +33,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import ru.tech.imageresizershrinker.core.filters.domain.FavoriteFiltersInteractor
import ru.tech.imageresizershrinker.core.filters.domain.FilterProvider
import ru.tech.imageresizershrinker.feature.filters.data.AndroidFavoriteFiltersInteractor
import ru.tech.imageresizershrinker.feature.filters.data.AndroidFilterMaskApplier
import ru.tech.imageresizershrinker.feature.filters.data.AndroidFilterProvider
import ru.tech.imageresizershrinker.feature.filters.data.FavoriteFiltersInteractorImpl
import ru.tech.imageresizershrinker.feature.filters.domain.FilterMaskApplier
import javax.inject.Singleton
@ -72,7 +72,7 @@ internal interface FilterModule {
@Singleton
@Binds
fun favoriteFiltersInteractor(
interactor: FavoriteFiltersInteractorImpl
interactor: AndroidFavoriteFiltersInteractor
): FavoriteFiltersInteractor
}

1
feature/noise-generation/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,8 @@
plugins {
alias(libs.plugins.image.toolbox.library)
alias(libs.plugins.image.toolbox.feature)
alias(libs.plugins.image.toolbox.hilt)
alias(libs.plugins.image.toolbox.compose)
}
android.namespace = "ru.tech.imageresizershrinker.feature.noise_generation"

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@ -0,0 +1,75 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.noise_generation.data
import android.graphics.Bitmap
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.toArgb
import kotlinx.coroutines.withContext
import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
import ru.tech.imageresizershrinker.noise_generation.domain.NoiseGenerator
import ru.tech.imageresizershrinker.noise_generation.domain.model.NoiseParams
import javax.inject.Inject
internal class AndroidNoiseGenerator @Inject constructor(
dispatchersHolder: DispatchersHolder
) : NoiseGenerator<Bitmap>, DispatchersHolder by dispatchersHolder {
override suspend fun generateNoise(
width: Int,
height: Int,
noiseParams: NoiseParams,
onFailure: (Throwable) -> Unit
): Bitmap? = withContext(defaultDispatcher) {
val generator = with(noiseParams) {
FastNoiseLite().apply {
setSeed(seed)
setFrequency(frequency)
setNoiseType(noiseType)
setRotationType3D(rotationType3D)
setFractalType(fractalType)
setFractalOctaves(fractalOctaves)
setFractalLacunarity(fractalLacunarity)
setFractalGain(fractalGain)
setFractalWeightedStrength(fractalWeightedStrength)
setFractalPingPongStrength(fractalPingPongStrength)
setCellularDistanceFunction(cellularDistanceFunction)
setCellularReturnType(cellularReturnType)
setCellularJitter(cellularJitter)
setDomainWarpType(domainWarpType)
setDomainWarpAmp(domainWarpAmp)
}
}
runCatching {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
for (x in 0..<width) {
for (y in 0..<height) {
val noise = generator.getNoise(x.toFloat(), y.toFloat()) + 1f
val color = lerp(Color.Black, Color.White, noise / 2f)
bitmap.setPixel(x, y, color.toArgb())
}
}
bitmap
}.onFailure(onFailure).getOrNull()
}
}

View File

@ -0,0 +1,37 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.noise_generation.di
import android.graphics.Bitmap
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ru.tech.imageresizershrinker.noise_generation.data.AndroidNoiseGenerator
import ru.tech.imageresizershrinker.noise_generation.domain.NoiseGenerator
@Module
@InstallIn(SingletonComponent::class)
internal interface NoiseGenerationModule {
@Binds
fun provideGenerator(
impl: AndroidNoiseGenerator
): NoiseGenerator<Bitmap>
}

View File

@ -0,0 +1,31 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.noise_generation.domain
import ru.tech.imageresizershrinker.noise_generation.domain.model.NoiseParams
interface NoiseGenerator<Image> {
suspend fun generateNoise(
width: Int,
height: Int,
noiseParams: NoiseParams,
onFailure: (Throwable) -> Unit = {}
): Image?
}

View File

@ -0,0 +1,25 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.noise_generation.domain.model
enum class CellularDistanceFunction {
Euclidean,
EuclideanSq,
Manhattan,
Hybrid
}

View File

@ -0,0 +1,28 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.noise_generation.domain.model
enum class CellularReturnType {
CellValue,
Distance,
Distance2,
Distance2Add,
Distance2Sub,
Distance2Mul,
Distance2Div
}

View File

@ -0,0 +1,24 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.noise_generation.domain.model
enum class DomainWarpType {
OpenSimplex2,
OpenSimplex2Reduced,
BasicGrid
}

View File

@ -0,0 +1,27 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.noise_generation.domain.model
enum class FractalType {
None,
FBm,
Ridged,
PingPong,
DomainWarpProgressive,
DomainWarpIndependent
}

View File

@ -0,0 +1,58 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.noise_generation.domain.model
data class NoiseParams(
val seed: Int,
val frequency: Float,
val noiseType: NoiseType,
val rotationType3D: RotationType3D,
val fractalType: FractalType,
val fractalOctaves: Int,
val fractalLacunarity: Float,
val fractalGain: Float,
val fractalWeightedStrength: Float,
val fractalPingPongStrength: Float,
val cellularDistanceFunction: CellularDistanceFunction,
val cellularReturnType: CellularReturnType,
val cellularJitter: Float,
val domainWarpType: DomainWarpType,
val domainWarpAmp: Float
) {
companion object {
val Default by lazy {
NoiseParams(
seed = 1337,
frequency = 0.01f,
noiseType = NoiseType.OpenSimplex2,
rotationType3D = RotationType3D.None,
fractalType = FractalType.None,
fractalOctaves = 3,
fractalLacunarity = 2f,
fractalGain = 0.5f,
fractalWeightedStrength = 0f,
fractalPingPongStrength = 2f,
cellularDistanceFunction = CellularDistanceFunction.EuclideanSq,
cellularReturnType = CellularReturnType.Distance,
cellularJitter = 1f,
domainWarpType = DomainWarpType.OpenSimplex2,
domainWarpAmp = 1f
)
}
}
}

View File

@ -0,0 +1,27 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.noise_generation.domain.model
enum class NoiseType {
OpenSimplex2,
OpenSimplex2S,
Cellular,
Perlin,
ValueCubic,
Value
}

View File

@ -0,0 +1,24 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.noise_generation.domain.model
enum class RotationType3D {
None,
ImproveXYPlanes,
ImproveXZPlanes
}

View File

@ -0,0 +1,224 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.noise_generation.presentation
import android.net.Uri
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import ru.tech.imageresizershrinker.core.domain.image.model.ImageInfo
import ru.tech.imageresizershrinker.core.resources.R
import ru.tech.imageresizershrinker.core.ui.utils.confetti.LocalConfettiHostState
import ru.tech.imageresizershrinker.core.ui.utils.helper.asClip
import ru.tech.imageresizershrinker.core.ui.utils.helper.isPortraitOrientationAsState
import ru.tech.imageresizershrinker.core.ui.utils.helper.parseSaveResult
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen
import ru.tech.imageresizershrinker.core.ui.utils.state.derivedValueOf
import ru.tech.imageresizershrinker.core.ui.widget.AdaptiveLayoutScreen
import ru.tech.imageresizershrinker.core.ui.widget.buttons.BottomButtonsBlock
import ru.tech.imageresizershrinker.core.ui.widget.buttons.ShareButton
import ru.tech.imageresizershrinker.core.ui.widget.controls.ResizeImageField
import ru.tech.imageresizershrinker.core.ui.widget.controls.selection.ImageFormatSelector
import ru.tech.imageresizershrinker.core.ui.widget.controls.selection.QualitySelector
import ru.tech.imageresizershrinker.core.ui.widget.dialogs.OneTimeSaveLocationSelectionDialog
import ru.tech.imageresizershrinker.core.ui.widget.image.Picture
import ru.tech.imageresizershrinker.core.ui.widget.modifier.container
import ru.tech.imageresizershrinker.core.ui.widget.other.Loading
import ru.tech.imageresizershrinker.core.ui.widget.other.LoadingDialog
import ru.tech.imageresizershrinker.core.ui.widget.other.LocalToastHostState
import ru.tech.imageresizershrinker.core.ui.widget.other.TopAppBarEmoji
import ru.tech.imageresizershrinker.core.ui.widget.sheets.ProcessImagesPreferenceSheet
import ru.tech.imageresizershrinker.core.ui.widget.text.marquee
import ru.tech.imageresizershrinker.noise_generation.presentation.components.NoiseParamsSelection
import ru.tech.imageresizershrinker.noise_generation.presentation.viewModel.NoiseGenerationViewModel
@Composable
fun NoiseGenerationContent(
onGoBack: () -> Unit,
onNavigate: (Screen) -> Unit,
viewModel: NoiseGenerationViewModel = hiltViewModel()
) {
val toastHostState = LocalToastHostState.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
val confettiHostState = LocalConfettiHostState.current
val showConfetti: () -> Unit = {
scope.launch {
confettiHostState.showConfetti()
}
}
val isPortrait by isPortraitOrientationAsState()
val saveBitmap: (oneTimeSaveLocationUri: String?) -> Unit = {
viewModel.saveNoise(it) { saveResult ->
context.parseSaveResult(
saveResult = saveResult,
onSuccess = showConfetti,
toastHostState = toastHostState,
scope = scope
)
}
}
AdaptiveLayoutScreen(
title = {
Text(
text = stringResource(R.string.noise_generation),
textAlign = TextAlign.Center,
modifier = Modifier.marquee()
)
},
onGoBack = onGoBack,
actions = {
var editSheetData by remember {
mutableStateOf(listOf<Uri>())
}
ShareButton(
onShare = {
viewModel.shareNoise(showConfetti)
},
onCopy = { manager ->
viewModel.cacheCurrentNoise { uri ->
manager.setClip(uri.asClip(context))
showConfetti()
}
},
onEdit = {
viewModel.cacheCurrentNoise {
editSheetData = listOf(it)
}
}
)
ProcessImagesPreferenceSheet(
uris = editSheetData,
visible = editSheetData.isNotEmpty(),
onDismiss = {
if (!it) {
editSheetData = emptyList()
}
},
onNavigate = { screen ->
scope.launch {
editSheetData = emptyList()
delay(200)
onNavigate(screen)
}
}
)
},
topAppBarPersistentActions = {
TopAppBarEmoji()
},
imagePreview = {
Box(
contentAlignment = Alignment.Center
) {
Picture(
model = viewModel.previewBitmap,
modifier = Modifier
.container(MaterialTheme.shapes.medium)
.aspectRatio(viewModel.noiseSize.safeAspectRatio),
shape = MaterialTheme.shapes.medium,
contentScale = ContentScale.FillBounds
)
if (viewModel.isImageLoading) Loading()
}
},
controls = {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
ResizeImageField(
imageInfo = derivedValueOf(viewModel.noiseSize) {
ImageInfo(viewModel.noiseSize.width, viewModel.noiseSize.height)
},
originalSize = null,
onWidthChange = viewModel::setNoiseWidth,
onHeightChange = viewModel::setNoiseHeight
)
NoiseParamsSelection(
value = viewModel.noiseParams,
onValueChange = viewModel::updateParams
)
Spacer(Modifier.height(4.dp))
ImageFormatSelector(
value = viewModel.imageFormat,
onValueChange = viewModel::setImageFormat
)
QualitySelector(
quality = viewModel.quality,
imageFormat = viewModel.imageFormat,
onQualityChange = viewModel::setQuality
)
}
},
buttons = {
var showFolderSelectionDialog by rememberSaveable {
mutableStateOf(false)
}
BottomButtonsBlock(
targetState = false to isPortrait,
isSecondaryButtonVisible = false,
onSecondaryButtonClick = {},
onPrimaryButtonClick = {
saveBitmap(null)
},
onPrimaryButtonLongClick = {
showFolderSelectionDialog = true
},
actions = it
)
if (showFolderSelectionDialog) {
OneTimeSaveLocationSelectionDialog(
onDismiss = { showFolderSelectionDialog = false },
onSaveRequest = saveBitmap
)
}
},
canShowScreenData = true,
isPortrait = isPortrait
)
if (viewModel.isSaving) {
LoadingDialog(onCancelLoading = viewModel::cancelSaving)
}
}

View File

@ -0,0 +1,253 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.noise_generation.presentation.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.RampLeft
import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material.icons.outlined.Waves
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.smarttoolfactory.extendedcolors.util.roundToTwoDigits
import ru.tech.imageresizershrinker.core.domain.utils.roundTo
import ru.tech.imageresizershrinker.core.resources.R
import ru.tech.imageresizershrinker.core.ui.widget.controls.EnhancedSliderItem
import ru.tech.imageresizershrinker.core.ui.widget.controls.selection.DataSelector
import ru.tech.imageresizershrinker.noise_generation.domain.model.CellularDistanceFunction
import ru.tech.imageresizershrinker.noise_generation.domain.model.CellularReturnType
import ru.tech.imageresizershrinker.noise_generation.domain.model.DomainWarpType
import ru.tech.imageresizershrinker.noise_generation.domain.model.FractalType
import ru.tech.imageresizershrinker.noise_generation.domain.model.NoiseParams
import ru.tech.imageresizershrinker.noise_generation.domain.model.NoiseType
import kotlin.math.roundToInt
@Composable
fun NoiseParamsSelection(
value: NoiseParams,
onValueChange: (NoiseParams) -> Unit
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
EnhancedSliderItem(
value = value.seed,
icon = Icons.Outlined.SettingsEthernet,
title = stringResource(R.string.seed),
valueRange = -10000f..10000f,
internalStateTransformation = {
it.roundToInt()
},
onValueChange = {
onValueChange(value.copy(seed = it.toInt()))
},
shape = RoundedCornerShape(24.dp)
)
EnhancedSliderItem(
value = value.frequency,
icon = Icons.Outlined.Waves,
title = stringResource(R.string.frequency),
valueRange = -0.5f..0.5f,
internalStateTransformation = {
it.roundTo(3)
},
onValueChange = {
onValueChange(value.copy(frequency = it))
},
shape = RoundedCornerShape(24.dp)
)
DataSelector(
value = value.noiseType,
onValueChange = {
onValueChange(value.copy(noiseType = it))
},
entries = NoiseType.entries,
title = stringResource(R.string.noise_type),
titleIcon = null,
itemContentText = {
it.name
},
spanCount = 2,
color = Color.Unspecified
)
DataSelector(
value = value.fractalType,
onValueChange = {
onValueChange(value.copy(fractalType = it))
},
entries = FractalType.entries,
title = stringResource(R.string.fractal_type),
titleIcon = null,
itemContentText = {
it.name
},
spanCount = 2,
color = Color.Unspecified
)
AnimatedVisibility(value.fractalType != FractalType.None) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
EnhancedSliderItem(
value = value.fractalOctaves,
title = stringResource(R.string.octaves),
valueRange = 1f..5f,
steps = 3,
internalStateTransformation = {
it.roundToInt()
},
onValueChange = {
onValueChange(value.copy(fractalOctaves = it.toInt()))
},
shape = RoundedCornerShape(24.dp)
)
EnhancedSliderItem(
value = value.fractalLacunarity,
title = stringResource(R.string.lacunarity),
valueRange = -50f..50f,
internalStateTransformation = {
it.roundToTwoDigits()
},
onValueChange = {
onValueChange(value.copy(fractalLacunarity = it))
},
shape = RoundedCornerShape(24.dp)
)
EnhancedSliderItem(
value = value.fractalGain,
title = stringResource(R.string.gain),
valueRange = -10f..10f,
internalStateTransformation = {
it.roundToTwoDigits()
},
onValueChange = {
onValueChange(value.copy(fractalGain = it))
},
shape = RoundedCornerShape(24.dp)
)
EnhancedSliderItem(
value = value.fractalWeightedStrength,
title = stringResource(R.string.weighted_strength),
valueRange = -3f..3f,
internalStateTransformation = {
it.roundToTwoDigits()
},
onValueChange = {
onValueChange(value.copy(fractalWeightedStrength = it))
},
shape = RoundedCornerShape(24.dp)
)
AnimatedVisibility(value.fractalType == FractalType.PingPong) {
EnhancedSliderItem(
value = value.fractalPingPongStrength,
title = stringResource(R.string.ping_pong_strength),
valueRange = 0f..20f,
internalStateTransformation = {
it.roundToTwoDigits()
},
onValueChange = {
onValueChange(value.copy(fractalPingPongStrength = it))
},
shape = RoundedCornerShape(24.dp)
)
}
AnimatedVisibility(value.noiseType == NoiseType.Cellular) {
Column {
DataSelector(
value = value.cellularDistanceFunction,
onValueChange = {
onValueChange(value.copy(cellularDistanceFunction = it))
},
entries = CellularDistanceFunction.entries,
title = stringResource(R.string.distance_function),
titleIcon = null,
itemContentText = {
it.name
},
spanCount = 2,
color = Color.Unspecified
)
Spacer(Modifier.height(8.dp))
DataSelector(
value = value.cellularReturnType,
onValueChange = {
onValueChange(value.copy(cellularReturnType = it))
},
entries = CellularReturnType.entries,
title = stringResource(R.string.return_type),
titleIcon = null,
itemContentText = {
it.name
},
spanCount = 2,
color = Color.Unspecified
)
Spacer(Modifier.height(8.dp))
EnhancedSliderItem(
value = value.cellularJitter,
title = stringResource(R.string.jitter),
valueRange = -10f..10f,
internalStateTransformation = {
it.roundToTwoDigits()
},
onValueChange = {
onValueChange(value.copy(cellularJitter = it))
},
shape = RoundedCornerShape(24.dp)
)
}
}
}
}
DataSelector(
value = value.domainWarpType,
onValueChange = {
onValueChange(value.copy(domainWarpType = it))
},
entries = DomainWarpType.entries,
title = stringResource(R.string.domain_warp),
titleIcon = null,
itemContentText = {
it.name
},
spanCount = 2,
color = Color.Unspecified
)
EnhancedSliderItem(
value = value.domainWarpAmp,
icon = Icons.Outlined.RampLeft,
title = stringResource(R.string.amplitude),
valueRange = -2000f..2000f,
internalStateTransformation = {
it.roundToTwoDigits()
},
onValueChange = {
onValueChange(value.copy(domainWarpAmp = it))
},
shape = RoundedCornerShape(24.dp)
)
}
}

View File

@ -0,0 +1,205 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.noise_generation.presentation.viewModel
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.core.net.toUri
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
import ru.tech.imageresizershrinker.core.domain.image.ImageCompressor
import ru.tech.imageresizershrinker.core.domain.image.ImageScaler
import ru.tech.imageresizershrinker.core.domain.image.ShareProvider
import ru.tech.imageresizershrinker.core.domain.image.model.ImageFormat
import ru.tech.imageresizershrinker.core.domain.image.model.ImageInfo
import ru.tech.imageresizershrinker.core.domain.image.model.Quality
import ru.tech.imageresizershrinker.core.domain.model.IntegerSize
import ru.tech.imageresizershrinker.core.domain.saving.FileController
import ru.tech.imageresizershrinker.core.domain.saving.model.ImageSaveTarget
import ru.tech.imageresizershrinker.core.domain.saving.model.SaveResult
import ru.tech.imageresizershrinker.core.domain.utils.smartJob
import ru.tech.imageresizershrinker.core.ui.utils.BaseViewModel
import ru.tech.imageresizershrinker.core.ui.utils.state.update
import ru.tech.imageresizershrinker.noise_generation.domain.NoiseGenerator
import ru.tech.imageresizershrinker.noise_generation.domain.model.NoiseParams
import javax.inject.Inject
@HiltViewModel
class NoiseGenerationViewModel @Inject constructor(
dispatchersHolder: DispatchersHolder,
private val noiseGenerator: NoiseGenerator<Bitmap>,
private val fileController: FileController,
private val shareProvider: ShareProvider<Bitmap>,
private val imageCompressor: ImageCompressor<Bitmap>,
private val imageScaler: ImageScaler<Bitmap>
) : BaseViewModel(dispatchersHolder) {
private val _previewBitmap: MutableState<Bitmap?> = mutableStateOf(null)
val previewBitmap: Bitmap? by _previewBitmap
private val _noiseParams: MutableState<NoiseParams> = mutableStateOf(NoiseParams.Default)
val noiseParams: NoiseParams by _noiseParams
private val _noiseSize: MutableState<IntegerSize> = mutableStateOf(IntegerSize(1000, 1000))
val noiseSize: IntegerSize by _noiseSize
private val _imageFormat: MutableState<ImageFormat> = mutableStateOf(ImageFormat.Default)
val imageFormat: ImageFormat by _imageFormat
private val _quality: MutableState<Quality> = mutableStateOf(Quality.Base(100))
val quality: Quality by _quality
private val _isSaving: MutableState<Boolean> = mutableStateOf(false)
val isSaving by _isSaving
private var savingJob: Job? by smartJob {
_isSaving.update { false }
}
fun saveNoise(
oneTimeSaveLocationUri: String?,
onComplete: (result: SaveResult) -> Unit,
) {
savingJob = viewModelScope.launch(defaultDispatcher) {
_isSaving.update { true }
noiseGenerator.generateNoise(
width = noiseSize.width,
height = noiseSize.height,
noiseParams = noiseParams,
onFailure = {
onComplete(SaveResult.Error.Exception(it))
}
)?.let { bitmap ->
val imageInfo = ImageInfo(
width = bitmap.width,
height = bitmap.height,
quality = quality,
imageFormat = imageFormat
)
onComplete(
fileController.save(
saveTarget = ImageSaveTarget(
imageInfo = imageInfo,
metadata = null,
originalUri = "Noise",
sequenceNumber = null,
data = imageCompressor.compress(
image = bitmap,
imageFormat = imageFormat,
quality = quality
)
),
keepOriginalMetadata = true,
oneTimeSaveLocationUri = oneTimeSaveLocationUri
).onSuccess(::registerSave)
)
}
_isSaving.update { false }
}
}
fun cacheCurrentNoise(onComplete: (Uri) -> Unit) {
savingJob = viewModelScope.launch {
_isSaving.update { true }
noiseGenerator.generateNoise(
width = noiseSize.width,
height = noiseSize.height,
noiseParams = noiseParams
)?.let { image ->
val imageInfo = ImageInfo(
width = image.width,
height = image.height,
quality = quality,
imageFormat = imageFormat
)
shareProvider.cacheImage(
image = image,
imageInfo = imageInfo
)?.let { uri ->
onComplete(uri.toUri())
}
}
_isSaving.update { false }
}
}
fun shareNoise(onComplete: () -> Unit) {
cacheCurrentNoise { uri ->
viewModelScope.launch {
shareProvider.shareUri(
uri = uri.toString(),
onComplete = onComplete
)
}
}
}
fun cancelSaving() {
savingJob?.cancel()
savingJob = null
_isSaving.update { false }
}
fun setImageFormat(imageFormat: ImageFormat) {
_imageFormat.update { imageFormat }
}
fun setQuality(quality: Quality) {
_quality.update { quality }
}
fun updateParams(params: NoiseParams) {
_noiseParams.update { params }
updatePreview()
}
fun setNoiseWidth(width: Int) {
_noiseSize.update { it.copy(width = width.coerceAtMost(2048)) }
updatePreview()
}
fun setNoiseHeight(height: Int) {
_noiseSize.update { it.copy(height = height.coerceAtMost(2048)) }
updatePreview()
}
private fun updatePreview() {
viewModelScope.launch {
_isImageLoading.update { true }
_previewBitmap.update { null }
noiseGenerator.generateNoise(
width = noiseSize.width,
height = noiseSize.height,
noiseParams = noiseParams
).also { bitmap ->
_previewBitmap.update { bitmap }
_isImageLoading.update { false }
}
}
}
init {
updatePreview()
}
}

View File

@ -60,4 +60,5 @@ dependencies {
implementation(projects.feature.imageSplitting)
implementation(projects.feature.colorTools)
implementation(projects.feature.webpTools)
implementation(projects.feature.noiseGeneration)
}

View File

@ -73,6 +73,7 @@ import ru.tech.imageresizershrinker.feature.watermarking.presentation.Watermarki
import ru.tech.imageresizershrinker.feature.webp_tools.presentation.WebpToolsContent
import ru.tech.imageresizershrinker.feature.zip.presentation.ZipContent
import ru.tech.imageresizershrinker.image_splitting.presentation.ImageSplitterContent
import ru.tech.imageresizershrinker.noise_generation.presentation.NoiseGenerationContent
@Composable
internal fun ScreenSelector(
@ -400,6 +401,13 @@ internal fun ScreenSelector(
onNavigate = onNavigate
)
}
Screen.NoiseGeneration -> {
NoiseGenerationContent(
onGoBack = onGoBack,
onNavigate = onNavigate
)
}
}
}
ScreenBasedMaxBrightnessEnforcement(navController.currentDestination())

View File

@ -38,7 +38,7 @@ import java.io.FileWriter
import javax.inject.Inject
internal class SvgManagerImpl @Inject constructor(
internal class AndroidSvgManager @Inject constructor(
@ApplicationContext private val context: Context,
private val randomStringGenerator: RandomStringGenerator,
private val imageGetter: ImageGetter<Bitmap, ExifInterface>,

View File

@ -21,7 +21,7 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ru.tech.imageresizershrinker.feature.svg_maker.data.SvgManagerImpl
import ru.tech.imageresizershrinker.feature.svg_maker.data.AndroidSvgManager
import ru.tech.imageresizershrinker.feature.svg_maker.domain.SvgManager
import javax.inject.Singleton
@ -33,7 +33,7 @@ internal interface SvgMakerModule {
@Singleton
@Binds
fun provideSvgManager(
manager: SvgManagerImpl
manager: AndroidSvgManager
): SvgManager
}

View File

@ -32,7 +32,7 @@ import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import javax.inject.Inject
internal class ZipManagerImpl @Inject constructor(
internal class AndroidZipManager @Inject constructor(
@ApplicationContext private val context: Context,
dispatchersHolder: DispatchersHolder
) : DispatchersHolder by dispatchersHolder, ZipManager {

View File

@ -21,7 +21,7 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ru.tech.imageresizershrinker.feature.zip.data.ZipManagerImpl
import ru.tech.imageresizershrinker.feature.zip.data.AndroidZipManager
import ru.tech.imageresizershrinker.feature.zip.domain.ZipManager
import javax.inject.Singleton
@ -33,7 +33,7 @@ internal interface ZipModule {
@Singleton
@Binds
fun provideZipManager(
manager: ZipManagerImpl
manager: AndroidZipManager
): ZipManager
}

View File

@ -17,12 +17,6 @@
@file:Suppress("UnstableApiUsage")
include(":feature:color-tools")
include(":feature:image-splitting")
pluginManagement {
repositories {
includeBuild("build-logic")
@ -103,7 +97,10 @@ include(":feature:format-conversion")
include(":feature:document-scanner")
include(":feature:scan-qr-code")
include(":feature:image-stacking")
include(":feature:image-splitting")
include(":feature:color-tools")
include(":feature:webp-tools")
include(":feature:noise-generation")
include(":feature:root")