Skip to content

Commit

Permalink
Added ability to use file checksum as filename by #1555
Browse files Browse the repository at this point in the history
  • Loading branch information
T8RIN committed Dec 30, 2024
1 parent c34da1c commit bc0d04a
Show file tree
Hide file tree
Showing 23 changed files with 384 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import ru.tech.imageresizershrinker.core.data.utils.computeFromByteArray
import ru.tech.imageresizershrinker.core.data.utils.getFilename
import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
import ru.tech.imageresizershrinker.core.domain.saving.FilenameCreator
Expand Down Expand Up @@ -64,6 +65,13 @@ internal class AndroidFilenameCreator @Inject constructor(
): String {
val extension = saveTarget.extension

val checksumType = settingsState.checksumTypeForFilename
if (checksumType != null && saveTarget.data.isNotEmpty()) {
val name = checksumType.computeFromByteArray(saveTarget.data)

if (name.isNotEmpty()) return "$name.$extension"
}

if (settingsState.randomizeFilename) return "${randomStringGenerator.generate(32)}.$extension"

val wh =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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.core.data.utils

import ru.tech.imageresizershrinker.core.domain.model.ChecksumType
import java.io.File
import java.io.InputStream
import java.security.MessageDigest

private const val STREAM_BUFFER_LENGTH = 1024

internal fun ChecksumType.computeFromFile(
filePath: String
): String = computeFromFile(File(filePath))

internal fun ChecksumType.computeFromFile(
file: File
): String = file.inputStream().use {
computeFromInputStream(it)
}

internal fun ChecksumType.computeFromByteArray(
byteArray: ByteArray
): String = byteArray.inputStream().use {
computeFromInputStream(it)
}

internal fun ChecksumType.computeFromInputStream(
inputStream: InputStream
): String {
val byteArray = updateDigest(inputStream).digest()
val hexCode = encodeHex(byteArray, true)
return String(hexCode)
}

/**
* Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
* The returned array will be double the length of the passed array, as it takes two characters to represent any
* given byte.
*
* @param data a byte[] to convert to Hex characters
* @param toLowerCase `true` converts to lowercase, `false` to uppercase
* @return A char[] containing hexadecimal characters in the selected case
*/
internal fun encodeHex(
data: ByteArray,
toLowerCase: Boolean
): CharArray = encodeHex(
data = data,
toDigits = if (toLowerCase) {
DIGITS_LOWER
} else {
DIGITS_UPPER
}
)

/**
* Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
* The returned array will be double the length of the passed array, as it takes two characters to represent any
* given byte.
*
* @param data a byte[] to convert to Hex characters
* @param toDigits the output alphabet (must contain at least 16 chars)
* @return A char[] containing the appropriate characters from the alphabet
* For best results, this should be either upper- or lower-case hex.
*/
internal fun encodeHex(
data: ByteArray,
toDigits: CharArray
): CharArray {
val l = data.size
val out = CharArray(l shl 1)
// two characters form the hex value.
var i = 0
var j = 0
while (i < l) {
out[j++] = toDigits[0xF0 and data[i].toInt() ushr 4]
out[j++] = toDigits[0x0F and data[i].toInt()]
i++
}
return out
}

/**
* Reads through an InputStream and updates the digest for the data
*
* @param ChecksumType The ChecksumType to use (e.g. MD5)
* @param data Data to digest
* @return the digest
*/
private fun ChecksumType.updateDigest(
data: InputStream
): MessageDigest {
val digest = toDigest()

val buffer = ByteArray(STREAM_BUFFER_LENGTH)
var read = data.read(buffer, 0, STREAM_BUFFER_LENGTH)
while (read > -1) {
digest.update(buffer, 0, read)
read = data.read(buffer, 0, STREAM_BUFFER_LENGTH)
}
return digest
}

/**
* Used to build output as Hex
*/
private val DIGITS_LOWER =
charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')

/**
* Used to build output as Hex
*/
private val DIGITS_UPPER =
charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')


private fun ChecksumType.toDigest(): MessageDigest = MessageDigest.getInstance(digest)
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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.core.domain.model

class ChecksumType private constructor(
val digest: String
) {
override fun toString(): String = "ChecksumType($digest)"

companion object {
val MD2 = ChecksumType("MD2")
val MD5 = ChecksumType("MD5")
val SHA_1 = ChecksumType("SHA-1")
val SHA_224 = ChecksumType("SHA-224")
val SHA_256 = ChecksumType("SHA-256")
val SHA_384 = ChecksumType("SHA-384")
val SHA_512 = ChecksumType("SHA-512")
// Not supported by Android
// val SHA_512_224 = ChecksumType("SHA-512/224")
// val SHA_512_256 = ChecksumType("SHA-512/256")
// val SHA3_224 = ChecksumType("SHA3-224")
// val SHA3_256 = ChecksumType("SHA3-256")
// val SHA3_384 = ChecksumType("SHA3-384")
// val SHA3_512 = ChecksumType("SHA3-512")

val entries: List<ChecksumType> by lazy {
listOf(
MD2,
MD5,
SHA_1,
SHA_224,
SHA_256,
SHA_384,
SHA_512,
// SHA_512_224,
// SHA_512_256,
// SHA3_224,
// SHA3_256,
// SHA3_384,
// SHA3_512,
)
}

fun fromString(
digest: String?
): ChecksumType? = digest?.let {
entries.find {
it.digest == digest
}
}
}
}
2 changes: 2 additions & 0 deletions core/resources/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1529,4 +1529,6 @@
<string name="outline_color">Outline Color</string>
<string name="outline_size">Outline Size</string>
<string name="rotation">Rotation</string>
<string name="checksum_as_filename">Checksum As Filename</string>
<string name="checksum_as_filename_sub">Output images will have name corresponding to their data checksum</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package ru.tech.imageresizershrinker.core.settings.domain

import ru.tech.imageresizershrinker.core.domain.image.model.ImageScaleMode
import ru.tech.imageresizershrinker.core.domain.image.model.ResizeType
import ru.tech.imageresizershrinker.core.domain.model.ChecksumType
import ru.tech.imageresizershrinker.core.domain.model.ColorModel
import ru.tech.imageresizershrinker.core.domain.model.PerformanceClass
import ru.tech.imageresizershrinker.core.domain.model.SystemBarsVisibility
Expand Down Expand Up @@ -211,6 +212,8 @@ interface SettingsInteractor : SimpleSettingsInteractor {
suspend fun toggleIsCenterAlignDialogButtons()

suspend fun setFastSettingsSide(side: FastSettingsSide)

suspend fun setChecksumTypeForFilename(type: ChecksumType?)
}

fun SettingsInteractor.toSimpleSettingsInteractor(): SimpleSettingsInteractor =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import ru.tech.imageresizershrinker.core.domain.image.model.ImageScaleMode
import ru.tech.imageresizershrinker.core.domain.image.model.Preset
import ru.tech.imageresizershrinker.core.domain.image.model.Preset.Percentage
import ru.tech.imageresizershrinker.core.domain.image.model.ResizeType
import ru.tech.imageresizershrinker.core.domain.model.ChecksumType
import ru.tech.imageresizershrinker.core.domain.model.ColorModel
import ru.tech.imageresizershrinker.core.domain.model.DomainAspectRatio
import ru.tech.imageresizershrinker.core.domain.model.SystemBarsVisibility
Expand Down Expand Up @@ -111,7 +112,8 @@ data class SettingsState(
val sliderType: SliderType,
val isCenterAlignDialogButtons: Boolean,
val fastSettingsSide: FastSettingsSide,
val settingGroupsInitialVisibility: Map<Int, Boolean>
val settingGroupsInitialVisibility: Map<Int, Boolean>,
val checksumTypeForFilename: ChecksumType?
) {

companion object {
Expand Down Expand Up @@ -202,7 +204,8 @@ data class SettingsState(
sliderType = SliderType.Fancy,
isCenterAlignDialogButtons = false,
fastSettingsSide = FastSettingsSide.CenterStart,
settingGroupsInitialVisibility = emptyMap()
settingGroupsInitialVisibility = emptyMap(),
checksumTypeForFilename = null
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -472,4 +472,9 @@ sealed class Setting(
title = R.string.fast_settings_side,
subtitle = R.string.fast_settings_side_sub
)

data object ChecksumAsFilename : Setting(
title = R.string.checksum_as_filename,
subtitle = R.string.checksum_as_filename_sub
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ sealed class SettingsGroup(
Setting.AddTimestampToFilename,
Setting.UseFormattedFilenameTimestamp,
Setting.OverwriteFiles,
Setting.ChecksumAsFilename,
Setting.RandomizeFilename
),
initialState = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import kotlinx.coroutines.launch
import ru.tech.imageresizershrinker.core.domain.image.model.ImageScaleMode
import ru.tech.imageresizershrinker.core.domain.image.model.Preset
import ru.tech.imageresizershrinker.core.domain.image.model.ResizeType
import ru.tech.imageresizershrinker.core.domain.model.ChecksumType
import ru.tech.imageresizershrinker.core.domain.model.DomainAspectRatio
import ru.tech.imageresizershrinker.core.domain.model.SystemBarsVisibility
import ru.tech.imageresizershrinker.core.resources.R
Expand Down Expand Up @@ -140,7 +141,8 @@ data class UiSettingsState(
val sliderType: SliderType,
val isCenterAlignDialogButtons: Boolean,
val fastSettingsSide: FastSettingsSide,
val settingGroupsInitialVisibility: Map<Int, Boolean>
val settingGroupsInitialVisibility: Map<Int, Boolean>,
val checksumTypeForFilename: ChecksumType?
)

fun UiSettingsState.isFirstLaunch(
Expand Down Expand Up @@ -359,7 +361,8 @@ fun SettingsState.toUiState(
sliderType = sliderType,
isCenterAlignDialogButtons = isCenterAlignDialogButtons,
fastSettingsSide = fastSettingsSide,
settingGroupsInitialVisibility = settingGroupsInitialVisibility
settingGroupsInitialVisibility = settingGroupsInitialVisibility,
checksumTypeForFilename = checksumTypeForFilename
)
}
}.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
import ru.tech.imageresizershrinker.core.domain.image.model.ImageScaleMode
import ru.tech.imageresizershrinker.core.domain.image.model.Preset
import ru.tech.imageresizershrinker.core.domain.image.model.ResizeType
import ru.tech.imageresizershrinker.core.domain.model.ChecksumType
import ru.tech.imageresizershrinker.core.domain.model.ColorModel
import ru.tech.imageresizershrinker.core.domain.model.PerformanceClass
import ru.tech.imageresizershrinker.core.domain.model.SystemBarsVisibility
Expand Down Expand Up @@ -68,6 +69,7 @@ import ru.tech.imageresizershrinker.feature.settings.data.SettingKeys.AUTO_CACHE
import ru.tech.imageresizershrinker.feature.settings.data.SettingKeys.BORDER_WIDTH
import ru.tech.imageresizershrinker.feature.settings.data.SettingKeys.CAN_ENTER_PRESETS_BY_TEXT_FIELD
import ru.tech.imageresizershrinker.feature.settings.data.SettingKeys.CENTER_ALIGN_DIALOG_BUTTONS
import ru.tech.imageresizershrinker.feature.settings.data.SettingKeys.CHECKSUM_TYPE_FOR_FILENAME
import ru.tech.imageresizershrinker.feature.settings.data.SettingKeys.COLOR_BLIND_TYPE
import ru.tech.imageresizershrinker.feature.settings.data.SettingKeys.COLOR_TUPLES
import ru.tech.imageresizershrinker.feature.settings.data.SettingKeys.CONFETTI_ENABLED
Expand Down Expand Up @@ -314,7 +316,8 @@ internal class AndroidSettingsManager @Inject constructor(
fastSettingsSide = prefs[FAST_SETTINGS_SIDE]?.let {
FastSettingsSide.fromOrdinal(it)
} ?: default.fastSettingsSide,
settingGroupsInitialVisibility = prefs[SETTINGS_GROUP_VISIBILITY].toSettingGroupsInitialVisibility()
settingGroupsInitialVisibility = prefs[SETTINGS_GROUP_VISIBILITY].toSettingGroupsInitialVisibility(),
checksumTypeForFilename = ChecksumType.fromString(prefs[CHECKSUM_TYPE_FOR_FILENAME])
)
}.onEach { currentSettings = it }

Expand Down Expand Up @@ -1125,6 +1128,12 @@ internal class AndroidSettingsManager @Inject constructor(
}
}

override suspend fun setChecksumTypeForFilename(type: ChecksumType?) {
dataStore.edit {
it[CHECKSUM_TYPE_FOR_FILENAME] = type?.digest ?: ""
}
}

private suspend fun setFavoriteScreens(data: List<Int>) {
dataStore.edit { prefs ->
prefs[FAVORITE_SCREENS] = data.joinToString("/") { it.toString() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,5 @@ internal object SettingKeys {
val CENTER_ALIGN_DIALOG_BUTTONS = booleanPreferencesKey("CENTER_ALIGN_DIALOG_BUTTONS")
val FAST_SETTINGS_SIDE = intPreferencesKey("FAST_SETTINGS_SIDE")
val SETTINGS_GROUP_VISIBILITY = stringSetPreferencesKey("SETTINGS_GROUP_VISIBILITY")
val CHECKSUM_TYPE_FOR_FILENAME = stringPreferencesKey("CHECKSUM_TYPE_FOR_FILENAME")
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ fun AddFileSizeSettingItem(
PreferenceRowSwitch(
shape = shape,
modifier = modifier,
enabled = !settingsState.randomizeFilename && !settingsState.overwriteFiles,
enabled = !settingsState.randomizeFilename && !settingsState.overwriteFiles && settingsState.checksumTypeForFilename == null,
startIcon = Icons.Outlined.ScaleUnbalanced,
onClick = {
onClick()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ fun AddOriginalFilenameSettingItem(
val settingsState = LocalSettingsState.current
PreferenceRowSwitch(
shape = shape,
enabled = !settingsState.randomizeFilename && !settingsState.overwriteFiles && settingsState.picturePickerMode != PicturePickerMode.PhotoPicker,
enabled = !settingsState.randomizeFilename && !settingsState.overwriteFiles && settingsState.picturePickerMode != PicturePickerMode.PhotoPicker && settingsState.checksumTypeForFilename == null,
modifier = modifier,
startIcon = Icons.Outlined.Difference,
onClick = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ fun AddTimestampToFilenameSettingItem(
onClick = {
onClick()
},
enabled = !settingsState.randomizeFilename && !settingsState.overwriteFiles,
enabled = !settingsState.randomizeFilename && !settingsState.overwriteFiles && settingsState.checksumTypeForFilename == null,
title = stringResource(R.string.add_timestamp),
subtitle = stringResource(R.string.add_timestamp_sub),
checked = settingsState.addTimestampToFilename,
Expand Down
Loading

0 comments on commit bc0d04a

Please sign in to comment.