From a796860188515c3eb4281b7b5ee8a7af07d2023b Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 10 Jun 2022 10:14:37 +0100 Subject: [PATCH] Support for color dithered bitmaps, and converting emoji->bitmaps --- .../devices/banglejs/BangleJSCoordinator.java | 13 ++ .../banglejs/BangleJSDeviceSupport.java | 163 ++++++++++++++---- 2 files changed, 144 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java index 109d6c0a0..32ca36b60 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java @@ -17,6 +17,8 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.devices.banglejs; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANGLEJS_TEXT_BITMAP; + import android.annotation.TargetApi; import android.app.Activity; import android.bluetooth.le.ScanFilter; @@ -32,6 +34,7 @@ import java.util.Collections; import java.util.Vector; import nodomain.freeyourgadget.gadgetbridge.BuildConfig; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; @@ -43,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class BangleJSCoordinator extends AbstractDeviceCoordinator { @@ -167,6 +171,15 @@ public class BangleJSCoordinator extends AbstractDeviceCoordinator { return null; } + @Override + public boolean supportsUnicodeEmojis() { + /* we say yes here (because we can't get a handle to our device's prefs to check) + and then in 'renderUnicodeAsImage' we call EmojiConverter.convertUnicodeEmojiToAscii + just like DeviceCommunicationService.sanitizeNotifText would have done if we'd + reported false *if* conversion is disabled */ + return true; + } + public int[] getSupportedDeviceSpecificSettings(GBDevice device) { Vector settings = new Vector(); settings.add(R.xml.devicesettings_banglejs); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java index b9f05dcef..d46ffa76a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java @@ -62,11 +62,15 @@ import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Locale; import java.util.SimpleTimeZone; import java.util.UUID; import java.lang.reflect.Field; +import io.wax911.emojify.Emoji; +import io.wax911.emojify.EmojiManager; +import io.wax911.emojify.EmojiUtils; import nodomain.freeyourgadget.gadgetbridge.BuildConfig; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; @@ -99,6 +103,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSuppo import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest; import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils; +import nodomain.freeyourgadget.gadgetbridge.util.EmojiConverter; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -602,13 +607,25 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { return true; } + private String renderUnicodeWordAsImage(String word) { + // check for emoji + boolean hasEmoji = false; + if (EmojiUtils.getAllEmojis()==null) + EmojiManager.initEmojiData(GBApplication.getContext()); + for(Emoji emoji : EmojiUtils.getAllEmojis()) + if (word.contains(emoji.getEmoji())) hasEmoji = true; + // if we had emoji, ensure we create 3 bit color (not 1 bit B&W) + return "\0"+bitmapToEspruinoString(textToBitmap(word), hasEmoji ? BangleJSBitmapStyle.RGB_3BPP : BangleJSBitmapStyle.MONOCHROME); + } + public String renderUnicodeAsImage(String txt) { if (txt==null) return null; - // If we're not doing conversion, pass this right back + /* If we're not doing conversion, pass this right back (we use the EmojiConverter + As we would have done if BangleJSCoordinator.supportsUnicodeEmojis had reported false */ Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress())); if (!devicePrefs.getBoolean(PREF_BANGLEJS_TEXT_BITMAP, false)) - return txt; - // Otherwise split up and check each word + return EmojiConverter.convertUnicodeEmojiToAscii(txt, GBApplication.getContext()); + // Otherwise split up and check each word String word = "", result = ""; boolean needsTranslate = false; for (int i=0;i=0) { // word split if (needsTranslate) { // convert word - result += "\0"+bitmapToEspruinoString(textToBitmap(word))+ch; + result += renderUnicodeWordAsImage(word)+ch; } else { // or just copy across result += word+ch; } @@ -629,7 +646,7 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { } } if (needsTranslate) { // convert word - result += "\0"+bitmapToEspruinoString(textToBitmap(word)); + result += renderUnicodeWordAsImage(word); } else { // or just copy across result += word; } @@ -954,44 +971,126 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { return image; } - /** Convert an Android bitmap to a base64 string for use in Espruino. - * Currently only 1bpp, no scaling */ - public static byte[] bitmapToEspruinoArray(Bitmap bitmap) { - int width = bitmap.getWidth(); - int height = bitmap.getHeight(); - byte bmp[] = new byte[((height * width + 7) >> 3) + 3]; - int n = 0, c = 0, cn = 0; - bmp[n++] = (byte)width; - bmp[n++] = (byte)height; - bmp[n++] = 1; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - boolean pixel = (bitmap.getPixel(x, y) & 255) > 128; - c = (c << 1) | (pixel?1:0); - cn++; - if (cn == 8) { - bmp[n++] = (byte)c; - cn = 0; - c = 0; - } + public enum BangleJSBitmapStyle { + MONOCHROME, + RGB_3BPP + }; + + /** Used for writing single bits to an array */ + public static class BitWriter { + int n; + byte[] bits; + int currentByte, bitIdx; + + public BitWriter(byte[] array, int offset) { + bits = array; + n = offset; + } + + public void push(boolean v) { + currentByte = (currentByte << 1) | (v?1:0); + bitIdx++; + if (bitIdx == 8) { + bits[n++] = (byte)currentByte; + bitIdx = 0; + currentByte = 0; + } + } + + public void finish() { + if (bitIdx > 0) bits[n++] = (byte)currentByte; + } + } + + /** Convert an Android bitmap to a base64 string for use in Espruino. + * Currently only 1bpp, no scaling */ + public static byte[] bitmapToEspruinoArray(Bitmap bitmap, BangleJSBitmapStyle style) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + int bpp = (style==BangleJSBitmapStyle.RGB_3BPP) ? 3 : 1; + byte pixels[] = new byte[width * height]; + final byte PIXELCOL_TRANSPARENT = -1; + final int ditherMatrix[] = {1*16,5*16,7*16,3*16}; // for bayer dithering + // if doing 3bpp, check image to see if it's transparent + boolean allowTransparency = (style != BangleJSBitmapStyle.MONOCHROME); + boolean isTransparent = false; + byte transparentColorIndex = 0; + /* Work out what colour index each pixel should be and write to pixels. + Also figure out if we're transparent at all, and how often each color is used */ + int colUsage[] = new int[8]; + int n = 0; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int pixel = bitmap.getPixel(x, y); + int r = pixel & 255; + int g = (pixel >> 8) & 255; + int b = (pixel >> 16) & 255; + int a = (pixel >> 24) & 255; + boolean pixelTransparent = allowTransparency && (a < 128); + if (pixelTransparent) { + isTransparent = true; + r = g = b = 0; + } + // do dithering here + int ditherAmt = ditherMatrix[(x&1) + (y&1)*2]; + r += ditherAmt; + g += ditherAmt; + b += ditherAmt; + int col = 0; + if (style == BangleJSBitmapStyle.MONOCHROME) + col = ((r+g+b) >= 768)?1:0; + else if (style == BangleJSBitmapStyle.RGB_3BPP) + col = ((r>=256)?1:0) | ((g>=256)?2:0) | ((b>=256)?4:0); + if (!pixelTransparent) colUsage[col]++; // if not transparent, record usage + // save colour, mark transparent separately + pixels[n++] = (byte)(pixelTransparent ? PIXELCOL_TRANSPARENT : col); + } + } + // if we're transparent, find the least-used color, and use that for transparency + if (isTransparent) { + // find least used + int minColUsage = -1; + for (int c=0;c<8;c++) { + if (minColUsage<0 || colUsage[c]> 3) + headerLen]; + bmp[0] = (byte)width; + bmp[1] = (byte)height; + bmp[2] = (byte)(bpp + (isTransparent?128:0)); + if (isTransparent) bmp[3] = transparentColorIndex; + // Now write the image out bit by bit + BitWriter bits = new BitWriter(bmp, headerLen); + n = 0; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int pixel = pixels[n++]; + for (int b=bpp-1;b>=0;b--) + bits.push(((pixel>>b)&1) != 0); } } - if (cn > 0) bmp[n++] = (byte)c; - //LOG.info("BMP: " + width + "x"+height+" n "+n); - // Convert to base64 return bmp; } /** Convert an Android bitmap to a base64 string for use in Espruino. * Currently only 1bpp, no scaling */ - public static String bitmapToEspruinoString(Bitmap bitmap) { - return new String(bitmapToEspruinoArray(bitmap), StandardCharsets.ISO_8859_1); + public static String bitmapToEspruinoString(Bitmap bitmap, BangleJSBitmapStyle style) { + return new String(bitmapToEspruinoArray(bitmap, style), StandardCharsets.ISO_8859_1); } /** Convert an Android bitmap to a base64 string for use in Espruino. * Currently only 1bpp, no scaling */ - public static String bitmapToEspruinoBase64(Bitmap bitmap) { - return Base64.encodeToString(bitmapToEspruinoArray(bitmap), Base64.DEFAULT).replaceAll("\n",""); + public static String bitmapToEspruinoBase64(Bitmap bitmap, BangleJSBitmapStyle style) { + return Base64.encodeToString(bitmapToEspruinoArray(bitmap, style), Base64.DEFAULT).replaceAll("\n",""); } /** Convert a drawable to a bitmap, for use with bitmapToEspruino */