diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 68f117907e..7a3152a0c2 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -9,14 +9,24 @@ new TypefaceMetricsKind argument. The getAscent(), getDescent(), and getHeightToPointsFactor() members have been replaced by getMetrics(), which returns the same metrics information all at once. +Font instances now store a metrics kind internally. Calls to Font::getAscent() +and other functions that query font metrics will always use the Font's stored +metrics kind. Calls to Font::operator== will take the metrics kinds into +account, so two fonts that differ only in their stored metrics kind will +be considered non-equal. + **Possible Issues** -Code that calls any of the affected functions will fail to compile. +Code that calls any of the affected Typeface functions will fail to compile. +Code that compares Font instances may behave differently if the compared font +instances use mismatched metrics kinds. **Workaround** Specify the kind of metrics you require when calling Typeface member functions. -Call getMetrics() instead of the old individual getters for metrics. +Call getMetrics() instead of the old individual getters for metrics. Review +calls to Font::operator==, especially where comparing against a +default-constructed Font. **Rationale** @@ -27,8 +37,9 @@ layout changes in existing projects), and portable metrics (more suitable for new or cross-platform projects). Most users will fetch metrics from Font objects rather than from the Typeface. Font will continue to return non-portable metrics when constructed using the -existing (deprecated) constructors. Portable metrics can be enabled by -switching to the new Font constructor that takes a FontOptions argument. +old (deprecated) constructors. Portable metrics can be enabled by switching to +the new Font constructor that takes a FontOptions argument. See the +documentation for TypefaceMetricsKind for more details. ## Change diff --git a/modules/juce_graphics/contexts/juce_LowLevelGraphicsPostScriptRenderer.cpp b/modules/juce_graphics/contexts/juce_LowLevelGraphicsPostScriptRenderer.cpp index 260da71968..bd0b6ab7cc 100644 --- a/modules/juce_graphics/contexts/juce_LowLevelGraphicsPostScriptRenderer.cpp +++ b/modules/juce_graphics/contexts/juce_LowLevelGraphicsPostScriptRenderer.cpp @@ -533,7 +533,7 @@ void LowLevelGraphicsPostScriptRenderer::drawGlyph (int glyphNumber, const Affin { Path p; Font& font = stateStack.getLast()->font; - font.getTypefacePtr()->getOutlineForGlyph (glyphNumber, p); + font.getTypefacePtr()->getOutlineForGlyph (font.getMetricsKind(), glyphNumber, p); fillPath (p, AffineTransform::scale (font.getHeight() * font.getHorizontalScale(), font.getHeight()).followedBy (transform)); } diff --git a/modules/juce_graphics/fonts/juce_Font.cpp b/modules/juce_graphics/fonts/juce_Font.cpp index 89ce72b777..25607cf23e 100644 --- a/modules/juce_graphics/fonts/juce_Font.cpp +++ b/modules/juce_graphics/fonts/juce_Font.cpp @@ -216,8 +216,20 @@ public: HbFont getFontPtr (const Font& f) { + const ScopedLock lock (mutex); + if (auto ptr = getTypefacePtr (f)) - return ptr->getNativeDetails().getFontAtSizeAndScale (f.getHeight(), f.getHorizontalScale()); + return ptr->getNativeDetails().getFontAtSizeAndScale (f.getMetricsKind(), f.getHeight(), f.getHorizontalScale()); + + return {}; + } + + TypefaceMetrics getMetrics (const Font& f) + { + const ScopedLock lock (mutex); + + if (auto ptr = getTypefacePtr (f)) + return ptr->getMetrics (f.getMetricsKind()); return {}; } @@ -228,16 +240,6 @@ public: typeface = nullptr; } - float getAscent (const Font& f) - { - const ScopedLock lock (mutex); - - if (approximatelyEqual (ascent, 0.0f)) - ascent = getTypefacePtr (f)->getAscent(); - - return getHeight() * ascent; - } - /* We do not need to lock in these functions, as it's guaranteed that these data members can only change if there is a single Font instance referencing the shared state. @@ -249,13 +251,14 @@ public: return StringArray (fallbacks.data(), (int) fallbacks.size()); } - String getTypefaceName() const { return options.getName(); } - String getTypefaceStyle() const { return options.getStyle(); } - float getHeight() const { return options.getHeight(); } - float getHorizontalScale() const { return options.getHorizontalScale(); } - float getKerning() const { return options.getKerningFactor(); } - bool getUnderline() const { return options.getUnderline(); } - bool getFallbackEnabled() const { return options.getFallbackEnabled(); } + String getTypefaceName() const { return options.getName(); } + String getTypefaceStyle() const { return options.getStyle(); } + float getHeight() const { return options.getHeight(); } + float getHorizontalScale() const { return options.getHorizontalScale(); } + float getKerning() const { return options.getKerningFactor(); } + bool getUnderline() const { return options.getUnderline(); } + bool getFallbackEnabled() const { return options.getFallbackEnabled(); } + TypefaceMetricsKind getMetricsKind() const { return options.getMetricsKind(); } /* This shared state may be shared between two or more Font instances that are being read/modified from multiple threads. @@ -269,7 +272,7 @@ public: jassert (getReferenceCount() == 1); typeface = newTypeface; - if (typeface != nullptr) + if (newTypeface != nullptr) { options = options.withTypeface (typeface) .withName (typeface->getName()) @@ -307,12 +310,6 @@ public: options = options.withKerningFactor (x); } - void setAscent (float x) - { - jassert (getReferenceCount() == 1); - ascent = x; - } - void setUnderline (bool x) { jassert (getReferenceCount() == 1); @@ -348,7 +345,6 @@ private: } Typeface::Ptr typeface; - float ascent{}; FontOptions options; CriticalSection mutex; }; @@ -359,22 +355,28 @@ Font::Font (FontOptions opt) { } -Font::Font() : font (new SharedFontInternal (FontOptions{})) {} -Font::Font (const Typeface::Ptr& typeface) : font (new SharedFontInternal (FontOptions { typeface })) {} +template +auto legacyArgs (Args&&... args) +{ + return FontOptions { std::forward (args)... }.withMetricsKind (TypefaceMetricsKind::legacy); +} + +Font::Font() : font (new SharedFontInternal (legacyArgs())) {} +Font::Font (const Typeface::Ptr& typeface) : font (new SharedFontInternal (legacyArgs (typeface))) {} Font::Font (const Font& other) noexcept : font (other.font) {} Font::Font (float fontHeight, int styleFlags) - : font (new SharedFontInternal (FontOptions { fontHeight, styleFlags })) + : font (new SharedFontInternal (legacyArgs (fontHeight, styleFlags))) { } Font::Font (const String& typefaceName, float fontHeight, int styleFlags) - : font (new SharedFontInternal (FontOptions { typefaceName, fontHeight, styleFlags })) + : font (new SharedFontInternal (legacyArgs (typefaceName, fontHeight, styleFlags))) { } Font::Font (const String& typefaceName, const String& typefaceStyle, float fontHeight) - : font (new SharedFontInternal (FontOptions { typefaceName, typefaceStyle, fontHeight })) + : font (new SharedFontInternal (legacyArgs (typefaceName, typefaceStyle, fontHeight))) { } @@ -461,7 +463,6 @@ void Font::setTypefaceName (const String& faceName) dupeInternalIfShared(); font->setTypefaceName (faceName); font->setTypeface (nullptr); - font->setAscent (0); } } @@ -472,7 +473,6 @@ void Font::setTypefaceStyle (const String& typefaceStyle) dupeInternalIfShared(); font->setTypefaceStyle (typefaceStyle); font->setTypeface (nullptr); - font->setAscent (0); } } @@ -531,7 +531,7 @@ Font Font::withHeight (const float newHeight) const float Font::getHeightToPointsFactor() const { - return getTypefacePtr()->getHeightToPointsFactor(); + return getTypefacePtr()->getMetrics (getMetricsKind()).heightToPoints; } Font Font::withPointHeight (float heightInPoints) const @@ -591,7 +591,6 @@ void Font::setStyleFlags (const int newFlags) font->setTypeface (nullptr); font->setTypefaceStyle (FontStyleHelpers::getStyleName (newFlags)); font->setUnderline ((newFlags & underlined) != 0); - font->setAscent (0); } } @@ -682,6 +681,8 @@ bool Font::isBold() const noexcept { return FontStyleHelpers::isBold bool Font::isItalic() const noexcept { return FontStyleHelpers::isItalic (font->getTypefaceStyle()); } bool Font::isUnderlined() const noexcept { return font->getUnderline(); } +TypefaceMetricsKind Font::getMetricsKind() const noexcept { return font->getMetricsKind(); } + void Font::setBold (const bool shouldBeBold) { auto flags = getStyleFlags(); @@ -705,7 +706,7 @@ void Font::setUnderline (const bool shouldBeUnderlined) float Font::getAscent() const { - return font->getAscent (*this); + return font->getMetrics (*this).ascent * getHeight(); } float Font::getHeight() const noexcept { return font->getHeight(); } @@ -722,13 +723,13 @@ int Font::getStringWidth (const String& text) const float Font::getStringWidthFloat (const String& text) const { - const auto w = getTypefacePtr()->getStringWidth (text, getHeight(), getHorizontalScale()); + const auto w = getTypefacePtr()->getStringWidth (font->getMetricsKind(), text, getHeight(), getHorizontalScale()); return w + (font->getHeight() * font->getHorizontalScale() * font->getKerning() * (float) text.length()); } void Font::getGlyphPositions (const String& text, Array& glyphs, Array& xOffsets) const { - getTypefacePtr()->getGlyphPositions (text, glyphs, xOffsets, getHeight(), getHorizontalScale()); + getTypefacePtr()->getGlyphPositions (font->getMetricsKind(), text, glyphs, xOffsets, getHeight(), getHorizontalScale()); if (auto num = xOffsets.size()) { diff --git a/modules/juce_graphics/fonts/juce_Font.h b/modules/juce_graphics/fonts/juce_Font.h index 0aab026e33..9a28fcfdd8 100644 --- a/modules/juce_graphics/fonts/juce_Font.h +++ b/modules/juce_graphics/fonts/juce_Font.h @@ -349,6 +349,9 @@ public: /** Returns true if the font is underlined. */ bool isUnderlined() const noexcept; + /** Returns the kind of metrics used by this Font. */ + TypefaceMetricsKind getMetricsKind() const noexcept; + //============================================================================== /** Returns the font's horizontal scale. A value of 1.0 is the normal scale, less than this will be narrower, greater diff --git a/modules/juce_graphics/fonts/juce_FontOptions.cpp b/modules/juce_graphics/fonts/juce_FontOptions.cpp index 387eb389ba..4f924f6a4d 100644 --- a/modules/juce_graphics/fonts/juce_FontOptions.cpp +++ b/modules/juce_graphics/fonts/juce_FontOptions.cpp @@ -92,6 +92,7 @@ auto FontOptions::tie() const style, typeface.get(), fallbacks, + metricsKind, height, tracking, horizontalScale, diff --git a/modules/juce_graphics/fonts/juce_FontOptions.h b/modules/juce_graphics/fonts/juce_FontOptions.h index 87fe5a65b4..a919a357a3 100644 --- a/modules/juce_graphics/fonts/juce_FontOptions.h +++ b/modules/juce_graphics/fonts/juce_FontOptions.h @@ -121,6 +121,9 @@ public: /** Returns a copy of these options with underline enabled or disabled, defaults to disabled. */ [[nodiscard]] FontOptions withUnderline (bool x = true) const { return withMember (*this, &FontOptions::underlined, x); } + /** Returns a copy of these options the specified metrics kind. */ + [[nodiscard]] FontOptions withMetricsKind (TypefaceMetricsKind x) const { return withMember (*this, &FontOptions::metricsKind, x); } + /** @see withName() */ [[nodiscard]] auto getName() const { return name; } /** @see withStyle() */ @@ -139,6 +142,8 @@ public: [[nodiscard]] auto getFallbackEnabled() const { return fallbackEnabled; } /** @see withUnderline() */ [[nodiscard]] auto getUnderline() const { return underlined; } + /** @see withMetricsKind() */ + [[nodiscard]] auto getMetricsKind() const { return metricsKind; } /** Equality operator. */ [[nodiscard]] bool operator== (const FontOptions& other) const; @@ -159,6 +164,7 @@ private: String name, style; Typeface::Ptr typeface; std::vector fallbacks; + TypefaceMetricsKind metricsKind { TypefaceMetricsKind::portable }; float height{}; float tracking{}; float horizontalScale = 1.0f; diff --git a/modules/juce_graphics/fonts/juce_GlyphArrangement.cpp b/modules/juce_graphics/fonts/juce_GlyphArrangement.cpp index 1c9ddd73d9..0b6417cd79 100644 --- a/modules/juce_graphics/fonts/juce_GlyphArrangement.cpp +++ b/modules/juce_graphics/fonts/juce_GlyphArrangement.cpp @@ -81,7 +81,7 @@ void PositionedGlyph::createPath (Path& path) const if (auto t = font.getTypefacePtr()) { Path p; - t->getOutlineForGlyph (glyph, p); + t->getOutlineForGlyph (font.getMetricsKind(), glyph, p); path.addPath (p, AffineTransform::scale (font.getHeight() * font.getHorizontalScale(), font.getHeight()) .translated (x, y)); @@ -96,7 +96,7 @@ bool PositionedGlyph::hitTest (float px, float py) const if (auto t = font.getTypefacePtr()) { Path p; - t->getOutlineForGlyph (glyph, p); + t->getOutlineForGlyph (font.getMetricsKind(), glyph, p); AffineTransform::translation (-x, -y) .scaled (1.0f / (font.getHeight() * font.getHorizontalScale()), 1.0f / font.getHeight()) diff --git a/modules/juce_graphics/fonts/juce_Typeface.cpp b/modules/juce_graphics/fonts/juce_Typeface.cpp index a09dc297a8..4b3b3708a2 100644 --- a/modules/juce_graphics/fonts/juce_Typeface.cpp +++ b/modules/juce_graphics/fonts/juce_Typeface.cpp @@ -153,7 +153,7 @@ static void overrideCTFontAdvances (hb_font_t* hb, CTFontRef fontRef) } #endif -struct TypefaceLegacyMetrics +struct TypefaceAscentDescent { float ascent{}; // in em units float descent{}; // in em units @@ -163,29 +163,46 @@ struct TypefaceLegacyMetrics float getPointsToHeightFactor() const { return ascent + descent; } float getHeightToPointsFactor() const { return 1.0f / getPointsToHeightFactor(); } + + TypefaceMetrics getTypefaceMetrics() const + { + return { getScaledAscent(), getHeightToPointsFactor() }; + } }; using HbFont = std::unique_ptr>; using HbFace = std::unique_ptr>; using HbBuffer = std::unique_ptr>; +using HbBlob = std::unique_ptr>; class Typeface::Native { public: - explicit Native (hb_font_t* fontRef) - : Native (fontRef, findLegacyMetrics (fontRef)) {} - - Native (hb_font_t* fontRef, TypefaceLegacyMetrics metrics) - : font (fontRef), legacyMetrics (metrics) {} + Native (hb_font_t* fontRef, TypefaceAscentDescent nonPortableMetricsIn) + : font (fontRef), nonPortable (nonPortableMetricsIn) + { + } auto* getFont() const { return font; } - auto getLegacyMetrics() const { return legacyMetrics; } + TypefaceAscentDescent getMetrics (TypefaceMetricsKind kind) const + { + switch (kind) + { + case TypefaceMetricsKind::legacy: + return nonPortable; - HbFont getFontAtSizeAndScale (float height, float horizontalScale) const + case TypefaceMetricsKind::portable: + return portable; + } + + return {}; + } + + HbFont getFontAtSizeAndScale (TypefaceMetricsKind kind, float height, float horizontalScale) const { HbFont subFont { hb_font_create_sub_font (font) }; - const auto points = height * getLegacyMetrics().getHeightToPointsFactor(); + const auto points = height * getMetrics (kind).getHeightToPointsFactor(); hb_font_set_ptem (subFont.get(), points); hb_font_set_scale (subFont.get(), HbScale::juceToHb (points * horizontalScale), HbScale::juceToHb (points)); @@ -198,28 +215,27 @@ public: } private: - static TypefaceLegacyMetrics findLegacyMetrics (hb_font_t* f) + static TypefaceAscentDescent findPortableMetrics (hb_font_t* f, TypefaceAscentDescent fallback) { hb_font_extents_t extents{}; if (! hb_font_get_h_extents (f, &extents)) - { - // jassertfalse; - return { 0.5f, 0.5f }; - } + return fallback; const auto ascent = std::abs ((float) extents.ascender); const auto descent = std::abs ((float) extents.descender); const auto upem = (float) hb_face_get_upem (hb_font_get_face (f)); - TypefaceLegacyMetrics result; + TypefaceAscentDescent result; result.ascent = ascent / upem; result.descent = descent / upem; return result; } hb_font_t* font = nullptr; - TypefaceLegacyMetrics legacyMetrics; + + TypefaceAscentDescent nonPortable; + TypefaceAscentDescent portable = findPortableMetrics (font, nonPortable); }; struct FontStyleHelpers @@ -292,7 +308,7 @@ struct FontStyleHelpers private: static String findName (const String& placeholder) { - const Font f (FontOptions (placeholder, 15.0f, Font::plain)); + const Font f (FontOptions { placeholder, 15.0f, Font::plain }); return Font::getDefaultTypefaceForFont (f)->getName(); } @@ -315,23 +331,20 @@ struct FontStyleHelpers static HbFace getFaceForBlob (Span bytes, unsigned int index) { - auto* blob = hb_blob_create_or_fail (bytes.data(), - (unsigned int) bytes.size(), - HB_MEMORY_MODE_DUPLICATE, - nullptr, - nullptr); - const ScopeGuard scope { [&] { hb_blob_destroy (blob); } }; + HbBlob blob { hb_blob_create_or_fail (bytes.data(), + (unsigned int) bytes.size(), + HB_MEMORY_MODE_DUPLICATE, + nullptr, + nullptr) }; - const auto count = hb_face_count (blob); + const auto count = hb_face_count (blob.get()); - if (count < 1) - { - // Attempted to create a font from invalid data. Perhaps the font format was unrecognised. - jassertfalse; - return {}; - } + if (index < count) + return HbFace { hb_face_create (blob.get(), index) }; - return HbFace { hb_face_create (blob, index) }; + // Attempted to create a font from invalid data. Perhaps the font format was unrecognised. + jassertfalse; + return {}; } }; @@ -388,11 +401,11 @@ static HbDrawFuncs getPathDrawFuncs() return result; } -void Typeface::getOutlineForGlyph (int glyphNumber, Path& path) +void Typeface::getOutlineForGlyph (TypefaceMetricsKind kind, int glyphNumber, Path& path) const { const auto native = getNativeDetails(); auto* font = native.getFont(); - const auto metrics = getNativeDetails().getLegacyMetrics(); + const auto metrics = native.getMetrics (kind); const auto scale = metrics.getHeightToPointsFactor() / (float) hb_face_get_upem (hb_font_get_face (font)); // getTypefaceGlyph returns glyphs in em space, getOutlineForGlyph returns glyphs in "special JUCE units" space @@ -405,10 +418,10 @@ void Typeface::applyVerticalHintingTransform (float, Path&) jassertfalse; } -EdgeTable* Typeface::getEdgeTableForGlyph (int glyphNumber, const AffineTransform& transform, float) +EdgeTable* Typeface::getEdgeTableForGlyph (TypefaceMetricsKind kind, int glyphNumber, const AffineTransform& transform, float) { Path path; - getOutlineForGlyph (glyphNumber, path); + getOutlineForGlyph (kind, glyphNumber, path); path.applyTransform (transform); return new EdgeTable (path.getBounds().getSmallestIntegerContainer().expanded (1, 0), std::move (path), {}); @@ -422,323 +435,6 @@ static Colour makeColour (hb_color_t c) hb_color_get_blue (c)); } -class HbPaintGroup -{ -public: - void pushClipGlyph (const AffineTransform& t, hb_codepoint_t glyph, hb_font_t* font) - { - auto path = getGlyphPathInGlyphUnits (glyph, font); - path.applyTransform (t); - pushClip (std::move (path)); - } - - void pushClipRect (const AffineTransform& t, Rectangle rect) - { - Path path; - path.addRectangle (rect); - path.applyTransform (t); - pushClip (std::move (path)); - } - - void popClip() - { - clip.pop_back(); - } - - void fill (hb_bool_t foreground, hb_color_t c) - { - addLayerChecked (foreground, c); - } - - void linearGradient (hb_color_line_t&, Point, Point, Point) - { - // Support for COLRv1 glyphs is not fully implemented. - jassertfalse; - } - - void radialGradient (hb_color_line_t&, Point, float, Point, float) - { - // Support for COLRv1 glyphs is not fully implemented. - jassertfalse; - } - - void sweepGradient (hb_color_line_t&, Point, float, float) - { - // Support for COLRv1 glyphs is not fully implemented. - jassertfalse; - } - - bool image (const AffineTransform& t, hb_blob_t* image, unsigned int width, unsigned int height, hb_tag_t format, float, hb_glyph_extents_t* extents) - { - switch (format) - { - case HB_PAINT_IMAGE_FORMAT_BGRA: - // Raw bitmap-based glyphs are not currently supported. - // If you hit this assertion, please let the JUCE team know which font you're - // attempting to use. - // Depending on demand, support for this feature may be added in the future. - jassertfalse; - return false; - - case HB_PAINT_IMAGE_FORMAT_PNG: - { - unsigned int imageDataSize{}; - const char* imageData = hb_blob_get_data (image, &imageDataSize); - const auto juceImage = PNGImageFormat::loadFrom (imageData, imageDataSize); - - if (juceImage.isNull()) - return false; - - const auto transform = AffineTransform::scale ((float) extents->width / (float) width, - (float) extents->height / (float) height) - .translated ((float) extents->x_bearing, - (float) extents->y_bearing) - .followedBy (t); - ImageLayer imageLayer { juceImage, transform }; - layers.push_back ({ std::move (imageLayer) }); - return true; - } - - case HB_PAINT_IMAGE_FORMAT_SVG: - // SVG-based glyphs are not currently supported. - // If you hit this assertion, please let the JUCE team know which font you're - // attempting to use. - // Depending on demand, support for this feature may be added in the future. - jassertfalse; - return false; - } - - jassertfalse; - return false; - } - - std::vector getLayers() && - { - return std::move (layers); - } - - void appendLayers (Span l) - { - for (auto& layer : l) - layers.emplace_back (std::move (layer)); - } - -private: - GlyphLayer makeLayer (hb_bool_t foreground, hb_color_t c) const - { - return { ColourLayer { clip.back(), foreground ? std::optional() : makeColour (c) } }; - } - - void pushClip (Path path) - { - pushClip ({ path.getBounds().getSmallestIntegerContainer().expanded (1, 0), path, {} }); - } - - template - void addLayerChecked (Args&&... args) - { - if (clip.empty()) - { - jassertfalse; - return; - } - - layers.push_back (makeLayer (std::forward (args)...)); - } - - void pushClip (const EdgeTable& et) - { - if (! clip.empty()) - { - clip.push_back (clip.back()); - clip.back().clipToEdgeTable (et); - } - else - { - clip.push_back (et); - } - } - - std::vector clip; - std::vector layers; -}; - -class HbPaintContext -{ -public: - explicit HbPaintContext (const AffineTransform& transformIn) - : baseTransform (transformIn) - { - } - - void addTransform (const AffineTransform& transform) - { - transforms.push_back (transforms.empty() ? transform : transform.followedBy (transforms.back())); - } - - void popTransform() - { - transforms.pop_back(); - } - - void pushClipGlyph (hb_codepoint_t glyph, hb_font_t* font) - { - groups.back().pushClipGlyph (getTransform(), glyph, font); - } - - void pushClipRect (Rectangle rect) - { - groups.back().pushClipRect (getTransform(), rect); - } - - void popClip() - { - groups.back().popClip(); - } - - void fill (hb_bool_t foreground, hb_color_t c) - { - groups.back().fill (foreground, c); - } - - void linearGradient (hb_color_line_t& line, Point p0, Point p1, Point p2) - { - groups.back().linearGradient (line, p0, p1, p2); - } - - void radialGradient (hb_color_line_t& line, Point p0, float r0, Point p1, float r1) - { - groups.back().radialGradient (line, p0, r0, p1, r1); - } - - void sweepGradient (hb_color_line_t& line, Point p, float begin, float end) - { - groups.back().sweepGradient (line, p, begin, end); - } - - bool image (hb_blob_t* image, unsigned int width, unsigned int height, hb_tag_t format, float slant, hb_glyph_extents_t* extents) - { - return groups.back().image (getTransform(), image, width, height, format, slant, extents); - } - - void pushGroup() - { - groups.emplace_back(); - } - - void popGroup ([[maybe_unused]] hb_paint_composite_mode_t mode) - { - // There is currently extremely limited support for colour glyph blend modes - jassert (mode == HB_PAINT_COMPOSITE_MODE_SRC_OVER); - - auto newLayers = std::move (groups.back()).getLayers(); - groups.pop_back(); - groups.back().appendLayers (newLayers); - } - - std::vector getLayers() && - { - return std::move (groups.back()).getLayers(); - } - -private: - AffineTransform getTransform() const - { - const auto glyphSpaceTransform = transforms.empty() ? AffineTransform{} : transforms.back(); - return glyphSpaceTransform.followedBy (baseTransform); - } - - AffineTransform baseTransform; - std::vector transforms; - std::vector groups = std::vector (1); -}; - -using HbPaintFuncs = std::unique_ptr>; - -static HbPaintFuncs getPathPaintFuncs() -{ - HbPaintFuncs funcs { hb_paint_funcs_create() }; - - hb_paint_funcs_set_push_transform_func (funcs.get(), [] (auto*, auto* data, auto xx, auto yx, auto xy, auto yy, auto dx, auto dy, auto*) - { - auto& context = *static_cast (data); - context.addTransform ({ xx, xy, dx, yx, yy, dy }); - }, nullptr, nullptr); - - hb_paint_funcs_set_pop_transform_func (funcs.get(), [] (auto*, void* data, auto*) - { - auto& context = *static_cast (data); - context.popTransform(); - }, nullptr, nullptr); - - hb_paint_funcs_set_push_clip_glyph_func (funcs.get(), [] (auto*, void* data, auto glyph, auto* font, auto*) - { - auto& context = *static_cast (data); - context.pushClipGlyph (glyph, font); - }, nullptr, nullptr); - - hb_paint_funcs_set_push_clip_rectangle_func (funcs.get(), [] (auto*, void* data, auto xmin, auto ymin, auto xmax, auto ymax, auto*) - { - auto& context = *static_cast (data); - context.pushClipRect (Rectangle::leftTopRightBottom (xmin, ymin, xmax, ymax)); - }, nullptr, nullptr); - - hb_paint_funcs_set_pop_clip_func (funcs.get(), [] (auto*, void* data, auto*) - { - auto& context = *static_cast (data); - context.popClip(); - }, nullptr, nullptr); - - hb_paint_funcs_set_color_func (funcs.get(), [] (auto*, void* data, auto foreground, auto colour, auto*) - { - auto& context = *static_cast (data); - context.fill (foreground, colour); - }, nullptr, nullptr); - - hb_paint_funcs_set_image_func (funcs.get(), [] (auto*, void* data, auto* image, auto w, auto h, auto format, auto slant, auto* extents, auto*) -> hb_bool_t - { - auto& context = *static_cast (data); - return context.image (image, w, h, format, slant, extents); - }, nullptr, nullptr); - - hb_paint_funcs_set_linear_gradient_func (funcs.get(), [] (auto*, auto* data, auto* colourLine, auto x0, auto y0, auto x1, auto y1, auto x2, auto y2, auto*) - { - auto& context = *static_cast (data); - context.linearGradient (*colourLine, { x0, y0 }, { x1, y1 }, { x2, y2 }); - }, nullptr, nullptr); - - hb_paint_funcs_set_radial_gradient_func (funcs.get(), [] (auto*, auto* data, auto* colourLine, auto x0, auto y0, auto r0, auto x1, auto y1, auto r1, auto*) - { - auto& context = *static_cast (data); - context.radialGradient (*colourLine, { x0, y0 }, r0, { x1, y1 }, r1); - }, nullptr, nullptr); - - hb_paint_funcs_set_sweep_gradient_func (funcs.get(), [] (auto*, auto* data, auto* colourLine, auto x0, auto y0, auto begin, auto end, auto*) - { - auto& context = *static_cast (data); - context.sweepGradient (*colourLine, { x0, y0 }, begin, end); - }, nullptr, nullptr); - - hb_paint_funcs_set_push_group_func (funcs.get(), [] (auto*, auto* data, auto*) - { - auto& context = *static_cast (data); - context.pushGroup(); - }, nullptr, nullptr); - - hb_paint_funcs_set_pop_group_func (funcs.get(), [] (auto*, auto* data, auto mode, auto*) - { - auto& context = *static_cast (data); - context.popGroup (mode); - }, nullptr, nullptr); - - hb_paint_funcs_set_custom_palette_color_func (funcs.get(), [] (auto*, auto*, auto, auto*, auto*) -> hb_bool_t - { - return false; - }, nullptr, nullptr); - - return funcs; -} - static std::vector getCOLRv0Layers (const Typeface& typeface, int glyphNumber, const AffineTransform& transform) { auto* font = typeface.getNativeDetails().getFont(); @@ -778,33 +474,60 @@ static std::vector getCOLRv0Layers (const Typeface& typeface, int gl return result; } -std::vector Typeface::getLayersForGlyph (int glyphNumber, const AffineTransform& transform, float) const +static std::vector getBitmapLayer (const Typeface& typeface, int glyphNumber, const AffineTransform& t) +{ + if ((typeface.getColourGlyphFormats() & Typeface::colourGlyphFormatBitmap) == 0) + return {}; + + auto* font = typeface.getNativeDetails().getFont(); + + const HbBlob blob { hb_ot_color_glyph_reference_png (font, (hb_codepoint_t) glyphNumber) }; + + unsigned int imageDataSize{}; + const char* imageData = hb_blob_get_data (blob.get(), &imageDataSize); + const auto juceImage = PNGImageFormat::loadFrom (imageData, imageDataSize); + + if (juceImage.isNull()) + return {}; + + hb_glyph_extents_t extents{}; + hb_font_get_glyph_extents (font, (hb_codepoint_t) glyphNumber, &extents); + + const auto wDenom = std::max (1, juceImage.getWidth()); + const auto hDenom = std::max (1, juceImage.getHeight()); + + const auto transform = AffineTransform::scale ((float) extents.width / (float) wDenom, + (float) extents.height / (float) hDenom) + .translated ((float) extents.x_bearing, + (float) extents.y_bearing) + .followedBy (t); + return { GlyphLayer { ImageLayer { juceImage, transform } } }; +} + +std::vector Typeface::getLayersForGlyph (TypefaceMetricsKind kind, int glyphNumber, const AffineTransform& transform, float) const { auto* font = getNativeDetails().getFont(); - const auto metrics = getNativeDetails().getLegacyMetrics(); + const auto metrics = getNativeDetails().getMetrics (kind); const auto scale = metrics.getHeightToPointsFactor() / (float) hb_face_get_upem (hb_font_get_face (font)); const auto combinedTransform = AffineTransform::scale (scale, -scale).followedBy (transform); - // Before calling through to the 'paint' API, which JUCE can't easily support due to complex + if (auto bitmapLayer = getBitmapLayer (*this, glyphNumber, combinedTransform); ! bitmapLayer.empty()) + return bitmapLayer; + + // Instead of calling through to the 'paint' API, which JUCE can't easily support due to complex // gradients and blend modes, attempt to load COLRv0 layers for the glyph, which we'll be able // to render more successfully. - auto basicLayers = getCOLRv0Layers (*this, glyphNumber, combinedTransform); + if (auto layers = getCOLRv0Layers (*this, glyphNumber, combinedTransform); ! layers.empty()) + return layers; - if (! basicLayers.empty()) - return basicLayers; + // No bitmap or COLRv0 for this glyph, so just get a simple monochromatic outline + auto path = getGlyphPathInGlyphUnits ((hb_codepoint_t) glyphNumber, font); - constexpr auto palette = 0; + if (path.isEmpty()) + return {}; - static const auto funcs = getPathPaintFuncs(); - - HbPaintContext context { combinedTransform }; - hb_font_paint_glyph (font, - (hb_codepoint_t) glyphNumber, - funcs.get(), - &context, - palette, - {}); - return std::move (context).getLayers(); + path.applyTransform (combinedTransform); + return { GlyphLayer { ColourLayer { EdgeTable { path.getBounds().getSmallestIntegerContainer().expanded (1, 0), path, {} }, {} } } }; } int Typeface::getColourGlyphFormats() const @@ -816,9 +539,10 @@ int Typeface::getColourGlyphFormats() const | (hb_ot_color_has_paint (face) ? colourGlyphFormatCOLRv1 : 0); } -float Typeface::getAscent() const { return getNativeDetails().getLegacyMetrics().getScaledAscent(); } -float Typeface::getDescent() const { return getNativeDetails().getLegacyMetrics().getScaledDescent(); } -float Typeface::getHeightToPointsFactor() const { return getNativeDetails().getLegacyMetrics().getHeightToPointsFactor(); } +TypefaceMetrics Typeface::getMetrics (TypefaceMetricsKind kind) const +{ + return getNativeDetails().getMetrics (kind).getTypefaceMetrics(); +} Typeface::Ptr Typeface::createSystemTypefaceFor (const void* fontFileData, size_t fontFileDataSize) { @@ -847,11 +571,12 @@ static constexpr auto hbTag (const char (&arr)[5]) } template -static void doSimpleShape (const Typeface& typeface, - const String& text, - float height, - float horizontalScale, - Consumer&& consumer) +static float doSimpleShapeWithNoBreaks (const Typeface& typeface, + TypefaceMetricsKind kind, + 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); @@ -859,7 +584,7 @@ static void doSimpleShape (const Typeface& typeface, hb_buffer_guess_segment_properties (buffer.get()); const auto& native = typeface.getNativeDetails(); - const auto sized = native.getFontAtSizeAndScale (height, horizontalScale); + const auto sized = native.getFontAtSizeAndScale (kind, height, horizontalScale); auto* font = sized.get(); // Disable ligatures, because TextEditor requires a 1:1 codepoint/glyph mapping for caret @@ -887,32 +612,66 @@ static void doSimpleShape (const Typeface& typeface, { const auto& info = infos[i]; const auto& position = positions[i]; - consumer (std::make_optional (info.codepoint), HbScale::hbToJuce (cursor.x + position.x_offset)); + consumer (info.codepoint, HbScale::hbToJuce (cursor.x + position.x_offset)); cursor += Point { position.x_advance, position.y_advance }; } - consumer (std::optional{}, HbScale::hbToJuce (cursor.x)); + return HbScale::hbToJuce (cursor.x); } -float Typeface::getStringWidth (const String& text, float height, float horizontalScale) +template +static float doSimpleShape (const Typeface& typeface, + TypefaceMetricsKind kind, + const String& originalText, + float height, + float horizontalScale, + Consumer&& consumer) { - float x{}; - doSimpleShape (*this, text, height, horizontalScale, [&] (auto, auto xOffset) + const juce_wchar zeroWidthSpace = 0x200b; + const auto text = originalText.replaceCharacter ('\n', zeroWidthSpace); + + float lastX{}; + + for (auto iter = text.begin(), end = text.end(); iter != end;) { - x = xOffset; - }); - return x; + const auto next = [&] + { + for (auto i = iter; i != end; ++i) + if (*i == zeroWidthSpace) + return i + 1; + + return end; + }(); + + lastX += doSimpleShapeWithNoBreaks (typeface, kind, String (iter, next), height, horizontalScale, [&] (auto codepoint, auto x) + { + consumer (codepoint, lastX + x); + }); + iter = next; + } + + return lastX; } -void Typeface::getGlyphPositions (const String& text, Array& glyphs, Array& xOffsets, float height, float horizontalScale) +float Typeface::getStringWidth (TypefaceMetricsKind kind, const String& text, float height, float horizontalScale) { - doSimpleShape (*this, text, height, horizontalScale, [&] (auto codepoint, auto xOffset) - { - if (codepoint.has_value()) - glyphs.add ((int) *codepoint); + return doSimpleShape (*this, kind, text, height, horizontalScale, [&] (auto, auto) {}); +} +void Typeface::getGlyphPositions (TypefaceMetricsKind kind, + const String& text, + Array& glyphs, + Array& xOffsets, + float height, + float horizontalScale) +{ + const auto width = doSimpleShape (*this, kind, text, height, horizontalScale, [&] (auto codepoint, auto xOffset) + { + glyphs.add ((int) codepoint); xOffsets.add (xOffset); }); + + xOffsets.add (width); } //============================================================================== @@ -957,7 +716,7 @@ public: beginTest ("Typefaces loaded from memory are found when creating font instances by name"); { - Font font (FontOptions (ptr->getName(), ptr->getStyle(), 12.0f)); + Font font (FontOptions { ptr->getName(), ptr->getStyle(), 12.0f }); expect (font.getTypefacePtr() != nullptr); expect (font.getTypefacePtr()->getName() == ptr->getName()); diff --git a/modules/juce_graphics/fonts/juce_Typeface.h b/modules/juce_graphics/fonts/juce_Typeface.h index 7a0917b1f4..709088b89c 100644 --- a/modules/juce_graphics/fonts/juce_Typeface.h +++ b/modules/juce_graphics/fonts/juce_Typeface.h @@ -59,6 +59,50 @@ struct GlyphLayer std::variant layer; }; +/** Identifiers for different styles of typeface metrics. + + In new projects, it's recommended to use the 'portable' metrics kind, so that fonts display + at a similar size on all platforms. + Portable metrics are enabled by default when constructing a Font using FontOptions. The old, + deprecated Font constructors will all request the legacy metrics kind instead. + JUCE components that display text will query LookAndFeel::getDefaultMetricsKind() to find the + kind of metrics that should be used. This function can be overridden to switch the metrics kind + for the entire LookAndFeel. + + The 'legacy' metrics kind uses the platform font facilities to retrieve font metrics. + Each platform has its own idiosyncratic behaviour when computing metrics - depending on the + typeface data, it's possible that the 'portable' metrics will differ from the 'legacy' metrics + on every platform. The biggest differences between legacy and portable metrics are likely to be + seen on Windows, so it may be helpful to check this platform first. +*/ +enum class TypefaceMetricsKind +{ + legacy, ///< Old-style metrics that may differ for the same font file when running on different platforms. + ///< This was the default behaviour prior to JUCE 8. + portable ///< Where possible, will return the same font metrics on all platforms. + ///< It's a good idea to use this for new JUCE projects, to keep text layout and + ///< sizing consistent on all platforms. +}; + +/** Font metrics using JUCE conventions. +*/ +struct TypefaceMetrics +{ + /** The proportion of the typeface's height that it above the baseline, as a value between 0 and 1. + Note that 'height' here refers to the result of adding the absolute ascent and descent values. + That is, the sum of the ascent and descent will equal 1. + The sum of the ascent and descent will normally differ from the em size of the font. + That is, for a font size of 14pt, there will be 14 points per em, but the sum of the ascent + and descent in points is unlikely to be equal to 14. + */ + float ascent{}; + + /** The factor by which a JUCE font height should be multiplied in order to convert to a font + size in points. + */ + float heightToPoints{}; +}; + //============================================================================== /** A typeface represents a size-independent font. @@ -121,24 +165,8 @@ public: /** Destructor. */ ~Typeface() override; - /** Returns the ascent of the font, as a proportion of its height. - The height is considered to always be normalised as 1.0, so this will be a - value less that 1.0, indicating the proportion of the font that lies above - its baseline. - */ - float getAscent() const; - - /** Returns the descent of the font, as a proportion of its height. - The height is considered to always be normalised as 1.0, so this will be a - value less that 1.0, indicating the proportion of the font that lies below - its baseline. - */ - float getDescent() const; - - /** Returns the value by which you should multiply a JUCE font-height value to - convert it to the equivalent point-size. - */ - float getHeightToPointsFactor() const; + /** Returns information about the horizontal metrics of this font. */ + [[nodiscard]] TypefaceMetrics getMetrics (TypefaceMetricsKind) const; /** @deprecated This function has several shortcomings: @@ -154,7 +182,10 @@ public: Measures the width of a line of text. You should never need to call this! */ - float getStringWidth (const String& text, float normalisedHeight = 1.0f, float horizontalScale = 1.0f); + float getStringWidth (TypefaceMetricsKind, + const String& text, + float normalisedHeight = 1.0f, + float horizontalScale = 1.0f); /** @deprecated This function has several shortcomings: @@ -171,7 +202,8 @@ public: Converts a line of text into its glyph numbers and their positions. You should never need to call this! */ - void getGlyphPositions (const String& text, + void getGlyphPositions (TypefaceMetricsKind, + const String& text, Array& glyphs, Array& xOffsets, float normalisedHeight = 1.0f, @@ -180,7 +212,7 @@ public: /** Returns the outline for a glyph. The path returned will be normalised to a font height of 1.0. */ - void getOutlineForGlyph (int glyphNumber, Path& path); + void getOutlineForGlyph (TypefaceMetricsKind, int glyphNumber, Path& path) const; /** @deprecated @@ -196,7 +228,10 @@ public: preferred in new code. */ [[deprecated ("Prefer getLayersForGlyph")]] - EdgeTable* getEdgeTableForGlyph (int glyphNumber, const AffineTransform& transform, float normalisedHeight); + EdgeTable* getEdgeTableForGlyph (TypefaceMetricsKind, + int glyphNumber, + const AffineTransform& transform, + float normalisedHeight); /** Returns the layers that should be painted in order to display this glyph. @@ -210,7 +245,10 @@ public: The height is specified in JUCE font-height units. */ - std::vector getLayersForGlyph (int glyphNumber, const AffineTransform&, float normalisedHeight) const; + std::vector getLayersForGlyph (TypefaceMetricsKind, + int glyphNumber, + const AffineTransform&, + float normalisedHeight) const; /** Kinds of colour glyph format that may be implemented by a particular typeface. Most typefaces are monochromatic, and do not support any colour formats. diff --git a/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm b/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm index 1627e3a057..336abca799 100644 --- a/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm +++ b/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm @@ -696,7 +696,7 @@ void CoreGraphicsContext::drawGlyph (int glyphNumber, const AffineTransform& tra { Path p; auto& f = state->font; - f.getTypefacePtr()->getOutlineForGlyph (glyphNumber, p); + f.getTypefacePtr()->getOutlineForGlyph (f.getMetricsKind(), glyphNumber, p); const auto scale = f.getHeight(); fillPath (p, AffineTransform::scale (scale * f.getHorizontalScale(), scale).followedBy (transform)); diff --git a/modules/juce_graphics/native/juce_Fonts_mac.mm b/modules/juce_graphics/native/juce_Fonts_mac.mm index 3382c2c8e5..236a054b18 100644 --- a/modules/juce_graphics/native/juce_Fonts_mac.mm +++ b/modules/juce_graphics/native/juce_Fonts_mac.mm @@ -616,7 +616,7 @@ public: Native getNativeDetails() const override { - return Native { hb.get() }; + return Native { hb.get(), nonPortableMetrics }; } Typeface::Ptr createSystemFallback (const String& c, const String& language) const override @@ -698,6 +698,13 @@ private: CFUniquePtr ctFont; HbFont hb; MemoryBlock storage; + TypefaceAscentDescent nonPortableMetrics = [&] + { + const CFUniquePtr cgFont { CTFontCopyGraphicsFont (ctFont.get(), nullptr) }; + const auto upem = (float) CGFontGetUnitsPerEm (cgFont.get()); + return TypefaceAscentDescent { (float) std::abs (CGFontGetAscent (cgFont.get()) / upem), + (float) std::abs (CGFontGetDescent (cgFont.get()) / upem) }; + }(); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CoreTextTypeface) }; diff --git a/modules/juce_graphics/native/juce_RenderingHelpers.h b/modules/juce_graphics/native/juce_RenderingHelpers.h index f08d5a8fef..d1372d4166 100644 --- a/modules/juce_graphics/native/juce_RenderingHelpers.h +++ b/modules/juce_graphics/native/juce_RenderingHelpers.h @@ -189,7 +189,8 @@ public: { auto fontHeight = key.font.getHeight(); auto typeface = key.font.getTypefacePtr(); - return typeface->getLayersForGlyph (key.glyph, + return typeface->getLayersForGlyph (key.font.getMetricsKind(), + key.glyph, AffineTransform::scale (fontHeight * key.font.getHorizontalScale(), fontHeight), fontHeight); @@ -2629,7 +2630,7 @@ public: const auto fontTransform = AffineTransform::scale (fontHeight * stack->font.getHorizontalScale(), fontHeight).followedBy (t); const auto fullTransform = stack->transform.getTransformWith (fontTransform); - return std::tuple (stack->font.getTypefacePtr()->getLayersForGlyph (i, fullTransform, fontHeight), Point{}); + return std::tuple (stack->font.getTypefacePtr()->getLayersForGlyph (stack->font.getMetricsKind(), i, fullTransform, fontHeight), Point{}); }(); const auto initialFill = stack->fillType;