1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-07-22 06:41:06 +02:00

Support for color dithered bitmaps, and converting emoji->bitmaps

# Conflicts:
#	app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java
This commit is contained in:
Gordon Williams 2022-06-10 12:01:12 +01:00
parent 12f2049ac6
commit 607441b6b0
2 changed files with 160 additions and 40 deletions

View File

@ -32,6 +32,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.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
@ -169,6 +170,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<Integer> settings = new Vector<Integer>();
settings.add(R.xml.devicesettings_banglejs);

View File

@ -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;
@ -97,6 +101,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
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.LimitedQueue;
@ -584,27 +589,50 @@ 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
String words[] = txt.split(" ");
for (int i=0;i<words.length;i++) {
boolean isRenderable = true;
for (int j=0;j<words[i].length();j++) {
char c = words[i].charAt(j);
return EmojiConverter.convertUnicodeEmojiToAscii(txt, GBApplication.getContext());
// Otherwise split up and check each word
String word = "", result = "";
boolean needsTranslate = false;
for (int i=0;i<txt.length();i++) {
char ch = txt.charAt(i);
if (" -_/:.,?!'\"&*()".indexOf(ch)>=0) {
// word split
if (needsTranslate) { // convert word
result += renderUnicodeWordAsImage(word)+ch;
} else { // or just copy across
result += word+ch;
}
word = "";
needsTranslate = false;
} else {
// TODO: better check?
if (c<0 || c>255) isRenderable = false;
if (ch<0 || ch>255) needsTranslate = true;
word += ch;
}
if (!isRenderable)
words[i] = "\0"+bitmapToEspruinoString(textToBitmap(words[i]));
}
return String.join(" ", words);
if (needsTranslate) { // convert word
result += renderUnicodeWordAsImage(word);
} else { // or just copy across
result += word;
}
return result;
}
@Override
@ -930,44 +958,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]<minColUsage) {
minColUsage = colUsage[c];
transparentColorIndex = (byte)c;
}
}
// rewrite any transparent pixels as the correct color for transparency
for (n=0;n<pixels.length;n++)
if (pixels[n]==PIXELCOL_TRANSPARENT)
pixels[n] = transparentColorIndex;
}
// Write the header
int headerLen = isTransparent ? 4 : 3;
byte bmp[] = new byte[(((height * width * bpp) + 7) >> 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 */