From cb380064bb768c1a20d2260ac4a032dbcd3184fe Mon Sep 17 00:00:00 2001 From: Chris Loer Date: Mon, 27 Nov 2017 12:50:28 -0800 Subject: [ios,macos] Darwin implementation of a CoreText-based LocalGlyphRasterizer. - Changing font weight does not currently appear to be working. - Glyph metric extraction code not working; currently unused. --- include/mbgl/renderer/renderer.hpp | 3 +- platform/darwin/src/CFHandle.hpp | 31 ++++ platform/darwin/src/image.mm | 14 +- platform/darwin/src/local_glyph_rasterizer.mm | 233 ++++++++++++++++++++++++++ platform/default/local_glyph_rasterizer.cpp | 9 + platform/ios/config.cmake | 4 +- platform/macos/config.cmake | 4 +- platform/macos/src/MGLMapView.mm | 10 +- src/mbgl/renderer/renderer.cpp | 5 +- src/mbgl/renderer/renderer_impl.cpp | 5 +- src/mbgl/renderer/renderer_impl.hpp | 2 +- src/mbgl/text/glyph_manager.hpp | 2 +- src/mbgl/text/local_glyph_rasterizer.hpp | 6 +- src/mbgl/util/i18n.cpp | 5 + src/mbgl/util/i18n.hpp | 4 + 15 files changed, 309 insertions(+), 28 deletions(-) create mode 100644 platform/darwin/src/CFHandle.hpp create mode 100644 platform/darwin/src/local_glyph_rasterizer.mm diff --git a/include/mbgl/renderer/renderer.hpp b/include/mbgl/renderer/renderer.hpp index 0937e9334b..db28ee92fc 100644 --- a/include/mbgl/renderer/renderer.hpp +++ b/include/mbgl/renderer/renderer.hpp @@ -25,7 +25,8 @@ class Renderer { public: Renderer(RendererBackend&, float pixelRatio_, FileSource&, Scheduler&, GLContextMode = GLContextMode::Unique, - const optional programCacheDir = {}); + const optional programCacheDir = {}, + const optional localFontFamily = {}); ~Renderer(); void markContextLost(); diff --git a/platform/darwin/src/CFHandle.hpp b/platform/darwin/src/CFHandle.hpp new file mode 100644 index 0000000000..edcc9aafdf --- /dev/null +++ b/platform/darwin/src/CFHandle.hpp @@ -0,0 +1,31 @@ +#pragma once + +/* + CFHandle is a minimal wrapper designed to hold and release CoreFoundation-style handles + It is non-transferrable: wrap it in something like a unique_ptr if you need to pass it around, + or just use unique_ptr with a custom deleter. + CFHandle has no special treatment for null handles -- be careful not to let it hold a null + handle if the behavior of the Releaser isn't defined for null. + + ex: + using CFDataHandle = CFHandle; + + CFDataHandle data(CFDataCreateWithBytesNoCopy( + kCFAllocatorDefault, reinterpret_cast(source.data()), source.size(), + kCFAllocatorNull)); +*/ + +namespace { + +template +struct CFHandle { + CFHandle(HandleType handle_): handle(handle_) {} + ~CFHandle() { Releaser(handle); } + HandleType operator*() { return handle; } + operator bool() { return handle; } +private: + HandleType handle; +}; + +} // namespace + diff --git a/platform/darwin/src/image.mm b/platform/darwin/src/image.mm index 122682883e..3a5adcca0a 100644 --- a/platform/darwin/src/image.mm +++ b/platform/darwin/src/image.mm @@ -2,19 +2,7 @@ #import -namespace { - -template -struct CFHandle { - CFHandle(T t_): t(t_) {} - ~CFHandle() { Releaser(t); } - T operator*() { return t; } - operator bool() { return t; } -private: - T t; -}; - -} // namespace +#import "CFHandle.hpp" using CGImageHandle = CFHandle; using CFDataHandle = CFHandle; diff --git a/platform/darwin/src/local_glyph_rasterizer.mm b/platform/darwin/src/local_glyph_rasterizer.mm new file mode 100644 index 0000000000..35c9c6733b --- /dev/null +++ b/platform/darwin/src/local_glyph_rasterizer.mm @@ -0,0 +1,233 @@ +#include +#include +#include + +#include + +#import +#import +#import + +#import "CFHandle.hpp" + +namespace mbgl { + +/* + 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. + - We use heuristics to extract a font-weight based on the incoming font stack + + Further improvements are possible: + - 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. +*/ + +using CGColorSpaceHandle = CFHandle; +using CGContextHandle = CFHandle; +using CFStringRefHandle = CFHandle; +using CFAttributedStringRefHandle = CFHandle; +using CFDictionaryRefHandle = CFHandle; +using CTFontDescriptorRefHandle = CFHandle; +using CTLineRefHandle = CFHandle; + +class LocalGlyphRasterizer::Impl { +public: + Impl(const optional fontFamily_) + : fontFamily(fontFamily_) + {} + + ~Impl() { + for (auto& pair : fontHandles) { + CFRelease(pair.second); + } + } + + + CTFontRef getFont(const FontStack& fontStack) { + if (!fontFamily) { + return NULL; + } + + if (fontHandles.find(fontStack) == fontHandles.end()) { + + NSDictionary* fontTraits = @{ (NSString *)kCTFontWeightTrait: [NSNumber numberWithFloat:getFontWeight(fontStack)] }; + + NSDictionary *fontAttributes = @{ + (NSString *)kCTFontSizeAttribute: [NSNumber numberWithFloat:24.0], + (NSString *)kCTFontFamilyNameAttribute: [[NSString alloc] initWithCString:fontFamily->c_str() encoding:NSUTF8StringEncoding], + (NSString *)kCTFontTraitsAttribute: fontTraits + //(NSString *)kCTFontStyleNameAttribute: (getFontWeight(fontStack) > .3) ? @"Bold" : @"Regular" + }; + + CTFontDescriptorRefHandle descriptor(CTFontDescriptorCreateWithAttributes((CFDictionaryRef)fontAttributes)); + CTFontRef font = CTFontCreateWithFontDescriptor(*descriptor, 0.0, NULL); + if (!font) { + throw std::runtime_error("CTFontCreateWithFontDescriptor failed"); + } + + fontHandles[fontStack] = font; + } + return fontHandles[fontStack]; + } + +private: + float getFontWeight(const FontStack& fontStack) { + // Analog to logic in glyph_manager.js + // From NSFontDescriptor.h (macOS 10.11+) NSFontWeight*: + constexpr float light = -.4; + constexpr float regular = 0.0; + constexpr float medium = .23; + constexpr float bold = .4; + + float fontWeight = regular; + for (auto font : fontStack) { + // Last font in the fontstack "wins" + std::string lowercaseFont = mbgl::platform::lowercase(font); + if (lowercaseFont.find("bold") != std::string::npos) { + fontWeight = bold; + } else if (lowercaseFont.find("medium") != std::string::npos) { + fontWeight = medium; + } else if (lowercaseFont.find("light") != std::string::npos) { + fontWeight = light; + } + } + + return fontWeight; + } + + std::unordered_map fontHandles; + optional fontFamily; +}; + +LocalGlyphRasterizer::LocalGlyphRasterizer(const optional fontFamily) + : impl(std::make_unique(fontFamily)) +{} + +LocalGlyphRasterizer::~LocalGlyphRasterizer() +{} + +bool LocalGlyphRasterizer::canRasterizeGlyph(const FontStack& fontStack, GlyphID glyphID) { + return util::i18n::allowsFixedWidthGlyphGeneration(glyphID) && impl->getFont(fontStack); +} + +/* +// TODO: In theory we should be able to transform user-coordinate bounding box and advance +// values into pixel glyph metrics. This would remove the need to use fixed glyph metrics +// (which will be slightly off depending on the font), and allow us to return non CJK glyphs +// (which will have variable "advance" values). +void extractGlyphMetrics(CTFontRef font, CTLineRef line) { + CFArrayRef glyphRuns = CTLineGetGlyphRuns(line); + CFIndex runCount = CFArrayGetCount(glyphRuns); + assert(runCount == 1); + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(glyphRuns, 0); + CFIndex glyphCount = CTRunGetGlyphCount(run); + assert(glyphCount == 1); + const CGGlyph *glyphs = CTRunGetGlyphsPtr(run); + + CGRect boundingRects[1]; + boundingRects[0] = CGRectMake(0, 0, 0, 0); + CGSize advances[1]; + advances[0] = CGSizeMake(0,0); + CGRect totalBoundingRect = CTFontGetBoundingRectsForGlyphs(font, kCTFontOrientationDefault, glyphs, boundingRects, 1); + double totalAdvance = CTFontGetAdvancesForGlyphs(font, kCTFontOrientationDefault, glyphs, advances, 1); + + // Break in the debugger to see these values: translating from "user coordinates" to bitmap pixel coordinates + // should be OK, but a lot of glyphs seem to have empty bounding boxes...? + (void)totalBoundingRect; + (void)totalAdvance; +} +*/ + +PremultipliedImage drawGlyphBitmap(GlyphID glyphID, CTFontRef font, Size size) { + PremultipliedImage rgbaBitmap(size); + + CFStringRefHandle string(CFStringCreateWithCharacters(NULL, reinterpret_cast(&glyphID), 1)); + + CGColorSpaceHandle colorSpace(CGColorSpaceCreateDeviceRGB()); + // TODO: Is there a way to just draw a single alpha channel instead of copying it out of an RGB image? Doesn't seem like the grayscale colorspace is what I'm looking for... + if (!colorSpace) { + throw std::runtime_error("CGColorSpaceCreateDeviceRGB failed"); + } + + constexpr const size_t bitsPerComponent = 8; + constexpr const size_t bytesPerPixel = 4; + const size_t bytesPerRow = bytesPerPixel * size.width; + + CGContextHandle context(CGBitmapContextCreate( + rgbaBitmap.data.get(), + size.width, + size.height, + bitsPerComponent, + bytesPerRow, + *colorSpace, + kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast)); + 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)); + + // For debugging only, doesn't get useful metrics yet + //extractGlyphMetrics(font, *line); + + // Start drawing a little bit below the top of the bitmap + CGContextSetTextPosition(*context, 0.0, 5.0); + CTLineDraw(*line, *context); + + return rgbaBitmap; +} + +Glyph LocalGlyphRasterizer::rasterizeGlyph(const FontStack& fontStack, GlyphID glyphID) { + Glyph fixedMetrics; + CTFontRef font = impl->getFont(fontStack); + if (!font) { + return fixedMetrics; + } + + fixedMetrics.id = glyphID; + + Size size(35, 35); + + 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); + + // 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++) { + fixedMetrics.bitmap.data[i] = rgbaBitmap.data[4 * i + 3]; + } + + return fixedMetrics; +} + +} // namespace mbgl diff --git a/platform/default/local_glyph_rasterizer.cpp b/platform/default/local_glyph_rasterizer.cpp index 7ace6cbfb1..7866f29420 100644 --- a/platform/default/local_glyph_rasterizer.cpp +++ b/platform/default/local_glyph_rasterizer.cpp @@ -2,6 +2,15 @@ namespace mbgl { +class LocalGlyphRasterizer::Impl { +}; + +LocalGlyphRasterizer::LocalGlyphRasterizer(const optional) +{} + +LocalGlyphRasterizer::~LocalGlyphRasterizer() +{} + bool LocalGlyphRasterizer::canRasterizeGlyph(const FontStack&, GlyphID) { return false; } diff --git a/platform/ios/config.cmake b/platform/ios/config.cmake index 47665c33f1..1caf372b25 100644 --- a/platform/ios/config.cmake +++ b/platform/ios/config.cmake @@ -28,11 +28,12 @@ macro(mbgl_platform_core) # Misc PRIVATE platform/darwin/mbgl/storage/reachability.h PRIVATE platform/darwin/mbgl/storage/reachability.m + PRIVATE platform/darwin/src/CFHandle.hpp + PRIVATE platform/darwin/src/local_glyph_rasterizer.mm PRIVATE platform/darwin/src/logging_nslog.mm PRIVATE platform/darwin/src/nsthread.mm PRIVATE platform/darwin/src/string_nsstring.mm PRIVATE platform/default/bidi.cpp - PRIVATE platform/default/local_glyph_rasterizer.cpp PRIVATE platform/default/thread_local.cpp PRIVATE platform/default/utf.cpp @@ -71,6 +72,7 @@ macro(mbgl_platform_core) target_link_libraries(mbgl-core PUBLIC "-lz" PUBLIC "-framework Foundation" + PUBLIC "-framework CoreText" PUBLIC "-framework CoreGraphics" PUBLIC "-framework OpenGLES" PUBLIC "-framework ImageIO" diff --git a/platform/macos/config.cmake b/platform/macos/config.cmake index 7c52e7d537..3b7d112455 100644 --- a/platform/macos/config.cmake +++ b/platform/macos/config.cmake @@ -14,11 +14,12 @@ macro(mbgl_platform_core) # Misc PRIVATE platform/darwin/mbgl/storage/reachability.h PRIVATE platform/darwin/mbgl/storage/reachability.m + PRIVATE platform/darwin/src/CFHandle.hpp + PRIVATE platform/darwin/src/local_glyph_rasterizer.mm PRIVATE platform/darwin/src/logging_nslog.mm PRIVATE platform/darwin/src/nsthread.mm PRIVATE platform/darwin/src/string_nsstring.mm PRIVATE platform/default/bidi.cpp - PRIVATE platform/default/local_glyph_rasterizer.cpp PRIVATE platform/default/thread_local.cpp PRIVATE platform/default/utf.cpp @@ -61,6 +62,7 @@ macro(mbgl_platform_core) target_link_libraries(mbgl-core PUBLIC "-lz" PUBLIC "-framework Foundation" + PUBLIC "-framework CoreText" PUBLIC "-framework CoreGraphics" PUBLIC "-framework OpenGL" PUBLIC "-framework ImageIO" diff --git a/platform/macos/src/MGLMapView.mm b/platform/macos/src/MGLMapView.mm index 60af0cd648..d5512cb6cc 100644 --- a/platform/macos/src/MGLMapView.mm +++ b/platform/macos/src/MGLMapView.mm @@ -217,7 +217,7 @@ public: - (instancetype)initWithFrame:(NSRect)frameRect { if (self = [super initWithFrame:frameRect]) { - [self commonInit]; + [self commonInit:nil]; self.styleURL = nil; } return self; @@ -225,7 +225,7 @@ public: - (instancetype)initWithFrame:(NSRect)frame styleURL:(nullable NSURL *)styleURL { if (self = [super initWithFrame:frame]) { - [self commonInit]; + [self commonInit:nil]; self.styleURL = styleURL; } return self; @@ -233,7 +233,7 @@ public: - (instancetype)initWithCoder:(nonnull NSCoder *)decoder { if (self = [super initWithCoder:decoder]) { - [self commonInit]; + [self commonInit:nil]; } return self; } @@ -252,7 +252,7 @@ public: return @[@"camera", @"debugMask"]; } -- (void)commonInit { +- (void)commonInit:(nullable NSString*)fontFamily { _isTargetingInterfaceBuilder = NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent; // Set up cross-platform controllers and resources. @@ -274,7 +274,7 @@ public: _mbglThreadPool = mbgl::sharedThreadPool(); - auto renderer = std::make_unique(*_mbglView, [NSScreen mainScreen].backingScaleFactor, *mbglFileSource, *_mbglThreadPool, mbgl::GLContextMode::Unique); + auto renderer = std::make_unique(*_mbglView, [NSScreen mainScreen].backingScaleFactor, *mbglFileSource, *_mbglThreadPool, mbgl::GLContextMode::Unique, mbgl::optional(), fontFamily ? std::string([fontFamily UTF8String]) : mbgl::optional()); _rendererFrontend = std::make_unique(std::move(renderer), self, *_mbglView, true); _mbglMap = new mbgl::Map(*_rendererFrontend, *_mbglView, self.size, [NSScreen mainScreen].backingScaleFactor, *mbglFileSource, *_mbglThreadPool, mbgl::MapMode::Continuous, mbgl::ConstrainMode::None, mbgl::ViewportMode::Default); diff --git a/src/mbgl/renderer/renderer.cpp b/src/mbgl/renderer/renderer.cpp index 8953b419f7..6d086c70b1 100644 --- a/src/mbgl/renderer/renderer.cpp +++ b/src/mbgl/renderer/renderer.cpp @@ -10,9 +10,10 @@ Renderer::Renderer(RendererBackend& backend, FileSource& fileSource_, Scheduler& scheduler_, GLContextMode contextMode_, - const optional programCacheDir_) + const optional programCacheDir_, + const optional localFontFamily_) : impl(std::make_unique(backend, pixelRatio_, fileSource_, scheduler_, - contextMode_, std::move(programCacheDir_))) { + contextMode_, std::move(programCacheDir_), std::move(localFontFamily_))) { } Renderer::~Renderer() { diff --git a/src/mbgl/renderer/renderer_impl.cpp b/src/mbgl/renderer/renderer_impl.cpp index aa138df662..4bed0e251b 100644 --- a/src/mbgl/renderer/renderer_impl.cpp +++ b/src/mbgl/renderer/renderer_impl.cpp @@ -42,7 +42,8 @@ Renderer::Impl::Impl(RendererBackend& backend_, FileSource& fileSource_, Scheduler& scheduler_, GLContextMode contextMode_, - const optional programCacheDir_) + const optional programCacheDir_, + const optional localFontFamily_) : backend(backend_) , scheduler(scheduler_) , fileSource(fileSource_) @@ -50,7 +51,7 @@ Renderer::Impl::Impl(RendererBackend& backend_, , contextMode(contextMode_) , pixelRatio(pixelRatio_) , programCacheDir(programCacheDir_) - , glyphManager(std::make_unique(fileSource)) + , glyphManager(std::make_unique(fileSource, std::make_unique(localFontFamily_))) , imageManager(std::make_unique()) , lineAtlas(std::make_unique(Size{ 256, 512 })) , imageImpls(makeMutable>>()) diff --git a/src/mbgl/renderer/renderer_impl.hpp b/src/mbgl/renderer/renderer_impl.hpp index 4f8139791c..5d0200a5df 100644 --- a/src/mbgl/renderer/renderer_impl.hpp +++ b/src/mbgl/renderer/renderer_impl.hpp @@ -38,7 +38,7 @@ class Renderer::Impl : public GlyphManagerObserver, public RenderSourceObserver{ public: Impl(RendererBackend&, float pixelRatio_, FileSource&, Scheduler&, GLContextMode, - const optional programCacheDir); + const optional programCacheDir, const optional localFontFamily); ~Impl() final; void markContextLost() { diff --git a/src/mbgl/text/glyph_manager.hpp b/src/mbgl/text/glyph_manager.hpp index 194f503ff1..84db2c4be5 100644 --- a/src/mbgl/text/glyph_manager.hpp +++ b/src/mbgl/text/glyph_manager.hpp @@ -25,7 +25,7 @@ public: class GlyphManager : public util::noncopyable { public: - GlyphManager(FileSource&, std::unique_ptr = std::make_unique()); + GlyphManager(FileSource&, std::unique_ptr = std::make_unique(optional())); ~GlyphManager(); // Workers send a `getGlyphs` message to the main thread once they have determined diff --git a/src/mbgl/text/local_glyph_rasterizer.hpp b/src/mbgl/text/local_glyph_rasterizer.hpp index c2bdbd2840..82b16b534d 100644 --- a/src/mbgl/text/local_glyph_rasterizer.hpp +++ b/src/mbgl/text/local_glyph_rasterizer.hpp @@ -32,11 +32,15 @@ namespace mbgl { class LocalGlyphRasterizer { public: - virtual ~LocalGlyphRasterizer() = default; + virtual ~LocalGlyphRasterizer(); + LocalGlyphRasterizer(const optional fontFamily = optional()); // virtual so that test harness can override platform-specific behavior virtual bool canRasterizeGlyph(const FontStack&, GlyphID); virtual Glyph rasterizeGlyph(const FontStack&, GlyphID); +private: + class Impl; + std::unique_ptr impl; }; } // namespace mbgl diff --git a/src/mbgl/util/i18n.cpp b/src/mbgl/util/i18n.cpp index 3e3a68e248..1fc13bfb7d 100644 --- a/src/mbgl/util/i18n.cpp +++ b/src/mbgl/util/i18n.cpp @@ -392,6 +392,11 @@ bool allowsIdeographicBreaking(char16_t chr) { // || isInCJKCompatibilityIdeographsSupplement(chr)); } +bool allowsFixedWidthGlyphGeneration(char16_t chr) { + // Mirrors conservative set of characters used in glyph_manager.js/_tinySDF + return isInCJKUnifiedIdeographs(chr) || isInHangulSyllables(chr); +} + bool allowsVerticalWritingMode(const std::u16string& string) { for (char32_t chr : string) { if (hasUprightVerticalOrientation(chr)) { diff --git a/src/mbgl/util/i18n.hpp b/src/mbgl/util/i18n.hpp index 61c5a1ea96..b3960c743c 100644 --- a/src/mbgl/util/i18n.hpp +++ b/src/mbgl/util/i18n.hpp @@ -23,6 +23,10 @@ bool allowsIdeographicBreaking(const std::u16string& string); by the given Unicode codepoint due to ideographic breaking. */ bool allowsIdeographicBreaking(char16_t chr); +/** Conservative set of characters expected to have relatively fixed sizes and + advances */ +bool allowsFixedWidthGlyphGeneration(char16_t chr); + /** Returns whether any substring of the given string can be drawn as vertical text with upright glyphs. */ bool allowsVerticalWritingMode(const std::u16string& string); -- cgit v1.2.1