From 703cfb6a6bab6f9e6ee2d4c6f4ac866e4a46c264 Mon Sep 17 00:00:00 2001 From: T8RIN Date: Mon, 4 Mar 2024 18:51:16 +0300 Subject: [PATCH] added jxl transcoding --- .../core/data/image/AndroidShareProvider.kt | 9 +- .../core/data/image/utils/SimpleCompressor.kt | 9 +- .../core/data/saving/FileControllerImpl.kt | 5 +- .../core/domain/image/ShareProvider.kt | 4 +- .../core/domain/model/ImageFormat.kt | 4 +- .../core/domain/model/Quality.kt | 2 +- .../core/domain/saving/FileController.kt | 5 +- .../resources/src/main/res/values/strings.xml | 7 + .../core/ui/icons/material/Jpg.kt | 79 +++ .../core/ui/icons/material/Jxl.kt | 138 ++--- .../core/ui/utils/helper/ImagePicker.kt | 6 +- .../core/ui/utils/navigation/Screen.kt | 48 +- .../presentation/ApngToolsScreen.kt | 5 +- .../viewModel/ApngToolsViewModel.kt | 2 +- .../gif_tools/presentation/GifToolsScreen.kt | 5 +- .../viewModel/GifToolsViewModel.kt | 2 +- feature/jxl-tools/.gitignore | 1 + feature/jxl-tools/build.gradle.kts | 25 + .../jxl-tools/src/main/AndroidManifest.xml | 20 + .../jxl_tools/data/AndroidJxlTranscoder.kt | 59 ++ .../feature/jxl_tools/di/JxlToolsModule.kt | 38 ++ .../feature/jxl_tools/domain/JxlTranscoder.kt | 32 ++ .../jxl_tools/presentation/JxlToolsScreen.kt | 521 ++++++++++++++++++ .../viewModel/JxlToolsViewModel.kt | 254 +++++++++ feature/main/build.gradle.kts | 1 + .../presentation/components/ScreenSelector.kt | 8 + .../viewModel/PdfToolsViewModel.kt | 2 +- gradle/libs.versions.toml | 6 +- settings.gradle.kts | 1 + 29 files changed, 1200 insertions(+), 98 deletions(-) create mode 100644 core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/icons/material/Jpg.kt create mode 100644 feature/jxl-tools/.gitignore create mode 100644 feature/jxl-tools/build.gradle.kts create mode 100644 feature/jxl-tools/src/main/AndroidManifest.xml create mode 100644 feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/data/AndroidJxlTranscoder.kt create mode 100644 feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/di/JxlToolsModule.kt create mode 100644 feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/domain/JxlTranscoder.kt create mode 100644 feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/presentation/JxlToolsScreen.kt create mode 100644 feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/presentation/viewModel/JxlToolsViewModel.kt diff --git a/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/AndroidShareProvider.kt b/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/AndroidShareProvider.kt index c7c4d0342..08d248c8d 100644 --- a/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/AndroidShareProvider.kt +++ b/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/AndroidShareProvider.kt @@ -136,16 +136,21 @@ internal class AndroidShareProvider @Inject constructor( context.startActivity(shareIntent) } - override suspend fun shareImageUris( + override suspend fun shareUris( uris: List ) = shareImageUris(uris.map { it.toUri() }) private fun shareImageUris(uris: List) { + if (uris.isEmpty()) return + val sendIntent = Intent(Intent.ACTION_SEND_MULTIPLE).apply { putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris)) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - type = "image/*" + type = MimeTypeMap.getSingleton() + .getMimeTypeFromExtension( + imageGetter.getExtension(uris.first().toString()) + ) ?: "*/*" } val shareIntent = Intent.createChooser(sendIntent, context.getString(R.string.share)) shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) diff --git a/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/utils/SimpleCompressor.kt b/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/utils/SimpleCompressor.kt index 0494db1b8..38231f12f 100644 --- a/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/utils/SimpleCompressor.kt +++ b/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/utils/SimpleCompressor.kt @@ -28,6 +28,7 @@ import com.awxkee.jxlcoder.JxlCoder import com.awxkee.jxlcoder.JxlColorSpace import com.awxkee.jxlcoder.JxlCompressionOption import com.awxkee.jxlcoder.JxlDecodingSpeed +import com.awxkee.jxlcoder.JxlEffort import com.radzivon.bartoshyk.avif.coder.HeifCoder import ru.tech.imageresizershrinker.core.domain.model.ImageFormat import ru.tech.imageresizershrinker.core.domain.model.Quality @@ -344,12 +345,12 @@ internal abstract class SimpleCompressor { quality: Quality ): ByteArray { val jxlQuality = quality as? Quality.Jxl ?: Quality.Jxl(quality.qualityValue) - return JxlCoder().encode( + return JxlCoder.encode( bitmap = image, colorSpace = JxlColorSpace.RGBA, compressionOption = JxlCompressionOption.LOSSY, quality = jxlQuality.qualityValue, - effort = jxlQuality.effort, + effort = JxlEffort.entries.first { it.ordinal == jxlQuality.effort }, decodingSpeed = JxlDecodingSpeed.entries.first { it.ordinal == jxlQuality.speed } ) } @@ -363,12 +364,12 @@ internal abstract class SimpleCompressor { quality: Quality ): ByteArray { val jxlQuality = quality as? Quality.Jxl ?: Quality.Jxl(quality.qualityValue) - return JxlCoder().encode( + return JxlCoder.encode( bitmap = image, colorSpace = JxlColorSpace.RGBA, compressionOption = JxlCompressionOption.LOSSLESS, quality = 100, - effort = jxlQuality.effort, + effort = JxlEffort.entries.first { it.ordinal == jxlQuality.effort }, decodingSpeed = JxlDecodingSpeed.entries.first { it.ordinal == jxlQuality.speed } ) } diff --git a/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/saving/FileControllerImpl.kt b/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/saving/FileControllerImpl.kt index 276eef731..c2c00561a 100644 --- a/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/saving/FileControllerImpl.kt +++ b/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/saving/FileControllerImpl.kt @@ -354,7 +354,8 @@ internal class FileControllerImpl @Inject constructor( ) override fun constructImageFilename( - saveTarget: ImageSaveTarget<*> + saveTarget: ImageSaveTarget<*>, + forceNotAddSizeInFilename: Boolean ): String { val extension = saveTarget.imageInfo.imageFormat.extension @@ -382,7 +383,7 @@ internal class FileControllerImpl @Inject constructor( context.getString(R.string.original_filename) } } - if (settingsState.addSizeInFilename) prefix += wh + if (settingsState.addSizeInFilename && !forceNotAddSizeInFilename) prefix += wh val timeStamp = SimpleDateFormat( "yyyy-MM-dd_HH-mm-ss", diff --git a/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/image/ShareProvider.kt b/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/image/ShareProvider.kt index 169e70b52..59531ba92 100644 --- a/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/image/ShareProvider.kt +++ b/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/image/ShareProvider.kt @@ -56,6 +56,8 @@ interface ShareProvider { type: String? ) - suspend fun shareImageUris(uris: List) + suspend fun shareUris( + uris: List + ) } \ No newline at end of file diff --git a/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/model/ImageFormat.kt b/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/model/ImageFormat.kt index 45d0af1c0..526f5d652 100644 --- a/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/model/ImageFormat.kt +++ b/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/model/ImageFormat.kt @@ -49,7 +49,7 @@ sealed class ImageFormat( data object Jpg : ImageFormat( title = "JPG", extension = "jpg", - type = "image/jpg", + type = "image/jpeg", canChangeCompressionValue = true, canWriteExif = true ) @@ -65,7 +65,7 @@ sealed class ImageFormat( data object MozJpeg : ImageFormat( title = "MozJpeg", extension = "jpg", - type = "image/jpg", + type = "image/jpeg", canChangeCompressionValue = true, canWriteExif = true ) diff --git a/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/model/Quality.kt b/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/model/Quality.kt index 01b8d360a..6e3fdc823 100644 --- a/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/model/Quality.kt +++ b/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/model/Quality.kt @@ -25,7 +25,7 @@ sealed class Quality( data class Jxl( @IntRange(from = 0, to = 100) override val qualityValue: Int = 100, - @IntRange(from = 1, to = 9) + @IntRange(from = 1, to = 10) val effort: Int = 7, @IntRange(from = 0, to = 5) val speed: Int = 2 diff --git a/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/saving/FileController.kt b/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/saving/FileController.kt index f346aee94..c7a488979 100644 --- a/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/saving/FileController.kt +++ b/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/saving/FileController.kt @@ -29,7 +29,10 @@ interface FileController { fun getSize(uri: String): Long? - fun constructImageFilename(saveTarget: ImageSaveTarget<*>): String + fun constructImageFilename( + saveTarget: ImageSaveTarget<*>, + forceNotAddSizeInFilename: Boolean = false + ): String fun clearCache(onComplete: (String) -> Unit = {}) diff --git a/core/resources/src/main/res/values/strings.xml b/core/resources/src/main/res/values/strings.xml index f7f714078..40db3ecac 100644 --- a/core/resources/src/main/res/values/strings.xml +++ b/core/resources/src/main/res/values/strings.xml @@ -827,4 +827,11 @@ Explode Rain Corners + JXL Tools + Transcode JXL to JPEG or JPEG to JXL with no quality loss + JXL to JPEG + Perform lossless transcoding from JXL to JPEG + Perform lossless transcoding from JPEG to JXL + JPEG to JXL + Pick JXL image to start \ No newline at end of file diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/icons/material/Jpg.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/icons/material/Jpg.kt new file mode 100644 index 000000000..3c6a02846 --- /dev/null +++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/icons/material/Jpg.kt @@ -0,0 +1,79 @@ +package ru.tech.imageresizershrinker.core.ui.icons.material + +import androidx.compose.material.icons.Icons +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val Icons.Outlined.Jpg: ImageVector by lazy { + Builder( + name = "Jpg", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp, viewportWidth + = 24.0f, viewportHeight = 24.0f + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero + ) { + moveTo(8.1429f, 13.9286f) + curveToRelative(0.0f, 1.4143f, -1.1571f, 1.9286f, -2.5714f, 1.9286f) + reflectiveCurveTo(3.0f, 15.3429f, 3.0f, 13.9286f) + verticalLineTo(12.0f) + horizontalLineToRelative(1.9286f) + verticalLineToRelative(1.9286f) + horizontalLineToRelative(1.2857f) + verticalLineTo(8.1429f) + horizontalLineToRelative(1.9286f) + verticalLineTo(13.9286f) + } + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero + ) { + moveTo(21.0f, 10.0714f) + horizontalLineToRelative(-3.2143f) + verticalLineToRelative(3.8571f) + horizontalLineToRelative(1.2857f) + verticalLineTo(12.0f) + horizontalLineTo(21.0f) + verticalLineToRelative(2.1857f) + curveToRelative(0.0f, 0.9f, -0.6429f, 1.6714f, -1.6714f, 1.6714f) + horizontalLineToRelative(-1.6714f) + curveToRelative(-1.0286f, 0.0f, -1.6714f, -0.9f, -1.6714f, -1.6714f) + verticalLineToRelative(-4.2429f) + curveTo(15.8571f, 9.0429f, 16.5f, 8.1429f, 17.5286f, 8.1429f) + horizontalLineToRelative(1.6714f) + curveToRelative(1.0286f, 0.0f, 1.6714f, 0.9f, 1.6714f, 1.6714f) + verticalLineToRelative(0.2571f) + } + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero + ) { + moveTo(12.6429f, 8.1429f) + horizontalLineTo(9.4286f) + verticalLineToRelative(7.7142f) + horizontalLineToRelative(1.9285f) + verticalLineToRelative(-2.5714f) + horizontalLineToRelative(1.2858f) + curveToRelative(1.0286f, 0.0f, 1.9285f, -0.9f, 1.9285f, -1.9286f) + verticalLineToRelative(-1.2857f) + curveTo(14.5714f, 9.0428f, 13.6714f, 8.1429f, 12.6429f, 8.1429f) + close() + moveTo(12.6429f, 11.3571f) + horizontalLineToRelative(-1.2858f) + verticalLineToRelative(-1.2857f) + horizontalLineToRelative(1.2858f) + verticalLineTo(11.3571f) + close() + } + }.build() +} diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/icons/material/Jxl.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/icons/material/Jxl.kt index 8a577ddb6..74033345e 100644 --- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/icons/material/Jxl.kt +++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/icons/material/Jxl.kt @@ -30,87 +30,89 @@ import androidx.compose.ui.unit.dp val Icons.Filled.Jxl: ImageVector by lazy { Builder( - name = "Jxl", defaultWidth = 48.dp, defaultHeight = 48.dp, - viewportWidth = 891.6f, viewportHeight = 836.9f + name = "Jxl", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp, viewportWidth + = 24.0f, viewportHeight = 24.0f ).apply { path( - fill = SolidColor(Color(0xFF5fb4b1)), stroke = null, strokeLineWidth = 0.0f, + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { - moveTo(495.7f, 420.6f) - curveTo(533.0f, 348.3f, 570.2f, 276.0f, 607.4f, 203.7f) - horizontalLineTo(503.8f) - curveToRelative(-24.0f, 46.6f, -48.1f, 93.3f, -72.1f, 139.9f) - curveToRelative(-38.8f, -46.6f, -77.6f, -93.3f, -116.4f, -139.9f) - horizontalLineTo(211.7f) - lineTo(392.0f, 420.6f) - curveToRelative(-36.5f, 70.8f, -73.0f, 141.7f, -109.5f, 212.5f) - horizontalLineToRelative(103.6f) - curveToRelative(23.3f, -45.2f, 46.6f, -90.4f, 69.8f, -135.5f) - curveToRelative(37.6f, 45.2f, 75.1f, 90.4f, 112.7f, 135.5f) - horizontalLineToRelative(103.6f) - curveToRelative(-58.7f, -70.8f, -117.6f, -141.6f, -176.5f, -212.5f) + moveTo(13.1206f, 12.0514f) + curveToRelative(0.8368f, -1.7278f, 1.6713f, -3.4556f, 2.5059f, -5.1834f) + horizontalLineToRelative(-2.3242f) + curveToRelative(-0.5384f, 1.1136f, -1.0791f, 2.2297f, -1.6175f, 3.3433f) + curveTo(10.8144f, 9.0976f, 9.9439f, 7.9816f, 9.0735f, 6.868f) + horizontalLineTo(6.7493f) + lineToRelative(4.0449f, 5.1834f) + curveToRelative(-0.8188f, 1.692f, -1.6377f, 3.3863f, -2.4565f, 5.0783f) + horizontalLineToRelative(2.3242f) + curveToRelative(0.5227f, -1.0802f, 1.0454f, -2.1604f, 1.5659f, -3.2381f) + curveToRelative(0.8435f, 1.0802f, 1.6848f, 2.1604f, 2.5283f, 3.2381f) + horizontalLineToRelative(2.3242f) + curveToRelative(-1.3169f, -1.692f, -2.6382f, -3.3839f, -3.9596f, -5.0783f) + verticalLineTo(12.0514f) close() - moveTo(153.0f, 625.6f) - lineToRelative(0.3f, 2.3f) - lineToRelative(0.7f, 2.6f) - curveToRelative(3.8f, 15.1f, 8.9f, 59.5f, -12.0f, 86.3f) - curveToRelative(-6.2f, 8.0f, -14.8f, 14.5f, -25.6f, 19.3f) - lineTo(53.9f, 836.9f) - curveToRelative(36.9f, 0.0f, 69.4f, -5.8f, 96.5f, -17.4f) - curveToRelative(25.9f, -11.0f, 47.2f, -27.2f, 63.2f, -48.1f) - curveToRelative(22.2f, -28.9f, 33.9f, -66.6f, 33.8f, -109.1f) - curveToRelative(0.0f, -24.8f, -4.0f, -44.6f, -5.7f, -52.0f) - lineTo(200.8f, 337.0f) - horizontalLineToRelative(0.1f) - verticalLineToRelative(-90.2f) - horizontalLineTo(0.0f) - verticalLineTo(337.0f) - horizontalLineToRelative(109.8f) - lineTo(153.0f, 625.6f) + moveTo(5.4324f, 16.9504f) + lineToRelative(0.0067f, 0.055f) + lineToRelative(0.0157f, 0.0621f) + curveToRelative(0.0852f, 0.3609f, 0.1997f, 1.4219f, -0.2692f, 2.0624f) + curveToRelative(-0.1391f, 0.1912f, -0.332f, 0.3465f, -0.5743f, 0.4612f) + lineTo(3.2092f, 22.0f) + curveToRelative(0.8278f, 0.0f, 1.5569f, -0.1386f, 2.1649f, -0.4158f) + curveToRelative(0.581f, -0.2629f, 1.0589f, -0.65f, 1.4178f, -1.1495f) + curveToRelative(0.498f, -0.6906f, 0.7605f, -1.5916f, 0.7583f, -2.6072f) + curveToRelative(0.0f, -0.5927f, -0.0897f, -1.0658f, -0.1279f, -1.2427f) + lineToRelative(-0.9176f, -6.5312f) + horizontalLineToRelative(0.0022f) + verticalLineTo(7.898f) + horizontalLineTo(2.0f) + verticalLineToRelative(2.1556f) + horizontalLineToRelative(2.4633f) + lineTo(5.4324f, 16.9504f) close() - moveTo(738.5f, 211.2f) - lineToRelative(-0.3f, -2.3f) - lineToRelative(-0.7f, -2.6f) - curveToRelative(-3.8f, -15.1f, -8.9f, -59.5f, 12.0f, -86.3f) - curveToRelative(6.2f, -8.0f, 14.8f, -14.5f, 25.6f, -19.3f) - lineTo(837.6f, 0.0f) - curveToRelative(-36.9f, 0.0f, -69.4f, 5.8f, -96.5f, 17.4f) - curveToRelative(-25.9f, 11.0f, -47.2f, 27.2f, -63.2f, 48.1f) - curveToRelative(-22.2f, 28.9f, -33.9f, 66.6f, -33.8f, 109.1f) - curveToRelative(0.0f, 24.8f, 4.0f, 44.6f, 5.7f, 52.0f) - lineToRelative(40.9f, 273.3f) - horizontalLineToRelative(-0.1f) - verticalLineToRelative(90.2f) - horizontalLineToRelative(200.9f) - verticalLineToRelative(-90.2f) - horizontalLineTo(781.7f) - lineToRelative(-43.2f, -288.7f) + moveTo(18.5676f, 7.0472f) + lineToRelative(-0.0067f, -0.055f) + lineToRelative(-0.0157f, -0.0621f) + curveToRelative(-0.0852f, -0.3609f, -0.1997f, -1.4219f, 0.2692f, -2.0624f) + curveToRelative(0.1391f, -0.1912f, 0.332f, -0.3465f, 0.5743f, -0.4612f) + lineTo(20.7908f, 2.0f) + curveToRelative(-0.8278f, 0.0f, -1.5569f, 0.1386f, -2.1649f, 0.4158f) + curveToRelative(-0.581f, 0.2629f, -1.0589f, 0.65f, -1.4178f, 1.1495f) + curveToRelative(-0.498f, 0.6906f, -0.7605f, 1.5916f, -0.7583f, 2.6072f) + curveToRelative(0.0f, 0.5927f, 0.0897f, 1.0658f, 0.1279f, 1.2427f) + lineToRelative(0.9176f, 6.5312f) + horizontalLineToRelative(-0.0022f) + verticalLineToRelative(2.1556f) + horizontalLineTo(22.0f) + verticalLineToRelative(-2.1556f) + horizontalLineToRelative(-2.4633f) + lineToRelative(-0.9692f, -6.8993f) + verticalLineTo(7.0472f) close() } path( - fill = SolidColor(Color(0xFF5fb4b1)), stroke = null, strokeLineWidth = 0.0f, + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { - moveTo(153.0f, 625.6f) - lineToRelative(0.3f, 2.3f) - lineToRelative(0.7f, 2.6f) - curveToRelative(3.8f, 15.1f, 8.9f, 59.5f, -12.0f, 86.3f) - curveToRelative(-6.2f, 8.0f, -14.8f, 14.5f, -25.6f, 19.3f) - lineTo(53.9f, 836.9f) - curveToRelative(36.9f, 0.0f, 69.4f, -5.8f, 96.5f, -17.4f) - curveToRelative(25.9f, -11.0f, 47.2f, -27.2f, 63.2f, -48.1f) - curveToRelative(22.2f, -28.9f, 33.9f, -66.6f, 33.8f, -109.1f) - curveToRelative(0.0f, -24.8f, -4.0f, -44.6f, -5.7f, -52.0f) - lineTo(200.8f, 337.0f) - horizontalLineToRelative(0.1f) - verticalLineToRelative(-90.2f) - horizontalLineTo(0.0f) - verticalLineTo(337.0f) - horizontalLineToRelative(109.8f) - lineTo(153.0f, 625.6f) + moveTo(5.4324f, 16.9504f) + lineToRelative(0.0067f, 0.055f) + lineToRelative(0.0157f, 0.0621f) + curveToRelative(0.0852f, 0.3609f, 0.1997f, 1.4219f, -0.2692f, 2.0624f) + curveToRelative(-0.1391f, 0.1912f, -0.332f, 0.3465f, -0.5743f, 0.4612f) + lineTo(3.2092f, 22.0f) + curveToRelative(0.8278f, 0.0f, 1.5569f, -0.1386f, 2.1649f, -0.4158f) + curveToRelative(0.581f, -0.2629f, 1.0589f, -0.65f, 1.4178f, -1.1495f) + curveToRelative(0.498f, -0.6906f, 0.7605f, -1.5916f, 0.7583f, -2.6072f) + curveToRelative(0.0f, -0.5927f, -0.0897f, -1.0658f, -0.1279f, -1.2427f) + lineToRelative(-0.9176f, -6.5312f) + horizontalLineToRelative(0.0022f) + verticalLineTo(7.898f) + horizontalLineTo(2.0f) + verticalLineToRelative(2.1556f) + horizontalLineToRelative(2.4633f) + lineTo(5.4324f, 16.9504f) close() } }.build() diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/helper/ImagePicker.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/helper/ImagePicker.kt index 11887b057..27f01aaad 100644 --- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/helper/ImagePicker.kt +++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/helper/ImagePicker.kt @@ -156,8 +156,10 @@ enum class Picker { } @Composable -fun localImagePickerMode(picker: Picker = Picker.Single): ImagePickerMode { - val modeInt = LocalSettingsState.current.imagePickerModeInt +fun localImagePickerMode( + picker: Picker = Picker.Single, + modeInt: Int = LocalSettingsState.current.imagePickerModeInt +): ImagePickerMode { val multiple = picker == Picker.Multiple return remember(modeInt) { derivedStateOf { diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/Screen.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/Screen.kt index 47c081309..008705775 100644 --- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/Screen.kt +++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/Screen.kt @@ -53,6 +53,8 @@ import ru.tech.imageresizershrinker.core.ui.icons.material.ImageEdit import ru.tech.imageresizershrinker.core.ui.icons.material.ImageLimit import ru.tech.imageresizershrinker.core.ui.icons.material.ImageText import ru.tech.imageresizershrinker.core.ui.icons.material.ImageWeight +import ru.tech.imageresizershrinker.core.ui.icons.material.Jpg +import ru.tech.imageresizershrinker.core.ui.icons.material.Jxl import ru.tech.imageresizershrinker.core.ui.icons.material.MultipleImageEdit import ru.tech.imageresizershrinker.core.ui.icons.material.PaletteSwatch import ru.tech.imageresizershrinker.core.ui.icons.material.Resize @@ -428,6 +430,48 @@ sealed class Screen( subtitle = R.string.zip_sub ) + data class JxlTools( + val type: Type? = null + ) : Screen( + id = 20, + icon = Icons.Filled.Jxl, + title = R.string.jxl_tools, + subtitle = R.string.jxl_tools_sub + ) { + @Parcelize + sealed class Type( + @StringRes val title: Int, + @StringRes val subtitle: Int, + @IgnoredOnParcel val icon: ImageVector? = null + ) : Parcelable { + + data class JxlToJpeg( + val jxlImageUris: List? = null + ) : Type( + title = R.string.jxl_type_to_jpeg, + subtitle = R.string.jxl_type_to_jpeg_sub, + icon = Icons.Outlined.Jpg + ) + + data class JpegToJxl( + val jpegImageUris: List? = null + ) : Type( + title = R.string.jpeg_type_to_jxl, + subtitle = R.string.jpeg_type_to_jxl_sub, + icon = Icons.Filled.Jxl + ) + + companion object { + val entries by lazy { + listOf( + JpegToJxl(), + JxlToJpeg() + ) + } + } + } + } + companion object { val typedEntries by lazy { listOf( @@ -465,6 +509,7 @@ sealed class Screen( GifTools(), ImagePreview(), GeneratePalette(), + JxlTools(), ApngTools(), LoadNetImage(), ) to Triple( @@ -490,6 +535,7 @@ sealed class Screen( Watermarking(), GifTools(), ApngTools(), + JxlTools(), ImagePreview(), LoadNetImage(), PickColorFromImage(), @@ -501,6 +547,6 @@ sealed class Screen( LimitResize() ) } - const val featuresCount = 31 + const val featuresCount = 32 } } \ No newline at end of file diff --git a/feature/apng-tools/src/main/java/ru/tech/imageresizershrinker/feature/apng_tools/presentation/ApngToolsScreen.kt b/feature/apng-tools/src/main/java/ru/tech/imageresizershrinker/feature/apng_tools/presentation/ApngToolsScreen.kt index 9c7ce65f7..53a605922 100644 --- a/feature/apng-tools/src/main/java/ru/tech/imageresizershrinker/feature/apng_tools/presentation/ApngToolsScreen.kt +++ b/feature/apng-tools/src/main/java/ru/tech/imageresizershrinker/feature/apng_tools/presentation/ApngToolsScreen.kt @@ -529,10 +529,7 @@ fun ApngToolsScreen( }, noDataControls = { val types = remember { - listOf( - Screen.ApngTools.Type.ImageToApng(), - Screen.ApngTools.Type.ApngToImage() - ) + Screen.ApngTools.Type.entries } val preference1 = @Composable { PreferenceItem( diff --git a/feature/apng-tools/src/main/java/ru/tech/imageresizershrinker/feature/apng_tools/presentation/viewModel/ApngToolsViewModel.kt b/feature/apng-tools/src/main/java/ru/tech/imageresizershrinker/feature/apng_tools/presentation/viewModel/ApngToolsViewModel.kt index 7cd492371..cd41394b9 100644 --- a/feature/apng-tools/src/main/java/ru/tech/imageresizershrinker/feature/apng_tools/presentation/viewModel/ApngToolsViewModel.kt +++ b/feature/apng-tools/src/main/java/ru/tech/imageresizershrinker/feature/apng_tools/presentation/viewModel/ApngToolsViewModel.kt @@ -338,7 +338,7 @@ class ApngToolsViewModel @Inject constructor( val uris = convertedImageUris.filterIndexed { index, _ -> index in positions } - shareProvider.shareImageUris(uris) + shareProvider.shareUris(uris) onComplete() } diff --git a/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/presentation/GifToolsScreen.kt b/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/presentation/GifToolsScreen.kt index d91d633b8..b52a79a71 100644 --- a/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/presentation/GifToolsScreen.kt +++ b/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/presentation/GifToolsScreen.kt @@ -505,10 +505,7 @@ fun GifToolsScreen( }, noDataControls = { val types = remember { - listOf( - Screen.GifTools.Type.ImageToGif(), - Screen.GifTools.Type.GifToImage() - ) + Screen.GifTools.Type.entries } val preference1 = @Composable { PreferenceItem( diff --git a/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/presentation/viewModel/GifToolsViewModel.kt b/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/presentation/viewModel/GifToolsViewModel.kt index 11ac3a5a5..64022d128 100644 --- a/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/presentation/viewModel/GifToolsViewModel.kt +++ b/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/presentation/viewModel/GifToolsViewModel.kt @@ -347,7 +347,7 @@ class GifToolsViewModel @Inject constructor( val uris = convertedImageUris.filterIndexed { index, _ -> index in positions } - shareProvider.shareImageUris(uris) + shareProvider.shareUris(uris) onComplete() } diff --git a/feature/jxl-tools/.gitignore b/feature/jxl-tools/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/jxl-tools/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/jxl-tools/build.gradle.kts b/feature/jxl-tools/build.gradle.kts new file mode 100644 index 000000000..c404b1f20 --- /dev/null +++ b/feature/jxl-tools/build.gradle.kts @@ -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 . + */ + +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.jxl_tools" \ No newline at end of file diff --git a/feature/jxl-tools/src/main/AndroidManifest.xml b/feature/jxl-tools/src/main/AndroidManifest.xml new file mode 100644 index 000000000..0862d94c4 --- /dev/null +++ b/feature/jxl-tools/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/data/AndroidJxlTranscoder.kt b/feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/data/AndroidJxlTranscoder.kt new file mode 100644 index 000000000..3955c9540 --- /dev/null +++ b/feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/data/AndroidJxlTranscoder.kt @@ -0,0 +1,59 @@ +/* + * 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 . + */ + +package ru.tech.imageresizershrinker.feature.jxl_tools.data + +import android.content.Context +import androidx.core.net.toUri +import com.awxkee.jxlcoder.JxlCoder +import dagger.hilt.android.qualifiers.ApplicationContext +import ru.tech.imageresizershrinker.feature.jxl_tools.domain.JxlTranscoder +import javax.inject.Inject + + +internal class AndroidJxlTranscoder @Inject constructor( + @ApplicationContext private val context: Context +) : JxlTranscoder { + + override suspend fun jpegToJxl( + jpegUris: List, + onProgress: suspend (originalUri: String, data: ByteArray) -> Unit + ) { + jpegUris.forEach { uri -> + val bytes = context.contentResolver.openInputStream(uri.toUri())?.use { + it.readBytes() + } ?: return + + onProgress(uri, JxlCoder.construct(bytes)) + } + } + + override suspend fun jxlToJpeg( + jxlUris: List, + onProgress: suspend (originalUri: String, data: ByteArray) -> Unit + ) { + jxlUris.forEach { uri -> + val bytes = context.contentResolver.openInputStream(uri.toUri())?.use { + it.readBytes() + } ?: return + + onProgress(uri, JxlCoder.reconstructJPEG(bytes)) + } + } + + +} \ No newline at end of file diff --git a/feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/di/JxlToolsModule.kt b/feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/di/JxlToolsModule.kt new file mode 100644 index 000000000..ad9ad35a7 --- /dev/null +++ b/feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/di/JxlToolsModule.kt @@ -0,0 +1,38 @@ +/* + * 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 . + */ + +package ru.tech.imageresizershrinker.feature.jxl_tools.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import ru.tech.imageresizershrinker.feature.jxl_tools.data.AndroidJxlTranscoder +import ru.tech.imageresizershrinker.feature.jxl_tools.domain.JxlTranscoder +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface JxlToolsModule { + + @Binds + @Singleton + fun provideTranscoder( + converter: AndroidJxlTranscoder + ): JxlTranscoder + +} \ No newline at end of file diff --git a/feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/domain/JxlTranscoder.kt b/feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/domain/JxlTranscoder.kt new file mode 100644 index 000000000..081e6daae --- /dev/null +++ b/feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/domain/JxlTranscoder.kt @@ -0,0 +1,32 @@ +/* + * 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 . + */ + +package ru.tech.imageresizershrinker.feature.jxl_tools.domain + +interface JxlTranscoder { + + suspend fun jpegToJxl( + jpegUris: List, + onProgress: suspend (originalUri: String, data: ByteArray) -> Unit + ) + + suspend fun jxlToJpeg( + jxlUris: List, + onProgress: suspend (originalUri: String, data: ByteArray) -> Unit + ) + +} \ No newline at end of file diff --git a/feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/presentation/JxlToolsScreen.kt b/feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/presentation/JxlToolsScreen.kt new file mode 100644 index 000000000..d0dd6ee07 --- /dev/null +++ b/feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/presentation/JxlToolsScreen.kt @@ -0,0 +1,521 @@ +/* + * 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 . + */ + +package ru.tech.imageresizershrinker.feature.jxl_tools.presentation + +import android.content.Context +import android.content.res.Configuration +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ContextualFlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.InsertDriveFile +import androidx.compose.material.icons.outlined.AddPhotoAlternate +import androidx.compose.material.icons.outlined.FolderOff +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material.icons.rounded.RemoveCircleOutline +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.olshevski.navigation.reimagined.hilt.hiltViewModel +import kotlinx.coroutines.launch +import ru.tech.imageresizershrinker.core.resources.R +import ru.tech.imageresizershrinker.core.settings.presentation.LocalSettingsState +import ru.tech.imageresizershrinker.core.ui.icons.material.Jxl +import ru.tech.imageresizershrinker.core.ui.utils.confetti.LocalConfettiHostState +import ru.tech.imageresizershrinker.core.ui.utils.helper.ContextUtils.getFilename +import ru.tech.imageresizershrinker.core.ui.utils.helper.failedToSaveImages +import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen +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.EnhancedChip +import ru.tech.imageresizershrinker.core.ui.widget.buttons.EnhancedIconButton +import ru.tech.imageresizershrinker.core.ui.widget.dialogs.ExitWithoutSavingDialog +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.modifier.withModifier +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.ToastDuration +import ru.tech.imageresizershrinker.core.ui.widget.other.TopAppBarEmoji +import ru.tech.imageresizershrinker.core.ui.widget.preferences.PreferenceItem +import ru.tech.imageresizershrinker.core.ui.widget.text.AutoSizeText +import ru.tech.imageresizershrinker.core.ui.widget.text.TopAppBarTitle +import ru.tech.imageresizershrinker.core.ui.widget.utils.LocalWindowSizeClass +import ru.tech.imageresizershrinker.feature.jxl_tools.presentation.viewModel.JxlToolsViewModel + +@Composable +fun JxlToolsScreen( + typeState: Screen.JxlTools.Type?, + onGoBack: () -> Unit, + viewModel: JxlToolsViewModel = hiltViewModel() +) { + val context = LocalContext.current as ComponentActivity + val toastHostState = LocalToastHostState.current + + val scope = rememberCoroutineScope() + val confettiHostState = LocalConfettiHostState.current + val showConfetti: () -> Unit = { + scope.launch { + confettiHostState.showConfetti() + } + } + + LaunchedEffect(typeState) { + typeState?.let { viewModel.setType(it) } + } + + val settingsState = LocalSettingsState.current + + val pickJpegsLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenMultipleDocuments() + ) { list -> + list.takeIf { it.isNotEmpty() }?.let { uris -> + viewModel.setType( + Screen.JxlTools.Type.JpegToJxl(uris) + ) + } + } + + val pickJxlsLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenMultipleDocuments() + ) { list -> + list.takeIf { it.isNotEmpty() }?.filter { + it.isJxl(context) + }?.let { uris -> + if (uris.isEmpty()) { + scope.launch { + toastHostState.showToast( + message = context.getString(R.string.select_jxl_image_to_start), + icon = Icons.Filled.Jxl + ) + } + } else { + viewModel.setType( + Screen.JxlTools.Type.JxlToJpeg(uris) + ) + } + } + } + + val addJpegsLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenMultipleDocuments() + ) { list -> + list.takeIf { it.isNotEmpty() }?.let { uris -> + viewModel.setType( + (viewModel.type as? Screen.JxlTools.Type.JpegToJxl)?.let { + it.copy(it.jpegImageUris?.plus(uris)?.distinct()) + } + ) + } + } + + val addJxlsLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenMultipleDocuments() + ) { list -> + list.takeIf { it.isNotEmpty() }?.filter { + it.isJxl(context) + }?.let { uris -> + if (uris.isEmpty()) { + scope.launch { + toastHostState.showToast( + message = context.getString(R.string.select_jxl_image_to_start), + icon = Icons.Filled.Jxl + ) + } + } else { + viewModel.setType( + (viewModel.type as? Screen.JxlTools.Type.JxlToJpeg)?.let { + it.copy(it.jxlImageUris?.plus(uris)?.distinct()) + } + ) + } + } + } + + fun pickImage(type: Screen.JxlTools.Type? = null) { + runCatching { + if ((type ?: viewModel.type) is Screen.JxlTools.Type.JpegToJxl) { + pickJpegsLauncher.launch(arrayOf("image/jpeg", "image/jpg")) + } else pickJxlsLauncher.launch(arrayOf("*/*")) + }.onFailure { + scope.launch { + toastHostState.showToast( + message = context.getString(R.string.activate_files), + icon = Icons.Outlined.FolderOff, + duration = ToastDuration.Long + ) + } + } + } + + val addImages: () -> Unit = { + runCatching { + if ((viewModel.type) is Screen.JxlTools.Type.JpegToJxl) { + addJpegsLauncher.launch(arrayOf("image/jpeg", "image/jpg")) + } else addJxlsLauncher.launch(arrayOf("*/*")) + }.onFailure { + scope.launch { + toastHostState.showToast( + message = context.getString(R.string.activate_files), + icon = Icons.Outlined.FolderOff, + duration = ToastDuration.Long + ) + } + } + } + + var showExitDialog by rememberSaveable { mutableStateOf(false) } + + val onBack = { + if (viewModel.type != null) showExitDialog = true + else onGoBack() + } + + val isPortrait = + LocalConfiguration.current.orientation != Configuration.ORIENTATION_LANDSCAPE || LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact + + val uris = when (val type = viewModel.type) { + is Screen.JxlTools.Type.JpegToJxl -> type.jpegImageUris + is Screen.JxlTools.Type.JxlToJpeg -> type.jxlImageUris + null -> null + } ?: emptyList() + + AdaptiveLayoutScreen( + title = { + TopAppBarTitle( + title = when (viewModel.type) { + is Screen.JxlTools.Type.JpegToJxl -> { + stringResource(R.string.jpeg_type_to_jxl) + } + + is Screen.JxlTools.Type.JxlToJpeg -> { + stringResource(R.string.jxl_type_to_jpeg) + } + + null -> stringResource(R.string.jxl_tools) + }, + input = viewModel.type, + isLoading = viewModel.isLoading, + size = null + ) + }, + onGoBack = onBack, + topAppBarPersistentActions = { + if (viewModel.type == null) TopAppBarEmoji() + }, + actions = { + EnhancedIconButton( + containerColor = Color.Transparent, + contentColor = LocalContentColor.current, + enableAutoShadowAndBorder = false, + onClick = { + viewModel.performSharing(showConfetti) + }, + enabled = !viewModel.isLoading && viewModel.type != null + ) { + Icon(Icons.Outlined.Share, null) + } + }, + imagePreview = {}, + placeImagePreview = false, + showImagePreviewAsStickyHeader = false, + autoClearFocus = false, + controls = { + Spacer(modifier = Modifier.height(24.dp)) + BoxWithConstraints { + val size = uris.size + 1f + + val count = if (isPortrait) { + size.coerceAtLeast(2f).coerceAtMost(3f) + } else { + size.coerceAtLeast(2f).coerceAtMost(8f) + } + + val width = maxWidth / count - 2.dp * (count - 1) + + ContextualFlowRow( + itemCount = uris.size + 1, + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { index -> + val uri = uris.getOrNull(index) + if (uri != null) { + Box( + modifier = Modifier.container( + shape = RoundedCornerShape(4.dp), + resultPadding = 0.dp, + color = MaterialTheme.colorScheme.surfaceContainerHighest + ) + ) { + Picture( + model = uri, + error = { + Box { + Icon( + imageVector = Icons.AutoMirrored.Outlined.InsertDriveFile, + contentDescription = null, + modifier = Modifier + .size(width / 3f) + .align(Alignment.Center), + tint = MaterialTheme.colorScheme.primary + ) + } + }, + modifier = Modifier + .width(width) + .aspectRatio(1f) + ) + Box( + modifier = Modifier + .matchParentSize() + .background(Color.Black.copy(0.5f)), + ) { + Text( + text = (index + 1).toString(), + color = Color.White, + fontSize = 24.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier + .padding(8.dp) + .align(Alignment.TopStart) + ) + Icon( + imageVector = Icons.Rounded.RemoveCircleOutline, + contentDescription = null, + modifier = Modifier + .padding(4.dp) + .clip(CircleShape) + .background(Color.Black.copy(0.2f)) + .clickable { + viewModel.removeUri(uri) + } + .padding(4.dp) + .align(Alignment.TopEnd), + tint = Color.White.copy(0.7f), + ) + val filename by remember(uri) { + derivedStateOf { + context.getFilename(uri) + } + } + filename?.let { + AutoSizeText( + text = it, + style = LocalTextStyle.current.copy( + color = Color.White, + fontSize = 11.sp, + lineHeight = 12.sp, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.End + ), + maxLines = 3, + modifier = Modifier + .padding(8.dp) + .align(Alignment.BottomEnd) + ) + } + } + } + } else { + Box( + modifier = Modifier + .container( + shape = RoundedCornerShape(4.dp), + resultPadding = 0.dp, + color = MaterialTheme.colorScheme.surfaceContainerHigh + ) + .width(width) + .aspectRatio(1f) + .clickable(onClick = addImages), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Outlined.AddPhotoAlternate, + contentDescription = null, + modifier = Modifier.size(width / 3f) + ) + } + } + } + } + }, + contentPadding = animateDpAsState( + if (viewModel.type == null) 12.dp + else 20.dp + ).value, + buttons = { + BottomButtonsBlock( + targetState = (viewModel.type == null) to isPortrait, + onSecondaryButtonClick = { pickImage() }, + isPrimaryButtonVisible = viewModel.type != null, + onPrimaryButtonClick = { + viewModel.save { results, path -> + context.failedToSaveImages( + scope = scope, + results = results, + toastHostState = toastHostState, + savingPathString = path, + isOverwritten = settingsState.overwriteFiles, + showConfetti = showConfetti + ) + } + }, + actions = { + EnhancedChip( + selected = true, + onClick = null, + selectedColor = MaterialTheme.colorScheme.secondaryContainer, + modifier = Modifier.padding(8.dp) + ) { + Text(uris.size.toString()) + } + }, + showNullDataButtonAsContainer = true + ) + }, + noDataControls = { + val types = remember { + Screen.JxlTools.Type.entries + } + val preference1 = @Composable { + PreferenceItem( + title = stringResource(types[0].title), + subtitle = stringResource(types[0].subtitle), + startIcon = types[0].icon, + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp), + onClick = { + pickImage(types[0]) + } + ) + } + val preference2 = @Composable { + PreferenceItem( + title = stringResource(types[1].title), + subtitle = stringResource(types[1].subtitle), + startIcon = types[1].icon, + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp), + onClick = { + pickImage(types[1]) + } + ) + } + if (isPortrait) { + Column { + preference1() + Spacer(modifier = Modifier.height(8.dp)) + preference2() + } + } else { + val direction = LocalLayoutDirection.current + Row( + modifier = Modifier.padding( + WindowInsets.displayCutout.asPaddingValues().let { + PaddingValues( + start = it.calculateStartPadding(direction), + end = it.calculateEndPadding(direction) + ) + } + ) + ) { + preference1.withModifier(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.width(8.dp)) + preference2.withModifier(modifier = Modifier.weight(1f)) + } + } + }, + isPortrait = isPortrait, + canShowScreenData = viewModel.type != null + ) + + if (viewModel.isSaving) { + if (viewModel.left != -1) { + LoadingDialog( + done = viewModel.done, + left = viewModel.left, + onCancelLoading = viewModel::cancelSaving + ) + } else { + LoadingDialog( + onCancelLoading = viewModel::cancelSaving + ) + } + } + + ExitWithoutSavingDialog( + onExit = viewModel::clearAll, + onDismiss = { showExitDialog = false }, + visible = showExitDialog + ) +} + +private fun Uri.isJxl(context: Context): Boolean { + return context.getFilename(this).toString().endsWith(".jxl") + .or(context.contentResolver.getType(this)?.contains("jxl") == true) +} diff --git a/feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/presentation/viewModel/JxlToolsViewModel.kt b/feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/presentation/viewModel/JxlToolsViewModel.kt new file mode 100644 index 000000000..8e912ee96 --- /dev/null +++ b/feature/jxl-tools/src/main/java/ru/tech/imageresizershrinker/feature/jxl_tools/presentation/viewModel/JxlToolsViewModel.kt @@ -0,0 +1,254 @@ +/* + * 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 . + */ + +package ru.tech.imageresizershrinker.feature.jxl_tools.presentation.viewModel + +import android.graphics.Bitmap +import android.net.Uri +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.exifinterface.media.ExifInterface +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import ru.tech.imageresizershrinker.core.domain.image.ShareProvider +import ru.tech.imageresizershrinker.core.domain.model.ImageFormat +import ru.tech.imageresizershrinker.core.domain.model.ImageInfo +import ru.tech.imageresizershrinker.core.domain.saving.FileController +import ru.tech.imageresizershrinker.core.domain.saving.SaveResult +import ru.tech.imageresizershrinker.core.domain.saving.model.FileSaveTarget +import ru.tech.imageresizershrinker.core.domain.saving.model.ImageSaveTarget +import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen +import ru.tech.imageresizershrinker.core.ui.utils.state.update +import ru.tech.imageresizershrinker.feature.jxl_tools.domain.JxlTranscoder +import javax.inject.Inject + +@HiltViewModel +class JxlToolsViewModel @Inject constructor( + private val jxlTranscoder: JxlTranscoder, + private val fileController: FileController, + private val shareProvider: ShareProvider +) : ViewModel() { + + private val _type: MutableState = mutableStateOf(null) + val type by _type + + private val _isLoading: MutableState = mutableStateOf(false) + val isLoading by _isLoading + + private val _done: MutableState = mutableIntStateOf(0) + val done by _done + + private val _left: MutableState = mutableIntStateOf(-1) + val left by _left + + private val _isSaving: MutableState = mutableStateOf(false) + val isSaving: Boolean by _isSaving + + fun setType(type: Screen.JxlTools.Type?) { + _type.update { type } + } + + private var savingJob: Job? = null + + fun save( + onResult: (List, String) -> Unit + ) { + _isSaving.value = false + savingJob?.cancel() + savingJob = viewModelScope.launch(Dispatchers.IO) { + _isSaving.value = true + _left.value = 1 + _done.value = 0 + when (val type = _type.value) { + is Screen.JxlTools.Type.JpegToJxl -> { + val results = mutableListOf() + val jpegUris = type.jpegImageUris?.map { it.toString() } + ?: emptyList() + _left.value = jpegUris.size + jxlTranscoder.jpegToJxl(jpegUris) { uri, jxlBytes -> + results.add( + fileController.save( + saveTarget = FileSaveTarget( + originalUri = uri, + filename = fileController.constructImageFilename( + ImageSaveTarget( + imageInfo = ImageInfo( + imageFormat = ImageFormat.Jxl.Lossless, + originalUri = uri + ), + originalUri = uri, + sequenceNumber = done + 1, + metadata = null, + data = jxlBytes + ), + forceNotAddSizeInFilename = true + ), + data = jxlBytes, + imageFormat = ImageFormat.Jxl.Lossless + ), + keepOriginalMetadata = true + ) + ) + _done.update { it + 1 } + } + + onResult(results, fileController.savingPath) + } + + is Screen.JxlTools.Type.JxlToJpeg -> { + val results = mutableListOf() + val jxlUris = type.jxlImageUris?.map { it.toString() } + ?: emptyList() + _left.value = jxlUris.size + jxlTranscoder.jxlToJpeg(jxlUris) { uri, jpegBytes -> + results.add( + fileController.save( + saveTarget = FileSaveTarget( + originalUri = uri, + filename = fileController.constructImageFilename( + ImageSaveTarget( + imageInfo = ImageInfo( + imageFormat = ImageFormat.Jpg, + originalUri = uri + ), + originalUri = uri, + sequenceNumber = done + 1, + metadata = null, + data = jpegBytes + ), + forceNotAddSizeInFilename = true + ), + data = jpegBytes, + imageFormat = ImageFormat.Jpg + ), + keepOriginalMetadata = true + ) + ) + _done.update { it + 1 } + } + + onResult(results, fileController.savingPath) + } + + null -> Unit + } + _isSaving.value = false + } + } + + fun cancelSaving() { + savingJob?.cancel() + savingJob = null + _isSaving.value = false + } + + fun performSharing(onComplete: () -> Unit) { + _isSaving.value = false + savingJob?.cancel() + savingJob = viewModelScope.launch(Dispatchers.IO) { + _isSaving.value = true + _left.value = 1 + _done.value = 0 + when (val type = _type.value) { + is Screen.JxlTools.Type.JpegToJxl -> { + val jpegUris = type.jpegImageUris?.map { it.toString() } ?: emptyList() + _left.value = jpegUris.size + + val results = mutableListOf() + jxlTranscoder.jpegToJxl(jpegUris) { uri, jxlBytes -> + results.add( + shareProvider.cacheByteArray( + byteArray = jxlBytes, + filename = fileController.constructImageFilename( + ImageSaveTarget( + imageInfo = ImageInfo( + imageFormat = ImageFormat.Jxl.Lossless, + originalUri = uri + ), + originalUri = uri, + sequenceNumber = done + 1, + metadata = null, + data = jxlBytes + ), + forceNotAddSizeInFilename = true + ) + ) + ) + _done.update { it + 1 } + } + + shareProvider.shareUris(results.filterNotNull()) + onComplete() + } + + is Screen.JxlTools.Type.JxlToJpeg -> { + val jxlUris = type.jxlImageUris?.map { it.toString() } ?: emptyList() + _left.value = jxlUris.size + + val results = mutableListOf() + jxlTranscoder.jxlToJpeg(jxlUris) { uri, jpegBytes -> + results.add( + shareProvider.cacheByteArray( + byteArray = jpegBytes, + filename = fileController.constructImageFilename( + saveTarget = ImageSaveTarget( + imageInfo = ImageInfo( + imageFormat = ImageFormat.Jpg, + originalUri = uri + ), + originalUri = uri, + sequenceNumber = done + 1, + metadata = null, + data = jpegBytes + ), + forceNotAddSizeInFilename = true + ) + ) + ) + _done.update { it + 1 } + } + + shareProvider.shareUris(results.filterNotNull()) + onComplete() + } + + null -> Unit + } + + _isSaving.value = false + } + } + + fun clearAll() = setType(null) + + fun removeUri(uri: Uri) { + _type.update { + when (val type = it) { + is Screen.JxlTools.Type.JpegToJxl -> type.copy(type.jpegImageUris?.minus(uri)) + is Screen.JxlTools.Type.JxlToJpeg -> type.copy(type.jxlImageUris?.minus(uri)) + null -> null + } + } + } + +} \ No newline at end of file diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index 321cc3bee..14f477f7e 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -48,4 +48,5 @@ dependencies { implementation(projects.feature.gifTools) implementation(projects.feature.apngTools) implementation(projects.feature.zip) + implementation(projects.feature.jxlTools) } \ No newline at end of file diff --git a/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/ScreenSelector.kt b/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/ScreenSelector.kt index 436467942..511e6ab2c 100644 --- a/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/ScreenSelector.kt +++ b/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/ScreenSelector.kt @@ -53,6 +53,7 @@ import ru.tech.imageresizershrinker.feature.gif_tools.presentation.GifToolsScree import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.GradientMakerScreen import ru.tech.imageresizershrinker.feature.image_preview.presentation.ImagePreviewScreen import ru.tech.imageresizershrinker.feature.image_stitch.presentation.ImageStitchingScreen +import ru.tech.imageresizershrinker.feature.jxl_tools.presentation.JxlToolsScreen import ru.tech.imageresizershrinker.feature.limits_resize.presentation.LimitsResizeScreen import ru.tech.imageresizershrinker.feature.load_net_image.presentation.LoadNetImageScreen import ru.tech.imageresizershrinker.feature.main.presentation.MainScreen @@ -290,6 +291,13 @@ fun ScreenSelector( onGoBack = onGoBack ) } + + is Screen.JxlTools -> { + JxlToolsScreen( + typeState = screen.type, + onGoBack = onGoBack + ) + } } } val currentScreen by remember(navController.backstack.entries) { diff --git a/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/presentation/viewModel/PdfToolsViewModel.kt b/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/presentation/viewModel/PdfToolsViewModel.kt index 02d746fe0..8da285aad 100644 --- a/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/presentation/viewModel/PdfToolsViewModel.kt +++ b/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/presentation/viewModel/PdfToolsViewModel.kt @@ -333,7 +333,7 @@ class PdfToolsViewModel @Inject constructor( }, onComplete = { _isSaving.value = false - shareProvider.shareImageUris(uris.filterNotNull()) + shareProvider.shareUris(uris.filterNotNull()) onComplete() } ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b3d6591d4..dce723f43 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,9 +11,9 @@ compose-compiler = "1.5.10" avifCoderCoil = "1.6.4" avifCoder = "1.6.4" -aire = "0.9.84" -jxlCoderCoil = "1.11.1" -jxlCoder = "1.11.1" +aire = "0.9.85" +jxlCoderCoil = "2.0.0" +jxlCoder = "2.0.0" tesseract = "4.3.0" composeVersion = "1.7.0-alpha03" diff --git a/settings.gradle.kts b/settings.gradle.kts index 508d20b82..7709cd8cd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -65,6 +65,7 @@ include(":feature:gradient-maker") include(":feature:gif-tools") include(":feature:apng-tools") include(":feature:zip") +include(":feature:jxl-tools") include(":core:settings") include(":core:resources")