1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-02-01 03:10:06 +00:00

Typeface: Fix advances for Apple Color Emoji

The glyph spacing for this font does not scale linearly with the font
size; at smaller font sizes, the spacing is relatively larger.

Typeface::getStringWidth, Typeface::getGlyphPositions, and the
equivalent Font member functions now take the font size and horizontal
scale into account when computing advances on Apple platforms.
This commit is contained in:
reuk 2024-03-04 20:45:41 +00:00
parent 4e90ef831b
commit 6c4c78baac
No known key found for this signature in database
GPG key ID: FCB43929F012EE5C
4 changed files with 159 additions and 190 deletions

View file

@ -52,130 +52,12 @@ namespace FontValues
float minimumHorizontalScale = 0.7f;
}
class HbScale
{
static constexpr float factor = 1 << 16;
public:
HbScale() = delete;
static constexpr hb_position_t juceToHb (float pos)
{
return (hb_position_t) (pos * factor);
}
static constexpr float hbToJuce (hb_position_t pos)
{
return (float) pos / (float) factor;
}
};
using GetTypefaceForFont = Typeface::Ptr (*)(const Font&);
GetTypefaceForFont juce_getTypefaceForFont = nullptr;
float Font::getDefaultMinimumHorizontalScaleFactor() noexcept { return FontValues::minimumHorizontalScale; }
void Font::setDefaultMinimumHorizontalScaleFactor (float newValue) noexcept { FontValues::minimumHorizontalScale = newValue; }
//==============================================================================
#if JUCE_MAC || JUCE_IOS
template <CTFontOrientation orientation>
void getAdvancesForGlyphs (hb_font_t* hbFont, CTFontRef ctFont, Span<const CGGlyph> glyphs, Span<CGSize> advances)
{
jassert (glyphs.size() == advances.size());
int x, y;
hb_font_get_scale (hbFont, &x, &y);
const auto scaleAdjustment = HbScale::hbToJuce (orientation == kCTFontOrientationHorizontal ? x : y) / CTFontGetSize (ctFont);
CTFontGetAdvancesForGlyphs (ctFont, orientation, std::data (glyphs), std::data (advances), (CFIndex) std::size (glyphs));
for (auto& advance : advances)
(orientation == kCTFontOrientationHorizontal ? advance.width : advance.height) *= scaleAdjustment;
}
template <CTFontOrientation orientation>
static auto getAdvanceFn()
{
return [] (hb_font_t* f, void*, hb_codepoint_t glyph, void* voidFontRef) -> hb_position_t
{
auto* fontRef = static_cast<CTFontRef> (voidFontRef);
const CGGlyph glyphs[] { (CGGlyph) glyph };
CGSize advances[std::size (glyphs)]{};
getAdvancesForGlyphs<orientation> (f, fontRef, glyphs, advances);
return HbScale::juceToHb ((float) (orientation == kCTFontOrientationHorizontal ? advances->width : advances->height));
};
}
template <CTFontOrientation orientation>
static auto getAdvancesFn()
{
return [] (hb_font_t* f,
void*,
unsigned int count,
const hb_codepoint_t* firstGlyph,
unsigned int glyphStride,
hb_position_t* firstAdvance,
unsigned int advanceStride,
void* voidFontRef)
{
auto* fontRef = static_cast<CTFontRef> (voidFontRef);
std::vector<CGGlyph> glyphs (count);
for (auto [index, glyph] : enumerate (glyphs))
glyph = (CGGlyph) *addBytesToPointer (firstGlyph, glyphStride * index);
std::vector<CGSize> advances (count);
getAdvancesForGlyphs<orientation> (f, fontRef, glyphs, advances);
for (auto [index, advance] : enumerate (advances))
*addBytesToPointer (firstAdvance, advanceStride * index) = HbScale::juceToHb ((float) (orientation == kCTFontOrientationHorizontal ? advance.width : advance.height));
};
}
/* This function overrides the callbacks that fetch glyph advances for fonts on macOS.
The built-in OpenType glyph metric callbacks that HarfBuzz uses by default for fonts such as
"Apple Color Emoji" don't always return correct advances, resulting in emoji that may overlap
with subsequent characters. This may be to do with ignoring the 'trak' table, but I'm not an
expert, so I'm not sure!
In any case, using CTFontGetAdvancesForGlyphs produces much nicer advances for emoji on Apple
platforms, as long as the CTFont is set to the size that will eventually be rendered.
This might need a bit of testing to make sure that it correctly handles advances for
custom (non-Apple?) fonts.
@param hb a hb_font_t to update with Apple-specific advances
@param fontRef the CTFontRef (normally with a custom point size) that will be queried when computing advances
*/
static void overrideCTFontAdvances (hb_font_t* hb, CTFontRef fontRef)
{
using HbFontFuncs = std::unique_ptr<hb_font_funcs_t, FunctionPointerDestructor<hb_font_funcs_destroy>>;
const HbFontFuncs funcs { hb_font_funcs_create() };
// We pass the CTFontRef as user data to each of these functions.
// We don't pass a custom destructor for the user data, as that will be handled by the custom
// destructor for the hb_font_funcs_t.
hb_font_funcs_set_glyph_h_advance_func (funcs.get(), getAdvanceFn <kCTFontOrientationHorizontal>(), (void*) fontRef, nullptr);
hb_font_funcs_set_glyph_v_advance_func (funcs.get(), getAdvanceFn <kCTFontOrientationVertical>(), (void*) fontRef, nullptr);
hb_font_funcs_set_glyph_h_advances_func (funcs.get(), getAdvancesFn<kCTFontOrientationHorizontal>(), (void*) fontRef, nullptr);
hb_font_funcs_set_glyph_v_advances_func (funcs.get(), getAdvancesFn<kCTFontOrientationVertical>(), (void*) fontRef, nullptr);
// We want to keep a copy of the font around so that all of our custom callbacks can query it,
// so retain it here and release it once the custom functions are no longer in use.
jassert (fontRef != nullptr);
CFRetain (fontRef);
hb_font_set_funcs (hb, funcs.get(), (void*) fontRef, [] (void* ptr)
{
CFRelease ((CTFontRef) ptr);
});
}
#endif
//==============================================================================
class TypefaceCache final : private DeletedAtShutdown
{
@ -410,24 +292,8 @@ public:
HbFont getFontPtr (const Font& f)
{
const ScopedLock lock (mutex);
if (auto ptr = getTypefacePtr (f))
{
if (HbFont subFont { hb_font_create_sub_font (ptr->getNativeDetails().getFont()) })
{
const auto points = legacyHeightToPoints (ptr, height);
hb_font_set_ptem (subFont.get(), points);
hb_font_set_scale (subFont.get(), HbScale::juceToHb (points * horizontalScale), HbScale::juceToHb (points));
#if JUCE_MAC || JUCE_IOS
overrideCTFontAdvances (subFont.get(), hb_coretext_font_get_ct_font (subFont.get()));
#endif
return subFont;
}
}
return ptr->getNativeDetails().getFontAtSizeAndScale (f.getHeight(), f.getHorizontalScale());
return {};
}
@ -913,17 +779,13 @@ int Font::getStringWidth (const String& text) const
float Font::getStringWidthFloat (const String& text) const
{
auto w = getTypefacePtr()->getStringWidth (text);
if (! approximatelyEqual (font->getKerning(), 0.0f))
w += font->getKerning() * (float) text.length();
return w * font->getHeight() * font->getHorizontalScale();
const auto w = getTypefacePtr()->getStringWidth (text, getHeight(), getHorizontalScale());
return w + (font->getHeight() * font->getHorizontalScale() * font->getKerning() * (float) text.length());
}
void Font::getGlyphPositions (const String& text, Array<int>& glyphs, Array<float>& xOffsets) const
{
getTypefacePtr()->getGlyphPositions (text, glyphs, xOffsets);
getTypefacePtr()->getGlyphPositions (text, glyphs, xOffsets, getHeight(), getHorizontalScale());
if (auto num = xOffsets.size())
{
@ -931,15 +793,8 @@ void Font::getGlyphPositions (const String& text, Array<int>& glyphs, Array<floa
auto* x = xOffsets.getRawDataPointer();
if (! approximatelyEqual (font->getKerning(), 0.0f))
{
for (int i = 0; i < num; ++i)
x[i] = (x[i] + (float) i * font->getKerning()) * scale;
}
else
{
for (int i = 0; i < num; ++i)
x[i] *= scale;
}
x[i] += ((float) i * font->getKerning() * scale);
}
}

View file

@ -35,6 +35,124 @@
namespace juce
{
class HbScale
{
static constexpr float factor = 1 << 16;
public:
HbScale() = delete;
static constexpr hb_position_t juceToHb (float pos)
{
return (hb_position_t) (pos * factor);
}
static constexpr float hbToJuce (hb_position_t pos)
{
return (float) pos / (float) factor;
}
};
//==============================================================================
#if JUCE_MAC || JUCE_IOS
template <CTFontOrientation orientation>
void getAdvancesForGlyphs (hb_font_t* hbFont, CTFontRef ctFont, Span<const CGGlyph> glyphs, Span<CGSize> advances)
{
jassert (glyphs.size() == advances.size());
int x, y;
hb_font_get_scale (hbFont, &x, &y);
const auto scaleAdjustment = HbScale::hbToJuce (orientation == kCTFontOrientationHorizontal ? x : y) / CTFontGetSize (ctFont);
CTFontGetAdvancesForGlyphs (ctFont, orientation, std::data (glyphs), std::data (advances), (CFIndex) std::size (glyphs));
for (auto& advance : advances)
(orientation == kCTFontOrientationHorizontal ? advance.width : advance.height) *= scaleAdjustment;
}
template <CTFontOrientation orientation>
static auto getAdvanceFn()
{
return [] (hb_font_t* f, void*, hb_codepoint_t glyph, void* voidFontRef) -> hb_position_t
{
auto* fontRef = static_cast<CTFontRef> (voidFontRef);
const CGGlyph glyphs[] { (CGGlyph) glyph };
CGSize advances[std::size (glyphs)]{};
getAdvancesForGlyphs<orientation> (f, fontRef, glyphs, advances);
return HbScale::juceToHb ((float) (orientation == kCTFontOrientationHorizontal ? advances->width : advances->height));
};
}
template <CTFontOrientation orientation>
static auto getAdvancesFn()
{
return [] (hb_font_t* f,
void*,
unsigned int count,
const hb_codepoint_t* firstGlyph,
unsigned int glyphStride,
hb_position_t* firstAdvance,
unsigned int advanceStride,
void* voidFontRef)
{
auto* fontRef = static_cast<CTFontRef> (voidFontRef);
std::vector<CGGlyph> glyphs (count);
for (auto [index, glyph] : enumerate (glyphs))
glyph = (CGGlyph) *addBytesToPointer (firstGlyph, glyphStride * index);
std::vector<CGSize> advances (count);
getAdvancesForGlyphs<orientation> (f, fontRef, glyphs, advances);
for (auto [index, advance] : enumerate (advances))
*addBytesToPointer (firstAdvance, advanceStride * index) = HbScale::juceToHb ((float) (orientation == kCTFontOrientationHorizontal ? advance.width : advance.height));
};
}
/* This function overrides the callbacks that fetch glyph advances for fonts on macOS.
The built-in OpenType glyph metric callbacks that HarfBuzz uses by default for fonts such as
"Apple Color Emoji" don't always return correct advances, resulting in emoji that may overlap
with subsequent characters. This may be to do with ignoring the 'trak' table, but I'm not an
expert, so I'm not sure!
In any case, using CTFontGetAdvancesForGlyphs produces much nicer advances for emoji on Apple
platforms, as long as the CTFont is set to the size that will eventually be rendered.
This might need a bit of testing to make sure that it correctly handles advances for
custom (non-Apple?) fonts.
@param hb a hb_font_t to update with Apple-specific advances
@param fontRef the CTFontRef (normally with a custom point size) that will be queried when computing advances
*/
static void overrideCTFontAdvances (hb_font_t* hb, CTFontRef fontRef)
{
using HbFontFuncs = std::unique_ptr<hb_font_funcs_t, FunctionPointerDestructor<hb_font_funcs_destroy>>;
const HbFontFuncs funcs { hb_font_funcs_create() };
// We pass the CTFontRef as user data to each of these functions.
// We don't pass a custom destructor for the user data, as that will be handled by the custom
// destructor for the hb_font_funcs_t.
hb_font_funcs_set_glyph_h_advance_func (funcs.get(), getAdvanceFn <kCTFontOrientationHorizontal>(), (void*) fontRef, nullptr);
hb_font_funcs_set_glyph_v_advance_func (funcs.get(), getAdvanceFn <kCTFontOrientationVertical>(), (void*) fontRef, nullptr);
hb_font_funcs_set_glyph_h_advances_func (funcs.get(), getAdvancesFn<kCTFontOrientationHorizontal>(), (void*) fontRef, nullptr);
hb_font_funcs_set_glyph_v_advances_func (funcs.get(), getAdvancesFn<kCTFontOrientationVertical>(), (void*) fontRef, nullptr);
// We want to keep a copy of the font around so that all of our custom callbacks can query it,
// so retain it here and release it once the custom functions are no longer in use.
jassert (fontRef != nullptr);
CFRetain (fontRef);
hb_font_set_funcs (hb, funcs.get(), (void*) fontRef, [] (void* ptr)
{
CFRelease ((CTFontRef) ptr);
});
}
#endif
struct TypefaceLegacyMetrics
{
float ascent{}; // in em units
@ -64,6 +182,21 @@ public:
auto getLegacyMetrics() const { return legacyMetrics; }
HbFont getFontAtSizeAndScale (float height, float horizontalScale) const
{
HbFont subFont { hb_font_create_sub_font (font) };
const auto points = height * getLegacyMetrics().getHeightToPointsFactor();
hb_font_set_ptem (subFont.get(), points);
hb_font_set_scale (subFont.get(), HbScale::juceToHb (points * horizontalScale), HbScale::juceToHb (points));
#if JUCE_MAC || JUCE_IOS
overrideCTFontAdvances (subFont.get(), hb_coretext_font_get_ct_font (subFont.get()));
#endif
return subFont;
}
private:
static TypefaceLegacyMetrics findLegacyMetrics (hb_font_t* f)
{
@ -705,7 +838,11 @@ static constexpr auto hbTag (const char (&arr)[5])
}
template <typename Consumer>
static void doSimpleShape (const Typeface& typeface, const String& text, Consumer&& consumer)
static void doSimpleShape (const Typeface& typeface,
const String& text,
float height,
float horizontalScale,
Consumer&& consumer)
{
HbBuffer buffer { hb_buffer_create() };
hb_buffer_add_utf8 (buffer.get(), text.toRawUTF8(), -1, 0, -1);
@ -713,7 +850,8 @@ static void doSimpleShape (const Typeface& typeface, const String& text, Consume
hb_buffer_guess_segment_properties (buffer.get());
const auto& native = typeface.getNativeDetails();
auto* font = native.getFont();
const auto sized = native.getFontAtSizeAndScale (height, horizontalScale);
auto* font = sized.get();
// Disable ligatures, because TextEditor requires a 1:1 codepoint/glyph mapping for caret
// positioning to work as expected.
@ -734,36 +872,32 @@ static void doSimpleShape (const Typeface& typeface, const String& text, Consume
auto* infos = hb_buffer_get_glyph_infos (buffer.get(), &numGlyphs);
auto* positions = hb_buffer_get_glyph_positions (buffer.get(), &numGlyphs);
const auto heightToPoints = native.getLegacyMetrics().getHeightToPointsFactor();
const auto upem = hb_face_get_upem (hb_font_get_face (font));
Point<hb_position_t> cursor{};
for (auto i = decltype (numGlyphs){}; i < numGlyphs; ++i)
{
const auto& info = infos[i];
const auto& position = positions[i];
consumer (std::make_optional (info.codepoint),
heightToPoints * ((float) cursor.x + (float) position.x_offset) / (float) upem);
consumer (std::make_optional (info.codepoint), HbScale::hbToJuce (cursor.x + position.x_offset));
cursor += Point { position.x_advance, position.y_advance };
}
consumer (std::optional<hb_codepoint_t>{}, heightToPoints * (float) cursor.x / (float) upem);
consumer (std::optional<hb_codepoint_t>{}, HbScale::hbToJuce (cursor.x));
}
float Typeface::getStringWidth (const String& text)
float Typeface::getStringWidth (const String& text, float height, float horizontalScale)
{
float x{};
doSimpleShape (*this, text, [&] (auto, auto xOffset)
doSimpleShape (*this, text, height, horizontalScale, [&] (auto, auto xOffset)
{
x = xOffset;
});
return x;
}
void Typeface::getGlyphPositions (const String& text, Array<int>& glyphs, Array<float>& xOffsets)
void Typeface::getGlyphPositions (const String& text, Array<int>& glyphs, Array<float>& xOffsets, float height, float horizontalScale)
{
doSimpleShape (*this, text, [&] (auto codepoint, auto xOffset)
doSimpleShape (*this, text, height, horizontalScale, [&] (auto codepoint, auto xOffset)
{
if (codepoint.has_value())
glyphs.add ((int) *codepoint);

View file

@ -142,12 +142,7 @@ public:
/** @deprecated
This function has several shortcomings:
- The returned value is based on a font with a normalised JUCE height of 1.0,
which will normally differ from the value that would be expected for a font
with a height of 1 pt.
- This function is unsuitable for measuring text with spacing that doesn't
scale linearly with point size, which might be the case for fonts that
implement optical sizing.
- The height parameter is specified in JUCE units rather than in points.
- The result is computed assuming that ligatures and other font features will
not be used when rendering the string. There's also no way of specifying a
language used for the string, which may affect the widths of CJK text.
@ -157,19 +152,13 @@ public:
glyphs.
Measures the width of a line of text.
The distance returned is based on the font having an normalised height of 1.0.
You should never need to call this!
*/
float getStringWidth (const String& text);
float getStringWidth (const String& text, float normalisedHeight = 1.0f, float horizontalScale = 1.0f);
/** @deprecated
This function has several shortcomings:
- The returned values are based on a font with a normalised JUCE height of 1.0,
which will normally differ from the value that would be expected for a font
with a height of 1 pt.
- This function is unsuitable for measuring text with spacing that doesn't
scale linearly with point size, which might be the case for fonts that
implement optical sizing.
- The height parameter is specified in JUCE units rather than in points.
- Ligatures are deliberately ignored, which will lead to ugly results if the
positions are used to paint text using latin scripts, and potentially
illegible results for other scripts. There's also no way of specifying a
@ -180,10 +169,13 @@ public:
glyphs.
Converts a line of text into its glyph numbers and their positions.
The distances returned are based on the font having an normalised height of 1.0.
You should never need to call this!
*/
void getGlyphPositions (const String& text, Array<int>& glyphs, Array<float>& xOffsets);
void getGlyphPositions (const String& text,
Array<int>& glyphs,
Array<float>& xOffsets,
float normalisedHeight = 1.0f,
float horizontalScale = 1.0f);
/** Returns the outline for a glyph.
The path returned will be normalised to a font height of 1.0.

View file

@ -668,18 +668,6 @@ public:
}
private:
CFUniquePtr<CFDictionaryRef> getAttributedStringAtts() const
{
const short zero = 0;
CFUniquePtr<CFNumberRef> numberRef (CFNumberCreate (nullptr, kCFNumberShortType, &zero));
CFStringRef keys[] = { kCTFontAttributeName, kCTLigatureAttributeName };
CFTypeRef values[] = { ctFont.get(), numberRef.get() };
return CFUniquePtr<CFDictionaryRef> (CFDictionaryCreate (nullptr, (const void**) &keys,
(const void**) &values, numElementsInArray (keys),
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
}
CoreTextTypeface (CFUniquePtr<CTFontRef> nativeFont,
HbFont fontIn,
const String& name,