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:
Taku Fukada
2026-07-01 20:52:01 +09:00
committed by GitHub
parent 60f15190c3
commit 887e6021e6
22 changed files with 357 additions and 84 deletions

View File

@@ -5,6 +5,7 @@
### ✨ New features
- *...Add new stuff here...*
- [core] Locally rasterized CJK glyphs now render at 2x texture resolution, preserving fine strokes. Mirrors [maplibre-gl-js#3006](https://github.com/maplibre/maplibre-gl-js/pull/3006).
- [core] Added new map observer events: onPreCompileShader, onPostCompileShader, onShaderCompileFailed, onGlyphsLoaded, onGlyphsError, onGlyphsRequested, onTileAction, onSpriteLoaded, onSpriteError, onSpriteRequested ([#2694](https://github.com/maplibre/maplibre-native/pull/2694)).
- [core] Add WebP image decoding support to default platform (Linux, Windows)
- [core] All CMake properties are now prefixed `MLN_*` [1054](https://github.com/maplibre/maplibre-native/pull/1054).

View File

@@ -60,14 +60,17 @@ GlyphRange getGlyphRange(GlyphID glyph);
struct GlyphMetrics {
uint32_t width = 0;
uint32_t height = 0;
int32_t left = 0;
int32_t top = 0;
float left = 0;
float top = 0;
uint32_t advance = 0;
// True when the glyph bitmap is rendered at 2x texture resolution
// (e.g. local CJK glyphs).
bool isDoubleResolution = false;
};
inline bool operator==(const GlyphMetrics &lhs, const GlyphMetrics &rhs) {
return lhs.width == rhs.width && lhs.height == rhs.height && lhs.left == rhs.left && lhs.top == rhs.top &&
lhs.advance == rhs.advance;
lhs.advance == rhs.advance && lhs.isDoubleResolution == rhs.isDoubleResolution;
}
class Glyph {

View File

@@ -31,8 +31,9 @@ public:
constexpr uint32_t GLYPHS_PER_GLYPH_RANGE = 256;
constexpr uint32_t GLYPH_RANGES_PER_FONT_STACK = 256;
// 256 - 126 ranges skipped w/ i18n::allowsFixedWidthGlyphGeneration
constexpr uint32_t NON_IDEOGRAPH_GLYPH_RANGES_PER_FONT_STACK = 130;
// Verified against i18n::glyphRangeIsEntirelyLocallyGenerated by
// i18n.GlyphRangeLocalGenerationCountMatchesConstant.
constexpr uint32_t NON_IDEOGRAPH_GLYPH_RANGES_PER_FONT_STACK = 97;
} // end namespace mbgl

View File

@@ -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]);

View File

@@ -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;

View File

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

View File

@@ -6,6 +6,7 @@
#include <mbgl/interface/native_apple_interface.h>
#include <cmath>
#include <unordered_map>
#import <CoreText/CoreText.h>
@@ -19,6 +20,14 @@
namespace mbgl {
// 2x bitmap with 1x logical metrics.
constexpr uint32_t kLocalGlyphTextureScale = 2;
constexpr CGFloat kLocalGlyphFontSize = util::ONE_EM * kLocalGlyphTextureScale;
constexpr uint32_t kLocalGlyphLogicalSize = 30;
constexpr uint32_t kLocalGlyphBitmapSize = kLocalGlyphLogicalSize * kLocalGlyphTextureScale;
constexpr CGFloat kLocalGlyphRasterBuffer = Glyph::borderSize * kLocalGlyphTextureScale;
constexpr CGFloat kLocalGlyphTopAdjustment = 26.5;
using CGColorSpaceHandle = CFHandle<CGColorSpaceRef, CGColorSpaceRef, CGColorSpaceRelease>;
using CGContextHandle = CFHandle<CGContextRef, CGContextRef, CGContextRelease>;
using CFStringRefHandle = CFHandle<CFStringRef, CFTypeRef, CFRelease>;
@@ -35,7 +44,7 @@ using CTLineRefHandle = CFHandle<CTLineRef, CFTypeRef, CFRelease>;
typesetting text using glyph sheets downloaded from a server.
This implementation is similar to the local glyph rasterization in GL JS:
- Only CJK glyphs are drawn locally, because it’s much more noticeable when a
- Only CJK-related BMP ranges are drawn locally, because it’s much more noticeable when a
non-CJK font mismatches the style than when a CJK font mismatches the style.
(Unlike GL JS, this implementation does respect the font’s metrics, so it
does work with variable-width fonts.)
@@ -44,8 +53,6 @@ using CTLineRefHandle = CFHandle<CTLineRef, CFTypeRef, CFRelease>;
This is a first step toward fully local font rendering:
<https://github.com/mapbox/mapbox-gl-native/issues/7862>. Further improvements:
- Make sure the font size is 24 points (`util::ONE_EM`) at all times, not the
system default of 12 points, to avoid blobbiness after rasterization.
- Sniff an appropriate font weight and style from the font stack’s font names,
as GL JS and the Android map SDK do.
- Enable local glyph rasterization for all writing systems, not just CJK. This
@@ -124,7 +131,7 @@ public:
if (!fontNames.count) {
NSDictionary *fontAttributes = @{
(NSString *)kCTFontSizeAttribute : @(util::ONE_EM),
(NSString *)kCTFontSizeAttribute : @(kLocalGlyphFontSize),
};
return CTFontDescriptorCreateWithAttributes((CFDictionaryRef)fontAttributes);
}
@@ -136,7 +143,7 @@ public:
CFArrayCreateMutable(kCFAllocatorDefault, fontNames.count, &kCFTypeArrayCallBacks));
for (NSString *name in [fontNames subarrayWithRange:NSMakeRange(1, fontNames.count - 1)]) {
NSDictionary *fontAttributes = @{
(NSString *)kCTFontSizeAttribute : @(util::ONE_EM),
(NSString *)kCTFontSizeAttribute : @(kLocalGlyphFontSize),
// This attribute is technically supposed to be a font’s
// PostScript name, but Core Text will fall back to matching
// font display names and font family names.
@@ -154,7 +161,7 @@ public:
kCTFontCascadeListAttribute,
};
CFTypeRef values[] = {
(__bridge CFNumberRef) @(util::ONE_EM),
(__bridge CFNumberRef) @(kLocalGlyphFontSize),
mainFontName,
*fallbackDescriptors,
};
@@ -241,9 +248,11 @@ PremultipliedImage drawGlyphBitmap(GlyphID glyphID, CTFontRef font, GlyphMetrics
throw std::runtime_error("Unable to create line from attributed string");
}
Size size(35, 35);
metrics.width = size.width;
metrics.height = size.height;
// Bitmap is allocated at 2x texture resolution; metrics report 1x logical size.
Size size(kLocalGlyphBitmapSize, kLocalGlyphBitmapSize);
metrics.width = kLocalGlyphLogicalSize;
metrics.height = kLocalGlyphLogicalSize;
metrics.isDoubleResolution = true;
PremultipliedImage rgbaBitmap(size);
@@ -270,16 +279,27 @@ PremultipliedImage drawGlyphBitmap(GlyphID glyphID, CTFontRef font, GlyphMetrics
CFRange wholeRunRange = CFRangeMake(0, CTRunGetGlyphCount(glyphRun));
std::vector<CGSize> advances(wholeRunRange.length);
CTRunGetAdvances(glyphRun, wholeRunRange, advances.data());
metrics.advance = std::round(advances[0].width);
// CTRunGetAdvances returns advances measured at the rasterization (2x) font
// size; report the logical 1x value using the historical integer metric.
metrics.advance = std::round(advances[0].width / static_cast<CGFloat>(kLocalGlyphTextureScale));
// Mimic glyph PBF metrics.
metrics.left = Glyph::borderSize;
metrics.top = -Glyph::borderSize;
const CGRect glyphBounds = CTRunGetImageBounds(glyphRun, *context, wholeRunRange);
CGFloat glyphTop = 0.0;
if (!CGRectIsNull(glyphBounds)) {
const CGFloat maxY = CGRectGetMaxY(glyphBounds);
if (std::isfinite(maxY)) {
glyphTop = maxY;
}
}
// Move the text upward to avoid clipping off descenders.
CGFloat descent;
CTRunGetTypographicBounds(glyphRun, wholeRunRange, NULL, &descent, NULL);
CGContextSetTextPosition(*context, 0.0, descent);
// GL JS/TinySDF records glyphTop per character and applies a calibrated
// topAdjustment. Keep the same model here so fallback fonts do not inherit a
// platform-wide fixed top value.
metrics.left = Glyph::borderSize + 0.5f;
metrics.top = glyphTop / kLocalGlyphTextureScale - kLocalGlyphTopAdjustment;
const CGFloat textBaselineY = size.height - kLocalGlyphRasterBuffer - glyphTop;
CGContextSetTextPosition(*context, 0.0, textBaselineY);
CTLineDraw(*line, *context);
@@ -318,8 +338,9 @@ Glyph LocalGlyphRasterizer::rasterizeGlyph(const FontStack &fontStack, GlyphID g
PremultipliedImage rgbaBitmap =
drawGlyphBitmap(glyphID, *font, manufacturedGlyph.metrics, isBold);
Size size(manufacturedGlyph.metrics.width, manufacturedGlyph.metrics.height);
const Size size = rgbaBitmap.size;
// Copy alpha values from RGBA bitmap into the AlphaImage output
// The bitmap may be larger than the logical metrics (2x texture resolution).
manufacturedGlyph.bitmap = AlphaImage(size);
for (uint32_t i = 0; i < size.width * size.height; i++) {
manufacturedGlyph.bitmap.data[i] = rgbaBitmap.data[4 * i + 3];

View File

@@ -341,9 +341,10 @@ void OfflineDownload::activateDownload() {
const bool includeIdeographs = std::visit([](auto& reg) { return reg.includeIdeographs; }, definition);
for (const auto& fontStack : parser.fontStacks()) {
for (char16_t i = 0; i < GLYPH_RANGES_PER_FONT_STACK; i++) {
// Assumes that if a glyph range starts with fixed width/ideographic
// characters, the entire range will be fixed width.
if (includeIdeographs || !util::i18n::allowsFixedWidthGlyphGeneration(i * GLYPHS_PER_GLYPH_RANGE)) {
// Only skip a range when every codepoint in it can be
// generated locally; some ranges (e.g. 0xFF00) are mixed.
if (includeIdeographs ||
!util::i18n::glyphRangeIsEntirelyLocallyGenerated(i * GLYPHS_PER_GLYPH_RANGE)) {
auto range = getGlyphRange(i * GLYPHS_PER_GLYPH_RANGE);
queueResource(Resource::glyphs(
parser.glyphURL, fontStack, std::pair<uint16_t, uint16_t>{range.first, range.second}));

View File

@@ -7,9 +7,15 @@
#include <QtGui/QImage>
#include <QtGui/QPainter>
#include <qglobal.h>
namespace mbgl {
// 2x bitmap with 1x logical metrics.
constexpr int kLocalGlyphTextureScale = 2;
constexpr uint32_t kLocalGlyphLogicalSize = 30;
constexpr uint32_t kLocalGlyphBitmapSize = kLocalGlyphLogicalSize * kLocalGlyphTextureScale;
constexpr int kLocalGlyphRasterBuffer = Glyph::borderSize * kLocalGlyphTextureScale;
constexpr double kLocalGlyphTopAdjustment = 26.5;
class LocalGlyphRasterizer::Impl {
public:
Impl(const std::optional<std::string>& fontFamily_);
@@ -25,7 +31,7 @@ LocalGlyphRasterizer::Impl::Impl(const std::optional<std::string>& fontFamily_)
: fontFamily(fontFamily_) {
if (isConfigured()) {
font.setFamily(QString::fromStdString(*fontFamily));
font.setPixelSize(util::ONE_EM);
font.setPixelSize(util::ONE_EM * kLocalGlyphTextureScale);
metrics = QFontMetrics(font);
}
}
@@ -53,25 +59,39 @@ Glyph LocalGlyphRasterizer::rasterizeGlyph(const FontStack&, GlyphID glyphID) {
return glyph;
}
glyph.metrics.width = impl->metrics->horizontalAdvance(glyphID.complex.code);
glyph.metrics.height = impl->metrics->height();
glyph.metrics.left = 3;
glyph.metrics.top = -8;
// QFontMetrics returns measurements at the 2x rasterization pixel size; the
// glyph's metrics are reported in 1x logical units.
const QString text(QChar(glyphID.complex.code));
const int advance2x = impl->metrics->horizontalAdvance(glyphID.complex.code);
const int height2x = impl->metrics->height();
const QRect glyphBounds = impl->metrics->tightBoundingRect(text);
const int glyphTop2x = std::max(0, -glyphBounds.top());
glyph.metrics.width = advance2x / kLocalGlyphTextureScale;
glyph.metrics.height = height2x / kLocalGlyphTextureScale;
glyph.metrics.left = 3.5f;
glyph.metrics.top = glyphTop2x / static_cast<double>(kLocalGlyphTextureScale) - kLocalGlyphTopAdjustment;
glyph.metrics.advance = glyph.metrics.width;
glyph.metrics.isDoubleResolution = true;
// Set width of a glyph's backing image to be util::ONE_EM.
Size size(util::ONE_EM, glyph.metrics.height);
// The backing image is drawn at 2x texture resolution.
Size size(kLocalGlyphBitmapSize, kLocalGlyphBitmapSize);
QImage image(QSize(size.width, size.height), QImage::Format_Alpha8);
image.fill(qRgba(0, 0, 0, 0));
QPainter painter(&image);
painter.setFont(impl->font);
painter.setRenderHints(QPainter::TextAntialiasing);
// Render at constant baseline, to align with glyphs that are rendered by node-fontnik.
painter.drawText(QPointF(0, 20), QString(QChar(glyphID.complex.code)));
// Align the glyph using its measured top, matching the local TinySDF path
// in GL JS rather than relying on a platform-wide fixed top value.
const double baselineY = kLocalGlyphRasterBuffer + glyphTop2x;
painter.drawText(QPointF(0, baselineY), text);
auto img = std::make_unique<uint8_t[]>(image.sizeInBytes());
memcpy(img.get(), image.constBits(), image.sizeInBytes());
// QImage::Format_Alpha8 may pad each row for alignment. Copy row-by-row
// using the unpadded width so AlphaImage's tight layout matches.
auto img = std::make_unique<uint8_t[]>(static_cast<size_t>(size.width) * size.height);
for (uint32_t y = 0; y < size.height; ++y) {
std::memcpy(img.get() + static_cast<size_t>(y) * size.width, image.constScanLine(y), size.width);
}
glyph.bitmap = AlphaImage{size, std::move(img)};

View File

@@ -71,7 +71,8 @@ void GlyphManager::getGlyphs(GlyphRequestor& requestor, GlyphDependencies glyphD
Glyph GlyphManager::generateLocalSDF(const FontStack& fontStack, GlyphID glyphID) {
Glyph local = localGlyphRasterizer->rasterizeGlyph(fontStack, glyphID);
local.bitmap = util::transformRasterToSDF(local.bitmap, 8, .25);
const double radius = local.metrics.isDoubleResolution ? 16.0 : 8.0;
local.bitmap = util::transformRasterToSDF(local.bitmap, radius, .25);
return local;
}

View File

@@ -11,21 +11,13 @@ namespace mbgl {
the glyph and appropriate metrics. GlyphManager will then use TinySDF to
transform the rasterized bitmap into an SDF.
The JS equivalent of this functionality will only generate glyphs in the
'CJK Unified Ideographs' and 'Hangul Syllables' ranges, for which it can
get away with rendering a fixed 30px square image and GlyphMetrics of:
width: 24,
height: 24,
left: 0,
top: -8,
advance: 24
The JS equivalent also uses heuristic evaluation of the font stack name
to control the font-weight it uses during rasterization.
The shared default codepoint filter is
util::i18n::allowsFixedWidthGlyphGeneration.
Following maplibre-gl-js, platform implementations use 2x bitmaps with
1x metrics via GlyphMetrics::isDoubleResolution.
It is left to platform-specific implementation to decide how best to
map a FontStack to a particular rasterization.
map a FontStack to a particular rasterization (font, weight, style).
The default implementation simply refuses to rasterize any glyphs.
*/

View File

@@ -267,11 +267,15 @@ SymbolQuads getGlyphQuads(const Shaping& shapedText,
builtInOffset = {0.0f, 0.0f};
}
// rectBuffer is in 1x logical units; only rect.w/h carry the texture
// scale. GL JS keeps 2x glyphs centered by storing fractional
// left/top metrics, so native local glyphs do the same.
const float textureScale = positionedGlyph.metrics.isDoubleResolution ? 2.0f : 1.0f;
const float x1 = (positionedGlyph.metrics.left - rectBuffer) * positionedGlyph.scale - halfAdvance +
builtInOffset.x;
const float y1 = (-positionedGlyph.metrics.top - rectBuffer) * positionedGlyph.scale + builtInOffset.y;
const float x2 = x1 + rect.w * positionedGlyph.scale / pixelRatio;
const float y2 = y1 + rect.h * positionedGlyph.scale / pixelRatio;
const float x2 = x1 + rect.w / textureScale * positionedGlyph.scale / pixelRatio;
const float y2 = y1 + rect.h / textureScale * positionedGlyph.scale / pixelRatio;
Point<float> tl{x1, y1};
Point<float> tr{x2, y1};

View File

@@ -18,6 +18,10 @@ namespace {
return codepoint >= (first) && codepoint <= (last); \
}
inline bool isInRange(char16_t codepoint, char16_t first, char16_t last) {
return codepoint >= first && codepoint <= last;
}
// The following table comes from <http://www.unicode.org/Public/12.0.0/ucd/Blocks.txt>.
// Keep it synchronized with <http://www.unicode.org/Public/UCD/latest/ucd/Blocks.txt>.
@@ -411,8 +415,59 @@ bool allowsIdeographicBreaking(char16_t chr) {
}
bool allowsFixedWidthGlyphGeneration(char16_t chr) {
// Mirrors conservative set of characters used in glyph_manager.js/_tinySDF
return isInCJKUnifiedIdeographs(chr) || isInHangulSyllables(chr) || isInKatakana(chr) || isInHiragana(chr);
// Mirrors the BMP ranges in GL JS's codePointUsesLocalIdeographFontFamily.
// Astral-plane ranges are not included because GlyphID only carries BMP
// codepoints for local glyph generation.
return
// Bopomofo tone marks.
isInRange(chr, 0x02EA, 0x02EB) ||
// Hangul Jamo.
isInHangulJamo(chr) ||
// CJK Radicals Supplement, Kangxi Radicals.
isInCJKRadicalsSupplement(chr) || isInKangxiRadicals(chr) ||
// CJK Symbols and Punctuation, Hiragana, Katakana.
isInCJKSymbolsandPunctuation(chr) || isInHiragana(chr) || isInKatakana(chr) ||
// Bopomofo.
isInRange(chr, 0x3105, 0x312F) ||
// Hangul Compatibility Jamo.
isInRange(chr, 0x3131, 0x318E) ||
// Bopomofo Extended through CJK Unified Ideographs Extension A.
isInBopomofoExtended(chr) || isInCJKStrokes(chr) || isInKatakanaPhoneticExtensions(chr) ||
isInEnclosedCJKLettersandMonths(chr) || isInCJKCompatibility(chr) || isInCJKUnifiedIdeographsExtensionA(chr) ||
// CJK Unified Ideographs and Yi Syllables.
isInCJKUnifiedIdeographs(chr) || isInRange(chr, 0xA000, 0xA48C) ||
// Yi Radicals.
isInRange(chr, 0xA490, 0xA4C6) ||
// Hangul Jamo Extended-A.
isInRange(chr, 0xA960, 0xA97C) ||
// Hangul Syllables and Hangul Jamo Extended-B.
isInHangulSyllables(chr) || isInRange(chr, 0xD7B0, 0xD7C6) ||
// Hangul Jamo Extended-B.
isInRange(chr, 0xD7CB, 0xD7FB) ||
// CJK Compatibility Ideographs.
isInRange(chr, 0xF900, 0xFA6D) ||
// CJK Compatibility Ideographs.
isInRange(chr, 0xFA70, 0xFAD9) ||
// Vertical Forms.
isInVerticalForms(chr) ||
// CJK Compatibility Forms.
isInCJKCompatibilityForms(chr) ||
// Halfwidth and Fullwidth Forms.
isInHalfwidthandFullwidthForms(chr);
}
bool glyphRangeIsEntirelyLocallyGenerated(char16_t rangeStart) {
// Some ranges (notably 0xFF00, where Halfwidth/Fullwidth Forms ends at
// 0xFFEF and the Specials block continues to 0xFFFD) mix locally-rendered
// and non-local codepoints, so it is not safe to infer the whole range
// from the first codepoint alone.
constexpr uint32_t glyphsPerRange = 256;
for (uint32_t offset = 0; offset < glyphsPerRange; ++offset) {
if (!allowsFixedWidthGlyphGeneration(static_cast<char16_t>(rangeStart + offset))) {
return false;
}
}
return true;
}
bool allowsVerticalWritingMode(const std::u16string& string) {

View File

@@ -27,6 +27,10 @@ bool allowsIdeographicBreaking(char16_t chr);
advances */
bool allowsFixedWidthGlyphGeneration(char16_t chr);
/** Returns whether the whole 256-codepoint glyph range can be generated
locally. Use this for range-level decisions because some ranges are mixed. */
bool glyphRangeIsEntirelyLocallyGenerated(char16_t rangeStart);
/** Returns whether any substring of the given string can be drawn as vertical
text with upright glyphs. */
bool allowsVerticalWritingMode(const std::u16string& string);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -10,6 +10,7 @@
#include <mbgl/storage/offline_database.hpp>
#include <mbgl/storage/offline_download.hpp>
#include <mbgl/storage/http_file_source.hpp>
#include <mbgl/text/glyph_range.hpp>
#include <mbgl/util/run_loop.hpp>
#include <mbgl/util/io.hpp>
#include <mbgl/util/compression.hpp>
@@ -346,10 +347,10 @@ TEST(OfflineDownload, ExcludeIdeographs) {
observer->statusChangedFn = [&](OfflineRegionStatus status) {
if (status.complete()) {
EXPECT_EQ(status.completedTileCount, status.requiredTileCount);
EXPECT_EQ(138u,
status.completedResourceCount); // 130 glyphs, 2 sprite images,
// 2 sprite jsons, 1 tile, 1
// style, source, image
EXPECT_EQ(NON_IDEOGRAPH_GLYPH_RANGES_PER_FONT_STACK + 8u,
status.completedResourceCount); // glyph ranges, 2 sprite images,
// 2 sprite jsons, 1 tile, 1 style,
// source, image
EXPECT_EQ(test.size, status.completedResourceSize);
download.setState(OfflineRegionDownloadState::Inactive);

View File

@@ -119,6 +119,13 @@ public:
}
};
class KatakanaOnlyLocalGlyphRasterizer : public StubLocalGlyphRasterizer {
public:
bool canRasterizeGlyph(const FontStack&, GlyphID glyphID) override {
return static_cast<char16_t>(glyphID) == u'テ';
}
};
class StubGlyphManagerObserver : public GlyphManagerObserver {
public:
void onGlyphsLoaded(const FontStack& fontStack, const GlyphRange& glyphRange) override {
@@ -148,7 +155,11 @@ public:
StubFileSource fileSource;
StubGlyphManagerObserver observer;
StubGlyphRequestor requestor;
GlyphManager glyphManager{std::make_unique<StubLocalGlyphRasterizer>()};
GlyphManager glyphManager;
GlyphManagerTest(
std::unique_ptr<LocalGlyphRasterizer> localGlyphRasterizer = std::make_unique<StubLocalGlyphRasterizer>())
: glyphManager(std::move(localGlyphRasterizer)) {}
void run(const std::string& url, GlyphDependencies dependencies) {
// Squelch logging.
@@ -317,8 +328,40 @@ TEST(GlyphManager, LoadLocalCJKGlyph) {
GlyphDependencies{.glyphs = {{{{"Test Stack"}}, {u'中'}}}, .shapes = {}});
}
TEST(GlyphManager, LoadLocalCJKGlyphAfterLoadingRangeFromURL) {
TEST(GlyphManager, LoadLocalCJKSymbols) {
GlyphManagerTest test;
int glyphResponses = 0;
test.fileSource.glyphsResponse = [&](const Resource&) {
glyphResponses++;
return std::optional<Response>();
};
test.observer.glyphsLoaded = [&](const FontStack&, const GlyphRange&) {
glyphResponses++;
};
test.requestor.glyphsAvailable = [&](GlyphMap glyphs) {
EXPECT_EQ(glyphResponses, 0); // Local generation should prevent requesting any glyphs.
const auto& testPositions = glyphs.at(FontStackHasher()({{"Test Stack"}}));
ASSERT_EQ(testPositions.size(), 2u);
ASSERT_EQ(testPositions.count(u'〒'), 1u);
ASSERT_EQ(testPositions.count(u'!'), 1u);
test.end();
};
test.run("test/fixtures/resources/glyphs.pbf",
GlyphDependencies{.glyphs = {{{{"Test Stack"}}, {u'〒', u'!'}}}, .shapes = {}});
}
TEST(GlyphManager, LocalCJKGlyphPrefersLocalRasterizationOverRange) {
// Verifies that locally-rasterized glyphs (like テ) are produced by the
// local rasterizer instead of using a glyph from an already-loaded PBF
// range.
GlyphManagerTest test(std::make_unique<KatakanaOnlyLocalGlyphRasterizer>());
bool firstGlyphResponse = false;
test.fileSource.glyphsResponse = [&](const Resource&) {
@@ -337,9 +380,6 @@ TEST(GlyphManager, LoadLocalCJKGlyphAfterLoadingRangeFromURL) {
ASSERT_EQ(testPositions.size(), 1u);
ASSERT_EQ(testPositions.count(u'々'), 1u);
// Katakana letter te, should be locally rasterized
// instead of using the glyph recieved from the range
// for the ideagraphic mark
test.glyphManager.getGlyphs(test.requestor,
GlyphDependencies{.glyphs = {{{{"Test Stack"}}, {u'テ'}}}, // 0x30c6
.shapes = {}},
@@ -443,3 +483,16 @@ TEST(GlyphManager, ImmediateFileSource) {
test.run("test/fixtures/resources/glyphs.pbf",
GlyphDependencies{.glyphs = {{{{"Test Stack"}}, {u'a', u'å', u' '}}}, .shapes = {}});
}
// Guards against silent drift between allowsFixedWidthGlyphGeneration's range
// coverage and the precomputed NON_IDEOGRAPH_GLYPH_RANGES_PER_FONT_STACK used
// for offline download progress estimation.
TEST(i18n, GlyphRangeLocalGenerationCountMatchesConstant) {
uint32_t locallyGeneratedRanges = 0;
for (uint32_t i = 0; i < GLYPH_RANGES_PER_FONT_STACK; ++i) {
if (util::i18n::glyphRangeIsEntirelyLocallyGenerated(static_cast<char16_t>(i * GLYPHS_PER_GLYPH_RANGE))) {
++locallyGeneratedRanges;
}
}
EXPECT_EQ(GLYPH_RANGES_PER_FONT_STACK - locallyGeneratedRanges, NON_IDEOGRAPH_GLYPH_RANGES_PER_FONT_STACK);
}

View File

@@ -65,7 +65,7 @@ TEST(LocalGlyphRasterizer, PingFang) {
test.map.getStyle().loadJSON(util::read_file("test/fixtures/local_glyphs/mixed.json"));
#if defined(__APPLE__) && !defined(__QT__)
test.checkRendering("ping_fang", 0.0161);
test.checkRendering("ping_fang", 0.025);
#elif defined(__QT__)
test.checkRendering("ping_fang_qt");
#endif // defined(__APPLE__)
@@ -77,7 +77,7 @@ TEST(LocalGlyphRasterizer, PingFangWithBoldInStyle) {
ss << std::regex_replace(
util::read_file("test/fixtures/local_glyphs/mixed.json"), std::regex("NotoCJK"), "NotoCJK Bold");
test.map.getStyle().loadJSON(ss.str());
test.checkRendering("ping_fang_with_bold_in_style");
test.checkRendering("ping_fang_with_bold_in_style", 0.05);
}
#if !defined(__QT__)
@@ -85,7 +85,7 @@ TEST(LocalGlyphRasterizer, PingFangSemibold) {
LocalGlyphRasterizerTest test(std::string("PingFang TC Semibold"));
test.map.getStyle().loadJSON(util::read_file("test/fixtures/local_glyphs/mixed.json"));
test.checkRendering("ping_fang_semibold", 0.0161);
test.checkRendering("ping_fang_semibold", 0.025);
}
#endif // !defined(__QT__)

View File

@@ -10,6 +10,55 @@
using namespace mbgl;
using namespace mbgl::style;
// Atlas padding is not scaled with textureScale, so a 2x glyph quad is 1
// logical pixel smaller than its 1x equivalent. GL JS-style fractional metrics
// keep its center aligned with the 1x quad.
TEST(getGlyphQuads, DoubleResolutionGlyphPreservesLogicalQuadSize) {
GlyphMetrics metrics1x;
metrics1x.width = 24;
metrics1x.height = 24;
metrics1x.left = 0;
metrics1x.top = -8;
metrics1x.advance = 24;
metrics1x.isDoubleResolution = false;
GlyphMetrics metrics2x = metrics1x;
metrics2x.left += 0.5f;
metrics2x.top -= 0.5f;
metrics2x.isDoubleResolution = true;
// Rects reflect the real DynamicTextureAtlas output: bitmap.size + 2 *
// ImagePosition::padding (= 2). 1x bitmap 30 -> rect 32; 2x bitmap 60 -> rect 62.
Shaping shapedText1x;
shapedText1x.positionedLines.emplace_back();
shapedText1x.positionedLines.back().positionedGlyphs.emplace_back(
PositionedGlyph(u'A', 0.0f, 0.0f, false, 0, 1.0f, Rect<uint16_t>(0, 0, 32, 32), metrics1x, std::nullopt));
Shaping shapedText2x;
shapedText2x.positionedLines.emplace_back();
shapedText2x.positionedLines.back().positionedGlyphs.emplace_back(
PositionedGlyph(u'A', 0.0f, 0.0f, false, 0, 1.0f, Rect<uint16_t>(0, 0, 62, 62), metrics2x, std::nullopt));
SymbolLayoutProperties::Evaluated layout;
SymbolQuads quads1x = getGlyphQuads(shapedText1x, {{0, 0}}, layout, SymbolPlacementType::Point, {}, false);
SymbolQuads quads2x = getGlyphQuads(shapedText2x, {{0, 0}}, layout, SymbolPlacementType::Point, {}, false);
ASSERT_EQ(quads1x.size(), 1u);
ASSERT_EQ(quads2x.size(), 1u);
// The 2x quad is 1 logical pixel smaller, and shifted by 0.5px to keep the
// same center as the 1x quad.
EXPECT_FLOAT_EQ(quads1x[0].tl.x + 0.5f, quads2x[0].tl.x);
EXPECT_FLOAT_EQ(quads1x[0].tl.y + 0.5f, quads2x[0].tl.y);
EXPECT_FLOAT_EQ(quads1x[0].br.x - 0.5f, quads2x[0].br.x);
EXPECT_FLOAT_EQ(quads1x[0].br.y - 0.5f, quads2x[0].br.y);
EXPECT_FLOAT_EQ(quads1x[0].br.x - quads1x[0].tl.x, 32.0f);
EXPECT_FLOAT_EQ(quads2x[0].br.x - quads2x[0].tl.x, 31.0f);
EXPECT_FLOAT_EQ(quads1x[0].br.y - quads1x[0].tl.y, 32.0f);
EXPECT_FLOAT_EQ(quads2x[0].br.y - quads2x[0].tl.y, 31.0f);
}
TEST(getIconQuads, normal) {
SymbolLayoutProperties::Evaluated layout;
Anchor anchor(2.0, 3.0, 0.0, 0);