-
Notifications
You must be signed in to change notification settings - Fork 164
Expand file tree
/
Copy pathGifCompressor.kt
More file actions
105 lines (92 loc) · 4.05 KB
/
Copy pathGifCompressor.kt
File metadata and controls
105 lines (92 loc) · 4.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/*
* Copyright (C) 2025 QUIK SMS
*
* This file is part of QUIK SMS.
*
* QUIK SMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QUIK SMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QUIK SMS. If not, see <http://www.gnu.org/licenses/>.
*/
package dev.octoshrimpy.quik.util
import android.content.Context
import android.content.res.AssetFileDescriptor
import android.provider.OpenableColumns
import dev.octoshrimpy.quik.model.Attachment
import dev.octoshrimpy.quik.util.ImageUtils.getScaledGif
import timber.log.Timber
import kotlin.math.ln
import kotlin.math.max
import kotlin.math.pow
import kotlin.math.sqrt
class GifCompressor(
/**
* Dampens the amount the target compression amount will influence scale ratio.
* In order to reduce compression, lower this number, and to increase it, raise this number
*
* @property compressionParameter
*/
val compressionParameter: Double = 0.35,
/**
* Determines minimum file dimensions,
* adjust to avoid large GIFS compressed to unreadable amounts
*/
val minFileDimensions: Int = 20
) {
/**
* GIFs are very expensive to compress, so does a quick compression
* by estimating the scale down ratio using byte density, and overall size
* then compressing that way.
*
* Can be fine-tuned using [compressionParameter], and currently leans toward overcompression
*/
fun compressGif(
context: Context,
attachment: Attachment,
maxBytes: Int,
origWidth: Int,
aspectRatio: Float
): ByteArray {
// Determine file properties
val rawFileSize = ImageUtils.fetchFileSize(context, attachment).coerceAtLeast(1)
val origHeight = (origWidth / aspectRatio).toInt()
val totalPixelsPerFrame = origWidth * origHeight
val byteDensity = max(1.0, rawFileSize.toDouble() / totalPixelsPerFrame)
// Figure out generally how much we will have to compress,
// then dampen it and create a base scale
val targetCompressionRatio = rawFileSize.toDouble() / maxBytes.toDouble()
val targetCompressionDampened =
compressionParameter * (compressionParameter * 10) / targetCompressionRatio
val baseScale = 1.0 / (sqrt(targetCompressionRatio) + targetCompressionDampened)
// Calculate how much byte density deviates from expected values
val byteDensityScore = byteDensityScore(byteDensity, rawFileSize.toDouble())
// Determine how aggressive the compression will be, based mainly on byteDensityScore
val compressionScale = (targetCompressionRatio / (compressionParameter * 2))
val compressionAggressiveness =
targetCompressionRatio.pow(byteDensityScore) / (compressionScale)
// How much we will have to scale down the GIF, can't be over 1
val scaleRatio = baseScale.pow(compressionAggressiveness).coerceAtMost(1.0)
// Determine compressed dimensions from scale ratio
val midWidthGif = (origWidth * scaleRatio).toInt().coerceAtLeast(minFileDimensions)
val midHeightGif = (midWidthGif / aspectRatio).toInt().coerceAtLeast(minFileDimensions)
return getScaledGif(context, attachment.uri, midWidthGif, midHeightGif)
}
/**
* Determines how much byte density deviates from expected.
* If there is a lot of deviation, we can assume that the GIF is going to require more compression
*/
private fun byteDensityScore(byteDensity: Double, rawFileSize: Double): Double {
val d = ln(1.0 + byteDensity)
val r = ln(1.0 + rawFileSize)
val t = d / (d + r)
return 2.0 - 8.0 * t * (1.0 - t)
}
}