blurhash icon indicating copy to clipboard operation
blurhash copied to clipboard

If Anyone Interested in Java decoder For Android App(Derived from Demo Kotlin library)

Open drayan85 opened this issue 5 years ago • 2 comments

import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Build;

import androidx.annotation.Nullable;

import java.util.HashMap;
import java.util.Map;

public class BlurHashDecoder {

    // cache Math.cos() calculations to improve performance.
    // The number of calculations can be huge for many bitmaps: width * height * numCompX * numCompY * 2 * nBitmaps
    // the cache is enabled by default, it is recommended to disable it only when just a few images are displayed
    private final HashMap<Integer, double[]> cacheCosinesX = new HashMap<>();
    private final HashMap<Integer, double[]> cacheCosinesY = new HashMap<>();
    private static Map<Character, Integer> charMap = new HashMap();

    private static final BlurHashDecoder INSTANCE = new BlurHashDecoder();

    public static BlurHashDecoder getInstance() {
        return INSTANCE;
    }

    private BlurHashDecoder() {
    }

    static {
        Character[] characters = {
                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
                'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
                'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',', '-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
        };
        for (int i = 0; i < characters.length; i++) {
            charMap.put(characters[i], i);
        }
    }

    /**
     * Clear calculations stored in memory cache.
     * The cache is not big, but will increase when many image sizes are used,
     * if the app needs memory it is recommended to clear it.
     */
    private void clearCache() {
        cacheCosinesX.clear();
        cacheCosinesY.clear();
    }

    /**
     * Decode a blur hash into a new bitmap.
     *
     * @param useCache use in memory cache for the calculated math, reused by images with same size.
     *                 if the cache does not exist yet it will be created and populated with new calculations.
     *                 By default it is true.
     */
    public Bitmap decode(@Nullable String blurHash, int width, int height, float punch, boolean useCache) {
        if (blurHash == null || blurHash.length() <= 6) {
            return null;
        }

        int numCompEnc = decode83(blurHash, 0, 1);
        int numCompX = numCompEnc % 9 + 1;
        int numCompY = numCompEnc / 9 + 1;
        if (blurHash.length() != 4 + 2 * numCompX * numCompY) {
            return null;
        } else {
            int maxAcEnc = this.decode83(blurHash, 1, 2);
            float maxAc = (float)(maxAcEnc + 1) / 166.0F;
            float[][] colors = new float[numCompX * numCompY][];

            for(int i = 0; i < numCompX * numCompY; ++i) {
                if (i == 0) {
                    int colorEnc = decode83(blurHash, 2, 6);
                    colors [i] = decodeDc(colorEnc);
                } else {
                    int from = 4 + i * 2;
                    int colorEnc = decode83(blurHash, from, from + 2);
                    colors [i] = decodeAc(colorEnc, maxAc * punch);
                }
            }
            return composeBitmap(width, height, numCompX, numCompY, colors, useCache);
        }
    }

    private int decode83(String str, int from, int to) {
        int result = 0;
        for (int i = from; i < to; i++) {
            int index = charMap.get(str.charAt(i));
            if (index != -1) {
                result = result * 83 + index;
            }
        }
        return result;
    }


    private float[] decodeDc(int colorEnc) {
        int r = colorEnc >> 16;
        int g = colorEnc >> 8 & 255;
        int b = colorEnc & 255;
        return new float[]{sRGBToLinear(r), sRGBToLinear(g), sRGBToLinear(b)};
    }

    private float sRGBToLinear(double colorEnc) {
        float v = (float)colorEnc / 255.0F;
        if (v <= 0.04045F) {
            return v / 12.92F;
        } else {
            return (float)Math.pow((v + 0.055F) / 1.055F, 2.4F);
        }
    }

    private float[] decodeAc(int value, float maxAc) {
        int r = value / 361;
        int g = (value / 19) % 19;
        int b = value % 19;
        return new float[]{
                signedPow2((r - 9) / 9.0F) * maxAc,
                signedPow2((g - 9) / 9.0F) * maxAc,
                signedPow2((b - 9) / 9.0F) * maxAc
        };
    }

    private float signedPow2(float value) {
        return Math.copySign((float)Math.pow((double)value, (double)2.0F), value);
    }

    private Bitmap composeBitmap(int width, int height, int numCompX, int numCompY, float[][] colors , boolean useCache) {
        // use an array for better performance when writing pixel colors
        int[] imageArray = new int[width * height];
        boolean calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX);
        double[] cosinesX = getArrayForCosinesX(calculateCosX, width, numCompX);
        boolean calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY);
        double[] cosinesY = getArrayForCosinesY(calculateCosY, height, numCompY);
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                float r = 0.0F;
                float g = 0.0F;
                float b = 0.0F;
                for (int j = 0; j < numCompY; j++) {
                    for (int i = 0; i < numCompX; i++) {
                        double cosX = getCos(cosinesX, calculateCosX, i, numCompX, x, width);
                        double cosY = getCos(cosinesY, calculateCosY, j, numCompY, y, height);
                        float basis = (float)(cosX * cosY);
                        float[] color = colors[j * numCompX + i];
                        r += color[0] * basis;
                        g += color[1] * basis;
                        b += color[2] * basis;
                    }
                }

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    imageArray[x + width * y] = Color.rgb(linearToSRGB(r), linearToSRGB(g), linearToSRGB(b));
                } else {
                    imageArray[x + width * y] = Color.argb(255, linearToSRGB(r), linearToSRGB(g), linearToSRGB(b));
                }
            }
        }
        return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888);
    }

    private double[] getArrayForCosinesY(boolean calculate, int height, int numCompY)  {
        if (calculate) {
            double[] cosinesY = new double[height * numCompY];
            cacheCosinesY.put(height * numCompY, cosinesY);
            return cosinesY;
        } else {
            return (double[]) cacheCosinesY.get(height * numCompY);
        }
    }

    private double[] getArrayForCosinesX(boolean calculate, int width, int numCompX) {
        if (calculate) {
            double[] cosinesX = new double[width * numCompX];
            cacheCosinesX.put(width * numCompX, cosinesX);
            return cosinesX;
        } else {
            return (double[]) cacheCosinesX.get(width * numCompX);
        }
    }

    private double getCos(double[] getCos, boolean calculate, int x, int numComp, int y, int size) {
        if (calculate) {
            getCos[x + numComp * y] = Math.cos(Math.PI * y * x / size);
        }
        return getCos[x + numComp * y];
    }

    private int linearToSRGB(double value) {
        double v = Math.max(0, Math.min(1, value));
        if (v <= 0.0031308F) {
            return (int) (v * 12.92F * 255.0F + 0.5F);
        } else {
            return (int) ((1.055F * Math.pow(v, (1 / 2.4F)) - 0.055F) * 255 + 0.5F);
        }
    }
}

drayan85 avatar Nov 21 '20 22:11 drayan85

Thanks a LOT for this!!

AmitJayant avatar Dec 06 '20 07:12 AmitJayant

importar android.graphics.Bitmap;reboke*** importar android.graphics.Color; > importar android.os.Build; > > importar androidx.annotation.Nullable; > > importar java.util.HashMap; > importar java.util.Map; > > clase pública BlurHashDecoder { > > // cache Math.cos() cálculos para mejorar el rendimiento. > // El número de cálculos puede ser enorme para muchos mapas de bits: ancho * alto * numCompX * numCompY * 2 * nBitmaps > // la caché está habilitada de forma predeterminada, se recomienda deshabilitarla solo cuando se muestren solo unas pocas imágenes > private final HashMap<Entero, double[]> cacheCosinesX = new HashMap<>(); > final privado HashMap<Entero, doble[]> cacheCosinesY = nuevo HashMap<>(); > Mapa estático privado<Personaje, Entero> charMap = nuevo HashMap(); > > instancia final estática privada de BlurHashDecoder = nuevo BlurHashDecoder(); > > public static BlurHashDecoder getInstance() { > devolver INSTANCIA; > } > > private BlurHashDecoder() { > } > > estático { > Carácter[] caracteres = { > '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J' > 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l' > 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+',', '-', '.', ':', ';', '=', '?', '@', '[', ' > }; > para (int i = 0; i < characters.length; i++) { > charMap.put(personajes[i], i); > } > } > > /** > * Borrar los cálculos almacenados en la caché de memoria. > * La caché no es grande, pero aumentará cuando se utilicen muchos tamaños de imagen, > * si la aplicación necesita memoria, se recomienda borrarla. > / > private void clearCache() { > cacheCosinesX.clear(); > cacheCosinesY.clear(); > } > > /* > * Decodifica un hash de desenfoque en un nuevo mapa de bits. > * > * @param useCache use en la caché de memoria para las matemáticas calculadas, reutilizadas por imágenes del mismo tamaño. > * si la caché aún no existe, se creará y rellenará con nuevos cálculos. > * Por defecto es cierto. > */ > public Bitmap decode(@Nullable String blurHash, int width, int height, float punch, boolean useCache) { > if (blurHash == null || blurHash.length() <= 6) { > devolver null; > } > > int numCompEnc = decode83(blurHash, 0, 1); > int numCompX = numCompEnc % 9 + 1; > int numCompY = numCompEnc / 9 + 1; > if (blurHash.length() ! = 4 + 2 * numCompX * numCompY) { > devolver null; > } else { > int maxAcEnc = this.decode83(blurHash, 1, 2); > float maxAc = (flotante)(maxAcEnc + 1) / 166.0F; > float[][] colors = new float[numCompX * numCompY][]; > > for(int i = 0; i < numCompX * numCompY; ++i) { > si (i == 0) { > int colorEnc = decode83(blurHash, 2, 6); > colores [i] = decodeDc(colorEnc); > } de lo contrario { > int from = 4 + i * 2; > int colorEnc = decode83(blurHash, from, from + 2); > colores [i] = decodeAc(colorEnc, maxAc * punch); > } > } > devolver composeBitmap(ancho, alto, numCompX, numCompY, colores, useCache); > } > } > > private int decode83(String str, int from, int to) { > resultado int = 0; > para (int i = from; i < to; i++) { > índice int = charMap.get(str.charAt(i)); > if (índice! = -1) { > resultado = resultado * 83 + índice; > } > } > resultado de retorno; > } > > > flotador privado[] decodeDc(int colorEnc) { > int r = colorEnc >> 16; > int g = colorEnc >> 8 y 255; > int b = colorEnc & 255; > devolver nuevo flotador[]{sRGBToLinear(r), sRGBToLinear(g), sRGBToLinear(b)}; > } > > flotador privado sRGBToLinear(double colorEnc) { > float v = (flotante)colorEnc / 255.0F; > si (v <= 0,04045F) { > return v / 12.92F; > } de lo contrario { > return (float)Math.pow((v + 0.055F) / 1.055F, 2.4F); > } > } > > float privado[] decodeAc(int value, float maxAc) { > int r = valor / 361; > int g = (valor / 19) % 19; > int b = valor % 19; > return new float[]{ > signedPow2((r - 9) / 9.0F) * maxAc, > signedPow2((g - 9) / 9.0F) * maxAc, > signedPow2((b - 9) / 9.0F) * maxAc > }; > } > > flotador privado signedPow2(valor flotante) { > devuelve Math.copySign((float)Math.pow((double)value, (double)2.0F), value); > } > > mapa de bits privado composeBitmap(int width, int height, int numCompX, int numCompY, float[][] colors , boolean useCache) { > // usar una matriz para un mejor rendimiento al escribir colores de píxeles > int[] imageArray = new int[width * height]; > booleano calculateCosX = ! useCache || ! cacheCosinesX.containsKey(ancho * numCompX); > double[] cosinesX = getArrayForCosinesX(calcularCosX, ancho, numCompX); > booleano calculateCosY = ! useCache || ! cacheCosinesY.containsKey(altura * numCompY); > double[] cosinesY = getArrayForCosinesY(calcularCosY, altura, numCompY); > para (int y = 0; y < altura; y++) { > para (int x = 0; x < ancho; x++) { > flotador r = 0,0F; > flotador g = 0,0F; > flotador b = 0,0F; > para (int j = 0; j < numCompY; j++) { > para (int i = 0; i < numCompX; i++) { > doble cosX = getCos(cosinesX, calculateCosX, i, numCompX, x, ancho); > doble cosY = getCos(cosinesY, calculateCosY, j, numCompY, y, altura); > base flotante = (flotante) (cosX * cosY); > float[] color = colores[j * numCompX + i]; > r += color[0] * base; > g += color[1] * base; > b += color[2] * base; > } > } > > if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { > imageArray[x + width * y] = Color.rgb(linearToSRGB(r), linearToSRGB(g), linearToSRGB(b)); > } de lo contrario { > imageArray[x + width * y] = Color.argb(255, linearToSRGB(r), linearToSRGB(g), linearToSRGB(b)); > } > } > } > devolver Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888); > } > > doble privado[] getArrayForCosinesY(boolean calculate, int height, int numCompY) { > si (calcular) { > double[] cosinesY = nuevo double[altura * numCompY]; > cacheCosinesY.put(altura * numCompY, cosinesY); > cosinesY de devolución; > } de lo contrario { > devolver (doble[]) cacheCosinesY.get(altura * numCompY); > } > } > > doble privado[] getArrayForCosinesX(boolean calculate, int width, int numCompX) { > si (calcular) { > doble[] cosinesX = nuevo doble[ancho * numCompX]; > cacheCosinesX.put(ancho * numCompX, cosinesX); > devolver cosinesX; > } de lo contrario { > devolver (doble[]) cacheCosinesX.get(width * numCompX); > } > } > > private double getCos(double[] getCos, boolean calculate, int x, int numComp, int y, int size) { > si (calcular) { > getCos[x + numComp * y] = Math.cos(Math.PI * y * x / tamaño); > } > devuelve getCos[x + numComp * y]; > } > > private int linearToSRGB(doble valor) { > double v = Math.max(0, Math.min(1, valor)); > si (v <= 0.0031308F) { > retorno (int) (v * 12.92F * 255.0F + 0.5F); > } de lo contrario { > return (int) ((1.055F * Math.pow(v, (1 / 2.4F)) - 0.055F) * 255 + 0.5F); > } > } > } R

M-Feve0-1 avatar Jul 11 '21 23:07 M-Feve0-1

Feel free to create a repo with the implementation - then we can link it in our README. Closing.

Thisen avatar May 22 '24 06:05 Thisen