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 | |
73 | namespace WebCore { |
74 | |
75 | SVGImage::SVGImage(ImageObserver& observer) |
76 | : Image(&observer) |
77 | , m_startAnimationTimer(*this, &SVGImage::startAnimationTimerFired) |
78 | { |
79 | } |
80 | |
81 | SVGImage::~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 | |
94 | inline RefPtr<SVGSVGElement> SVGImage::rootElement() const |
95 | { |
96 | if (!m_page) |
97 | return nullptr; |
98 | return SVGDocument::rootElement(*m_page->mainFrame().document()); |
99 | } |
100 | |
101 | bool 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 | |
127 | void 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 | |
145 | IntSize 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 | |
176 | ImageDrawResult 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. |
210 | NativeImagePtr 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) |
228 | NativeImagePtr 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 | |
254 | void 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 | |
290 | ImageDrawResult 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 | |
344 | RenderBox* SVGImage::embeddedContentBox() const |
345 | { |
346 | auto rootElement = this->rootElement(); |
347 | if (!rootElement) |
348 | return nullptr; |
349 | return downcast<RenderBox>(rootElement->renderer()); |
350 | } |
351 | |
352 | FrameView* SVGImage::frameView() const |
353 | { |
354 | if (!m_page) |
355 | return nullptr; |
356 | return m_page->mainFrame().view(); |
357 | } |
358 | |
359 | bool SVGImage::hasRelativeWidth() const |
360 | { |
361 | auto rootElement = this->rootElement(); |
362 | if (!rootElement) |
363 | return false; |
364 | return rootElement->intrinsicWidth().isPercentOrCalculated(); |
365 | } |
366 | |
367 | bool SVGImage::hasRelativeHeight() const |
368 | { |
369 | auto rootElement = this->rootElement(); |
370 | if (!rootElement) |
371 | return false; |
372 | return rootElement->intrinsicHeight().isPercentOrCalculated(); |
373 | } |
374 | |
375 | void 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 | |
391 | void SVGImage::startAnimationTimerFired() |
392 | { |
393 | startAnimation(); |
394 | } |
395 | |
396 | void SVGImage::scheduleStartAnimation() |
397 | { |
398 | auto rootElement = this->rootElement(); |
399 | if (!rootElement || !rootElement->animationsPaused()) |
400 | return; |
401 | m_startAnimationTimer.startOneShot(0_s); |
402 | } |
403 | |
404 | void SVGImage::startAnimation() |
405 | { |
406 | auto rootElement = this->rootElement(); |
407 | if (!rootElement || !rootElement->animationsPaused()) |
408 | return; |
409 | rootElement->unpauseAnimations(); |
410 | rootElement->setCurrentTime(0); |
411 | } |
412 | |
413 | void SVGImage::stopAnimation() |
414 | { |
415 | m_startAnimationTimer.stop(); |
416 | auto rootElement = this->rootElement(); |
417 | if (!rootElement) |
418 | return; |
419 | rootElement->pauseAnimations(); |
420 | } |
421 | |
422 | void SVGImage::resetAnimation() |
423 | { |
424 | stopAnimation(); |
425 | } |
426 | |
427 | bool SVGImage::isAnimating() const |
428 | { |
429 | auto rootElement = this->rootElement(); |
430 | if (!rootElement) |
431 | return false; |
432 | return rootElement->hasActiveAnimation(); |
433 | } |
434 | |
435 | void 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 | |
450 | EncodedDataStatus 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 | |
499 | String SVGImage::filenameExtension() const |
500 | { |
501 | return "svg"_s ; |
502 | } |
503 | |
504 | bool 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 | |