Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
skia_canvas.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#ifdef PULP_HAS_SKIA
7
8// Forward declare Skia types to avoid header dependency in public API
9class SkCanvas;
10class SkSurface;
11class SkFont;
12class SkImage;
13class SkPaint;
14class SkPathBuilder;
15
16// These need full definitions for member variables
17#include "include/core/SkRefCnt.h"
18#include "include/core/SkBlendMode.h"
19#include "include/core/SkMatrix.h"
20// SkSamplingOptions appears as the return type of
21// sampling_options_for_image_smoothing(); without the full type the
22// declaration breaks compilation on consumers that include this
23// header without separately bringing in SkSamplingOptions.h.
24#include "include/core/SkSamplingOptions.h"
25#include <cstddef>
26#include <memory>
27#include <vector>
28class SkShader;
29class SkPathEffect;
30class SkImageFilter;
31
32namespace skgpu::graphite {
33class Recorder;
34}
35class GrDirectContext;
36
37// Which Skia GPU backend this build links. Native builds link Graphite on
38// Dawn; the Emscripten slice links Ganesh on WebGL2, which ships no Graphite
39// or Dawn symbols at all. The two are mutually exclusive, and neither is
40// defined for a CPU-raster-only build. These macros gate implementation code
41// only — the SkiaCanvas layout below is identical on every backend so a TU
42// that misses the Skia compile definitions can still consume this header.
43#if defined(SK_GRAPHITE)
44#define PULP_CANVAS_GRAPHITE 1
45#else
46#define PULP_CANVAS_GRAPHITE 0
47#endif
48#if defined(SK_GANESH)
49#define PULP_CANVAS_GANESH 1
50#else
51#define PULP_CANVAS_GANESH 0
52#endif
53
54namespace pulp::canvas {
55
56// Skia-backed Canvas implementation for GPU-accelerated rendering
57// Uses Skia Graphite when available, falls back to Ganesh or CPU
58class SkiaCanvas : public Canvas {
59public:
60 RendererBackend renderer_backend() const noexcept override;
61 float backing_scale() const noexcept override;
67 class RetainedLayerStore;
68 static std::shared_ptr<RetainedLayerStore> create_retained_layer_store();
69
78 std::size_t prune_abandoned_retained_layers();
79
83 std::size_t retained_layer_count() const;
84
86 static std::size_t max_retained_layers();
87
88 // Create wrapping an existing SkCanvas (e.g., from a surface)
89 explicit SkiaCanvas(SkCanvas* canvas, skgpu::graphite::Recorder* recorder = nullptr);
90 SkiaCanvas(SkCanvas* canvas,
91 skgpu::graphite::Recorder* recorder,
92 std::shared_ptr<RetainedLayerStore> retained_layers);
93 ~SkiaCanvas() override;
94
95 // Attach the Ganesh direct context that owns the surface this canvas draws
96 // into. Raster-decoded images must be uploaded to a GPU texture before a
97 // GPU-backed canvas can draw them — Graphite canvases do that through the
98 // recorder passed to the constructor, Ganesh canvases through this context.
99 // Leaving it null keeps the CPU-raster behavior (the decoded image is
100 // drawn as-is), which is correct for raster surfaces and degraded — draws
101 // are dropped by the backend — for a GPU surface.
102 void set_gpu_upload_context(GrDirectContext* context);
103
104 // ── State ────────────────────────────────────────────────────────────
105 void save() override;
106 void restore() override;
107 int save_count() const override;
108 void restore_to_count(int target) override;
109
110 // ── Transform ────────────────────────────────────────────────────────
111 void translate(float x, float y) override;
112 void scale(float sx, float sy) override;
113 void rotate(float radians) override;
114 void set_transform(float a, float b, float c,
115 float d, float e, float f) override;
116 void capture_paint_baseline_transform() override;
117 void concat_transform(float a, float b, float c,
118 float d, float e, float f) override;
119 AffineTransform2x3 current_transform() const override;
120
121 // ── Clipping ─────────────────────────────────────────────────────────
122 void clip_rect(float x, float y, float w, float h) override;
123 void clip(FillRule rule = FillRule::nonzero) override;
124 void clip_path_svg(const std::string& svg_path_d) override;
125
126 // ── Fill and stroke ──────────────────────────────────────────────────
127 void set_fill_color(Color c) override;
128 void set_stroke_color(Color c) override;
129 void set_line_width(float w) override;
130 void set_line_cap(LineCap cap) override;
131 void set_line_join(LineJoin join) override;
132
133 // Canvas2D ctx.miterLimit and ctx.imageSmoothingEnabled /
134 // ctx.imageSmoothingQuality. Sticky paint state honored by subsequent
135 // stroke / drawImage calls.
136 void set_miter_limit(float limit) override;
137 void set_image_smoothing(bool enabled,
138 ImageSmoothingQuality quality) override;
139
140 // ── Shapes ───────────────────────────────────────────────────────────
141 void fill_rect(float x, float y, float w, float h) override;
142 void clear_rect(float x, float y, float w, float h) override;
143 void stroke_rect(float x, float y, float w, float h) override;
144 void fill_rounded_rect(float x, float y, float w, float h, float radius) override;
145 void stroke_rounded_rect(float x, float y, float w, float h, float radius) override;
146 void fill_circle(float cx, float cy, float radius) override;
147 void stroke_circle(float cx, float cy, float radius) override;
148
149 // ── Arcs ─────────────────────────────────────────────────────────────
150 void stroke_arc(float cx, float cy, float radius,
151 float start_angle, float end_angle) override;
152
153 // ── Lines ────────────────────────────────────────────────────────────
154 void stroke_line(float x0, float y0, float x1, float y1) override;
155
156 // ── Polyline / polygon ───────────────────────────────────────────────
157 // Build one SkPath and paint it once. The base-class stroke_path fallback
158 // degrades to N independent stroke_line calls (each with its own caps and
159 // no joins, which beads a dense curve), and the base fill_path round-trips
160 // through the path-recording state. Overriding both keeps the Skia backend
161 // at parity with CgCanvas. fill_path maps `rule` onto the SkPath fill type
162 // (kEvenOdd / kWinding).
163 void stroke_path(const Point2D* points, size_t count) override;
164 void fill_path(const Point2D* points, size_t count,
165 FillRule rule = FillRule::nonzero) override;
166
167 // ── Retained paths (pulp::canvas::Path) ──────────────────────────────
168 // Implemented in skia_canvas_path.cpp. Each converts the Path to one
169 // SkPath and issues a single draw, rather than replaying verbs through the
170 // scratch path builder. The `using` declarations keep the polygon
171 // overloads above visible — a same-named override would otherwise hide
172 // them from anyone holding a SkiaCanvas&.
173 using Canvas::fill_path;
174 using Canvas::stroke_path;
175 void fill_path(const Path& path, FillRule rule = FillRule::nonzero) override;
176 void stroke_path(const Path& path, const StrokeStyle& style) override;
177 void clip_path(const Path& path, FillRule rule = FillRule::nonzero) override;
178 void draw_path_shadow(const Path& path, float dx, float dy,
179 float blur, float spread, Color color) override;
180
181 // ── Retained compositing layers ──────────────────────────────────────
182 // Real offscreen SkSurfaces, sealed to SkImages that OUTLIVE the frame
183 // when `cacheable` — which is the whole point of the handle API.
184 LayerHandle begin_layer(Rect2D bounds, bool cacheable = false) override;
185 LayerHandle end_layer() override;
186 void draw_layer(LayerHandle layer, float alpha = 1.0f,
187 BlendMode mode = BlendMode::normal) override;
188 void draw_layer_fitted(LayerHandle layer, Rect2D dest) override;
189 void draw_layer_rotated(LayerHandle layer, float angle_rad) override;
190 bool layer_valid(LayerHandle layer) const override;
191 void invalidate_layer(LayerHandle layer) override;
192
193 // ── Scene recording (subtree cache, FU-3) ─────────────────────────────
194 // record_scene captures `paint` into an SkPicture via SkPictureRecorder;
195 // draw_scene replays it with SkCanvas::drawPicture at the current CTM/clip.
196 // Resolution-independent — no image shader is created, so the Graphite
197 // ensure_gpu_image() upload path is not in play for the replay itself; any
198 // images drawn DURING the record are GPU-uploaded at record time because
199 // the recording SkiaCanvas is handed this canvas's recorder + context.
200 std::shared_ptr<SceneRecording> record_scene(
201 float w, float h, const std::function<void(Canvas&)>& paint) override;
202 bool draw_scene(const SceneRecording& rec) override;
203
204 // ── Text ─────────────────────────────────────────────────────────────
205 void set_font(const std::string& family, float size) override;
206 void set_font_full(const std::string& family, float size,
207 int weight, int slant, float letter_spacing) override;
208 void set_font_smoothing(FontSmoothing smoothing) override;
209 // OpenType feature flags via SkShaper Feature API.
210 // Captured into font_features_ vector and flushed at shape time.
211 void set_font_features(std::vector<FontFeature> features) override;
212 void clear_font_features() override;
213 void set_text_align(TextAlign align) override;
214 void fill_text(const std::string& text, float x, float y) override;
215 // Typed text-anchor variant.
216 void fill_text_anchored(const std::string& text, float x, float y,
217 TextAnchor anchor) override;
218 // Canvas2D fillText(text,x,y,maxWidth) + strokeText(text,x,y,maxWidth).
219 void fill_text_with_max_width(const std::string& text,
220 float x, float y, float max_width) override;
221 void stroke_text(const std::string& text, float x, float y,
222 float max_width = 0.0f) override;
223 void fill_text_sdf(const std::string& text, float x, float y,
224 const SdfAtlas& atlas) override;
225 float measure_text(const std::string& text) override;
226 TextMetrics measure_text_full(const std::string& text) override;
227 // Caret x for a byte boundary within the full shaped run (same
228 // make_paragraph(...) fill_text uses), so kerned / letter-spaced text
229 // reports the exact painted advance.
230 float text_x_for_byte(const std::string& text,
231 std::size_t byte_index) override;
232
233 // ── Capabilities ────────────────────────────────────────────────────
234 // Skia renders every listed verb faithfully (SkImageFilters color ops,
235 // SkSVGDOM, path-clip parser, real Gaussian box-shadow, SkSL runtime
236 // effects, and the FU-3 SkPicture-backed scene_cache record/replay).
237 bool supports(CanvasCapability cap) const override {
238 switch (cap) {
239 case CanvasCapability::images:
240 case CanvasCapability::svg:
241 case CanvasCapability::clip_path_svg:
242 case CanvasCapability::filter_chain:
243 case CanvasCapability::mask_layer:
244 case CanvasCapability::backdrop_filter:
245 case CanvasCapability::backdrop_filter_chain:
246 case CanvasCapability::bloom_layer:
247 case CanvasCapability::sksl_draw:
248 case CanvasCapability::sksl_post_effect:
249 case CanvasCapability::box_shadow_gaussian:
250 case CanvasCapability::scene_cache: // record_scene/draw_scene (FU-3)
251 return true;
252 case CanvasCapability::retained_layer_cache:
253 return retained_layer_cache_supported_;
254 case CanvasCapability::count: // sentinel; never a real query
255 return false;
256 }
257 return false;
258 }
259
260 // ── Images ──────────────────────────────────────────────────────────
261 bool draw_image_from_data(const uint8_t* data, size_t size,
262 float x, float y, float w, float h) override;
263 bool draw_image_from_file(const std::string& path,
264 float x, float y, float w, float h) override;
265 bool draw_svg(const std::string& svg_document,
266 float x, float y, float w, float h) override;
267 // 9-arg drawImage source-rect form: routes through
268 // SkCanvas::drawImageRect(image, src, dst, sampling) so a sub-rect of
269 // the decoded SkImage maps onto the destination rect. Supports
270 // sprite-sheet slicing without re-decoding the source bitmap.
271 bool draw_image_from_data_rect(const uint8_t* data, size_t size,
272 float sx, float sy, float sw, float sh,
273 float dx, float dy, float dw, float dh) override;
274 bool draw_image_from_static_data_rect(const uint8_t* data, size_t size, float sx, float sy,
275 float sw, float sh, float dx, float dy, float dw,
276 float dh) override;
277 // Content-keyed forms: decode + GPU-upload once per (key, backend) through
278 // ImageFileCache, under a NUL-prefixed key no filesystem path can alias.
279 bool draw_image_from_keyed_data(const std::string& content_key, const uint8_t* data,
280 size_t size, float x, float y, float w, float h) override;
281 bool draw_image_from_keyed_data_rect(const std::string& content_key, const uint8_t* data,
282 size_t size, float sx, float sy, float sw, float sh,
283 float dx, float dy, float dw, float dh) override;
284 bool draw_image_from_file_rect(const std::string& path,
285 float sx, float sy, float sw, float sh,
286 float dx, float dy, float dw, float dh) override;
287 bool measure_image_from_file(const std::string& path,
288 float& out_width, float& out_height) override;
289 // Reads the header through SkCodec; no pixels are decoded.
290 bool measure_image_from_data(const uint8_t* data, size_t size, float& out_width,
291 float& out_height) override;
292
293 // Skia-specific: draw an already-constructed SkImage (e.g. a GPU
294 // surface snapshot or atlas texture) into the active rect. The
295 // decode-from-bytes paths above always yield raster images; this
296 // overload is the seam for drawing a GPU-texture-backed image, which
297 // is the case the `.skp` capture's image proc must handle. Returns
298 // false when the canvas or image is null.
299 bool draw_skia_image(const sk_sp<SkImage>& image,
300 float x, float y, float w, float h);
301
302 // ── Line dash / pixel manipulation ─────────────────────────────────
303 void set_line_dash(const float* intervals, int count, float phase) override;
304 bool read_pixels(int x, int y, int width, int height, uint8_t* out) override;
305 bool write_pixels(const uint8_t* data, int width, int height,
306 int dx, int dy) override;
307 void draw_waveform(const float* samples, size_t count,
308 float x, float y, float width, float height,
309 const WaveformStyle& style) override;
310 // Gradients
311 void set_fill_gradient_linear(float x0, float y0, float x1, float y1,
312 const Color* colors, const float* positions, int count) override;
313 void set_fill_gradient_radial(float cx, float cy, float radius,
314 const Color* colors, const float* positions, int count) override;
316 void set_fill_gradient_radial_elliptical(
317 float cx, float cy, float rx, float ry,
318 const Color* colors, const float* positions, int count) override;
320 void set_fill_gradient_radial_two_circles(
321 float x0, float y0, float r0,
322 float x1, float y1, float r1,
323 const Color* colors, const float* positions, int count) override;
324 void set_fill_gradient_conic_repeating(float cx, float cy, float start_angle,
325 float sweep_turns,
326 const Color* colors, const float* positions,
327 int count) override;
329 void set_fill_gradient_linear_repeating(float x0, float y0, float x1, float y1,
330 const Color* colors, const float* positions,
331 int count) override;
332 void set_fill_gradient_conic(float cx, float cy, float start_angle,
333 const Color* colors, const float* positions, int count) override;
334 void clear_fill_gradient() override;
335
336 // Canvas2D `ctx.strokeStyle = createLinearGradient(...)` (and radial /
337 // two-circle / conic counterparts). Routes through SkShaders::*Gradient and
338 // stores the resulting shader on `stroke_shader_`, which
339 // `apply_stroke_state` attaches to every stroke paint. Mirrors the fill-side
340 // surface so the bridge can expose `canvasSetStrokeLinearGradient` etc.
341 // without inventing a separate paint pipeline.
342 void set_stroke_gradient_linear(float x0, float y0, float x1, float y1,
343 const Color* colors, const float* positions, int count) override;
344 void set_stroke_gradient_radial(float cx, float cy, float radius,
345 const Color* colors, const float* positions, int count) override;
346 void set_stroke_gradient_radial_two_circles(
347 float x0, float y0, float r0,
348 float x1, float y1, float r1,
349 const Color* colors, const float* positions, int count) override;
350 void set_stroke_gradient_conic(float cx, float cy, float start_angle,
351 const Color* colors, const float* positions, int count) override;
352 void clear_stroke_gradient() override;
353
354 // Canvas2D ctx.createPattern.
355 // Routes through SkShader::MakeImage with SkTileMode per axis.
356 // Stored on the same gradient_shader_ field used by gradient fills
357 // (the field already represents "active non-solid fill paint"), so
358 // current_fill_paint() picks it up uniformly.
359 void set_fill_pattern(const std::string& image_src,
360 PatternTileMode tile_x,
361 PatternTileMode tile_y) override;
362 void set_stroke_pattern(const std::string& image_src,
363 PatternTileMode tile_x,
364 PatternTileMode tile_y) override;
365
366 // Blend modes
367 void set_blend_mode(BlendMode mode) override;
368
369 // Path building
370 void begin_path() override;
371 void move_to(float x, float y) override;
372 void line_to(float x, float y) override;
373 void quad_to(float cpx, float cpy, float x, float y) override;
374 void cubic_to(float cp1x, float cp1y, float cp2x, float cp2y, float x, float y) override;
375 void close_path() override;
376 void fill_current_path(FillRule rule = FillRule::nonzero) override;
377 void stroke_current_path() override;
378
379 // Native arc subpaths via SkPath::arcTo / SkRRect.
380 void arc(float cx, float cy, float radius,
381 float start_angle, float end_angle,
382 bool anticlockwise) override;
383 void arc_to(float x1, float y1, float x2, float y2,
384 float radius) override;
385 void ellipse(float cx, float cy, float rx, float ry,
386 float rotation,
387 float start_angle, float end_angle,
388 bool anticlockwise) override;
389 void round_rect(float x, float y, float w, float h,
390 float tl_x, float tl_y,
391 float tr_x, float tr_y,
392 float br_x, float br_y,
393 float bl_x, float bl_y) override;
394
395 // SDF shapes
396 void draw_sdf_shape(SDFShape shape, float x, float y, float w, float h,
397 const SDFStyle& style) override;
398 bool draw_sdf_shape_with_shader(SDFShape shape, float x, float y,
399 float w, float h, const SDFStyle& style,
400 const std::string& sksl,
401 const ShaderDrawOptions& options) override;
402 void draw_blurred_backdrop(float x, float y, float w, float h,
403 float blur_radius, float corner_radius,
404 Color tint) override;
405 void save_backdrop_filter(float x, float y, float w, float h,
406 float blur_radius) override;
407 void save_backdrop_filter_chain(float x, float y, float w, float h,
408 const FilterChainEntry* chain,
409 int count) override;
410
411 // Box shadow uses SkImageFilters::DropShadowOnly for outset shadows; inset
412 // shadows clip to the box and stroke with a blurred mask.
413 void draw_box_shadow(float x, float y, float w, float h,
414 float dx, float dy, float blur, float spread,
415 Color color, bool inset, float corner_radius) override;
416
417 // Canvas2D drop-shadow state. Sticky values attach an
418 // SkImageFilters::DropShadow to the active fill/stroke paints until cleared
419 // (color alpha == 0 or blur == 0 with both offsets == 0). See
420 // current_fill_paint() and apply_shadow_filter() for the wrapping logic.
421 void set_shadow_color(Color color) override;
422 void set_shadow_blur(float blur) override;
423 void set_shadow_offset_x(float dx) override;
424 void set_shadow_offset_y(float dy) override;
425
426 // Canvas2D ctx.direction / ctx.filter sticky state.
427 // Direction selects SkShaper's leftToRight flag (and later the HarfBuzz
428 // buffer direction for proper bidi mixed-script). The
429 // filter parses a CSS <filter-function-list> string ("blur(5px)
430 // sepia(80%) ...") into an SkImageFilter chain that wraps each
431 // subsequent paint via current_fill_paint() / apply_stroke_state.
432 void set_direction(TextDirection direction) override;
433 void set_filter(const std::string& filter) override;
434
435 // Custom SkSL shader rendering (GPU-accelerated)
436 bool draw_with_sksl(const std::string& sksl,
437 float x, float y, float w, float h,
438 const ShaderUniforms& uniforms) override;
439 bool draw_with_sksl(const std::string& sksl,
440 float x, float y, float w, float h,
441 const ShaderDrawOptions& options) override;
442
443 // Child-shader compositor: post-process an already-painted layer with a
444 // custom SkSL shader (declares `uniform shader content`).
445 bool save_layer_with_sksl_post_effect(
446 float x, float y, float w, float h,
447 const std::string& sksl, const ShaderUniforms& uniforms,
448 float sample_radius = 0.0f,
449 const std::vector<NamedUniform>& extra_uniforms = {},
450 BlendMode blend_mode = BlendMode::normal) override;
451
452 // Draw a Dawn-backed texture into the current Skia canvas when Graphite is active.
453 bool draw_native_dawn_texture(void* texture_handle,
454 uint32_t width,
455 uint32_t height,
456 const std::string& format,
457 float x,
458 float y,
459 float w,
460 float h);
461
462 // Standalone text measurement. Returns full HTML5
463 // TextMetrics for `text` rendered with `family` at `size` pixels —
464 // no canvas instance required. Used by the JS bridge's
465 // canvasMeasureText so JS callers can pre-measure text for layout
466 // without needing an active draw surface.
467 static Canvas::TextMetrics measure_text_with_font(
468 const std::string& family, float size, const std::string& text);
469
470 // Opacity & compositing layers
471 void set_opacity(float alpha) override;
472 void save_layer(float x, float y, float w, float h,
473 float opacity, float blur_radius) override;
474 // saveLayer with explicit blend mode (CSS / RN mix-blend-mode). The Skia
475 // backend honors the requested blend mode on the layer-paint so the subtree
476 // composites back with multiply / screen / overlay / etc.
477 void save_layer_with_blend(float x, float y, float w, float h,
478 float opacity, float blur_radius,
479 Canvas::BlendMode mode) override;
480 // Full CSS filter chain.
481 void save_layer_with_filters(float x, float y, float w, float h,
482 float opacity,
483 const FilterChainEntry* chain,
484 int count) override;
485
486 // Curated named GPU post-effect layer (crt / grain / vignette / noise /
487 // brushed / bloom). Builds an SkImageFilters::RuntimeShader from the
488 // vetted SkSL library and applies it to the compositing layer. Unknown
489 // names / compile failures fall back to a plain saveLayer (effect
490 // skipped) — see core/canvas/src/named_shader_effects.{hpp,cpp}.
491 void save_layer_with_shader_effect(float x, float y, float w, float h,
492 const std::string& effect_name,
493 float intensity) override;
494
495 // CSS mask-image + mask-size paint composite.
496 // SkiaCanvas implements the 2-saveLayer pattern with kDstIn:
497 // 1. open the content layer (saveLayer with opacity)
498 // 2. caller paints the subtree
499 // 3. caller calls restore() — SkiaCanvas::restore() detects this
500 // layer has a pending mask, draws the mask shader via a kDstIn
501 // saveLayer first, then closes the content layer
502 // Pending masks are tracked on `pending_masks_` keyed by Skia's
503 // internal save count so the restore-side dispatcher knows which
504 // restore call belongs to which mask layer. SkiaCanvas::restore() handles
505 // the deferred mask composite: it walks pending_masks_ for a matching save
506 // count before delegating to canvas_->restore().
507 void save_layer_with_mask(float x, float y, float w, float h,
508 float opacity,
509 const std::string& mask_image,
510 const std::string& mask_size) override;
511 void save_layer_with_bloom(float x, float y, float w, float h,
512 float intensity, float threshold,
513 float radius) override;
514
515private:
516 // Shared saveLayer for every compositing-layer entry point (opacity, blur,
517 // blend, bloom, CSS filter chain, SkSL post-effect). Builds the layer paint
518 // from the given properties — `opacity < 1` sets the layer alpha; a non-null
519 // `image_filter` is used as the layer's image filter, else `blur_radius > 0`
520 // adds a Gaussian blur; `mode != normal` sets the composite blend — pushes
521 // the layer, and tracks it on the non-opaque-layer stack when the layer is
522 // not fully opaque (opacity < 1 OR `force_non_opaque`, e.g. a filter chain
523 // that reduces coverage). Consolidates what were four near-identical bodies.
524 void push_layer(float x, float y, float w, float h,
525 float opacity, float blur_radius, Canvas::BlendMode mode,
526 sk_sp<SkImageFilter> image_filter,
527 bool force_non_opaque = false);
528
529 // Build the active fill paint, honoring `gradient_shader_` when set
530 // so shape fills (rect / rrect / circle / arc / oval / polygon) render
531 // gradients consistently with `fill_current_path()`.
532 SkPaint current_fill_paint() const;
533
534 // Apply the sticky Canvas2D shadow* state to an
535 // arbitrary paint (typically a stroke paint built via the free
536 // `make_stroke_paint` helper). Members can't be reached from the
537 // free helper, so the call sites that build a stroke paint apply
538 // the shadow filter via this method before draw. No-op when shadow
539 // is inactive (alpha == 0 OR (blur == 0 AND offsets == 0)).
540 void apply_shadow_filter(SkPaint& paint) const;
541
542 // Predicate used by `apply_shadow_filter` AND `current_fill_paint`
543 // so the gating logic stays in one place.
544 bool shadow_is_active() const;
545
546 // Apply sticky stroke join + miter limit to a freshly-constructed stroke
547 // paint. Called from the stroke_* paths after make_stroke_paint() but
548 // before any paint submit. Centralises the policy so future stroke-state
549 // setters can join here without touching every site.
550 void apply_stroke_state(SkPaint& paint) const;
551
552 // Translate the sticky imageSmoothingEnabled / imageSmoothingQuality state
553 // into an SkSamplingOptions for drawImageRect. Spec defaults match Skia
554 // defaults so non-set callers keep getting kLinear.
555 SkSamplingOptions sampling_options_for_image_smoothing() const;
556
557 // Upload a raster-decoded SkImage to a GPU texture when a Graphite
558 // recorder or a Ganesh context is attached. Returns the input image
559 // unchanged on the CPU raster path or if the upload fails. Centralised so
560 // every draw_image_* method takes the same branch.
561 sk_sp<SkImage> ensure_gpu_image(sk_sp<SkImage> image) const;
562
563 // Decode a file image and GPU-upload it, going through the process-wide
564 // ImageFileCache so a file image drawn every frame is read + decoded +
565 // uploaded ONCE instead of per draw. Keyed on (path, this canvas's GPU
566 // backend identity). Returns null on read/decode failure. See
567 // image_file_cache.hpp for the path-keyed (not content-keyed) semantics.
568 sk_sp<SkImage> image_from_file_cached(const std::string& path) const;
569 // Decode + GPU-upload encoded bytes once per (content key, backend) through
570 // ImageFileCache. An empty key builds uncached. The cached image owns a
571 // copy of the bytes, so it outlives the caller's buffer.
572 sk_sp<SkImage> image_from_keyed_data_cached(const std::string& content_key, const uint8_t* data,
573 size_t size) const;
574 // The backend identity token used as part of the cache key: the Graphite
575 // recorder or the Ganesh context, or null for a CPU raster canvas.
576 const void* image_cache_backend_key() const;
577
578 SkCanvas* canvas_; // Non-owning — owned by surface or caller
579 skgpu::graphite::Recorder* recorder_ = nullptr; // Non-owning — owned by SkiaSurface
580 GrDirectContext* gr_context_ = nullptr; // Non-owning — owned by the GL host
581 Color fill_color_ = Color::rgba(1.0f, 1.0f, 1.0f);
582 Color stroke_color_ = Color::rgba(1.0f, 1.0f, 1.0f);
583 float line_width_ = 1.0f;
584 LineCap line_cap_ = LineCap::butt;
585 LineJoin line_join_ = LineJoin::miter;
586 // Canvas2D ctx.miterLimit. Spec default is 10 (matches Skia's SkPaint
587 // default).
588 float miter_limit_ = 10.0f;
589 // Canvas2D ctx.imageSmoothingEnabled / ctx.imageSmoothingQuality. Defaults
590 // match the spec (`true`, `"low"`). Honored by draw_image_from_data /
591 // draw_image_from_file.
592 bool image_smoothing_enabled_ = true;
593 ImageSmoothingQuality image_smoothing_quality_ = ImageSmoothingQuality::low;
594 std::string font_family_ = "sans-serif";
595 int font_weight_ = 400;
596 int font_slant_ = 0;
597 float letter_spacing_ = 0.0f;
599 FontSmoothing font_smoothing_ = FontSmoothing::auto_smoothing;
600 bool glyph_hinting() const {
601 return font_smoothing_ != FontSmoothing::antialiased;
602 }
606 struct SmoothingScope {
607 int save_count;
608 FontSmoothing previous;
609 };
610 std::vector<SmoothingScope> smoothing_scopes_;
612 void unwind_font_smoothing();
613 // OpenType feature flags (e.g. tnum / smcp / onum /
614 // lnum / pnum) for CSS font-variant. Captured by set_font_features
615 // / cleared by clear_font_features. Applied via
616 // `TextStyle::addFontFeature` in `make_paragraph()` at
617 // fill_text / stroke_text / measure_text time. Empty vector → no
618 // OpenType features set.
619 std::vector<FontFeature> font_features_;
620
621 // Pending mask state for save_layer_with_mask.
622 // Each entry is the deferred mask composite to apply when the
623 // matching restore() fires. Tracked by Skia's getSaveCount() so
624 // restore() can look up "is the layer I'm about to close a
625 // mask layer". sk_sp<SkShader> may be null when the mask string
626 // didn't parse — restore() then just closes the layer plainly.
627 struct PendingMask {
628 sk_sp<class SkShader> shader; // forward-declared at top of file
629 struct { float left, top, right, bottom; } bounds;
630 int save_count_after_open; // canvas_->getSaveCount() AFTER saveLayer
631 };
632 std::vector<PendingMask> pending_masks_;
633
634public:
635 // Returns true when at least one currently-open save_layer* has alpha < 1
636 // on its layer-paint. Text paint paths (fill_text, stroke_text) consult this
637 // at paint time and select greyscale AA over LCD subpixel AA: Skia's LCD
638 // subpixel patterns can't antialias correctly into a partially transparent
639 // pixel, so glyphs render faint inside CSS-opacity layers without this flip.
640 // Browsers (Blink / WebKit) do the same. Exposed publicly so tests can
641 // assert the stack-tracking around nested save_layer / restore /
642 // restore_to_count.
643 bool inside_non_opaque_layer() const {
644 return !non_opaque_layer_stack_.empty();
645 }
646
647private:
648 // Each entry is Skia's getSaveCount() after the layer was opened.
649 // restore() pops the top entry if its save count matches the canvas's
650 // current save count; restore_to_count() pops any entries strictly above
651 // the target (mirrors SkCanvas::restoreToCount). Plain save() / save_layer
652 // with opacity == 1 do not push.
653 std::vector<int> non_opaque_layer_stack_;
654
655 TextAlign text_align_ = TextAlign::left;
656
657 // Gradient state
658 bool has_gradient_ = false;
659 sk_sp<SkShader> gradient_shader_;
660
661 // Canvas2D ctx.createPattern stroke shader. Lives separately from
662 // gradient_shader_ so stroke paths can opt in via apply_stroke_state without
663 // disturbing fill state.
664 sk_sp<SkShader> stroke_shader_;
665
666 // Path building state
667 std::unique_ptr<SkPathBuilder> path_builder_;
668
669 // Blend mode
670 SkBlendMode blend_mode_ = SkBlendMode::kSrcOver;
671
672 // Paint-baseline matrix — captured at CanvasWidget::paint() entry so that
673 // JS-driven setTransform() composes onto the inbound View transform
674 // instead of overwriting it. Defaults to identity for callers (e.g.
675 // screenshot host) that drive SkiaCanvas directly without going through
676 // CanvasWidget — there setTransform behaves per the HTML spec literally.
677 SkMatrix paint_baseline_ = SkMatrix::I();
678
679 // Line-dash pattern. Empty == solid stroke (no dashing).
680 // Held as floats so the stroke-paint helper can rebuild the SkDashPathEffect
681 // each time stroke_color/line_width change.
682 std::vector<float> line_dash_;
683 float line_dash_phase_ = 0.0f;
684
685 // Canvas2D drop-shadow state. Sticky: every
686 // draw helper queries `make_shadow_filter()` and, when a shadow is
687 // active, wraps its paint via `SkPaint::setImageFilter`. The shadow
688 // is gated on color.a > 0 AND (blur > 0 OR dx != 0 OR dy != 0) to
689 // match Canvas2D spec ("if all four shadow attributes are at their
690 // defaults, no shadow is rendered"). Default = transparent black
691 // shadow with zero blur + zero offset = inactive.
692 Color shadow_color_ = Color::rgba(0.0f, 0.0f, 0.0f, 0.0f);
693 float shadow_blur_ = 0.0f;
694 float shadow_offset_x_ = 0.0f;
695 float shadow_offset_y_ = 0.0f;
696
697 // Canvas2D ctx.direction sticky state. Defaults to ltr, rtl flips the
698 // shaper flag, and inherit currently behaves as ltr until a per-View
699 // writing-direction lookup lands.
700 TextDirection direction_ = TextDirection::ltr;
701
702 // Canvas2D ctx.filter sticky state. Stored as the raw
703 // CSS string for round-trip diagnostics; the parsed SkImageFilter
704 // chain caches alongside so we don't re-parse on every draw. Both
705 // are reset together by set_filter(). When `filter_image_filter_`
706 // is null we skip the wrap entirely (the "none" / no-op path).
707 std::string filter_source_ = "none";
708 sk_sp<SkImageFilter> filter_image_filter_;
709
710 // ── Retained layer state (skia_canvas_path.cpp) ──────────────────────
711 // A sealed layer is an SkImage that survives past end_layer(). Cacheable
712 // layers stay in the renderer-owned store across ordinary frames and
713 // per-frame canvases. Explicit invalidation, a backing-scale change, or a
714 // GPU context change drops them; callers recheck layer_valid() each frame.
715 // Non-cacheable ones are dropped the first time they are drawn.
716 struct SkiaLayer {
717 sk_sp<SkImage> image;
718 sk_sp<SkSurface> surface;
719 Rect2D bounds;
720 float scale_x = 1.0f;
721 float scale_y = 1.0f;
722 bool cacheable = false;
723 };
724 std::shared_ptr<RetainedLayerStore> retained_layers_;
725 bool retained_layer_cache_supported_ = false;
726
729 struct OpenLayer {
730 uint64_t id = 0;
731 SkCanvas* previous_canvas = nullptr;
732 SkiaLayer layer;
733 bool cancelled = false;
734 };
735 std::vector<OpenLayer> open_layers_;
736
737 SkiaLayer* find_layer(uint64_t id);
738 const SkiaLayer* find_layer(uint64_t id) const;
739 void bind_retained_layer_store(const void* owner, uint8_t backend_kind);
741 void draw_layer_common(LayerHandle layer, const SkMatrix& placement,
742 float alpha, BlendMode mode);
743
744public:
745 // Wrap an arbitrary paint with the active ctx.filter
746 // SkImageFilter chain (no-op when filter is "none" or unparsable).
747 // Centralised so fill / stroke / text / image draws can opt in
748 // without touching every call site.
749 // Public for test_canvas.cpp filter-pipeline tests; the implementation is a
750 // pure paint-state mutation, no class invariant gets violated by external
751 // callers.
752 void apply_filter(SkPaint& paint) const;
753};
754
755} // namespace pulp::canvas
756
757#endif // PULP_HAS_SKIA
Definition canvas.hpp:240
@ cancelled
Definition status.hpp:61
Definition affine_transform.hpp:24
TextAlign
Definition canvas.hpp:221
@ right
Definition canvas.hpp:221
@ left
Definition canvas.hpp:221
@ bottom
Definition canvas.hpp:222
@ top
Definition canvas.hpp:222
FontSmoothing
CSS -webkit-font-smoothing as it affects glyph rasterization on macOS.
Definition text_raster_policy.hpp:40
RendererBackend
Runtime identity of the concrete renderer receiving draw calls.
Definition canvas.hpp:29
TextDirection
Definition canvas.hpp:231
LineJoin
Join applied where two segments meet when stroking.
Definition path.hpp:60
@ image
Definition scene_node.hpp:95
LineCap
Line cap applied to the ends of open subpaths when stroking.
Definition path.hpp:57
void apply_filter(HostQuirks &q, QuirkFilter filter)
In-place: zero out (reset to default) every field in q whose meta tier is not allowed by filter.
canvas::Color Color
Definition theme.hpp:12
@ bounds
Definition waveform_editor_primitives.hpp:192
Definition skia_surface.hpp:16