/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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 "nsMathMLChar.h" #include "gfxContext.h" #include "gfxTextRun.h" #include "gfxUtils.h" #include "mozilla/dom/Document.h" #include "mozilla/gfx/2D.h" #include "mozilla/intl/UnicodeScriptCodes.h" #include "mozilla/ComputedStyle.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/UniquePtr.h" #include "mozilla/Unused.h" #include "mozilla/StaticPrefs_mathml.h" #include "nsCOMPtr.h" #include "nsDeviceContext.h" #include "nsFontMetrics.h" #include "nsIFrame.h" #include "nsLayoutUtils.h" #include "nsPresContext.h" #include "nsUnicharUtils.h" #include "mozilla/Preferences.h" #include "nsIPersistentProperties2.h" #include "nsIObserverService.h" #include "nsIObserver.h" #include "nsNetUtil.h" #include "nsContentUtils.h" #include "mozilla/LookAndFeel.h" #include "nsCSSRendering.h" #include "mozilla/Sprintf.h" #include "nsDisplayList.h" #include "nsMathMLOperators.h" #include #include "gfxMathTable.h" using namespace mozilla; using namespace mozilla::gfx; using namespace mozilla::image; //#define NOISY_SEARCH 1 // BUG 848725 Drawing failure with stretchy horizontal parenthesis when no fonts // are installed. "kMaxScaleFactor" is required to limit the scale for the // vertical and horizontal stretchy operators. static const float kMaxScaleFactor = 20.0; static const float kLargeOpFactor = float(M_SQRT2); static const float kIntegralFactor = 2.0; static void NormalizeDefaultFont(nsFont& aFont, float aFontSizeInflation) { aFont.size.ScaleBy(aFontSizeInflation); } // ----------------------------------------------------------------------------- static const nsGlyphCode kNullGlyph = {{{0, 0}}, 0}; // ----------------------------------------------------------------------------- // nsGlyphTable is a class that provides an interface for accessing glyphs // of stretchy chars. It acts like a table that stores the variants of bigger // sizes (if any) and the partial glyphs needed to build extensible symbols. // // Bigger sizes (if any) of the char can then be retrieved with BigOf(...). // Partial glyphs can be retrieved with ElementAt(...). // // A table consists of "nsGlyphCode"s which are viewed either as Unicode // points (for nsPropertiesTable) or as direct glyph indices (for // nsOpenTypeTable) // ----------------------------------------------------------------------------- class nsGlyphTable { public: virtual ~nsGlyphTable() = default; virtual const nsCString& FontNameFor(const nsGlyphCode& aGlyphCode) const = 0; // Getters for the parts virtual nsGlyphCode ElementAt(DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, char16_t aChar, bool aVertical, uint32_t aPosition) = 0; virtual nsGlyphCode BigOf(DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, char16_t aChar, bool aVertical, uint32_t aSize) = 0; // True if this table contains parts to render this char virtual bool HasPartsOf(DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, char16_t aChar, bool aVertical) = 0; virtual already_AddRefed MakeTextRun( DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, const nsGlyphCode& aGlyph) = 0; protected: nsGlyphTable() : mCharCache(0) {} // For speedy re-use, we always cache the last data used in the table. // mCharCache is the Unicode point of the last char that was queried in this // table. char16_t mCharCache; }; // An instance of nsPropertiesTable is associated with one primary font. Extra // glyphs can be taken in other additional fonts when stretching certain // characters. // These supplementary fonts are referred to as "external" fonts to the table. // General format of MathFont Property Files from which glyph data are // retrieved: // ----------------------------------------------------------------------------- // Each font should have its set of glyph data. For example, the glyph data for // the "Symbol" font and the "MT Extra" font are in "mathfontSymbol.properties" // and "mathfontMTExtra.properties", respectively. The mathfont property file // is a set of all the stretchy MathML characters that can be rendered with that // font using larger and/or partial glyphs. The entry of each stretchy character // in the mathfont property file gives, in that order, the 4 partial glyphs: // Top (or Left), Middle, Bottom (or Right), Glue; and the variants of bigger // sizes (if any). // A position that is not relevant to a particular character is indicated there // with the UNICODE REPLACEMENT CHARACTER 0xFFFD. // ----------------------------------------------------------------------------- #define NS_TABLE_STATE_ERROR -1 #define NS_TABLE_STATE_EMPTY 0 #define NS_TABLE_STATE_READY 1 // helper to trim off comments from data in a MathFont Property File static void Clean(nsString& aValue) { // chop the trailing # comment portion if any ... int32_t comment = aValue.RFindChar('#'); if (comment > 0) aValue.Truncate(comment); aValue.CompressWhitespace(); } // helper to load a MathFont Property File static nsresult LoadProperties(const nsACString& aName, nsCOMPtr& aProperties) { nsAutoCString uriStr; uriStr.AssignLiteral("resource://gre/res/fonts/mathfont"); uriStr.Append(aName); uriStr.StripWhitespace(); // that may come from aName uriStr.AppendLiteral(".properties"); return NS_LoadPersistentPropertiesFromURISpec(getter_AddRefs(aProperties), uriStr); } class nsPropertiesTable final : public nsGlyphTable { public: explicit nsPropertiesTable(const nsACString& aPrimaryFontName) : mState(NS_TABLE_STATE_EMPTY) { MOZ_COUNT_CTOR(nsPropertiesTable); mGlyphCodeFonts.AppendElement(aPrimaryFontName); } MOZ_COUNTED_DTOR(nsPropertiesTable) const nsCString& PrimaryFontName() const { return mGlyphCodeFonts[0]; } const nsCString& FontNameFor(const nsGlyphCode& aGlyphCode) const override { NS_ASSERTION(!aGlyphCode.IsGlyphID(), "nsPropertiesTable can only access glyphs by code point"); return mGlyphCodeFonts[aGlyphCode.font]; } virtual nsGlyphCode ElementAt(DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, char16_t aChar, bool aVertical, uint32_t aPosition) override; virtual nsGlyphCode BigOf(DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, char16_t aChar, bool aVertical, uint32_t aSize) override { return ElementAt(aDrawTarget, aAppUnitsPerDevPixel, aFontGroup, aChar, aVertical, 4 + aSize); } virtual bool HasPartsOf(DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, char16_t aChar, bool aVertical) override { return (ElementAt(aDrawTarget, aAppUnitsPerDevPixel, aFontGroup, aChar, aVertical, 0) .Exists() || ElementAt(aDrawTarget, aAppUnitsPerDevPixel, aFontGroup, aChar, aVertical, 1) .Exists() || ElementAt(aDrawTarget, aAppUnitsPerDevPixel, aFontGroup, aChar, aVertical, 2) .Exists() || ElementAt(aDrawTarget, aAppUnitsPerDevPixel, aFontGroup, aChar, aVertical, 3) .Exists()); } virtual already_AddRefed MakeTextRun( DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, const nsGlyphCode& aGlyph) override; private: // mGlyphCodeFonts[0] is the primary font associated to this table. The // others are possible "external" fonts for glyphs not in the primary font // but which are needed to stretch certain characters in the table nsTArray mGlyphCodeFonts; // Tri-state variable for error/empty/ready int32_t mState; // The set of glyph data in this table, as provided by the MathFont Property // File nsCOMPtr mGlyphProperties; // mGlyphCache is a buffer containing the glyph data associated with // mCharCache. // For a property line 'key = value' in the MathFont Property File, // mCharCache will retain the 'key' -- which is a Unicode point, while // mGlyphCache will retain the 'value', which is a consecutive list of // nsGlyphCodes, i.e., the pairs of 'code@font' needed by the char -- in // which 'code@0' can be specified // without the optional '@0'. However, to ease subsequent processing, // mGlyphCache excludes the '@' symbol and explicitly inserts all optional '0' // that indicates the primary font identifier. Specifically therefore, the // k-th glyph is characterized by : // 1) mGlyphCache[3*k],mGlyphCache[3*k+1] : its Unicode point // 2) mGlyphCache[3*k+2] : the numeric identifier of the font where it comes // from. // A font identifier of '0' means the default primary font associated to this // table. Other digits map to the "external" fonts that may have been // specified in the MathFont Property File. nsString mGlyphCache; }; /* virtual */ nsGlyphCode nsPropertiesTable::ElementAt(DrawTarget* /* aDrawTarget */, int32_t /* aAppUnitsPerDevPixel */, gfxFontGroup* /* aFontGroup */, char16_t aChar, bool /* aVertical */, uint32_t aPosition) { if (mState == NS_TABLE_STATE_ERROR) return kNullGlyph; // Load glyph properties if this is the first time we have been here if (mState == NS_TABLE_STATE_EMPTY) { nsresult rv = LoadProperties(PrimaryFontName(), mGlyphProperties); #ifdef DEBUG nsAutoCString uriStr; uriStr.AssignLiteral("resource://gre/res/fonts/mathfont"); uriStr.Append(PrimaryFontName()); uriStr.StripWhitespace(); // that may come from mGlyphCodeFonts uriStr.AppendLiteral(".properties"); printf("Loading %s ... %s\n", uriStr.get(), (NS_FAILED(rv)) ? "Failed" : "Done"); #endif if (NS_FAILED(rv)) { mState = NS_TABLE_STATE_ERROR; // never waste time with this table again return kNullGlyph; } mState = NS_TABLE_STATE_READY; // see if there are external fonts needed for certain chars in this table nsAutoCString key; nsAutoString value; for (int32_t i = 1;; i++) { key.AssignLiteral("external."); key.AppendInt(i, 10); rv = mGlyphProperties->GetStringProperty(key, value); if (NS_FAILED(rv)) { break; } Clean(value); mGlyphCodeFonts.AppendElement(NS_ConvertUTF16toUTF8(value)); } } // Update our cache if it is not associated to this character if (mCharCache != aChar) { // The key in the property file is interpreted as ASCII and kept // as such ... char key[10]; SprintfLiteral(key, "\\u%04X", aChar); nsAutoString value; nsresult rv = mGlyphProperties->GetStringProperty(nsDependentCString(key), value); if (NS_FAILED(rv)) return kNullGlyph; Clean(value); // See if this char uses external fonts; e.g., if the 2nd glyph is taken // from the external font '1', the property line looks like // \uNNNN = \uNNNN\uNNNN@1\uNNNN. // This is where mGlyphCache is pre-processed to explicitly store all glyph // codes as combined pairs of 'code@font', excluding the '@' separator. This // means that mGlyphCache[3*k],mGlyphCache[3*k+1] will later be rendered // with mGlyphCodeFonts[mGlyphCache[3*k+2]] // Note: font identifier is internally an ASCII digit to avoid the null // char issue nsAutoString buffer; int32_t length = value.Length(); int32_t i = 0; // index in value while (i < length) { char16_t code = value[i]; ++i; buffer.Append(code); // Read the next word if we have a non-BMP character. if (i < length && NS_IS_HIGH_SURROGATE(code)) { code = value[i]; ++i; } else { code = char16_t('\0'); } buffer.Append(code); // See if an external font is needed for the code point. // Limit of 9 external fonts char16_t font = 0; if (i + 1 < length && value[i] == char16_t('@') && value[i + 1] >= char16_t('0') && value[i + 1] <= char16_t('9')) { ++i; font = value[i] - '0'; ++i; if (font >= mGlyphCodeFonts.Length()) { NS_ERROR("Nonexistent font referenced in glyph table"); return kNullGlyph; } } buffer.Append(font); } // update our cache with the new settings mGlyphCache.Assign(buffer); mCharCache = aChar; } // 3* is to account for the code@font pairs uint32_t index = 3 * aPosition; if (index + 2 >= mGlyphCache.Length()) return kNullGlyph; nsGlyphCode ch; ch.code[0] = mGlyphCache.CharAt(index); ch.code[1] = mGlyphCache.CharAt(index + 1); ch.font = mGlyphCache.CharAt(index + 2); return ch.code[0] == char16_t(0xFFFD) ? kNullGlyph : ch; } /* virtual */ already_AddRefed nsPropertiesTable::MakeTextRun( DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, const nsGlyphCode& aGlyph) { NS_ASSERTION(!aGlyph.IsGlyphID(), "nsPropertiesTable can only access glyphs by code point"); return aFontGroup->MakeTextRun(aGlyph.code, aGlyph.Length(), aDrawTarget, aAppUnitsPerDevPixel, gfx::ShapedTextFlags(), nsTextFrameUtils::Flags(), nullptr); } // An instance of nsOpenTypeTable is associated with one gfxFontEntry that // corresponds to an Open Type font with a MATH table. All the glyphs come from // the same font and the calls to access size variants and parts are directly // forwarded to the gfx code. class nsOpenTypeTable final : public nsGlyphTable { public: MOZ_COUNTED_DTOR(nsOpenTypeTable) virtual nsGlyphCode ElementAt(DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, char16_t aChar, bool aVertical, uint32_t aPosition) override; virtual nsGlyphCode BigOf(DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, char16_t aChar, bool aVertical, uint32_t aSize) override; virtual bool HasPartsOf(DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, char16_t aChar, bool aVertical) override; const nsCString& FontNameFor(const nsGlyphCode& aGlyphCode) const override { NS_ASSERTION(aGlyphCode.IsGlyphID(), "nsOpenTypeTable can only access glyphs by id"); return mFontFamilyName; } virtual already_AddRefed MakeTextRun( DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, const nsGlyphCode& aGlyph) override; // This returns a new OpenTypeTable instance to give access to OpenType MATH // table or nullptr if the font does not have such table. Ownership is passed // to the caller. static UniquePtr Create(gfxFont* aFont) { if (!aFont->TryGetMathTable()) { return nullptr; } return WrapUnique(new nsOpenTypeTable(aFont)); } private: RefPtr mFont; nsCString mFontFamilyName; uint32_t mGlyphID; explicit nsOpenTypeTable(gfxFont* aFont) : mFont(aFont), mFontFamilyName(aFont->GetFontEntry()->FamilyName()), mGlyphID(0) { MOZ_COUNT_CTOR(nsOpenTypeTable); } void UpdateCache(DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, char16_t aChar); }; void nsOpenTypeTable::UpdateCache(DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, char16_t aChar) { if (mCharCache != aChar) { RefPtr textRun = aFontGroup->MakeTextRun( &aChar, 1, aDrawTarget, aAppUnitsPerDevPixel, gfx::ShapedTextFlags(), nsTextFrameUtils::Flags(), nullptr); const gfxTextRun::CompressedGlyph& data = textRun->GetCharacterGlyphs()[0]; if (data.IsSimpleGlyph()) { mGlyphID = data.GetSimpleGlyph(); } else if (data.GetGlyphCount() == 1) { mGlyphID = textRun->GetDetailedGlyphs(0)->mGlyphID; } else { mGlyphID = 0; } mCharCache = aChar; } } /* virtual */ nsGlyphCode nsOpenTypeTable::ElementAt(DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, char16_t aChar, bool aVertical, uint32_t aPosition) { UpdateCache(aDrawTarget, aAppUnitsPerDevPixel, aFontGroup, aChar); uint32_t parts[4]; if (!mFont->MathTable()->VariantsParts(mGlyphID, aVertical, parts)) { return kNullGlyph; } uint32_t glyphID = parts[aPosition]; if (!glyphID) { return kNullGlyph; } nsGlyphCode glyph; glyph.glyphID = glyphID; glyph.font = -1; return glyph; } /* virtual */ nsGlyphCode nsOpenTypeTable::BigOf(DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, char16_t aChar, bool aVertical, uint32_t aSize) { UpdateCache(aDrawTarget, aAppUnitsPerDevPixel, aFontGroup, aChar); uint32_t glyphID = mFont->MathTable()->VariantsSize(mGlyphID, aVertical, aSize); if (!glyphID) { return kNullGlyph; } nsGlyphCode glyph; glyph.glyphID = glyphID; glyph.font = -1; return glyph; } /* virtual */ bool nsOpenTypeTable::HasPartsOf(DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, char16_t aChar, bool aVertical) { UpdateCache(aDrawTarget, aAppUnitsPerDevPixel, aFontGroup, aChar); uint32_t parts[4]; if (!mFont->MathTable()->VariantsParts(mGlyphID, aVertical, parts)) { return false; } return parts[0] || parts[1] || parts[2] || parts[3]; } /* virtual */ already_AddRefed nsOpenTypeTable::MakeTextRun( DrawTarget* aDrawTarget, int32_t aAppUnitsPerDevPixel, gfxFontGroup* aFontGroup, const nsGlyphCode& aGlyph) { NS_ASSERTION(aGlyph.IsGlyphID(), "nsOpenTypeTable can only access glyphs by id"); gfxTextRunFactory::Parameters params = { aDrawTarget, nullptr, nullptr, nullptr, 0, aAppUnitsPerDevPixel}; RefPtr textRun = gfxTextRun::Create(¶ms, 1, aFontGroup, gfx::ShapedTextFlags(), nsTextFrameUtils::Flags()); RefPtr font = aFontGroup->GetFirstValidFont(); textRun->AddGlyphRun(font, FontMatchType::Kind::kFontGroup, 0, false, gfx::ShapedTextFlags::TEXT_ORIENT_HORIZONTAL, false); // We don't care about CSS writing mode here; // math runs are assumed to be horizontal. gfxTextRun::DetailedGlyph detailedGlyph; detailedGlyph.mGlyphID = aGlyph.glyphID; detailedGlyph.mAdvance = NSToCoordRound( aAppUnitsPerDevPixel * font->GetGlyphAdvance(aGlyph.glyphID)); textRun->SetDetailedGlyphs(0, 1, &detailedGlyph); return textRun.forget(); } // ----------------------------------------------------------------------------- // This is the list of all the applicable glyph tables. // We will maintain a single global instance that will only reveal those // glyph tables that are associated to fonts currently installed on the // user' system. The class is an XPCOM shutdown observer to allow us to // free its allocated data at shutdown class nsGlyphTableList final : public nsIObserver { public: NS_DECL_ISUPPORTS NS_DECL_NSIOBSERVER nsPropertiesTable mUnicodeTable; nsGlyphTableList() : mUnicodeTable("Unicode"_ns) {} nsresult Initialize(); nsresult Finalize(); // Add a glyph table in the list, return the new table that was added nsGlyphTable* AddGlyphTable(const nsACString& aPrimaryFontName); // Find the glyph table in the list corresponding to the given font family. nsGlyphTable* GetGlyphTableFor(const nsACString& aFamily); private: ~nsGlyphTableList() = default; nsPropertiesTable* PropertiesTableAt(int32_t aIndex) { return &mPropertiesTableList.ElementAt(aIndex); } int32_t PropertiesTableCount() { return mPropertiesTableList.Length(); } // List of glyph tables; nsTArray mPropertiesTableList; }; NS_IMPL_ISUPPORTS(nsGlyphTableList, nsIObserver) // ----------------------------------------------------------------------------- // Here is the global list of applicable glyph tables that we will be using static nsGlyphTableList* gGlyphTableList = nullptr; static bool gGlyphTableInitialized = false; // XPCOM shutdown observer NS_IMETHODIMP nsGlyphTableList::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* someData) { Finalize(); return NS_OK; } // Add an observer to XPCOM shutdown so that we can free our data at shutdown nsresult nsGlyphTableList::Initialize() { nsCOMPtr obs = mozilla::services::GetObserverService(); if (!obs) return NS_ERROR_FAILURE; nsresult rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } // Remove our observer and free the memory that were allocated for us nsresult nsGlyphTableList::Finalize() { // Remove our observer from the observer service nsresult rv = NS_OK; nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) rv = obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); else rv = NS_ERROR_FAILURE; gGlyphTableInitialized = false; // our oneself will be destroyed when our |Release| is called by the observer NS_IF_RELEASE(gGlyphTableList); return rv; } nsGlyphTable* nsGlyphTableList::AddGlyphTable( const nsACString& aPrimaryFontName) { // See if there is already a special table for this family. nsGlyphTable* glyphTable = GetGlyphTableFor(aPrimaryFontName); if (glyphTable != &mUnicodeTable) return glyphTable; // allocate a table glyphTable = mPropertiesTableList.AppendElement(aPrimaryFontName); return glyphTable; } nsGlyphTable* nsGlyphTableList::GetGlyphTableFor(const nsACString& aFamily) { for (int32_t i = 0; i < PropertiesTableCount(); i++) { nsPropertiesTable* glyphTable = PropertiesTableAt(i); const nsCString& primaryFontName = glyphTable->PrimaryFontName(); // TODO: would be nice to consider StripWhitespace and other aliasing if (primaryFontName.Equals(aFamily, nsCaseInsensitiveCStringComparator)) { return glyphTable; } } // Fall back to default Unicode table return &mUnicodeTable; } // ----------------------------------------------------------------------------- static nsresult InitCharGlobals() { NS_ASSERTION(!gGlyphTableInitialized, "Error -- already initialized"); gGlyphTableInitialized = true; // Allocate the placeholders for the preferred parts and variants nsresult rv = NS_ERROR_OUT_OF_MEMORY; RefPtr glyphTableList = new nsGlyphTableList(); if (glyphTableList) { rv = glyphTableList->Initialize(); } if (NS_FAILED(rv)) { return rv; } // The gGlyphTableList has been successfully registered as a shutdown // observer and will be deleted at shutdown. We now add some private // per font-family tables for stretchy operators, in order of preference. // Do not include the Unicode table in this list. if (!glyphTableList->AddGlyphTable("STIXGeneral"_ns)) { rv = NS_ERROR_OUT_OF_MEMORY; } glyphTableList.forget(&gGlyphTableList); return rv; } // ----------------------------------------------------------------------------- // And now the implementation of nsMathMLChar nsMathMLChar::~nsMathMLChar() { MOZ_COUNT_DTOR(nsMathMLChar); } ComputedStyle* nsMathMLChar::GetComputedStyle() const { NS_ASSERTION(mComputedStyle, "chars should always have a ComputedStyle"); return mComputedStyle; } void nsMathMLChar::SetComputedStyle(ComputedStyle* aComputedStyle) { MOZ_ASSERT(aComputedStyle); mComputedStyle = aComputedStyle; } void nsMathMLChar::SetData(nsString& aData) { if (!gGlyphTableInitialized) { InitCharGlobals(); } mData = aData; // some assumptions until proven otherwise // note that mGlyph is not initialized mDirection = NS_STRETCH_DIRECTION_UNSUPPORTED; mBoundingMetrics = nsBoundingMetrics(); // check if stretching is applicable ... if (gGlyphTableList && (1 == mData.Length())) { mDirection = nsMathMLOperators::GetStretchyDirection(mData); // default tentative table (not the one that is necessarily going // to be used) } } // ----------------------------------------------------------------------------- /* The Stretch: @param aContainerSize - suggested size for the stretched char @param aDesiredStretchSize - OUT parameter. The desired size after stretching. If no stretching is done, the output will simply give the base size. How it works? Summary:- The Stretch() method first looks for a glyph of appropriate size; If a glyph is found, it is cached by this object and its size is returned in aDesiredStretchSize. The cached glyph will then be used at the painting stage. If no glyph of appropriate size is found, a search is made to see if the char can be built by parts. Details:- A character gets stretched through the following pipeline : 1) If the base size of the char is sufficient to cover the container' size, we use that. If not, it will still be used as a fallback if the other stages in the pipeline fail. Issues : a) The base size, the parts and the variants of a char can be in different fonts. For eg., the base size for '(' should come from a normal ascii font if CMEX10 is used, since CMEX10 only contains the stretched versions. Hence, there are two ComputedStyles in use throughout the process. The leaf style context of the char holds fonts with which to try to stretch the char. The parent ComputedStyle of the char contains fonts for normal rendering. So the parent context is the one used to get the initial base size at the start of the pipeline. b) For operators that can be largeop's in display mode, we will skip the base size even if it fits, so that the next stage in the pipeline is given a chance to find a largeop variant. If the next stage fails, we fallback to the base size. 2) We search for the first larger variant of the char that fits the container' size. We first search for larger variants using the glyph table corresponding to the first existing font specified in the list of stretchy fonts held by the leaf ComputedStyle (from -moz-math-stretchy in mathml.css). Generic fonts are resolved by the preference "font.mathfont-family". Issues : a) the largeop and display settings determine the starting size when we do the above search, regardless of whether smaller variants already fit the container' size. b) if it is a largeopOnly request (i.e., a displaystyle operator with largeop=true and stretchy=false), we break after finding the first starting variant, regardless of whether that variant fits the container's size. 3) If a variant of appropriate size wasn't found, we see if the char can be built by parts using the same glyph table. Issue: There are chars that have no middle and glue glyphs. For such chars, the parts need to be joined using the rule. By convention (TeXbook p.225), the descent of the parts is zero while their ascent gives the thickness of the rule that should be used to join them. 4) If a match was not found in that glyph table, repeat from 2 to search the ordered list of stretchy fonts for the first font with a glyph table that provides a fit to the container size. If no fit is found, the closest fit is used. Of note: When the pipeline completes successfully, the desired size of the stretched char can actually be slightly larger or smaller than aContainerSize. But it is the responsibility of the caller to account for the spacing when setting aContainerSize, and to leave any extra margin when placing the stretched char. */ // ----------------------------------------------------------------------------- // plain TeX settings (TeXbook p.152) #define NS_MATHML_DELIMITER_FACTOR 0.901f #define NS_MATHML_DELIMITER_SHORTFALL_POINTS 5.0f static bool IsSizeOK(nscoord a, nscoord b, uint32_t aHint) { // Normal: True if 'a' is around +/-10% of the target 'b' (10% is // 1-DelimiterFactor). This often gives a chance to the base size to // win, especially in the context of sloppy markups without protective // bool isNormal = (aHint & NS_STRETCH_NORMAL) && Abs(a - b) < (1.0f - NS_MATHML_DELIMITER_FACTOR) * float(b); // Nearer: True if 'a' is around max{ +/-10% of 'b' , 'b' - 5pt }, // as documented in The TeXbook, Ch.17, p.152. // i.e. within 10% and within 5pt bool isNearer = false; if (aHint & (NS_STRETCH_NEARER | NS_STRETCH_LARGEOP)) { float c = std::max(float(b) * NS_MATHML_DELIMITER_FACTOR, float(b) - nsPresContext::CSSPointsToAppUnits( NS_MATHML_DELIMITER_SHORTFALL_POINTS)); isNearer = Abs(b - a) <= float(b) - c; } // Smaller: Mainly for transitory use, to compare two candidate // choices bool isSmaller = (aHint & NS_STRETCH_SMALLER) && float(a) >= NS_MATHML_DELIMITER_FACTOR * float(b) && a <= b; // Larger: Critical to the sqrt code to ensure that the radical // size is tall enough bool isLarger = (aHint & (NS_STRETCH_LARGER | NS_STRETCH_LARGEOP)) && a >= b; return (isNormal || isSmaller || isNearer || isLarger); } static bool IsSizeBetter(nscoord a, nscoord olda, nscoord b, uint32_t aHint) { if (0 == olda) return true; if (aHint & (NS_STRETCH_LARGER | NS_STRETCH_LARGEOP)) return (a >= olda) ? (olda < b) : (a >= b); if (aHint & NS_STRETCH_SMALLER) return (a <= olda) ? (olda > b) : (a <= b); // XXXkt prob want log scale here i.e. 1.5 is closer to 1 than 0.5 return Abs(a - b) < Abs(olda - b); } // We want to place the glyphs even when they don't fit at their // full extent, i.e., we may clip to tolerate a small amount of // overlap between the parts. This is important to cater for fonts // with long glues. static nscoord ComputeSizeFromParts(nsPresContext* aPresContext, nsGlyphCode* aGlyphs, nscoord* aSizes, nscoord aTargetSize) { enum { first, middle, last, glue }; // Add the parts that cannot be left out. nscoord sum = 0; for (int32_t i = first; i <= last; i++) { sum += aSizes[i]; } // Determine how much is used in joins nscoord oneDevPixel = aPresContext->AppUnitsPerDevPixel(); int32_t joins = aGlyphs[middle] == aGlyphs[glue] ? 1 : 2; // Pick a maximum size using a maximum number of glue glyphs that we are // prepared to draw for one character. const int32_t maxGlyphs = 1000; // This also takes into account the fact that, if the glue has no size, // then the character can't be lengthened. nscoord maxSize = sum - 2 * joins * oneDevPixel + maxGlyphs * aSizes[glue]; if (maxSize < aTargetSize) return maxSize; // settle with the maximum size // Get the minimum allowable size using some flex. nscoord minSize = NSToCoordRound(NS_MATHML_DELIMITER_FACTOR * sum); if (minSize > aTargetSize) return minSize; // settle with the minimum size // Fill-up the target area return aTargetSize; } // Update the font if there is a family change and returns the font group. bool nsMathMLChar::SetFontFamily(nsPresContext* aPresContext, const nsGlyphTable* aGlyphTable, const nsGlyphCode& aGlyphCode, const StyleFontFamilyList& aDefaultFamilyList, nsFont& aFont, RefPtr* aFontGroup) { StyleFontFamilyList glyphCodeFont; if (aGlyphCode.font) { glyphCodeFont = StyleFontFamilyList::WithOneUnquotedFamily( aGlyphTable->FontNameFor(aGlyphCode)); } const StyleFontFamilyList& familyList = aGlyphCode.font ? glyphCodeFont : aDefaultFamilyList; if (!*aFontGroup || aFont.family.families != familyList) { nsFont font = aFont; font.family.families = familyList; const nsStyleFont* styleFont = mComputedStyle->StyleFont(); nsFontMetrics::Params params; params.language = styleFont->mLanguage; params.explicitLanguage = styleFont->mExplicitLanguage; params.userFontSet = aPresContext->GetUserFontSet(); params.textPerf = aPresContext->GetTextPerfMetrics(); params.featureValueLookup = aPresContext->GetFontFeatureValuesLookup(); RefPtr fm = aPresContext->GetMetricsFor(font, params); // Set the font if it is an unicode table or if the same family name has // been found. const bool shouldSetFont = [&] { if (aGlyphTable == &gGlyphTableList->mUnicodeTable) { return true; } if (familyList.list.IsEmpty()) { return false; } const auto& firstFontInList = familyList.list.AsSpan()[0]; RefPtr firstFont = fm->GetThebesFontGroup()->GetFirstValidFont(); RefPtr firstFontName = NS_Atomize(firstFont->GetFontEntry()->FamilyName()); return firstFontInList.IsFamilyName() && firstFontInList.AsFamilyName().name.AsAtom() == firstFontName; }(); if (!shouldSetFont) { return false; } aFont.family.families = familyList; *aFontGroup = fm->GetThebesFontGroup(); } return true; } static nsBoundingMetrics MeasureTextRun(DrawTarget* aDrawTarget, gfxTextRun* aTextRun) { gfxTextRun::Metrics metrics = aTextRun->MeasureText(gfxFont::TIGHT_HINTED_OUTLINE_EXTENTS, aDrawTarget); nsBoundingMetrics bm; bm.leftBearing = NSToCoordFloor(metrics.mBoundingBox.X()); bm.rightBearing = NSToCoordCeil(metrics.mBoundingBox.XMost()); bm.ascent = NSToCoordCeil(-metrics.mBoundingBox.Y()); bm.descent = NSToCoordCeil(metrics.mBoundingBox.YMost()); bm.width = NSToCoordRound(metrics.mAdvanceWidth); return bm; } class nsMathMLChar::StretchEnumContext { public: StretchEnumContext(nsMathMLChar* aChar, nsPresContext* aPresContext, DrawTarget* aDrawTarget, float aFontSizeInflation, nsStretchDirection aStretchDirection, nscoord aTargetSize, uint32_t aStretchHint, nsBoundingMetrics& aStretchedMetrics, const StyleFontFamilyList& aFamilyList, bool& aGlyphFound) : mChar(aChar), mPresContext(aPresContext), mDrawTarget(aDrawTarget), mFontSizeInflation(aFontSizeInflation), mDirection(aStretchDirection), mTargetSize(aTargetSize), mStretchHint(aStretchHint), mBoundingMetrics(aStretchedMetrics), mFamilyList(aFamilyList), mTryVariants(true), mTryParts(true), mGlyphFound(aGlyphFound) {} static bool EnumCallback(const StyleSingleFontFamily& aFamily, void* aData); private: bool TryVariants(nsGlyphTable* aGlyphTable, RefPtr* aFontGroup, const StyleFontFamilyList& aFamilyList); bool TryParts(nsGlyphTable* aGlyphTable, RefPtr* aFontGroup, const StyleFontFamilyList& aFamilyList); nsMathMLChar* mChar; nsPresContext* mPresContext; DrawTarget* mDrawTarget; float mFontSizeInflation; const nsStretchDirection mDirection; const nscoord mTargetSize; const uint32_t mStretchHint; nsBoundingMetrics& mBoundingMetrics; // Font families to search const StyleFontFamilyList& mFamilyList; public: bool mTryVariants; bool mTryParts; private: AutoTArray mTablesTried; bool& mGlyphFound; }; // 2. See if there are any glyphs of the appropriate size. // Returns true if the size is OK, false to keep searching. // Always updates the char if a better match is found. bool nsMathMLChar::StretchEnumContext::TryVariants( nsGlyphTable* aGlyphTable, RefPtr* aFontGroup, const StyleFontFamilyList& aFamilyList) { // Use our stretchy ComputedStyle now that stretching is in progress ComputedStyle* sc = mChar->mComputedStyle; nsFont font = sc->StyleFont()->mFont; NormalizeDefaultFont(font, mFontSizeInflation); bool isVertical = (mDirection == NS_STRETCH_DIRECTION_VERTICAL); nscoord oneDevPixel = mPresContext->AppUnitsPerDevPixel(); char16_t uchar = mChar->mData[0]; bool largeop = (NS_STRETCH_LARGEOP & mStretchHint) != 0; bool largeopOnly = largeop && (NS_STRETCH_VARIABLE_MASK & mStretchHint) == 0; bool maxWidth = (NS_STRETCH_MAXWIDTH & mStretchHint) != 0; nscoord bestSize = isVertical ? mBoundingMetrics.ascent + mBoundingMetrics.descent : mBoundingMetrics.rightBearing - mBoundingMetrics.leftBearing; bool haveBetter = false; // start at size = 1 (size = 0 is the char at its normal size) int32_t size = 1; nsGlyphCode ch; nscoord displayOperatorMinHeight = 0; if (largeopOnly) { NS_ASSERTION(isVertical, "Stretching should be in the vertical direction"); ch = aGlyphTable->BigOf(mDrawTarget, oneDevPixel, *aFontGroup, uchar, isVertical, 0); if (ch.IsGlyphID()) { RefPtr mathFont = aFontGroup->get()->GetFirstMathFont(); // For OpenType MATH fonts, we will rely on the DisplayOperatorMinHeight // to select the right size variant. Note that the value is sometimes too // small so we use kLargeOpFactor/kIntegralFactor as a minimum value. if (mathFont) { displayOperatorMinHeight = mathFont->MathTable()->Constant( gfxMathTable::DisplayOperatorMinHeight, oneDevPixel); RefPtr textRun = aGlyphTable->MakeTextRun(mDrawTarget, oneDevPixel, *aFontGroup, ch); nsBoundingMetrics bm = MeasureTextRun(mDrawTarget, textRun.get()); float largeopFactor = kLargeOpFactor; if (nsMathMLOperators::IsIntegralOperator(mChar->mData)) { // integrals are drawn taller largeopFactor = kIntegralFactor; } nscoord minHeight = largeopFactor * (bm.ascent + bm.descent); if (displayOperatorMinHeight < minHeight) { displayOperatorMinHeight = minHeight; } } } } #ifdef NOISY_SEARCH printf(" searching in %s ...\n", NS_LossyConvertUTF16toASCII(aFamily).get()); #endif while ((ch = aGlyphTable->BigOf(mDrawTarget, oneDevPixel, *aFontGroup, uchar, isVertical, size)) .Exists()) { if (!mChar->SetFontFamily(mPresContext, aGlyphTable, ch, aFamilyList, font, aFontGroup)) { // if largeopOnly is set, break now if (largeopOnly) break; ++size; continue; } RefPtr textRun = aGlyphTable->MakeTextRun(mDrawTarget, oneDevPixel, *aFontGroup, ch); nsBoundingMetrics bm = MeasureTextRun(mDrawTarget, textRun.get()); if (ch.IsGlyphID()) { RefPtr mathFont = aFontGroup->get()->GetFirstMathFont(); if (mathFont) { // MeasureTextRun should have set the advance width to the right // bearing for OpenType MATH fonts. We now subtract the italic // correction, so that nsMathMLmmultiscripts will place the scripts // correctly. // Note that STIX-Word does not provide italic corrections but its // advance widths do not match right bearings. // (http://sourceforge.net/p/stixfonts/tracking/50/) gfxFloat italicCorrection = mathFont->MathTable()->ItalicsCorrection(ch.glyphID); if (italicCorrection) { bm.width -= NSToCoordRound(italicCorrection * oneDevPixel); if (bm.width < 0) { bm.width = 0; } } } } nscoord charSize = isVertical ? bm.ascent + bm.descent : bm.rightBearing - bm.leftBearing; if (largeopOnly || IsSizeBetter(charSize, bestSize, mTargetSize, mStretchHint)) { mGlyphFound = true; if (maxWidth) { // IsSizeBetter() checked that charSize < maxsize; // Leave ascent, descent, and bestsize as these contain maxsize. if (mBoundingMetrics.width < bm.width) mBoundingMetrics.width = bm.width; if (mBoundingMetrics.leftBearing > bm.leftBearing) mBoundingMetrics.leftBearing = bm.leftBearing; if (mBoundingMetrics.rightBearing < bm.rightBearing) mBoundingMetrics.rightBearing = bm.rightBearing; // Continue to check other sizes unless largeopOnly haveBetter = largeopOnly; } else { mBoundingMetrics = bm; haveBetter = true; bestSize = charSize; mChar->mGlyphs[0] = std::move(textRun); mChar->mDraw = DRAW_VARIANT; } #ifdef NOISY_SEARCH printf(" size:%d Current best\n", size); #endif } else { #ifdef NOISY_SEARCH printf(" size:%d Rejected!\n", size); #endif if (haveBetter) break; // Not making an futher progress, stop searching } // If this a largeop only operator, we stop if the glyph is large enough. if (largeopOnly && (bm.ascent + bm.descent) >= displayOperatorMinHeight) { break; } ++size; } return haveBetter && (largeopOnly || IsSizeOK(bestSize, mTargetSize, mStretchHint)); } // 3. Build by parts. // Returns true if the size is OK, false to keep searching. // Always updates the char if a better match is found. bool nsMathMLChar::StretchEnumContext::TryParts( nsGlyphTable* aGlyphTable, RefPtr* aFontGroup, const StyleFontFamilyList& aFamilyList) { // Use our stretchy ComputedStyle now that stretching is in progress nsFont font = mChar->mComputedStyle->StyleFont()->mFont; NormalizeDefaultFont(font, mFontSizeInflation); // Compute the bounding metrics of all partial glyphs RefPtr textRun[4]; nsGlyphCode chdata[4]; nsBoundingMetrics bmdata[4]; nscoord sizedata[4]; bool isVertical = (mDirection == NS_STRETCH_DIRECTION_VERTICAL); nscoord oneDevPixel = mPresContext->AppUnitsPerDevPixel(); char16_t uchar = mChar->mData[0]; bool maxWidth = (NS_STRETCH_MAXWIDTH & mStretchHint) != 0; if (!aGlyphTable->HasPartsOf(mDrawTarget, oneDevPixel, *aFontGroup, uchar, isVertical)) return false; // to next table for (int32_t i = 0; i < 4; i++) { nsGlyphCode ch = aGlyphTable->ElementAt(mDrawTarget, oneDevPixel, *aFontGroup, uchar, isVertical, i); chdata[i] = ch; if (ch.Exists()) { if (!mChar->SetFontFamily(mPresContext, aGlyphTable, ch, aFamilyList, font, aFontGroup)) return false; textRun[i] = aGlyphTable->MakeTextRun(mDrawTarget, oneDevPixel, *aFontGroup, ch); nsBoundingMetrics bm = MeasureTextRun(mDrawTarget, textRun[i].get()); bmdata[i] = bm; sizedata[i] = isVertical ? bm.ascent + bm.descent : bm.rightBearing - bm.leftBearing; } else { // Null glue indicates that a rule will be drawn, which can stretch to // fill any space. textRun[i] = nullptr; bmdata[i] = nsBoundingMetrics(); sizedata[i] = i == 3 ? mTargetSize : 0; } } // For the Unicode table, we check that all the glyphs are actually found and // come from the same font. if (aGlyphTable == &gGlyphTableList->mUnicodeTable) { gfxFont* unicodeFont = nullptr; for (int32_t i = 0; i < 4; i++) { if (!textRun[i]) { continue; } if (textRun[i]->GetLength() != 1 || textRun[i]->GetCharacterGlyphs()[0].IsMissing()) { return false; } uint32_t numGlyphRuns; const gfxTextRun::GlyphRun* glyphRuns = textRun[i]->GetGlyphRuns(&numGlyphRuns); if (numGlyphRuns != 1) { return false; } if (!unicodeFont) { unicodeFont = glyphRuns[0].mFont; } else if (unicodeFont != glyphRuns[0].mFont) { return false; } } } // Build by parts if we have successfully computed the // bounding metrics of all parts. nscoord computedSize = ComputeSizeFromParts(mPresContext, chdata, sizedata, mTargetSize); nscoord currentSize = isVertical ? mBoundingMetrics.ascent + mBoundingMetrics.descent : mBoundingMetrics.rightBearing - mBoundingMetrics.leftBearing; if (!IsSizeBetter(computedSize, currentSize, mTargetSize, mStretchHint)) { #ifdef NOISY_SEARCH printf(" Font %s Rejected!\n", NS_LossyConvertUTF16toASCII(fontName).get()); #endif return false; // to next table } #ifdef NOISY_SEARCH printf(" Font %s Current best!\n", NS_LossyConvertUTF16toASCII(fontName).get()); #endif // The computed size is the best we have found so far... // now is the time to compute and cache our bounding metrics if (isVertical) { int32_t i; // Try and find the first existing part and then determine the extremal // horizontal metrics of the parts. for (i = 0; i <= 3 && !textRun[i]; i++) ; if (i == 4) { NS_ERROR("Cannot stretch - All parts missing"); return false; } nscoord lbearing = bmdata[i].leftBearing; nscoord rbearing = bmdata[i].rightBearing; nscoord width = bmdata[i].width; i++; for (; i <= 3; i++) { if (!textRun[i]) continue; lbearing = std::min(lbearing, bmdata[i].leftBearing); rbearing = std::max(rbearing, bmdata[i].rightBearing); width = std::max(width, bmdata[i].width); } if (maxWidth) { lbearing = std::min(lbearing, mBoundingMetrics.leftBearing); rbearing = std::max(rbearing, mBoundingMetrics.rightBearing); width = std::max(width, mBoundingMetrics.width); } mBoundingMetrics.width = width; // When maxWidth, updating ascent and descent indicates that no characters // larger than this character's minimum size need to be checked as they // will not be used. mBoundingMetrics.ascent = bmdata[0].ascent; // not used except with descent // for height mBoundingMetrics.descent = computedSize - mBoundingMetrics.ascent; mBoundingMetrics.leftBearing = lbearing; mBoundingMetrics.rightBearing = rbearing; } else { int32_t i; // Try and find the first existing part and then determine the extremal // vertical metrics of the parts. for (i = 0; i <= 3 && !textRun[i]; i++) ; if (i == 4) { NS_ERROR("Cannot stretch - All parts missing"); return false; } nscoord ascent = bmdata[i].ascent; nscoord descent = bmdata[i].descent; i++; for (; i <= 3; i++) { if (!textRun[i]) continue; ascent = std::max(ascent, bmdata[i].ascent); descent = std::max(descent, bmdata[i].descent); } mBoundingMetrics.width = computedSize; mBoundingMetrics.ascent = ascent; mBoundingMetrics.descent = descent; mBoundingMetrics.leftBearing = 0; mBoundingMetrics.rightBearing = computedSize; } mGlyphFound = true; if (maxWidth) return false; // Continue to check other sizes // reset mChar->mDraw = DRAW_PARTS; for (int32_t i = 0; i < 4; i++) { mChar->mGlyphs[i] = std::move(textRun[i]); mChar->mBmData[i] = bmdata[i]; } return IsSizeOK(computedSize, mTargetSize, mStretchHint); } // Returns true iff stretching succeeded with the given family. // This is called for each family, whether it exists or not. bool nsMathMLChar::StretchEnumContext::EnumCallback( const StyleSingleFontFamily& aFamily, void* aData) { StretchEnumContext* context = static_cast(aData); // for comparisons, force use of unquoted names StyleFontFamilyList family; if (aFamily.IsFamilyName()) { family = StyleFontFamilyList::WithOneUnquotedFamily( nsAtomCString(aFamily.AsFamilyName().name.AsAtom())); } // Check font family if it is not a generic one // We test with the kNullGlyph ComputedStyle* sc = context->mChar->mComputedStyle; nsFont font = sc->StyleFont()->mFont; NormalizeDefaultFont(font, context->mFontSizeInflation); RefPtr fontGroup; if (!aFamily.IsGeneric() && !context->mChar->SetFontFamily(context->mPresContext, nullptr, kNullGlyph, family, font, &fontGroup)) { return false; // Could not set the family } // Determine the glyph table to use for this font. UniquePtr openTypeTable; nsGlyphTable* glyphTable; if (aFamily.IsGeneric()) { // This is a generic font, use the Unicode table. glyphTable = &gGlyphTableList->mUnicodeTable; } else { // If the font contains an Open Type MATH table, use it. RefPtr font = fontGroup->GetFirstValidFont(); openTypeTable = nsOpenTypeTable::Create(font); if (openTypeTable) { glyphTable = openTypeTable.get(); } else if (StaticPrefs::mathml_stixgeneral_operator_stretching_disabled()) { glyphTable = &gGlyphTableList->mUnicodeTable; } else { // Otherwise try to find a .properties file corresponding to that font // family or fallback to the Unicode table. glyphTable = gGlyphTableList->GetGlyphTableFor( nsAtomCString(aFamily.AsFamilyName().name.AsAtom())); } } if (!openTypeTable) { if (context->mTablesTried.Contains(glyphTable)) return false; // already tried this one // Only try this table once. context->mTablesTried.AppendElement(glyphTable); } // If the unicode table is being used, then search all font families. If a // special table is being used then the font in this family should have the // specified glyphs. const StyleFontFamilyList& familyList = glyphTable == &gGlyphTableList->mUnicodeTable ? context->mFamilyList : family; return (context->mTryVariants && context->TryVariants(glyphTable, &fontGroup, familyList)) || (context->mTryParts && context->TryParts(glyphTable, &fontGroup, familyList)); } static void AppendFallbacks(nsTArray& aNames, const nsTArray& aFallbacks) { for (const nsCString& fallback : aFallbacks) { aNames.AppendElement(StyleSingleFontFamily::FamilyName( StyleFamilyName{StyleAtom(NS_Atomize(fallback)), StyleFontFamilyNameSyntax::Identifiers})); } } // insert math fallback families just before the first generic or at the end // when no generic present static void InsertMathFallbacks(StyleFontFamilyList& aFamilyList, nsTArray& aFallbacks) { nsTArray mergedList; bool inserted = false; for (const auto& name : aFamilyList.list.AsSpan()) { if (!inserted && name.IsGeneric()) { inserted = true; AppendFallbacks(mergedList, aFallbacks); } mergedList.AppendElement(name); } if (!inserted) { AppendFallbacks(mergedList, aFallbacks); } aFamilyList = StyleFontFamilyList::WithNames(std::move(mergedList)); } nsresult nsMathMLChar::StretchInternal( nsIFrame* aForFrame, DrawTarget* aDrawTarget, float aFontSizeInflation, nsStretchDirection& aStretchDirection, const nsBoundingMetrics& aContainerSize, nsBoundingMetrics& aDesiredStretchSize, uint32_t aStretchHint, // These are currently only used when // aStretchHint & NS_STRETCH_MAXWIDTH: float aMaxSize, bool aMaxSizeIsAbsolute) { nsPresContext* presContext = aForFrame->PresContext(); // if we have been called before, and we didn't actually stretch, our // direction may have been set to NS_STRETCH_DIRECTION_UNSUPPORTED. // So first set our direction back to its instrinsic value nsStretchDirection direction = nsMathMLOperators::GetStretchyDirection(mData); // Set default font and get the default bounding metrics // mComputedStyle is a leaf context used only when stretching happens. // For the base size, the default font should come from the parent context nsFont font = aForFrame->StyleFont()->mFont; NormalizeDefaultFont(font, aFontSizeInflation); const nsStyleFont* styleFont = mComputedStyle->StyleFont(); nsFontMetrics::Params params; params.language = styleFont->mLanguage; params.explicitLanguage = styleFont->mExplicitLanguage; params.userFontSet = presContext->GetUserFontSet(); params.textPerf = presContext->GetTextPerfMetrics(); RefPtr fm = presContext->GetMetricsFor(font, params); uint32_t len = uint32_t(mData.Length()); mGlyphs[0] = fm->GetThebesFontGroup()->MakeTextRun( static_cast(mData.get()), len, aDrawTarget, presContext->AppUnitsPerDevPixel(), gfx::ShapedTextFlags(), nsTextFrameUtils::Flags(), presContext->MissingFontRecorder()); aDesiredStretchSize = MeasureTextRun(aDrawTarget, mGlyphs[0].get()); bool maxWidth = (NS_STRETCH_MAXWIDTH & aStretchHint) != 0; if (!maxWidth) { mUnscaledAscent = aDesiredStretchSize.ascent; } ////////////////////////////////////////////////////////////////////////////// // 1. Check the common situations where stretching is not actually needed ////////////////////////////////////////////////////////////////////////////// // quick return if there is nothing special about this char if ((aStretchDirection != direction && aStretchDirection != NS_STRETCH_DIRECTION_DEFAULT) || (aStretchHint & ~NS_STRETCH_MAXWIDTH) == NS_STRETCH_NONE) { mDirection = NS_STRETCH_DIRECTION_UNSUPPORTED; return NS_OK; } // if no specified direction, attempt to stretch in our preferred direction if (aStretchDirection == NS_STRETCH_DIRECTION_DEFAULT) { aStretchDirection = direction; } // see if this is a particular largeop or largeopOnly request bool largeop = (NS_STRETCH_LARGEOP & aStretchHint) != 0; bool stretchy = (NS_STRETCH_VARIABLE_MASK & aStretchHint) != 0; bool largeopOnly = largeop && !stretchy; bool isVertical = (direction == NS_STRETCH_DIRECTION_VERTICAL); nscoord targetSize = isVertical ? aContainerSize.ascent + aContainerSize.descent : aContainerSize.rightBearing - aContainerSize.leftBearing; if (maxWidth) { // See if it is only necessary to consider glyphs up to some maximum size. // Set the current height to the maximum size, and set aStretchHint to // NS_STRETCH_SMALLER if the size is variable, so that only smaller sizes // are considered. targetSize from GetMaxWidth() is 0. if (stretchy) { // variable size stretch - consider all sizes < maxsize aStretchHint = (aStretchHint & ~NS_STRETCH_VARIABLE_MASK) | NS_STRETCH_SMALLER; } // Use NS_MATHML_DELIMITER_FACTOR to allow some slightly larger glyphs as // maxsize is not enforced exactly. if (aMaxSize == NS_MATHML_OPERATOR_SIZE_INFINITY) { aDesiredStretchSize.ascent = nscoord_MAX; aDesiredStretchSize.descent = 0; } else { nscoord height = aDesiredStretchSize.ascent + aDesiredStretchSize.descent; if (height == 0) { if (aMaxSizeIsAbsolute) { aDesiredStretchSize.ascent = NSToCoordRound(aMaxSize / NS_MATHML_DELIMITER_FACTOR); aDesiredStretchSize.descent = 0; } // else: leave height as 0 } else { float scale = aMaxSizeIsAbsolute ? aMaxSize / height : aMaxSize; scale /= NS_MATHML_DELIMITER_FACTOR; aDesiredStretchSize.ascent = NSToCoordRound(scale * aDesiredStretchSize.ascent); aDesiredStretchSize.descent = NSToCoordRound(scale * aDesiredStretchSize.descent); } } } nsBoundingMetrics initialSize = aDesiredStretchSize; nscoord charSize = isVertical ? initialSize.ascent + initialSize.descent : initialSize.rightBearing - initialSize.leftBearing; bool done = false; if (!maxWidth && !largeop) { // Doing Stretch() not GetMaxWidth(), // and not a largeop in display mode; we're done if size fits if ((targetSize <= 0) || ((isVertical && charSize >= targetSize) || IsSizeOK(charSize, targetSize, aStretchHint))) done = true; } ////////////////////////////////////////////////////////////////////////////// // 2/3. Search for a glyph or set of part glyphs of appropriate size ////////////////////////////////////////////////////////////////////////////// bool glyphFound = false; if (!done) { // normal case // Use the css font-family but add preferred fallback fonts. font = mComputedStyle->StyleFont()->mFont; NormalizeDefaultFont(font, aFontSizeInflation); // really shouldn't be doing things this way but for now // insert fallbacks into the list AutoTArray mathFallbacks; nsAutoCString value; gfxPlatformFontList* pfl = gfxPlatformFontList::PlatformFontList(); pfl->Lock(); if (pfl->GetFontPrefs()->LookupName("serif.x-math"_ns, value)) { gfxFontUtils::ParseFontList(value, mathFallbacks); } if (pfl->GetFontPrefs()->LookupNameList("serif.x-math"_ns, value)) { gfxFontUtils::ParseFontList(value, mathFallbacks); } pfl->Unlock(); InsertMathFallbacks(font.family.families, mathFallbacks); #ifdef NOISY_SEARCH nsAutoString fontlistStr; font.fontlist.ToString(fontlistStr, false, true); printf( "Searching in " % s " for a glyph of appropriate size for: 0x%04X:%c\n", NS_ConvertUTF16toUTF8(fontlistStr).get(), mData[0], mData[0] & 0x00FF); #endif StretchEnumContext enumData(this, presContext, aDrawTarget, aFontSizeInflation, aStretchDirection, targetSize, aStretchHint, aDesiredStretchSize, font.family.families, glyphFound); enumData.mTryParts = !largeopOnly; for (const StyleSingleFontFamily& name : font.family.families.list.AsSpan()) { if (StretchEnumContext::EnumCallback(name, &enumData)) { if (name.IsNamedFamily(u"STIXGeneral"_ns)) { AutoTArray params{ u"https://developer.mozilla.org/docs/Mozilla/" "MathML_Project/Fonts"_ns}; aForFrame->PresContext()->Document()->WarnOnceAbout( dom::DeprecatedOperations:: eMathML_DeprecatedStixgeneralOperatorStretching, false, params); } break; } } } if (!maxWidth) { // Now, we know how we are going to draw the char. Update the member // variables accordingly. mUnscaledAscent = aDesiredStretchSize.ascent; } if (glyphFound) { return NS_OK; } // We did not find a size variant or a glyph assembly to stretch this // operator. Verify whether a font with an OpenType MATH table is available // and record missing math script otherwise. gfxMissingFontRecorder* MFR = presContext->MissingFontRecorder(); RefPtr firstMathFont = fm->GetThebesFontGroup()->GetFirstMathFont(); if (MFR && !firstMathFont) { MFR->RecordScript(intl::Script::MATHEMATICAL_NOTATION); } // If the scale_stretchy_operators option is disabled, we are done. if (!Preferences::GetBool("mathml.scale_stretchy_operators.enabled", true)) { return NS_OK; } // stretchy character if (stretchy) { if (isVertical) { float scale = std::min( kMaxScaleFactor, float(aContainerSize.ascent + aContainerSize.descent) / (aDesiredStretchSize.ascent + aDesiredStretchSize.descent)); if (!largeop || scale > 1.0) { // make the character match the desired height. if (!maxWidth) { mScaleY *= scale; } aDesiredStretchSize.ascent *= scale; aDesiredStretchSize.descent *= scale; } } else { float scale = std::min( kMaxScaleFactor, float(aContainerSize.rightBearing - aContainerSize.leftBearing) / (aDesiredStretchSize.rightBearing - aDesiredStretchSize.leftBearing)); if (!largeop || scale > 1.0) { // make the character match the desired width. if (!maxWidth) { mScaleX *= scale; } aDesiredStretchSize.leftBearing *= scale; aDesiredStretchSize.rightBearing *= scale; aDesiredStretchSize.width *= scale; } } } // We do not have a char variant for this largeop in display mode, so we // apply a scale transform to the base char. if (largeop) { float scale; float largeopFactor = kLargeOpFactor; // increase the width if it is not largeopFactor times larger // than the initial one. if ((aDesiredStretchSize.rightBearing - aDesiredStretchSize.leftBearing) < largeopFactor * (initialSize.rightBearing - initialSize.leftBearing)) { scale = (largeopFactor * (initialSize.rightBearing - initialSize.leftBearing)) / (aDesiredStretchSize.rightBearing - aDesiredStretchSize.leftBearing); if (!maxWidth) { mScaleX *= scale; } aDesiredStretchSize.leftBearing *= scale; aDesiredStretchSize.rightBearing *= scale; aDesiredStretchSize.width *= scale; } // increase the height if it is not largeopFactor times larger // than the initial one. if (nsMathMLOperators::IsIntegralOperator(mData)) { // integrals are drawn taller largeopFactor = kIntegralFactor; } if ((aDesiredStretchSize.ascent + aDesiredStretchSize.descent) < largeopFactor * (initialSize.ascent + initialSize.descent)) { scale = (largeopFactor * (initialSize.ascent + initialSize.descent)) / (aDesiredStretchSize.ascent + aDesiredStretchSize.descent); if (!maxWidth) { mScaleY *= scale; } aDesiredStretchSize.ascent *= scale; aDesiredStretchSize.descent *= scale; } } return NS_OK; } nsresult nsMathMLChar::Stretch(nsIFrame* aForFrame, DrawTarget* aDrawTarget, float aFontSizeInflation, nsStretchDirection aStretchDirection, const nsBoundingMetrics& aContainerSize, nsBoundingMetrics& aDesiredStretchSize, uint32_t aStretchHint, bool aRTL) { NS_ASSERTION( !(aStretchHint & ~(NS_STRETCH_VARIABLE_MASK | NS_STRETCH_LARGEOP)), "Unexpected stretch flags"); mDraw = DRAW_NORMAL; mMirrored = aRTL && nsMathMLOperators::IsMirrorableOperator(mData); mScaleY = mScaleX = 1.0; mDirection = aStretchDirection; nsresult rv = StretchInternal(aForFrame, aDrawTarget, aFontSizeInflation, mDirection, aContainerSize, aDesiredStretchSize, aStretchHint); // Record the metrics mBoundingMetrics = aDesiredStretchSize; return rv; } // What happens here is that the StretchInternal algorithm is used but // modified by passing the NS_STRETCH_MAXWIDTH stretch hint. That causes // StretchInternal to return horizontal bounding metrics that are the maximum // that might be returned from a Stretch. // // In order to avoid considering widths of some characters in fonts that will // not be used for any stretch size, StretchInternal sets the initial height // to infinity and looks for any characters smaller than this height. When a // character built from parts is considered, (it will be used by Stretch for // any characters greater than its minimum size, so) the height is set to its // minimum size, so that only widths of smaller subsequent characters are // considered. nscoord nsMathMLChar::GetMaxWidth(nsIFrame* aForFrame, DrawTarget* aDrawTarget, float aFontSizeInflation, uint32_t aStretchHint) { nsBoundingMetrics bm; nsStretchDirection direction = NS_STRETCH_DIRECTION_VERTICAL; const nsBoundingMetrics container; // zero target size StretchInternal(aForFrame, aDrawTarget, aFontSizeInflation, direction, container, bm, aStretchHint | NS_STRETCH_MAXWIDTH); return std::max(bm.width, bm.rightBearing) - std::min(0, bm.leftBearing); } namespace mozilla { class nsDisplayMathMLSelectionRect final : public nsPaintedDisplayItem { public: nsDisplayMathMLSelectionRect(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, const nsRect& aRect) : nsPaintedDisplayItem(aBuilder, aFrame), mRect(aRect) { MOZ_COUNT_CTOR(nsDisplayMathMLSelectionRect); } MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayMathMLSelectionRect) virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; NS_DISPLAY_DECL_NAME("MathMLSelectionRect", TYPE_MATHML_SELECTION_RECT) private: nsRect mRect; }; void nsDisplayMathMLSelectionRect::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) { DrawTarget* drawTarget = aCtx->GetDrawTarget(); Rect rect = NSRectToSnappedRect(mRect + ToReferenceFrame(), mFrame->PresContext()->AppUnitsPerDevPixel(), *drawTarget); // get color to use for selection from the look&feel object nscolor bgColor = LookAndFeel::Color(LookAndFeel::ColorID::Highlight, mFrame); drawTarget->FillRect(rect, ColorPattern(ToDeviceColor(bgColor))); } class nsDisplayMathMLCharForeground final : public nsPaintedDisplayItem { public: nsDisplayMathMLCharForeground(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, nsMathMLChar* aChar, const bool aIsSelected) : nsPaintedDisplayItem(aBuilder, aFrame), mChar(aChar), mIsSelected(aIsSelected) { MOZ_COUNT_CTOR(nsDisplayMathMLCharForeground); } MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayMathMLCharForeground) virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) const override { *aSnap = false; nsRect rect; mChar->GetRect(rect); nsPoint offset = ToReferenceFrame() + rect.TopLeft(); nsBoundingMetrics bm; mChar->GetBoundingMetrics(bm); nsRect temp(offset.x + bm.leftBearing, offset.y, bm.rightBearing - bm.leftBearing, bm.ascent + bm.descent); // Bug 748220 temp.Inflate(mFrame->PresContext()->AppUnitsPerDevPixel()); return temp; } virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override { mChar->PaintForeground(mFrame, *aCtx, ToReferenceFrame(), mIsSelected); } NS_DISPLAY_DECL_NAME("MathMLCharForeground", TYPE_MATHML_CHAR_FOREGROUND) virtual nsRect GetComponentAlphaBounds( nsDisplayListBuilder* aBuilder) const override { bool snap; return GetBounds(aBuilder, &snap); } private: nsMathMLChar* mChar; bool mIsSelected; }; #ifdef DEBUG class nsDisplayMathMLCharDebug final : public nsPaintedDisplayItem { public: nsDisplayMathMLCharDebug(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, const nsRect& aRect) : nsPaintedDisplayItem(aBuilder, aFrame), mRect(aRect) { MOZ_COUNT_CTOR(nsDisplayMathMLCharDebug); } MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayMathMLCharDebug) virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; NS_DISPLAY_DECL_NAME("MathMLCharDebug", TYPE_MATHML_CHAR_DEBUG) private: nsRect mRect; }; void nsDisplayMathMLCharDebug::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) { // for visual debug Sides skipSides; nsPresContext* presContext = mFrame->PresContext(); ComputedStyle* computedStyle = mFrame->Style(); nsRect rect = mRect + ToReferenceFrame(); PaintBorderFlags flags = aBuilder->ShouldSyncDecodeImages() ? PaintBorderFlags::SyncDecodeImages : PaintBorderFlags(); // Since this is used only for debugging, we don't need to worry about // tracking the ImgDrawResult. Unused << nsCSSRendering::PaintBorder(presContext, *aCtx, mFrame, GetPaintRect(aBuilder, aCtx), rect, computedStyle, flags, skipSides); nsCSSRendering::PaintNonThemedOutline(presContext, *aCtx, mFrame, GetPaintRect(aBuilder, aCtx), rect, computedStyle); } #endif } // namespace mozilla void nsMathMLChar::Display(nsDisplayListBuilder* aBuilder, nsIFrame* aForFrame, const nsDisplayListSet& aLists, uint32_t aIndex, const nsRect* aSelectedRect) { ComputedStyle* computedStyle = mComputedStyle; if (!computedStyle->StyleVisibility()->IsVisible()) { return; } const bool isSelected = aSelectedRect && !aSelectedRect->IsEmpty(); if (isSelected) { aLists.BorderBackground()->AppendNewToTop( aBuilder, aForFrame, *aSelectedRect); } else if (mRect.width && mRect.height) { #if defined(DEBUG) && defined(SHOW_BOUNDING_BOX) // for visual debug aLists.BorderBackground()->AppendNewToTop( aBuilder, aForFrame, mRect); #endif } aLists.Content()->AppendNewToTopWithIndex( aBuilder, aForFrame, aIndex, this, isSelected); } void nsMathMLChar::ApplyTransforms(gfxContext* aThebesContext, int32_t aAppUnitsPerGfxUnit, nsRect& r) { // apply the transforms if (mMirrored) { nsPoint pt = r.TopRight(); gfxPoint devPixelOffset(NSAppUnitsToFloatPixels(pt.x, aAppUnitsPerGfxUnit), NSAppUnitsToFloatPixels(pt.y, aAppUnitsPerGfxUnit)); aThebesContext->SetMatrixDouble(aThebesContext->CurrentMatrixDouble() .PreTranslate(devPixelOffset) .PreScale(-mScaleX, mScaleY)); } else { nsPoint pt = r.TopLeft(); gfxPoint devPixelOffset(NSAppUnitsToFloatPixels(pt.x, aAppUnitsPerGfxUnit), NSAppUnitsToFloatPixels(pt.y, aAppUnitsPerGfxUnit)); aThebesContext->SetMatrixDouble(aThebesContext->CurrentMatrixDouble() .PreTranslate(devPixelOffset) .PreScale(mScaleX, mScaleY)); } // update the bounding rectangle. r.x = r.y = 0; r.width /= mScaleX; r.height /= mScaleY; } void nsMathMLChar::PaintForeground(nsIFrame* aForFrame, gfxContext& aRenderingContext, nsPoint aPt, bool aIsSelected) { ComputedStyle* computedStyle = mComputedStyle; nsPresContext* presContext = aForFrame->PresContext(); if (mDraw == DRAW_NORMAL) { // normal drawing if there is nothing special about this char // Use our parent element's style computedStyle = aForFrame->Style(); } // Set color ... nscolor fgColor = computedStyle->GetVisitedDependentColor( &nsStyleText::mWebkitTextFillColor); if (aIsSelected) { // get color to use for selection from the look&feel object fgColor = LookAndFeel::Color(LookAndFeel::ColorID::Highlighttext, aForFrame, fgColor); } aRenderingContext.SetColor(sRGBColor::FromABGR(fgColor)); aRenderingContext.Save(); nsRect r = mRect + aPt; ApplyTransforms(&aRenderingContext, aForFrame->PresContext()->AppUnitsPerDevPixel(), r); switch (mDraw) { case DRAW_NORMAL: case DRAW_VARIANT: // draw a single glyph (base size or size variant) // XXXfredw verify if mGlyphs[0] is non-null to workaround bug 973322. if (mGlyphs[0]) { mGlyphs[0]->Draw(Range(mGlyphs[0].get()), gfx::Point(0.0, mUnscaledAscent), gfxTextRun::DrawParams(&aRenderingContext)); } break; case DRAW_PARTS: { // paint by parts if (NS_STRETCH_DIRECTION_VERTICAL == mDirection) PaintVertically(presContext, &aRenderingContext, r, fgColor); else if (NS_STRETCH_DIRECTION_HORIZONTAL == mDirection) PaintHorizontally(presContext, &aRenderingContext, r, fgColor); break; } default: MOZ_ASSERT_UNREACHABLE("Unknown drawing method"); break; } aRenderingContext.Restore(); } /* ============================================================================= Helper routines that actually do the job of painting the char by parts */ class AutoPushClipRect { gfxContext* mThebesContext; public: AutoPushClipRect(gfxContext* aThebesContext, int32_t aAppUnitsPerGfxUnit, const nsRect& aRect) : mThebesContext(aThebesContext) { mThebesContext->Save(); gfxRect clip = nsLayoutUtils::RectToGfxRect(aRect, aAppUnitsPerGfxUnit); mThebesContext->SnappedClip(clip); } ~AutoPushClipRect() { mThebesContext->Restore(); } }; static nsPoint SnapToDevPixels(const gfxContext* aThebesContext, int32_t aAppUnitsPerGfxUnit, const nsPoint& aPt) { gfxPoint pt(NSAppUnitsToFloatPixels(aPt.x, aAppUnitsPerGfxUnit), NSAppUnitsToFloatPixels(aPt.y, aAppUnitsPerGfxUnit)); pt = aThebesContext->UserToDevice(pt); pt.Round(); pt = aThebesContext->DeviceToUser(pt); return nsPoint(NSFloatPixelsToAppUnits(pt.x, aAppUnitsPerGfxUnit), NSFloatPixelsToAppUnits(pt.y, aAppUnitsPerGfxUnit)); } static void PaintRule(DrawTarget& aDrawTarget, int32_t aAppUnitsPerGfxUnit, nsRect& aRect, nscolor aColor) { Rect rect = NSRectToSnappedRect(aRect, aAppUnitsPerGfxUnit, aDrawTarget); ColorPattern color(ToDeviceColor(aColor)); aDrawTarget.FillRect(rect, color); } // paint a stretchy char by assembling glyphs vertically nsresult nsMathMLChar::PaintVertically(nsPresContext* aPresContext, gfxContext* aThebesContext, nsRect& aRect, nscolor aColor) { DrawTarget& aDrawTarget = *aThebesContext->GetDrawTarget(); // Get the device pixel size in the vertical direction. // (This makes no effort to optimize for non-translation transformations.) nscoord oneDevPixel = aPresContext->AppUnitsPerDevPixel(); // get metrics data to be re-used later int32_t i = 0; nscoord dx = aRect.x; nscoord offset[3], start[3], end[3]; for (i = 0; i <= 2; ++i) { const nsBoundingMetrics& bm = mBmData[i]; nscoord dy; if (0 == i) { // top dy = aRect.y + bm.ascent; } else if (2 == i) { // bottom dy = aRect.y + aRect.height - bm.descent; } else { // middle dy = aRect.y + bm.ascent + (aRect.height - (bm.ascent + bm.descent)) / 2; } // _cairo_scaled_font_show_glyphs snaps origins to device pixels. // Do this now so that we can get the other dimensions right. // (This may not achieve much with non-rectangular transformations.) dy = SnapToDevPixels(aThebesContext, oneDevPixel, nsPoint(dx, dy)).y; // abcissa passed to Draw offset[i] = dy; // _cairo_scaled_font_glyph_device_extents rounds outwards to the nearest // pixel, so the bm values can include 1 row of faint pixels on each edge. // Don't rely on this pixel as it can look like a gap. if (bm.ascent + bm.descent >= 2 * oneDevPixel) { start[i] = dy - bm.ascent + oneDevPixel; // top join end[i] = dy + bm.descent - oneDevPixel; // bottom join } else { // To avoid overlaps, we don't add one pixel on each side when the part // is too small. start[i] = dy - bm.ascent; // top join end[i] = dy + bm.descent; // bottom join } } // If there are overlaps, then join at the mid point for (i = 0; i < 2; ++i) { if (end[i] > start[i + 1]) { end[i] = (end[i] + start[i + 1]) / 2; start[i + 1] = end[i]; } } nsRect unionRect = aRect; unionRect.x += mBoundingMetrics.leftBearing; unionRect.width = mBoundingMetrics.rightBearing - mBoundingMetrics.leftBearing; unionRect.Inflate(oneDevPixel); gfxTextRun::DrawParams params(aThebesContext); ///////////////////////////////////// // draw top, middle, bottom for (i = 0; i <= 2; ++i) { // glue can be null if (mGlyphs[i]) { nscoord dy = offset[i]; // Draw a glyph in a clipped area so that we don't have hairy chars // pending outside nsRect clipRect = unionRect; // Clip at the join to get a solid edge (without overlap or gap), when // this won't change the glyph too much. If the glyph is too small to // clip then we'll overlap rather than have a gap. nscoord height = mBmData[i].ascent + mBmData[i].descent; if (height * (1.0 - NS_MATHML_DELIMITER_FACTOR) > oneDevPixel) { if (0 == i) { // top clipRect.height = end[i] - clipRect.y; } else if (2 == i) { // bottom clipRect.height -= start[i] - clipRect.y; clipRect.y = start[i]; } else { // middle clipRect.y = start[i]; clipRect.height = end[i] - start[i]; } } if (!clipRect.IsEmpty()) { AutoPushClipRect clip(aThebesContext, oneDevPixel, clipRect); mGlyphs[i]->Draw(Range(mGlyphs[i].get()), gfx::Point(dx, dy), params); } } } /////////////// // fill the gap between top and middle, and between middle and bottom. if (!mGlyphs[3]) { // null glue : draw a rule // figure out the dimensions of the rule to be drawn : // set lbearing to rightmost lbearing among the two current successive // parts. // set rbearing to leftmost rbearing among the two current successive parts. // this not only satisfies the convention used for over/underbraces // in TeX, but also takes care of broken fonts like the stretchy integral // in Symbol for small font sizes in unix. nscoord lbearing, rbearing; int32_t first = 0, last = 1; while (last <= 2) { if (mGlyphs[last]) { lbearing = mBmData[last].leftBearing; rbearing = mBmData[last].rightBearing; if (mGlyphs[first]) { if (lbearing < mBmData[first].leftBearing) lbearing = mBmData[first].leftBearing; if (rbearing > mBmData[first].rightBearing) rbearing = mBmData[first].rightBearing; } } else if (mGlyphs[first]) { lbearing = mBmData[first].leftBearing; rbearing = mBmData[first].rightBearing; } else { NS_ERROR("Cannot stretch - All parts missing"); return NS_ERROR_UNEXPECTED; } // paint the rule between the parts nsRect rule(aRect.x + lbearing, end[first], rbearing - lbearing, start[last] - end[first]); PaintRule(aDrawTarget, oneDevPixel, rule, aColor); first = last; last++; } } else if (mBmData[3].ascent + mBmData[3].descent > 0) { // glue is present nsBoundingMetrics& bm = mBmData[3]; // Ensure the stride for the glue is not reduced to less than one pixel if (bm.ascent + bm.descent >= 3 * oneDevPixel) { // To protect against gaps, pretend the glue is smaller than it is, // in order to trim off ends and thus get a solid edge for the join. bm.ascent -= oneDevPixel; bm.descent -= oneDevPixel; } nsRect clipRect = unionRect; for (i = 0; i < 2; ++i) { // Make sure not to draw outside the character nscoord dy = std::max(end[i], aRect.y); nscoord fillEnd = std::min(start[i + 1], aRect.YMost()); while (dy < fillEnd) { clipRect.y = dy; clipRect.height = std::min(bm.ascent + bm.descent, fillEnd - dy); AutoPushClipRect clip(aThebesContext, oneDevPixel, clipRect); dy += bm.ascent; mGlyphs[3]->Draw(Range(mGlyphs[3].get()), gfx::Point(dx, dy), params); dy += bm.descent; } } } #ifdef DEBUG else { for (i = 0; i < 2; ++i) { NS_ASSERTION(end[i] >= start[i + 1], "gap between parts with missing glue glyph"); } } #endif return NS_OK; } // paint a stretchy char by assembling glyphs horizontally nsresult nsMathMLChar::PaintHorizontally(nsPresContext* aPresContext, gfxContext* aThebesContext, nsRect& aRect, nscolor aColor) { DrawTarget& aDrawTarget = *aThebesContext->GetDrawTarget(); // Get the device pixel size in the horizontal direction. // (This makes no effort to optimize for non-translation transformations.) nscoord oneDevPixel = aPresContext->AppUnitsPerDevPixel(); // get metrics data to be re-used later int32_t i = 0; nscoord dy = aRect.y + mBoundingMetrics.ascent; nscoord offset[3], start[3], end[3]; for (i = 0; i <= 2; ++i) { const nsBoundingMetrics& bm = mBmData[i]; nscoord dx; if (0 == i) { // left dx = aRect.x - bm.leftBearing; } else if (2 == i) { // right dx = aRect.x + aRect.width - bm.rightBearing; } else { // middle dx = aRect.x + (aRect.width - bm.width) / 2; } // _cairo_scaled_font_show_glyphs snaps origins to device pixels. // Do this now so that we can get the other dimensions right. // (This may not achieve much with non-rectangular transformations.) dx = SnapToDevPixels(aThebesContext, oneDevPixel, nsPoint(dx, dy)).x; // abcissa passed to Draw offset[i] = dx; // _cairo_scaled_font_glyph_device_extents rounds outwards to the nearest // pixel, so the bm values can include 1 row of faint pixels on each edge. // Don't rely on this pixel as it can look like a gap. if (bm.rightBearing - bm.leftBearing >= 2 * oneDevPixel) { start[i] = dx + bm.leftBearing + oneDevPixel; // left join end[i] = dx + bm.rightBearing - oneDevPixel; // right join } else { // To avoid overlaps, we don't add one pixel on each side when the part // is too small. start[i] = dx + bm.leftBearing; // left join end[i] = dx + bm.rightBearing; // right join } } // If there are overlaps, then join at the mid point for (i = 0; i < 2; ++i) { if (end[i] > start[i + 1]) { end[i] = (end[i] + start[i + 1]) / 2; start[i + 1] = end[i]; } } nsRect unionRect = aRect; unionRect.Inflate(oneDevPixel); gfxTextRun::DrawParams params(aThebesContext); /////////////////////////// // draw left, middle, right for (i = 0; i <= 2; ++i) { // glue can be null if (mGlyphs[i]) { nscoord dx = offset[i]; nsRect clipRect = unionRect; // Clip at the join to get a solid edge (without overlap or gap), when // this won't change the glyph too much. If the glyph is too small to // clip then we'll overlap rather than have a gap. nscoord width = mBmData[i].rightBearing - mBmData[i].leftBearing; if (width * (1.0 - NS_MATHML_DELIMITER_FACTOR) > oneDevPixel) { if (0 == i) { // left clipRect.width = end[i] - clipRect.x; } else if (2 == i) { // right clipRect.width -= start[i] - clipRect.x; clipRect.x = start[i]; } else { // middle clipRect.x = start[i]; clipRect.width = end[i] - start[i]; } } if (!clipRect.IsEmpty()) { AutoPushClipRect clip(aThebesContext, oneDevPixel, clipRect); mGlyphs[i]->Draw(Range(mGlyphs[i].get()), gfx::Point(dx, dy), params); } } } //////////////// // fill the gap between left and middle, and between middle and right. if (!mGlyphs[3]) { // null glue : draw a rule // figure out the dimensions of the rule to be drawn : // set ascent to lowest ascent among the two current successive parts. // set descent to highest descent among the two current successive parts. // this satisfies the convention used for over/underbraces, and helps // fix broken fonts. nscoord ascent, descent; int32_t first = 0, last = 1; while (last <= 2) { if (mGlyphs[last]) { ascent = mBmData[last].ascent; descent = mBmData[last].descent; if (mGlyphs[first]) { if (ascent > mBmData[first].ascent) ascent = mBmData[first].ascent; if (descent > mBmData[first].descent) descent = mBmData[first].descent; } } else if (mGlyphs[first]) { ascent = mBmData[first].ascent; descent = mBmData[first].descent; } else { NS_ERROR("Cannot stretch - All parts missing"); return NS_ERROR_UNEXPECTED; } // paint the rule between the parts nsRect rule(end[first], dy - ascent, start[last] - end[first], ascent + descent); PaintRule(aDrawTarget, oneDevPixel, rule, aColor); first = last; last++; } } else if (mBmData[3].rightBearing - mBmData[3].leftBearing > 0) { // glue is present nsBoundingMetrics& bm = mBmData[3]; // Ensure the stride for the glue is not reduced to less than one pixel if (bm.rightBearing - bm.leftBearing >= 3 * oneDevPixel) { // To protect against gaps, pretend the glue is smaller than it is, // in order to trim off ends and thus get a solid edge for the join. bm.leftBearing += oneDevPixel; bm.rightBearing -= oneDevPixel; } nsRect clipRect = unionRect; for (i = 0; i < 2; ++i) { // Make sure not to draw outside the character nscoord dx = std::max(end[i], aRect.x); nscoord fillEnd = std::min(start[i + 1], aRect.XMost()); while (dx < fillEnd) { clipRect.x = dx; clipRect.width = std::min(bm.rightBearing - bm.leftBearing, fillEnd - dx); AutoPushClipRect clip(aThebesContext, oneDevPixel, clipRect); dx -= bm.leftBearing; mGlyphs[3]->Draw(Range(mGlyphs[3].get()), gfx::Point(dx, dy), params); dx += bm.rightBearing; } } } #ifdef DEBUG else { // no glue for (i = 0; i < 2; ++i) { NS_ASSERTION(end[i] >= start[i + 1], "gap between parts with missing glue glyph"); } } #endif return NS_OK; }