summaryrefslogtreecommitdiff
path: root/devel-docs/adding_a_property.rst
blob: b8ade9855b434ae69a1326001b81f0e889ab0188 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
Adding a new CSS property to librsvg
====================================

This document is a little tour on how to add support for a CSS property
to librsvg. We will implement the ```mask-type``
property <https://www.w3.org/TR/css-masking-1/#the-mask-type>`__ from
the **CSS Masking Module Level 1** specification.

What is ``mask-type``?
----------------------

`The spec says about
``mask-type`` <https://www.w3.org/TR/css-masking-1/#the-mask-type>`__:

   The mask-type property defines whether the content of the mask
   element is treated as as luminance mask or alpha mask, as described
   in Calculating mask values.

A **luminance mask** takes the RGB values of each pixel, converts them
to a single luminance value, and uses that as a mask.

An **alpha mask** just takes the alpha value of each pixel and uses it
as a mask.

The only mask type that SVG1.1 supported was luminance masks; there
wasn’t even a ``mask-type`` property back then. The SVG2 spec removed
descriptions of masking, and offloaded them to the `CSS Masking Module
Level 1 <https://www.w3.org/TR/css-masking-1/>`__ specification, which
it adds the ``mask-type`` property and others as well.

Let’s start by figuring out how to read the spec.

What the specification says
---------------------------

The specification for ``mask-type`` is in
https://www.w3.org/TR/css-masking-1/#the-mask-type

In the specs, most of the descriptions for properties start with a table
that summarizes the property. For example, if you visit that link, you
will find a table that starts with these items:

-  **Name:** ``mask-type``
-  **Value:** ``luminance | alpha``
-  **Initial:** ``luminance``
-  **Applies to:** mask elements
-  **Inherited:** no
-  **Computed value:** as specified

Let’s go through each of these:

**Name:** We have the name of the property (``mask-type``). Properties
are case-insensitive, and librsvg already has machinery to handle that.

**Value:** The possible values for the property can be ``luminance`` or
``alpha``. In the spec’s web page, even the little ``|`` between those
two values is a hyperlink; clicking it will take you to the
specification for CSS Values and Units, where it describes the grammar
that the CSS specs use to describe their values. Here you just need to
know that ``|`` means that exactly one of the two alternatives must
occur.

As you may imagine, librsvg already parses a lot of similar properties
that are just symbolic values. For example, the ``stroke-linecap``
property can have values ``butt | round | square``. We’ll see how to
write a parser for this kind of property with a minimal amount of code.

**Initial:** Then there is the initial or default value, which is
``luminance``. This means that if the ``mask-type`` property is not
specified on an element, it takes ``luminance`` as its default. This is
a sensible choice, since an SVG1.1 file that is processed by SVG2
software should retain the same semantics. It also means that if there
is a parse error, for example if you typed ``ahlpha``, the property will
silently revert back to the default ``luminance`` value.

**Applies to:** Librsvg doesn’t pay much attention to “applies to” — it
just carries property values for all elements, and the elements that
don’t handle a property just ignore it.

**Inherited:** This property is not inherited, which means that by
default, its value does not cascade. So if you have this:

.. code:: xml

   <mask style="mask-type: alpha;">
     <other>
       <elements>
         <here/>
       </elements>
     </other>
   </mask>

Then the ``other``, ``elements``, ``here`` will not inherit the
``mask-type`` value from their ancestor.

**Computed value:** Finally, the computed value is “as specified”, which
means that librsvg does not need to modify it in any way when resolving
the CSS cascade. Other properties, like ``width: 1em;`` may need to be
resolved against the ``font-size`` to obtain the computed value.

The W3C specifications can get pretty verbose and it takes some practice
to read them, but fortunately this property is short and sweet.

Let’s go on.

How librsvg represents properties
---------------------------------

Each property has a Rust type that can hold its values. Remember the
part of the masking spec from above, that says the ``mask-type``
property can have values ``luminance`` or ``alpha``, and the
initial/default is ``luminance``? This translates easily to Rust types:

.. code:: rust

   #[derive(Debug, Copy, Clone, PartialEq)]
   pub enum MaskType {
       Luminance,
       Alpha,
   }

   impl Default for MaskType {
       fn default() -> MaskType {
           MaskType::Luminance
       }
   }

Additionally, we need to be able to say that the property does not
inherit by default, and that its computed value is the same as the
specified value (e.g. we can just copy the original value without
changing it). Librsvg defines a ``Property`` trait for those actions:

.. code:: rust

   pub trait Property {
       fn inherits_automatically() -> bool;

       fn compute(&self, _: &ComputedValues) -> Self;
   }

For the ``mask-type`` property, we want ``inherits_automatically`` to
return ``false``, and ``compute`` to return the value unchanged. So,
like this:

.. code:: rust

   impl Property for MaskType {
       fn inherits_automatically() -> bool {
           false
       }

       fn compute(&self, _: &ComputedValues) -> Self {
           self.clone()
       }
   }

Ignore the ``ComputedValues`` argument for now — it is how librsvg
represents an element’s complete set of property values.

As you can imagine, there are a lot of properties like ``mask-type``,
whose values are just symbolic names that map well to a data-less enum.
For all of them, it would be a lot of repetitive code to define their
default value, return whether they inherit or not, and clone them for
the computed value. Additionally, we have not even written the parser
for this property’s values yet.

Fortunately, librsvg has a ``make_property!`` macro that lets you do
this instead:

.. code:: rust

   make_property!(
       /// `mask-type` property.                                          // (1)
       ///
       /// https://www.w3.org/TR/css-masking-1/#the-mask-type
       MaskType,                                                          // (2)
       default: Luminance,                                                // (3)
       inherits_automatically: false,                                     // (4)

       identifiers:                                                       // (5)
       "luminance" => Luminance,
       "alpha" => Alpha,
   );

-  

   (1) is a documentation comment for the ``MaskType`` enum being
       defined.

-  

   (2) is ``MaskType``, the name we will use for the ``mask-type``
       property.

-  

   (3) indicates the “initial value”, or default, for the property.

-  

   (4) … whether the spec says the property should inherit or not.

-  

   (5) Finally, ``identifiers:`` is what makes the ``make_property!``
       macro know that it should generate a parser for the symbolic
       names ``luminance`` and ``alpha``, and that they should
       correspond to the values ``MaskType::Luminance`` and
       ``MaskType::Alpha``, respectively.

This saves a lot of typing! Also, it makes it easier to gradually change
the way properties are represented, as librsvg evolves.

Properties that use the same data type
--------------------------------------

Consider the ``stroke`` and ``fill`` properties; both store a
```<paint>`` <https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint>`__
value, which librsvg represents with a type called ``PaintServer``. The
``make_property!`` macro has a case for properties like that, so in the
librsvg source code you will find both of thsese:

.. code:: rust

   make_property!(
       /// `fill` property.
       ///
       /// https://www.w3.org/TR/SVG/painting.html#FillProperty
       ///
       /// https://www.w3.org/TR/SVG2/painting.html#FillProperty
       Fill,
       default: PaintServer::parse_str("#000").unwrap(),
       inherits_automatically: true,
       newtype_parse: PaintServer,
   );

   make_property!(
       /// `stroke` property.
       ///
       /// https://www.w3.org/TR/SVG2/painting.html#SpecifyingStrokePaint
       Stroke,
       default: PaintServer::None,
       inherits_automatically: true,
       newtype_parse: PaintServer,
   );

The ``newtype_parse:`` is what tells the macro that it should generate a
newtype like ``struct Stroke(PaintServer)``, and that it should just use
the parser that ``PaintServer`` already has.

Which parser is that? Read on.

Custom parsers
--------------

Librsvg has a ``Parse`` trait for property values which looks rather
scary:

.. code:: rust

   pub trait Parse: Sized {
       fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>>;
   }

Don’t let the lifetimes scare you. They are required because of
``cssparser::Parser``, from the ``cssparser`` crate, tries really hard
to let you implement zero-copy parsers, which give you string tokens as
slices from the original string being parsed, instead of allocating lots
of little ``String`` values. What this ``Parse`` trait means is, you get
tokens out of the ``Parser``, and return what is basically a
``Result<Self, Error>``.

In this tutorial we will just show you the parser for simple numeric
types, for example, for properties that can just be represented with an
``f64``. There is the ``stroke-miterlimit`` property defined like this:

.. code:: rust

   make_property!(
       /// `stroke-miterlimit` property.
       ///
       /// https://www.w3.org/TR/SVG2/painting.html#StrokeMiterlimitProperty
       StrokeMiterlimit,
       default: 4f64,
       inherits_automatically: true,
       newtype_parse: f64,
   );

And the ``impl Parse for f64`` looks like this:

.. code:: rust

   impl Parse for f64 {
       fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
           let loc = parser.current_source_location();                                          // (1)
           let n = parser.expect_number()?;                                                     // (2)
           if n.is_finite() {                                                                   // (3)
               Ok(f64::from(n))                                                                 // (4)
           } else {
               Err(loc.new_custom_error(ValueErrorKind::value_error("expected finite number"))) // (5)
           }
       }
   }

-  

   (1) Store the current location in the parser.

-  

   (2) Ask the parser for a number. If a non-numeric token comes out
       (e.g. if the user put ``stroke-miterlimit: foo`` instead of
       ``stroke-miterlimit: 5``), ``expect_number`` will return an
       ``Err``, which we propagate upwards with the ``?``.

-  

   (3) Check the number for being non-infinite or NaN….

-  

   (4) … and return the number converted to f64 (``cssparser`` returns
       f32, but we promote them so that subsequent calculations can use
       the extra precision)…

-  

   (5) … or return an error based on the location from (1).

My advice: implement new parsers by doing cut&paste from existing ones,
and you’ll be okay.

Registering the property
------------------------

Okay! We defined ``MaskType`` and its symbolic identifiers with the
``make_property!`` macro, and the macro took care of writing a parser
for it and implementing the traits that the property needs.

Now we need to modify the code in a few places to process the property.

Register the property
---------------------

-  First, look for ``longhands:`` in ``properties.rs``. You will find
   that it is part of a long macro invocation:

.. code:: rust

   make_properties! {
       // ... stuff omitted here

       longhands: {
          // ... stuff omitted here

           "marker-end"                  => (PresentationAttr::Yes, marker_end                  : MarkerEnd),
           "marker-mid"                  => (PresentationAttr::Yes, marker_mid                  : MarkerMid),
           "marker-start"                => (PresentationAttr::Yes, marker_start                : MarkerStart),
           "mask"                        => (PresentationAttr::Yes, mask                        : Mask),
           // "mask-type"                => (PresentationAttr::Yes, unimplemented),
           "opacity"                     => (PresentationAttr::Yes, opacity                     : Opacity),
           "overflow"                    => (PresentationAttr::Yes, overflow                    : Overflow),

           // ... stuff omitted here
       }
   }

In there, there is an entry for ``mask-type`` commented out. Let’s
uncomment it and turn it into this:

.. code:: rust

           "mask-type"                   => (PresentationAttr::Yes, mask_type                   : MaskType),

``PresentationAttr::Yes`` indicates whether the property has a
corresponding presentation attribute. This means that you can do
``<mask style="mask-type: alpha;">`` which is property, as well as
``<mask mask-type="alpha">``, which is a presentation attribute.

How did we find out that ``mask-type`` also exists as a presentation
attribute? Well, `the
spec <https://www.w3.org/TR/css-masking-1/#the-mask-type>`__ says:

   The mask-type property is a presentation attribute for SVG elements.

But wait! If we compile, we get this:

::

   error: no rules expected the token `"mask-type"`
      --> src/properties.rs:450:9
       |
   450 |         "mask-type"                   => (PresentationAttr::Yes, mask_type                   : MaskType),
       |         ^^^^^^^^^^^ no rules expected this token in macro call

When you see that error in exactly that macro invocation, it means this:
librsvg uses a crate called ``markup5ever`` to have a compact
representation of the names of properties/attributes/elements. It uses
string interning so that, for example, there is a single definition of
``rect`` in the program’s heap instead of there being a thousands of
duplicated ``rect`` strings when you load a big document. The thing is,
``markup5ever`` only has ready-made definitions of the most common
HTML/SVG/CSS names, but unfortunately ``mask-type`` is not one of them.

So, we scroll down in ``properties.rs`` and move the ``mask-type``
registration there:

.. code:: rust

       longhands_not_supported_by_markup5ever: {
           "line-height"                 => (PresentationAttr::No,  line_height                 : LineHeight),
           "mask-type"                   => (PresentationAttr::Yes, mask_type                   : MaskType),     // <- right here
           "mix-blend-mode"              => (PresentationAttr::No,  mix_blend_mode              : MixBlendMode),
           "paint-order"                 => (PresentationAttr::Yes, paint_order                 : PaintOrder),
       }

That block named ``longhands_not_supported_by_markup5ever`` is, well,
exactly what it says — a separate section with property names that are
not built into ``markup5ever``, so they must be dealt with specially.
Just put the property there and that’s it.

Next, we have to calculate the computed value for the property.

Calculate the computed value
----------------------------

In ``properties.rs``, look for ``compute!``. You will find many
invocations of this macro:

.. code:: rust

           compute!(MarkerEnd, marker_end);
           compute!(MarkerMid, marker_mid);
           compute!(MarkerStart, marker_start);
           compute!(Mask, mask);
           compute!(MixBlendMode, mix_blend_mode);
           compute!(Opacity, opacity);
           compute!(Overflow, overflow);

Add a call for ``MaskType``:

.. code:: rust

           compute!(MarkerEnd, marker_end);
           compute!(MarkerMid, marker_mid);
           compute!(MarkerStart, marker_start);
           compute!(Mask, mask);
           compute!(MaskType, mask_type);          // this is new
           compute!(MixBlendMode, mix_blend_mode);
           compute!(Opacity, opacity);
           compute!(Overflow, overflow);

You will see that all those calls to ``compute!`` are inside a method
called ``SpecifiedValues::to_computed_values()``. This method is run as
part of the CSS cascade: it takes the ``SpecifiedValues`` from an
element and composes them onto the ``ComputedValues`` from its parent
element. For example, if you have a document with this bit:

.. code:: xml

   <g stroke="red" fill="blue">     // ComputedValues with stroke:red, fill:blue
     <rect fill="green"/>           // SpecifiedValues with fill:green
   </g>

The ``ComputedValues`` that results from the ``<g>`` will have
properties ``stroke:red`` and ``fill:blue`` in it. The
``SpecifiedValues`` from the ``<rect>`` just has ``fill:green``.
Composing them together for the ``<rect>`` gives us ``ComputedValues``
with ``stroke:red`` and ``fill:green``.

Now that the property is registered, we can actually handle it in the
drawing code!

Handling the property
---------------------

First, a digression: let’s change the name of a few methods to better
reflect what the new structure of the code will be like.

There are a few methods called ``to_mask`` in the code, that take an
RGBA surface and turn it into an Alpha-only surface with the luminance
of the original surface; and also the corresponding method to do this
for a single pixel. Let’s do this kind of renaming:

::

   -    pub fn to_mask(&self, opacity: UnitInterval) -> Result<SharedImageSurface, cairo::Error> {
   +    pub fn to_luminance_mask(&self, opacity: UnitInterval) -> Result<SharedImageSurface, cairo::Error> {

Librsvg only effectively supported ``mask-type: luminance`` since that
is what was in SVG1.1, but now for SVG2 we want to add behavior for
``mask-type: alpha`` as well. So, it makes sense to rename ``to_mask``
as ``to_luminance_mask``.

``SharedImageSurface`` is the type that librsvg uses to represent images
in memory. They can be RGBA or Alpha-only. There is already a method
called ``extract_alpha`` that we can use to create an Alpha-only mask:

.. code:: rust

   // there's a type alias SharedImageSurface for this
   impl ImageSurface<Shared> {
       pub fn extract_alpha(&self, bounds: IRect) -> Result<SharedImageSurface, cairo::Error> { ... }
   }

Now let’s look at where ``drawing_ctx.rs`` has this:

.. code:: rust

           let mask = SharedImageSurface::wrap(mask_content_surface, SurfaceType::SRgb)?    // (1)
               .to_luminance_mask()?                                                        // (2)
               .into_image_surface()?;                                                      // (3)

-  

   (1) Wraps a ``SharedImageSurface`` around the Cairo surface that was
       just rendered with the mask contents.

-  

   (2) Converts it to a luminance mask. We will need to change this!

-  

   (3) Extracts the Cairo image surface from the ``SharedImageSurface``,
       for further processing.

Remember the ``ComputedValues`` where we had the ``mask_type``? We can
extract it with ``values.mask_type()``. Now let’s change the lines above
to this:

.. code:: rust

           let tmp = SharedImageSurface::wrap(mask_content_surface, SurfaceType::SRgb)?;

           let mask_result = match values.mask_type() {
               MaskType::Luminance => tmp.to_luminance_mask()?,
               MaskType::Alpha => tmp.extract_alpha(IRect::from_size(tmp.width(), tmp.height()))?,
           };

           let mask = mask_result.into_image_surface()?;

But wait! We don’t have a test for this yet! Aaaaaargh, we are doing
test-driven development backwards!

No biggie. Let’s write the tests.

Adding tests
------------

Testing graphical output is really annoying if you compare PNG files,
because any time Cairo changes something and antialiasing changes
juuuuuust a bit, the tests break. So, librsvg tries to do “reftests”, or
reference tests, by comparing the rendered results of two things:

-  The SVG you actually want to test.
-  An equivalent SVG that works only with known-good features.

For ``mask-type``, we need an SVG document that actually uses that
property with both of its values, and another document that produces the
same results but with simpler primitives.

Librsvg already has tests for luminance masks, as they were the only
available kind in SVG1.1. So we can be confident that they already work
- we just need to test that the presence of ``mask-type="luminance"``
actually does the same thing.

First, let’s dissect the SVG that we want to test:

.. code:: xml

   <?xml version="1.0" encoding="UTF-8"?>
   <svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">
     <mask id="luminance" mask-type="luminance" maskContentUnits="objectBoundingBox">
       <rect x="0.1" y="0.1" width="0.8" height="0.8" fill="white"/>
     </mask>
     <mask id="alpha" mask-type="alpha" maskContentUnits="objectBoundingBox">
       <rect x="0.1" y="0.1" width="0.8" height="0.8" fill="black"/>
     </mask>

     <rect x="0" y="0" width="100" height="100" fill="green" mask="url(#luminance)"/>

     <rect x="100" y="0" width="100" height="100" fill="green" mask="url(#alpha)"/>
   </svg>

The image has two 100x100 ``green`` squares side by side. The one on the
left gets masked with the ``luminance`` mask, which reduces it to an
80x80 rectangle. That mask is a **white** square, so its has full
luminance at every pixel.

The square on the right gets masked with the ``alpha`` mask. That mask
is a **black** square, but with alpha=1.0, so it should produce the same
result as the first one.

Note that to make things easy, we use **white** for the luminance mask.
White pixels have full luminance (1.0), which gets used as the mask.
Conversely, we use **black** for the alpha mask. Those black pixels are
fully opaque, and since ``mask-type="alpha"`` only considers the alpha
channel, it will be using the full opacity of each pixel (1.0), which
also gets used as the mask. So, the masks should be equivalent.

Okay! Now let’s write the reference SVG, the one built out of simpler
elements but that should produce the same rendering:

.. code:: xml

   <?xml version="1.0" encoding="UTF-8"?>
   <svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">
     <rect x="10" y="10" width="80" height="80" fill="green"/>

     <rect x="110" y="10" width="80" height="80" fill="green"/>
   </svg>

This is just the two original squares, but already clipped or masked to
the final result.

Now, where do we put those SVG documents for the tests?

Near the end of ``tests/src/filters.rs`` we can include this:

.. code:: rust

   test_compare_render_output!(
       mask_type,
       200,
       100,
       br##"<?xml version="1.0" encoding="UTF-8"?>
   <svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">
     <mask id="luminance" mask-type="luminance" maskContentUnits="objectBoundingBox">
       <rect x="0.1" y="0.1" width="0.8" height="0.8" fill="white"/>
     </mask>
     <mask id="alpha" mask-type="alpha" maskContentUnits="objectBoundingBox">
       <rect x="0.1" y="0.1" width="0.8" height="0.8" fill="black"/>
     </mask>

     <rect x="0" y="0" width="100" height="100" fill="green" mask="url(#luminance)"/>

     <rect x="100" y="0" width="100" height="100" fill="green" mask="url(#alpha)"/>
   </svg>
   "##,
       br##"<?xml version="1.0" encoding="UTF-8"?>
   <svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">
     <rect x="10" y="10" width="80" height="80" fill="green"/>

     <rect x="110" y="10" width="80" height="80" fill="green"/>
   </svg>
   "##,
   );

Here, ``test_compare_render_output!`` is a macro that takes two SVG
documents, the test and the reference, and compares their rendered
results. It also takes a test name (``mask_type`` in this case), and the
pixel size of the image to generate for testing (200x100).

Final steps: documentation
--------------------------

To help people who are wondering what SVG features are supported in
librsvg, there is a ``FEATURES.md`` file. It has a section called “CSS
properties” with a big list of property names and notes about them.

We’ll patch it like this:

::

    | marker-mid                  |                                                        |
    | marker-start                |                                                        |
    | mask                        |                                                        |
   +| mask-type                   |                                                        |
    | mix-blend-mode              | Not available as a presentation attribute.             |
    | opacity                     |                                                        |
    | overflow                    |                                                        |

There is nothing remarkable about ``mask-type``, it is a plain old
property that also has a presentation attribute (remember the
``PresentationAttr::Yes`` from above?), so we don’t need to list any
extra information.

And with that, we are done implementing ``mask-type``. Have fun!