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>
@@ -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).
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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}));
|
||||
|
||||
@@ -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)};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
BIN
test/fixtures/local_glyphs/ping_fang/expected.png
vendored
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||