package com.vgmlr.varnish
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ImageDecoder
import android.graphics.Rect
import android.media.MediaMetadataRetriever
import androidx.core.graphics.createBitmap
import androidx.exifinterface.media.ExifInterface
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.ConcurrentHashMap
object VarnishCodec {
private val EXIF_TAGS = listOf(
ExifInterface.TAG_DATETIME,
ExifInterface.TAG_DATETIME_ORIGINAL,
ExifInterface.TAG_DATETIME_DIGITIZED,
ExifInterface.TAG_OFFSET_TIME,
ExifInterface.TAG_OFFSET_TIME_ORIGINAL,
ExifInterface.TAG_OFFSET_TIME_DIGITIZED,
ExifInterface.TAG_SUBSEC_TIME,
ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
ExifInterface.TAG_MAKE,
ExifInterface.TAG_MODEL,
ExifInterface.TAG_SOFTWARE,
ExifInterface.TAG_ARTIST,
ExifInterface.TAG_COPYRIGHT,
ExifInterface.TAG_IMAGE_DESCRIPTION,
ExifInterface.TAG_USER_COMMENT,
ExifInterface.TAG_F_NUMBER,
ExifInterface.TAG_APERTURE_VALUE,
ExifInterface.TAG_EXPOSURE_TIME,
ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
ExifInterface.TAG_EXPOSURE_PROGRAM,
ExifInterface.TAG_EXPOSURE_MODE,
ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
ExifInterface.TAG_FOCAL_LENGTH,
ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
ExifInterface.TAG_FLASH,
ExifInterface.TAG_WHITE_BALANCE,
ExifInterface.TAG_METERING_MODE,
ExifInterface.TAG_LENS_MAKE,
ExifInterface.TAG_LENS_MODEL,
ExifInterface.TAG_GPS_LATITUDE,
ExifInterface.TAG_GPS_LATITUDE_REF,
ExifInterface.TAG_GPS_LONGITUDE,
ExifInterface.TAG_GPS_LONGITUDE_REF,
ExifInterface.TAG_GPS_ALTITUDE,
ExifInterface.TAG_GPS_ALTITUDE_REF,
ExifInterface.TAG_GPS_TIMESTAMP,
ExifInterface.TAG_GPS_DATESTAMP,
ExifInterface.TAG_GPS_PROCESSING_METHOD
)
private val runtimes = ConcurrentHashMap<String, Long>()
fun runtime(path: String, modified: Long): Long? {
val k = "$modified:$path"
runtimes[k]?.let { return it }
val ms = runCatching {
MediaMetadataRetriever().use { r ->
r.setDataSource(path)
r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull()
}
}.getOrNull() ?: return null
runtimes[k] = ms
return ms
}
fun bounds(f: File): Pair<Int, Int>? {
val o = BitmapFactory.Options().apply { inJustDecodeBounds = true }
runCatching { BitmapFactory.decodeFile(f.absolutePath, o) }
if (o.outWidth <= 0 || o.outHeight <= 0) return null
val rot = runCatching { ExifInterface(f).rotationDegrees }.getOrDefault(0)
return if (rot == 90 || rot == 270) o.outHeight to o.outWidth else o.outWidth to o.outHeight
}
fun crop(src: File, rect: Rect, format: OutFormat, quality: Int, keepExif: Boolean): File? {
val dir = src.parentFile ?: return null
val full = decode(src) ?: return null
val r = Rect(rect)
if (!r.intersect(0, 0, full.width, full.height) || r.isEmpty) {
full.recycle()
return null
}
val cut = Bitmap.createBitmap(full, r.left, r.top, r.width(), r.height())
val out = if (format == OutFormat.JPG && cut.hasAlpha()) flatten(cut) else cut
val dst = freeFile(dir, "${src.nameWithoutExtension}_cropped", format.ext)
val ok = runCatching {
FileOutputStream(dst).use { out.compress(format.compress, quality, it) }
}.getOrDefault(false)
listOf(out, cut, full).forEach { it.recycle() }
if (!ok) {
dst.delete()
return null
}
if (keepExif) copyExif(src, dst, r.width(), r.height())
return dst
}
private fun decode(f: File): Bitmap? = runCatching {
ImageDecoder.decodeBitmap(ImageDecoder.createSource(f)) { d, _, _ ->
d.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
}
}.getOrNull()
private fun flatten(b: Bitmap): Bitmap {
val o = createBitmap(b.width, b.height)
Canvas(o).apply {
drawColor(Color.WHITE)
drawBitmap(b, 0f, 0f, null)
}
return o
}
private fun copyExif(src: File, dst: File, w: Int, h: Int) {
runCatching {
val a = ExifInterface(src)
val b = ExifInterface(dst)
EXIF_TAGS.forEach { t -> a.getAttribute(t)?.let { b.setAttribute(t, it) } }
b.setAttribute(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL.toString())
b.setAttribute(ExifInterface.TAG_PIXEL_X_DIMENSION, w.toString())
b.setAttribute(ExifInterface.TAG_PIXEL_Y_DIMENSION, h.toString())
b.saveAttributes()
}
}
}
internal fun freeFile(dir: File, base: String, ext: String): File {
fun name(n: Int) = (if (n == 0) base else "$base($n)") + if (ext.isEmpty()) "" else ".$ext"
var n = 0
while (File(dir, name(n)).exists()) n++
return File(dir, name(n))
}