| /* |
| * Copyright (C) 2004, 2005, 2007, 2008 Nikolas Zimmermann <zimmermann@kde.org> |
| * Copyright (C) 2004, 2005, 2006, 2007, 2008 Rob Buis <buis@kde.org> |
| * Copyright (C) Research In Motion Limited 2009-2010. All rights reserved. |
| * Copyright (C) 2011 Dirk Schulze <krit@webkit.org> |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Library General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public License |
| * along with this library; see the file COPYING.LIB. If not, write to |
| * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| #include "third_party/blink/renderer/core/layout/svg/layout_svg_resource_clipper.h" |
| |
| #include "third_party/blink/renderer/core/dom/element_traversal.h" |
| #include "third_party/blink/renderer/core/layout/hit_test_result.h" |
| #include "third_party/blink/renderer/core/layout/layout_box_model_object.h" |
| #include "third_party/blink/renderer/core/layout/svg/svg_layout_support.h" |
| #include "third_party/blink/renderer/core/paint/paint_info.h" |
| #include "third_party/blink/renderer/core/svg/svg_clip_path_element.h" |
| #include "third_party/blink/renderer/core/svg/svg_geometry_element.h" |
| #include "third_party/blink/renderer/core/svg/svg_use_element.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/paint_record.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/paint_record_builder.h" |
| #include "third_party/skia/include/pathops/SkPathOps.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| enum class ClipStrategy { kNone, kMask, kPath }; |
| |
| ClipStrategy ModifyStrategyForClipPath(const ComputedStyle& style, |
| ClipStrategy strategy) { |
| // If the shape in the clip-path gets clipped too then fallback to masking. |
| if (strategy != ClipStrategy::kPath || !style.ClipPath()) |
| return strategy; |
| return ClipStrategy::kMask; |
| } |
| |
| ClipStrategy DetermineClipStrategy(const SVGGraphicsElement& element) { |
| const LayoutObject* layout_object = element.GetLayoutObject(); |
| if (!layout_object) |
| return ClipStrategy::kNone; |
| const ComputedStyle& style = layout_object->StyleRef(); |
| if (style.Display() == EDisplay::kNone || |
| style.Visibility() != EVisibility::kVisible) |
| return ClipStrategy::kNone; |
| ClipStrategy strategy = ClipStrategy::kNone; |
| // Only shapes, paths and texts are allowed for clipping. |
| if (layout_object->IsSVGShape()) { |
| strategy = ClipStrategy::kPath; |
| } else if (layout_object->IsSVGText()) { |
| // Text requires masking. |
| strategy = ClipStrategy::kMask; |
| } |
| return ModifyStrategyForClipPath(style, strategy); |
| } |
| |
| ClipStrategy DetermineClipStrategy(const SVGElement& element) { |
| // <use> within <clipPath> have a restricted content model. |
| // (https://drafts.fxtf.org/css-masking/#ClipPathElement) |
| if (IsSVGUseElement(element)) { |
| const LayoutObject* use_layout_object = element.GetLayoutObject(); |
| if (!use_layout_object || |
| use_layout_object->StyleRef().Display() == EDisplay::kNone) |
| return ClipStrategy::kNone; |
| const SVGGraphicsElement* shape_element = |
| ToSVGUseElement(element).VisibleTargetGraphicsElementForClipping(); |
| if (!shape_element) |
| return ClipStrategy::kNone; |
| ClipStrategy shape_strategy = DetermineClipStrategy(*shape_element); |
| return ModifyStrategyForClipPath(use_layout_object->StyleRef(), |
| shape_strategy); |
| } |
| if (!element.IsSVGGraphicsElement()) |
| return ClipStrategy::kNone; |
| return DetermineClipStrategy(ToSVGGraphicsElement(element)); |
| } |
| |
| bool ContributesToClip(const SVGElement& element) { |
| return DetermineClipStrategy(element) != ClipStrategy::kNone; |
| } |
| |
| Path PathFromElement(const SVGElement& element) { |
| if (IsSVGGeometryElement(element)) |
| return ToSVGGeometryElement(element).ToClipPath(); |
| |
| // Guaranteed by DetermineClipStrategy() above, only <use> element and |
| // SVGGraphicsElement that has a LayoutSVGShape can reach here. |
| SECURITY_DCHECK(IsSVGUseElement(element)); |
| return ToSVGUseElement(element).ToClipPath(); |
| } |
| |
| } // namespace |
| |
| LayoutSVGResourceClipper::LayoutSVGResourceClipper(SVGClipPathElement* node) |
| : LayoutSVGResourceContainer(node), in_clip_expansion_(false) {} |
| |
| LayoutSVGResourceClipper::~LayoutSVGResourceClipper() = default; |
| |
| void LayoutSVGResourceClipper::RemoveAllClientsFromCache( |
| bool mark_for_invalidation) { |
| clip_content_path_validity_ = kClipContentPathUnknown; |
| clip_content_path_.Clear(); |
| cached_paint_record_.reset(); |
| local_clip_bounds_ = FloatRect(); |
| MarkAllClientsForInvalidation( |
| mark_for_invalidation ? SVGResourceClient::kLayoutInvalidation | |
| SVGResourceClient::kBoundariesInvalidation |
| : SVGResourceClient::kParentOnlyInvalidation); |
| } |
| |
| base::Optional<Path> LayoutSVGResourceClipper::AsPath() { |
| if (clip_content_path_validity_ == kClipContentPathValid) |
| return base::Optional<Path>(clip_content_path_); |
| if (clip_content_path_validity_ == kClipContentPathInvalid) |
| return base::nullopt; |
| DCHECK_EQ(clip_content_path_validity_, kClipContentPathUnknown); |
| |
| clip_content_path_validity_ = kClipContentPathInvalid; |
| // If the current clip-path gets clipped itself, we have to fallback to |
| // masking. |
| if (StyleRef().ClipPath()) |
| return base::nullopt; |
| |
| unsigned op_count = 0; |
| base::Optional<SkOpBuilder> clip_path_builder; |
| SkPath resolved_path; |
| for (const SVGElement& child_element : |
| Traversal<SVGElement>::ChildrenOf(*GetElement())) { |
| ClipStrategy strategy = DetermineClipStrategy(child_element); |
| if (strategy == ClipStrategy::kNone) |
| continue; |
| if (strategy == ClipStrategy::kMask) |
| return base::nullopt; |
| |
| // Multiple shapes require PathOps. In some degenerate cases PathOps can |
| // exhibit quadratic behavior, so we cap the number of ops to a reasonable |
| // count. |
| const unsigned kMaxOps = 42; |
| if (++op_count > kMaxOps) |
| return base::nullopt; |
| if (clip_path_builder) { |
| clip_path_builder->add(PathFromElement(child_element).GetSkPath(), |
| kUnion_SkPathOp); |
| } else if (resolved_path.isEmpty()) { |
| resolved_path = PathFromElement(child_element).GetSkPath(); |
| } else { |
| clip_path_builder.emplace(); |
| clip_path_builder->add(std::move(resolved_path), kUnion_SkPathOp); |
| clip_path_builder->add(PathFromElement(child_element).GetSkPath(), |
| kUnion_SkPathOp); |
| } |
| } |
| |
| if (clip_path_builder) |
| clip_path_builder->resolve(&resolved_path); |
| clip_content_path_ = std::move(resolved_path); |
| clip_content_path_validity_ = kClipContentPathValid; |
| return base::Optional<Path>(clip_content_path_); |
| } |
| |
| sk_sp<const PaintRecord> LayoutSVGResourceClipper::CreatePaintRecord() { |
| DCHECK(GetFrame()); |
| if (cached_paint_record_) |
| return cached_paint_record_; |
| |
| PaintRecordBuilder builder(nullptr, nullptr); |
| // Switch to a paint behavior where all children of this <clipPath> will be |
| // laid out using special constraints: |
| // - fill-opacity/stroke-opacity/opacity set to 1 |
| // - masker/filter not applied when laying out the children |
| // - fill is set to the initial fill paint server (solid, black) |
| // - stroke is set to the initial stroke paint server (none) |
| PaintInfo info(builder.Context(), LayoutRect::InfiniteIntRect(), |
| PaintPhase::kForeground, kGlobalPaintNormalPhase, |
| kPaintLayerPaintingRenderingClipPathAsMask | |
| kPaintLayerPaintingRenderingResourceSubtree); |
| |
| for (const SVGElement& child_element : |
| Traversal<SVGElement>::ChildrenOf(*GetElement())) { |
| if (!ContributesToClip(child_element)) |
| continue; |
| // Use the LayoutObject of the direct child even if it is a <use>. In that |
| // case, we will paint the targeted element indirectly. |
| const LayoutObject* layout_object = child_element.GetLayoutObject(); |
| layout_object->Paint(info); |
| } |
| |
| cached_paint_record_ = builder.EndRecording(); |
| return cached_paint_record_; |
| } |
| |
| void LayoutSVGResourceClipper::CalculateLocalClipBounds() { |
| // This is a rough heuristic to appraise the clip size and doesn't consider |
| // clip on clip. |
| for (const SVGElement& child_element : |
| Traversal<SVGElement>::ChildrenOf(*GetElement())) { |
| if (!ContributesToClip(child_element)) |
| continue; |
| const LayoutObject* layout_object = child_element.GetLayoutObject(); |
| local_clip_bounds_.Unite(layout_object->LocalToSVGParentTransform().MapRect( |
| layout_object->VisualRectInLocalSVGCoordinates())); |
| } |
| } |
| |
| SVGUnitTypes::SVGUnitType LayoutSVGResourceClipper::ClipPathUnits() const { |
| return ToSVGClipPathElement(GetElement()) |
| ->clipPathUnits() |
| ->CurrentValue() |
| ->EnumValue(); |
| } |
| |
| AffineTransform LayoutSVGResourceClipper::CalculateClipTransform( |
| const FloatRect& reference_box) const { |
| AffineTransform transform = |
| ToSVGClipPathElement(GetElement()) |
| ->CalculateTransform(SVGElement::kIncludeMotionTransform); |
| if (ClipPathUnits() == SVGUnitTypes::kSvgUnitTypeObjectboundingbox) { |
| transform.Translate(reference_box.X(), reference_box.Y()); |
| transform.ScaleNonUniform(reference_box.Width(), reference_box.Height()); |
| } |
| return transform; |
| } |
| |
| bool LayoutSVGResourceClipper::HitTestClipContent( |
| const FloatRect& object_bounding_box, |
| const FloatPoint& node_at_point) { |
| FloatPoint point = node_at_point; |
| if (!SVGLayoutSupport::IntersectsClipPath(*this, point)) |
| return false; |
| |
| AffineTransform user_space_transform = |
| CalculateClipTransform(object_bounding_box); |
| if (!user_space_transform.IsInvertible()) |
| return false; |
| |
| point = user_space_transform.Inverse().MapPoint(point); |
| |
| for (const SVGElement& child_element : |
| Traversal<SVGElement>::ChildrenOf(*GetElement())) { |
| if (!ContributesToClip(child_element)) |
| continue; |
| HitTestLocation location(point); |
| HitTestResult result(HitTestRequest::kSVGClipContent, location); |
| LayoutObject* layout_object = child_element.GetLayoutObject(); |
| |
| DCHECK(!layout_object->IsBoxModelObject() || |
| !ToLayoutBoxModelObject(layout_object)->HasSelfPaintingLayer()); |
| |
| if (layout_object->NodeAtPoint(result, location, LayoutPoint(), |
| kHitTestForeground)) |
| return true; |
| } |
| return false; |
| } |
| |
| FloatRect LayoutSVGResourceClipper::ResourceBoundingBox( |
| const FloatRect& reference_box) { |
| // The resource has not been layouted yet. Return the reference box. |
| if (SelfNeedsLayout()) |
| return reference_box; |
| |
| if (local_clip_bounds_.IsEmpty()) |
| CalculateLocalClipBounds(); |
| |
| return CalculateClipTransform(reference_box).MapRect(local_clip_bounds_); |
| } |
| |
| void LayoutSVGResourceClipper::StyleDidChange(StyleDifference diff, |
| const ComputedStyle* old_style) { |
| LayoutSVGResourceContainer::StyleDidChange(diff, old_style); |
| if (diff.TransformChanged()) { |
| MarkAllClientsForInvalidation(SVGResourceClient::kBoundariesInvalidation | |
| SVGResourceClient::kPaintInvalidation); |
| } |
| } |
| |
| void LayoutSVGResourceClipper::WillBeDestroyed() { |
| MarkAllClientsForInvalidation(SVGResourceClient::kBoundariesInvalidation | |
| SVGResourceClient::kPaintInvalidation); |
| LayoutSVGResourceContainer::WillBeDestroyed(); |
| } |
| |
| } // namespace blink |