From 2d065ddb53a31c1b717c88902bcdf4f6b24f320f Mon Sep 17 00:00:00 2001 From: Mitch Curtis Date: Thu, 13 Jun 2013 15:41:33 +0200 Subject: [PATCH] Correctly parse Context2D font as per CSS shorthand font property Task-number: QTBUG-31721 Change-Id: I6a6ba99ed29392fa7ed67f6a3dba567947f9c46b Reviewed-by: Alan Alpert --- src/quick/items/context2d/qquickcontext2d.cpp | 193 ++++++++++++++++++--- .../quick/qquickcanvasitem/data/tst_context.qml | 79 ++++++++- 2 files changed, 242 insertions(+), 30 deletions(-) diff --git a/src/quick/items/context2d/qquickcontext2d.cpp b/src/quick/items/context2d/qquickcontext2d.cpp index 111d02e..437c264 100644 --- a/src/quick/items/context2d/qquickcontext2d.cpp +++ b/src/quick/items/context2d/qquickcontext2d.cpp @@ -192,31 +192,153 @@ QColor qt_color_from_string(v8::Local name) return QColor(); } -QFont qt_font_from_string(const QString& fontString) { - QFont font; - // ### this is simplified and incomplete - // ### TODO:get code from Qt webkit - const QStringList tokens = fontString.split(QLatin1Char(' ')); - foreach (const QString &token, tokens) { - if (token == QLatin1String("italic")) - font.setItalic(true); - else if (token == QLatin1String("bold")) - font.setBold(true); - else if (token.endsWith(QLatin1String("px"))) { - QString number = token; - number.remove(QLatin1String("px")); - bool ok = false; - float pixelSize = number.trimmed().toFloat(&ok); - if (ok) - font.setPixelSize(int(pixelSize)); - } else - font.setFamily(token); - } - - return font; +static bool qSetFontSizeFromToken(QFont &font, const QString &fontSizeToken) +{ + const QString trimmedToken = fontSizeToken.trimmed(); + QString unit = trimmedToken.right(2); + QString value = trimmedToken.left(fontSizeToken.size() - 2); + bool ok = false; + float size = value.trimmed().toFloat(&ok); + if (ok) { + int intSize = int(size); + if (unit.compare(QLatin1String("px")) == 0) { + font.setPixelSize(intSize); + return true; + } else if (unit.compare(QLatin1String("pt")) == 0) { + font.setPointSize(intSize); + return true; + } + } + qWarning().nospace() << "Context2D: A font size of " << fontSizeToken << " is invalid."; + return false; } +static bool qSetFontFamilyFromToken(QFont &font, const QString &fontFamilyToken) +{ + const QString trimmedToken = fontFamilyToken.trimmed(); + QFontDatabase fontDatabase; + if (fontDatabase.hasFamily(trimmedToken)) { + font.setFamily(trimmedToken); + 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. + QFont tmp; + int styleHint = -1; + if (fontFamilyToken.compare(QLatin1String("serif")) == 0) { + styleHint = QFont::Serif; + } else if (fontFamilyToken.compare(QLatin1String("sans-serif")) == 0) { + styleHint = QFont::SansSerif; + } else if (fontFamilyToken.compare(QLatin1String("cursive")) == 0) { + styleHint = QFont::Cursive; + } else if (fontFamilyToken.compare(QLatin1String("monospace")) == 0) { + styleHint = QFont::Monospace; + } else if (fontFamilyToken.compare(QLatin1String("fantasy")) == 0) { + styleHint = QFont::Fantasy; + } + if (styleHint != -1) { + tmp.setStyleHint(static_cast(styleHint)); + font.setFamily(tmp.defaultFamily()); + return true; + } + } + qWarning().nospace() << "Context2D: The font family " << fontFamilyToken << " is invalid."; + return false; +} +enum FontToken +{ + NoTokens = 0x00, + FontStyle = 0x01, + FontVariant = 0x02, + FontWeight = 0x04 +}; + +#define Q_TRY_SET_TOKEN(token, value, setStatement) \ +if (!(usedTokens & token)) { \ + usedTokens |= token; \ + setStatement; \ +} else { \ + qWarning().nospace() << "Context2D: Duplicate token " << QLatin1String(value) << " found in font string."; \ + return currentFont; \ +} + +/*! + Parses a font string based on the CSS shorthand font property. + + See: http://www.w3.org/TR/css3-fonts/#font-prop +*/ +static QFont qt_font_from_string(const QString& fontString, const QFont ¤tFont) { + const QStringList tokens = fontString.split(QLatin1Char(' ')); + if (tokens.size() < 2) { + qWarning().nospace() << "Context2D: Insufficent amount of tokens in font string."; + return currentFont; + } + + QFont newFont; + QStringList remainingTokens = tokens; + int usedTokens = NoTokens; + // Optional properties can be in any order, but font-size and font-family must be last. + while (remainingTokens.size() > 2) { + const QString token = remainingTokens.takeFirst(); + if (token.compare(QLatin1String("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(QLatin1String("bold")) == 0) { + Q_TRY_SET_TOKEN(FontWeight, "bold", newFont.setBold(true)) + } else if (token.compare(QLatin1String("italic")) == 0) { + Q_TRY_SET_TOKEN(FontStyle, "italic", newFont.setStyle(QFont::StyleItalic)) + } else if (token.compare(QLatin1String("oblique")) == 0) { + Q_TRY_SET_TOKEN(FontStyle, "oblique", newFont.setStyle(QFont::StyleOblique)) + } else if (token.compare(QLatin1String("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) { + if (weight >= 0 && weight <= 99) { + Q_TRY_SET_TOKEN(FontWeight, "", newFont.setWeight(weight)) + continue; + } else { + qWarning().nospace() << "Context2D: Invalid font weight " << weight << " found in font string; " + << "must be between 0 and 99, inclusive."; + return currentFont; + } + } + // 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; + } + } + if (remainingTokens.size() == 2) { + // Order must be: font-size font-family. + if (!qSetFontSizeFromToken(newFont, remainingTokens.first())) { + return currentFont; + } + if (!qSetFontFamilyFromToken(newFont, remainingTokens.last())) { + return currentFont; + } + return newFont; + } else { + qWarning().nospace() << "Context2D: Missing font-size and/or font-family tokens in font string."; + return currentFont; + } + return newFont; +} class QQuickContext2DEngineData : public QV8Engine::Deletable { @@ -2038,13 +2160,28 @@ static v8::Handle ctx2d_caretBlinkRate(const v8::Arguments &args) V8THROW_DOM(DOMEXCEPTION_NOT_SUPPORTED_ERR, "Context2D::caretBlinkRate is not supported"); } -// text + /*! - \qmlproperty string QtQuick2::Context2D::font - Holds the current font settings. + \qmlproperty string QtQuick2::Context2D::font + Holds the current font settings. + + A subset of the + \l {http://www.w3.org/TR/2dcontext/#dom-context-2d-font}{w3C 2d context standard for font} + is supported: + + \list + \li font-style (optional): + normal | italic | oblique + \li font-variant (optional): normal | small-caps + \li font-weight (optional): normal | bold | 0 ... 99 + \li font-size: Npx | Npt (where N is a positive number) + \li font-family: See \l {http://www.w3.org/TR/CSS2/fonts.html#propdef-font-family} + \endlist + + Note that font-size and font-family are mandatory and must be in the order + they are shown in above. - The default font value is "10px sans-serif". - See \l {http://www.w3.org/TR/2dcontext/#dom-context-2d-font}{w3C 2d context standard for font} + The default font value is "10px sans-serif". */ v8::Handle ctx2d_font(v8::Local, const v8::AccessorInfo &info) { @@ -2063,7 +2200,7 @@ static void ctx2d_font_set(v8::Local, v8::Local value, co QV8Engine *engine = V8ENGINE_ACCESSOR(); QString fs = engine->toString(value); - QFont font = qt_font_from_string(fs); + QFont font = qt_font_from_string(fs, r->context->state.font); if (font != r->context->state.font) { r->context->state.font = font; } diff --git a/tests/auto/quick/qquickcanvasitem/data/tst_context.qml b/tests/auto/quick/qquickcanvasitem/data/tst_context.qml index ad44f6d..ab351f0 100644 --- a/tests/auto/quick/qquickcanvasitem/data/tst_context.qml +++ b/tests/auto/quick/qquickcanvasitem/data/tst_context.qml @@ -71,6 +71,7 @@ Canvas { } } + // See: http://www.w3.org/TR/css3-fonts/#font-prop TestCase { name: "ContextFontValidation" when: canvas.available @@ -82,8 +83,82 @@ Canvas { var ctx = canvas.getContext("2d"); compare(ctx.font, "sans-serif,-1,10,5,50,0,0,0,0,0"); - ctx.font = "80.1px sans-serif"; - compare(ctx.font, "sans-serif,-1,80,5,50,0,0,0,0,0"); + ctx.font = "80.1px cursive"; + // Can't verify the chosen font family since it's different for each platform. + compare(ctx.font.substr(ctx.font.indexOf(",") + 1), "-1,80,5,50,0,0,0,0,0"); + } + + function test_valid() { + wait(100); + compare(contextSpy.count, 1); + + var ctx = canvas.getContext("2d"); + + var validFonts = [ + { string: "12px sans-serif", expected: "sans-serif,-1,12,5,50,0,0,0,0,0" }, + { string: "12px serif", expected: "serif,-1,12,5,50,0,0,0,0,0" }, + { string: "12pt sans-serif", expected: "sans-serif,12,-1,5,50,0,0,0,0,0" }, + { string: "12pt serif", expected: "serif,12,-1,5,50,0,0,0,0,0" }, + { string: "normal 12px sans-serif", expected: "sans-serif,-1,12,5,50,0,0,0,0,0" }, + { string: "normal normal 12px sans-serif", expected: "sans-serif,-1,12,5,50,0,0,0,0,0" }, + { string: "normal normal normal 12px sans-serif", expected: "sans-serif,-1,12,5,50,0,0,0,0,0" }, + { string: "italic 12px sans-serif", expected: "sans-serif,-1,12,5,50,1,0,0,0,0" }, + { string: "italic normal 12px sans-serif", expected: "sans-serif,-1,12,5,50,1,0,0,0,0" }, + { string: "italic normal normal 12px sans-serif", expected: "sans-serif,-1,12,5,50,1,0,0,0,0" }, + { string: "oblique 12px sans-serif", expected: "sans-serif,-1,12,5,50,2,0,0,0,0" }, + { string: "oblique normal 12px sans-serif", expected: "sans-serif,-1,12,5,50,2,0,0,0,0" }, + { string: "oblique normal normal 12px sans-serif", expected: "sans-serif,-1,12,5,50,2,0,0,0,0" }, + { string: "bold 12px sans-serif", expected: "sans-serif,-1,12,5,75,0,0,0,0,0" }, + { string: "0 12px sans-serif", expected: "sans-serif,-1,12,5,0,0,0,0,0,0" }, + { string: "small-caps 12px sans-serif", expected: "sans-serif,-1,12,5,50,0,0,0,0,0" }, + ]; + for (var i = 0; i < validFonts.length; ++i) { + ctx.font = validFonts[i].string; + compare(ctx.font.substr(ctx.font.indexOf(",") + 1), + validFonts[i].expected.substr(validFonts[i].expected.indexOf(",") + 1)); + } + } + + function test_invalid() { + wait(100); + compare(contextSpy.count, 1); + + var ctx = canvas.getContext("2d"); + var originalFont = ctx.font; + var i = 0; + + var insufficientQtyTokens = ["", "12px", "sans-serif"]; + for (i = 0; i < insufficientQtyTokens.length; ++i) { + ignoreWarning("Context2D: Insufficent amount of tokens in font string."); + ctx.font = insufficientQtyTokens[i]; + compare(ctx.font, originalFont); + } + + var invalidFontSizes = ["z12px sans-serif", "1z2px sans-serif", "12zpx sans-serif", + "12pzx sans-serif", "12pxz sans-serif", "sans-serif 12px"]; + for (i = 0; i < invalidFontSizes.length; ++i) { + ignoreWarning("Context2D: A font size of \"" + invalidFontSizes[i].split(" ")[0] + "\" is invalid."); + ctx.font = invalidFontSizes[i]; + compare(ctx.font, originalFont); + } + + var invalidFontFamilies = ["12px !@weeeeeeee!@!@Don'tNameYourFontThis", "12px )(&*^^^%#$@*!!@#$JSPOR)"]; + for (i = 0; i < invalidFontFamilies.length; ++i) { + ignoreWarning("Context2D: The font family \"" + invalidFontFamilies[i].split(" ")[1] + "\" is invalid."); + ctx.font = invalidFontFamilies[i]; + compare(ctx.font, originalFont); + } + + var duplicates = [ + { duplicate: "normal", string: "normal normal normal normal 12px sans-serif" }, + { duplicate: "bold", string: "normal normal bold bold 12px sans-serif" }, + { duplicate: "bold", string: "bold bold 12px sans-serif" } + ]; + for (i = 0; i < duplicates.length; ++i) { + ignoreWarning("Context2D: Duplicate token \"" + duplicates[i].duplicate + "\" found in font string."); + ctx.font = duplicates[i].string; + compare(ctx.font, originalFont); + } } } } -- 2.7.4