Page MenuHomePhabricator

D7780.diff
No OneTemporary

D7780.diff

diff --git a/native/expo-modules/thumbhash/android/src/main/java/app/comm/android/thumbhash/ThumbhashModule.kt b/native/expo-modules/thumbhash/android/src/main/java/app/comm/android/thumbhash/ThumbhashModule.kt
--- a/native/expo-modules/thumbhash/android/src/main/java/app/comm/android/thumbhash/ThumbhashModule.kt
+++ b/native/expo-modules/thumbhash/android/src/main/java/app/comm/android/thumbhash/ThumbhashModule.kt
@@ -1,14 +1,112 @@
package app.comm.android.thumbhash
+import android.content.ContentResolver
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.ImageDecoder
+import android.net.Uri
+import android.os.Build
+import android.util.Base64
+import androidx.annotation.RequiresApi
+import androidx.core.graphics.scale
+import androidx.core.util.component1
+import androidx.core.util.component2
+import expo.modules.core.errors.CodedException
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
+import thirdparty.ThumbHash
+import kotlin.math.max
+
+// Thumbhash requires images that are max 100x100 pixels
+private const val THUMB_SIZE = 100
class ThumbhashModule : Module() {
override fun definition() = ModuleDefinition {
Name("Thumbhash")
- AsyncFunction("generateThumbHash") {
- "unimplemented"
+ AsyncFunction("generateThumbHash", this@ThumbhashModule::generateThumbHash)
+ }
+
+ // region Function implementations
+
+ private fun generateThumbHash(photoURI: Uri): String {
+ val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ // For API >= 28 we can use the ImageDecoder API which is more modern
+ // and allows to resize bitmap to desired size while loading the file
+ val source = ImageDecoder.createSource(this.contentResolver, photoURI)
+ ImageDecoder.decodeBitmap(source, this.decoderListener)
+ } else {
+ // For older devices, we use the BitmapFactory thing which is less
+ // flexible, but at least we can request for the ARGB_8888 to avoid
+ // future conversions. We rescale the bitmap afterwards
+ val opts = BitmapFactory.Options().apply {
+ inPreferredConfig = Bitmap.Config.ARGB_8888
+ }
+ this.contentResolver.openInputStream(photoURI).use { stream ->
+ BitmapFactory.decodeStream(stream, null, opts)
+ }?.let {
+ val w = THUMB_SIZE * it.width / max(it.width, it.height)
+ val h = THUMB_SIZE * it.height / max(it.width, it.height)
+ it.scale(w, h, filter = false)
+ } ?: throw BitmapDecodeFailureException(photoURI.toString())
}
+
+ val rgba = bitmap.toRGBA()
+ val thumbHash = ThumbHash.rgbaToThumbHash(bitmap.width, bitmap.height, rgba)
+ return Base64.encodeToString(thumbHash, Base64.DEFAULT)
}
+
+ // endregion
+
+ @RequiresApi(Build.VERSION_CODES.P)
+ private val decoderListener =
+ ImageDecoder.OnHeaderDecodedListener { imageDecoder, imageInfo, _ ->
+ val (w, h) = imageInfo.size
+ val newWidth = THUMB_SIZE * w / max(w, h)
+ val newHeight = THUMB_SIZE * h / max(w, h)
+ // this usually defaults bitmap config to ARGB_8888
+ imageDecoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+ imageDecoder.setTargetSize(newWidth, newHeight)
+ }
+
+ private val contentResolver: ContentResolver
+ get() = requireNotNull(this.appContext.reactContext) {
+ "React Application Context is null"
+ }.contentResolver
}
+
+// region Utility extension functions
+
+fun Bitmap.toRGBA(): ByteArray {
+ // ensure we're using the ARGB_8888 format
+ val bitmap: Bitmap = when (this.config) {
+ Bitmap.Config.ARGB_8888 -> this
+ else -> this.copy(Bitmap.Config.ARGB_8888, false)
+ }
+ val pixels = IntArray(bitmap.width * bitmap.height).also {
+ bitmap.getPixels(it, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
+ }
+ val bytes = ByteArray(pixels.size * 4)
+ var i = 0
+ for (pixel in pixels) {
+ // Get components assuming is ARGB
+ val a = pixel shr 24 and 0xff
+ val r = pixel shr 16 and 0xff
+ val g = pixel shr 8 and 0xff
+ val b = pixel and 0xff
+ bytes[i++] = r.toByte()
+ bytes[i++] = g.toByte()
+ bytes[i++] = b.toByte()
+ bytes[i++] = a.toByte()
+ }
+ return bytes
+}
+
+// endregion
+
+// region Exception definitions
+
+private class BitmapDecodeFailureException(uri: String) :
+ CodedException("Failed to decode Bitmap for URI: $uri")
+
+// endregion
diff --git a/native/expo-modules/thumbhash/android/src/main/java/thirdparty/LICENSE.txt b/native/expo-modules/thumbhash/android/src/main/java/thirdparty/LICENSE.txt
new file mode 100644
--- /dev/null
+++ b/native/expo-modules/thumbhash/android/src/main/java/thirdparty/LICENSE.txt
@@ -0,0 +1,19 @@
+Copyright (c) 2023 Evan Wallace
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/native/expo-modules/thumbhash/android/src/main/java/thirdparty/ThumbHash.java b/native/expo-modules/thumbhash/android/src/main/java/thirdparty/ThumbHash.java
new file mode 100644
--- /dev/null
+++ b/native/expo-modules/thumbhash/android/src/main/java/thirdparty/ThumbHash.java
@@ -0,0 +1,148 @@
+package thirdparty;
+
+import androidx.annotation.NonNull;
+
+// ThumbHash Java implementation thanks to @evanw https://github.com/evanw/thumbhash
+public final class ThumbHash {
+ /**
+ * Encodes an RGBA image to a ThumbHash. RGB should not be premultiplied by A.
+ *
+ * @param w The width of the input image. Must be ≤100px.
+ * @param h The height of the input image. Must be ≤100px.
+ * @param rgba The pixels in the input image, row-by-row. Must have w*h*4 elements.
+ * @return The ThumbHash as a byte array.
+ */
+ @NonNull
+ public static byte[] rgbaToThumbHash(int w, int h, byte[] rgba) {
+ // Encoding an image larger than 100x100 is slow with no benefit
+ if (w > 100 || h > 100) throw new IllegalArgumentException(w + "x" + h + " doesn't fit in 100x100");
+
+ // Determine the average color
+ float avg_r = 0, avg_g = 0, avg_b = 0, avg_a = 0;
+ for (int i = 0, j = 0; i < w * h; i++, j += 4) {
+ float alpha = (rgba[j + 3] & 255) / 255.0f;
+ avg_r += alpha / 255.0f * (rgba[j] & 255);
+ avg_g += alpha / 255.0f * (rgba[j + 1] & 255);
+ avg_b += alpha / 255.0f * (rgba[j + 2] & 255);
+ avg_a += alpha;
+ }
+ if (avg_a > 0) {
+ avg_r /= avg_a;
+ avg_g /= avg_a;
+ avg_b /= avg_a;
+ }
+
+ boolean hasAlpha = avg_a < w * h;
+ int l_limit = hasAlpha ? 5 : 7; // Use fewer luminance bits if there's alpha
+ int lx = Math.max(1, Math.round((float) (l_limit * w) / (float) Math.max(w, h)));
+ int ly = Math.max(1, Math.round((float) (l_limit * h) / (float) Math.max(w, h)));
+ float[] l = new float[w * h]; // luminance
+ float[] p = new float[w * h]; // yellow - blue
+ float[] q = new float[w * h]; // red - green
+ float[] a = new float[w * h]; // alpha
+
+ // Convert the image from RGBA to LPQA (composite atop the average color)
+ for (int i = 0, j = 0; i < w * h; i++, j += 4) {
+ float alpha = (rgba[j + 3] & 255) / 255.0f;
+ float r = avg_r * (1.0f - alpha) + alpha / 255.0f * (rgba[j] & 255);
+ float g = avg_g * (1.0f - alpha) + alpha / 255.0f * (rgba[j + 1] & 255);
+ float b = avg_b * (1.0f - alpha) + alpha / 255.0f * (rgba[j + 2] & 255);
+ l[i] = (r + g + b) / 3.0f;
+ p[i] = (r + g) / 2.0f - b;
+ q[i] = r - g;
+ a[i] = alpha;
+ }
+
+ // Encode using the DCT into DC (constant) and normalized AC (varying) terms
+ Channel l_channel = new Channel(Math.max(3, lx), Math.max(3, ly)).encode(w, h, l);
+ Channel p_channel = new Channel(3, 3).encode(w, h, p);
+ Channel q_channel = new Channel(3, 3).encode(w, h, q);
+ Channel a_channel = hasAlpha ? new Channel(5, 5).encode(w, h, a) : null;
+
+ // Write the constants
+ boolean isLandscape = w > h;
+ int header24 = Math.round(63.0f * l_channel.dc)
+ | (Math.round(31.5f + 31.5f * p_channel.dc) << 6)
+ | (Math.round(31.5f + 31.5f * q_channel.dc) << 12)
+ | (Math.round(31.0f * l_channel.scale) << 18)
+ | (hasAlpha ? 1 << 23 : 0);
+ int header16 = (isLandscape ? ly : lx)
+ | (Math.round(63.0f * p_channel.scale) << 3)
+ | (Math.round(63.0f * q_channel.scale) << 9)
+ | (isLandscape ? 1 << 15 : 0);
+ int ac_start = hasAlpha ? 6 : 5;
+ int ac_count = l_channel.ac.length + p_channel.ac.length + q_channel.ac.length
+ + (hasAlpha ? a_channel.ac.length : 0);
+ byte[] hash = new byte[ac_start + (ac_count + 1) / 2];
+ hash[0] = (byte) header24;
+ hash[1] = (byte) (header24 >> 8);
+ hash[2] = (byte) (header24 >> 16);
+ hash[3] = (byte) header16;
+ hash[4] = (byte) (header16 >> 8);
+ if (hasAlpha) hash[5] = (byte) (Math.round(15.0f * a_channel.dc)
+ | (Math.round(15.0f * a_channel.scale) << 4));
+
+ // Write the varying factors
+ int ac_index = 0;
+ ac_index = l_channel.writeTo(hash, ac_start, ac_index);
+ ac_index = p_channel.writeTo(hash, ac_start, ac_index);
+ ac_index = q_channel.writeTo(hash, ac_start, ac_index);
+ if (hasAlpha) a_channel.writeTo(hash, ac_start, ac_index);
+ return hash;
+ }
+
+ private static final class Channel {
+ int nx;
+ int ny;
+ float dc;
+ float[] ac;
+ float scale;
+
+ Channel(int nx, int ny) {
+ this.nx = nx;
+ this.ny = ny;
+ int n = 0;
+ for (int cy = 0; cy < ny; cy++)
+ for (int cx = cy > 0 ? 0 : 1; cx * ny < nx * (ny - cy); cx++)
+ n++;
+ ac = new float[n];
+ }
+
+ Channel encode(int w, int h, float[] channel) {
+ int n = 0;
+ float[] fx = new float[w];
+ for (int cy = 0; cy < ny; cy++) {
+ for (int cx = 0; cx * ny < nx * (ny - cy); cx++) {
+ float f = 0;
+ for (int x = 0; x < w; x++)
+ fx[x] = (float) Math.cos(Math.PI / w * cx * (x + 0.5f));
+ for (int y = 0; y < h; y++) {
+ float fy = (float) Math.cos(Math.PI / h * cy * (y + 0.5f));
+ for (int x = 0; x < w; x++)
+ f += channel[x + y * w] * fx[x] * fy;
+ }
+ f /= w * h;
+ if (cx > 0 || cy > 0) {
+ ac[n++] = f;
+ scale = Math.max(scale, Math.abs(f));
+ } else {
+ dc = f;
+ }
+ }
+ }
+ if (scale > 0)
+ for (int i = 0; i < ac.length; i++)
+ ac[i] = 0.5f + 0.5f / scale * ac[i];
+ return this;
+ }
+
+ int writeTo(byte[] hash, int start, int index) {
+ for (float v : ac) {
+ hash[start + (index >> 1)] |= Math.round(15.0f * v) << ((index & 1) << 2);
+ index++;
+ }
+ return index;
+ }
+ }
+}
+

File Metadata

Mime Type
text/plain
Expires
Sat, Nov 30, 7:40 PM (21 h, 47 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2600569
Default Alt Text
D7780.diff (11 KB)

Event Timeline