1/*
2 * Copyright (C) 2004, 2005, 2007, 2008 Nikolas Zimmermann <zimmermann@kde.org>
3 * Copyright (C) 2004, 2005, 2006, 2007, 2008 Rob Buis <buis@kde.org>
4 * Copyright (C) Research In Motion Limited 2009-2010. All rights reserved.
5 * Copyright (C) 2011 Dirk Schulze <krit@webkit.org>
6 *
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Library General Public
9 * License as published by the Free Software Foundation; either
10 * version 2 of the License, or (at your option) any later version.
11 *
12 * This library is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Library General Public License for more details.
16 *
17 * You should have received a copy of the GNU Library General Public License
18 * along with this library; see the file COPYING.LIB. If not, write to
19 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20 * Boston, MA 02110-1301, USA.
21 */
22
23#include "config.h"
24#include "RenderSVGResourceClipper.h"
25
26#include "ElementIterator.h"
27#include "Frame.h"
28#include "FrameView.h"
29#include "HitTestRequest.h"
30#include "HitTestResult.h"
31#include "IntRect.h"
32#include "RenderObject.h"
33#include "RenderStyle.h"
34#include "RenderView.h"
35#include "SVGNames.h"
36#include "SVGRenderingContext.h"
37#include "SVGResources.h"
38#include "SVGResourcesCache.h"
39#include "SVGUseElement.h"
40#include <wtf/IsoMallocInlines.h>
41
42namespace WebCore {
43
44WTF_MAKE_ISO_ALLOCATED_IMPL(RenderSVGResourceClipper);
45
46RenderSVGResourceClipper::RenderSVGResourceClipper(SVGClipPathElement& element, RenderStyle&& style)
47 : RenderSVGResourceContainer(element, WTFMove(style))
48{
49}
50
51RenderSVGResourceClipper::~RenderSVGResourceClipper() = default;
52
53void RenderSVGResourceClipper::removeAllClientsFromCache(bool markForInvalidation)
54{
55 m_clipBoundaries = FloatRect();
56 m_clipper.clear();
57
58 markAllClientsForInvalidation(markForInvalidation ? LayoutAndBoundariesInvalidation : ParentOnlyInvalidation);
59}
60
61void RenderSVGResourceClipper::removeClientFromCache(RenderElement& client, bool markForInvalidation)
62{
63 m_clipper.remove(&client);
64
65 markClientForInvalidation(client, markForInvalidation ? BoundariesInvalidation : ParentOnlyInvalidation);
66}
67
68bool RenderSVGResourceClipper::applyResource(RenderElement& renderer, const RenderStyle&, GraphicsContext*& context, OptionSet<RenderSVGResourceMode> resourceMode)
69{
70 ASSERT(context);
71 ASSERT_UNUSED(resourceMode, !resourceMode);
72
73 return applyClippingToContext(renderer, renderer.objectBoundingBox(), renderer.repaintRectInLocalCoordinates(), *context);
74}
75
76bool RenderSVGResourceClipper::pathOnlyClipping(GraphicsContext& context, const AffineTransform& animatedLocalTransform, const FloatRect& objectBoundingBox)
77{
78 // If the current clip-path gets clipped itself, we have to fallback to masking.
79 if (!style().svgStyle().clipperResource().isEmpty())
80 return false;
81 WindRule clipRule = WindRule::NonZero;
82 Path clipPath = Path();
83
84 // If clip-path only contains one visible shape or path, we can use path-based clipping. Invisible
85 // shapes don't affect the clipping and can be ignored. If clip-path contains more than one
86 // visible shape, the additive clipping may not work, caused by the clipRule. EvenOdd
87 // as well as NonZero can cause self-clipping of the elements.
88 // See also http://www.w3.org/TR/SVG/painting.html#FillRuleProperty
89 for (Node* childNode = clipPathElement().firstChild(); childNode; childNode = childNode->nextSibling()) {
90 RenderObject* renderer = childNode->renderer();
91 if (!renderer)
92 continue;
93 // Only shapes or paths are supported for direct clipping. We need to fallback to masking for texts.
94 if (renderer->isSVGText())
95 return false;
96 if (!childNode->isSVGElement() || !downcast<SVGElement>(*childNode).isSVGGraphicsElement())
97 continue;
98 SVGGraphicsElement& styled = downcast<SVGGraphicsElement>(*childNode);
99 const RenderStyle& style = renderer->style();
100 if (style.display() == DisplayType::None || style.visibility() != Visibility::Visible)
101 continue;
102 const SVGRenderStyle& svgStyle = style.svgStyle();
103 // Current shape in clip-path gets clipped too. Fallback to masking.
104 if (!svgStyle.clipperResource().isEmpty())
105 return false;
106 // Fallback to masking, if there is more than one clipping path.
107 if (clipPath.isEmpty()) {
108 clipPath = styled.toClipPath();
109 clipRule = svgStyle.clipRule();
110 } else
111 return false;
112 }
113 // Only one visible shape/path was found. Directly continue clipping and transform the content to userspace if necessary.
114 if (clipPathElement().clipPathUnits() == SVGUnitTypes::SVG_UNIT_TYPE_OBJECTBOUNDINGBOX) {
115 AffineTransform transform;
116 transform.translate(objectBoundingBox.location());
117 transform.scale(objectBoundingBox.size());
118 clipPath.transform(transform);
119 }
120
121 // Transform path by animatedLocalTransform.
122 clipPath.transform(animatedLocalTransform);
123
124 // The SVG specification wants us to clip everything, if clip-path doesn't have a child.
125 if (clipPath.isEmpty())
126 clipPath.addRect(FloatRect());
127 context.clipPath(clipPath, clipRule);
128 return true;
129}
130
131bool RenderSVGResourceClipper::applyClippingToContext(RenderElement& renderer, const FloatRect& objectBoundingBox, const FloatRect& repaintRect, GraphicsContext& context)
132{
133 ClipperMaskImage& clipperMaskImage = addRendererToClipper(renderer);
134 bool shouldCreateClipperMaskImage = !clipperMaskImage;
135
136 AffineTransform animatedLocalTransform = clipPathElement().animatedLocalTransform();
137
138 if (shouldCreateClipperMaskImage && pathOnlyClipping(context, animatedLocalTransform, objectBoundingBox))
139 return true;
140
141 AffineTransform absoluteTransform = SVGRenderingContext::calculateTransformationToOutermostCoordinateSystem(renderer);
142
143 if (shouldCreateClipperMaskImage && !repaintRect.isEmpty()) {
144 // FIXME (149469): This image buffer should not be unconditionally unaccelerated. Making it match the context breaks nested clipping, though.
145 clipperMaskImage = SVGRenderingContext::createImageBuffer(repaintRect, absoluteTransform, ColorSpaceSRGB, Unaccelerated, &context);
146 if (!clipperMaskImage)
147 return false;
148
149 GraphicsContext& maskContext = clipperMaskImage->context();
150 maskContext.concatCTM(animatedLocalTransform);
151
152 // clipPath can also be clipped by another clipPath.
153 auto* resources = SVGResourcesCache::cachedResourcesForRenderer(*this);
154 RenderSVGResourceClipper* clipper;
155 bool succeeded;
156 if (resources && (clipper = resources->clipper())) {
157 GraphicsContextStateSaver stateSaver(maskContext);
158
159 if (!clipper->applyClippingToContext(*this, objectBoundingBox, repaintRect, maskContext))
160 return false;
161
162 succeeded = drawContentIntoMaskImage(clipperMaskImage, objectBoundingBox);
163 // The context restore applies the clipping on non-CG platforms.
164 } else
165 succeeded = drawContentIntoMaskImage(clipperMaskImage, objectBoundingBox);
166
167 if (!succeeded)
168 clipperMaskImage.reset();
169 }
170
171 if (!clipperMaskImage)
172 return false;
173
174 SVGRenderingContext::clipToImageBuffer(context, absoluteTransform, repaintRect, clipperMaskImage, shouldCreateClipperMaskImage);
175 return true;
176}
177
178bool RenderSVGResourceClipper::drawContentIntoMaskImage(const ClipperMaskImage& clipperMaskImage, const FloatRect& objectBoundingBox)
179{
180 ASSERT(clipperMaskImage);
181
182 GraphicsContext& maskContext = clipperMaskImage->context();
183
184 AffineTransform maskContentTransformation;
185 if (clipPathElement().clipPathUnits() == SVGUnitTypes::SVG_UNIT_TYPE_OBJECTBOUNDINGBOX) {
186 maskContentTransformation.translate(objectBoundingBox.location());
187 maskContentTransformation.scale(objectBoundingBox.size());
188 maskContext.concatCTM(maskContentTransformation);
189 }
190
191 // Switch to a paint behavior where all children of this <clipPath> will be rendered using special constraints:
192 // - fill-opacity/stroke-opacity/opacity set to 1
193 // - masker/filter not applied when rendering the children
194 // - fill is set to the initial fill paint server (solid, black)
195 // - stroke is set to the initial stroke paint server (none)
196 auto oldBehavior = view().frameView().paintBehavior();
197 view().frameView().setPaintBehavior(oldBehavior | PaintBehavior::RenderingSVGMask);
198
199 // Draw all clipPath children into a global mask.
200 for (auto& child : childrenOfType<SVGElement>(clipPathElement())) {
201 auto renderer = child.renderer();
202 if (!renderer)
203 continue;
204 if (renderer->needsLayout()) {
205 view().frameView().setPaintBehavior(oldBehavior);
206 return false;
207 }
208 const RenderStyle& style = renderer->style();
209 if (style.display() == DisplayType::None || style.visibility() != Visibility::Visible)
210 continue;
211
212 WindRule newClipRule = style.svgStyle().clipRule();
213 bool isUseElement = child.hasTagName(SVGNames::useTag);
214 if (isUseElement) {
215 SVGUseElement& useElement = downcast<SVGUseElement>(child);
216 renderer = useElement.rendererClipChild();
217 if (!renderer)
218 continue;
219 if (!useElement.hasAttributeWithoutSynchronization(SVGNames::clip_ruleAttr))
220 newClipRule = renderer->style().svgStyle().clipRule();
221 }
222
223 // Only shapes, paths and texts are allowed for clipping.
224 if (!renderer->isSVGShape() && !renderer->isSVGText())
225 continue;
226
227 maskContext.setFillRule(newClipRule);
228
229 // In the case of a <use> element, we obtained its renderere above, to retrieve its clipRule.
230 // We have to pass the <use> renderer itself to renderSubtreeToImageBuffer() to apply it's x/y/transform/etc. values when rendering.
231 // So if isUseElement is true, refetch the childNode->renderer(), as renderer got overridden above.
232 SVGRenderingContext::renderSubtreeToImageBuffer(clipperMaskImage.get(), isUseElement ? *child.renderer() : *renderer, maskContentTransformation);
233 }
234
235 view().frameView().setPaintBehavior(oldBehavior);
236 return true;
237}
238
239void RenderSVGResourceClipper::calculateClipContentRepaintRect()
240{
241 // This is a rough heuristic to appraise the clip size and doesn't consider clip on clip.
242 for (Node* childNode = clipPathElement().firstChild(); childNode; childNode = childNode->nextSibling()) {
243 RenderObject* renderer = childNode->renderer();
244 if (!childNode->isSVGElement() || !renderer)
245 continue;
246 if (!renderer->isSVGShape() && !renderer->isSVGText() && !childNode->hasTagName(SVGNames::useTag))
247 continue;
248 const RenderStyle& style = renderer->style();
249 if (style.display() == DisplayType::None || style.visibility() != Visibility::Visible)
250 continue;
251 m_clipBoundaries.unite(renderer->localToParentTransform().mapRect(renderer->repaintRectInLocalCoordinates()));
252 }
253 m_clipBoundaries = clipPathElement().animatedLocalTransform().mapRect(m_clipBoundaries);
254}
255
256ClipperMaskImage& RenderSVGResourceClipper::addRendererToClipper(const RenderObject& object)
257{
258 return m_clipper.add(&object, ClipperMaskImage()).iterator->value;
259}
260
261bool RenderSVGResourceClipper::hitTestClipContent(const FloatRect& objectBoundingBox, const FloatPoint& nodeAtPoint)
262{
263 FloatPoint point = nodeAtPoint;
264 if (!SVGRenderSupport::pointInClippingArea(*this, point))
265 return false;
266
267 if (clipPathElement().clipPathUnits() == SVGUnitTypes::SVG_UNIT_TYPE_OBJECTBOUNDINGBOX) {
268 AffineTransform transform;
269 transform.translate(objectBoundingBox.location());
270 transform.scale(objectBoundingBox.size());
271 point = transform.inverse().valueOr(AffineTransform()).mapPoint(point);
272 }
273
274 point = clipPathElement().animatedLocalTransform().inverse().valueOr(AffineTransform()).mapPoint(point);
275
276 for (Node* childNode = clipPathElement().firstChild(); childNode; childNode = childNode->nextSibling()) {
277 RenderObject* renderer = childNode->renderer();
278 if (!childNode->isSVGElement() || !renderer)
279 continue;
280 if (!renderer->isSVGShape() && !renderer->isSVGText() && !childNode->hasTagName(SVGNames::useTag))
281 continue;
282 IntPoint hitPoint;
283 HitTestResult result(hitPoint);
284 if (renderer->nodeAtFloatPoint(HitTestRequest(HitTestRequest::SVGClipContent | HitTestRequest::DisallowUserAgentShadowContent), result, point, HitTestForeground))
285 return true;
286 }
287
288 return false;
289}
290
291FloatRect RenderSVGResourceClipper::resourceBoundingBox(const RenderObject& object)
292{
293 // Resource was not layouted yet. Give back the boundingBox of the object.
294 if (selfNeedsLayout()) {
295 addRendererToClipper(object);
296 return object.objectBoundingBox();
297 }
298
299 if (m_clipBoundaries.isEmpty())
300 calculateClipContentRepaintRect();
301
302 if (clipPathElement().clipPathUnits() == SVGUnitTypes::SVG_UNIT_TYPE_OBJECTBOUNDINGBOX) {
303 FloatRect objectBoundingBox = object.objectBoundingBox();
304 AffineTransform transform;
305 transform.translate(objectBoundingBox.location());
306 transform.scale(objectBoundingBox.size());
307 return transform.mapRect(m_clipBoundaries);
308 }
309
310 return m_clipBoundaries;
311}
312
313}
314