blob: e8f55ef5dbb145f3d25fd74af08c4714bdd53ed2 [file] [log] [blame]
/*
* Copyright (C) 2006 Eric Seidel <eric@webkit.org>
* Copyright (C) 2008, 2009 Apple Inc. All rights reserved.
* Copyright (C) Research In Motion Limited 2011. 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 "core/svg/graphics/SVGImage.h"
#include "core/animation/DocumentTimeline.h"
#include "core/dom/NodeTraversal.h"
#include "core/dom/shadow/FlatTreeTraversal.h"
#include "core/frame/FrameView.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/Settings.h"
#include "core/style/ComputedStyle.h"
#include "core/layout/svg/LayoutSVGRoot.h"
#include "core/loader/FrameLoadRequest.h"
#include "core/paint/FloatClipRecorder.h"
#include "core/paint/TransformRecorder.h"
#include "core/svg/SVGDocumentExtensions.h"
#include "core/svg/SVGFEImageElement.h"
#include "core/svg/SVGImageElement.h"
#include "core/svg/SVGSVGElement.h"
#include "core/svg/animation/SMILTimeContainer.h"
#include "core/svg/graphics/SVGImageChromeClient.h"
#include "platform/EventDispatchForbiddenScope.h"
#include "platform/LengthFunctions.h"
#include "platform/ScriptForbiddenScope.h"
#include "platform/TraceEvent.h"
#include "platform/geometry/IntRect.h"
#include "platform/graphics/GraphicsContext.h"
#include "platform/graphics/ImageBuffer.h"
#include "platform/graphics/ImageObserver.h"
#include "platform/graphics/paint/ClipRecorder.h"
#include "platform/graphics/paint/CullRect.h"
#include "platform/graphics/paint/DrawingRecorder.h"
#include "platform/graphics/paint/SkPictureBuilder.h"
#include "third_party/skia/include/core/SkPicture.h"
#include "wtf/PassRefPtr.h"
namespace blink {
SVGImage::SVGImage(ImageObserver* observer)
: Image(observer), m_hasPendingTimelineRewind(false) {}
SVGImage::~SVGImage() {
if (m_page) {
// Store m_page in a local variable, clearing m_page, so that
// SVGImageChromeClient knows we're destructed.
Page* currentPage = m_page.release();
// Break both the loader and view references to the frame
currentPage->willBeDestroyed();
}
// Verify that page teardown destroyed the Chrome
ASSERT(!m_chromeClient || !m_chromeClient->image());
}
bool SVGImage::isInSVGImage(const Node* node) {
ASSERT(node);
Page* page = node->document().page();
if (!page)
return false;
return page->chromeClient().isSVGImageChromeClient();
}
bool SVGImage::currentFrameHasSingleSecurityOrigin() const {
if (!m_page)
return true;
LocalFrame* frame = toLocalFrame(m_page->mainFrame());
RELEASE_ASSERT(frame->document()->loadEventFinished());
SVGSVGElement* rootElement =
frame->document()->accessSVGExtensions().rootElement();
if (!rootElement)
return true;
// Don't allow foreignObject elements or images that are not known to be
// single-origin since these can leak cross-origin information.
for (Node* node = rootElement; node; node = FlatTreeTraversal::next(*node)) {
if (isSVGForeignObjectElement(*node))
return false;
if (isSVGImageElement(*node)) {
if (!toSVGImageElement(*node).currentFrameHasSingleSecurityOrigin())
return false;
} else if (isSVGFEImageElement(*node)) {
if (!toSVGFEImageElement(*node).currentFrameHasSingleSecurityOrigin())
return false;
}
}
// Because SVG image rendering disallows external resources and links, these
// images effectively are restricted to a single security origin.
return true;
}
static SVGSVGElement* svgRootElement(Page* page) {
if (!page)
return nullptr;
LocalFrame* frame = toLocalFrame(page->mainFrame());
return frame->document()->accessSVGExtensions().rootElement();
}
IntSize SVGImage::containerSize() const {
SVGSVGElement* rootElement = svgRootElement(m_page.get());
if (!rootElement)
return IntSize();
LayoutSVGRoot* layoutObject = toLayoutSVGRoot(rootElement->layoutObject());
if (!layoutObject)
return IntSize();
// If a container size is available it has precedence.
IntSize containerSize = layoutObject->containerSize();
if (!containerSize.isEmpty())
return containerSize;
// Assure that a container size is always given for a non-identity zoom level.
ASSERT(layoutObject->style()->effectiveZoom() == 1);
// No set container size; use concrete object size.
return m_intrinsicSize;
}
static float resolveWidthForRatio(float height,
const FloatSize& intrinsicRatio) {
return height * intrinsicRatio.width() / intrinsicRatio.height();
}
static float resolveHeightForRatio(float width,
const FloatSize& intrinsicRatio) {
return width * intrinsicRatio.height() / intrinsicRatio.width();
}
bool SVGImage::hasIntrinsicDimensions() const {
return !concreteObjectSize(FloatSize()).isEmpty();
}
FloatSize SVGImage::concreteObjectSize(
const FloatSize& defaultObjectSize) const {
SVGSVGElement* svg = svgRootElement(m_page.get());
if (!svg)
return FloatSize();
LayoutSVGRoot* layoutObject = toLayoutSVGRoot(svg->layoutObject());
if (!layoutObject)
return FloatSize();
LayoutReplaced::IntrinsicSizingInfo intrinsicSizingInfo;
layoutObject->computeIntrinsicSizingInfo(intrinsicSizingInfo);
// https://www.w3.org/TR/css3-images/#default-sizing
if (intrinsicSizingInfo.hasWidth && intrinsicSizingInfo.hasHeight)
return intrinsicSizingInfo.size;
if (svg->preserveAspectRatio()->currentValue()->align() ==
SVGPreserveAspectRatio::kSvgPreserveaspectratioNone) {
// TODO(davve): The intrinsic aspect ratio is not used to resolve a missing
// intrinsic width or height when preserveAspectRatio is none. It's unclear
// whether this is correct. See crbug.com/584172.
return defaultObjectSize;
}
if (intrinsicSizingInfo.hasWidth) {
if (intrinsicSizingInfo.aspectRatio.isEmpty())
return FloatSize(intrinsicSizingInfo.size.width(),
defaultObjectSize.height());
return FloatSize(intrinsicSizingInfo.size.width(),
resolveHeightForRatio(intrinsicSizingInfo.size.width(),
intrinsicSizingInfo.aspectRatio));
}
if (intrinsicSizingInfo.hasHeight) {
if (intrinsicSizingInfo.aspectRatio.isEmpty())
return FloatSize(defaultObjectSize.width(),
intrinsicSizingInfo.size.height());
return FloatSize(resolveWidthForRatio(intrinsicSizingInfo.size.height(),
intrinsicSizingInfo.aspectRatio),
intrinsicSizingInfo.size.height());
}
if (!intrinsicSizingInfo.aspectRatio.isEmpty()) {
// "A contain constraint is resolved by setting the concrete object size to
// the largest rectangle that has the object's intrinsic aspect ratio and
// additionally has neither width nor height larger than the constraint
// rectangle's width and height, respectively."
float solutionWidth = resolveWidthForRatio(defaultObjectSize.height(),
intrinsicSizingInfo.aspectRatio);
if (solutionWidth <= defaultObjectSize.width())
return FloatSize(solutionWidth, defaultObjectSize.height());
float solutionHeight = resolveHeightForRatio(
defaultObjectSize.width(), intrinsicSizingInfo.aspectRatio);
return FloatSize(defaultObjectSize.width(), solutionHeight);
}
return defaultObjectSize;
}
void SVGImage::drawForContainer(SkCanvas* canvas,
const SkPaint& paint,
const FloatSize containerSize,
float zoom,
const FloatRect& dstRect,
const FloatRect& srcRect,
const KURL& url) {
if (!m_page)
return;
// Temporarily disable the image observer to prevent changeInRect() calls due
// re-laying out the image.
ImageObserverDisabler imageObserverDisabler(this);
IntSize roundedContainerSize = roundedIntSize(containerSize);
if (SVGSVGElement* rootElement = svgRootElement(m_page.get())) {
if (LayoutSVGRoot* layoutObject =
toLayoutSVGRoot(rootElement->layoutObject()))
layoutObject->setContainerSize(roundedContainerSize);
}
FloatRect scaledSrc = srcRect;
scaledSrc.scale(1 / zoom);
// Compensate for the container size rounding by adjusting the source rect.
FloatSize adjustedSrcSize = scaledSrc.size();
adjustedSrcSize.scale(roundedContainerSize.width() / containerSize.width(),
roundedContainerSize.height() / containerSize.height());
scaledSrc.setSize(adjustedSrcSize);
drawInternal(canvas, paint, dstRect, scaledSrc, DoNotRespectImageOrientation,
ClampImageToSourceRect, url);
}
sk_sp<SkImage> SVGImage::imageForCurrentFrame() {
return imageForCurrentFrameForContainer(KURL(), size());
}
void SVGImage::drawPatternForContainer(GraphicsContext& context,
const FloatSize containerSize,
float zoom,
const FloatRect& srcRect,
const FloatSize& tileScale,
const FloatPoint& phase,
SkXfermode::Mode compositeOp,
const FloatRect& dstRect,
const FloatSize& repeatSpacing,
const KURL& url) {
// Tile adjusted for scaling/stretch.
FloatRect tile(srcRect);
tile.scale(tileScale.width(), tileScale.height());
// Expand the tile to account for repeat spacing.
FloatRect spacedTile(tile);
spacedTile.expand(FloatSize(repeatSpacing));
SkPictureBuilder patternPicture(spacedTile, nullptr, &context);
{
DrawingRecorder patternPictureRecorder(
patternPicture.context(), patternPicture, DisplayItem::Type::kSVGImage,
spacedTile);
// When generating an expanded tile, make sure we don't draw into the
// spacing area.
if (tile != spacedTile)
patternPicture.context().clip(tile);
SkPaint paint;
drawForContainer(patternPicture.context().canvas(), paint, containerSize,
zoom, tile, srcRect, url);
}
sk_sp<SkPicture> tilePicture = patternPicture.endRecording();
SkMatrix patternTransform;
patternTransform.setTranslate(phase.x() + spacedTile.x(),
phase.y() + spacedTile.y());
SkPaint paint;
paint.setShader(SkShader::MakePictureShader(
std::move(tilePicture), SkShader::kRepeat_TileMode,
SkShader::kRepeat_TileMode, &patternTransform, nullptr));
paint.setXfermodeMode(compositeOp);
paint.setColorFilter(sk_ref_sp(context.getColorFilter()));
context.drawRect(dstRect, paint);
}
sk_sp<SkImage> SVGImage::imageForCurrentFrameForContainer(
const KURL& url,
const IntSize& containerSize) {
if (!m_page)
return nullptr;
const FloatRect containerRect((FloatPoint()), FloatSize(containerSize));
SkPictureRecorder recorder;
SkCanvas* canvas = recorder.beginRecording(containerRect);
drawForContainer(canvas, SkPaint(), containerRect.size(), 1, containerRect,
containerRect, url);
return SkImage::MakeFromPicture(
recorder.finishRecordingAsPicture(),
SkISize::Make(containerSize.width(), containerSize.height()), nullptr,
nullptr);
}
static bool drawNeedsLayer(const SkPaint& paint) {
if (SkColorGetA(paint.getColor()) < 255)
return true;
SkXfermode::Mode xfermode;
if (SkXfermode::AsMode(paint.getXfermode(), &xfermode)) {
if (xfermode != SkXfermode::kSrcOver_Mode)
return true;
}
return false;
}
void SVGImage::draw(SkCanvas* canvas,
const SkPaint& paint,
const FloatRect& dstRect,
const FloatRect& srcRect,
RespectImageOrientationEnum shouldRespectImageOrientation,
ImageClampingMode clampMode) {
if (!m_page)
return;
drawInternal(canvas, paint, dstRect, srcRect, shouldRespectImageOrientation,
clampMode, KURL());
}
void SVGImage::drawInternal(SkCanvas* canvas,
const SkPaint& paint,
const FloatRect& dstRect,
const FloatRect& srcRect,
RespectImageOrientationEnum,
ImageClampingMode,
const KURL& url) {
DCHECK(m_page);
FrameView* view = toLocalFrame(m_page->mainFrame())->view();
view->resize(containerSize());
// Always call processUrlFragment, even if the url is empty, because
// there may have been a previous url/fragment that needs to be reset.
view->processUrlFragment(url);
// If the image was reset, we need to rewind the timeline back to 0. This
// needs to be done before painting, or else we wouldn't get the correct
// reset semantics (we'd paint the "last" frame rather than the one at
// time=0.) The reason we do this here and not in resetAnimation() is to
// avoid setting timers from the latter.
flushPendingTimelineRewind();
SkPictureBuilder imagePicture(dstRect);
{
ClipRecorder clipRecorder(imagePicture.context(), imagePicture,
DisplayItem::kClipNodeImage,
enclosingIntRect(dstRect));
// We can only draw the entire frame, clipped to the rect we want. So
// compute where the top left of the image would be if we were drawing
// without clipping, and translate accordingly.
FloatSize scale(dstRect.width() / srcRect.width(),
dstRect.height() / srcRect.height());
FloatSize topLeftOffset(srcRect.location().x() * scale.width(),
srcRect.location().y() * scale.height());
FloatPoint destOffset = dstRect.location() - topLeftOffset;
AffineTransform transform =
AffineTransform::translation(destOffset.x(), destOffset.y());
transform.scale(scale.width(), scale.height());
TransformRecorder transformRecorder(imagePicture.context(), imagePicture,
transform);
view->updateAllLifecyclePhasesExceptPaint();
view->paint(imagePicture.context(), CullRect(enclosingIntRect(srcRect)));
ASSERT(!view->needsLayout());
}
{
SkAutoCanvasRestore ar(canvas, false);
if (drawNeedsLayer(paint)) {
SkRect layerRect = dstRect;
canvas->saveLayer(&layerRect, &paint);
}
sk_sp<const SkPicture> recording = imagePicture.endRecording();
canvas->drawPicture(recording.get());
}
if (getImageObserver())
getImageObserver()->didDraw(this);
// Start any (SMIL) animations if needed. This will restart or continue
// animations if preceded by calls to resetAnimation or stopAnimation
// respectively.
startAnimation();
}
LayoutReplaced* SVGImage::embeddedReplacedContent() const {
SVGSVGElement* rootElement = svgRootElement(m_page.get());
if (!rootElement)
return nullptr;
return toLayoutSVGRoot(rootElement->layoutObject());
}
void SVGImage::scheduleTimelineRewind() {
m_hasPendingTimelineRewind = true;
}
void SVGImage::flushPendingTimelineRewind() {
if (!m_hasPendingTimelineRewind)
return;
if (SVGSVGElement* rootElement = svgRootElement(m_page.get()))
rootElement->setCurrentTime(0);
m_hasPendingTimelineRewind = false;
}
// FIXME: support CatchUpAnimation = CatchUp.
void SVGImage::startAnimation(CatchUpAnimation) {
SVGSVGElement* rootElement = svgRootElement(m_page.get());
if (!rootElement)
return;
m_chromeClient->resumeAnimation();
if (rootElement->animationsPaused())
rootElement->unpauseAnimations();
}
void SVGImage::stopAnimation() {
SVGSVGElement* rootElement = svgRootElement(m_page.get());
if (!rootElement)
return;
m_chromeClient->suspendAnimation();
rootElement->pauseAnimations();
}
void SVGImage::resetAnimation() {
SVGSVGElement* rootElement = svgRootElement(m_page.get());
if (!rootElement)
return;
m_chromeClient->suspendAnimation();
rootElement->pauseAnimations();
scheduleTimelineRewind();
}
bool SVGImage::hasAnimations() const {
SVGSVGElement* rootElement = svgRootElement(m_page.get());
if (!rootElement)
return false;
return rootElement->timeContainer()->hasAnimations() ||
toLocalFrame(m_page->mainFrame())
->document()
->timeline()
.hasPendingUpdates();
}
void SVGImage::serviceAnimations(double monotonicAnimationStartTime) {
// If none of our observers (sic!) are visible, or for some other reason
// does not want us to keep running animations, stop them until further
// notice (next paint.)
if (getImageObserver()->shouldPauseAnimation(this)) {
stopAnimation();
return;
}
// serviceScriptedAnimations runs requestAnimationFrame callbacks, but SVG
// images can't have any so we assert there's no script.
ScriptForbiddenScope forbidScript;
// The calls below may trigger GCs, so set up the required persistent
// reference on the ImageResource which owns this SVGImage. By transitivity,
// that will keep the associated SVGImageChromeClient object alive.
Persistent<ImageObserver> protect(getImageObserver());
m_page->animator().serviceScriptedAnimations(monotonicAnimationStartTime);
m_page->animator().updateAllLifecyclePhases(
*toLocalFrame(m_page->mainFrame()));
}
void SVGImage::advanceAnimationForTesting() {
if (SVGSVGElement* rootElement = svgRootElement(m_page.get())) {
rootElement->timeContainer()->advanceFrameForTesting();
// The following triggers animation updates which can issue a new draw
// but will not permanently change the animation timeline.
// TODO(pdr): Actually advance the document timeline so CSS animations
// can be properly tested.
m_page->animator().serviceScriptedAnimations(rootElement->getCurrentTime());
getImageObserver()->animationAdvanced(this);
}
}
SVGImageChromeClient& SVGImage::chromeClientForTesting() {
return *m_chromeClient;
}
void SVGImage::updateUseCounters(const Document& document) const {
if (SVGSVGElement* rootElement = svgRootElement(m_page.get())) {
if (rootElement->timeContainer()->hasAnimations())
UseCounter::count(document,
UseCounter::SVGSMILAnimationInImageRegardlessOfCache);
}
}
Image::SizeAvailability SVGImage::dataChanged(bool allDataReceived) {
TRACE_EVENT0("blink", "SVGImage::dataChanged");
// Don't do anything if is an empty image.
if (!data()->size())
return SizeAvailable;
if (allDataReceived) {
// SVGImage will fire events (and the default C++ handlers run) but doesn't
// actually allow script to run so it's fine to call into it. We allow this
// since it means an SVG data url can synchronously load like other image
// types.
EventDispatchForbiddenScope::AllowUserAgentEvents allowUserAgentEvents;
DEFINE_STATIC_LOCAL(FrameLoaderClient, dummyFrameLoaderClient,
(EmptyFrameLoaderClient::create()));
if (m_page) {
toLocalFrame(m_page->mainFrame())
->loader()
.load(FrameLoadRequest(
0, blankURL(),
SubstituteData(data(), AtomicString("image/svg+xml"),
AtomicString("UTF-8"), KURL(),
ForceSynchronousLoad)));
return SizeAvailable;
}
Page::PageClients pageClients;
fillWithEmptyClients(pageClients);
m_chromeClient = SVGImageChromeClient::create(this);
pageClients.chromeClient = m_chromeClient.get();
// FIXME: If this SVG ends up loading itself, we might leak the world.
// The Cache code does not know about ImageResources holding Frames and
// won't know to break the cycle.
// This will become an issue when SVGImage will be able to load other
// SVGImage objects, but we're safe now, because SVGImage can only be
// loaded by a top-level document.
Page* page;
{
TRACE_EVENT0("blink", "SVGImage::dataChanged::createPage");
page = Page::create(pageClients);
page->settings().setScriptEnabled(false);
page->settings().setPluginsEnabled(false);
page->settings().setAcceleratedCompositingEnabled(false);
// Because this page is detached, it can't get default font settings
// from the embedder. Copy over font settings so we have sensible
// defaults. These settings are fixed and will not update if changed.
if (!Page::ordinaryPages().isEmpty()) {
Settings& defaultSettings =
(*Page::ordinaryPages().begin())->settings();
page->settings().genericFontFamilySettings() =
defaultSettings.genericFontFamilySettings();
page->settings().setMinimumFontSize(defaultSettings.minimumFontSize());
page->settings().setMinimumLogicalFontSize(
defaultSettings.minimumLogicalFontSize());
page->settings().setDefaultFontSize(defaultSettings.defaultFontSize());
page->settings().setDefaultFixedFontSize(
defaultSettings.defaultFixedFontSize());
}
}
LocalFrame* frame = nullptr;
{
TRACE_EVENT0("blink", "SVGImage::dataChanged::createFrame");
frame =
LocalFrame::create(&dummyFrameLoaderClient, &page->frameHost(), 0);
frame->setView(FrameView::create(frame));
frame->init();
}
FrameLoader& loader = frame->loader();
loader.forceSandboxFlags(SandboxAll);
frame->view()->setScrollbarsSuppressed(true);
// SVG Images will always synthesize a viewBox, if it's not available, and
// thus never see scrollbars.
frame->view()->setCanHaveScrollbars(false);
// SVG Images are transparent.
frame->view()->setTransparent(true);
m_page = page;
TRACE_EVENT0("blink", "SVGImage::dataChanged::load");
loader.load(FrameLoadRequest(
0, blankURL(),
SubstituteData(data(), AtomicString("image/svg+xml"),
AtomicString("UTF-8"), KURL(), ForceSynchronousLoad)));
// Set the concrete object size before a container size is available.
m_intrinsicSize = roundedIntSize(concreteObjectSize(FloatSize(
LayoutReplaced::defaultWidth, LayoutReplaced::defaultHeight)));
}
return m_page ? SizeAvailable : SizeUnavailable;
}
String SVGImage::filenameExtension() const {
return "svg";
}
} // namespace blink