diff options
author | Marge Bot <marge-bot@gnome.org> | 2022-08-26 01:20:53 +0000 |
---|---|---|
committer | Marge Bot <marge-bot@gnome.org> | 2022-08-26 01:20:53 +0000 |
commit | d823f14c26881d38803bb04829d1c008b7b74f98 (patch) | |
tree | e1d6d9211b40d7dd6007f98df13b0d9bc33faf94 | |
parent | 5658411389a75da7141bdfba57fc965389e7bbec (diff) | |
parent | d01fb71464c4bab0bcf0b0811005ff2eccbea7a4 (diff) | |
download | librsvg-d823f14c26881d38803bb04829d1c008b7b74f98.tar.gz |
Merge branch 'devel-docs' into 'main'
Move more content to the development guide
Closes #886
See merge request GNOME/librsvg!735
-rw-r--r-- | ARCHITECTURE.md | 407 | ||||
-rw-r--r-- | Cargo.lock | 250 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | Makefile.am | 2 | ||||
-rw-r--r-- | NEWS (renamed from NEWS.md) | 0 | ||||
-rw-r--r-- | README.md | 6 | ||||
-rw-r--r-- | RELEASING.md | 219 | ||||
-rw-r--r-- | devel-docs/README.md | 11 | ||||
-rw-r--r-- | devel-docs/adding-a-property.md | 587 | ||||
-rw-r--r-- | devel-docs/adding_a_property.rst | 679 | ||||
-rw-r--r-- | devel-docs/architecture.rst | 474 | ||||
-rw-r--r-- | devel-docs/index.rst | 52 | ||||
-rw-r--r-- | devel-docs/releasing.rst | 241 |
13 files changed, 1577 insertions, 1352 deletions
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index 7a9e7e3c..00000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,407 +0,0 @@ -Architecture of librsvg -======================= - -This document roughly describes the architecture of librsvg, and -future plans for it. The code is continually evolving, so don't -consider this as the ground truth, but rather like a cheap map you buy -at a street corner. - -The library's internals are documented as Rust documentation comments; -you can look at the rendered version at -https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/index.html - -You may also want to see the section below on [interesting parts of -the code](#some_interesting_parts_of_the_code). - -# A bit of history - -Librsvg is an old library. It started around 2001, when Eazel (the -original makers of GNOME's file manager Nautilus) needed a library to -render SVG images. At that time the SVG format was being -standardized, so librsvg grew along with the SVG specification. This -is why you will sometimes see references to deprecated SVG features in -the source code. - -Librsvg started as an experiment to use libxml2's new SAX parser, so -that SVG could be streamed in and rendered on the fly, instead of -first creating a DOM tree. Originally it used [libart] as a rendering -library; this was GNOME's first antialiased renderer with alpha -compositing. Later, the renderer was replaced with [Cairo]. Librsvg -is currently striving to support other rendering backends. - -(These days librsvg indeed builds a DOM tree by itself; it needs the -tree to run the CSS cascade and do selector matching.) - -Librsvg started as a C library with an ad-hoc API. At some point it -got turned into a [GObject] library, so that the main `RsvgHandle` -object defines most of the entry points into the library. Through -[GObject Introspection], this allows librsvg to be used from other -programming languages. - -In 2016, librsvg started getting ported to Rust. As of early 2021, -the whole library is implemented in Rust, and exports an intact C -API/ABI. It also exports a more idiomatic Rust API as well. - -[libart]: https://levien.com/libart/ -[Cairo]: https://www.cairographics.org/ -[GObject Introspection]: https://gi.readthedocs.io/en/latest/ -[gi]: https://people.gnome.org/~federico/blog/magic-of-gobject-introspection.html - -# The C and Rust APIs - -Librsvg exports two public APIs, one for C and one for Rust. - -The C API has hard requirements for API/ABI stability, because it is -used all over the GNOME project and API/ABI breaks would be highly -disruptive. Also, the C API is what allows librsvg to be called from -other programming languages, through [GObject Introspection]. - -The Rust API is a bit more lax in its API stability, but we try to -stick to [semantic versioning][semver] as is common in Rust. - -The public Rust API is implemented in `src/api.rs`. This has all the -primitives needed to load and render SVG documents or individual -elements, and to configure loading/rendering options. - -The public C API is implemented in `src/c_api/`, and it is implemented -in terms of the public Rust API. Note that as of 2021/Feb the -corresponding C header files are hand-written in `include/librsvg/`; -maybe in the future they will be generated automatically with -[cbindgen]. - -We consider it good practice to provide simple and clean primitives in -the Rust API, and have `c_api` deal with all the idiosyncrasies and -historical considerations for the C API. - -``` -+----------------+ -| Public C API | -| src/c_api | -+----------------+ - | - calls - | - v -+-------------------+ -| Public Rust API | -| src/api.rs | -+-------------------+ - | - calls - | - v -+-------------------+ -| library internals | -| src/*.rs | -+-------------------+ -``` - -[semver]: https://semver.org/ -[cbindgen]: https://github.com/eqrion/cbindgen/blob/master/docs.md - -# The test suite - -The test suite is documented in `tests/README.md`. - -# Code flow - -The caller of librsvg loads a document into a handle, and later may -ask to render the document or one of its elements, or measure their -geometries. - -## Loading an SVG document - -The Rust API starts by constructing an `SvgHandle` from a `Loader`; -both of those are public types. Internally the `SvgHandle` is just a -wrapper around a `Handle`, which is a private type. `Handle` -represents an SVG document loaded in memory; it acts as a wrapper -around a `Document`, and provides the basic primitive operations like -"render the whole document" or "compute the geometry of an element" -that are needed to implement the public APIs. - -A `Document` gets created by loading XML from a stream, into a tree of -`Node` structures. This is similar to a web browser's DOM tree. - -Each XML element causes a new `Node` to get created with an `Element` -in it. The `Element` enum can represent all the SVG element types; -for example, a `<path>` element from XML gets turned into a -`Node::Element(Element::Path)`. - -When an `Element` is created from its corresponding XML, its -`Attributes` get parsed. On one hand, attributes that are specific to -a particular element type, like the `d` in `<path d="...">` get parsed -by the `set_attributes` method of each particular element type (in -that case, `Path::set_attributes`). - -On the other hand, attributes that refer to styles, and which may -appear for any kind of element, get all parsed into a -`SpecifiedValues` struct. This is a memory-efficient representation -of the CSS style properties that an element has. - -There is a lot of parsing going on; see below for the details of -[parsing](#parsing). - -When the XML document is fully parsed, a `Document` contains a tree of -`Node` structs and their inner `Element` structs. The tree has also -been validated to ensure that the root is an `<svg>` element. - -After that, the CSS cascade step gets run. - -## The CSS cascade - -Each `Element` has a `SpecifiedValues`, which has the CSS style -properties that the XML specified for that element. However, -`SpecifiedValues` is sparse, as not all the possible style properties -may have been filled in. Cascading means following the CSS/SVG rules -for each property type to inherit missing properties from parent -elements. For example, in this document fragment: - -``` -<g stroke-width="2" stroke="black"> - <path d="M0,0 L10,0" fill="blue"/> - <path d="M20,0 L30,0" fill="green"/> -</g> -``` - -Each `<path>` element has a different fill color, but they both -*inherit* the `stroke-width` and `stroke` values from their parent -group. This is because both the `stroke-width` and `stroke` -properties are defined in the CSS/SVG specifications to inherit -automatically. Some other properties, like `opacity`, do not inherit -and are thus not copied to child elements. - -In librsvg, the individual types for CSS properties are defined with -the `make_property` macro. - -The cascading step takes each element's `SpecifiedValues` and composes -it by CSS inheritance onto a `ComputedValues`. The latter is a -memory-efficient representation of all the possible CSS properties -that an element can have. - -When cascading is done, each `Element` has a fully resolved -`ComputedValues` struct, which is what gets used during rendering to -look up things like the element's stroke width or fill color. - -## Parsing - -### XML into a tree of Nodes / Elements - -Librsvg uses an XML parser (libxml2 at the time of this writing) to do -the first-stage parsing of the SVG document. `XmlState` contains the -XML parsing state, which is a stack of contexts depending on the XML -nesting structure. `XmlState` has public methods, called from the XML -parser as it goes. The most important one is `start_element`; this -is responsible for creating new `Node` structures in the tree, within -the `DocumentBuilder` being built. - -Nodes are either SVG elements (the `Element` enum), or text data -inside elements (the `Chars` struct); this last one will not concern -us here, and we will only talk about `Element`. - -Each supported kind of `Element` parses its attributes in a -`set_attributes()` method. Each attribute is just a key/value pair; -for example, the `<rect width="5px">` element has a `width` attribute -whose value is `5px`. - -While parsing its attributes, an element may encounter an invalid -value, for example, a negative width where only nonnegative ones are -allowed. In this case, the element's `set_attributes` method may -return a `Result::Err`. The caller will then do `set_error` to mark -that element as being in an error state. If an element is in error, -its children will get parsed as usual, but the element and its -children will be ignored during the rendering stage. - -The SVG spec says that SVG rendering should stop on the first element -that is "in error". However, most implementations simply seem to -ignore erroneous elements instead of completely stopping rendering, -and we do the same in librsvg. - -### CSS and styles - -Librsvg uses Servo's `cssparser` crate as a CSS tokenizer, and -`selectors` as a high-level parser for CSS style data. - -With the `cssparser` crate, the caller is responsible for providing an -implementation of the `DeclarationParser` trait. Its `parse_value` -method takes the name of a CSS property name like `fill`, plus a value -like `rgb(255, 0, 0)`, and it must return a value that represents a -parsed declaration. Librsvg uses the `Declaration` struct for this. - -The core of parsing CSS is the `parse_value` function, which returns a `ParsedProperty`: - -```rust -pub enum ParsedProperty { - BaselineShift(SpecifiedValue<BaselineShift>), - ClipPath(SpecifiedValue<ClipPath>), - Color(SpecifiedValue<Color>), - // etc. -} -``` - -What is `SpecifiedValue`? It is the parsed value for a CSS property directly as it comes out of the SVG document: - -```rust -pub enum SpecifiedValue<T> -where - T: Property<ComputedValues> + Clone + Default, -{ - Unspecified, - Inherit, - Specified(T), -} -``` - -A property declaration can look like `opacity: inherit;` - this would -create a `ParsedProperty::Opacity(SpecifiedValue::Inherit)`. - -Or it can look like `opacity: 0.5;` - this would create a -`ParsedProperty::Opacity(SpecifiedValue::Specified(Opacity(UnitInterval(0.5))))`. -Let's break this down: - -* `ParsedProperty::Opacity` - which property did we parse? - -* `SpecifiedValue::Specified` - it actually was specified by the - document with a value; the other interesting alternative is - `Inherit`, which corresponds to the value `inherit` that all CSS - property declarations can have. - -* `Opacity(UnitInterval(0.5))` - This is the type `Opacity` property, - which is a newtype around an internal `UnitInterval` type, which in - turn guarantees that we have a float in the range `[0.0, 1.0]`. - -There is a Rust type for every CSS property that librsvg supports; -many of these types are newtypes around primitive types like `f64`. - -Eventually an entire CSS stylesheet, like the contents of a `<style>` -element, gets parsed into a `Stylesheet` struct. A stylesheet has a -list of rules, where each rule is the CSS selectors defined for it, -and the style declarations that should be applied for the `Node`s that -match the selectors. For example, in a little stylesheet like this: - -```xml -<style type="text/css"> - rect, #some_id { - fill: blue; - stroke-width: 5px; - } -</style> -``` - -This stylesheet has a single rule. The rule has a selector list with -two selectors (`rect` and `#some_id`) and two style declarations -(`fill: blue` and `stroke-width: 5px`). - -After parsing is done, there is a **cascading stage** where librsvg -walks the tree of nodes, and for each node it finds the CSS rules that -should be applied to it. - -## Rendering - -The rendering process starts at the `draw_tree()` function. This sets -up a `DrawingCtx`, which carries around all the mutable state during -rendering. - -Rendering is a recursive process, which goes back and forth between -the utility functions in `DrawingCtx` and the `draw` method in -elements. - -The main job of `DrawingCtx` is to deal with the SVG drawing model. -Each element renders itself independently, and its result gets -modified before getting composited onto the final image: - -1. Render an element to a temporary surface (example: stroke and fill a path). -2. Apply filter effects (blur, color mapping, etc.). -3. Apply clipping paths. -4. Apply masks. -5. Composite the result onto the final image. - -The temporary result from the last step also gets put in a stack; this -is because filter effects sometimes need to look at the -currently-drawn background to apply further filtering to it. - -You'll see that most of the rendering-related functions return a -`Result<BoundingBox, RenderingError>`. Some SVG features require -knowing the bounding box of the object that is being rendered; for -historical reasons this bounding box is computed as part of the -rendering process in librsvg. When computing a subtree's bounding -box, the bounding boxes from the leaves get aggregated up to the root -of the subtree. Each node in the tree has its own coordinate system; -`BoundingBox` is able to transform coordinate systems to get a -bounding box that is meaningful with respect to the root's transform. - -# Comparing floating-point numbers - -Librsvg sometimes needs to compute things like "are these points -equal?" or "did this computed result equal this test reference -number?". - -We use `f64` numbers in Rust for all computations on real numbers. -Floating-point numbers cannot be compared with `==` effectively, since -it doesn't work when the numbers are slightly different due to -numerical inaccuracies. - -Similarly, we don't `assert_eq!(a, b)` for floating-point numbers. - -Most of the time we are dealing with coordinates which will get passed -to Cairo. In turn, Cairo converts them from doubles to a fixed-point -representation (as of March 2018, Cairo uses 24.8 fixnums with 24 bits of -integral part and 8 bits of fractional part). - -So, we can consider two numbers to be "equal" if they would be represented -as the same fixed-point value by Cairo. Librsvg implements this in -the [`ApproxEqCairo` trait][ApproxEqCairo] trait. You can use it like -this: - -```rust -use float_eq_cairo::ApproxEqCairo; // bring the trait into scope - -let a: f64 = ...; -let b: f64 = ...; - -if a.approx_eq_cairo(&b) { // not a == b - ... // equal! -} - -assert!(1.0_f64.approx_eq_cairo(&1.001953125_f64)); // 1 + 1/512 - cairo rounds to 1 -``` - -[ApproxEqCairo]: src/float_eq_cairo.rs - -# Some interesting parts of the code - -* Are you adding support for a CSS property? Look in the - [`property_defs`] and [`properties`] modules. `property_defs` - defines most of the CSS properties that librsvg supports, and - `properties` actually puts all those properties in the - `SpecifiedValues` and `ComputedValues` structs. - -* The [`Handle`] struct provides the primitives to implement the - public APIs, such as loading an SVG file and rendering it. - -* The [`DrawingCtx`] struct is active while an SVG handle is being - drawn. It has all the mutable state related to the drawing process, - such as the stack of temporary rendered surfaces, and the viewport - stack. - -* The [`Document`] struct represents a loaded SVG document. It holds - the tree of [`Node`] structs, some of which contain [`Element`]s. A - `Document` also contains a mapping of `id` attributes to the - corresponding element nodes. - -* The [`xml`] module receives events from an XML parser, and builds a - [`Document`] tree. - -* The [`css`] module has the high-level machinery for parsing CSS and - representing parsed stylesheets. The low-level parsers for - individual properties are in [`property_defs`] and [`font_props`]. - -[`css`]: src/css.rs -[`Document`]: src/document.rs -[`DrawingCtx`]: src/drawing_ctx.rs -[`Element`]: src/element.rs -[`font_props`]: src/font_props.rs -[`Handle`]: src/handle.rs -[`Node`]: src/node.rs -[`properties`]: src/properties.rs -[`property_defs`]: src/property_defs.rs -[`xml`]: src/xml/mod.rs @@ -24,6 +24,15 @@ dependencies = [ ] [[package]] +name = "android_system_properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" +dependencies = [ + "libc", +] + +[[package]] name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -34,9 +43,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.59" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91f1f46651137be86f3a2b9a8359f9ab421d04d941c62b5982e1ca21113adf9" +checksum = "1485d4d2cc45e7b201ee3767015c96faa5904387c9d87c6efdd0fb511f12d305" [[package]] name = "approx" @@ -125,15 +134,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.10.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" [[package]] name = "bytemuck" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5377c8865e74a160d21f29c2d40669f53286db6eab59b88540cbb12ffc8b835" +checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" [[package]] name = "byteorder" @@ -188,14 +197,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.19" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" dependencies = [ - "libc", + "iana-time-zone", + "js-sys", "num-integer", "num-traits", "time 0.1.44", + "wasm-bindgen", "winapi", ] @@ -233,6 +244,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] name = "crc32fast" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -333,10 +350,10 @@ dependencies = [ "itoa 0.4.8", "matches", "phf", - "proc-macro2 1.0.42", - "quote 1.0.20", + "proc-macro2 1.0.43", + "quote 1.0.21", "smallvec", - "syn 1.0.98", + "syn 1.0.99", ] [[package]] @@ -345,8 +362,8 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e" dependencies = [ - "quote 1.0.20", - "syn 1.0.98", + "quote 1.0.21", + "syn 1.0.99", ] [[package]] @@ -396,10 +413,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case", - "proc-macro2 1.0.42", - "quote 1.0.20", + "proc-macro2 1.0.43", + "quote 1.0.21", "rustc_version 0.4.0", - "syn 1.0.98", + "syn 1.0.99", ] [[package]] @@ -437,9 +454,9 @@ dependencies = [ [[package]] name = "either" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "encoding" @@ -561,24 +578,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "2bfc52cbddcfd745bf1740338492bb0bd83d76c67b445f91c5fb29fae29ecaa1" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115" [[package]] name = "futures-executor" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +checksum = "1d11aa21b5b587a64682c0094c2bdd4df0076c5324961a40cc3abd7f37930528" dependencies = [ "futures-core", "futures-task", @@ -587,21 +604,21 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "93a66fc6d035a26a3ae255a6d2bca35eda63ae4c5512bef54449113f7a1228e5" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "842fc63b931f4056a24d59de13fb1272134ce261816e063e634ad0c15cdc5306" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "f0828a5471e340229c11c77ca80017937ce3c58cb788a17e5f1c2d5c485a9577" dependencies = [ "futures-core", "futures-task", @@ -727,9 +744,9 @@ dependencies = [ "heck", "proc-macro-crate", "proc-macro-error", - "proc-macro2 1.0.42", - "quote 1.0.20", - "syn 1.0.98", + "proc-macro2 1.0.43", + "quote 1.0.21", + "syn 1.0.99", ] [[package]] @@ -781,6 +798,19 @@ dependencies = [ ] [[package]] +name = "iana-time-zone" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] name = "idna" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -817,9 +847,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "js-sys" @@ -844,9 +874,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.126" +version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" [[package]] name = "librsvg" @@ -1051,9 +1081,9 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01fcc0b8149b4632adc89ac3b7b31a12fb6099a0317a4eb2ebff574ef7de7218" dependencies = [ - "proc-macro2 1.0.42", - "quote 1.0.20", - "syn 1.0.98", + "proc-macro2 1.0.43", + "quote 1.0.21", + "syn 1.0.99", ] [[package]] @@ -1154,9 +1184,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" [[package]] name = "oorandom" @@ -1241,9 +1271,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +checksum = "9423e2b32f7a043629287a536f21951e8c6a82482d0acb1eeebfc90bc2225b22" [[package]] name = "percent-encoding" @@ -1301,9 +1331,9 @@ dependencies = [ "phf_generator 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", - "proc-macro2 1.0.42", - "quote 1.0.20", - "syn 1.0.98", + "proc-macro2 1.0.43", + "quote 1.0.21", + "syn 1.0.99", ] [[package]] @@ -1344,9 +1374,9 @@ checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "plotters" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9428003b84df1496fb9d6eeee9c5f8145cb41ca375eb0dad204328888832811f" +checksum = "716b4eeb6c4a1d3ecc956f75b43ec2e8e8ba80026413e70a3f41fd3313d3492b" dependencies = [ "num-traits", "plotters-backend", @@ -1363,9 +1393,9 @@ checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" [[package]] name = "plotters-svg" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0918736323d1baff32ee0eade54984f6f201ad7e97d5cfb5d6ab4a358529615" +checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" dependencies = [ "plotters-backend", ] @@ -1432,9 +1462,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d50bfb8c23f23915855a00d98b5a35ef2e0b871bb52937bacadb798fbb66c8" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" dependencies = [ "once_cell", "thiserror", @@ -1448,9 +1478,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.42", - "quote 1.0.20", - "syn 1.0.98", + "proc-macro2 1.0.43", + "quote 1.0.21", + "syn 1.0.99", "version_check", ] @@ -1460,8 +1490,8 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.42", - "quote 1.0.20", + "proc-macro2 1.0.43", + "quote 1.0.21", "version_check", ] @@ -1482,9 +1512,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ "unicode-ident", ] @@ -1532,11 +1562,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ - "proc-macro2 1.0.42", + "proc-macro2 1.0.43", ] [[package]] @@ -1730,7 +1760,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.12", + "semver 1.0.13", ] [[package]] @@ -1747,9 +1777,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "safe_arch" @@ -1804,9 +1834,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" +checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711" [[package]] name = "semver-parser" @@ -1816,9 +1846,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.141" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7af873f2c95b99fcb0bd0fe622a43e29514658873c8ceba88c4cb88833a22500" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" dependencies = [ "serde_derive", ] @@ -1835,22 +1865,22 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.141" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75743a150d003dd863b51dc809bcad0d73f2102c53632f1e954e738192a3413f" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" dependencies = [ - "proc-macro2 1.0.42", - "quote 1.0.20", - "syn 1.0.98", + "proc-macro2 1.0.43", + "quote 1.0.21", + "syn 1.0.99", ] [[package]] name = "serde_json" -version = "1.0.82" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ - "itoa 1.0.2", + "itoa 1.0.3", "ryu", "serde", ] @@ -1949,11 +1979,11 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" dependencies = [ - "proc-macro2 1.0.42", - "quote 1.0.20", + "proc-macro2 1.0.43", + "quote 1.0.21", "serde", "serde_derive", - "syn 1.0.98", + "syn 1.0.99", ] [[package]] @@ -1963,13 +1993,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" dependencies = [ "base-x", - "proc-macro2 1.0.42", - "quote 1.0.20", + "proc-macro2 1.0.43", + "quote 1.0.21", "serde", "serde_derive", "serde_json", "sha1", - "syn 1.0.98", + "syn 1.0.99", ] [[package]] @@ -2000,8 +2030,8 @@ checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" dependencies = [ "phf_generator 0.10.0", "phf_shared 0.10.0", - "proc-macro2 1.0.42", - "quote 1.0.20", + "proc-macro2 1.0.43", + "quote 1.0.21", ] [[package]] @@ -2023,12 +2053,12 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" dependencies = [ - "proc-macro2 1.0.42", - "quote 1.0.20", + "proc-macro2 1.0.43", + "quote 1.0.21", "unicode-ident", ] @@ -2099,22 +2129,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" dependencies = [ - "proc-macro2 1.0.42", - "quote 1.0.20", - "syn 1.0.98", + "proc-macro2 1.0.43", + "quote 1.0.21", + "syn 1.0.99", ] [[package]] @@ -2160,10 +2190,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" dependencies = [ "proc-macro-hack", - "proc-macro2 1.0.42", - "quote 1.0.20", + "proc-macro2 1.0.43", + "quote 1.0.21", "standback", - "syn 1.0.98", + "syn 1.0.99", ] [[package]] @@ -2214,9 +2244,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" [[package]] name = "unicode-normalization" @@ -2332,9 +2362,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.42", - "quote 1.0.20", - "syn 1.0.98", + "proc-macro2 1.0.43", + "quote 1.0.21", + "syn 1.0.99", "wasm-bindgen-shared", ] @@ -2344,7 +2374,7 @@ version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" dependencies = [ - "quote 1.0.20", + "quote 1.0.21", "wasm-bindgen-macro-support", ] @@ -2354,9 +2384,9 @@ version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" dependencies = [ - "proc-macro2 1.0.42", - "quote 1.0.20", - "syn 1.0.98", + "proc-macro2 1.0.43", + "quote 1.0.21", + "syn 1.0.99", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9,6 +9,7 @@ homepage = "https://wiki.gnome.org/Projects/LibRsvg" repository = "https://gitlab.gnome.org/GNOME/librsvg/" build = "build.rs" edition = "2021" +rust-version = "1.58" [package.metadata.system-deps] cairo-pdf = { version = "1.16", optional = true } diff --git a/Makefile.am b/Makefile.am index b962614c..3c6388c9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -254,7 +254,7 @@ EXTRA_DIST = \ $(RSVG_CONVERT_SRC) \ AUTHORS \ COPYING.LIB \ - NEWS.md \ + NEWS \ SECURITY.md \ Rsvg-2.0-custom.vala \ Rsvg-2.0.metadata \ @@ -197,7 +197,7 @@ ways: [contributing]: CONTRIBUTING.md [reporting-bugs]: CONTRIBUTING.md#reporting-bugs [d-d-l]: https://mail.gnome.org/mailman/listinfo/desktop-devel-list -[federico]: https://people.gnome.org/~federico/ +[federico]: https://viruta.org/ [platform]: https://developer.gnome.org/ -[guadec-presentation-1]: https://people.gnome.org/~federico/blog/docs/fmq-porting-c-to-rust.pdf -[guadec-presentation-2]: https://people.gnome.org/~federico/blog/docs/fmq-refactoring-c-to-rust.pdf +[guadec-presentation-1]: https://viruta.org/docs/fmq-porting-c-to-rust.pdf +[guadec-presentation-2]: https://viruta.org/docs/fmq-refactoring-c-to-rust.pdf diff --git a/RELEASING.md b/RELEASING.md deleted file mode 100644 index 4eab87ff..00000000 --- a/RELEASING.md +++ /dev/null @@ -1,219 +0,0 @@ -# Release process checklist for librsvg - -Feel free to print this document or copy it to a text editor to check -off items while making a release. - -- [ ] Refresh your memory with https://wiki.gnome.org/MaintainersCorner/Releasing -- [ ] Increase the package version number in `configure.ac` (it may - already be increased but not released; double-check it). -- [ ] Copy version number to `Cargo.toml`. -- [ ] Copy version number to `doc/librsvg.toml`. -- [ ] `cargo update` - needed because you tweaked `Cargo.toml`, and - also to get new dependencies. -- [ ] Tweak the library version number in `configure.ac` if the API changed; follow the steps there. -- [ ] Update `NEWS.md`, see below for the preferred format. -- [ ] Commit the changes above. -- [ ] Make a tarball with `./autogen.sh --enable-vala && make distcheck DESTDIR=/tmp/foo` - fix things until it passes. -- [ ] Create a signed tag - `git tag -s x.y.z` with the version number. -- [ ] `git push` to the appropriate branch to gitlab.gnome.org/GNOME/librsvg -- [ ] `git push` the signed tag to gitlab.gnome.org/GNOME/librsvg -- [ ] `scp librsvg-x.y.z.tar.xz master.gnome.org:` -- [ ] `ssh master.gnome.org` and then `ftpadmin install librsvg-x.y.z.tar.xz` -- [ ] Create a [release in Gitlab](#gitlab-release). - -For `x.y.0` releases, at least, do the following: - -- [ ] [Notify the release team][release-team] on whether to use this - `librsvg-x.y.0` for the next GNOME version via an issue on their - `GNOME/releng` project. - -- [ ] `cargo-audit audit` and ensure we don't have vulnerable dependencies. - -## Gitlab release - -- [ ] Go to https://gitlab.gnome.org/GNOME/librsvg/-/releases and click the **New release** button. - -- [ ] Select the tag `x.y.z` you created as part of the release steps. - -- [ ] If there is an associated milestone, select it too. - -- [ ] Fill in the release title - `x.y.z - stable` or `x.y.z - development`. - -- [ ] Copy the release notes from NEWS.md. - -- [ ] Add a release asset link to - `https://download.gnome.org/sources/librsvg/x.y/librsvg-x.y.z.tar.xz` - and call it `librsvg-x.y.z.tar.xz - release tarball`. - -- [ ] Add a release asset link to - `https://download.gnome.org/sources/librsvg/x.y/librsvg-x.y.z.sha256sum` - and call it `librsvg-x.y.z.sha256sum - release tarball - sha256sum`. - -## Version numbers - -`configure.ac` and `Cargo.toml` must have the same **package version** -number - this is the number that users of the library see. - -`configure.ac` is where the **library version** is defined; this is -what gets encoded in the SONAME of `librsvg.so`. - -Librsvg follows an even/odd numbering scheme for the **package -version**. For example, the 2.50.x series is for stable releases, and -2.51.x is for unstable/development ones. The [release-team] needs to -be notified when a new series comes about, so they can adjust their -tooling for the stable or development GNOME releases. File an issue -in their [repository][release-team] to indicate whether the new -`librsvg-x.y.0` is a stable or development series. - -## Minimum supported Rust version (MSRV) - -While it may seem desirable to always require the latest released -version of the Rust toolchain, to get new language features and such, -this is really inconvenient for distributors of librsvg which do not -update Rust all the time. So, we make a compromise. - -The `configure.ac` script defines `MININUM_RUST_MAJOR` and -`MINIMUM_RUST_MINOR` variables with librsvg's minimum supported Rust -version (MSRV). These ensure that distros will get an early failure during a -build, at the `configure` step, if they have a version of Rust that is -too old — instead of getting an obscure error message from `rustc` in -the middle of the build when it finds an unsupported language -construct. - -As of March 2021, Cargo does not allow setting a minimum supported -Rust version; you may want to keep an eye on [the MSRV RFC][msrv-rfc]. - -Sometimes librsvg's dependencies update their MSRV and librsvg may -need to increase it as well. Please consider the following before -doing this: - -* Absolutely do not require a nightly snapshot of the compiler, or - crates that only build on nightly. - -* Distributions with rolling releases usually keep their Rust - toolchains fairly well updated, maybe not always at the latest, but - within two or three releases earlier than the latest. If the MSRV - you want is within about six months of the latest, things are - probably safe. - -* Enterprise distributions update more slowly. It is useful to watch - for the MSRV that Firefox requires, although sometimes Firefox - updates Rust very slowly as well. Now that distributions are - shipping packages other than Firefox that require Rust, they will - probably start updating more frequently. - -Generally — two or three releases earlier than the latest stable Rust -is OK for rolling distros, probably perilous for enterprise distros. -Releases within a year of an enterprise distro's shipping date are -probably OK. - -If you are not sure, ask on the [forum for GNOME -distributors][distributors] about their plans! (That is, posts on -`discourse.gnome.org` with the `distributor` tag.) - -[msrv-rfc]: https://github.com/rust-lang/rfcs/pull/2495 -[distributors]: https://discourse.gnome.org/tag/distributor - -## Format for release notes in NEWS.md - -The `NEWS.md` file contains the release notes. Please use something -close to this format; it is not mandatory, but makes the formatting -consistent, and is what tooling expects elsewhere - also by writing -Markdown, you can just cut&paste it into a Gitlab release. You can -skim bits of the `NEWS.md` file for examples on style and content. - -New entries go at the **top** of the file. - -``` -Version x.y.z -============= - -Commentary on the release; put anything here that you want to -highlight. Note changes in the build process, if any, or any other -things that may trip up distributors. - -## Description of a special feature - -You can include headings with `##` in Markdown syntax. - -Blah blah blah. - - -Next is a list of features added and issues fixed; use gitlab's issue -numbers. I tend to use this order: first security bugs, then new -features and user-visible changes, finally regular bugs. The -rationale is that if people stop reading early, at least they will -have seen the most important stuff first. - -## Changes: - -- #123 - title of the issue, or short summary if it warrants more - discussion than just the title. - -- #456 - fix blah blah (Contributor's Name). - -## Special thanks for this release: - -- Any people that you want to highlight. Feel free to omit this - section if the release is otherwise unremarkable. -``` - -## Making a tarball - -``` -make distcheck DESTDIR=/tmp/foo -``` - -The `DESTDIR` is a quirk, required because otherwise the gdk-pixbuf -loader will try to install itself into the system's location for -pixbuf loaders, and it won't work. The `DESTDIR` is what Linux -distribution packaging scripts use to `make install` the compiled -artifacts to a temporary location before building a system package. - -## Copying the tarball to master.gnome.org - -If you don't have a maintainer account there, ask federico@gnome.org -to do it or [ask the release team][release-team] to do it by filing an -issue on their `GNOME/releng` project. - -[release-team]: https://gitlab.gnome.org/GNOME/releng/-/issues - -## Rust dependencies - -Release tarballs get generated with *vendored dependencies*, that is, -the source code for all the crates that librsvg depends on gets bundled -into the tarball itself. It is important to keep these dependencies -updated; you can do that regularly with the `cargo update` step listed -in the checklist above. - -[`cargo-audit`][cargo-audit] is very useful to scan the list of -dependencies for registered vulnerabilities in the [RustSec -vulnerability database][rustsec]. Run it especially before making a -new `x.y.0` release. - -Sometimes cargo-audit will report crates that are not vulnerable, but -that are unmaintained. Keep an eye of those; you may want to file -bugs upstream to see if the crates are really unmaintained or if they -should be substituted for something else. - -[cargo-audit]: https://github.com/RustSec/cargo-audit -[rustsec]: https://rustsec.org/ - -## Creating a stable release branch - -* Create a branch named `librsvg-xx.yy`, e.g. `librsvg-2.54` - -* Make the `BASE_TAG` in `ci/container-builds.yml` refer to the new - `librsvg-xx.yy` branch instead of `main`. - -* Push that branch to origin. - -* (Branches with that naming scheme are already automatically - protected in gitlab's Settings/Repository/Protected branches.) - -* Edit the badge for the stable branch so it points to the new branch: - Settings/General/Badges, find the existing badge for the stable - branch, click on the edit button that looks like a pencil. Change - the **Link** and **Badge image URL**; usually it is enough to just - change the version number in both. diff --git a/devel-docs/README.md b/devel-docs/README.md deleted file mode 100644 index 5443fc2f..00000000 --- a/devel-docs/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Development documentation for librsvg - -Before embarking on big changes, please write a little design document -modeled on the following ones, and submit a merge request. We can -then discuss it before coding. This way we will have a sort of -big-picture development history apart from commit messages. - -Design documents: - -* [`adding-a-property.md`](adding-a-property.md) - Tutorial on how to - add support for a new CSS property. Should remain always current. diff --git a/devel-docs/adding-a-property.md b/devel-docs/adding-a-property.md deleted file mode 100644 index 90f953e2..00000000 --- a/devel-docs/adding-a-property.md +++ /dev/null @@ -1,587 +0,0 @@ -# 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: - -```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: - -```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: - -```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: - -```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: - -```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: - -```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: - -```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: - -```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: - -```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: - -```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: - -```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: - -```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: - -```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`: - -```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: - -```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: - -```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: - -```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: - -```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: - -```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: - -```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: - -```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! diff --git a/devel-docs/adding_a_property.rst b/devel-docs/adding_a_property.rst new file mode 100644 index 00000000..b8ade985 --- /dev/null +++ b/devel-docs/adding_a_property.rst @@ -0,0 +1,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! diff --git a/devel-docs/architecture.rst b/devel-docs/architecture.rst new file mode 100644 index 00000000..c8b7d4e1 --- /dev/null +++ b/devel-docs/architecture.rst @@ -0,0 +1,474 @@ +Architecture of librsvg +======================= + +This document roughly describes the architecture of librsvg, and future +plans for it. The code is continually evolving, so don’t consider this +as the ground truth, but rather like a cheap map you buy at a street +corner. + +The library’s internals are documented as Rust documentation comments; +you can look at the rendered version at +https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/index.html + +You may also want to see the section below on `interesting parts of the +code <#some-interesting-parts-of-the-code>`__. + +A bit of history +---------------- + +Librsvg is an old library. It started around 2001, when Eazel (the +original makers of GNOME’s file manager Nautilus) needed a library to +render SVG images. At that time the SVG format was being standardized, +so librsvg grew along with the SVG specification. This is why you will +sometimes see references to deprecated SVG features in the source code. + +Librsvg started as an experiment to use libxml2’s new SAX parser, so +that SVG could be streamed in and rendered on the fly, instead of first +creating a DOM tree. Originally it used +`libart <https://levien.com/libart/>`__ as a rendering library; this was +GNOME’s first antialiased renderer with alpha compositing. Later, the +renderer was replaced with `Cairo <https://www.cairographics.org/>`__. +Librsvg is currently striving to support other rendering backends. + +These days librsvg indeed builds a DOM tree by itself; it needs the +tree to run the CSS cascade, do selector matching, and to support +cross-element references like in SVG filters. + +Librsvg started as a C library with an ad-hoc API. At some point it +got turned into a GObject library, so that the main `RsvgHandle +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/c_api/handle/struct.RsvgHandle.html>`_ +class defines most of the entry points into the library. Through +`GObject Introspection <https://gi.readthedocs.io/en/latest/>`__, this +allows librsvg to be used from other programming languages. + +In 2016, librsvg started getting ported to Rust. As of early 2021, the +whole library is implemented in Rust, and exports an intact C API/ABI. +It also exports a more idiomatic Rust API as well. + +The C and Rust APIs +------------------- + +Librsvg exports two public APIs, one for C and one for Rust. + +The C API has hard requirements for API/ABI stability, because it is +used all over the GNOME project and API/ABI breaks would be highly +disruptive. Also, the C API is what allows librsvg to be called from +other programming languages, through `GObject +Introspection <https://gi.readthedocs.io/en/latest/>`__. + +The Rust API is a bit more lax in its API stability, but we try to stick +to `semantic versioning <https://semver.org/>`__ as is common in Rust. + +The public Rust API is implemented in `src/api.rs +<https://gitlab.gnome.org/GNOME/librsvg/-/blob/main/src/api.rs>`_. This +has all the primitives needed to load and render SVG documents or +individual elements, and to configure loading/rendering options. + +The public C API is implemented in `src/c_api/ +<https://gitlab.gnome.org/GNOME/librsvg/-/tree/main/src/c_api>`_, and +it is implemented in terms of the public Rust API. Note that as of +2021/Feb the corresponding C header files are hand-written in +`include/librsvg/ +<https://gitlab.gnome.org/GNOME/librsvg/-/tree/main/include/librsvg>`_; +maybe in the future they will be generated automatically with +`cbindgen <https://github.com/eqrion/cbindgen/blob/master/docs.md>`__. + +We consider it good practice to provide simple and clean primitives in +the Rust API, and have ``c_api`` deal with all the idiosyncrasies and +historical considerations for the C API. + +In short: the public C API calls the public Rust API, and the public +Rust API calls into the library's internals. + +:: + + +----------------+ + | Public C API | + | src/c_api | + +----------------+ + | + calls + | + v + +-------------------+ + | Public Rust API | + | src/api.rs | + +-------------------+ + | + calls + | + v + +-------------------+ + | library internals | + | src/*.rs | + +-------------------+ + +The test suite +-------------- + +The test suite is documented in ``tests/README.md``. + +Code flow +--------- + +The caller of librsvg loads a document into a handle, and later may ask +to render the document or one of its elements, or measure their +geometries. + +Loading an SVG document +~~~~~~~~~~~~~~~~~~~~~~~ + +The Rust API starts by constructing an `SvgHandle +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/api/struct.SvgHandle.html>`_ +from a `Loader +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/api/struct.Loader.html>`_; +both of those are public types. Internally the ``SvgHandle`` is just a +wrapper around a `Handle +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/handle/struct.Handle.html>`_, +which is a private type. ``Handle`` represents an SVG document loaded +in memory; it acts as a wrapper around a `Document +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/document/struct.Document.html>`_, +and provides the basic primitive operations like “render the whole +document” or “compute the geometry of an element” that are needed to +implement the public APIs. + +A ``Document`` gets created by loading XML from a stream, into a tree +of `Node +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/node/type.Node.html>`_ +structures. This is similar to a web browser’s DOM tree. + +Each XML element causes a new ``Node`` to get created with an `Element +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/element/enum.Element.html>`_ +in it. The ``Element`` enum can represent all the SVG element types; +for example, a ``<path>`` element from XML gets turned into a +``Node::Element(Element::Path)``. + +When an ``Element`` is created from its corresponding XML, its +`Attributes +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/xml/attributes/struct.Attributes.html>`_ +get parsed. On one hand, attributes that are specific to a particular +element type, like the ``d`` in ``<path d="...">`` get parsed by the +`set_attributes +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/element/trait.SetAttributes.html#method.set_attributes>`_ +method of each particular element type (in that case, +`Path::set_attributes +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/shapes/struct.Path.html#method.set_attributes>`_). + +On the other hand, attributes that refer to styles, and which may +appear for any kind of element, get all parsed into a `SpecifiedValues +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/properties/struct.SpecifiedValues.html>`_ +struct. This is a memory-efficient representation of the CSS style +properties that an element has. + +When the XML document is fully parsed, a ``Document`` contains a tree of +``Node`` structs and their inner ``Element`` structs. The tree has also +been validated to ensure that the root is an ``<svg>`` element. + +After that, the CSS cascade step gets run. + +The CSS cascade +~~~~~~~~~~~~~~~ + +Each ``Element`` has a `SpecifiedValues +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/properties/struct.SpecifiedValues.html>`_, +which has the CSS style properties that the XML specified for that +element. However, ``SpecifiedValues`` is sparse, as not all the +possible style properties may have been filled in. Cascading means +following the CSS/SVG rules for each property type to inherit missing +properties from parent elements. For example, in this document +fragment: + +:: + + <g stroke-width="2" stroke="black"> + <path d="M0,0 L10,0" fill="blue"/> + <path d="M20,0 L30,0" fill="green"/> + </g> + +Each ``<path>`` element has a different fill color, but they both +*inherit* the ``stroke-width`` and ``stroke`` values from their parent +group. This is because both the ``stroke-width`` and ``stroke`` +properties are defined in the CSS/SVG specifications to inherit +automatically. Some other properties, like ``opacity``, do not inherit +and are thus not copied to child elements. + +In librsvg, the individual types for CSS properties are defined with +the `make_property +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/macro.make_property.html>`_ +macro. + +The cascading step takes each element’s ``SpecifiedValues`` and +composes it by CSS inheritance onto a `ComputedValues +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/properties/struct.ComputedValues.html>`_, +which has the result of the cascade for each element's properties. + +When cascading is done, each ``Element`` has a fully resolved +``ComputedValues`` struct, which is what gets used during rendering to +look up things like the element’s stroke width or fill color. + +Parsing XML into a tree of Nodes / Elements +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Librsvg uses an XML parser (`libxml2 +<https://gitlab.gnome.org/GNOME/libxml2/-/wikis/home>`_ at the time of +this writing) to do the first-stage parsing of the SVG +document. `XmlState +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/xml/struct.XmlState.html>`_ +contains the XML parsing state, which is a stack of contexts depending +on the XML nesting structure. ``XmlState`` has public methods, called +from the XML parser as it goes. The most important one is +`start_element +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/xml/struct.XmlState.html#method.start_element>`_; +this is responsible for creating new ``Node`` structures in the tree, +within the `DocumentBuilder +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/document/struct.DocumentBuilder.html>`_ +being built. + +Nodes are either SVG elements (the `Element +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/element/enum.Element.html>`_ +enum), or text data inside elements (the `Chars +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/text/struct.Chars.html>`_ +struct); this last one will not concern us here, and we will only talk +about ``Element``. + +Each supported kind of ``Element`` parses its attributes in a +`set_attributes +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/element/trait.SetAttributes.html#method.set_attributes>`_ +method. Each attribute is just a key/value pair; for example, the +``<rect width="5px">`` element has a ``width`` attribute whose value +is ``5px``. + +While parsing its attributes, an element may encounter an invalid value, +for example, a negative width where only nonnegative ones are allowed. +In this case, the element’s ``set_attributes`` method may return a +``Result::Err``. The caller will then do ``set_error`` to mark that +element as being in an error state. If an element is in error, its +children will get parsed as usual, but the element and its children will +be ignored during the rendering stage. + +The SVG spec says that SVG rendering should stop on the first element +that is “in error”. However, most implementations simply seem to ignore +erroneous elements instead of completely stopping rendering, and we do +the same in librsvg. + +CSS and styles +~~~~~~~~~~~~~~ + +Librsvg uses Servo’s `cssparser <https://crates.io/crates/cssparser>`_ +crate as a CSS tokenizer, and `selectors +<https://crates.io/crates/selectors>`_ as a high-level parser for CSS +style data. + +With the ``cssparser`` crate, the caller is responsible for providing +an implementation of the `DeclarationParser +<https://docs.rs/cssparser/0.29.6/cssparser/trait.DeclarationParser.html>`_ +trait. Its `parse_value +<https://docs.rs/cssparser/0.29.6/cssparser/trait.DeclarationParser.html#tymethod.parse_value>`_ +method takes the name of a CSS property name like ``fill``, plus a +value like ``rgb(255, 0, 0)``, and it must return a value that +represents a parsed declaration. Librsvg uses the `Declaration +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/css/struct.Declaration.html>`_ +struct for this. + +The core of parsing CSS is the ``parse_value`` function, which returns +a `ParsedProperty +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/properties/enum.ParsedProperty.html>`_: + +.. code:: rust + + pub enum ParsedProperty { + BaselineShift(SpecifiedValue<BaselineShift>), + ClipPath(SpecifiedValue<ClipPath>), + Color(SpecifiedValue<Color>), + // etc. + } + +What is `SpecifiedValue +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/properties/enum.SpecifiedValue.html>`_? +It is the parsed value for a CSS property directly as it comes out of +the SVG document: + +.. code:: rust + + pub enum SpecifiedValue<T> + where + T: Property + Clone + Default, + { + Unspecified, + Inherit, + Specified(T), + } + +A property declaration can look like ``opacity: inherit;`` - this would +create a ``ParsedProperty::Opacity(SpecifiedValue::Inherit)``. + +Or it can look like ``opacity: 0.5;`` - this would create a +``ParsedProperty::Opacity(SpecifiedValue::Specified(Opacity(UnitInterval(0.5))))``. +Let’s break this down: + +- ``ParsedProperty::Opacity`` - which property did we parse? + +- ``SpecifiedValue::Specified`` - it actually was specified by the + document with a value; the other interesting alternative is + ``Inherit``, which corresponds to the value ``inherit`` that all CSS + property declarations can have. + +- ``Opacity(UnitInterval(0.5))`` - This is the type `Opacity + <https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/property_defs/struct.Opacity.html>`_ + property, which is a newtype around an internal `UnitInterval + <https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/unit_interval/struct.UnitInterval.html>`_ + type, which in turn guarantees that we have a float in the range + ``[0.0, 1.0]``. + +There is a Rust type for every CSS property that librsvg supports; many +of these types are newtypes around primitive types like ``f64``. + +Eventually an entire CSS stylesheet, like the contents of a +``<style>`` element, gets parsed into a `Stylesheet +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/css/struct.Stylesheet.html>`_ +struct. A stylesheet has a list of rules, where each rule is the CSS +selectors defined for it, and the style declarations that should be +applied for the ``Node``\ s that match the selectors. For example, in +a little stylesheet like this: + +.. code:: xml + + <style type="text/css"> + rect, #some_id { + fill: blue; + stroke-width: 5px; + } + </style> + +This stylesheet has a single rule. The rule has a selector list with two +selectors (``rect`` and ``#some_id``) and two style declarations +(``fill: blue`` and ``stroke-width: 5px``). + +After parsing is done, there is a **cascading stage** where librsvg +walks the tree of nodes, and for each node it finds the CSS rules that +should be applied to it. + +Rendering +--------- + +The rendering process starts at the `draw_tree() +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/drawing_ctx/fn.draw_tree.html>`_ +function. This sets up a `DrawingCtx +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/drawing_ctx/struct.DrawingCtx.html>`_, +which carries around all the mutable state during rendering. + +Rendering is a recursive process, which goes back and forth between +the utility functions in ``DrawingCtx`` and the `draw +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/element/trait.Draw.html#method.draw>`_ +method in elements. + +The main job of ``DrawingCtx`` is to deal with the SVG drawing model. +Each element renders itself independently, and its result gets modified +before getting composited onto the final image: + +1. Render an element to a temporary surface (example: stroke and fill a + path). +2. Apply filter effects (blur, color mapping, etc.). +3. Apply clipping paths. +4. Apply masks. +5. Composite the result onto the final image. + +The temporary result from the last step also gets put in a stack; this +is because filter effects sometimes need to look at the currently-drawn +background to apply further filtering to it. + +You’ll see that most of the rendering-related functions return a +``Result<BoundingBox, RenderingError>``. Some SVG features require +knowing the bounding box of the object that is being rendered; for +historical reasons this bounding box is computed as part of the +rendering process in librsvg. When computing a subtree’s bounding box, +the bounding boxes from the leaves get aggregated up to the root of +the subtree. Each node in the tree has its own coordinate system; +`BoundingBox +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/bbox/struct.BoundingBox.html>`_ +is able to transform coordinate systems to get a bounding box that is +meaningful with respect to the root’s transform. + +Comparing floating-point numbers +-------------------------------- + +Librsvg sometimes needs to compute things like “are these points equal?” +or “did this computed result equal this test reference number?”. + +We use ``f64`` numbers in Rust for all computations on real numbers. +Floating-point numbers cannot be compared with ``==`` effectively, since +it doesn’t work when the numbers are slightly different due to numerical +inaccuracies. + +Similarly, we don’t ``assert_eq!(a, b)`` for floating-point numbers. + +Most of the time we are dealing with coordinates which will get passed +to Cairo. In turn, Cairo converts them from doubles to a fixed-point +representation (as of March 2018, Cairo uses 24.8 fixnums with 24 bits +of integral part and 8 bits of fractional part). + +So, we can consider two numbers to be “equal” if they would be +represented as the same fixed-point value by Cairo. Librsvg implements +this in the `ApproxEqCairo +<https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/float_eq_cairo/trait.ApproxEqCairo.html>`_ +trait. You can use it like this: + +.. code:: rust + + use float_eq_cairo::ApproxEqCairo; // bring the trait into scope + + let a: f64 = ...; + let b: f64 = ...; + + if a.approx_eq_cairo(&b) { // not a == b + ... // equal! + } + + assert!(1.0_f64.approx_eq_cairo(&1.001953125_f64)); // 1 + 1/512 - cairo rounds to 1 + +Some interesting parts of the code +---------------------------------- + +- Are you adding support for a CSS property? Look at the + :doc:`adding_a_property` tutorial; look in the `property_defs + <https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/property_defs/index.html>`_ + and `properties + <https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/properties/index.html>`_ + modules. ``property_defs`` defines most of the CSS properties that + librsvg supports, and ``properties`` actually puts all those + properties in the ``SpecifiedValues`` and ``ComputedValues`` + structs. + +- The `Handle + <https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/handle/struct.Handle.html>`_ + struct provides the primitives to implement the public APIs, such as + loading an SVG file and rendering it. + +- The `DrawingCtx + <https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/drawing_ctx/struct.DrawingCtx.html>`_ + struct is active while an SVG handle is being drawn. It has all the + mutable state related to the drawing process, such as the stack of + temporary rendered surfaces, and the viewport stack. + +- The `Document + <https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/document/struct.Document.html>`_ + struct represents a loaded SVG document. It holds the tree of `Node + <https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/node/type.Node.html>`_ + structs, some of which contain `Element + <https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/element/enum.Element.html>`_ + and some other contain `Chars + <https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/text/struct.Chars.html>`_ + for text data in the XML. A ``Document`` also contains a mapping of + ``id`` attributes to the corresponding element nodes. + +- The `xml + <https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/xml/index.html>`_ + module receives events from an XML parser, and builds a + ``Document`` tree. + +- The `css + <https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/css/index.html>`_ + module has the high-level machinery for parsing CSS and representing + parsed stylesheets. The low-level parsers for individual properties + are in `property_defs <https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/property_defs/index.html>`_ and + `font_props <https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/font_props/index.html>`_. diff --git a/devel-docs/index.rst b/devel-docs/index.rst index 3d8f28a9..3a3d2e42 100644 --- a/devel-docs/index.rst +++ b/devel-docs/index.rst @@ -5,11 +5,14 @@ Development guide for librsvg product roadmap devel_environment + architecture + adding_a_property memory_leaks contributing ci text_layout api_observability + releasing :maxdepth: 1 :caption: Contents: @@ -41,19 +44,17 @@ FIXME: link to doc with stuff from CONTRIBUTING.md's "Hacking on librsvg" Add basic info on cloning the repo, getting a gitlab account, forking. -Development roadmap. - Understand the code ------------------- -FIXME: Overview of the source tree. - Tour of the code - load a file, render it. Test suite - move tests/readme here? -Link to the internals documentation. +- `Documentation of the library's internals <https://gnome.pages.gitlab.gnome.org/librsvg/internals/librsvg/index.html>`_ +- :doc:`architecture` +- :doc:`adding_a_property` - :doc:`memory_leaks` Design documents @@ -74,7 +75,7 @@ contributions. Information for maintainers --------------------------- -FIXME: Move RELEASING.md here +- :doc:`releasing` Overview of the maintainer's workflow. @@ -85,15 +86,38 @@ Documentation on the CI. References ---------- -Link to SVG/CSS specs; other useful bits. +- `SVG2 specification <https://www.w3.org/TR/SVG2/>`_. This is the current Candidate Recommendation and it should + be your main reference... -Links to Mozilla's SVG, WebKit, resvg, Inkscape. +- ... except for things which are later clarified in the `SVG2 Editor's Draft <https://svgwg.org/svg2-draft/>`_. -Talks on librsvg. +- `Filter Effects Module Level 1 <https://www.w3.org/TR/filter-effects/>`_. + +- `References listed in the SVG2 spec + <https://www.w3.org/TR/SVG2/refs.html>`_ - if you need to consult + the CSS specifications. + +- `SVG1.1 specification <https://www.w3.org/TR/SVG11/>`_. Use this mostly for historical reference. + +- `SVG Working Group repository + <https://github.com/w3c/svgwg/tree/master>`_. The github issues are + especially interesting. Use this to ask for clarifications of the + spec. -Indices and tables ------------------- +- `SVG Working Group page <https://svgwg.org/>`_. -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +- Presentation at GUADEC 2017, `Replacing C library code with Rust: What I learned with + librsvg <https://viruta.org/docs/fmq-porting-c-to-rust.pdf>`_. It gives + a little history of librsvg, and how/why it was being ported to Rust + from C. + +- Presentation at GUADEC 2018, `Patterns of refactoring C to Rust: the case of + librsvg <https://viruta.org/docs/fmq-refactoring-c-to-rust.pdf>`_. It + describes ways in which librsvg's C code was refactored to allow + porting it to Rust. + +- `Federico Mena's blog posts on librsvg + <https://viruta.org/tag/librsvg.html>`_ - plenty of of history and + stories from the development process. + +Talks on librsvg. diff --git a/devel-docs/releasing.rst b/devel-docs/releasing.rst new file mode 100644 index 00000000..b0d9579d --- /dev/null +++ b/devel-docs/releasing.rst @@ -0,0 +1,241 @@ +Release process checklist for librsvg +===================================== + +Feel free to print this document or copy it to a text editor to check +off items while making a release. + +- ☐ Refresh your memory with + https://wiki.gnome.org/MaintainersCorner/Releasing +- ☐ Increase the package version number in ``configure.ac`` (it may + already be increased but not released; double-check it). +- ☐ Copy version number to ``Cargo.toml``. +- ☐ Copy version number to ``doc/librsvg.toml``. +- ☐ ``cargo update`` - needed because you tweaked ``Cargo.toml``, and + also to get new dependencies. +- ☐ Tweak the library version number in ``configure.ac`` if the API + changed; follow the steps there. +- ☐ Update ``NEWS``, see below for the preferred format. +- ☐ Commit the changes above. +- ☐ Make a tarball with + ``./autogen.sh --enable-vala && make distcheck DESTDIR=/tmp/foo`` - + fix things until it passes. +- ☐ Create a signed tag - ``git tag -s x.y.z`` with the version number. +- ☐ ``git push`` to the appropriate branch to + gitlab.gnome.org/GNOME/librsvg +- ☐ ``git push`` the signed tag to gitlab.gnome.org/GNOME/librsvg +- ☐ ``scp librsvg-x.y.z.tar.xz master.gnome.org:`` +- ☐ ``ssh master.gnome.org`` and then + ``ftpadmin install librsvg-x.y.z.tar.xz`` +- ☐ Create a `release in Gitlab <https://gitlab.gnome.org/GNOME/librsvg/-/releases>`_. + +For ``x.y.0`` releases, at least, do the following: + +- ☐ `Notify the release + team <https://gitlab.gnome.org/GNOME/releng/-/issues>`__ on whether + to use this ``librsvg-x.y.0`` for the next GNOME version via an issue + on their ``GNOME/releng`` project. + +- ☐ ``cargo-audit audit`` and ensure we don’t have vulnerable + dependencies. + +Gitlab release +-------------- + +- ☐ Go to https://gitlab.gnome.org/GNOME/librsvg/-/releases and click + the **New release** button. + +- ☐ Select the tag ``x.y.z`` you created as part of the release steps. + +- ☐ If there is an associated milestone, select it too. + +- ☐ Fill in the release title - ``x.y.z - stable`` or + ``x.y.z - development``. + +- ☐ Copy the release notes from NEWS. + +- ☐ Add a release asset link to + ``https://download.gnome.org/sources/librsvg/x.y/librsvg-x.y.z.tar.xz`` + and call it ``librsvg-x.y.z.tar.xz - release tarball``. + +- ☐ Add a release asset link to + ``https://download.gnome.org/sources/librsvg/x.y/librsvg-x.y.z.sha256sum`` + and call it + ``librsvg-x.y.z.sha256sum - release tarball sha256sum``. + +Version numbers +--------------- + +``configure.ac`` and ``Cargo.toml`` must have the same **package +version** number - this is the number that users of the library see. + +``configure.ac`` is where the **library version** is defined; this is +what gets encoded in the SONAME of ``librsvg.so``. + +Librsvg follows an even/odd numbering scheme for the **package +version**. For example, the 2.50.x series is for stable releases, and +2.51.x is for unstable/development ones. The +`release-team <https://gitlab.gnome.org/GNOME/releng/-/issues>`__ needs +to be notified when a new series comes about, so they can adjust their +tooling for the stable or development GNOME releases. File an issue in +their `repository <https://gitlab.gnome.org/GNOME/releng/-/issues>`__ to +indicate whether the new ``librsvg-x.y.0`` is a stable or development +series. + +Minimum supported Rust version (MSRV) +------------------------------------- + +While it may seem desirable to always require the latest released +version of the Rust toolchain, to get new language features and such, +this is really inconvenient for distributors of librsvg which do not +update Rust all the time. So, we make a compromise. + +The ``configure.ac`` script defines ``MININUM_RUST_MAJOR`` and +``MINIMUM_RUST_MINOR`` variables with librsvg’s minimum supported Rust +version (MSRV). These ensure that distros will get an early failure +during a build, at the ``configure`` step, if they have a version of +Rust that is too old — instead of getting an obscure error message from +``rustc`` in the middle of the build when it finds an unsupported +language construct. + +Please update both of these when increasing the MSRV: + +- The ``MININUM_RUST_MAJOR`` and ``MINIMUM_RUST_MINOR`` values in ``configure.ac``. + +- The ``rust-version`` value in ``Cargo.toml``. + +Sometimes librsvg’s dependencies update their MSRV and librsvg may need +to increase it as well. Please consider the following before doing this: + +- Absolutely do not require a nightly snapshot of the compiler, or + crates that only build on nightly. + +- Distributions with rolling releases usually keep their Rust + toolchains fairly well updated, maybe not always at the latest, but + within two or three releases earlier than the latest. If the MSRV you + want is within about six months of the latest, things are probably + safe. + +- Enterprise distributions update more slowly. It is useful to watch + for the MSRV that Firefox requires, although sometimes Firefox + updates Rust very slowly as well. Now that distributions are shipping + packages other than Firefox that require Rust, they will probably + start updating more frequently. + +Generally — two or three releases earlier than the latest stable Rust is +OK for rolling distros, probably perilous for enterprise distros. +Releases within a year of an enterprise distro’s shipping date are +probably OK. + +If you are not sure, ask on the `forum for GNOME +distributors <https://discourse.gnome.org/tag/distributor>`__ about +their plans! (That is, posts on ``discourse.gnome.org`` with the +``distributor`` tag.) + +Format for release notes in NEWS +-------------------------------- + +The ``NEWS`` file contains the release notes. Please use something +close to this format; it is not mandatory, but makes the formatting +consistent, and is what tooling expects elsewhere - also by writing +Markdown, you can just cut&paste it into a Gitlab release. You can skim +bits of the ``NEWS`` file for examples on style and content. + +New entries go at the **top** of the file. + +:: + + Version x.y.z + ============= + + Commentary on the release; put anything here that you want to + highlight. Note changes in the build process, if any, or any other + things that may trip up distributors. + + ## Description of a special feature + + You can include headings with `##` in Markdown syntax. + + Blah blah blah. + + + Next is a list of features added and issues fixed; use gitlab's issue + numbers. I tend to use this order: first security bugs, then new + features and user-visible changes, finally regular bugs. The + rationale is that if people stop reading early, at least they will + have seen the most important stuff first. + + ## Changes: + + - #123 - title of the issue, or short summary if it warrants more + discussion than just the title. + + - #456 - fix blah blah (Contributor's Name). + + ## Special thanks for this release: + + - Any people that you want to highlight. Feel free to omit this + section if the release is otherwise unremarkable. + +Making a tarball +---------------- + +:: + + make distcheck DESTDIR=/tmp/foo + +The ``DESTDIR`` is a quirk, required because otherwise the gdk-pixbuf +loader will try to install itself into the system’s location for pixbuf +loaders, and it won’t work. The ``DESTDIR`` is what Linux distribution +packaging scripts use to ``make install`` the compiled artifacts to a +temporary location before building a system package. + +Copying the tarball to master.gnome.org +--------------------------------------- + +If you don’t have a maintainer account there, ask federico@gnome.org to +do it or `ask the release +team <https://gitlab.gnome.org/GNOME/releng/-/issues>`__ to do it by +filing an issue on their ``GNOME/releng`` project. + +Rust dependencies +----------------- + +Librsvg's `Cargo.lock` is checked into git because the resolved +versions of crates that it mentions are the ones that were actually +used to run the test suite automatically in CI, and are "known good". +In other words: `keep the results of dependency resolution in version +control, and update those results manually +<https://blog.ometer.com/2017/01/10/dear-package-managers-dependency-resolution-results-should-be-in-version-control/>`_. + +It is important to keep these dependencies updated; you can do that +regularly with the ``cargo update`` step listed in the checklist +above. + +`cargo-audit <https://github.com/RustSec/cargo-audit>`__ is very +useful to scan the list of dependencies for registered vulnerabilities +in the `RustSec vulnerability database <https://rustsec.org/>`__. Run +it especially before making a new ``x.y.0`` release. + +Sometimes cargo-audit will report crates that are not vulnerable, but +that are unmaintained. Keep an eye of those; you may want to file bugs +upstream to see if the crates are really unmaintained or if they should +be substituted for something else. + +Creating a stable release branch +-------------------------------- + +- Create a branch named ``librsvg-xx.yy``, e.g. ``librsvg-2.54`` + +- Make the ``BASE_TAG`` in ``ci/container-builds.yml`` refer to the new + ``librsvg-xx.yy`` branch instead of ``main``. + +- Push that branch to origin. + +- (Branches with that naming scheme are already automatically protected + in gitlab’s Settings/Repository/Protected branches.) + +- Edit the badge for the stable branch so it points to the new branch: + Settings/General/Badges, find the existing badge for the stable + branch, click on the edit button that looks like a pencil. Change the + **Link** and **Badge image URL**; usually it is enough to just change + the version number in both. |