diff --git a/CHANGELOG.md b/CHANGELOG.md index 823a47415f4..7308c2e3c89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,10 +26,18 @@ By default, the source is not volatile. -### Bug fixes +- [ios, macos] Allow specifying multiple fonts or font families for local font rendering ([#16253](https://github.com/mapbox/mapbox-gl-native/pull/16253)) + + By default, CJK characters are now set in the font specified by the `text-font` layout property. If the named font is not installed on the device or bundled with the application, the characters are set in one of the fallback fonts passed into the `localFontFamily` parameter of `mbgl::Renderer::Renderer()` and `mbgl::MapSnapshotter::MapSnapshotter()`. This parameter can now contain a list of font family names, font display names, and font PostScript names, each name separated by a newline. + +### 🐞 Bug fixes - [ios, macos] Fixed error receiving local file URL response ([#16428](https://github.com/mapbox/mapbox-gl-native/pull/16428)) +- [ios, macos] Corrected metrics of locally rendered fonts ([#16253](https://github.com/mapbox/mapbox-gl-native/pull/16253)) + + CJK characters are now laid out according to the font, so fonts with nonsquare glyphs have the correct kerning. This also fixes an issue where the baseline for CJK characters was too low compared to non-CJK characters. + ## maps-v1.6.0-rc.1 ### ✨ New features @@ -205,7 +213,6 @@ - When feature is exactly on the geometry boundary, `within` expression returns inconsistent values for different zoom levels ([#16301](https://github.com/mapbox/mapbox-gl-native/issues/16301)) - ## maps-v1.3.0 (2020.02-relvanillashake) ### 🐞 Bug fixes diff --git a/include/mbgl/util/constants.hpp b/include/mbgl/util/constants.hpp index 56f42ac8940..bb026817c81 100644 --- a/include/mbgl/util/constants.hpp +++ b/include/mbgl/util/constants.hpp @@ -66,6 +66,9 @@ constexpr uint32_t DEFAULT_MAXIMUM_CONCURRENT_REQUESTS = 20; constexpr uint8_t TERRAIN_RGB_MAXZOOM = 15; +constexpr const char* LAST_RESORT_ALPHABETIC_FONT = "Open Sans Regular"; +constexpr const char* LAST_RESORT_PAN_UNICODE_FONT = "Arial Unicode MS Regular"; + } // namespace util namespace debug { diff --git a/platform/darwin/src/local_glyph_rasterizer.mm b/platform/darwin/src/local_glyph_rasterizer.mm index 6ff346873ae..c9e0b960a2c 100644 --- a/platform/darwin/src/local_glyph_rasterizer.mm +++ b/platform/darwin/src/local_glyph_rasterizer.mm @@ -1,6 +1,7 @@ #include #include #include +#include #include @@ -10,82 +11,155 @@ #import "CFHandle.hpp" -namespace mbgl { +/// Enables local glyph rasterization for all writing systems, not just CJK. +#define MBGL_DARWIN_NO_REMOTE_FONTS 0 -/* - Darwin implementation of LocalGlyphRasterizer: - Draws CJK glyphs using locally available fonts. - - Mirrors GL JS implementation in that: - - Only CJK glyphs are drawn locally (because we can guess their metrics effectively) - * Render size/metrics determined experimentally by rendering a few different fonts - - Configuration is done at map creation time by setting a "font family" - * JS uses a CSS font-family, this uses kCTFontFamilyNameAttribute which has - somewhat different behavior. - - Further improvements are possible: - - GL JS heuristically determines a font weight based on the strings included in - the FontStack. Android follows a simpler heuristic that just picks up the - "Bold" property from the FontStack. Although both should be possible with CoreText, - our initial implementation couldn't reliably control the font-weight, so we're - skipping that functionality on darwin. - (See commit history for attempted implementation) - - If we could reliably extract glyph metrics, we wouldn't be limited to CJK glyphs - - We could push the font configuration down to individual style layers, which would - allow any current style to be reproducible using local fonts. - - Instead of just exposing "font family" as a configuration, we could expose a richer - CTFontDescriptor configuration option (although we'd have to override font size to - make sure it stayed at 24pt). - - Because Apple exposes glyph paths via `CTFontCreatePathForGlyph` we could potentially - render directly to SDF instead of going through TinySDF -- although it's not clear - how much of an improvement it would be. -*/ +namespace mbgl { using CGColorSpaceHandle = CFHandle; using CGContextHandle = CFHandle; using CFStringRefHandle = CFHandle; using CFAttributedStringRefHandle = CFHandle; +using CFMutableArrayRefHandle = CFHandle; using CFDictionaryRefHandle = CFHandle; +using CTFontRefHandle = CFHandle; using CTFontDescriptorRefHandle = CFHandle; using CTLineRefHandle = CFHandle; +/** + Draws glyphs applying fonts that are installed on the system or bundled with + the application. This is a more flexible and performant alternative to + 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 + 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.) + - Fallback fonts can be specified globally, similar to the seldom-used + advanced font preferences in most Web browsers. + + This is a first step toward fully local font rendering: + . 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 + would require providing a more attractive default Latin font than Helvetica + or Arial Unicode MS. + - Allow the developer to specify a `CTFontDescriptor` or + `NSFontDescriptor`/`UIFontDescriptor` to customize more attributes besides + the font (but not the font size). + - Render glyphs directly to SDF using Core Text’s `CTFontCreatePathForGlyph()` + instead of going through TinySDF. + - Typeset an entire text label in one shot using `CTFramesetter` to take + advantage of Core Text’s superior complex text shaping capabilities and + support for astral plane characters. mbgl would need to stop conflating + codepoints with glyph IDs. +*/ class LocalGlyphRasterizer::Impl { public: - Impl(const optional fontFamily_) - : fontFamily(fontFamily_) - , fontHandle(NULL) - {} - - ~Impl() { - if (fontHandle) { - CFRelease(fontHandle); + /** + Creates a new rasterizer with the given font names as a fallback. + + The fallback font names can also be specified in the style as a font stack + or in the `MGLIdeographicFontFamilyName` key of + `NSUserDefaults.standardUserDefaults`. The font stack takes precedence, + followed by the `MGLIdeographicFontFamilyName` user default, then finally + the `fallbackFontNames_` parameter as a last resort. + + @param fallbackFontNames_ A list of font names, one per line. Each font + name can be the PostScript name or display name of a specific font face + or a font family name. Set this parameter to `nullptr` to disable local + glyph rasterization globally. The system font is a good default value to + pass into this constructor. + */ + Impl(const optional fallbackFontNames_) + { + fallbackFontNames = [[NSUserDefaults standardUserDefaults] stringArrayForKey:@"MGLIdeographicFontFamilyName"]; + if (fallbackFontNames_) { + fallbackFontNames = [fallbackFontNames ?: @[] arrayByAddingObjectsFromArray:[@(fallbackFontNames_->c_str()) componentsSeparatedByString:@"\n"]]; } } - - CTFontRef getFont() { - if (!fontFamily) { - return NULL; + /** + Returns whether local glyph rasterization is enabled globally. + + The developer can disable local glyph rasterization by specifying no + fallback font names. + */ + bool isEnabled() { return fallbackFontNames; } + + /** + Creates a font descriptor representing the given font stack and any global + fallback fonts. + + @param fontStack The font stack that takes precedence. + @returns A font descriptor representing the first font in the font stack + with a cascade list representing the rest of the fonts in the font stack + and any fallback fonts. The font descriptor is not cached. + + @post The caller is responsible for releasing the font descriptor. + */ + CTFontDescriptorRef createFontDescriptor(const FontStack& fontStack) { + NSMutableArray *fontNames = [NSMutableArray arrayWithCapacity:fontStack.size() + fallbackFontNames.count]; + for (auto& fontName : fontStack) { + // Per the Mapbox Style Specification, the text-font property comes + // with these last resort fonts by default, but they shouldn’t take + // precedence over any application or system fallback font that may + // be more appropriate to the current device. + if (fontName != util::LAST_RESORT_ALPHABETIC_FONT && fontName != util::LAST_RESORT_PAN_UNICODE_FONT) { + [fontNames addObject:@(fontName.c_str())]; + } } + [fontNames addObjectsFromArray:fallbackFontNames]; - if (!fontHandle) { - NSDictionary *fontAttributes = @{ - (NSString *)kCTFontSizeAttribute: [NSNumber numberWithFloat:24.0], - (NSString *)kCTFontFamilyNameAttribute: [[NSString alloc] initWithCString:fontFamily->c_str() encoding:NSUTF8StringEncoding] + if (!fontNames.count) { + NSDictionary *fontAttributes = @{ + (NSString *)kCTFontSizeAttribute: @(util::ONE_EM), }; - + return CTFontDescriptorCreateWithAttributes((CFDictionaryRef)fontAttributes); + } + + // Apply the first font name to the returned font descriptor; apply the + // rest of the font names to the cascade list. + CFStringRef mainFontName = (__bridge CFStringRef)fontNames.firstObject; + CFMutableArrayRefHandle fallbackDescriptors(CFArrayCreateMutable(kCFAllocatorDefault, fontNames.count, &kCFTypeArrayCallBacks)); + for (NSString *name in [fontNames subarrayWithRange:NSMakeRange(1, fontNames.count - 1)]) { + NSDictionary *fontAttributes = @{ + (NSString *)kCTFontSizeAttribute: @(util::ONE_EM), + // 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. + (NSString *)kCTFontNameAttribute: name, + }; + CTFontDescriptorRefHandle descriptor(CTFontDescriptorCreateWithAttributes((CFDictionaryRef)fontAttributes)); - fontHandle = CTFontCreateWithFontDescriptor(*descriptor, 0.0, NULL); - if (!fontHandle) { - throw std::runtime_error("CTFontCreateWithFontDescriptor failed"); - } + CFArrayAppendValue(*fallbackDescriptors, *descriptor); } - return fontHandle; + + CFStringRef keys[] = { + kCTFontSizeAttribute, + kCTFontNameAttribute, + kCTFontCascadeListAttribute, + }; + CFTypeRef values[] = { + (__bridge CFNumberRef)@(util::ONE_EM), + mainFontName, + *fallbackDescriptors, + }; + + CFDictionaryRefHandle attributes( + CFDictionaryCreate(kCFAllocatorDefault, (const void**)&keys, + (const void**)&values, sizeof(keys) / sizeof(keys[0]), + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks)); + return CTFontDescriptorCreateWithAttributes(*attributes); } private: - optional fontFamily; - CTFontRef fontHandle; + NSArray *fallbackFontNames; }; LocalGlyphRasterizer::LocalGlyphRasterizer(const optional& fontFamily) @@ -95,15 +169,63 @@ CTFontRef getFont() { LocalGlyphRasterizer::~LocalGlyphRasterizer() {} +/** + Returns whether the rasterizer can rasterize a glyph for the given codepoint. + + @param glyphID A font-agnostic Unicode codepoint, not a glyph index. + @returns Whether a glyph for the codepoint can be rasterized. + */ bool LocalGlyphRasterizer::canRasterizeGlyph(const FontStack&, GlyphID glyphID) { - return util::i18n::allowsFixedWidthGlyphGeneration(glyphID) && impl->getFont(); +#if MBGL_DARWIN_NO_REMOTE_FONTS + return impl->isEnabled(); +#else + return util::i18n::allowsFixedWidthGlyphGeneration(glyphID) && impl->isEnabled(); +#endif } -PremultipliedImage drawGlyphBitmap(GlyphID glyphID, CTFontRef font, Size size) { - PremultipliedImage rgbaBitmap(size); - +/** + Draws the given codepoint into an image, gathers metrics about the glyph, and + returns the image. + + @param glyphID A font-agnostic Unicode codepoint, not a glyph index. + @param font The font to apply to the codepoint. + @param metrics Upon return, the metrics match the font’s metrics for the glyph + representing the codepoint. + @returns An image containing the glyph. + */ +PremultipliedImage drawGlyphBitmap(GlyphID glyphID, CTFontRef font, GlyphMetrics& metrics) { CFStringRefHandle string(CFStringCreateWithCharacters(NULL, reinterpret_cast(&glyphID), 1)); + if (!string) { + throw std::runtime_error("Unable to create string from codepoint"); + } + + CFStringRef keys[] = { kCTFontAttributeName }; + CFTypeRef values[] = { font }; + + CFDictionaryRefHandle attributes( + CFDictionaryCreate(kCFAllocatorDefault, (const void**)&keys, + (const void**)&values, sizeof(keys) / sizeof(keys[0]), + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks)); + if (!attributes) { + throw std::runtime_error("Unable to create attributed string attributes dictionary"); + } + CFAttributedStringRefHandle attrString(CFAttributedStringCreate(kCFAllocatorDefault, *string, *attributes)); + if (!attrString) { + throw std::runtime_error("Unable to create attributed string"); + } + CTLineRefHandle line(CTLineCreateWithAttributedString(*attrString)); + if (!line) { + throw std::runtime_error("Unable to create line from attributed string"); + } + + Size size(35, 35); + metrics.width = size.width; + metrics.height = size.height; + + PremultipliedImage rgbaBitmap(size); + CGColorSpaceHandle colorSpace(CGColorSpaceCreateDeviceRGB()); if (!colorSpace) { throw std::runtime_error("CGColorSpaceCreateDeviceRGB failed"); @@ -124,52 +246,58 @@ CGContextHandle context(CGBitmapContextCreate( if (!context) { throw std::runtime_error("CGBitmapContextCreate failed"); } - - CFStringRef keys[] = { kCTFontAttributeName }; - CFTypeRef values[] = { font }; - - CFDictionaryRefHandle attributes( - CFDictionaryCreate(kCFAllocatorDefault, (const void**)&keys, - (const void**)&values, sizeof(keys) / sizeof(keys[0]), - &kCFTypeDictionaryKeyCallBacks, - &kCFTypeDictionaryValueCallBacks)); - - CFAttributedStringRefHandle attrString(CFAttributedStringCreate(kCFAllocatorDefault, *string, *attributes)); - CTLineRefHandle line(CTLineCreateWithAttributedString(*attrString)); - // Start drawing a little bit below the top of the bitmap - CGContextSetTextPosition(*context, 0.0, 5.0); + CFArrayRef glyphRuns = CTLineGetGlyphRuns(*line); + CTRunRef glyphRun = (CTRunRef)CFArrayGetValueAtIndex(glyphRuns, 0); + CFRange wholeRunRange = CFRangeMake(0, CTRunGetGlyphCount(glyphRun)); + CGSize advances[wholeRunRange.length]; + CTRunGetAdvances(glyphRun, wholeRunRange, advances); + metrics.advance = std::round(advances[0].width); + + // Mimic glyph PBF metrics. + metrics.left = Glyph::borderSize; + metrics.top = -1; + + // Move the text upward to avoid clipping off descenders. + CGFloat descent; + CTRunGetTypographicBounds(glyphRun, wholeRunRange, NULL, &descent, NULL); + CGContextSetTextPosition(*context, 0.0, descent); + CTLineDraw(*line, *context); return rgbaBitmap; } -Glyph LocalGlyphRasterizer::rasterizeGlyph(const FontStack&, GlyphID glyphID) { - Glyph fixedMetrics; - CTFontRef font = impl->getFont(); +/** + Returns all the information mbgl needs about a glyph representation of the + given codepoint. + + @param fontStack The best matching font in this font stack is applied to the + codepoint. + @param glyphID A font-agnostic Unicode codepoint, not a glyph index. + @returns A glyph representation of the given codepoint with its bitmap data and + metrics set. + */ +Glyph LocalGlyphRasterizer::rasterizeGlyph(const FontStack& fontStack, GlyphID glyphID) { + Glyph manufacturedGlyph; + CTFontDescriptorRefHandle descriptor(impl->createFontDescriptor(fontStack)); + CTFontRefHandle font(CTFontCreateWithFontDescriptor(*descriptor, 0.0, NULL)); if (!font) { - return fixedMetrics; + return manufacturedGlyph; } - fixedMetrics.id = glyphID; + manufacturedGlyph.id = glyphID; - Size size(35, 35); + PremultipliedImage rgbaBitmap = drawGlyphBitmap(glyphID, *font, manufacturedGlyph.metrics); - fixedMetrics.metrics.width = size.width; - fixedMetrics.metrics.height = size.height; - fixedMetrics.metrics.left = 3; - fixedMetrics.metrics.top = -1; - fixedMetrics.metrics.advance = 24; - - PremultipliedImage rgbaBitmap = drawGlyphBitmap(glyphID, font, size); - + Size size(manufacturedGlyph.metrics.width, manufacturedGlyph.metrics.height); // Copy alpha values from RGBA bitmap into the AlphaImage output - fixedMetrics.bitmap = AlphaImage(size); + manufacturedGlyph.bitmap = AlphaImage(size); for (uint32_t i = 0; i < size.width * size.height; i++) { - fixedMetrics.bitmap.data[i] = rgbaBitmap.data[4 * i + 3]; + manufacturedGlyph.bitmap.data[i] = rgbaBitmap.data[4 * i + 3]; } - return fixedMetrics; + return manufacturedGlyph; } } // namespace mbgl diff --git a/src/mbgl/style/layers/symbol_layer_impl.cpp b/src/mbgl/style/layers/symbol_layer_impl.cpp index e35e7b0b9fe..60ebe57f96a 100644 --- a/src/mbgl/style/layers/symbol_layer_impl.cpp +++ b/src/mbgl/style/layers/symbol_layer_impl.cpp @@ -27,13 +27,11 @@ void SymbolLayer::Impl::populateFontStack(std::set& fontStack) const } layout.get().match( - [&fontStack] (Undefined) { - fontStack.insert({"Open Sans Regular", "Arial Unicode MS Regular"}); + [&fontStack](Undefined) { + fontStack.insert({util::LAST_RESORT_ALPHABETIC_FONT, util::LAST_RESORT_PAN_UNICODE_FONT}); }, - [&fontStack] (const FontStack& constant) { - fontStack.insert(constant); - }, - [&] (const auto& function) { + [&fontStack](const FontStack& constant) { fontStack.insert(constant); }, + [&](const auto& function) { for (const auto& value : function.possibleOutputs()) { if (value) { fontStack.insert(*value); @@ -42,8 +40,7 @@ void SymbolLayer::Impl::populateFontStack(std::set& fontStack) const break; } } - } - ); + }); } } // namespace style diff --git a/test/fixtures/local_glyphs/ping_fang/expected.png b/test/fixtures/local_glyphs/ping_fang/expected.png index 8c891a52323..6e7e5c80b9c 100644 Binary files a/test/fixtures/local_glyphs/ping_fang/expected.png and b/test/fixtures/local_glyphs/ping_fang/expected.png differ diff --git a/test/fixtures/local_glyphs/ping_fang_semibold/expected.png b/test/fixtures/local_glyphs/ping_fang_semibold/expected.png new file mode 100644 index 00000000000..6bb0a2503fc Binary files /dev/null and b/test/fixtures/local_glyphs/ping_fang_semibold/expected.png differ diff --git a/test/text/local_glyph_rasterizer.test.cpp b/test/text/local_glyph_rasterizer.test.cpp index d109b28f32d..e1eef46843c 100644 --- a/test/text/local_glyph_rasterizer.test.cpp +++ b/test/text/local_glyph_rasterizer.test.cpp @@ -55,7 +55,7 @@ class LocalGlyphRasterizerTest { #if defined(__APPLE__) TEST(LocalGlyphRasterizer, PingFang) { - LocalGlyphRasterizerTest test(std::string("PingFang")); + LocalGlyphRasterizerTest test(std::string("PingFang TC")); test.fileSource->glyphsResponse = [&] (const Resource& resource) { EXPECT_EQ(Resource::Kind::Glyphs, resource.kind); @@ -65,12 +65,27 @@ 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"); + test.checkRendering("ping_fang", 0.0161); #elif defined(__QT__) test.checkRendering("ping_fang_qt"); #endif // defined(__APPLE__) } +#if !defined(__QT__) +TEST(LocalGlyphRasterizer, PingFangSemibold) { + LocalGlyphRasterizerTest test(std::string("PingFang TC Semibold")); + + test.fileSource->glyphsResponse = [&](const Resource& resource) { + EXPECT_EQ(Resource::Kind::Glyphs, resource.kind); + Response response; + response.data = std::make_shared(util::read_file("test/fixtures/resources/glyphs.pbf")); + return response; + }; + test.map.getStyle().loadJSON(util::read_file("test/fixtures/local_glyphs/mixed.json")); + test.checkRendering("ping_fang_semibold", 0.0161); +} +#endif // !defined(__QT__) + #endif // defined(__APPLE__) #if defined(__linux__) && defined(__QT__)