feat(core): render local CJK glyphs at 2x texture resolution (#4304)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bart Louwers <bart@emeel.net>
This commit is contained in:
@@ -59,6 +59,15 @@ PremultipliedImage LocalGlyphRasterizer::drawGlyphBitmap(const std::string& font
|
||||
static_cast<jni::jchar>(glyphID)));
|
||||
}
|
||||
|
||||
float LocalGlyphRasterizer::getLastGlyphTop() {
|
||||
UniqueEnv env{AttachEnv()};
|
||||
|
||||
static auto& javaClass = jni::Class<LocalGlyphRasterizer>::Singleton(*env);
|
||||
static auto getLastGlyphTop = javaClass.GetMethod<jni::jfloat()>(*env, "getLastGlyphTop");
|
||||
|
||||
return javaObject.Call(*env, getLastGlyphTop);
|
||||
}
|
||||
|
||||
void LocalGlyphRasterizer::registerNative(jni::JNIEnv& env) {
|
||||
jni::Class<LocalGlyphRasterizer>::Singleton(env);
|
||||
}
|
||||
@@ -84,6 +93,8 @@ public:
|
||||
return androidLocalGlyphRasterizer.drawGlyphBitmap(*fontFamily, bold, glyphID);
|
||||
}
|
||||
|
||||
float getLastGlyphTop() { return androidLocalGlyphRasterizer.getLastGlyphTop(); }
|
||||
|
||||
private:
|
||||
std::optional<std::string> fontFamily;
|
||||
android::LocalGlyphRasterizer androidLocalGlyphRasterizer;
|
||||
@@ -98,6 +109,11 @@ bool LocalGlyphRasterizer::canRasterizeGlyph(const FontStack&, GlyphID glyphID)
|
||||
return util::i18n::allowsFixedWidthGlyphGeneration(glyphID) && impl->isConfigured();
|
||||
}
|
||||
|
||||
// 2x bitmap with 1x logical metrics.
|
||||
constexpr uint32_t kLocalGlyphTextureScale = 2;
|
||||
constexpr uint32_t kLocalGlyphLogicalSize = 30;
|
||||
constexpr uint32_t kLocalGlyphBitmapSize = kLocalGlyphLogicalSize * kLocalGlyphTextureScale;
|
||||
|
||||
Glyph LocalGlyphRasterizer::rasterizeGlyph(const FontStack& fontStack, GlyphID glyphID) {
|
||||
Glyph fixedMetrics;
|
||||
if (!impl->isConfigured()) {
|
||||
@@ -106,19 +122,21 @@ Glyph LocalGlyphRasterizer::rasterizeGlyph(const FontStack& fontStack, GlyphID g
|
||||
|
||||
fixedMetrics.id = glyphID;
|
||||
|
||||
Size size(35, 35);
|
||||
|
||||
fixedMetrics.metrics.width = size.width;
|
||||
fixedMetrics.metrics.height = size.height;
|
||||
fixedMetrics.metrics.left = -2;
|
||||
fixedMetrics.metrics.top = -5;
|
||||
// Metrics are reported in 1x logical units; the bitmap is allocated at 2x.
|
||||
fixedMetrics.metrics.width = kLocalGlyphLogicalSize;
|
||||
fixedMetrics.metrics.height = kLocalGlyphLogicalSize;
|
||||
fixedMetrics.metrics.left = -1.5f;
|
||||
fixedMetrics.metrics.advance = 24;
|
||||
fixedMetrics.metrics.isDoubleResolution = true;
|
||||
|
||||
PremultipliedImage rgbaBitmap = impl->drawGlyphBitmap(fontStack, glyphID);
|
||||
fixedMetrics.metrics.top = impl->getLastGlyphTop();
|
||||
|
||||
// Copy alpha values from RGBA bitmap into the AlphaImage output
|
||||
fixedMetrics.bitmap = AlphaImage(size);
|
||||
for (uint32_t i = 0; i < size.width * size.height; i++) {
|
||||
// Copy alpha values from RGBA bitmap into the AlphaImage output. The Java side
|
||||
// produces a kLocalGlyphBitmapSize x kLocalGlyphBitmapSize bitmap.
|
||||
const Size bitmapSize(kLocalGlyphBitmapSize, kLocalGlyphBitmapSize);
|
||||
fixedMetrics.bitmap = AlphaImage(bitmapSize);
|
||||
for (uint32_t i = 0; i < bitmapSize.width * bitmapSize.height; i++) {
|
||||
fixedMetrics.bitmap.data[i] = 0xff -
|
||||
round(0.2126 * rgbaBitmap.data[4 * i] + 0.7152 * rgbaBitmap.data[4 * i + 1] +
|
||||
0.0722 * rgbaBitmap.data[4 * i + 2]);
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include <jni/jni.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/*
|
||||
android::LocalGlyphRasterizer is the JNI wrapper of
|
||||
org/maplibre/android/text/LocalGlyphRasterizer
|
||||
@@ -24,6 +26,7 @@ public:
|
||||
LocalGlyphRasterizer();
|
||||
|
||||
PremultipliedImage drawGlyphBitmap(const std::string& fontFamily, const bool bold, const char16_t glyphID);
|
||||
float getLastGlyphTop();
|
||||
|
||||
private:
|
||||
jni::Global<jni::Object<LocalGlyphRasterizer>, jni::EnvAttachingDeleter> javaObject;
|
||||
|
||||
@@ -4,38 +4,76 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* LocalGlyphRasterizer is the Android-specific platform implementation used
|
||||
* by the portable local_glyph_rasterizer.hpp
|
||||
*/
|
||||
@Keep
|
||||
public class LocalGlyphRasterizer {
|
||||
private static final int TEXTURE_SCALE = 2;
|
||||
private static final int BITMAP_SIZE = 60;
|
||||
private static final int TEXT_SIZE = 48;
|
||||
private static final int RASTER_BUFFER = 3 * TEXTURE_SCALE;
|
||||
private static final float TOP_ADJUSTMENT = 26.5f;
|
||||
private static final float BASELINE_X = 10;
|
||||
|
||||
private final Bitmap bitmap;
|
||||
@NonNull
|
||||
private final Paint paint;
|
||||
@NonNull
|
||||
private final Canvas canvas;
|
||||
@NonNull
|
||||
private final Rect textBounds;
|
||||
@NonNull
|
||||
// Tiny cache (font stacks × 2 for bold/regular) so glyphs don't
|
||||
// pay Typeface.create's cache lookup and internal lock per call.
|
||||
private final Map<String, Typeface> typefaceCache;
|
||||
// Updated by drawGlyphBitmap so native code can read the measured top without
|
||||
// allocating a per-glyph result object.
|
||||
private float glyphTop;
|
||||
|
||||
LocalGlyphRasterizer() {
|
||||
/*
|
||||
35x35px dimensions are hardwired to match local_glyph_rasterizer.cpp
|
||||
These dimensions are large enough to draw a 24 point character in the middle
|
||||
of the bitmap (y: 20) with some buffer around the edge
|
||||
60x60px dimensions are hardwired to match local_glyph_rasterizer.cpp.
|
||||
Glyphs are drawn at 2x texture resolution, while logical metrics stay at 1x.
|
||||
These dimensions are large enough to draw a 48 px character in the middle
|
||||
of the bitmap with some buffer around the edge.
|
||||
*/
|
||||
bitmap = Bitmap.createBitmap(35, 35, Bitmap.Config.ARGB_8888);
|
||||
bitmap = Bitmap.createBitmap(BITMAP_SIZE, BITMAP_SIZE, Bitmap.Config.ARGB_8888);
|
||||
|
||||
paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
paint.setTextSize(24);
|
||||
paint.setTextSize(TEXT_SIZE);
|
||||
|
||||
canvas = new Canvas();
|
||||
canvas.setBitmap(bitmap);
|
||||
textBounds = new Rect();
|
||||
typefaceCache = new HashMap<>();
|
||||
}
|
||||
|
||||
private void setTypeface(String fontFamily, boolean bold) {
|
||||
String key = (bold ? "bold:" : "regular:") + fontFamily;
|
||||
Typeface typeface = typefaceCache.get(key);
|
||||
if (typeface == null) {
|
||||
typeface = Typeface.create(fontFamily, bold ? Typeface.BOLD : Typeface.NORMAL);
|
||||
typefaceCache.put(key, typeface);
|
||||
}
|
||||
paint.setTypeface(typeface);
|
||||
}
|
||||
|
||||
private int measureGlyphTop(String glyph) {
|
||||
paint.getTextBounds(glyph, 0, glyph.length(), textBounds);
|
||||
return Math.max(0, -textBounds.top);
|
||||
}
|
||||
|
||||
/***
|
||||
@@ -51,9 +89,17 @@ public class LocalGlyphRasterizer {
|
||||
*/
|
||||
@WorkerThread
|
||||
protected Bitmap drawGlyphBitmap(String fontFamily, boolean bold, char glyphID) {
|
||||
paint.setTypeface(Typeface.create(fontFamily, bold ? Typeface.BOLD : Typeface.NORMAL));
|
||||
setTypeface(fontFamily, bold);
|
||||
String glyph = String.valueOf(glyphID);
|
||||
int glyphTopPx = measureGlyphTop(glyph);
|
||||
glyphTop = glyphTopPx / (float) TEXTURE_SCALE - TOP_ADJUSTMENT;
|
||||
canvas.drawColor(Color.WHITE);
|
||||
canvas.drawText(String.valueOf(glyphID), 5, 25, paint);
|
||||
canvas.drawText(glyph, BASELINE_X, RASTER_BUFFER + glyphTopPx, paint);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
protected float getLastGlyphTop() {
|
||||
return glyphTop;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user