Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
drayan85 opened this issue Nov 21, 2020 · 3 comments
Labels
documentation Improvements or additions to documentation

Comments

@drayan85
Copy link

drayan85 commented Nov 21, 2020

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);
        }
    }
}

@AmitJayant
Copy link

Thanks a LOT for this!!

@M-Feve0-1
Copy link

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

@jerry-git jerry-git added the documentation Improvements or additions to documentation label Jun 10, 2022
@Thisen
Copy link
Collaborator

Thisen commented May 22, 2024

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

@Thisen Thisen closed this as completed May 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

5 participants