added jxl transcoding

This commit is contained in:
T8RIN
2024-03-04 18:51:16 +03:00
parent 215c783806
commit 703cfb6a6b
29 changed files with 1200 additions and 98 deletions

View File

@ -136,16 +136,21 @@ internal class AndroidShareProvider @Inject constructor(
context.startActivity(shareIntent)
}
override suspend fun shareImageUris(
override suspend fun shareUris(
uris: List<String>
) = shareImageUris(uris.map { it.toUri() })
private fun shareImageUris(uris: List<Uri>) {
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)

View File

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

View File

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

View File

@ -56,6 +56,8 @@ interface ShareProvider<I> {
type: String?
)
suspend fun shareImageUris(uris: List<String>)
suspend fun shareUris(
uris: List<String>
)
}

View File

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

View File

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

View File

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

View File

@ -827,4 +827,11 @@
<string name="explode">Explode</string>
<string name="rain">Rain</string>
<string name="corners">Corners</string>
<string name="jxl_tools">JXL Tools</string>
<string name="jxl_tools_sub">Transcode JXL to JPEG or JPEG to JXL with no quality loss</string>
<string name="jxl_type_to_jpeg">JXL to JPEG</string>
<string name="jxl_type_to_jpeg_sub">Perform lossless transcoding from JXL to JPEG</string>
<string name="jpeg_type_to_jxl_sub">Perform lossless transcoding from JPEG to JXL</string>
<string name="jpeg_type_to_jxl">JPEG to JXL</string>
<string name="select_jxl_image_to_start">Pick JXL image to start</string>
</resources>

View File

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

View File

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

View File

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

View File

@ -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<Uri>? = 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<Uri>? = 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
}
}

View File

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

View File

@ -338,7 +338,7 @@ class ApngToolsViewModel @Inject constructor(
val uris = convertedImageUris.filterIndexed { index, _ ->
index in positions
}
shareProvider.shareImageUris(uris)
shareProvider.shareUris(uris)
onComplete()
}

View File

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

View File

@ -347,7 +347,7 @@ class GifToolsViewModel @Inject constructor(
val uris = convertedImageUris.filterIndexed { index, _ ->
index in positions
}
shareProvider.shareImageUris(uris)
shareProvider.shareUris(uris)
onComplete()
}

1
feature/jxl-tools/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

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>.
*/
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"

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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>.
-->
<manifest>
</manifest>

View File

@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
*/
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<String>,
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<String>,
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))
}
}
}

View File

@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
*/
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
}

View File

@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.feature.jxl_tools.domain
interface JxlTranscoder {
suspend fun jpegToJxl(
jpegUris: List<String>,
onProgress: suspend (originalUri: String, data: ByteArray) -> Unit
)
suspend fun jxlToJpeg(
jxlUris: List<String>,
onProgress: suspend (originalUri: String, data: ByteArray) -> Unit
)
}

View File

@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
*/
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)
}

View File

@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
*/
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<Bitmap>
) : ViewModel() {
private val _type: MutableState<Screen.JxlTools.Type?> = mutableStateOf(null)
val type by _type
private val _isLoading: MutableState<Boolean> = mutableStateOf(false)
val isLoading by _isLoading
private val _done: MutableState<Int> = mutableIntStateOf(0)
val done by _done
private val _left: MutableState<Int> = mutableIntStateOf(-1)
val left by _left
private val _isSaving: MutableState<Boolean> = 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<SaveResult>, 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<SaveResult>()
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<ExifInterface>(
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<SaveResult>()
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<ExifInterface>(
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<String?>()
jxlTranscoder.jpegToJxl(jpegUris) { uri, jxlBytes ->
results.add(
shareProvider.cacheByteArray(
byteArray = jxlBytes,
filename = fileController.constructImageFilename(
ImageSaveTarget<ExifInterface>(
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<String?>()
jxlTranscoder.jxlToJpeg(jxlUris) { uri, jpegBytes ->
results.add(
shareProvider.cacheByteArray(
byteArray = jpegBytes,
filename = fileController.constructImageFilename(
saveTarget = ImageSaveTarget<ExifInterface>(
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
}
}
}
}

View File

@ -48,4 +48,5 @@ dependencies {
implementation(projects.feature.gifTools)
implementation(projects.feature.apngTools)
implementation(projects.feature.zip)
implementation(projects.feature.jxlTools)
}

View File

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

View File

@ -333,7 +333,7 @@ class PdfToolsViewModel @Inject constructor(
},
onComplete = {
_isSaving.value = false
shareProvider.shareImageUris(uris.filterNotNull())
shareProvider.shareUris(uris.filterNotNull())
onComplete()
}
)

View File

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

View File

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