1/*
2 * Copyright (C) 2006 Eric Seidel <eric@webkit.org>
3 * Copyright (C) 2008-2009, 2015-2016 Apple Inc. All rights reserved.
4 * Copyright (C) Research In Motion Limited 2011. All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
16 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
19 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
23 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28#include "config.h"
29#include "SVGImage.h"
30
31#include "CacheStorageProvider.h"
32#include "Chrome.h"
33#include "CommonVM.h"
34#include "DOMWindow.h"
35#include "DocumentLoader.h"
36#include "EditorClient.h"
37#include "ElementIterator.h"
38#include "Frame.h"
39#include "FrameLoader.h"
40#include "FrameView.h"
41#include "ImageBuffer.h"
42#include "ImageObserver.h"
43#include "IntRect.h"
44#include "JSDOMWindowBase.h"
45#include "LibWebRTCProvider.h"
46#include "Page.h"
47#include "PageConfiguration.h"
48#include "RenderSVGRoot.h"
49#include "RenderStyle.h"
50#include "RenderView.h"
51#include "SVGDocument.h"
52#include "SVGFEImageElement.h"
53#include "SVGForeignObjectElement.h"
54#include "SVGImageClients.h"
55#include "SVGImageElement.h"
56#include "SVGSVGElement.h"
57#include "ScriptDisallowedScope.h"
58#include "Settings.h"
59#include "SocketProvider.h"
60#include <JavaScriptCore/JSCInlines.h>
61#include <JavaScriptCore/JSLock.h>
62#include <wtf/text/TextStream.h>
63
64#if PLATFORM(MAC)
65#include "LocalDefaultSystemAppearance.h"
66#endif
67
68#if USE(DIRECT2D)
69#include "COMPtr.h"
70#include <d2d1.h>
71#endif
72
73namespace WebCore {
74
75SVGImage::SVGImage(ImageObserver& observer)
76 : Image(&observer)
77 , m_startAnimationTimer(*this, &SVGImage::startAnimationTimerFired)
78{
79}
80
81SVGImage::~SVGImage()
82{
83 if (m_page) {
84 ScriptDisallowedScope::DisableAssertionsInScope disabledScope;
85 // Store m_page in a local variable, clearing m_page, so that SVGImageChromeClient knows we're destructed.
86 std::unique_ptr<Page> currentPage = WTFMove(m_page);
87 currentPage->mainFrame().loader().frameDetached(); // Break both the loader and view references to the frame
88 }
89
90 // Verify that page teardown destroyed the Chrome
91 ASSERT(!m_chromeClient || !m_chromeClient->image());
92}
93
94inline RefPtr<SVGSVGElement> SVGImage::rootElement() const
95{
96 if (!m_page)
97 return nullptr;
98 return SVGDocument::rootElement(*m_page->mainFrame().document());
99}
100
101bool SVGImage::hasSingleSecurityOrigin() const
102{
103 auto rootElement = this->rootElement();
104 if (!rootElement)
105 return true;
106
107 // FIXME: Once foreignObject elements within SVG images are updated to not leak cross-origin data
108 // (e.g., visited links, spellcheck) we can remove the SVGForeignObjectElement check here and
109 // research if we can remove the Image::hasSingleSecurityOrigin mechanism entirely.
110 for (auto& element : descendantsOfType<SVGElement>(*rootElement)) {
111 if (is<SVGForeignObjectElement>(element))
112 return false;
113 if (is<SVGImageElement>(element)) {
114 if (!downcast<SVGImageElement>(element).hasSingleSecurityOrigin())
115 return false;
116 } else if (is<SVGFEImageElement>(element)) {
117 if (!downcast<SVGFEImageElement>(element).hasSingleSecurityOrigin())
118 return false;
119 }
120 }
121
122 // Because SVG image rendering disallows external resources and links,
123 // these images effectively are restricted to a single security origin.
124 return true;
125}
126
127void SVGImage::setContainerSize(const FloatSize& size)
128{
129 if (!usesContainerSize())
130 return;
131
132 auto rootElement = this->rootElement();
133 if (!rootElement)
134 return;
135 auto* renderer = downcast<RenderSVGRoot>(rootElement->renderer());
136 if (!renderer)
137 return;
138
139 auto view = makeRefPtr(frameView());
140 view->resize(this->containerSize());
141
142 renderer->setContainerSize(IntSize(size));
143}
144
145IntSize SVGImage::containerSize() const
146{
147 auto rootElement = this->rootElement();
148 if (!rootElement)
149 return IntSize();
150
151 auto* renderer = downcast<RenderSVGRoot>(rootElement->renderer());
152 if (!renderer)
153 return IntSize();
154
155 // If a container size is available it has precedence.
156 IntSize containerSize = renderer->containerSize();
157 if (!containerSize.isEmpty())
158 return containerSize;
159
160 // Assure that a container size is always given for a non-identity zoom level.
161 ASSERT(renderer->style().effectiveZoom() == 1);
162
163 FloatSize currentSize;
164 if (rootElement->hasIntrinsicWidth() && rootElement->hasIntrinsicHeight())
165 currentSize = rootElement->currentViewportSize();
166 else
167 currentSize = rootElement->currentViewBoxRect().size();
168
169 if (!currentSize.isEmpty())
170 return IntSize(static_cast<int>(ceilf(currentSize.width())), static_cast<int>(ceilf(currentSize.height())));
171
172 // As last resort, use CSS default intrinsic size.
173 return IntSize(300, 150);
174}
175
176ImageDrawResult SVGImage::drawForContainer(GraphicsContext& context, const FloatSize containerSize, float containerZoom, const URL& initialFragmentURL, const FloatRect& dstRect,
177 const FloatRect& srcRect, CompositeOperator compositeOp, BlendMode blendMode)
178{
179 if (!m_page)
180 return ImageDrawResult::DidNothing;
181
182 ImageObserver* observer = imageObserver();
183 ASSERT(observer);
184
185 // Temporarily reset image observer, we don't want to receive any changeInRect() calls due to this relayout.
186 setImageObserver(nullptr);
187
188 IntSize roundedContainerSize = roundedIntSize(containerSize);
189 setContainerSize(roundedContainerSize);
190
191 FloatRect scaledSrc = srcRect;
192 scaledSrc.scale(1 / containerZoom);
193
194 // Compensate for the container size rounding by adjusting the source rect.
195 FloatSize adjustedSrcSize = scaledSrc.size();
196 adjustedSrcSize.scale(roundedContainerSize.width() / containerSize.width(), roundedContainerSize.height() / containerSize.height());
197 scaledSrc.setSize(adjustedSrcSize);
198
199 frameView()->scrollToFragment(initialFragmentURL);
200
201 ImageDrawResult result = draw(context, dstRect, scaledSrc, compositeOp, blendMode, DecodingMode::Synchronous, ImageOrientationDescription());
202
203 setImageObserver(observer);
204 return result;
205}
206
207#if USE(CAIRO)
208// Passes ownership of the native image to the caller so NativeImagePtr needs
209// to be a smart pointer type.
210NativeImagePtr SVGImage::nativeImageForCurrentFrame(const GraphicsContext*)
211{
212 if (!m_page)
213 return nullptr;
214
215 // Cairo does not use the accelerated drawing flag, so it's OK to make an unconditionally unaccelerated buffer.
216 std::unique_ptr<ImageBuffer> buffer = ImageBuffer::create(size(), Unaccelerated);
217 if (!buffer) // failed to allocate image
218 return nullptr;
219
220 draw(buffer->context(), rect(), rect(), CompositeSourceOver, BlendMode::Normal, DecodingMode::Synchronous, ImageOrientationDescription());
221
222 // FIXME: WK(Bug 113657): We should use DontCopyBackingStore here.
223 return buffer->copyImage(CopyBackingStore)->nativeImageForCurrentFrame();
224}
225#endif
226
227#if USE(DIRECT2D)
228NativeImagePtr SVGImage::nativeImage(const GraphicsContext* targetContext)
229{
230 ASSERT(targetContext);
231 if (!m_page || !targetContext)
232 return nullptr;
233
234 auto platformContext = targetContext->platformContext();
235 ASSERT(platformContext);
236
237 // Draw the SVG into a bitmap.
238 COMPtr<ID2D1BitmapRenderTarget> nativeImageTarget;
239 HRESULT hr = platformContext->CreateCompatibleRenderTarget(IntSize(rect().size()), &nativeImageTarget);
240 ASSERT(SUCCEEDED(hr));
241
242 GraphicsContext localContext(nativeImageTarget.get());
243
244 draw(localContext, rect(), rect(), CompositeSourceOver, BlendMode::Normal, DecodingMode::Synchronous, ImageOrientationDescription());
245
246 COMPtr<ID2D1Bitmap> nativeImage;
247 hr = nativeImageTarget->GetBitmap(&nativeImage);
248 ASSERT(SUCCEEDED(hr));
249
250 return nativeImage;
251}
252#endif
253
254void SVGImage::drawPatternForContainer(GraphicsContext& context, const FloatSize& containerSize, float containerZoom, const URL& initialFragmentURL, const FloatRect& srcRect,
255 const AffineTransform& patternTransform, const FloatPoint& phase, const FloatSize& spacing, CompositeOperator compositeOp, const FloatRect& dstRect, BlendMode blendMode)
256{
257 FloatRect zoomedContainerRect = FloatRect(FloatPoint(), containerSize);
258 zoomedContainerRect.scale(containerZoom);
259
260 // The ImageBuffer size needs to be scaled to match the final resolution.
261 AffineTransform transform = context.getCTM();
262 FloatSize imageBufferScale = FloatSize(transform.xScale(), transform.yScale());
263 ASSERT(imageBufferScale.width());
264 ASSERT(imageBufferScale.height());
265
266 FloatRect imageBufferSize = zoomedContainerRect;
267 imageBufferSize.scale(imageBufferScale.width(), imageBufferScale.height());
268
269 std::unique_ptr<ImageBuffer> buffer = ImageBuffer::createCompatibleBuffer(expandedIntSize(imageBufferSize.size()), 1, ColorSpaceSRGB, context);
270 if (!buffer) // Failed to allocate buffer.
271 return;
272 drawForContainer(buffer->context(), containerSize, containerZoom, initialFragmentURL, imageBufferSize, zoomedContainerRect, CompositeSourceOver, BlendMode::Normal);
273 if (context.drawLuminanceMask())
274 buffer->convertToLuminanceMask();
275
276 RefPtr<Image> image = ImageBuffer::sinkIntoImage(WTFMove(buffer), PreserveResolution::Yes);
277 if (!image)
278 return;
279
280 // Adjust the source rect and transform due to the image buffer's scaling.
281 FloatRect scaledSrcRect = srcRect;
282 scaledSrcRect.scale(imageBufferScale.width(), imageBufferScale.height());
283 AffineTransform unscaledPatternTransform(patternTransform);
284 unscaledPatternTransform.scale(1 / imageBufferScale.width(), 1 / imageBufferScale.height());
285
286 context.setDrawLuminanceMask(false);
287 image->drawPattern(context, dstRect, scaledSrcRect, unscaledPatternTransform, phase, spacing, compositeOp, blendMode);
288}
289
290ImageDrawResult SVGImage::draw(GraphicsContext& context, const FloatRect& dstRect, const FloatRect& srcRect, CompositeOperator compositeOp, BlendMode blendMode, DecodingMode, ImageOrientationDescription)
291{
292 if (!m_page)
293 return ImageDrawResult::DidNothing;
294
295 auto view = makeRefPtr(frameView());
296 ASSERT(view);
297
298 GraphicsContextStateSaver stateSaver(context);
299 context.setCompositeOperation(compositeOp, blendMode);
300 context.clip(enclosingIntRect(dstRect));
301
302 float alpha = context.alpha();
303 bool compositingRequiresTransparencyLayer = compositeOp != CompositeSourceOver || blendMode != BlendMode::Normal || alpha < 1;
304 if (compositingRequiresTransparencyLayer) {
305 context.beginTransparencyLayer(alpha);
306 context.setCompositeOperation(CompositeSourceOver, BlendMode::Normal);
307 }
308
309 FloatSize scale(dstRect.size() / srcRect.size());
310
311 // We can only draw the entire frame, clipped to the rect we want. So compute where the top left
312 // of the image would be if we were drawing without clipping, and translate accordingly.
313 FloatSize topLeftOffset(srcRect.location().x() * scale.width(), srcRect.location().y() * scale.height());
314 FloatPoint destOffset = dstRect.location() - topLeftOffset;
315
316 context.translate(destOffset);
317 context.scale(scale);
318
319 view->resize(containerSize());
320
321 {
322 ScriptDisallowedScope::DisableAssertionsInScope disabledScope;
323 if (view->needsLayout())
324 view->layoutContext().layout();
325 }
326
327#if PLATFORM(MAC)
328 LocalDefaultSystemAppearance localAppearance(view->useDarkAppearance());
329#endif
330
331 view->paint(context, intersection(context.clipBounds(), enclosingIntRect(srcRect)));
332
333 if (compositingRequiresTransparencyLayer)
334 context.endTransparencyLayer();
335
336 stateSaver.restore();
337
338 if (imageObserver())
339 imageObserver()->didDraw(*this);
340
341 return ImageDrawResult::DidDraw;
342}
343
344RenderBox* SVGImage::embeddedContentBox() const
345{
346 auto rootElement = this->rootElement();
347 if (!rootElement)
348 return nullptr;
349 return downcast<RenderBox>(rootElement->renderer());
350}
351
352FrameView* SVGImage::frameView() const
353{
354 if (!m_page)
355 return nullptr;
356 return m_page->mainFrame().view();
357}
358
359bool SVGImage::hasRelativeWidth() const
360{
361 auto rootElement = this->rootElement();
362 if (!rootElement)
363 return false;
364 return rootElement->intrinsicWidth().isPercentOrCalculated();
365}
366
367bool SVGImage::hasRelativeHeight() const
368{
369 auto rootElement = this->rootElement();
370 if (!rootElement)
371 return false;
372 return rootElement->intrinsicHeight().isPercentOrCalculated();
373}
374
375void SVGImage::computeIntrinsicDimensions(Length& intrinsicWidth, Length& intrinsicHeight, FloatSize& intrinsicRatio)
376{
377 auto rootElement = this->rootElement();
378 if (!rootElement)
379 return;
380
381 intrinsicWidth = rootElement->intrinsicWidth();
382 intrinsicHeight = rootElement->intrinsicHeight();
383 if (rootElement->preserveAspectRatio().align() == SVGPreserveAspectRatioValue::SVG_PRESERVEASPECTRATIO_NONE)
384 return;
385
386 intrinsicRatio = rootElement->viewBox().size();
387 if (intrinsicRatio.isEmpty() && intrinsicWidth.isFixed() && intrinsicHeight.isFixed())
388 intrinsicRatio = FloatSize(floatValueForLength(intrinsicWidth, 0), floatValueForLength(intrinsicHeight, 0));
389}
390
391void SVGImage::startAnimationTimerFired()
392{
393 startAnimation();
394}
395
396void SVGImage::scheduleStartAnimation()
397{
398 auto rootElement = this->rootElement();
399 if (!rootElement || !rootElement->animationsPaused())
400 return;
401 m_startAnimationTimer.startOneShot(0_s);
402}
403
404void SVGImage::startAnimation()
405{
406 auto rootElement = this->rootElement();
407 if (!rootElement || !rootElement->animationsPaused())
408 return;
409 rootElement->unpauseAnimations();
410 rootElement->setCurrentTime(0);
411}
412
413void SVGImage::stopAnimation()
414{
415 m_startAnimationTimer.stop();
416 auto rootElement = this->rootElement();
417 if (!rootElement)
418 return;
419 rootElement->pauseAnimations();
420}
421
422void SVGImage::resetAnimation()
423{
424 stopAnimation();
425}
426
427bool SVGImage::isAnimating() const
428{
429 auto rootElement = this->rootElement();
430 if (!rootElement)
431 return false;
432 return rootElement->hasActiveAnimation();
433}
434
435void SVGImage::reportApproximateMemoryCost() const
436{
437 auto document = makeRefPtr(m_page->mainFrame().document());
438 size_t decodedImageMemoryCost = 0;
439
440 for (RefPtr<Node> node = document; node; node = NodeTraversal::next(*node))
441 decodedImageMemoryCost += node->approximateMemoryCost();
442
443 JSC::VM& vm = commonVM();
444 JSC::JSLockHolder lock(vm);
445 // FIXME: Adopt reportExtraMemoryVisited, and switch to reportExtraMemoryAllocated.
446 // https://bugs.webkit.org/show_bug.cgi?id=142595
447 vm.heap.deprecatedReportExtraMemory(decodedImageMemoryCost + data()->size());
448}
449
450EncodedDataStatus SVGImage::dataChanged(bool allDataReceived)
451{
452 // Don't do anything; it is an empty image.
453 if (!data()->size())
454 return EncodedDataStatus::Complete;
455
456 if (allDataReceived) {
457 auto pageConfiguration = pageConfigurationWithEmptyClients();
458 m_chromeClient = std::make_unique<SVGImageChromeClient>(this);
459 pageConfiguration.chromeClient = m_chromeClient.get();
460
461 // FIXME: If this SVG ends up loading itself, we might leak the world.
462 // The Cache code does not know about CachedImages holding Frames and
463 // won't know to break the cycle.
464 // This will become an issue when SVGImage will be able to load other
465 // SVGImage objects, but we're safe now, because SVGImage can only be
466 // loaded by a top-level document.
467 m_page = std::make_unique<Page>(WTFMove(pageConfiguration));
468 m_page->settings().setMediaEnabled(false);
469 m_page->settings().setScriptEnabled(false);
470 m_page->settings().setPluginsEnabled(false);
471 m_page->settings().setAcceleratedCompositingEnabled(false);
472 m_page->settings().setShouldAllowUserInstalledFonts(false);
473
474 Frame& frame = m_page->mainFrame();
475 frame.setView(FrameView::create(frame));
476 frame.init();
477 FrameLoader& loader = frame.loader();
478 loader.forceSandboxFlags(SandboxAll);
479
480 frame.view()->setCanHaveScrollbars(false); // SVG Images will always synthesize a viewBox, if it's not available, and thus never see scrollbars.
481 frame.view()->setTransparent(true); // SVG Images are transparent.
482
483 ASSERT(loader.activeDocumentLoader()); // DocumentLoader should have been created by frame->init().
484 loader.activeDocumentLoader()->writer().setMIMEType("image/svg+xml");
485 loader.activeDocumentLoader()->writer().begin(URL()); // create the empty document
486 loader.activeDocumentLoader()->writer().addData(data()->data(), data()->size());
487 loader.activeDocumentLoader()->writer().end();
488
489 frame.document()->updateLayoutIgnorePendingStylesheets();
490
491 // Set the intrinsic size before a container size is available.
492 m_intrinsicSize = containerSize();
493 reportApproximateMemoryCost();
494 }
495
496 return m_page ? EncodedDataStatus::Complete : EncodedDataStatus::Unknown;
497}
498
499String SVGImage::filenameExtension() const
500{
501 return "svg"_s;
502}
503
504bool isInSVGImage(const Element* element)
505{
506 ASSERT(element);
507
508 Page* page = element->document().page();
509 if (!page)
510 return false;
511
512 return page->chrome().client().isSVGImageChromeClient();
513}
514
515}
516