Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

[ios] POC: implementation of a CoreText-based LocalGlyphRasterizer #10572

Merged
merged 14 commits into from
Nov 29, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion include/mbgl/renderer/renderer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ class Renderer {
public:
Renderer(RendererBackend&, float pixelRatio_, FileSource&, Scheduler&,
GLContextMode = GLContextMode::Unique,
const optional<std::string> programCacheDir = {});
const optional<std::string> programCacheDir = {},
const optional<std::string> localFontFamily = {});
~Renderer();

void markContextLost();
Expand Down
31 changes: 31 additions & 0 deletions platform/darwin/src/CFHandle.hpp
Original file line number Diff line number Diff line change
@@ -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<CFDataRef, CFTypeRef, CFRelease>;

CFDataHandle data(CFDataCreateWithBytesNoCopy(
kCFAllocatorDefault, reinterpret_cast<const unsigned char*>(source.data()), source.size(),
kCFAllocatorNull));
*/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing #pragma once

We also typically make sure that header files include all of the symbols they reference. Instead of defining all of the aliases in the header file, we can continue to define them where we need them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Maybe we don't need CFHandle at all? It seems like something we could replace with a portable "unique handle holder" or maybe just std::unique_ptr with custom deleters?

namespace {

template <typename HandleType, typename ReleaserArgumentType, void (*Releaser)(ReleaserArgumentType)>
struct CFHandle {
CFHandle(HandleType handle_): handle(handle_) {}
~CFHandle() { Releaser(handle); }
HandleType operator*() { return handle; }
operator bool() { return handle; }
private:
HandleType handle;
};

} // namespace

14 changes: 1 addition & 13 deletions platform/darwin/src/image.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,7 @@

#import <ImageIO/ImageIO.h>

namespace {

template <typename T, typename S, void (*Releaser)(S)>
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<CGImageRef, CGImageRef, CGImageRelease>;
using CFDataHandle = CFHandle<CFDataRef, CFTypeRef, CFRelease>;
Expand Down
233 changes: 233 additions & 0 deletions platform/darwin/src/local_glyph_rasterizer.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
#include <mbgl/text/local_glyph_rasterizer.hpp>
#include <mbgl/util/i18n.hpp>
#include <mbgl/util/platform.hpp>

#include <unordered_map>

#import <Foundation/Foundation.h>
#import <CoreText/CoreText.h>
#import <ImageIO/ImageIO.h>

#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<CGColorSpaceRef, CGColorSpaceRef, CGColorSpaceRelease>;
using CGContextHandle = CFHandle<CGContextRef, CGContextRef, CGContextRelease>;
using CFStringRefHandle = CFHandle<CFStringRef, CFTypeRef, CFRelease>;
using CFAttributedStringRefHandle = CFHandle<CFAttributedStringRef, CFTypeRef, CFRelease>;
using CFDictionaryRefHandle = CFHandle<CFDictionaryRef, CFTypeRef, CFRelease>;
using CTFontDescriptorRefHandle = CFHandle<CTFontDescriptorRef, CFTypeRef, CFRelease>;
using CTLineRefHandle = CFHandle<CTLineRef, CFTypeRef, CFRelease>;

class LocalGlyphRasterizer::Impl {
public:
Impl(const optional<std::string> 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 *)kCTFontSymbolicTrait: [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<FontStack, CTFontRef, FontStackHash> fontHandles;
optional<std::string> fontFamily;
};

LocalGlyphRasterizer::LocalGlyphRasterizer(const optional<std::string> fontFamily)
: impl(std::make_unique<Impl>(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<UniChar*>(&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
9 changes: 9 additions & 0 deletions platform/default/local_glyph_rasterizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

namespace mbgl {

class LocalGlyphRasterizer::Impl {
};

LocalGlyphRasterizer::LocalGlyphRasterizer(const optional<std::string>)
{}

LocalGlyphRasterizer::~LocalGlyphRasterizer()
{}

bool LocalGlyphRasterizer::canRasterizeGlyph(const FontStack&, GlyphID) {
return false;
}
Expand Down
4 changes: 3 additions & 1 deletion platform/ios/config.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -84,6 +85,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"
Expand Down
4 changes: 3 additions & 1 deletion platform/macos/config.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -64,6 +65,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"
Expand Down
Loading