chore: import upstream snapshot with attribution
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 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)
|
||||
}
|
||||
|
||||
android.namespace = "com.t8rin.ascii"
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ ImageToolbox is an image editor for android
|
||||
~ Copyright (c) 2026 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>
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 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>.
|
||||
*/
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package com.t8rin.ascii
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import androidx.core.graphics.alpha
|
||||
import androidx.core.graphics.applyCanvas
|
||||
import androidx.core.graphics.blue
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.get
|
||||
import androidx.core.graphics.green
|
||||
import androidx.core.graphics.red
|
||||
import androidx.core.graphics.scale
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
typealias GradientMap = Map<String, Float>
|
||||
|
||||
/**
|
||||
* Convert Bitmap into its ASCII equivalent [Bitmap] or [String].
|
||||
*/
|
||||
class ASCIIConverter(
|
||||
private val fontSize: Float = 18.0f,
|
||||
private val reverseLuma: Boolean = false,
|
||||
private val mapper: AsciiMapper = AsciiMapper()
|
||||
) {
|
||||
private var columns: Int = 0
|
||||
|
||||
suspend fun convertToAscii(
|
||||
bitmap: Bitmap,
|
||||
): String = convert(bitmap) { grid ->
|
||||
grid.rotate90AndMirror().joinToString("\n") { row ->
|
||||
row.joinToString(" ") {
|
||||
it.luma()?.let(mapper::mapToAscii) ?: " "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun convertToAsciiBitmap(
|
||||
bitmap: Bitmap,
|
||||
typeface: Typeface = Typeface.DEFAULT,
|
||||
backgroundColor: Int = Color.TRANSPARENT,
|
||||
isGrayscale: Boolean = false,
|
||||
): Bitmap = convert(bitmap) { grid ->
|
||||
prepareCanvas(
|
||||
bitmap = bitmap,
|
||||
backgroundColor = backgroundColor,
|
||||
typeface = typeface
|
||||
) { paint ->
|
||||
grid.forEachIndexed { row, blocks ->
|
||||
blocks.forEachIndexed { col, color ->
|
||||
val luma = color.luma()
|
||||
|
||||
if (isGrayscale && luma != null) {
|
||||
paint.setSrgbColor(
|
||||
color = Color.GRAY,
|
||||
alpha = (luma * 255.0f).toInt()
|
||||
)
|
||||
} else {
|
||||
paint.setSrgbColor(color)
|
||||
}
|
||||
|
||||
drawText(
|
||||
luma?.let(mapper::mapToAscii) ?: " ",
|
||||
row * fontSize,
|
||||
col * fontSize,
|
||||
paint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Grid.transpose(): Grid =
|
||||
first().indices.map { y ->
|
||||
indices.map { x -> this[x][y] }
|
||||
}
|
||||
|
||||
private fun Grid.rotate90AndMirror(): Grid =
|
||||
first().indices.map { x ->
|
||||
indices.map { y ->
|
||||
this[y][x]
|
||||
}.reversed()
|
||||
}.map { it.reversed() }
|
||||
|
||||
private inline fun prepareCanvas(
|
||||
bitmap: Bitmap,
|
||||
backgroundColor: Int,
|
||||
typeface: Typeface,
|
||||
action: Canvas.(Paint) -> Unit
|
||||
): Bitmap = createBitmap(bitmap.width, bitmap.height).applyCanvas {
|
||||
if (backgroundColor != Color.TRANSPARENT) drawColor(backgroundColor)
|
||||
action(
|
||||
Paint().apply {
|
||||
setTypeface(typeface)
|
||||
textSize = fontSize
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend inline fun <T> convert(
|
||||
bitmap: Bitmap,
|
||||
crossinline action: suspend (Grid) -> T
|
||||
) = coroutineScope {
|
||||
checkColumns(bitmap)
|
||||
action(bitmap.resize().toGrid())
|
||||
}
|
||||
|
||||
private fun checkColumns(bitmap: Bitmap) {
|
||||
columns = bitmap.toColumns()
|
||||
require(columns >= 5) { "Columns count is very small. Font size needs to be reduced" }
|
||||
}
|
||||
|
||||
private fun Bitmap.toColumns(): Int =
|
||||
if (columns < 5) (width / fontSize).roundToInt() else columns
|
||||
|
||||
private fun Bitmap.toGrid(): Grid {
|
||||
require(width > 0 && height > 0) { "Width and height must be positive values." }
|
||||
|
||||
return List(width) { x ->
|
||||
List(height) { y ->
|
||||
this[x, y]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bitmap.resize(): Bitmap {
|
||||
if (columns <= 1) return this
|
||||
|
||||
val min = min(height, width)
|
||||
val scaleFactor = if (columns > min) min else columns
|
||||
val ratio = scaleFactor.toFloat() / width.toFloat()
|
||||
|
||||
return copy(Bitmap.Config.ARGB_8888, false).scale(
|
||||
width = scaleFactor,
|
||||
height = (ratio * height).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
private fun Int.luma(): Float? {
|
||||
if (alpha <= 0) return null
|
||||
|
||||
val luminance = (0.2126 * (red / 255f))
|
||||
.plus(0.7152 * (green / 255f))
|
||||
.plus(0.0722 * (blue / 255f))
|
||||
|
||||
return (if (reverseLuma) 1 - luminance else luminance).toFloat()
|
||||
}
|
||||
|
||||
private fun Paint.setSrgbColor(
|
||||
color: Int,
|
||||
alpha: Int = color.alpha
|
||||
) {
|
||||
setARGB(alpha, color.red, color.green, color.blue)
|
||||
}
|
||||
}
|
||||
|
||||
fun interface AsciiMapper {
|
||||
fun mapToAscii(luma: Float): String
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
value class Gradient(val value: String) {
|
||||
|
||||
fun toMap(): GradientMap {
|
||||
val length = value.length
|
||||
return value.mapIndexed { index, char ->
|
||||
char.toString() to (1.0f - index.toFloat() / (length - 1).coerceAtLeast(1))
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val NORMAL = Gradient(".:-=+*#%@")
|
||||
val NORMAL2 = Gradient(" `.,-~+<>o=*%X@")
|
||||
val ARROWS = Gradient("↖←↙↓↘→↗↑")
|
||||
val OLD = Gradient("░▒▓█")
|
||||
val EXTENDED_HIGH = Gradient(".:-~=+*^><)(][}{#%@")
|
||||
val MINIMAL = Gradient(".-+#")
|
||||
val MATH = Gradient("π√∞≈≠=÷×-+")
|
||||
val NUMERICAL = Gradient("7132546980")
|
||||
|
||||
val entries by lazy {
|
||||
listOf(
|
||||
NORMAL,
|
||||
NORMAL2,
|
||||
ARROWS,
|
||||
OLD,
|
||||
EXTENDED_HIGH,
|
||||
MINIMAL,
|
||||
MATH,
|
||||
NUMERICAL
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.toGradientMap(): GradientMap = Gradient(this).toMap()
|
||||
|
||||
fun GradientMap.toMapper(): AsciiMapper = AsciiMapperImpl(map = this)
|
||||
|
||||
fun Gradient.toMapper(): AsciiMapper = toMap().toMapper()
|
||||
|
||||
fun AsciiMapper(map: GradientMap): AsciiMapper = map.toMapper()
|
||||
|
||||
fun AsciiMapper(gradient: Gradient = Gradient.NORMAL): AsciiMapper = gradient.toMapper()
|
||||
|
||||
|
||||
private class AsciiMapperImpl(map: GradientMap) : AsciiMapper {
|
||||
private val metrics: List<Pair<String, Float>> = map.toList().sortedByDescending { it.second }
|
||||
|
||||
override fun mapToAscii(
|
||||
luma: Float
|
||||
): String = metrics.find {
|
||||
luma >= it.second
|
||||
}?.first ?: metrics.firstOrNull()?.first.orEmpty()
|
||||
}
|
||||
|
||||
private typealias Grid = List<List<Int>>
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 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.compose)
|
||||
}
|
||||
|
||||
android.namespace = "com.t8rin.collages"
|
||||
|
||||
dependencies {
|
||||
implementation(libs.appCompat)
|
||||
implementation(libs.coilCompose)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ ImageToolbox is an image editor for android
|
||||
~ Copyright (c) 2026 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>
|
||||
|
After Width: | Height: | Size: 132 B |
|
After Width: | Height: | Size: 118 B |
|
After Width: | Height: | Size: 110 B |
|
After Width: | Height: | Size: 122 B |
|
After Width: | Height: | Size: 116 B |
|
After Width: | Height: | Size: 122 B |
|
After Width: | Height: | Size: 124 B |
|
After Width: | Height: | Size: 120 B |
|
After Width: | Height: | Size: 128 B |
|
After Width: | Height: | Size: 130 B |
|
After Width: | Height: | Size: 120 B |
|
After Width: | Height: | Size: 124 B |
|
After Width: | Height: | Size: 118 B |
|
After Width: | Height: | Size: 130 B |
|
After Width: | Height: | Size: 124 B |
|
After Width: | Height: | Size: 126 B |
|
After Width: | Height: | Size: 124 B |
|
After Width: | Height: | Size: 126 B |
|
After Width: | Height: | Size: 128 B |
|
After Width: | Height: | Size: 130 B |
|
After Width: | Height: | Size: 124 B |
|
After Width: | Height: | Size: 124 B |
|
After Width: | Height: | Size: 112 B |
|
After Width: | Height: | Size: 110 B |
|
After Width: | Height: | Size: 136 B |
|
After Width: | Height: | Size: 110 B |
|
After Width: | Height: | Size: 130 B |
|
After Width: | Height: | Size: 100 B |
|
After Width: | Height: | Size: 130 B |
|
After Width: | Height: | Size: 142 B |
|
After Width: | Height: | Size: 132 B |
|
After Width: | Height: | Size: 126 B |
|
After Width: | Height: | Size: 126 B |
|
After Width: | Height: | Size: 136 B |
|
After Width: | Height: | Size: 132 B |
|
After Width: | Height: | Size: 142 B |
|
After Width: | Height: | Size: 136 B |
|
After Width: | Height: | Size: 136 B |
|
After Width: | Height: | Size: 134 B |
|
After Width: | Height: | Size: 136 B |
|
After Width: | Height: | Size: 138 B |
|
After Width: | Height: | Size: 132 B |
|
After Width: | Height: | Size: 136 B |
|
After Width: | Height: | Size: 132 B |
|
After Width: | Height: | Size: 114 B |
|
After Width: | Height: | Size: 138 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 134 B |
|
After Width: | Height: | Size: 138 B |
|
After Width: | Height: | Size: 138 B |
|
After Width: | Height: | Size: 134 B |
|
After Width: | Height: | Size: 140 B |
|
After Width: | Height: | Size: 136 B |
|
After Width: | Height: | Size: 140 B |
|
After Width: | Height: | Size: 118 B |
|
After Width: | Height: | Size: 114 B |
|
After Width: | Height: | Size: 114 B |
|
After Width: | Height: | Size: 124 B |
|
After Width: | Height: | Size: 160 B |
|
After Width: | Height: | Size: 142 B |
|
After Width: | Height: | Size: 140 B |
|
After Width: | Height: | Size: 138 B |
|
After Width: | Height: | Size: 152 B |
|
After Width: | Height: | Size: 146 B |
|
After Width: | Height: | Size: 140 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 114 B |
|
After Width: | Height: | Size: 140 B |
|
After Width: | Height: | Size: 152 B |
|
After Width: | Height: | Size: 102 B |
|
After Width: | Height: | Size: 146 B |
|
After Width: | Height: | Size: 146 B |
|
After Width: | Height: | Size: 140 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 146 B |
|
After Width: | Height: | Size: 146 B |
|
After Width: | Height: | Size: 146 B |
|
After Width: | Height: | Size: 134 B |
|
After Width: | Height: | Size: 134 B |
|
After Width: | Height: | Size: 156 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 148 B |
|
After Width: | Height: | Size: 142 B |
|
After Width: | Height: | Size: 150 B |
|
After Width: | Height: | Size: 150 B |
|
After Width: | Height: | Size: 152 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 140 B |
|
After Width: | Height: | Size: 118 B |
|
After Width: | Height: | Size: 106 B |
|
After Width: | Height: | Size: 158 B |
|
After Width: | Height: | Size: 118 B |
|
After Width: | Height: | Size: 136 B |