| /* | |
| * Copyright 2019 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "include/core/SkCanvas.h" | |
| #include "include/core/SkColor.h" | |
| #include "include/core/SkColorFilter.h" | |
| #include "include/core/SkFont.h" | |
| #include "include/core/SkImage.h" | |
| #include "include/core/SkImageFilter.h" | |
| #include "include/core/SkImageInfo.h" | |
| #include "include/core/SkPaint.h" | |
| #include "include/core/SkPathEffect.h" | |
| #include "include/core/SkPoint.h" | |
| #include "include/core/SkRect.h" | |
| #include "include/core/SkSurface.h" | |
| #include "include/effects/SkDashPathEffect.h" | |
| #include "include/effects/SkGradient.h" | |
| #include "include/effects/SkImageFilters.h" | |
| #include "src/core/SkImageFilter_Base.h" | |
| #include "src/core/SkMatrixPriv.h" | |
| #include "src/core/SkSpecialImage.h" | |
| #include "tools/ToolUtils.h" | |
| #include "tools/fonts/FontToolUtils.h" | |
| #include "tools/viewer/Slide.h" | |
| namespace { | |
| struct FilterNode { | |
| // Pointer to the actual filter in the DAG, so it still contains its input filters and | |
| // may be used as an input in an earlier node. Null when this represents the "source" input | |
| sk_sp<SkImageFilter> fFilter; | |
| // FilterNodes wrapping each of fFilter's inputs. Leaf node when fInputNodes is empty. | |
| std::vector<FilterNode> fInputNodes; | |
| // Distance from root filter | |
| int fDepth; | |
| // The source content rect (this is the same for all nodes, but is stored here for convenience) | |
| skif::ParameterSpace<SkRect> fContent; | |
| // The mapping for the filter dag (same for all nodes, but stored here for convenience) | |
| skif::Mapping fMapping; | |
| // Cached reverse bounds using device-space clip bounds (e.g. no local bounds hint passed to | |
| // saveLayer). This represents the layer calculated in SkCanvas for the filtering. | |
| skif::LayerSpace<SkIRect> fUnhintedLayerBounds; | |
| // Cached input bounds using the local draw bounds (e.g. saveLayer with a bounds rect, or | |
| // an auto-layer for a draw with image filter). This represents the layer bounds up to this | |
| // point of the DAG. | |
| skif::LayerSpace<SkIRect> fHintedLayerBounds; | |
| // Cached output bounds based on local draw bounds. This represents the output up to this | |
| // point of the DAG. | |
| skif::LayerSpace<SkIRect> fOutputBounds; | |
| FilterNode(const SkImageFilter* filter, | |
| const skif::Mapping& mapping, | |
| const skif::ParameterSpace<SkRect>& content, | |
| int depth) | |
| : fFilter(sk_ref_sp(filter)) | |
| , fDepth(depth) | |
| , fContent(content) | |
| , fMapping(mapping) { | |
| this->computeInputBounds(); | |
| this->computeOutputBounds(); | |
| if (fFilter) { | |
| fInputNodes.reserve(fFilter->countInputs()); | |
| for (int i = 0; i < fFilter->countInputs(); ++i) { | |
| fInputNodes.emplace_back(fFilter->getInput(i), mapping, content, depth + 1); | |
| } | |
| } | |
| } | |
| private: | |
| void computeOutputBounds() { | |
| if (fFilter) { | |
| // For visualization purposes, we want the output bounds in layer space, before it's | |
| // been transformed to device space. To achieve that, we mock a new mapping with the | |
| // identity matrix transform. | |
| skif::Mapping layerOnly{fMapping.layerMatrix()}; | |
| std::optional<skif::DeviceSpace<SkIRect>> pseudoDeviceBounds = | |
| as_IFB(fFilter)->getOutputBounds(layerOnly, fContent); | |
| // Since layerOnly's device matrix is I, this is effectively a cast to layer space | |
| if (pseudoDeviceBounds) { | |
| fOutputBounds = layerOnly.deviceToLayer(*pseudoDeviceBounds); | |
| } else { | |
| // Skip drawing infinite output bounds | |
| fOutputBounds = skif::LayerSpace<SkIRect>::Empty(); | |
| } | |
| } else { | |
| fOutputBounds = fMapping.paramToLayer(fContent).roundOut(); | |
| } | |
| // Fill in children | |
| for (size_t i = 0; i < fInputNodes.size(); ++i) { | |
| fInputNodes[i].computeOutputBounds(); | |
| } | |
| } | |
| void computeInputBounds() { | |
| // As a proxy for what the base device had, use the content rect mapped to device space | |
| // (e.g. clipRect() was called with the same coords prior to the draw). | |
| skif::DeviceSpace<SkIRect> targetOutput(SkMatrixPriv::MapRect(fMapping.totalMatrix(), | |
| SkRect(fContent)) | |
| .roundOut()); | |
| if (fFilter) { | |
| fHintedLayerBounds = as_IFB(fFilter)->getInputBounds(fMapping, targetOutput, fContent); | |
| fUnhintedLayerBounds = as_IFB(fFilter)->getInputBounds(fMapping, targetOutput, {}); | |
| } else { | |
| fHintedLayerBounds = fMapping.paramToLayer(fContent).roundOut(); | |
| fUnhintedLayerBounds = fMapping.deviceToLayer(targetOutput); | |
| } | |
| } | |
| }; | |
| } // anonymous namespace | |
| static FilterNode build_dag(const SkM44& ctm, const SkRect& rect, | |
| const SkImageFilter* rootFilter) { | |
| // Emulate SkCanvas::internalSaveLayer's decomposition of the CTM. | |
| skif::ParameterSpace<SkRect> content(rect); | |
| skif::ParameterSpace<SkPoint> center({rect.centerX(), rect.centerY()}); | |
| skif::Mapping mapping; | |
| SkAssertResult(mapping.decomposeCTM(ctm, rootFilter, center)); | |
| return FilterNode(rootFilter, mapping, content, 0); | |
| } | |
| static void draw_node(SkCanvas* canvas, const FilterNode& node) { | |
| canvas->clear(SK_ColorTRANSPARENT); | |
| SkPaint filterPaint; | |
| filterPaint.setImageFilter(node.fFilter); | |
| SkRect content = SkRect(node.fContent); | |
| SkPaint paint; | |
| static const SkColor4f kColors[2] = {SkColors::kGreen, SkColors::kWhite}; | |
| SkPoint points[2] = { {content.fLeft + 15.f, content.fTop + 15.f}, | |
| {content.fRight - 15.f, content.fBottom - 15.f} }; | |
| paint.setShader(SkShaders::LinearGradient(points, {{kColors, {}, SkTileMode::kRepeat}, {}})); | |
| SkPaint line; | |
| line.setStrokeWidth(0.f); | |
| line.setStyle(SkPaint::kStroke_Style); | |
| canvas->save(); | |
| canvas->concat(node.fMapping.layerToDevice()); | |
| canvas->save(); | |
| canvas->concat(node.fMapping.layerMatrix()); | |
| canvas->saveLayer(&content, &filterPaint); | |
| canvas->drawRect(content, paint); | |
| canvas->restore(); // Completes the image filter | |
| // Draw content-rect bounds | |
| line.setColor(SK_ColorBLACK); | |
| canvas->drawRect(content, line); | |
| // Bounding boxes have all been mapped by the layer matrix from local to layer space, so undo | |
| // the layer matrix, leaving just the device matrix. | |
| canvas->restore(); | |
| // The hinted bounds of the layer saved for the filtering | |
| line.setColor(SK_ColorRED); | |
| canvas->drawRect(SkRect::Make(SkIRect(node.fHintedLayerBounds)).makeOutset(3.f, 3.f), line); | |
| // The bounds of the layer if there was no local content hint | |
| line.setColor(SK_ColorGREEN); | |
| canvas->drawRect(SkRect::Make(SkIRect(node.fUnhintedLayerBounds)).makeOutset(2.f, 2.f), line); | |
| // The output bounds in layer space | |
| line.setColor(SK_ColorBLUE); | |
| canvas->drawRect(SkRect::Make(SkIRect(node.fOutputBounds)).makeOutset(1.f, 1.f), line); | |
| // Device-space bounding box of the output bounds (e.g. what legacy DAG manipulation via | |
| // MatrixTransform would produce). | |
| static const SkScalar kDashParams[] = {6.f, 12.f}; | |
| line.setPathEffect(SkDashPathEffect::Make(kDashParams, 0.f)); | |
| SkRect devOutputBounds = SkRect::Make(SkIRect(node.fMapping.layerToDevice(node.fOutputBounds))); | |
| canvas->restore(); // undoes device matrix | |
| canvas->drawRect(devOutputBounds, line); | |
| } | |
| static constexpr float kLineHeight = 16.f; | |
| static constexpr float kLineInset = 8.f; | |
| static float print_matrix(SkCanvas* canvas, const char* prefix, const SkMatrix& matrix, | |
| float x, float y, const SkFont& font, const SkPaint& paint) { | |
| canvas->drawString(prefix, x, y, font, paint); | |
| y += kLineHeight; | |
| for (int i = 0; i < 3; ++i) { | |
| SkString row; | |
| row.appendf("[%.2f %.2f %.2f]", | |
| matrix.get(i * 3), matrix.get(i * 3 + 1), matrix.get(i * 3 + 2)); | |
| canvas->drawString(row, x, y, font, paint); | |
| y += kLineHeight; | |
| } | |
| return y; | |
| } | |
| static float print_size(SkCanvas* canvas, const char* prefix, const SkIRect& rect, | |
| float x, float y, const SkFont& font, const SkPaint& paint) { | |
| canvas->drawString(prefix, x, y, font, paint); | |
| y += kLineHeight; | |
| SkString sz; | |
| sz.appendf("%d x %d", rect.width(), rect.height()); | |
| canvas->drawString(sz, x, y, font, paint); | |
| return y + kLineHeight; | |
| } | |
| static float print_info(SkCanvas* canvas, const FilterNode& node) { | |
| SkFont font(ToolUtils::DefaultTypeface(), 12); | |
| SkPaint text; | |
| text.setAntiAlias(true); | |
| float y = kLineHeight; | |
| if (node.fFilter) { | |
| canvas->drawString(node.fFilter->getTypeName(), kLineInset, y, font, text); | |
| y += kLineHeight; | |
| if (node.fDepth == 0) { | |
| // The mapping is the same for all nodes, so only print at the root | |
| y = print_matrix(canvas, | |
| "Param->Layer", | |
| node.fMapping.layerMatrix().asM33(), | |
| kLineInset, | |
| y, | |
| font, | |
| text); | |
| y = print_matrix(canvas, | |
| "Layer->Device", | |
| node.fMapping.layerToDevice().asM33(), | |
| kLineInset, | |
| y, | |
| font, | |
| text); | |
| } | |
| y = print_size(canvas, "Layer Size", SkIRect(node.fUnhintedLayerBounds), | |
| kLineInset, y, font, text); | |
| y = print_size(canvas, "Layer Size (hinted)", SkIRect(node.fHintedLayerBounds), | |
| kLineInset, y, font, text); | |
| } else { | |
| canvas->drawString("Source Input", kLineInset, y, font, text); | |
| y += kLineHeight; | |
| } | |
| return y; | |
| } | |
| // Returns bottom edge in pixels that the subtree reached in canvas | |
| static float draw_dag(SkCanvas* canvas, SkSurface* nodeSurface, const FilterNode& node) { | |
| // First capture the results of the node, into nodeSurface | |
| draw_node(nodeSurface->getCanvas(), node); | |
| sk_sp<SkImage> nodeResults = nodeSurface->makeImageSnapshot(); | |
| // Fill in background of the filter node with a checkerboard | |
| canvas->save(); | |
| canvas->clipRect(SkRect::MakeWH(nodeResults->width(), nodeResults->height())); | |
| ToolUtils::draw_checkerboard(canvas, SK_ColorGRAY, SK_ColorLTGRAY, 10); | |
| canvas->restore(); | |
| // Display filtered results in current canvas' location (assumed CTM is set for this node) | |
| canvas->drawImage(nodeResults, 0, 0); | |
| SkPaint line; | |
| line.setAntiAlias(true); | |
| line.setStyle(SkPaint::kStroke_Style); | |
| line.setStrokeWidth(3.f); | |
| // Text info | |
| canvas->save(); | |
| canvas->translate(0, nodeResults->height()); | |
| float textHeight = print_info(canvas, node); | |
| canvas->restore(); | |
| // Border around filtered results + text info | |
| canvas->drawRect(SkRect::MakeWH(nodeResults->width(), nodeResults->height() + textHeight), | |
| line); | |
| static const float kPad = 20.f; | |
| float x = nodeResults->width() + kPad; | |
| float y = 0; | |
| for (size_t i = 0; i < node.fInputNodes.size(); ++i) { | |
| // Line connecting this node to its child | |
| canvas->drawLine(nodeResults->width(), 0.5f * nodeResults->height(), // right of node | |
| x, y + 0.5f * nodeResults->height(), line); // left of child | |
| canvas->save(); | |
| canvas->translate(x, y); | |
| y += draw_dag(canvas, nodeSurface, node.fInputNodes[i]); | |
| canvas->restore(); | |
| } | |
| return std::max(y, nodeResults->height() + textHeight + kPad); | |
| } | |
| static void draw_dag(SkCanvas* canvas, SkImageFilter* filter, | |
| const SkRect& rect, const SkISize& surfaceSize) { | |
| // Get the current CTM, which includes all the viewer's UI modifications, which we want to | |
| // pass into our mock canvases for each DAG node. | |
| SkM44 ctm = canvas->getLocalToDevice(); | |
| canvas->save(); | |
| // Reset the matrix so that the DAG layout and instructional text is fixed to the window. | |
| canvas->resetMatrix(); | |
| // Process the image filter DAG to display intermediate results later on, which will apply the | |
| // provided CTM during draw_node calls. | |
| FilterNode dag = build_dag(ctm, rect, filter); | |
| sk_sp<SkSurface> nodeSurface = | |
| canvas->makeSurface(canvas->imageInfo().makeDimensions(surfaceSize)); | |
| draw_dag(canvas, nodeSurface.get(), dag); | |
| canvas->restore(); | |
| } | |
| class ImageFilterDAGSlide : public Slide { | |
| public: | |
| ImageFilterDAGSlide() { fName = "ImageFilterDAG"; } | |
| void draw(SkCanvas* canvas) override { | |
| // Make a large DAG | |
| // /--- Color Filter <---- Blur <--- Offset | |
| // Merge < | |
| // \--- Blur <--- Drop Shadow | |
| sk_sp<SkImageFilter> drop2 = SkImageFilters::DropShadow( | |
| 10.f, 5.f, 3.f, 3.f, SK_ColorBLACK, nullptr); | |
| sk_sp<SkImageFilter> blur1 = SkImageFilters::Blur(2.f, 2.f, std::move(drop2)); | |
| sk_sp<SkImageFilter> offset3 = SkImageFilters::Offset(-5.f, -5.f, nullptr); | |
| sk_sp<SkImageFilter> blur2 = SkImageFilters::Blur(4.f, 4.f, std::move(offset3)); | |
| sk_sp<SkImageFilter> cf1 = SkImageFilters::ColorFilter( | |
| SkColorFilters::Blend(SK_ColorGRAY, SkBlendMode::kModulate), std::move(blur2)); | |
| sk_sp<SkImageFilter> merge0 = SkImageFilters::Merge(std::move(blur1), std::move(cf1)); | |
| draw_dag(canvas, merge0.get(), kFilterRect, kFilterSurfaceSize); | |
| } | |
| // We want to use the viewer calculated CTM in the mini surfaces used per DAG node. The rotation | |
| // matrix viewer calculates is based on the slide content size. | |
| SkISize getDimensions() const override { return kFilterSurfaceSize; } | |
| private: | |
| static constexpr SkRect kFilterRect = SkRect::MakeXYWH(20.f, 20.f, 60.f, 60.f); | |
| static constexpr SkISize kFilterSurfaceSize = SkISize::Make( | |
| 2 * (kFilterRect.fRight + kFilterRect.fLeft), | |
| 2 * (kFilterRect.fBottom + kFilterRect.fTop)); | |
| }; | |
| DEF_SLIDE(return new ImageFilterDAGSlide();) |