@@ -7,7 +7,9 @@ package us.shandian.giga.postprocessing
77
88import android.graphics.Bitmap
99import java.io.ByteArrayOutputStream
10- import java.util.Base64
10+ import kotlin.math.max
11+ import kotlin.math.min
12+ import kotlin.math.sqrt
1113import org.schabi.newpipe.ktx.scale
1214
1315object ImageUtils {
@@ -31,35 +33,79 @@ object ImageUtils {
3133 )
3234
3335 fun compressToSize (original : Bitmap , maxSizeBytes : Int ): CompressedImage ? {
36+ // Strategy:
37+ // 1. Compress once and measure binary size; compute base64 size via formula:
38+ // base64Len = 4 * ceil(binaryLen / 3)
39+ // See https://de.wikipedia.org/wiki/Base64#Platzbedarf (en wiki doesn't have formula)
40+ // 2. If too big, try an adaptive quality reduction proportional to the ratio:
41+ // newQuality ≈ quality * (maxSize / measuredBase64Size)
42+ // 3. If quality hits minimum and still too big,
43+ // compute scale factor ≈ sqrt(maxSize / measuredBase64Size)
44+ // to reduce width/height (area scales ~ scale^2).
45+ // Repeat until fits or min dimension reached.
46+ val MIN_DIMENSION = 50
47+ val MIN_QUALITY = 70
3448 var quality = 100
3549 var scale = 1.0f
3650 var width = original.width
3751 var height = original.height
38- var compressedSize: Int
3952
40- do {
41- var bitmap = original.copy(original.config ? : Bitmap .Config .ARGB_8888 , false )
42- if (scale < 1.0f ) {
43- bitmap = bitmap.scale(width = width, height = height)
53+ while (width > MIN_DIMENSION && height > MIN_DIMENSION ) { // loop for scaling down
54+ // Prepare bitmap at current dimensions
55+ val bitmap = if (scale < 1.0f ) {
56+ original.scale(width = width, height = height)
57+ } else {
58+ // use a copy to ensure compress works on mutable config if needed
59+ original.copy(original.config ? : Bitmap .Config .ARGB_8888 , false )
4460 }
45- do {
61+
62+ while (true ) { // loop for iterative quality adjustments for this size
4663 val outputStream = ByteArrayOutputStream ()
4764 bitmap.compress(Bitmap .CompressFormat .JPEG , quality, outputStream)
48- compressedSize = Base64 .getEncoder().encodeToString(outputStream.toByteArray()).length
49- quality - = 5 // Decrease quality by 5% for the next iteration
50- } while (compressedSize > maxSizeBytes && quality > 70 )
51- if (compressedSize <= maxSizeBytes) {
52- return CompressedImage (bitmap, quality, width, height)
53- }
54- if (scale > 0.5f ) {
55- scale - = 0.1f
56- } else {
57- scale * = 0.9f
65+ val binarySize = outputStream.size() // actual compressed bytes
66+ // base64 size formula: 4 * ceil(binarySize / 3)
67+ // is the same as ((binarySize + 2) / 3) * 4 which is more efficient
68+ val base64Size = ((binarySize + 2 ) / 3 ) * 4
69+
70+ if (base64Size <= maxSizeBytes) {
71+ return CompressedImage (bitmap, quality, width, height)
72+ }
73+
74+ // Try to compute an adaptive new quality based on ratio assuming linear scaling
75+ val ratio = maxSizeBytes.toDouble() / base64Size.toDouble()
76+ val computedQuality = max(
77+ MIN_QUALITY ,
78+ min(quality - 5 , (quality * ratio).toInt())
79+ )
80+
81+ if (computedQuality >= quality) {
82+ // If quality cannot be effectively reduced further, break to scale image down
83+ break
84+ }
85+ quality = computedQuality
5886 }
87+
88+ // If quality reductions were insufficient, reduce scale using sqrt of ratio
89+ // Re-compress once to get a size for scaling decision (conservative)
90+ val probeStream = ByteArrayOutputStream ()
91+ bitmap.compress(Bitmap .CompressFormat .JPEG , quality, probeStream)
92+ val probeBinary = probeStream.size()
93+ val probeBase64 = ((probeBinary + 2 ) / 3 ) * 4
94+
95+ // avoid division by zero, should not happen but just in case
96+ if (probeBase64 == 0 ) return null
97+
98+ // Desired overall scale factor: sqrt(maxSize / observedSize)
99+ val desiredRatio = maxSizeBytes.toDouble() / probeBase64.toDouble()
100+ val scaleFactor = sqrt(desiredRatio).coerceAtMost(0.95 )
101+
102+ // Update scale and dimensions, reset quality to allow better quality at smaller size
103+ scale * = scaleFactor.toFloat()
59104 width = (original.width * scale).toInt()
60105 height = (original.height * scale).toInt()
61- quality = 100 // Reset quality for the next size reduction
62- } while (width > 50 && height > 50 ) // Prevent too much downscaling
106+ quality = 100
107+ }
108+
63109 return null
64110 }
65111}
0 commit comments