Revision control
++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "qcanvas2dutils_p.h"
++ #include <QtCore/private/qlocale_tools_p.h>
++ #include <QtCore/qstringview.h>
++ #include <QtGui/qfont.h>
++ #include <QtGui/qfontdatabase.h>
++
++ QT_BEGIN_NAMESPACE
++
++ enum FontToken
++ {
++ NoTokens = 0x00,
++ FontStyle = 0x01,
++ FontVariant = 0x02,
++ FontWeight = 0x04
++ };
++
++ static int parseFontSizeFromToken(QStringView fontSizeToken, bool &ok)
++ {
++ ok = false;
++ float size = fontSizeToken.trimmed().toFloat(&ok);
++ if (ok) {
++ return int(size);
++ }
++ qWarning().nospace() << "Context2D: A font size of " << fontSizeToken << " is invalid.";
++ return 0;
++ }
++
++ /*
++ Attempts to set the font size of \a font to \a fontSizeToken, returning
++ \c true if successful. If the font size is invalid, \c false is returned
++ and a warning is printed.
++ */
++ static bool setFontSizeFromToken(QFont &font, QStringView fontSizeToken)
++ {
++ const QStringView trimmedToken = fontSizeToken.trimmed();
++ const QStringView unitStr = trimmedToken.right(2);
++ const QStringView value = trimmedToken.left(trimmedToken.size() - 2);
++ bool ok = false;
++ int size = 0;
++ if (unitStr == QStringLiteral("px")) {
++ size = parseFontSizeFromToken(value, ok);
++ if (ok) {
++ font.setPixelSize(size);
++ return true;
++ }
++ } else if (unitStr == QStringLiteral("pt")) {
++ size = parseFontSizeFromToken(value, ok);
++ if (ok) {
++ font.setPointSize(size);
++ return true;
++ }
++ } else {
++ qWarning().nospace() << "Context2D: Invalid font size unit in font string.";
++ }
++ return false;
++ }
++
++
++ /*
++ Returns a list of all of the families in \a fontFamiliesString, where
++ each family is separated by spaces. Families with spaces in their name
++ must be quoted.
++ */
++ static QStringList extractFontFamiliesFromString(QStringView fontFamiliesString)
++ {
++ QStringList extractedFamilies;
++ int quoteIndex = -1;
++ QString currentFamily;
++ for (int index = 0; index < fontFamiliesString.size(); ++index) {
++ const QChar ch = fontFamiliesString.at(index);
++ if (ch == u'"' || ch == u'\'') {
++ if (quoteIndex == -1) {
++ quoteIndex = index;
++ } else {
++ if (ch == fontFamiliesString.at(quoteIndex)) {
++ // Found the matching quote. +1/-1 because we don't want the quote as part of the name.
++ const QString family = fontFamiliesString.mid(quoteIndex + 1, index - quoteIndex - 1).toString();
++ extractedFamilies.push_back(family);
++ currentFamily.clear();
++ quoteIndex = -1;
++ } else {
++ qWarning().nospace() << "Context2D: Mismatched quote in font string.";
++ return QStringList();
++ }
++ }
++ } else if (ch == u' ' && quoteIndex == -1) {
++ // This is a space that's not within quotes...
++ if (!currentFamily.isEmpty()) {
++ // and there is a current family; consider it the end of the current family.
++ extractedFamilies.push_back(currentFamily);
++ currentFamily.clear();
++ } // else: ignore the space
++ } else {
++ currentFamily.push_back(ch);
++ }
++ }
++ if (!currentFamily.isEmpty()) {
++ if (quoteIndex == -1) {
++ // This is the end of the string, so add this family to our list.
++ extractedFamilies.push_back(currentFamily);
++ } else {
++ qWarning().nospace() << "Context2D: Unclosed quote in font string.";
++ return QStringList();
++ }
++ }
++ if (extractedFamilies.isEmpty()) {
++ qWarning().nospace() << "Context2D: Missing or misplaced font family in font string"
++ << " (it must come after the font size).";
++ }
++ return extractedFamilies;
++ }
++
++ /*
++ Tries to set a family on \a font using the families provided in \a fontFamilyTokens.
++
++ The list is ordered by preference, with the first family having the highest preference.
++ If the first family is invalid, the next family in the list is evaluated.
++ This process is repeated until a valid font is found (at which point the function
++ will return \c true and the family set on \a font) or there are no more
++ families left, at which point a warning is printed and \c false is returned.
++ */
++ static bool setFontFamilyFromTokens(QFont &font, const QStringList &fontFamilyTokens)
++ {
++ for (const QString &fontFamilyToken : fontFamilyTokens) {
++ if (QFontDatabase::hasFamily(fontFamilyToken)) {
++ font.setFamily(fontFamilyToken);
++ return true;
++ } else {
++ // Can't find a family matching this name; if it's a generic family,
++ // try searching for the default family for it by using style hints.
++ int styleHint = -1;
++ if (fontFamilyToken.compare(QStringLiteral("serif")) == 0) {
++ styleHint = QFont::Serif;
++ } else if (fontFamilyToken.compare(QStringLiteral("sans-serif")) == 0) {
++ styleHint = QFont::SansSerif;
++ } else if (fontFamilyToken.compare(QStringLiteral("cursive")) == 0) {
++ styleHint = QFont::Cursive;
++ } else if (fontFamilyToken.compare(QStringLiteral("monospace")) == 0) {
++ styleHint = QFont::Monospace;
++ } else if (fontFamilyToken.compare(QStringLiteral("fantasy")) == 0) {
++ styleHint = QFont::Fantasy;
++ }
++ if (styleHint != -1) {
++ QFont tmp;
++ tmp.setStyleHint(static_cast<QFont::StyleHint>(styleHint));
++ font.setFamily(tmp.defaultFamily());
++ return true;
++ }
++ }
++ }
++ qWarning("Context2D: The font families specified are invalid: %s", qPrintable(fontFamilyTokens.join(QString()).trimmed()));
++ return false;
++ }
++
++ #define Q_TRY_SET_TOKEN(token, value, setStatement) \
++ if (!(usedTokens & token)) { \
++ usedTokens |= token; \
++ setStatement; \
++ } else { \
++ qWarning().nospace() << "Context2D: Duplicate token " << QStringLiteral(value) << " found in font string."; \
++ return currentFont; \
++ }
++
++ /*
++ Parses a font string based on the CSS shorthand font property.
++
++ */
++ QFont QCanvas2DUtils::qFontFromString(const QString& fontString, const QFont ¤tFont) {
++ if (fontString.isEmpty()) {
++ qWarning().nospace() << "Context2D: Font string is empty.";
++ return currentFont;
++ }
++
++ // We know that font-size must be specified and it must be before font-family
++ // (which could potentially have "px" or "pt" in its name), so extract it now.
++ int fontSizeEnd = fontString.indexOf(QStringLiteral("px"));
++ if (fontSizeEnd == -1)
++ fontSizeEnd = fontString.indexOf(QStringLiteral("pt"));
++ if (fontSizeEnd == -1) {
++ qWarning().nospace() << "Context2D: Invalid font size unit in font string.";
++ return currentFont;
++ }
++
++ int fontSizeStart = fontString.lastIndexOf(u' ', fontSizeEnd);
++ if (fontSizeStart == -1) {
++ // The font size might be the first token in the font string, which is OK.
++ // Regardless, we'll find out if the font is invalid with setFontSizeFromToken().
++ fontSizeStart = 0;
++ } else {
++ // Don't want to take the leading space.
++ ++fontSizeStart;
++ }
++
++ // + 2 for the unit, +1 for the space that we require.
++ fontSizeEnd += 3;
++
++ QFont newFont;
++ if (!setFontSizeFromToken(newFont, QStringView{fontString}.mid(fontSizeStart, fontSizeEnd - fontSizeStart)))
++ return currentFont;
++
++ // We don't want to parse the size twice, so remove it now.
++ QString remainingFontString = fontString;
++ remainingFontString.remove(fontSizeStart, fontSizeEnd - fontSizeStart);
++ QStringView remainingFontStringRef(remainingFontString);
++
++ // Next, we have to take any font families out, as QString::split() will ruin quoted family names.
++ const QStringView fontFamiliesString = remainingFontStringRef.mid(fontSizeStart);
++ remainingFontStringRef.truncate(fontSizeStart);
++ QStringList fontFamilies = extractFontFamiliesFromString(fontFamiliesString);
++ if (fontFamilies.isEmpty()) {
++ return currentFont;
++ }
++ if (!setFontFamilyFromTokens(newFont, fontFamilies))
++ return currentFont;
++
++ // Now that we've removed the messy parts, we can split the font string on spaces.
++ const QStringView trimmedTokensStr = remainingFontStringRef.trimmed();
++ if (trimmedTokensStr.isEmpty()) {
++ // No optional properties.
++ return newFont;
++ }
++ const auto tokens = trimmedTokensStr.split(QLatin1Char(' '));
++
++ int usedTokens = NoTokens;
++ // Optional properties can be in any order, but font-size and font-family must be last.
++ for (const QStringView &token : tokens) {
++ if (token.compare(QStringLiteral("normal")) == 0) {
++ if (!(usedTokens & FontStyle) || !(usedTokens & FontVariant) || !(usedTokens & FontWeight)) {
++ // Could be font-style, font-variant or font-weight.
++ if (!(usedTokens & FontStyle)) {
++ // QFont::StyleNormal is the default for QFont::style.
++ usedTokens = usedTokens | FontStyle;
++ } else if (!(usedTokens & FontVariant)) {
++ // QFont::MixedCase is the default for QFont::capitalization.
++ usedTokens |= FontVariant;
++ } else if (!(usedTokens & FontWeight)) {
++ // QFont::Normal is the default for QFont::weight.
++ usedTokens |= FontWeight;
++ }
++ } else {
++ qWarning().nospace() << "Context2D: Duplicate token \"normal\" found in font string.";
++ return currentFont;
++ }
++ } else if (token.compare(QStringLiteral("bold")) == 0) {
++ Q_TRY_SET_TOKEN(FontWeight, "bold", newFont.setBold(true))
++ } else if (token.compare(QStringLiteral("italic")) == 0) {
++ Q_TRY_SET_TOKEN(FontStyle, "italic", newFont.setStyle(QFont::StyleItalic))
++ } else if (token.compare(QStringLiteral("oblique")) == 0) {
++ Q_TRY_SET_TOKEN(FontStyle, "oblique", newFont.setStyle(QFont::StyleOblique))
++ } else if (token.compare(QStringLiteral("small-caps")) == 0) {
++ Q_TRY_SET_TOKEN(FontVariant, "small-caps", newFont.setCapitalization(QFont::SmallCaps))
++ } else {
++ bool conversionOk = false;
++ int weight = token.toInt(&conversionOk);
++ if (conversionOk) {
++ Q_TRY_SET_TOKEN(FontWeight, "<font-weight>",
++ newFont.setWeight(QFont::Weight(weight)))
++ } else {
++ // The token is invalid or in the wrong place/order in the font string.
++ qWarning().nospace() << "Context2D: Invalid or misplaced token " << token
++ << " found in font string.";
++ return currentFont;
++ }
++ }
++ }
++ return newFont;
++ }
++
++ QT_END_NAMESPACE