librsvg source for verification 2026-05-22
This commit is contained in:
686
devel-docs/adding_a_property.rst
Normal file
686
devel-docs/adding_a_property.rst
Normal file
@@ -0,0 +1,686 @@
|
||||
How to add a new CSS property
|
||||
=============================
|
||||
|
||||
This document is a little tour on how to add support for a CSS property
|
||||
to librsvg. We will implement the |mask-type|_ property from the
|
||||
**CSS Masking Module Level 1** specification.
|
||||
|
||||
What is ``mask-type``?
|
||||
----------------------
|
||||
|
||||
The spec says about |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>|_
|
||||
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!
|
||||
|
||||
|
||||
|
||||
.. See https://docutils.sourceforge.net/FAQ.html#is-nested-inline-markup-possible
|
||||
|
||||
.. |mask-type| replace:: ``mask-type``
|
||||
.. _mask-type: https://www.w3.org/TR/css-masking-1/#the-mask-type
|
||||
|
||||
.. |<paint>| replace:: ``<paint>``
|
||||
.. _<paint>: https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint
|
||||
Reference in New Issue
Block a user