1 /*
2 * Copyright 2019 Google LLC
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8 #include "samplecode/Sample.h"
9
10 #include "include/core/SkCanvas.h"
11 #include "include/core/SkColor.h"
12 #include "include/core/SkColorFilter.h"
13 #include "include/core/SkFont.h"
14 #include "include/core/SkImage.h"
15 #include "include/core/SkImageFilter.h"
16 #include "include/core/SkImageInfo.h"
17 #include "include/core/SkPaint.h"
18 #include "include/core/SkPoint.h"
19 #include "include/core/SkRect.h"
20 #include "include/core/SkSurface.h"
21
22 #include "include/effects/SkDashPathEffect.h"
23 #include "include/effects/SkGradientShader.h"
24 #include "include/effects/SkImageFilters.h"
25
26 #include "src/core/SkImageFilter_Base.h"
27 #include "src/core/SkSpecialImage.h"
28
29 #include "tools/ToolUtils.h"
30
31 namespace {
32
33 struct FilterNode {
34 // Pointer to the actual filter in the DAG, so it still contains its input filters and
35 // may be used as an input in an earlier node. Null when this represents the "source" input
36 sk_sp<SkImageFilter> fFilter;
37
38 // FilterNodes wrapping each of fFilter's inputs. Leaf node when fInputNodes is empty.
39 SkTArray<FilterNode> fInputNodes;
40
41 // Distance from root filter
42 int fDepth;
43
44 // The source content rect (this is the same for all nodes, but is stored here for convenience)
45 skif::ParameterSpace<SkRect> fContent;
46 // The mapping for the filter dag (same for all nodes, but stored here for convenience)
47 skif::Mapping fMapping;
48
49 // Cached reverse bounds using device-space clip bounds (e.g. no local bounds hint passed to
50 // saveLayer). This represents the layer calculated in SkCanvas for the filtering.
51 skif::LayerSpace<SkIRect> fUnhintedLayerBounds;
52
53 // Cached input bounds using the local draw bounds (e.g. saveLayer with a bounds rect, or
54 // an auto-layer for a draw with image filter). This represents the layer bounds up to this
55 // point of the DAG.
56 skif::LayerSpace<SkIRect> fHintedLayerBounds;
57
58 // Cached output bounds based on local draw bounds. This represents the output up to this
59 // point of the DAG.
60 skif::LayerSpace<SkIRect> fOutputBounds;
61
FilterNode__anon89dae7630111::FilterNode62 FilterNode(const SkImageFilter* filter,
63 const skif::Mapping& mapping,
64 const skif::ParameterSpace<SkRect>& content,
65 int depth)
66 : fFilter(sk_ref_sp(filter))
67 , fDepth(depth)
68 , fContent(content)
69 , fMapping(mapping) {
70 this->computeInputBounds();
71 this->computeOutputBounds();
72 if (fFilter) {
73 fInputNodes.reserve_back(fFilter->countInputs());
74 for (int i = 0; i < fFilter->countInputs(); ++i) {
75 fInputNodes.emplace_back(fFilter->getInput(i), mapping, content, depth + 1);
76 }
77 }
78 }
79
80 private:
computeOutputBounds__anon89dae7630111::FilterNode81 void computeOutputBounds() {
82 if (fFilter) {
83 // For visualization purposes, we want the output bounds in layer space, before it's
84 // been transformed to device space. To achieve that, we mock a new mapping with the
85 // identity matrix transform.
86 skif::Mapping layerOnly = skif::Mapping(SkMatrix::I(), fMapping.layerMatrix());
87 skif::DeviceSpace<SkIRect> pseudoDeviceBounds =
88 as_IFB(fFilter)->getOutputBounds(layerOnly, fContent);
89 // Since layerOnly's device matrix is I, this is effectively a cast to layer space
90 fOutputBounds = layerOnly.deviceToLayer(pseudoDeviceBounds);
91 } else {
92 fOutputBounds = fMapping.paramToLayer(fContent).roundOut();
93 }
94
95 // Fill in children
96 for (int i = 0; i < fInputNodes.count(); ++i) {
97 fInputNodes[i].computeOutputBounds();
98 }
99 }
100
computeInputBounds__anon89dae7630111::FilterNode101 void computeInputBounds() {
102 // As a proxy for what the base device had, use the content rect mapped to device space
103 // (e.g. clipRect() was called with the same coords prior to the draw).
104 skif::DeviceSpace<SkIRect> targetOutput(fMapping.totalMatrix()
105 .mapRect(SkRect(fContent))
106 .roundOut());
107
108 if (fFilter) {
109 fHintedLayerBounds = as_IFB(fFilter)->getInputBounds(fMapping, targetOutput, &fContent);
110 fUnhintedLayerBounds = as_IFB(fFilter)->getInputBounds(fMapping, targetOutput, nullptr);
111 } else {
112 fHintedLayerBounds = fMapping.paramToLayer(fContent).roundOut();
113 fUnhintedLayerBounds = fMapping.deviceToLayer(targetOutput);
114 }
115 }
116 };
117
118 } // anonymous namespace
119
build_dag(const SkMatrix & ctm,const SkRect & rect,const SkImageFilter * rootFilter)120 static FilterNode build_dag(const SkMatrix& ctm, const SkRect& rect,
121 const SkImageFilter* rootFilter) {
122 // Emulate SkCanvas::internalSaveLayer's decomposition of the CTM.
123 skif::ParameterSpace<SkRect> content(rect);
124 skif::ParameterSpace<SkPoint> center({rect.centerX(), rect.centerY()});
125 skif::Mapping mapping = skif::Mapping::DecomposeCTM(ctm, rootFilter, center);
126 return FilterNode(rootFilter, mapping, content, 0);
127 }
128
draw_node(SkCanvas * canvas,const FilterNode & node)129 static void draw_node(SkCanvas* canvas, const FilterNode& node) {
130 canvas->clear(SK_ColorTRANSPARENT);
131
132 SkPaint filterPaint;
133 filterPaint.setImageFilter(node.fFilter);
134
135 SkRect content = SkRect(node.fContent);
136 SkPaint paint;
137 static const SkColor kColors[2] = {SK_ColorGREEN, SK_ColorWHITE};
138 SkPoint points[2] = { {content.fLeft + 15.f, content.fTop + 15.f},
139 {content.fRight - 15.f, content.fBottom - 15.f} };
140 paint.setShader(SkGradientShader::MakeLinear(points, kColors, nullptr, SK_ARRAY_COUNT(kColors),
141 SkTileMode::kRepeat));
142
143 SkPaint line;
144 line.setStrokeWidth(0.f);
145 line.setStyle(SkPaint::kStroke_Style);
146
147 canvas->save();
148 canvas->concat(node.fMapping.deviceMatrix());
149 canvas->save();
150 canvas->concat(node.fMapping.layerMatrix());
151
152 canvas->saveLayer(&content, &filterPaint);
153 canvas->drawRect(content, paint);
154 canvas->restore(); // Completes the image filter
155
156 // Draw content-rect bounds
157 line.setColor(SK_ColorBLACK);
158 canvas->drawRect(content, line);
159
160 // Bounding boxes have all been mapped by the layer matrix from local to layer space, so undo
161 // the layer matrix, leaving just the device matrix.
162 canvas->restore();
163
164 // The hinted bounds of the layer saved for the filtering
165 line.setColor(SK_ColorRED);
166 canvas->drawRect(SkRect::Make(SkIRect(node.fHintedLayerBounds)).makeOutset(3.f, 3.f), line);
167 // The bounds of the layer if there was no local content hint
168 line.setColor(SK_ColorGREEN);
169 canvas->drawRect(SkRect::Make(SkIRect(node.fUnhintedLayerBounds)).makeOutset(2.f, 2.f), line);
170
171 // The output bounds in layer space
172 line.setColor(SK_ColorBLUE);
173 canvas->drawRect(SkRect::Make(SkIRect(node.fOutputBounds)).makeOutset(1.f, 1.f), line);
174 // Device-space bounding box of the output bounds (e.g. what legacy DAG manipulation via
175 // MatrixTransform would produce).
176 static const SkScalar kDashParams[] = {6.f, 12.f};
177 line.setPathEffect(SkDashPathEffect::Make(kDashParams, 2, 0.f));
178 SkRect devOutputBounds = SkRect::Make(SkIRect(node.fMapping.layerToDevice(node.fOutputBounds)));
179 canvas->restore(); // undoes device matrix
180 canvas->drawRect(devOutputBounds, line);
181 }
182
183 static constexpr float kLineHeight = 16.f;
184 static constexpr float kLineInset = 8.f;
185
print_matrix(SkCanvas * canvas,const char * prefix,const SkMatrix & matrix,float x,float y,const SkFont & font,const SkPaint & paint)186 static float print_matrix(SkCanvas* canvas, const char* prefix, const SkMatrix& matrix,
187 float x, float y, const SkFont& font, const SkPaint& paint) {
188 canvas->drawString(prefix, x, y, font, paint);
189 y += kLineHeight;
190 for (int i = 0; i < 3; ++i) {
191 SkString row;
192 row.appendf("[%.2f %.2f %.2f]",
193 matrix.get(i * 3), matrix.get(i * 3 + 1), matrix.get(i * 3 + 2));
194 canvas->drawString(row, x, y, font, paint);
195 y += kLineHeight;
196 }
197 return y;
198 }
199
print_size(SkCanvas * canvas,const char * prefix,const SkIRect & rect,float x,float y,const SkFont & font,const SkPaint & paint)200 static float print_size(SkCanvas* canvas, const char* prefix, const SkIRect& rect,
201 float x, float y, const SkFont& font, const SkPaint& paint) {
202 canvas->drawString(prefix, x, y, font, paint);
203 y += kLineHeight;
204 SkString sz;
205 sz.appendf("%d x %d", rect.width(), rect.height());
206 canvas->drawString(sz, x, y, font, paint);
207 return y + kLineHeight;
208 }
209
print_info(SkCanvas * canvas,const FilterNode & node)210 static float print_info(SkCanvas* canvas, const FilterNode& node) {
211 SkFont font(nullptr, 12);
212 SkPaint text;
213 text.setAntiAlias(true);
214
215 float y = kLineHeight;
216 if (node.fFilter) {
217 canvas->drawString(node.fFilter->getTypeName(), kLineInset, y, font, text);
218 y += kLineHeight;
219 if (node.fDepth == 0) {
220 // The mapping is the same for all nodes, so only print at the root
221 y = print_matrix(canvas, "Param->Layer", node.fMapping.layerMatrix(),
222 kLineInset, y, font, text);
223 y = print_matrix(canvas, "Layer->Device", node.fMapping.deviceMatrix(),
224 kLineInset, y, font, text);
225 }
226
227 y = print_size(canvas, "Layer Size", SkIRect(node.fUnhintedLayerBounds),
228 kLineInset, y, font, text);
229 y = print_size(canvas, "Layer Size (hinted)", SkIRect(node.fHintedLayerBounds),
230 kLineInset, y, font, text);
231 } else {
232 canvas->drawString("Source Input", kLineInset, y, font, text);
233 y += kLineHeight;
234 }
235
236 return y;
237 }
238
239 // Returns bottom edge in pixels that the subtree reached in canvas
draw_dag(SkCanvas * canvas,SkSurface * nodeSurface,const FilterNode & node)240 static float draw_dag(SkCanvas* canvas, SkSurface* nodeSurface, const FilterNode& node) {
241 // First capture the results of the node, into nodeSurface
242 draw_node(nodeSurface->getCanvas(), node);
243 sk_sp<SkImage> nodeResults = nodeSurface->makeImageSnapshot();
244
245 // Fill in background of the filter node with a checkerboard
246 canvas->save();
247 canvas->clipRect(SkRect::MakeWH(nodeResults->width(), nodeResults->height()));
248 ToolUtils::draw_checkerboard(canvas, SK_ColorGRAY, SK_ColorLTGRAY, 10);
249 canvas->restore();
250
251 // Display filtered results in current canvas' location (assumed CTM is set for this node)
252 canvas->drawImage(nodeResults, 0, 0);
253
254 SkPaint line;
255 line.setAntiAlias(true);
256 line.setStyle(SkPaint::kStroke_Style);
257 line.setStrokeWidth(3.f);
258
259 // Text info
260 canvas->save();
261 canvas->translate(0, nodeResults->height());
262 float textHeight = print_info(canvas, node);
263 canvas->restore();
264
265 // Border around filtered results + text info
266 canvas->drawRect(SkRect::MakeWH(nodeResults->width(), nodeResults->height() + textHeight),
267 line);
268
269 static const float kPad = 20.f;
270 float x = nodeResults->width() + kPad;
271 float y = 0;
272 for (int i = 0; i < node.fInputNodes.count(); ++i) {
273 // Line connecting this node to its child
274 canvas->drawLine(nodeResults->width(), 0.5f * nodeResults->height(), // right of node
275 x, y + 0.5f * nodeResults->height(), line); // left of child
276 canvas->save();
277 canvas->translate(x, y);
278 y = draw_dag(canvas, nodeSurface, node.fInputNodes[i]);
279 canvas->restore();
280 }
281 return std::max(y, nodeResults->height() + textHeight + kPad);
282 }
283
draw_dag(SkCanvas * canvas,sk_sp<SkImageFilter> filter,const SkRect & rect,const SkISize & surfaceSize)284 static void draw_dag(SkCanvas* canvas, sk_sp<SkImageFilter> filter,
285 const SkRect& rect, const SkISize& surfaceSize) {
286 // Get the current CTM, which includes all the viewer's UI modifications, which we want to
287 // pass into our mock canvases for each DAG node.
288 SkMatrix ctm = canvas->getTotalMatrix();
289
290 canvas->save();
291 // Reset the matrix so that the DAG layout and instructional text is fixed to the window.
292 canvas->resetMatrix();
293
294 // Process the image filter DAG to display intermediate results later on, which will apply the
295 // provided CTM during draw_node calls.
296 FilterNode dag = build_dag(ctm, rect, filter.get());
297
298 sk_sp<SkSurface> nodeSurface =
299 canvas->makeSurface(canvas->imageInfo().makeDimensions(surfaceSize));
300 draw_dag(canvas, nodeSurface.get(), dag);
301
302 canvas->restore();
303 }
304
305 class ImageFilterDAGSample : public Sample {
306 public:
ImageFilterDAGSample()307 ImageFilterDAGSample() {}
308
onDrawContent(SkCanvas * canvas)309 void onDrawContent(SkCanvas* canvas) override {
310 static const SkRect kFilterRect = SkRect::MakeXYWH(20.f, 20.f, 60.f, 60.f);
311 static const SkISize kFilterSurfaceSize = SkISize::Make(
312 2 * (kFilterRect.fRight + kFilterRect.fLeft),
313 2 * (kFilterRect.fBottom + kFilterRect.fTop));
314
315 // Somewhat clunky, but we want to use the viewer calculated CTM in the mini surfaces used
316 // per DAG node. The rotation matrix viewer calculates is based on the sample size so trick
317 // it into calculating the right matrix for us w/ 1 frame latency.
318 this->setSize(kFilterSurfaceSize.width(), kFilterSurfaceSize.height());
319
320 // Make a large DAG
321 // /--- Color Filter <---- Blur <--- Offset
322 // Merge <
323 // \--- Blur <--- Drop Shadow
324 sk_sp<SkImageFilter> drop2 = SkImageFilters::DropShadow(
325 10.f, 5.f, 3.f, 3.f, SK_ColorBLACK, nullptr);
326 sk_sp<SkImageFilter> blur1 = SkImageFilters::Blur(2.f, 2.f, std::move(drop2));
327
328 sk_sp<SkImageFilter> offset3 = SkImageFilters::Offset(-5.f, -5.f, nullptr);
329 sk_sp<SkImageFilter> blur2 = SkImageFilters::Blur(4.f, 4.f, std::move(offset3));
330 sk_sp<SkImageFilter> cf1 = SkImageFilters::ColorFilter(
331 SkColorFilters::Blend(SK_ColorGRAY, SkBlendMode::kModulate), std::move(blur2));
332
333 sk_sp<SkImageFilter> merge0 = SkImageFilters::Merge(std::move(blur1), std::move(cf1));
334
335 draw_dag(canvas, std::move(merge0), kFilterRect, kFilterSurfaceSize);
336 }
337
name()338 SkString name() override { return SkString("ImageFilterDAG"); }
339
340 private:
341
342 using INHERITED = Sample;
343 };
344
345 DEF_SAMPLE(return new ImageFilterDAGSample();)
346