mirror of
https://github.com/immich-app/immich.git
synced 2026-07-08 05:17:54 -07:00
58b1c5e8fb
* fix(mobile): fix 10-bit heic and avif colors on android * fix(mobile): share the decode pool across flutter engines * vectorize conversion --------- Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
174 lines
7.3 KiB
C
174 lines
7.3 KiB
C
#include <jni.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <android/bitmap.h>
|
|
|
|
// Cache-friendly block size for the tiled rotation (in pixels). 32x32 uint32 = 4KB, fits L1.
|
|
#define TILE 32
|
|
|
|
// EXIF orientation values (androidx.exifinterface.media.ExifInterface.ORIENTATION_*).
|
|
enum {
|
|
ORIENTATION_FLIP_HORIZONTAL = 2,
|
|
ORIENTATION_ROTATE_180 = 3,
|
|
ORIENTATION_FLIP_VERTICAL = 4,
|
|
ORIENTATION_TRANSPOSE = 5,
|
|
ORIENTATION_ROTATE_90 = 6,
|
|
ORIENTATION_TRANSVERSE = 7,
|
|
ORIENTATION_ROTATE_270 = 8,
|
|
};
|
|
|
|
// The orientations that swap width and height. Must stay in sync with affine_for's dim usage.
|
|
static int swaps_dims(int o) {
|
|
return o == ORIENTATION_ROTATE_90 || o == ORIENTATION_ROTATE_270 ||
|
|
o == ORIENTATION_TRANSPOSE || o == ORIENTATION_TRANSVERSE;
|
|
}
|
|
|
|
// A source pixel (sx, sy) maps to destination index base + sx*stepX + sy*stepY, where dw is the
|
|
// destination width. This affine form covers all 8 EXIF orientations and matches the pixel layout
|
|
// of Bitmap.createBitmap(src, matrixForExifOrientation(o)). int64_t so it stays correct on
|
|
// armeabi-v7a (32-bit long) regardless of how large MAX_RAW_DECODE_PIXELS grows.
|
|
static void affine_for(int o, int sw, int sh, int dw, int64_t *base, int64_t *stepX, int64_t *stepY) {
|
|
switch (o) {
|
|
case ORIENTATION_ROTATE_90: *base = sh - 1; *stepX = dw; *stepY = -1; break;
|
|
case ORIENTATION_ROTATE_270: *base = (int64_t) (sw - 1) * dw; *stepX = -dw; *stepY = 1; break;
|
|
case ORIENTATION_ROTATE_180: *base = (int64_t) (sh - 1) * dw + (sw - 1); *stepX = -1; *stepY = -dw; break;
|
|
case ORIENTATION_FLIP_HORIZONTAL: *base = sw - 1; *stepX = -1; *stepY = dw; break;
|
|
case ORIENTATION_FLIP_VERTICAL: *base = (int64_t) (sh - 1) * dw; *stepX = 1; *stepY = -dw; break;
|
|
case ORIENTATION_TRANSPOSE: *base = 0; *stepX = dw; *stepY = 1; break;
|
|
case ORIENTATION_TRANSVERSE: *base = (int64_t) (sw - 1) * dw + (sh - 1); *stepX = -dw; *stepY = -1; break;
|
|
default: *base = 0; *stepX = 1; *stepY = dw; break;
|
|
}
|
|
}
|
|
|
|
// Copy each source pixel (whole uint32, so channel order/premult is irrelevant) to its rotated
|
|
// destination, walking TILE x TILE blocks so the scattered writes of a 90/270 transpose stay
|
|
// cache-resident. dst is densely packed (rowBytes == dw*4, no padding), which the affine math relies on.
|
|
static void rotate_tiled(const uint8_t *src, int srcStride, uint32_t *dst,
|
|
int sw, int sh, int64_t base, int64_t stepX, int64_t stepY) {
|
|
for (int ty = 0; ty < sh; ty += TILE) {
|
|
int yEnd = ty + TILE < sh ? ty + TILE : sh;
|
|
for (int tx = 0; tx < sw; tx += TILE) {
|
|
int xEnd = tx + TILE < sw ? tx + TILE : sw;
|
|
for (int sy = ty; sy < yEnd; sy++) {
|
|
const uint32_t *srcRow = (const uint32_t *) (src + (size_t) sy * srcStride);
|
|
int64_t idx = base + (int64_t) sy * stepY + (int64_t) tx * stepX;
|
|
for (int sx = tx; sx < xEnd; sx++) {
|
|
dst[idx] = srcRow[sx];
|
|
idx += stepX;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rotates an RGBA_8888 bitmap to the given EXIF orientation into a freshly malloc'd buffer (free it
|
|
// via NativeBuffer.free). Fills outInfo with {width, height, rowBytes} and returns the buffer
|
|
// address, or 0 if the bitmap can't be handled (e.g. a non-8888 format) so the caller can fall back.
|
|
JNIEXPORT jlong JNICALL
|
|
Java_app_alextran_immich_NativeImage_rotate(
|
|
JNIEnv *env, jclass clazz, jobject bitmap, jint orientation, jintArray outInfo) {
|
|
AndroidBitmapInfo info;
|
|
if (AndroidBitmap_getInfo(env, bitmap, &info) != ANDROID_BITMAP_RESULT_SUCCESS) {
|
|
return 0;
|
|
}
|
|
if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
|
|
return 0;
|
|
}
|
|
|
|
int sw = (int) info.width;
|
|
int sh = (int) info.height;
|
|
int dw = swaps_dims(orientation) ? sh : sw;
|
|
int dh = swaps_dims(orientation) ? sw : sh;
|
|
|
|
uint32_t *dst = (uint32_t *) malloc((size_t) dw * dh * 4);
|
|
if (dst == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
void *srcPixels = NULL;
|
|
if (AndroidBitmap_lockPixels(env, bitmap, &srcPixels) != ANDROID_BITMAP_RESULT_SUCCESS) {
|
|
free(dst);
|
|
return 0;
|
|
}
|
|
|
|
int64_t base, stepX, stepY;
|
|
affine_for(orientation, sw, sh, dw, &base, &stepX, &stepY);
|
|
rotate_tiled((const uint8_t *) srcPixels, (int) info.stride, dst, sw, sh, base, stepX, stepY);
|
|
|
|
AndroidBitmap_unlockPixels(env, bitmap);
|
|
|
|
jint dims[3] = {dw, dh, dw * 4};
|
|
(*env)->SetIntArrayRegion(env, outInfo, 0, 3, dims);
|
|
// Keep ownership in C until the buffer is safely handed back: if outInfo was somehow too small,
|
|
// SetIntArrayRegion left a pending exception and Kotlin will never receive (or free) dst.
|
|
if ((*env)->ExceptionCheck(env)) {
|
|
free(dst);
|
|
return 0;
|
|
}
|
|
return (jlong) dst;
|
|
}
|
|
|
|
// Convert an RGBA_1010102 buffer to densely-packed RGBA_8888, matching Skia's
|
|
// Bitmap.copy(ARGB_8888) byte-for-byte so it's a drop-in for the intermediate 8888 bitmap.
|
|
// Each source pixel is a native (little-endian on every Android ABI) u32 with R in bits 0-9,
|
|
// G in 10-19, B in 20-29, A in 30-31 (standard RGB10_A2). Each 10-bit channel maps to 8-bit via
|
|
// round(v*255/1023). The 2-bit alpha maps to a*85. Both are kept as plain arithmetic as the whole
|
|
// loop auto-vectorizes to NEON and measures faster than a LUT on-device. Output is R,G,B,A bytes
|
|
// per pixel, i.e. Android ARGB_8888 memory == Dart PixelFormat.rgba8888.
|
|
static void convert_1010102(const uint8_t *src, int srcStride, uint32_t *dst, int w, int h) {
|
|
for (int y = 0; y < h; y++) {
|
|
const uint32_t *srcRow = (const uint32_t *) (src + (size_t) y * srcStride);
|
|
uint32_t *dstRow = dst + (size_t) y * w;
|
|
for (int x = 0; x < w; x++) {
|
|
uint32_t px = srcRow[x];
|
|
uint32_t r = ((px & 0x3FF) * 16336u + 32768u) >> 16;
|
|
uint32_t g = (((px >> 10) & 0x3FF) * 16336u + 32768u) >> 16;
|
|
uint32_t b = (((px >> 20) & 0x3FF) * 16336u + 32768u) >> 16;
|
|
uint32_t a = ((px >> 30) & 0x3) * 85u;
|
|
dstRow[x] = r | (g << 8) | (b << 16) | (a << 24);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Converts an RGBA_1010102 bitmap (what a 10-bit HEIC/AVIF decodes to on API 33+) into a freshly
|
|
// malloc'd RGBA_8888 buffer. Fills outInfo with {width, height, rowBytes} and returns the buffer
|
|
// address, or 0 (so the caller falls back to a Skia copy) if the bitmap isn't 1010102 or can't be
|
|
// locked. Same ownership contract as rotate: free the returned buffer via NativeBuffer.free.
|
|
JNIEXPORT jlong JNICALL
|
|
Java_app_alextran_immich_NativeImage_convert1010102(
|
|
JNIEnv *env, jclass clazz, jobject bitmap, jintArray outInfo) {
|
|
AndroidBitmapInfo info;
|
|
if (AndroidBitmap_getInfo(env, bitmap, &info) != ANDROID_BITMAP_RESULT_SUCCESS) {
|
|
return 0;
|
|
}
|
|
if (info.format != ANDROID_BITMAP_FORMAT_RGBA_1010102) {
|
|
return 0;
|
|
}
|
|
|
|
int w = (int) info.width;
|
|
int h = (int) info.height;
|
|
|
|
uint32_t *dst = (uint32_t *) malloc((size_t) w * h * 4);
|
|
if (dst == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
void *srcPixels = NULL;
|
|
if (AndroidBitmap_lockPixels(env, bitmap, &srcPixels) != ANDROID_BITMAP_RESULT_SUCCESS) {
|
|
free(dst);
|
|
return 0;
|
|
}
|
|
|
|
convert_1010102((const uint8_t *) srcPixels, (int) info.stride, dst, w, h);
|
|
|
|
AndroidBitmap_unlockPixels(env, bitmap);
|
|
|
|
jint dims[3] = {w, h, w * 4};
|
|
(*env)->SetIntArrayRegion(env, outInfo, 0, 3, dims);
|
|
if ((*env)->ExceptionCheck(env)) {
|
|
free(dst);
|
|
return 0;
|
|
}
|
|
return (jlong) dst;
|
|
}
|