diff options
Diffstat (limited to 'gfx/thebes/gfxMacFont.cpp')
-rw-r--r-- | gfx/thebes/gfxMacFont.cpp | 565 |
1 files changed, 565 insertions, 0 deletions
diff --git a/gfx/thebes/gfxMacFont.cpp b/gfx/thebes/gfxMacFont.cpp new file mode 100644 index 0000000000..8780030a60 --- /dev/null +++ b/gfx/thebes/gfxMacFont.cpp @@ -0,0 +1,565 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gfxMacFont.h" + +#include "mozilla/MemoryReporting.h" +#include "mozilla/Sprintf.h" +#include "mozilla/StaticPrefs_gfx.h" + +#include "gfxCoreTextShaper.h" +#include <algorithm> +#include "gfxPlatformMac.h" +#include "gfxContext.h" +#include "gfxFontUtils.h" +#include "gfxMacPlatformFontList.h" +#include "gfxFontConstants.h" +#include "gfxTextRun.h" +#include "gfxUtils.h" +#include "nsCocoaFeatures.h" +#include "cairo-quartz.h" + +using namespace mozilla; +using namespace mozilla::gfx; + +template <class T> +struct TagEquals { + bool Equals(const T& aIter, uint32_t aTag) const { + return aIter.mTag == aTag; + } +}; + +gfxMacFont::gfxMacFont(const RefPtr<UnscaledFontMac>& aUnscaledFont, + MacOSFontEntry* aFontEntry, + const gfxFontStyle* aFontStyle) + : gfxFont(aUnscaledFont, aFontEntry, aFontStyle), + mCGFont(nullptr), + mCTFont(nullptr), + mFontSmoothingBackgroundColor(aFontStyle->fontSmoothingBackgroundColor), + mVariationFont(aFontEntry->HasVariations()) { + mApplySyntheticBold = aFontStyle->NeedsSyntheticBold(aFontEntry); + + if (mVariationFont) { + CGFontRef baseFont = aUnscaledFont->GetFont(); + if (!baseFont) { + mIsValid = false; + return; + } + + // Get the variation settings needed to instantiate the fontEntry + // for a particular fontStyle. + AutoTArray<gfxFontVariation, 4> vars; + aFontEntry->GetVariationsForStyle(vars, *aFontStyle); + + // Because of a Core Text bug, we need to ensure that if the font has + // an 'opsz' axis, it is always explicitly set, and NOT to the font's + // default value. (See bug 1457417, bug 1478720.) + // We record the result of searching the font's axes in the font entry, + // so that this only has to be done by the first instance created for + // a given font resource. + const uint32_t kOpszTag = HB_TAG('o', 'p', 's', 'z'); + const float kOpszFudgeAmount = 0.01f; + + if (!aFontEntry->mCheckedForOpszAxis) { + aFontEntry->mCheckedForOpszAxis = true; + AutoTArray<gfxFontVariationAxis, 4> axes; + aFontEntry->GetVariationAxes(axes); + auto index = axes.IndexOf(kOpszTag, 0, TagEquals<gfxFontVariationAxis>()); + if (index == axes.NoIndex) { + aFontEntry->mHasOpszAxis = false; + } else { + const auto& axis = axes[index]; + aFontEntry->mHasOpszAxis = true; + aFontEntry->mOpszAxis = axis; + // Pick a slightly-adjusted version of the default that we'll + // use to work around Core Text's habit of ignoring any attempt + // to explicitly set the default value. + aFontEntry->mAdjustedDefaultOpsz = + axis.mDefaultValue == axis.mMinValue + ? axis.mDefaultValue + kOpszFudgeAmount + : axis.mDefaultValue - kOpszFudgeAmount; + } + } + + // Add 'opsz' if not present, or tweak its value if it looks too close + // to the default (after clamping to the font's available range). + if (aFontEntry->mHasOpszAxis) { + auto index = vars.IndexOf(kOpszTag, 0, TagEquals<gfxFontVariation>()); + if (index == vars.NoIndex) { + gfxFontVariation opsz{kOpszTag, aFontEntry->mAdjustedDefaultOpsz}; + vars.AppendElement(opsz); + } else { + // Figure out a "safe" value that Core Text won't ignore. + auto& value = vars[index].mValue; + auto& axis = aFontEntry->mOpszAxis; + value = fmin(fmax(value, axis.mMinValue), axis.mMaxValue); + if (std::abs(value - axis.mDefaultValue) < kOpszFudgeAmount) { + value = aFontEntry->mAdjustedDefaultOpsz; + } + } + } + + mCGFont = UnscaledFontMac::CreateCGFontWithVariations( + baseFont, aUnscaledFont->AxesCache(), vars.Length(), vars.Elements()); + if (!mCGFont) { + ::CFRetain(baseFont); + mCGFont = baseFont; + } + } else { + mCGFont = aUnscaledFont->GetFont(); + if (!mCGFont) { + mIsValid = false; + return; + } + ::CFRetain(mCGFont); + } + + // InitMetrics will handle the sizeAdjust factor and set mAdjustedSize + InitMetrics(); + if (!mIsValid) { + return; + } + + // turn off font anti-aliasing based on user pref setting + if (mAdjustedSize <= + (gfxFloat)gfxPlatformMac::GetPlatform()->GetAntiAliasingThreshold()) { + mAntialiasOption = kAntialiasNone; + } else if (mStyle.useGrayscaleAntialiasing) { + mAntialiasOption = kAntialiasGrayscale; + } +} + +gfxMacFont::~gfxMacFont() { + if (mCGFont) { + ::CFRelease(mCGFont); + } + if (mCTFont) { + ::CFRelease(mCTFont); + } +} + +bool gfxMacFont::ShapeText(DrawTarget* aDrawTarget, const char16_t* aText, + uint32_t aOffset, uint32_t aLength, Script aScript, + nsAtom* aLanguage, bool aVertical, + RoundingFlags aRounding, + gfxShapedText* aShapedText) { + if (!mIsValid) { + NS_WARNING("invalid font! expect incorrect text rendering"); + return false; + } + + // Currently, we don't support vertical shaping via CoreText, + // so we ignore RequiresAATLayout if vertical is requested. + auto macFontEntry = static_cast<MacOSFontEntry*>(GetFontEntry()); + if (macFontEntry->RequiresAATLayout() && !aVertical && + StaticPrefs::gfx_font_rendering_coretext_enabled()) { + if (!mCoreTextShaper) { + mCoreTextShaper = MakeUnique<gfxCoreTextShaper>(this); + } + if (mCoreTextShaper->ShapeText(aDrawTarget, aText, aOffset, aLength, + aScript, aLanguage, aVertical, aRounding, + aShapedText)) { + PostShapingFixup(aDrawTarget, aText, aOffset, aLength, aVertical, + aShapedText); + if (GetFontEntry()->HasTrackingTable()) { + // Convert font size from device pixels back to CSS px + // to use in selecting tracking value + float trackSize = GetAdjustedSize() * + aShapedText->GetAppUnitsPerDevUnit() / + AppUnitsPerCSSPixel(); + float tracking = + GetFontEntry()->TrackingForCSSPx(trackSize) * mFUnitsConvFactor; + // Applying tracking is a lot like the adjustment we do for + // synthetic bold: we want to apply between clusters, not to + // non-spacing glyphs within a cluster. So we can reuse that + // helper here. + aShapedText->AdjustAdvancesForSyntheticBold(tracking, aOffset, aLength); + } + return true; + } + } + + return gfxFont::ShapeText(aDrawTarget, aText, aOffset, aLength, aScript, + aLanguage, aVertical, aRounding, aShapedText); +} + +gfxFont::RunMetrics gfxMacFont::Measure(const gfxTextRun* aTextRun, + uint32_t aStart, uint32_t aEnd, + BoundingBoxType aBoundingBoxType, + DrawTarget* aRefDrawTarget, + Spacing* aSpacing, + gfx::ShapedTextFlags aOrientation) { + gfxFont::RunMetrics metrics = + gfxFont::Measure(aTextRun, aStart, aEnd, aBoundingBoxType, aRefDrawTarget, + aSpacing, aOrientation); + + // if aBoundingBoxType is not TIGHT_HINTED_OUTLINE_EXTENTS then we need to add + // a pixel column each side of the bounding box in case of antialiasing + // "bleed" + if (aBoundingBoxType != TIGHT_HINTED_OUTLINE_EXTENTS && + metrics.mBoundingBox.width > 0) { + metrics.mBoundingBox.x -= aTextRun->GetAppUnitsPerDevUnit(); + metrics.mBoundingBox.width += aTextRun->GetAppUnitsPerDevUnit() * 2; + } + + return metrics; +} + +void gfxMacFont::InitMetrics() { + mIsValid = false; + ::memset(&mMetrics, 0, sizeof(mMetrics)); + + uint32_t upem = 0; + + // try to get unitsPerEm from sfnt head table, to avoid calling CGFont + // if possible (bug 574368) and because CGFontGetUnitsPerEm does not + // return the true value for OpenType/CFF fonts (it normalizes to 1000, + // which then leads to metrics errors when we read the 'hmtx' table to + // get glyph advances for HarfBuzz, see bug 580863) + CFDataRef headData = + ::CGFontCopyTableForTag(mCGFont, TRUETYPE_TAG('h', 'e', 'a', 'd')); + if (headData) { + if (size_t(::CFDataGetLength(headData)) >= sizeof(HeadTable)) { + const HeadTable* head = + reinterpret_cast<const HeadTable*>(::CFDataGetBytePtr(headData)); + upem = head->unitsPerEm; + } + ::CFRelease(headData); + } + if (!upem) { + upem = ::CGFontGetUnitsPerEm(mCGFont); + } + + if (upem < 16 || upem > 16384) { + // See http://www.microsoft.com/typography/otspec/head.htm +#ifdef DEBUG + char warnBuf[1024]; + SprintfLiteral(warnBuf, + "Bad font metrics for: %s (invalid unitsPerEm value)", + mFontEntry->Name().get()); + NS_WARNING(warnBuf); +#endif + return; + } + + mAdjustedSize = std::max(mStyle.size, 1.0); + mFUnitsConvFactor = mAdjustedSize / upem; + + // For CFF fonts, when scaling values read from CGFont* APIs, we need to + // use CG's idea of unitsPerEm, which may differ from the "true" value in + // the head table of the font (see bug 580863) + gfxFloat cgConvFactor; + if (static_cast<MacOSFontEntry*>(mFontEntry.get())->IsCFF()) { + cgConvFactor = mAdjustedSize / ::CGFontGetUnitsPerEm(mCGFont); + } else { + cgConvFactor = mFUnitsConvFactor; + } + + // Try to read 'sfnt' metrics; for local, non-sfnt fonts ONLY, fall back to + // platform APIs. The InitMetrics...() functions will set mIsValid on success. + if (!InitMetricsFromSfntTables(mMetrics) && + (!mFontEntry->IsUserFont() || mFontEntry->IsLocalUserFont())) { + InitMetricsFromPlatform(); + } + if (!mIsValid) { + return; + } + + if (mMetrics.xHeight == 0.0) { + mMetrics.xHeight = ::CGFontGetXHeight(mCGFont) * cgConvFactor; + } + + if (mMetrics.capHeight == 0.0) { + mMetrics.capHeight = ::CGFontGetCapHeight(mCGFont) * cgConvFactor; + } + + if (mStyle.sizeAdjust > 0.0 && mStyle.size > 0.0 && mMetrics.xHeight > 0.0) { + // apply font-size-adjust, and recalculate metrics + gfxFloat aspect = mMetrics.xHeight / mStyle.size; + mAdjustedSize = mStyle.GetAdjustedSize(aspect); + mFUnitsConvFactor = mAdjustedSize / upem; + if (static_cast<MacOSFontEntry*>(mFontEntry.get())->IsCFF()) { + cgConvFactor = mAdjustedSize / ::CGFontGetUnitsPerEm(mCGFont); + } else { + cgConvFactor = mFUnitsConvFactor; + } + mMetrics.xHeight = 0.0; + if (!InitMetricsFromSfntTables(mMetrics) && + (!mFontEntry->IsUserFont() || mFontEntry->IsLocalUserFont())) { + InitMetricsFromPlatform(); + } + if (!mIsValid) { + // this shouldn't happen, as we succeeded earlier before applying + // the size-adjust factor! But check anyway, for paranoia's sake. + return; + } + if (mMetrics.xHeight == 0.0) { + mMetrics.xHeight = ::CGFontGetXHeight(mCGFont) * cgConvFactor; + } + } + + // Once we reach here, we've got basic metrics and set mIsValid = TRUE; + // there should be no further points of actual failure in InitMetrics(). + // (If one is introduced, be sure to reset mIsValid to FALSE!) + + mMetrics.emHeight = mAdjustedSize; + + // Measure/calculate additional metrics, independent of whether we used + // the tables directly or ATS metrics APIs + + CFDataRef cmap = + ::CGFontCopyTableForTag(mCGFont, TRUETYPE_TAG('c', 'm', 'a', 'p')); + + uint32_t glyphID; + if (mMetrics.aveCharWidth <= 0) { + mMetrics.aveCharWidth = GetCharWidth(cmap, 'x', &glyphID, cgConvFactor); + if (glyphID == 0) { + // we didn't find 'x', so use maxAdvance rather than zero + mMetrics.aveCharWidth = mMetrics.maxAdvance; + } + } + if (IsSyntheticBold()) { + mMetrics.aveCharWidth += GetSyntheticBoldOffset(); + mMetrics.maxAdvance += GetSyntheticBoldOffset(); + } + + mMetrics.spaceWidth = GetCharWidth(cmap, ' ', &glyphID, cgConvFactor); + if (glyphID == 0) { + // no space glyph?! + mMetrics.spaceWidth = mMetrics.aveCharWidth; + } + mSpaceGlyph = glyphID; + + mMetrics.zeroWidth = GetCharWidth(cmap, '0', &glyphID, cgConvFactor); + if (glyphID == 0) { + mMetrics.zeroWidth = -1.0; // indicates not found + } + + if (cmap) { + ::CFRelease(cmap); + } + + CalculateDerivedMetrics(mMetrics); + + SanitizeMetrics(&mMetrics, mFontEntry->mIsBadUnderlineFont); + +#if 0 + fprintf (stderr, "Font: %p (%s) size: %f\n", this, + NS_ConvertUTF16toUTF8(GetName()).get(), mStyle.size); +// fprintf (stderr, " fbounds.origin.x %f y %f size.width %f height %f\n", fbounds.origin.x, fbounds.origin.y, fbounds.size.width, fbounds.size.height); + fprintf (stderr, " emHeight: %f emAscent: %f emDescent: %f\n", mMetrics.emHeight, mMetrics.emAscent, mMetrics.emDescent); + fprintf (stderr, " maxAscent: %f maxDescent: %f maxAdvance: %f\n", mMetrics.maxAscent, mMetrics.maxDescent, mMetrics.maxAdvance); + fprintf (stderr, " internalLeading: %f externalLeading: %f\n", mMetrics.internalLeading, mMetrics.externalLeading); + fprintf (stderr, " spaceWidth: %f aveCharWidth: %f xHeight: %f capHeight: %f\n", mMetrics.spaceWidth, mMetrics.aveCharWidth, mMetrics.xHeight, mMetrics.capHeight); + fprintf (stderr, " uOff: %f uSize: %f stOff: %f stSize: %f\n", mMetrics.underlineOffset, mMetrics.underlineSize, mMetrics.strikeoutOffset, mMetrics.strikeoutSize); +#endif +} + +gfxFloat gfxMacFont::GetCharWidth(CFDataRef aCmap, char16_t aUniChar, + uint32_t* aGlyphID, gfxFloat aConvFactor) { + CGGlyph glyph = 0; + + if (aCmap) { + glyph = gfxFontUtils::MapCharToGlyph(::CFDataGetBytePtr(aCmap), + ::CFDataGetLength(aCmap), aUniChar); + } + + if (aGlyphID) { + *aGlyphID = glyph; + } + + if (glyph) { + int advance; + if (::CGFontGetGlyphAdvances(mCGFont, &glyph, 1, &advance)) { + return advance * aConvFactor; + } + } + + return 0; +} + +/* static */ +CTFontRef gfxMacFont::CreateCTFontFromCGFontWithVariations( + CGFontRef aCGFont, CGFloat aSize, bool aInstalledFont, + CTFontDescriptorRef aFontDesc) { + // Avoid calling potentially buggy variation APIs on pre-Sierra macOS + // versions (see bug 1331683). + // + // And on HighSierra, CTFontCreateWithGraphicsFont properly carries over + // variation settings from the CGFont to CTFont, so we don't need to do + // the extra work here -- and this seems to avoid Core Text crashiness + // seen in bug 1454094. + // + // However, for installed fonts it seems we DO need to copy the variations + // explicitly even on 10.13, otherwise fonts fail to render (as in bug + // 1455494) when non-default values are used. Fortunately, the crash + // mentioned above occurs with data fonts, not (AFAICT) with system- + // installed fonts. + // + // So we only need to do this "the hard way" on Sierra, and on HighSierra + // for system-installed fonts; in other cases just let the standard CTFont + // function do its thing. + // + // NOTE in case this ever needs further adjustment: there is similar logic + // in four places in the tree (sadly): + // CreateCTFontFromCGFontWithVariations in gfxMacFont.cpp + // CreateCTFontFromCGFontWithVariations in ScaledFontMac.cpp + // CreateCTFontFromCGFontWithVariations in cairo-quartz-font.c + // ctfont_create_exact_copy in SkFontHost_mac.cpp + + CTFontRef ctFont; + if (nsCocoaFeatures::OnSierraExactly() || + (aInstalledFont && nsCocoaFeatures::OnHighSierraOrLater())) { + CFDictionaryRef variations = ::CGFontCopyVariations(aCGFont); + if (variations) { + CFDictionaryRef varAttr = ::CFDictionaryCreate( + nullptr, (const void**)&kCTFontVariationAttribute, + (const void**)&variations, 1, &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + ::CFRelease(variations); + + CTFontDescriptorRef varDesc = + aFontDesc + ? ::CTFontDescriptorCreateCopyWithAttributes(aFontDesc, varAttr) + : ::CTFontDescriptorCreateWithAttributes(varAttr); + ::CFRelease(varAttr); + + ctFont = ::CTFontCreateWithGraphicsFont(aCGFont, aSize, nullptr, varDesc); + ::CFRelease(varDesc); + } else { + ctFont = + ::CTFontCreateWithGraphicsFont(aCGFont, aSize, nullptr, aFontDesc); + } + } else { + ctFont = ::CTFontCreateWithGraphicsFont(aCGFont, aSize, nullptr, aFontDesc); + } + + return ctFont; +} + +int32_t gfxMacFont::GetGlyphWidth(uint16_t aGID) { + if (mVariationFont) { + // Avoid a potential Core Text crash (bug 1450209) by using + // CoreGraphics glyph advance API. This is inferior for 'sbix' + // fonts, but those won't have variations, so it's OK. + int cgAdvance; + if (::CGFontGetGlyphAdvances(mCGFont, &aGID, 1, &cgAdvance)) { + return cgAdvance * mFUnitsConvFactor * 0x10000; + } + } + + if (!mCTFont) { + bool isInstalledFont = + !mFontEntry->IsUserFont() || mFontEntry->IsLocalUserFont(); + mCTFont = CreateCTFontFromCGFontWithVariations(mCGFont, mAdjustedSize, + isInstalledFont); + if (!mCTFont) { // shouldn't happen, but let's be safe + NS_WARNING("failed to create CTFontRef to measure glyph width"); + return 0; + } + } + + CGSize advance; + ::CTFontGetAdvancesForGlyphs(mCTFont, kCTFontDefaultOrientation, &aGID, + &advance, 1); + return advance.width * 0x10000; +} + +bool gfxMacFont::GetGlyphBounds(uint16_t aGID, gfxRect* aBounds, bool aTight) { + CGRect bb; + if (!::CGFontGetGlyphBBoxes(mCGFont, &aGID, 1, &bb)) { + return false; + } + + // broken fonts can return incorrect bounds for some null characters, + // see https://bugzilla.mozilla.org/show_bug.cgi?id=534260 + if (bb.origin.x == -32767 && bb.origin.y == -32767 && + bb.size.width == 65534 && bb.size.height == 65534) { + *aBounds = gfxRect(0, 0, 0, 0); + return true; + } + + gfxRect bounds(bb.origin.x, -(bb.origin.y + bb.size.height), bb.size.width, + bb.size.height); + bounds.Scale(mFUnitsConvFactor); + *aBounds = bounds; + return true; +} + +// Try to initialize font metrics via platform APIs (CG/CT), +// and set mIsValid = TRUE on success. +// We ONLY call this for local (platform) fonts that are not sfnt format; +// for sfnts, including ALL downloadable fonts, we prefer to use +// InitMetricsFromSfntTables and avoid platform APIs. +void gfxMacFont::InitMetricsFromPlatform() { + CTFontRef ctFont = + ::CTFontCreateWithGraphicsFont(mCGFont, mAdjustedSize, nullptr, nullptr); + if (!ctFont) { + return; + } + + mMetrics.underlineOffset = ::CTFontGetUnderlinePosition(ctFont); + mMetrics.underlineSize = ::CTFontGetUnderlineThickness(ctFont); + + mMetrics.externalLeading = ::CTFontGetLeading(ctFont); + + mMetrics.maxAscent = ::CTFontGetAscent(ctFont); + mMetrics.maxDescent = ::CTFontGetDescent(ctFont); + + // this is not strictly correct, but neither CTFont nor CGFont seems to + // provide maxAdvance, unless we were to iterate over all the glyphs + // (which isn't worth the cost here) + CGRect r = ::CTFontGetBoundingBox(ctFont); + mMetrics.maxAdvance = r.size.width; + + // aveCharWidth is also not provided, so leave it at zero + // (fallback code in gfxMacFont::InitMetrics will then try measuring 'x'); + // this could lead to less-than-"perfect" text field sizing when width is + // specified as a number of characters, and the font in use is a non-sfnt + // legacy font, but that's a sufficiently obscure edge case that we can + // ignore the potential discrepancy. + mMetrics.aveCharWidth = 0; + + mMetrics.xHeight = ::CTFontGetXHeight(ctFont); + mMetrics.capHeight = ::CTFontGetCapHeight(ctFont); + + ::CFRelease(ctFont); + + mIsValid = true; +} + +already_AddRefed<ScaledFont> gfxMacFont::GetScaledFont(DrawTarget* aTarget) { + if (!mAzureScaledFont) { + mAzureScaledFont = Factory::CreateScaledFontForMacFont( + GetCGFontRef(), GetUnscaledFont(), GetAdjustedSize(), + ToDeviceColor(mFontSmoothingBackgroundColor), + !mStyle.useGrayscaleAntialiasing, IsSyntheticBold()); + if (!mAzureScaledFont) { + return nullptr; + } + InitializeScaledFont(); + } + + RefPtr<ScaledFont> scaledFont(mAzureScaledFont); + return scaledFont.forget(); +} + +bool gfxMacFont::ShouldRoundXOffset(cairo_t* aCairo) const { + // Quartz surfaces implement show_glyphs for Quartz fonts + return aCairo && cairo_surface_get_type(cairo_get_target(aCairo)) != + CAIRO_SURFACE_TYPE_QUARTZ; +} + +void gfxMacFont::AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf, + FontCacheSizes* aSizes) const { + gfxFont::AddSizeOfExcludingThis(aMallocSizeOf, aSizes); + // mCGFont is shared with the font entry, so not counted here; +} + +void gfxMacFont::AddSizeOfIncludingThis(MallocSizeOf aMallocSizeOf, + FontCacheSizes* aSizes) const { + aSizes->mFontInstances += aMallocSizeOf(this); + AddSizeOfExcludingThis(aMallocSizeOf, aSizes); +} |