blob: d44c0b7c1f10be70152025bd29bda4efaee56841 [file] [log] [blame]
/*
* Copyright (C) 2004-2017 Apple Inc. All rights reserved.
* Copyright (C) 2008, 2010 Nokia Corporation and/or its subsidiary(-ies)
* Copyright (C) 2007 Alp Toker <alp@atoker.com>
* Copyright (C) 2008 Eric Seidel <eric@webkit.org>
* Copyright (C) 2008 Dirk Schulze <krit@webkit.org>
* Copyright (C) 2010 Torch Mobile (Beijing) Co. Ltd. All rights reserved.
* Copyright (C) 2012 Intel Corporation. All rights reserved.
* Copyright (C) 2013, 2014 Adobe Systems Incorporated. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "CanvasRenderingContext2D.h"
#include "CSSFontSelector.h"
#include "CSSParser.h"
#include "CSSPropertyNames.h"
#include "ImageBuffer.h"
#include "ImageData.h"
#include "InspectorInstrumentation.h"
#include "Path2D.h"
#include "RenderTheme.h"
#include "ResourceLoadObserver.h"
#include "RuntimeEnabledFeatures.h"
#include "StyleProperties.h"
#include "StyleResolver.h"
#include "TextMetrics.h"
#include "TextRun.h"
#include <wtf/CheckedArithmetic.h>
#include <wtf/IsoMallocInlines.h>
#include <wtf/MathExtras.h>
#include <wtf/text/StringBuilder.h>
namespace WebCore {
using namespace HTMLNames;
WTF_MAKE_ISO_ALLOCATED_IMPL(CanvasRenderingContext2D);
std::unique_ptr<CanvasRenderingContext2D> CanvasRenderingContext2D::create(CanvasBase& canvas, bool usesCSSCompatibilityParseMode, bool usesDashboardCompatibilityMode)
{
auto renderingContext = std::unique_ptr<CanvasRenderingContext2D>(new CanvasRenderingContext2D(canvas, usesCSSCompatibilityParseMode, usesDashboardCompatibilityMode));
InspectorInstrumentation::didCreateCanvasRenderingContext(*renderingContext);
return renderingContext;
}
CanvasRenderingContext2D::CanvasRenderingContext2D(CanvasBase& canvas, bool usesCSSCompatibilityParseMode, bool usesDashboardCompatibilityMode)
: CanvasRenderingContext2DBase(canvas, usesCSSCompatibilityParseMode, usesDashboardCompatibilityMode)
{
}
CanvasRenderingContext2D::~CanvasRenderingContext2D() = default;
void CanvasRenderingContext2D::drawFocusIfNeeded(Element& element)
{
drawFocusIfNeededInternal(m_path, element);
}
void CanvasRenderingContext2D::drawFocusIfNeeded(Path2D& path, Element& element)
{
drawFocusIfNeededInternal(path.path(), element);
}
void CanvasRenderingContext2D::drawFocusIfNeededInternal(const Path& path, Element& element)
{
auto* context = drawingContext();
if (!element.focused() || !state().hasInvertibleTransform || path.isEmpty() || !element.isDescendantOf(canvas()) || !context)
return;
context->drawFocusRing(path, 1, 1, RenderTheme::singleton().focusRingColor(element.document().styleColorOptions(canvas().computedStyle())));
}
String CanvasRenderingContext2D::font() const
{
if (!state().font.realized())
return DefaultFont;
StringBuilder serializedFont;
const auto& fontDescription = state().font.fontDescription();
if (fontDescription.italic())
serializedFont.appendLiteral("italic ");
if (fontDescription.variantCaps() == FontVariantCaps::Small)
serializedFont.appendLiteral("small-caps ");
serializedFont.appendNumber(fontDescription.computedPixelSize());
serializedFont.appendLiteral("px");
for (unsigned i = 0; i < fontDescription.familyCount(); ++i) {
if (i)
serializedFont.append(',');
// FIXME: We should append family directly to serializedFont rather than building a temporary string.
String family = fontDescription.familyAt(i);
if (family.startsWith("-webkit-"))
family = family.substring(8);
if (family.contains(' '))
family = makeString('"', family, '"');
serializedFont.append(' ');
serializedFont.append(family);
}
return serializedFont.toString();
}
void CanvasRenderingContext2D::setFont(const String& newFont)
{
if (newFont == state().unparsedFont && state().font.realized())
return;
auto parsedStyle = MutableStyleProperties::create();
CSSParser::parseValue(parsedStyle, CSSPropertyFont, newFont, true, strictToCSSParserMode(!m_usesCSSCompatibilityParseMode));
if (parsedStyle->isEmpty())
return;
String fontValue = parsedStyle->getPropertyValue(CSSPropertyFont);
// According to http://lists.w3.org/Archives/Public/public-html/2009Jul/0947.html,
// the "inherit" and "initial" values must be ignored.
if (fontValue == "inherit" || fontValue == "initial")
return;
// The parse succeeded.
String newFontSafeCopy(newFont); // Create a string copy since newFont can be deleted inside realizeSaves.
realizeSaves();
modifiableState().unparsedFont = newFontSafeCopy;
// Map the <canvas> font into the text style. If the font uses keywords like larger/smaller, these will work
// relative to the canvas.
auto newStyle = RenderStyle::createPtr();
Document& document = canvas().document();
document.updateStyleIfNeeded();
if (auto* computedStyle = canvas().computedStyle())
newStyle->setFontDescription(FontCascadeDescription { computedStyle->fontDescription() });
else {
FontCascadeDescription defaultFontDescription;
defaultFontDescription.setOneFamily(DefaultFontFamily);
defaultFontDescription.setSpecifiedSize(DefaultFontSize);
defaultFontDescription.setComputedSize(DefaultFontSize);
newStyle->setFontDescription(WTFMove(defaultFontDescription));
}
newStyle->fontCascade().update(&document.fontSelector());
// Now map the font property longhands into the style.
StyleResolver& styleResolver = canvas().styleResolver();
styleResolver.applyPropertyToStyle(CSSPropertyFontFamily, parsedStyle->getPropertyCSSValue(CSSPropertyFontFamily).get(), WTFMove(newStyle));
styleResolver.applyPropertyToCurrentStyle(CSSPropertyFontStyle, parsedStyle->getPropertyCSSValue(CSSPropertyFontStyle).get());
styleResolver.applyPropertyToCurrentStyle(CSSPropertyFontVariantCaps, parsedStyle->getPropertyCSSValue(CSSPropertyFontVariantCaps).get());
styleResolver.applyPropertyToCurrentStyle(CSSPropertyFontWeight, parsedStyle->getPropertyCSSValue(CSSPropertyFontWeight).get());
// As described in BUG66291, setting font-size and line-height on a font may entail a CSSPrimitiveValue::computeLengthDouble call,
// which assumes the fontMetrics are available for the affected font, otherwise a crash occurs (see http://trac.webkit.org/changeset/96122).
// The updateFont() calls below update the fontMetrics and ensure the proper setting of font-size and line-height.
styleResolver.updateFont();
styleResolver.applyPropertyToCurrentStyle(CSSPropertyFontSize, parsedStyle->getPropertyCSSValue(CSSPropertyFontSize).get());
styleResolver.updateFont();
styleResolver.applyPropertyToCurrentStyle(CSSPropertyLineHeight, parsedStyle->getPropertyCSSValue(CSSPropertyLineHeight).get());
modifiableState().font.initialize(document.fontSelector(), *styleResolver.style());
}
static CanvasTextAlign toCanvasTextAlign(TextAlign textAlign)
{
switch (textAlign) {
case StartTextAlign:
return CanvasTextAlign::Start;
case EndTextAlign:
return CanvasTextAlign::End;
case LeftTextAlign:
return CanvasTextAlign::Left;
case RightTextAlign:
return CanvasTextAlign::Right;
case CenterTextAlign:
return CanvasTextAlign::Center;
}
ASSERT_NOT_REACHED();
return CanvasTextAlign::Start;
}
static TextAlign fromCanvasTextAlign(CanvasTextAlign canvasTextAlign)
{
switch (canvasTextAlign) {
case CanvasTextAlign::Start:
return StartTextAlign;
case CanvasTextAlign::End:
return EndTextAlign;
case CanvasTextAlign::Left:
return LeftTextAlign;
case CanvasTextAlign::Right:
return RightTextAlign;
case CanvasTextAlign::Center:
return CenterTextAlign;
}
ASSERT_NOT_REACHED();
return StartTextAlign;
}
CanvasTextAlign CanvasRenderingContext2D::textAlign() const
{
return toCanvasTextAlign(state().textAlign);
}
void CanvasRenderingContext2D::setTextAlign(CanvasTextAlign canvasTextAlign)
{
auto textAlign = fromCanvasTextAlign(canvasTextAlign);
if (state().textAlign == textAlign)
return;
realizeSaves();
modifiableState().textAlign = textAlign;
}
static CanvasTextBaseline toCanvasTextBaseline(TextBaseline textBaseline)
{
switch (textBaseline) {
case TopTextBaseline:
return CanvasTextBaseline::Top;
case HangingTextBaseline:
return CanvasTextBaseline::Hanging;
case MiddleTextBaseline:
return CanvasTextBaseline::Middle;
case AlphabeticTextBaseline:
return CanvasTextBaseline::Alphabetic;
case IdeographicTextBaseline:
return CanvasTextBaseline::Ideographic;
case BottomTextBaseline:
return CanvasTextBaseline::Bottom;
}
ASSERT_NOT_REACHED();
return CanvasTextBaseline::Top;
}
static TextBaseline fromCanvasTextBaseline(CanvasTextBaseline canvasTextBaseline)
{
switch (canvasTextBaseline) {
case CanvasTextBaseline::Top:
return TopTextBaseline;
case CanvasTextBaseline::Hanging:
return HangingTextBaseline;
case CanvasTextBaseline::Middle:
return MiddleTextBaseline;
case CanvasTextBaseline::Alphabetic:
return AlphabeticTextBaseline;
case CanvasTextBaseline::Ideographic:
return IdeographicTextBaseline;
case CanvasTextBaseline::Bottom:
return BottomTextBaseline;
}
ASSERT_NOT_REACHED();
return TopTextBaseline;
}
CanvasTextBaseline CanvasRenderingContext2D::textBaseline() const
{
return toCanvasTextBaseline(state().textBaseline);
}
void CanvasRenderingContext2D::setTextBaseline(CanvasTextBaseline canvasTextBaseline)
{
auto textBaseline = fromCanvasTextBaseline(canvasTextBaseline);
if (state().textBaseline == textBaseline)
return;
realizeSaves();
modifiableState().textBaseline = textBaseline;
}
inline TextDirection CanvasRenderingContext2D::toTextDirection(Direction direction, const RenderStyle** computedStyle) const
{
auto* style = (computedStyle || direction == Direction::Inherit) ? canvas().computedStyle() : nullptr;
if (computedStyle)
*computedStyle = style;
switch (direction) {
case Direction::Inherit:
return style ? style->direction() : TextDirection::LTR;
case Direction::Rtl:
return TextDirection::RTL;
case Direction::Ltr:
return TextDirection::LTR;
}
ASSERT_NOT_REACHED();
return TextDirection::LTR;
}
CanvasDirection CanvasRenderingContext2D::direction() const
{
if (state().direction == Direction::Inherit)
canvas().document().updateStyleIfNeeded();
return toTextDirection(state().direction) == TextDirection::RTL ? CanvasDirection::Rtl : CanvasDirection::Ltr;
}
void CanvasRenderingContext2D::setDirection(CanvasDirection direction)
{
if (state().direction == direction)
return;
realizeSaves();
modifiableState().direction = direction;
}
void CanvasRenderingContext2D::fillText(const String& text, float x, float y, Optional<float> maxWidth)
{
drawTextInternal(text, x, y, true, maxWidth);
}
void CanvasRenderingContext2D::strokeText(const String& text, float x, float y, Optional<float> maxWidth)
{
drawTextInternal(text, x, y, false, maxWidth);
}
static inline bool isSpaceThatNeedsReplacing(UChar c)
{
// According to specification all space characters should be replaced with 0x0020 space character.
// http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#text-preparation-algorithm
// The space characters according to specification are : U+0020, U+0009, U+000A, U+000C, and U+000D.
// http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#space-character
// This function returns true for 0x000B also, so that this is backward compatible.
// Otherwise, the test LayoutTests/canvas/philip/tests/2d.text.draw.space.collapse.space.html will fail
return c == 0x0009 || c == 0x000A || c == 0x000B || c == 0x000C || c == 0x000D;
}
static void normalizeSpaces(String& text)
{
size_t i = text.find(isSpaceThatNeedsReplacing);
if (i == notFound)
return;
unsigned textLength = text.length();
Vector<UChar> charVector(textLength);
StringView(text).getCharactersWithUpconvert(charVector.data());
charVector[i++] = ' ';
for (; i < textLength; ++i) {
if (isSpaceThatNeedsReplacing(charVector[i]))
charVector[i] = ' ';
}
text = String::adopt(WTFMove(charVector));
}
Ref<TextMetrics> CanvasRenderingContext2D::measureText(const String& text)
{
if (RuntimeEnabledFeatures::sharedFeatures().webAPIStatisticsEnabled()) {
auto& canvas = this->canvas();
ResourceLoadObserver::shared().logCanvasWriteOrMeasure(canvas.document(), text);
ResourceLoadObserver::shared().logCanvasRead(canvas.document());
}
Ref<TextMetrics> metrics = TextMetrics::create();
String normalizedText = text;
normalizeSpaces(normalizedText);
const RenderStyle* computedStyle;
auto direction = toTextDirection(state().direction, &computedStyle);
bool override = computedStyle ? isOverride(computedStyle->unicodeBidi()) : false;
TextRun textRun(normalizedText, 0, 0, AllowTrailingExpansion, direction, override, true);
auto& font = fontProxy();
auto& fontMetrics = font.fontMetrics();
GlyphOverflow glyphOverflow;
glyphOverflow.computeBounds = true;
float fontWidth = font.width(textRun, &glyphOverflow);
metrics->setWidth(fontWidth);
FloatPoint offset = textOffset(fontWidth, direction);
metrics->setActualBoundingBoxAscent(glyphOverflow.top - offset.y());
metrics->setActualBoundingBoxDescent(glyphOverflow.bottom + offset.y());
metrics->setFontBoundingBoxAscent(fontMetrics.ascent() - offset.y());
metrics->setFontBoundingBoxDescent(fontMetrics.descent() + offset.y());
metrics->setEmHeightAscent(fontMetrics.ascent() - offset.y());
metrics->setEmHeightDescent(fontMetrics.descent() + offset.y());
metrics->setHangingBaseline(fontMetrics.ascent() - offset.y());
metrics->setAlphabeticBaseline(-offset.y());
metrics->setIdeographicBaseline(-fontMetrics.descent() - offset.y());
metrics->setActualBoundingBoxLeft(glyphOverflow.left - offset.x());
metrics->setActualBoundingBoxRight(fontWidth + glyphOverflow.right + offset.x());
return metrics;
}
auto CanvasRenderingContext2D::fontProxy() -> const FontProxy& {
auto& canvas = downcast<HTMLCanvasElement>(canvasBase());
canvas.document().updateStyleIfNeeded();
if (!state().font.realized())
setFont(state().unparsedFont);
return state().font;
}
FloatPoint CanvasRenderingContext2D::textOffset(float width, TextDirection direction)
{
auto& fontMetrics = fontProxy().fontMetrics();
FloatPoint offset;
switch (state().textBaseline) {
case TopTextBaseline:
case HangingTextBaseline:
offset.setY(fontMetrics.ascent());
break;
case BottomTextBaseline:
case IdeographicTextBaseline:
offset.setY(-fontMetrics.descent());
break;
case MiddleTextBaseline:
offset.setY(fontMetrics.height() / 2 - fontMetrics.descent());
break;
case AlphabeticTextBaseline:
default:
break;
}
bool isRTL = direction == TextDirection::RTL;
auto align = state().textAlign;
if (align == StartTextAlign)
align = isRTL ? RightTextAlign : LeftTextAlign;
else if (align == EndTextAlign)
align = isRTL ? LeftTextAlign : RightTextAlign;
switch (align) {
case CenterTextAlign:
offset.setX(-width / 2);
break;
case RightTextAlign:
offset.setX(-width);
break;
default:
break;
}
return offset;
}
void CanvasRenderingContext2D::drawTextInternal(const String& text, float x, float y, bool fill, Optional<float> maxWidth)
{
if (RuntimeEnabledFeatures::sharedFeatures().webAPIStatisticsEnabled())
ResourceLoadObserver::shared().logCanvasWriteOrMeasure(this->canvas().document(), text);
auto& fontProxy = this->fontProxy();
const auto& fontMetrics = fontProxy.fontMetrics();
auto* c = drawingContext();
if (!c)
return;
if (!state().hasInvertibleTransform)
return;
if (!std::isfinite(x) | !std::isfinite(y))
return;
if (maxWidth && (!std::isfinite(maxWidth.value()) || maxWidth.value() <= 0))
return;
// If gradient size is zero, then paint nothing.
auto gradient = c->strokeGradient();
if (!fill && gradient && gradient->isZeroSize())
return;
gradient = c->fillGradient();
if (fill && gradient && gradient->isZeroSize())
return;
String normalizedText = text;
normalizeSpaces(normalizedText);
// FIXME: Need to turn off font smoothing.
const RenderStyle* computedStyle;
auto direction = toTextDirection(state().direction, &computedStyle);
bool override = computedStyle ? isOverride(computedStyle->unicodeBidi()) : false;
TextRun textRun(normalizedText, 0, 0, AllowTrailingExpansion, direction, override, true);
float fontWidth = fontProxy.width(textRun);
bool useMaxWidth = maxWidth && maxWidth.value() < fontWidth;
float width = useMaxWidth ? maxWidth.value() : fontWidth;
FloatPoint location(x, y);
location += textOffset(width, direction);
// The slop built in to this mask rect matches the heuristic used in FontCGWin.cpp for GDI text.
FloatRect textRect = FloatRect(location.x() - fontMetrics.height() / 2, location.y() - fontMetrics.ascent() - fontMetrics.lineGap(),
width + fontMetrics.height(), fontMetrics.lineSpacing());
if (!fill)
inflateStrokeRect(textRect);
#if USE(CG)
const CanvasStyle& drawStyle = fill ? state().fillStyle : state().strokeStyle;
if (drawStyle.canvasGradient() || drawStyle.canvasPattern()) {
IntRect maskRect = enclosingIntRect(textRect);
// If we have a shadow, we need to draw it before the mask operation.
// Follow a procedure similar to paintTextWithShadows in TextPainter.
if (shouldDrawShadows()) {
GraphicsContextStateSaver stateSaver(*c);
FloatSize offset(0, 2 * maskRect.height());
FloatSize shadowOffset;
float shadowRadius;
Color shadowColor;
c->getShadow(shadowOffset, shadowRadius, shadowColor);
FloatRect shadowRect(maskRect);
shadowRect.inflate(shadowRadius * 1.4);
shadowRect.move(shadowOffset * -1);
c->clip(shadowRect);
shadowOffset += offset;
c->setLegacyShadow(shadowOffset, shadowRadius, shadowColor);
if (fill)
c->setFillColor(Color::black);
else
c->setStrokeColor(Color::black);
fontProxy.drawBidiText(*c, textRun, location + offset, FontCascade::UseFallbackIfFontNotReady);
}
auto maskImage = ImageBuffer::createCompatibleBuffer(maskRect.size(), ColorSpaceSRGB, *c);
if (!maskImage)
return;
auto& maskImageContext = maskImage->context();
if (fill)
maskImageContext.setFillColor(Color::black);
else {
maskImageContext.setStrokeColor(Color::black);
maskImageContext.setStrokeThickness(c->strokeThickness());
}
maskImageContext.setTextDrawingMode(fill ? TextModeFill : TextModeStroke);
if (useMaxWidth) {
maskImageContext.translate(location - maskRect.location());
// We draw when fontWidth is 0 so compositing operations (eg, a "copy" op) still work.
maskImageContext.scale(FloatSize((fontWidth > 0 ? (width / fontWidth) : 0), 1));
fontProxy.drawBidiText(maskImageContext, textRun, FloatPoint(0, 0), FontCascade::UseFallbackIfFontNotReady);
} else {
maskImageContext.translate(-maskRect.location());
fontProxy.drawBidiText(maskImageContext, textRun, location, FontCascade::UseFallbackIfFontNotReady);
}
GraphicsContextStateSaver stateSaver(*c);
c->clipToImageBuffer(*maskImage, maskRect);
drawStyle.applyFillColor(*c);
c->fillRect(maskRect);
return;
}
#endif
c->setTextDrawingMode(fill ? TextModeFill : TextModeStroke);
GraphicsContextStateSaver stateSaver(*c);
if (useMaxWidth) {
c->translate(location);
// We draw when fontWidth is 0 so compositing operations (eg, a "copy" op) still work.
c->scale(FloatSize((fontWidth > 0 ? (width / fontWidth) : 0), 1));
location = FloatPoint();
}
if (isFullCanvasCompositeMode(state().globalComposite)) {
beginCompositeLayer();
fontProxy.drawBidiText(*c, textRun, location, FontCascade::UseFallbackIfFontNotReady);
endCompositeLayer();
didDrawEntireCanvas();
} else if (state().globalComposite == CompositeCopy) {
clearCanvas();
fontProxy.drawBidiText(*c, textRun, location, FontCascade::UseFallbackIfFontNotReady);
didDrawEntireCanvas();
} else {
fontProxy.drawBidiText(*c, textRun, location, FontCascade::UseFallbackIfFontNotReady);
didDraw(textRect);
}
}
} // namespace WebCore