librsvg-2.59.0/COPYING.LIB000064400000000000000000000636421046102023000130440ustar 00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! librsvg-2.59.0/Cargo.toml0000644000000120150000000000100105760ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.77.2" name = "librsvg" version = "2.59.0" authors = [ "Federico Mena Quintero ", "Many others", ] exclude = [ "tests/fixtures", "tests/resources", ] description = """ A library to render SVG images to Cairo surfaces. GNOME uses this to render SVG icons. Outside of GNOME, other desktop environments use it for similar purposes. Wikimedia uses it for Wikipedia's SVG diagrams. """ homepage = "https://wiki.gnome.org/Projects/LibRsvg" documentation = "https://gnome.pages.gitlab.gnome.org/librsvg/doc/rsvg/index.html" readme = "README.md" license = "LGPL-2.1-or-later" repository = "https://gitlab.gnome.org/GNOME/librsvg/" [package.metadata.docs.rs] rustc-args = [ "--cfg", "docsrs", ] rustdoc-args = [ "--cfg", "docsrs", ] [package.metadata.system-deps] cairo = "1.18" cairo-gobject = "1.18" cairo-png = "1.18" freetype2 = "20.0.14" harfbuzz = "2.0" pangocairo = "1.50" [package.metadata.system-deps.cairo-pdf] optional = true version = "1.18" [package.metadata.system-deps.cairo-ps] optional = true version = "1.18" [package.metadata.system-deps.cairo-svg] optional = true version = "1.18" [package.metadata.system-deps."cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))".fontconfig] version = "1.7" [package.metadata.system-deps."cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))".pangoft2] version = "1.50" [package.metadata.system-deps."cfg(any(target_os = \"macos\", target_os = \"windows\"))".fontconfig] optional = true version = "1.7" [package.metadata.system-deps."cfg(any(target_os = \"macos\", target_os = \"windows\"))".pangoft2] optional = true version = "1.50" [package.metadata.system-deps.gio] name = "gio-2.0" version = "2.24" [package.metadata.system-deps.glib] name = "glib-2.0" version = "2.50" [package.metadata.system-deps.libxml2] fallback-names = ["libxml2"] name = "libxml-2.0" version = "2.9" [lib] name = "rsvg" [[bench]] name = "box_blur" harness = false [[bench]] name = "composite" harness = false [[bench]] name = "lighting" harness = false [[bench]] name = "path_parser" harness = false [[bench]] name = "pixel_iterators" harness = false [[bench]] name = "pixel_ops" harness = false [[bench]] name = "srgb" harness = false [dependencies.cairo-rs] version = "0.20" features = [ "v1_16", "png", "pdf", "ps", "svg", ] [dependencies.cast] version = "0.3.0" [dependencies.cssparser] version = "~0.31" [dependencies.data-url] version = "0.3.0" [dependencies.encoding_rs] version = "0.8.32" [dependencies.float-cmp] version = "0.9.0" [dependencies.gio] version = "0.20" [dependencies.glib] version = "0.20" [dependencies.image] version = "0.25.0" features = [ "jpeg", "png", "gif", "webp", ] default-features = false [dependencies.itertools] version = "0.13.0" [dependencies.language-tags] version = "0.3.1" [dependencies.libc] version = "0.2" [dependencies.locale_config] version = "0.3.0" [dependencies.markup5ever] version = "0.12.0" [dependencies.nalgebra] version = "0.33.0" [dependencies.num-traits] version = "0.2" [dependencies.pango] version = "0.20" features = ["v1_46"] [dependencies.pangocairo] version = "0.20" [dependencies.rayon] version = "1" [dependencies.rctree] version = "0.6.0" [dependencies.regex] version = "1.7.1" [dependencies.rgb] version = "0.8" features = ["argb"] [dependencies.selectors] version = "0.25.0" [dependencies.string_cache] version = "0.8.0" [dependencies.tinyvec] version = "1.2.0" features = [ "alloc", "rustc_1_55", ] [dependencies.url] version = "2" [dependencies.xml5ever] version = "0.18.0" [dev-dependencies.anyhow] version = "1.0" [dev-dependencies.chrono] version = "0.4.23" features = [ "clock", "std", ] default-features = false [dev-dependencies.criterion] version = "0.5" [dev-dependencies.lopdf] version = "0.33" [dev-dependencies.matches] version = "0.1" [dev-dependencies.png] version = "0.17.2" [dev-dependencies.predicates] version = "3.0.3" [dev-dependencies.proptest] version = "1.0.0" [dev-dependencies.quick-error] version = "2.0.0" [dev-dependencies.serde] version = "1.0" features = ["derive"] [dev-dependencies.serde_json] version = "1.0" [dev-dependencies.tempfile] version = "3" [build-dependencies.system-deps] version = "7.0.0" [features] avif = ["image/avif-native"] capi = [] test-utils = ["yeslogic-fontconfig-sys"] [target."cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))".dependencies.yeslogic-fontconfig-sys] version = "6.0.0" optional = true [lints.rust.unexpected_cfgs] level = "warn" priority = 0 librsvg-2.59.0/Cargo.toml.orig000064400000000000000000000066061046102023000142700ustar 00000000000000[package] name = "librsvg" documentation = "https://gnome.pages.gitlab.gnome.org/librsvg/doc/rsvg/index.html" version = "2.59.0" authors.workspace = true description.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true edition.workspace = true rust-version.workspace = true exclude = ["tests/fixtures", "tests/resources"] [package.metadata.docs.rs] rustc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"] [package.metadata.system-deps] cairo = "1.18" cairo-gobject = "1.18" cairo-png = "1.18" cairo-pdf = { version = "1.18", optional = true } cairo-ps = { version = "1.18", optional = true } cairo-svg = { version = "1.18", optional = true } freetype2 = "20.0.14" gio = { name = "gio-2.0", version = "2.24" } glib = { name = "glib-2.0", version = "2.50" } harfbuzz = "2.0" libxml2 = { name = "libxml-2.0", fallback-names = ["libxml2"], version = "2.9" } pangocairo = "1.50" [package.metadata.system-deps.'cfg(any(target_os = "macos", target_os = "windows"))'] fontconfig = { version = "1.7", optional = true } pangoft2 = { version = "1.50", optional = true } [package.metadata.system-deps.'cfg(all(not(target_os = "macos"), not(target_os = "windows")))'] fontconfig = { version = "1.7" } pangoft2 = { version = "1.50" } [features] avif = ["image/avif-native"] capi = [] test-utils = ["yeslogic-fontconfig-sys"] [lib] name = "rsvg" [dependencies] # Keep these in sync with respect to the cairo-rs version: # src/lib.rs - toplevel example in the docs cairo-rs = { workspace = true, features = ["v1_16", "png", "pdf", "ps", "svg"] } cast.workspace = true cssparser.workspace = true data-url.workspace = true encoding_rs.workspace = true float-cmp.workspace = true gio.workspace = true glib.workspace = true image = { workspace = true, features = ["jpeg", "png", "gif", "webp"] } itertools.workspace = true language-tags.workspace = true libc.workspace = true locale_config.workspace = true markup5ever.workspace = true nalgebra.workspace = true num-traits.workspace = true pango = { workspace = true, features = ["v1_46"] } pangocairo.workspace = true rayon.workspace = true rctree.workspace = true regex.workspace = true rgb = { workspace = true, features = ["argb"] } selectors.workspace = true string_cache.workspace = true tinyvec = { workspace = true, features = ["alloc", "rustc_1_55"] } url.workspace = true xml5ever.workspace = true [target.'cfg(all(not(target_os = "macos"), not(target_os = "windows")))'.dependencies] yeslogic-fontconfig-sys = { workspace = true, optional = true } [dev-dependencies] anyhow.workspace = true chrono = { workspace = true, features = ["clock", "std"] } criterion.workspace = true lopdf.workspace = true matches.workspace = true png.workspace = true predicates.workspace = true proptest.workspace = true quick-error.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true tempfile.workspace = true [build-dependencies] system-deps.workspace = true [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(system_deps_have_fontconfig)', 'cfg(system_deps_have_pangoft2)', ] } [[bench]] name = "box_blur" harness = false [[bench]] name = "composite" harness = false [[bench]] name = "lighting" harness = false [[bench]] name = "path_parser" harness = false [[bench]] name = "pixel_iterators" harness = false [[bench]] name = "pixel_ops" harness = false [[bench]] name = "srgb" harness = false librsvg-2.59.0/README.md000064400000000000000000000160611046102023000126540ustar 00000000000000# Librsvg This is librsvg - A small library to render Scalable Vector Graphics ([SVG][svg]), associated with the [GNOME Project][gnome]. It renders SVG files to [Cairo][cairo] surfaces. Cairo is the 2D, antialiased drawing library that GNOME uses to draw things to the screen or to generate output for printing. Do you want to render non-animated SVGs to a Cairo surface with a minimal API? Librsvg may be adequate for you. **Supported SVG/CSS features:** Please see the chapter for [supported features][features] in the development guide. This is the `README.md` file for the Rust crate; you may want to see the [main README.md](https://gitlab.gnome.org/GNOME/librsvg/-/blob/main/README.md) for the whole project. # Using librsvg * [Rust API documentation][rust-docs] * [Release notes][release-notes] **Non-Rust dependencies:** Librsvg depends on a number of non-Rust libraries that must be installed on your system. They need to have a pkg-config `.pc` file installed so that librsvg's compilation can find them via [system-deps][system-deps]: * Cairo - used for the main rendering * FreeType2 - font renderer * gdk-pixbuf - image decoder * gio/glib - I/O primitives and streams * Harfbuzz - text shaping * libxml2 - XML parser * Pangocairo - text rendering * PangoFT2 - render text via Pango and FreeType2 * Fontconfig - system fonts and rules for using them There are some [security considerations][sec-libs] for these non-Rust libraries, which you may want to read. [system-deps]: https://github.com/gdesmott/system-deps [sec-libs]: https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/security.html#librsvgs-dependencies **Bug tracking:** If you have found a bug, take a look at [our bug tracker][bugs]. Please see the "[reporting bugs][reporting-bugs]" page in the development guide to see how to provide a good bug report. **Asking questions:** Feel free to ask questions about using librsvg in the "Platform" category of [GNOME's Discourse][discourse]. You can also ask via chat in the Matrix room for [GNOME Rust][gnome-rust]. **Security:** For a list of releases with security issues, instructions on reporting security-related bugs, and the security considerations for librsvg's dependencies, see the [Security chapter][security] in the development guide. [rust-docs]: https://gnome.pages.gitlab.gnome.org/librsvg/doc/rsvg/index.html [release-notes]: https://gitlab.gnome.org/GNOME/librsvg/-/blob/main/NEWS # Contributing to librsvg's development There is a code of conduct for contributors to librsvg; please see the [GNOME Code of Conduct][coc]. Please see the [Development Guide for librsvg][devel-guide] on how to contribute to librsvg, how to report bugs, how set up your development environment, and for a description of librsvg's architecture. # Goals of librsvg Librsvg aims to be a low-footprint library for rendering SVG1.1 and SVG2 images. It is used primarily in the [GNOME project](https://www.gnome.org) to render SVG icons and vector images that appear on the desktop. It is also used in Wikimedia to render the SVG images that appear in Wikipedia, so that even old web browsers can display them. Many projects which casually need to render static SVG images use librsvg. We aim to be a "render this SVG for me, quickly, and with a minimal API" kind of library. Feature additions will be considered on a case-by-case basis. You can read about librsvg's [supported SVG and CSS features][features] in the development guide. # Non-goals of librsvg We don't aim to: * Implement every single SVG feature that is in the spec. * Implement scripting or external access to the SVG's DOM. * Implement support for CSS-based animations (but if you can think of a nice API to do this, we would be glad to know!) * Replace the industrial-strength SVG rendering machinery in modern web browsers. Of course, [contributions are welcome][contributing]. In particular, if you find nice ways of doing the above while still maintaining the existing API of librsvg, we would love to know about it! # Who uses librsvg? Librsvg is part of the [GNOME platform][platform]. Inside GNOME, librsvg takes multiple roles: * Loads SVGs from the generic gdk-pixbuf loader infrastructure, so any application which uses gdk-pixbuf can load SVGs as if they were raster images. * Loads SVG icons for the desktop. * Creates SVG thumbnails for the file manager. * Loads SVGs within GNOME's default image viewer, Eye of Gnome. Outside of GNOME's core: * GNOME games (chess, five-or-more, etc. to draw game pieces) * GIMP * GCompris * Claws-mail * Darktable * Mate-panel * Evas/Enlightenment * Emacs * ImageMagick * Wikipedia, to render SVGs as raster images for old browsers. *Special thanks to Wikimedia for providing excellent bug reports.* # Presentations on librsvg "[Replacing C library code with Rust: What I learned with librsvg][guadec-presentation-1]" was presented at GUADEC 2017. It gives a little history of librsvg, and how/why it is being ported to Rust from C. "[Patterns of refactoring C to Rust: the case of librsvg][guadec-presentation-2]" was presented at GUADEC 2018. It describes ways in which librsvg's C code was refactored to allow porting it to Rust. # Maintainers The maintainer of librsvg is [Federico Mena Quintero][federico]. Feel free to contact me for any questions you may have about librsvg, both its usage and its development. You can contact me in the following ways: * [Mail me][mail] at federico@gnome.org. * Matrix: I am `@federico` on the [GNOME Hackers][gnome-hackers] and [Rust ❤️ GNOME][gnome-rust] channels on gnome.org's Matrix. I'm there most weekdays (Mon-Fri) starting at about UTC 14:00 (that's 08:00 my time; I am in the UTC-6 timezone). If this is not a convenient time for you, feel free to [mail me][mail] and we can arrange a time. * I frequently [blog about librsvg][blog]. You may be interested in the articles about porting librsvg from C to Rust, which happened between 2016 and 2020. [svg]: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics [gnome]: https://www.gnome.org/ [cairo]: https://www.cairographics.org/ [contributing]: https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/contributing.html [coc]: https://conduct.gnome.org [devel-guide]: https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/index.html [features]: https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/features.html [security]: https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/security.html [bugs]: https://gitlab.gnome.org/GNOME/librsvg/issues [reporting-bugs]: https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/bugs.html [discourse]: https://discourse.gnome.org/c/platform/5 [gnome-rust]: https://matrix.to/#/#rust:gnome.org [platform]: https://developer.gnome.org/ [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 [federico]: https://viruta.org/ [mail]: mailto:federico@gnome.org [gnome-hackers]: https://matrix.to/#/#gnome-hackers:gnome.org [gnome-rust]: https://matrix.to/#/#rust:gnome.org [blog]: https://viruta.org/tag/librsvg.html librsvg-2.59.0/benches/box_blur.rs000064400000000000000000000037771046102023000152000ustar 00000000000000use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use rsvg::bench_only::{ AlphaOnly, Horizontal, IRect, NotAlphaOnly, SharedImageSurface, SurfaceType, Vertical, }; const SURFACE_SIDE: i32 = 512; const BOUNDS: IRect = IRect { x0: 64, y0: 64, x1: 64 + 64, y1: 64 + 64, }; fn bench_box_blur(c: &mut Criterion) { let mut group = c.benchmark_group("box_blur 9"); for input in [(false, false), (false, true), (true, false), (true, true)].iter() { group.bench_with_input( BenchmarkId::from_parameter(format!("{:?}", input)), &input, |b, &(vertical, alpha_only)| { let surface_type = if *alpha_only { SurfaceType::AlphaOnly } else { SurfaceType::SRgb }; let input_surface = SharedImageSurface::empty(SURFACE_SIDE, SURFACE_SIDE, surface_type).unwrap(); let mut output_surface = cairo::ImageSurface::create(cairo::Format::ARgb32, SURFACE_SIDE, SURFACE_SIDE) .unwrap(); const KERNEL_SIZE: usize = 9; let f = match (vertical, alpha_only) { (true, true) => SharedImageSurface::box_blur_loop::, (true, false) => SharedImageSurface::box_blur_loop::, (false, true) => SharedImageSurface::box_blur_loop::, (false, false) => SharedImageSurface::box_blur_loop::, }; b.iter(|| { f( &input_surface, &mut output_surface, BOUNDS, KERNEL_SIZE, KERNEL_SIZE / 2, ) }) }, ); } } criterion_group!(benches, bench_box_blur); criterion_main!(benches); librsvg-2.59.0/benches/composite.rs000064400000000000000000000022541046102023000153530ustar 00000000000000use criterion::{black_box, criterion_group, criterion_main, Criterion}; use rsvg::bench_only::{ composite_arithmetic, ExclusiveImageSurface, IRect, SharedImageSurface, SurfaceType, }; const SURFACE_SIDE: i32 = 512; const BOUNDS: IRect = IRect { x0: 64, y0: 64, x1: 64 + 64, y1: 64 + 64, }; fn bench_composite(c: &mut Criterion) { c.bench_function("composite arithmetic", |b| { let input_surface = SharedImageSurface::empty(SURFACE_SIDE, SURFACE_SIDE, SurfaceType::SRgb).unwrap(); let input_2_surface = SharedImageSurface::empty(SURFACE_SIDE, SURFACE_SIDE, SurfaceType::SRgb).unwrap(); let mut output_surface = ExclusiveImageSurface::new(SURFACE_SIDE, SURFACE_SIDE, SurfaceType::SRgb).unwrap(); let bounds = black_box(BOUNDS); b.iter(|| { composite_arithmetic( &input_surface, &input_2_surface, &mut output_surface, bounds, 0.5, 0.5, 0.5, 0.5, ); }) }); } criterion_group!(benches, bench_composite); criterion_main!(benches); librsvg-2.59.0/benches/lighting.rs000064400000000000000000000157211046102023000151610ustar 00000000000000use criterion::{criterion_group, criterion_main, Criterion}; use nalgebra::{Matrix3, Vector2}; use rsvg::bench_only::Normal; use rsvg::bench_only::{EdgeMode, IRect, PixelRectangle, Pixels, SharedImageSurface, SurfaceType}; /// Computes and returns the normal vector for the light filters. fn normal(surface: &SharedImageSurface, bounds: IRect, x: u32, y: u32) -> Normal { assert!(x as i32 >= bounds.x0); assert!(y as i32 >= bounds.y0); assert!((x as i32) < bounds.x1); assert!((y as i32) < bounds.y1); // Get the correct sobel kernel and factor for the pixel position. // Performance note: it's possible to replace the matrices with normal arrays. #[rustfmt::skip] let (factor_x, kx, factor_y, ky) = match (x as i32, y as i32) { (x, y) if (x, y) == (bounds.x0, bounds.y0) => ( 2. / 3., Matrix3::new( 0, 0, 0, 0, -2, 2, 0, -1, 1, ), 2. / 3., Matrix3::new( 0, 0, 0, 0, -2, -1, 0, 2, 1, ), ), (x, y) if (x + 1, y) == (bounds.x1, bounds.y0) => ( 2. / 3., Matrix3::new( 0, 0, 0, -2, 2, 0, -1, 1, 0, ), 2. / 3., Matrix3::new( 0, 0, 0, -1, -2, 0, 1, 2, 0, ), ), (x, y) if (x, y + 1) == (bounds.x0, bounds.y1) => ( 2. / 3., Matrix3::new( 0, -1, 1, 0, -2, 2, 0, 0, 0, ), 2. / 3., Matrix3::new( 0, -2, -1, 0, 2, 1, 0, 0, 0, ), ), (x, y) if (x + 1, y + 1) == (bounds.x1, bounds.y1) => ( 2. / 3., Matrix3::new( -1, 1, 0, -2, 2, 0, 0, 0, 0, ), 2. / 3., Matrix3::new( -1, -2, 0, 1, 2, 0, 0, 0, 0, ), ), (_, y) if y == bounds.y0 => ( 1. / 3., Matrix3::new( 0, 0, 0, -2, 0, 2, -1, 0, 1, ), 1. / 2., Matrix3::new( 0, 0, 0, -1, -2, -1, 1, 2, 1, ), ), (x, _) if x == bounds.x0 => ( 1. / 2., Matrix3::new( 0, -1, 1, 0, -2, 2, 0, -1, 1, ), 1. / 3., Matrix3::new( 0, -2, -1, 0, 0, 0, 0, 2, 1, ), ), (x, _) if x + 1 == bounds.x1 => ( 1. / 2., Matrix3::new( -1, 1, 0, -2, 2, 0, -1, 1, 0, ), 1. / 3., Matrix3::new( -1, -2, 0, 0, 0, 0, 1, 2, 0, ), ), (_, y) if y + 1 == bounds.y1 => ( 1. / 3., Matrix3::new( -1, 0, 1, -2, 0, 2, 0, 0, 0, ), 1. / 2., Matrix3::new( -1, -2, -1, 1, 2, 1, 0, 0, 0, ), ), _ => ( 1. / 4., Matrix3::new( -1, 0, 1, -2, 0, 2, -1, 0, 1, ), 1. / 4., Matrix3::new( -1, -2, -1, 0, 0, 0, 1, 2, 1, ), ), }; let kernel_bounds = IRect::new(x as i32 - 1, y as i32 - 1, x as i32 + 2, y as i32 + 2); let mut nx = 0; let mut ny = 0; for (x, y, pixel) in PixelRectangle::within(surface, bounds, kernel_bounds, EdgeMode::None) { let kernel_x = (x - kernel_bounds.x0) as usize; let kernel_y = (y - kernel_bounds.y0) as usize; nx += i16::from(pixel.a) * kx[(kernel_y, kernel_x)]; ny += i16::from(pixel.a) * ky[(kernel_y, kernel_x)]; } // Negative nx and ny to account for the different coordinate system. Normal { factor: Vector2::new(factor_x, factor_y), normal: Vector2::new(-nx, -ny), } } const SURFACE_SIDE: i32 = 512; const BOUNDS: IRect = IRect { x0: 64, y0: 64, x1: 64 + 64, y1: 64 + 64, }; fn bench_normal(c: &mut Criterion) { c.bench_function("normal", |b| { let surface = SharedImageSurface::empty(SURFACE_SIDE, SURFACE_SIDE, SurfaceType::SRgb).unwrap(); b.iter(|| { let mut z = 0; for (x, y, _pixel) in Pixels::within(&surface, BOUNDS) { let n = normal(&surface, BOUNDS, x, y); z += n.normal.x; } z }) }); c.bench_function("normal unrolled", |b| { let surface = SharedImageSurface::empty(SURFACE_SIDE, SURFACE_SIDE, SurfaceType::SRgb).unwrap(); b.iter(|| { let mut z = 0; // Top left. { let n = Normal::top_left(&surface, BOUNDS); z += n.normal.x; } // Top right. { let n = Normal::top_right(&surface, BOUNDS); z += n.normal.x; } // Bottom left. { let n = Normal::bottom_left(&surface, BOUNDS); z += n.normal.x; } // Bottom right. { let n = Normal::bottom_right(&surface, BOUNDS); z += n.normal.x; } // Top row. for x in BOUNDS.x0 as u32 + 1..BOUNDS.x1 as u32 - 1 { let n = Normal::top_row(&surface, BOUNDS, x); z += n.normal.x; } // Bottom row. for x in BOUNDS.x0 as u32 + 1..BOUNDS.x1 as u32 - 1 { let n = Normal::bottom_row(&surface, BOUNDS, x); z += n.normal.x; } // Left column. for y in BOUNDS.y0 as u32 + 1..BOUNDS.y1 as u32 - 1 { let n = Normal::left_column(&surface, BOUNDS, y); z += n.normal.x; } // Right column. for y in BOUNDS.y0 as u32 + 1..BOUNDS.y1 as u32 - 1 { let n = Normal::right_column(&surface, BOUNDS, y); z += n.normal.x; } // Interior pixels. for y in BOUNDS.y0 as u32 + 1..BOUNDS.y1 as u32 - 1 { for x in BOUNDS.x0 as u32 + 1..BOUNDS.x1 as u32 - 1 { let n = Normal::interior(&surface, BOUNDS, x, y); z += n.normal.x; } } z }) }); } criterion_group!(benches, bench_normal); criterion_main!(benches); librsvg-2.59.0/benches/path_parser.rs000064400000000000000000000033311046102023000156560ustar 00000000000000use criterion::{black_box, criterion_group, criterion_main, Criterion}; use rsvg::bench_only::Lexer; use rsvg::bench_only::PathBuilder; static INPUT: &str = "M10 20 C 30,40 50 60-70,80,90 100,110 120,130,140"; static BYTES: &[u8; 49] = b"M10 20 C 30,40 50 60-70,80,90 100,110 120,130,140"; static SLICE_EDGES: [(usize, usize); 14] = [ (1, 3), (4, 6), (9, 11), (12, 14), (15, 17), (18, 20), (20, 23), (24, 26), (27, 29), (30, 33), (34, 37), (38, 41), (42, 45), (46, 49), ]; fn lex_path(input: &str) { let lexer = Lexer::new(black_box(input)); for (_pos, _token) in lexer { // no-op } } fn path_parser(c: &mut Criterion) { c.bench_function("parse path into builder", |b| { let input = black_box(INPUT); b.iter(|| { let mut builder = PathBuilder::default(); let _ = builder.parse(input); }); }); c.bench_function("lex str", |b| { let input = black_box(INPUT); b.iter(|| { lex_path(input); }); }); // look at how much time *just* the parse:: part of the lexer should be taking... c.bench_function("std i32 parse (bytes)", |b| { let input = black_box(BYTES); let slice_boundaries = black_box(SLICE_EDGES); b.iter(|| { for (a, b) in slice_boundaries.iter() { let a: usize = *a; let b: usize = *b; unsafe { let _ = std::str::from_utf8_unchecked(&input[a..b]) .parse::() .unwrap(); } } }); }); } criterion_group!(benches, path_parser); criterion_main!(benches); librsvg-2.59.0/benches/pixel_iterators.rs000064400000000000000000000052161046102023000165670ustar 00000000000000use criterion::{black_box, criterion_group, criterion_main, Criterion}; use rsvg::bench_only::{ExclusiveImageSurface, IRect, Pixels, SharedImageSurface, SurfaceType}; const SURFACE_SIDE: i32 = 512; const BOUNDS: IRect = IRect { x0: 64, y0: 32, x1: 448, y1: 480, }; fn bench_pixel_iterators(c: &mut Criterion) { c.bench_function("pixel_iterators straightforward", |b| { let mut surface = ExclusiveImageSurface::new(SURFACE_SIDE, SURFACE_SIDE, SurfaceType::SRgb).unwrap(); let stride = surface.stride() as i32; let data = surface.data(); let bounds = black_box(BOUNDS); b.iter(|| { let mut r = 0usize; let mut g = 0usize; let mut b = 0usize; let mut a = 0usize; for y in bounds.y_range() { for x in bounds.x_range() { let base = (y * stride + x * 4) as usize; r += data[base] as usize; g += data[base + 1] as usize; b += data[base + 2] as usize; a += data[base + 3] as usize; } } (r, g, b, a) }) }); c.bench_function("pixel_iterators get_pixel", |b| { let surface = SharedImageSurface::empty(SURFACE_SIDE, SURFACE_SIDE, SurfaceType::SRgb).unwrap(); let bounds = black_box(BOUNDS); b.iter(|| { let mut r = 0usize; let mut g = 0usize; let mut b = 0usize; let mut a = 0usize; for y in bounds.y_range() { for x in bounds.x_range() { let pixel = surface.get_pixel(x as u32, y as u32); r += pixel.r as usize; g += pixel.g as usize; b += pixel.b as usize; a += pixel.a as usize; } } (r, g, b, a) }) }); c.bench_function("pixel_iterators pixels", |b| { let surface = SharedImageSurface::empty(SURFACE_SIDE, SURFACE_SIDE, SurfaceType::SRgb).unwrap(); let bounds = black_box(BOUNDS); b.iter(|| { let mut r = 0usize; let mut g = 0usize; let mut b = 0usize; let mut a = 0usize; for (_x, _y, pixel) in Pixels::within(&surface, bounds) { r += pixel.r as usize; g += pixel.g as usize; b += pixel.b as usize; a += pixel.a as usize; } (r, g, b, a) }) }); } criterion_group!(benches, bench_pixel_iterators); criterion_main!(benches); librsvg-2.59.0/benches/pixel_ops.rs000064400000000000000000000026551046102023000153600ustar 00000000000000use criterion::{black_box, criterion_group, criterion_main, Criterion}; use rsvg::bench_only::{Pixel, PixelOps}; const OTHER: Pixel = Pixel { r: 0x10, g: 0x20, b: 0x30, a: 0x40, }; const N: usize = 1024; fn make_pixels(n: usize) -> Vec { (0..n) .map(|i| Pixel { r: (i / 2) as u8, g: (i / 3) as u8, b: (i / 4) as u8, a: i as u8, }) .collect() } fn bench_op(pixels: &[Pixel], op: F) where F: Fn(&Pixel) -> Pixel, { let result: Vec = pixels.iter().map(op).collect(); black_box(result); } fn bench_pixel_ops(c: &mut Criterion) { c.bench_function("pixel_diff", |b| { let pixels = black_box(make_pixels(N)); let other = black_box(OTHER); b.iter(|| bench_op(&pixels, |pixel| pixel.diff(&other))) }); c.bench_function("pixel_to_luminance_mask", |b| { let pixels = black_box(make_pixels(N)); b.iter(|| bench_op(&pixels, |pixel| pixel.to_luminance_mask())) }); c.bench_function("pixel_premultiply", |b| { let pixels = black_box(make_pixels(N)); b.iter(|| bench_op(&pixels, |pixel| pixel.premultiply())) }); c.bench_function("pixel_unpremultiply", |b| { let pixels = black_box(make_pixels(N)); b.iter(|| bench_op(&pixels, |pixel| pixel.unpremultiply())) }); } criterion_group!(benches, bench_pixel_ops); criterion_main!(benches); librsvg-2.59.0/benches/srgb.rs000064400000000000000000000030051046102023000143010ustar 00000000000000use criterion::{black_box, criterion_group, criterion_main, Criterion}; use rsvg::bench_only::{ linearize, map_unpremultiplied_components_loop, ExclusiveImageSurface, IRect, ImageSurfaceDataExt, Pixel, SurfaceType, }; const SURFACE_SIDE: i32 = 512; const BOUNDS: IRect = IRect { x0: 64, y0: 32, x1: 448, y1: 480, }; fn bench_srgb_linearization(c: &mut Criterion) { c.bench_function("srgb map_unpremultiplied_components", |b| { let mut surface = ExclusiveImageSurface::new(SURFACE_SIDE, SURFACE_SIDE, SurfaceType::LinearRgb).unwrap(); // Fill the surface with non-zero alpha (otherwise linearization is a no-op). surface.modify(&mut |data, stride| { for y in BOUNDS.y_range() { for x in BOUNDS.x_range() { let pixel = Pixel { r: 0, g: 0, b: 0, a: 127, }; data.set_pixel(stride, pixel, x as u32, y as u32); } } }); let surface = surface.share().unwrap(); let mut output_surface = ExclusiveImageSurface::new(SURFACE_SIDE, SURFACE_SIDE, SurfaceType::SRgb).unwrap(); let bounds = black_box(BOUNDS); b.iter(|| { map_unpremultiplied_components_loop(&surface, &mut output_surface, bounds, linearize); }) }); } criterion_group!(benches, bench_srgb_linearization); criterion_main!(benches); librsvg-2.59.0/build.rs000064400000000000000000000033041046102023000130360ustar 00000000000000use std::env; use std::fs::File; use std::io::{BufWriter, Write}; use std::path::Path; #[cfg(docsrs)] fn probe_system_deps() { // do not probe libraries since the docs.rs environment doesn't have them } #[cfg(not(docsrs))] fn probe_system_deps() { if let Err(e) = system_deps::Config::new().probe() { eprintln!("{e}"); std::process::exit(1); } } fn main() { probe_system_deps(); generate_srgb_tables(); } /// Converts an sRGB color value to a linear sRGB color value (undoes the gamma correction). /// /// The input and the output are supposed to be in the [0, 1] range. #[inline] fn linearize(c: f64) -> f64 { if c <= (12.92 * 0.0031308) { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) } } /// Converts a linear sRGB color value to a normal sRGB color value (applies the gamma correction). /// /// The input and the output are supposed to be in the [0, 1] range. #[inline] fn unlinearize(c: f64) -> f64 { if c <= 0.0031308 { 12.92 * c } else { 1.055 * c.powf(1f64 / 2.4) - 0.055 } } fn print_table(w: &mut W, name: &str, f: F, len: u32) where W: Write, F: Fn(f64) -> f64, { writeln!(w, "const {name}: [u8; {len}] = [").unwrap(); for i in 0..len { let x = f(i as f64 / 255.0); let v = (x * 255.0).round() as u8; writeln!(w, " {v},").unwrap(); } writeln!(w, "];").unwrap(); } fn generate_srgb_tables() { let path = Path::new(&env::var("OUT_DIR").unwrap()).join("srgb-codegen.rs"); let mut file = BufWriter::new(File::create(path).unwrap()); print_table(&mut file, "LINEARIZE", linearize, 256); print_table(&mut file, "UNLINEARIZE", unlinearize, 256); } librsvg-2.59.0/example.svg000064400000000000000000000045341046102023000135530ustar 00000000000000librsvg-2.59.0/meson.build000064400000000000000000000251261046102023000135410ustar 00000000000000library_sources = files( 'Cargo.toml', 'build.rs', 'src/accept_language.rs', 'src/angle.rs', 'src/api.rs', 'src/aspect_ratio.rs', 'src/bbox.rs', 'src/color.rs', 'src/cond.rs', 'src/coord_units.rs', 'src/css.rs', 'src/dasharray.rs', 'src/document.rs', 'src/dpi.rs', 'src/drawing_ctx.rs', 'src/element.rs', 'src/error.rs', 'src/filter_func.rs', 'src/filter.rs', 'src/filters/blend.rs', 'src/filters/bounds.rs', 'src/filters/color_matrix.rs', 'src/filters/component_transfer.rs', 'src/filters/composite.rs', 'src/filters/context.rs', 'src/filters/convolve_matrix.rs', 'src/filters/displacement_map.rs', 'src/filters/error.rs', 'src/filters/flood.rs', 'src/filters/gaussian_blur.rs', 'src/filters/image.rs', 'src/filters/lighting.rs', 'src/filters/merge.rs', 'src/filters/mod.rs', 'src/filters/morphology.rs', 'src/filters/offset.rs', 'src/filters/tile.rs', 'src/filters/turbulence.rs', 'src/float_eq_cairo.rs', 'src/font_props.rs', 'src/gradient.rs', 'src/href.rs', 'src/image.rs', 'src/io.rs', 'src/iri.rs', 'src/layout.rs', 'src/length.rs', 'src/lib.rs', 'src/limits.rs', 'src/log.rs', 'src/marker.rs', 'src/node.rs', 'src/paint_server.rs', 'src/parsers.rs', 'src/path_builder.rs', 'src/path_parser.rs', 'src/pattern.rs', 'src/properties.rs', 'src/property_defs.rs', 'src/property_macros.rs', 'src/rect.rs', 'src/shapes.rs', 'src/space.rs', 'src/structure.rs', 'src/style.rs', 'src/surface_utils/iterators.rs', 'src/surface_utils/mod.rs', 'src/surface_utils/shared_surface.rs', 'src/surface_utils/srgb.rs', 'src/text.rs', 'src/transform.rs', 'src/ua.css', 'src/unit_interval.rs', 'src/url_resolver.rs', 'src/util.rs', 'src/viewbox.rs', 'src/ua.css', 'src/xml/attributes.rs', 'src/xml/mod.rs', 'src/xml/xml2_load.rs', 'src/xml/xml2.rs', ) rust_artifacts = custom_target( 'librsvg-2', build_by_default: true, output: '@0@rsvg_@1@.@2@'.format(lib_prefix, librsvg_api_major, ext_static), console: true, depend_files: library_sources + librsvg_c_sources, env: extra_env, command: [ cargo_wrapper, cargo_wrapper_args, '--command=cbuild', '--current-build-dir', '@OUTDIR@', '--current-source-dir', meson.current_source_dir(), '--packages', 'librsvg-c', '--extension', ext_static, ] + avif_feature_args + pixbuf_feature_args, ) librsvg_c_lib = rust_artifacts[0] makedef_args = [ makedef, '--regex', '^rsvg_.', ] if host_system in ['darwin', 'ios'] makedef_args += ['--os', 'darwin'] elif host_system in ['windows', 'cygwin'] makedef_args += ['--os', 'win'] else makedef_args += ['--os', 'linux'] endif if cc.symbols_have_underscore_prefix() makedef_args += ['--prefix', '_'] else makedef_args += ['--prefix', ''] endif makedef_tool_args = [] if is_msvc_style dumpbin = find_program('dumpbin') makedef_tool_args += ['--dumpbin', dumpbin] else nm = find_program('llvm-nm', required: false) if not nm.found() if host_system in ['darwin', 'ios'] warning('llvm-nm not found, you may experience problems when creating the shared librsvg.') warning('Please install the llvm-tools component through rustup, or point Meson to an existing LLVM installation.') endif nm = find_program('nm') endif makedef_tool_args += ['--nm', nm] endif sym_files = files( '../win32/librsvg.symbols' ) if pixbuf_dep.found() sym_files += files( '../win32/librsvg-pixbuf.symbols' ) endif librsvg_def = configure_file( command: [makedef_args, '--list', '@INPUT@'], input: sym_files, output: '@0@.def'.format(librsvg_pc), capture: true, ) librsvg_ver = custom_target( '@0@.ver'.format(librsvg_pc), command: [makedef_args, makedef_tool_args, '@INPUT@'], input: librsvg_c_lib, output: '@0@.ver'.format(librsvg_pc), capture: true, ) version_script = meson.current_build_dir() / '@0@.def'.format(librsvg_pc) if host_system in ['darwin', 'ios'] vflag = '-Wl,-exported_symbols_list,@0@'.format(version_script) else vflag = '-Wl,--version-script,@0@'.format(version_script) endif # This is not strictly needed, but since we are telling Cargo to build a staticlib, it puts in # all of Rust's standard library and code from dependencies even when it is not needed. # With the following, we shrink the final .so size; see issue #965. # # Amyspark: this is still needed after switching to cargo-cbuild, since # GIR requires a shared library under MSVC, and Meson will only accept it # if the library is a Meson generated target. # No further flags are needed under MSVC, because LINK will already strip # the library by default. # # Also check for -Bsymbolic-functions linker flag used to avoid # intra-library PLT jumps, if available. strip_link_args = cc.get_supported_link_arguments( '-Wl,-dead_strip', '-Wl,--gc-sections', '-Wl,-Bsymbolic-functions', ) link_args = cc.get_supported_link_arguments([vflag]) + strip_link_args # Some systems, reportedly OpenBSD and macOS, refuse # to create libraries without any object files. Compiling # this file, and adding its object file to the library, # will prevent the library from being empty. if cc.has_function_attribute('unused') rsvg_dummy = configure_file( command: [ python, '-c', 'print("static int __attribute__((unused)) __rsvg_dummy;")' ], capture: true, output: '_rsvg_dummy.c' ) else rsvg_dummy = configure_file( command: [ python, '-c', 'print("static int __rsvg_dummy; int dummy(void) { return __rsvg_dummy; }")' ], capture: true, output: '_rsvg_dummy.c' ) endif librsvg_lib = librsvg_c_lib # Wait on this dependency before trying to build using Rust librsvg_rust_dep = rust_artifacts if get_option('default_library') in ['shared', 'both'] librsvg_shared_lib = shared_library( rsvg_ver, rsvg_dummy, link_whole: librsvg_c_lib, link_args: link_args, link_depends: librsvg_ver, dependencies: library_dependencies, include_directories: [includeinc], vs_module_defs: librsvg_def, install: true, version: meson.project_version() # it's not exact as m4 ) if get_option('default_library') == 'shared' librsvg_lib = librsvg_shared_lib endif librsvg_rust_dep = librsvg_shared_lib endif if get_option('default_library') in ['static', 'both'] # Rust cannot yet use import libraries for MSVC which do not end in .lib. # The static library must be manually generated so that it matches Meson's # naming convention. # See https://github.com/rust-lang/rust/issues/122455 librsvg_lib = custom_target( 'rsvg-2-static', input: librsvg_c_lib, output: '@0@rsvg-@1@.@2@'.format(lib_prefix, librsvg_api_major, 'a'), command: [ python, '-c', 'import os; import sys; import shutil; shutil.copy(sys.argv[1], sys.argv[2])', '@INPUT0@', '@OUTPUT0@' ], install: true, install_dir: get_option('libdir'), ) librsvg_rust_dep = librsvg_lib endif # This is the main dependency to use for tests; it means "link to the library we just built" librsvg_dep = declare_dependency( sources: includes, include_directories: [includeinc], dependencies: library_dependencies, link_with: librsvg_lib, ) meson.override_dependency('librsvg-2.0', librsvg_dep) pkg = import('pkgconfig') # If any of the dependencies is e.g. wrap, ignore as we can't include # them without knowing how they exposed the pkg-config module # (if CMake, there's simply no way at all) has_pkgconfig_dependencies = true foreach i : library_dependencies_sole if i.found() and i.type_name() != 'pkgconfig' warning('One or more dependencies are not provided by pkg-config, skipping generation of the pkg-config module.') has_pkgconfig_dependencies = false break endif endforeach build_pixbuf_loader = build_pixbuf_loader.require(has_pkgconfig_dependencies, error_message: 'The gdk-pixbuf loader target requires the pkg-config module' ) if has_pkgconfig_dependencies librsvg_c_pc = pkg.generate( name : 'librsvg', filebase: librsvg_pc, description : 'library that renders svg files', libraries : [librsvg_lib] + other_library_dependencies, subdirs: librsvg_pc, requires: library_dependencies_sole, libraries_private: private_dependencies, ) endif if build_gir.allowed() gir_args = [ '-DRSVG_COMPILATION' ] gir_includes = [ 'GLib-2.0', 'GObject-2.0', 'Gio-2.0', 'cairo-1.0', ] vala_includes = [ 'gio-2.0', 'cairo', ] if build_pixbuf_loader.allowed() gir_includes += [ 'GdkPixbuf-2.0', ] vala_includes += [ 'gdk-pixbuf-2.0', ] endif if get_option('default_library') == 'shared' librsvg_gir = librsvg_lib else if is_msvc_style librsvg_gir = librsvg_shared_lib else librsvg_gir = shared_library( 'rsvg-gir', rsvg_dummy, link_whole: librsvg_c_lib, dependencies: library_dependencies, include_directories: [includeinc], ) endif endif rsvg_gir = gnome.generate_gir( librsvg_gir, namespace: 'Rsvg', nsversion: '2.0', symbol_prefix: [ 'rsvg', 'librsvg' ], dependencies: library_dependencies, includes: gir_includes, env: extra_env, export_packages: librsvg_pc, header: ['librsvg/rsvg.h'], install: true, sources: includes, extra_args: gir_args, fatal_warnings: get_option('werror'), ) endif if build_tests library_test_sources = files( 'tests/api.rs', 'tests/bugs.rs', 'tests/errors.rs', 'tests/filters.rs', 'tests/geometries.rs', 'tests/intrinsic_dimensions.rs', 'tests/loading_crash.rs', 'tests/loading_disallowed.rs', 'tests/primitive_geometries.rs', 'tests/primitives.rs', 'tests/reference.rs', 'tests/render_crash.rs', 'tests/shapes.rs', 'tests/text.rs', 'src/test_utils/compare_surfaces.rs', 'src/test_utils/mod.rs', 'src/test_utils/reference_utils.rs', ) test( 'Rust tests (rsvg)', cargo_wrapper, timeout: -1, # no timeout args: [ cargo_wrapper_args, '--current-build-dir', meson.current_build_dir(), '--command=test', '--current-source-dir', meson.current_source_dir(), '--packages', 'librsvg', ] + avif_feature_args, env: extra_env, depends: librsvg_rust_dep ) test( 'Rust tests (librsvg)', cargo_wrapper, timeout: -1, # no timeout args: [ cargo_wrapper_args, '--current-build-dir', meson.current_build_dir(), '--command=test', '--current-source-dir', meson.current_source_dir(), '--packages', 'librsvg-c', ] + avif_feature_args + pixbuf_feature_args, env: extra_env, depends: librsvg_rust_dep ) endif librsvg-2.59.0/src/accept_language.rs000064400000000000000000000351341046102023000156360ustar 00000000000000//! Parser for an Accept-Language HTTP header. use language_tags::{LanguageTag, ParseError}; use locale_config::{LanguageRange, Locale}; use std::error; use std::fmt; use std::str::FromStr; #[cfg(doc)] use crate::api::CairoRenderer; /// Used to set the language for rendering. /// /// SVG documents can use the `` element, whose children have a `systemLanguage` /// attribute; only the first child which has a `systemLanguage` that matches the /// preferred languages will be rendered. /// /// This enum, used with [`CairoRenderer::with_language`], configures how to obtain the /// user's prefererred languages. pub enum Language { /// Use the Unix environment variables `LANGUAGE`, `LC_ALL`, `LC_MESSAGES` and `LANG` to obtain the /// user's language. /// /// This uses [`g_get_language_names()`][ggln] underneath. /// /// [ggln]: https://docs.gtk.org/glib/func.get_language_names.html FromEnvironment, /// Use a list of languages in the form of an HTTP Accept-Language header, like `es, en;q=0.8`. /// /// This is convenient when you want to select an explicit set of languages, instead of /// assuming that the Unix environment has the language you want. AcceptLanguage(AcceptLanguage), } /// `Language` but with the environment's locale converted to something we can use. #[derive(Clone)] pub enum UserLanguage { LanguageTags(LanguageTags), AcceptLanguage(AcceptLanguage), } #[derive(Clone, Debug, PartialEq)] struct Weight(Option); impl Weight { fn numeric(&self) -> f32 { self.0.unwrap_or(1.0) } } #[derive(Clone, Debug, PartialEq)] struct Item { tag: LanguageTag, weight: Weight, } /// Stores a parsed version of an HTTP Accept-Language header. /// /// RFC 7231: #[derive(Clone, Debug, PartialEq)] pub struct AcceptLanguage(Box<[Item]>); /// Errors when parsing an `AcceptLanguage`. #[derive(Debug, PartialEq)] enum AcceptLanguageError { NoElements, InvalidCharacters, InvalidLanguageTag(ParseError), InvalidWeight, } impl error::Error for AcceptLanguageError {} impl fmt::Display for AcceptLanguageError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::NoElements => write!(f, "no language tags in list"), Self::InvalidCharacters => write!(f, "invalid characters in language list"), Self::InvalidLanguageTag(e) => write!(f, "invalid language tag: {e}"), Self::InvalidWeight => write!(f, "invalid q= weight"), } } } /// Optional whitespace, Space or Tab, per RFC 7230. /// /// RFC 7230: const OWS: [char; 2] = ['\x20', '\x09']; impl AcceptLanguage { /// Parses the payload of an HTTP Accept-Language header. /// /// For example, a valid header looks like `es, en;q=0.8`, and means, "I prefer Spanish, /// but will also accept English". /// /// Use this function to construct a [`Language::AcceptLanguage`] /// variant to pass to the [`CairoRenderer::with_language`] function. /// /// See RFC 7231 for details: pub fn parse(s: &str) -> Result { AcceptLanguage::parse_internal(s).map_err(|e| format!("{}", e)) } /// Internal constructor. We don't expose [`AcceptLanguageError`] in the public API; /// there we just use a [`String`]. fn parse_internal(s: &str) -> Result { if !s.is_ascii() { return Err(AcceptLanguageError::InvalidCharacters); } let mut items = Vec::new(); for val in s.split(',') { let trimmed = val.trim_matches(&OWS[..]); if trimmed.is_empty() { continue; } items.push(Item::parse(trimmed)?); } if items.is_empty() { Err(AcceptLanguageError::NoElements) } else { Ok(AcceptLanguage(items.into_boxed_slice())) } } fn iter(&self) -> impl Iterator { self.0.iter().map(|item| (&item.tag, item.weight.numeric())) } fn any_matches(&self, tag: &LanguageTag) -> bool { self.iter().any(|(self_tag, _weight)| tag.matches(self_tag)) } } impl Item { fn parse(s: &str) -> Result { let semicolon_pos = s.find(';'); let (before_semicolon, after_semicolon) = if let Some(semi) = semicolon_pos { (&s[..semi], Some(&s[semi + 1..])) } else { (s, None) }; let tag = LanguageTag::parse(before_semicolon) .map_err(AcceptLanguageError::InvalidLanguageTag)?; let weight = if let Some(quality) = after_semicolon { let quality = quality.trim_start_matches(&OWS[..]); let number = if let Some(qvalue) = quality.strip_prefix("q=") { if qvalue.starts_with(&['0', '1'][..]) { let first_digit = qvalue.chars().next().unwrap(); if let Some(decimals) = qvalue[1..].strip_prefix('.') { if (first_digit == '0' && decimals.len() <= 3 && decimals.chars().all(|c| c.is_ascii_digit())) || (first_digit == '1' && decimals.len() <= 3 && decimals.chars().all(|c| c == '0')) { qvalue } else { return Err(AcceptLanguageError::InvalidWeight); } } else { qvalue } } else { return Err(AcceptLanguageError::InvalidWeight); } } else { return Err(AcceptLanguageError::InvalidWeight); }; Weight(Some( f32::from_str(number).map_err(|_| AcceptLanguageError::InvalidWeight)?, )) } else { Weight(None) }; Ok(Item { tag, weight }) } } /// A list of BCP47 language tags. /// /// RFC 5664: #[derive(Debug, Clone, PartialEq)] pub struct LanguageTags(Vec); impl LanguageTags { pub fn empty() -> Self { LanguageTags(Vec::new()) } /// Converts a `Locale` to a set of language tags. pub fn from_locale(locale: &Locale) -> Result { let mut tags = Vec::new(); for locale_range in locale.tags_for("messages") { if locale_range == LanguageRange::invariant() { continue; } let str_locale_range = locale_range.as_ref(); let locale_tag = LanguageTag::from_str(str_locale_range).map_err(|e| { format!("invalid language tag \"{str_locale_range}\" in locale: {e}") })?; if !locale_tag.is_language_range() { return Err(format!( "language tag \"{locale_tag}\" is not a language range" )); } tags.push(locale_tag); } Ok(LanguageTags(tags)) } pub fn from(tags: Vec) -> LanguageTags { LanguageTags(tags) } pub fn iter(&self) -> impl Iterator { self.0.iter() } pub fn any_matches(&self, language_tag: &LanguageTag) -> bool { self.0.iter().any(|tag| tag.matches(language_tag)) } } impl UserLanguage { pub fn any_matches(&self, tags: &LanguageTags) -> bool { match *self { UserLanguage::LanguageTags(ref language_tags) => { tags.iter().any(|tag| language_tags.any_matches(tag)) } UserLanguage::AcceptLanguage(ref accept_language) => { tags.iter().any(|tag| accept_language.any_matches(tag)) } } } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_accept_language() { // plain tag assert_eq!( AcceptLanguage::parse_internal("es-MX").unwrap(), AcceptLanguage( vec![Item { tag: LanguageTag::parse("es-MX").unwrap(), weight: Weight(None) }] .into_boxed_slice() ) ); // with quality assert_eq!( AcceptLanguage::parse_internal("es-MX;q=1").unwrap(), AcceptLanguage( vec![Item { tag: LanguageTag::parse("es-MX").unwrap(), weight: Weight(Some(1.0)) }] .into_boxed_slice() ) ); // with quality assert_eq!( AcceptLanguage::parse_internal("es-MX;q=0").unwrap(), AcceptLanguage( vec![Item { tag: LanguageTag::parse("es-MX").unwrap(), weight: Weight(Some(0.0)) }] .into_boxed_slice() ) ); // zero decimals are allowed assert_eq!( AcceptLanguage::parse_internal("es-MX;q=0.").unwrap(), AcceptLanguage( vec![Item { tag: LanguageTag::parse("es-MX").unwrap(), weight: Weight(Some(0.0)) }] .into_boxed_slice() ) ); // zero decimals are allowed assert_eq!( AcceptLanguage::parse_internal("es-MX;q=1.").unwrap(), AcceptLanguage( vec![Item { tag: LanguageTag::parse("es-MX").unwrap(), weight: Weight(Some(1.0)) }] .into_boxed_slice() ) ); // one decimal assert_eq!( AcceptLanguage::parse_internal("es-MX;q=1.0").unwrap(), AcceptLanguage( vec![Item { tag: LanguageTag::parse("es-MX").unwrap(), weight: Weight(Some(1.0)) }] .into_boxed_slice() ) ); // two decimals assert_eq!( AcceptLanguage::parse_internal("es-MX;q=1.00").unwrap(), AcceptLanguage( vec![Item { tag: LanguageTag::parse("es-MX").unwrap(), weight: Weight(Some(1.0)) }] .into_boxed_slice() ) ); // three decimals assert_eq!( AcceptLanguage::parse_internal("es-MX;q=1.000").unwrap(), AcceptLanguage( vec![Item { tag: LanguageTag::parse("es-MX").unwrap(), weight: Weight(Some(1.0)) }] .into_boxed_slice() ) ); // multiple elements assert_eq!( AcceptLanguage::parse_internal("es-MX, en; q=0.5").unwrap(), AcceptLanguage( vec![ Item { tag: LanguageTag::parse("es-MX").unwrap(), weight: Weight(None) }, Item { tag: LanguageTag::parse("en").unwrap(), weight: Weight(Some(0.5)) }, ] .into_boxed_slice() ) ); // superfluous whitespace assert_eq!( AcceptLanguage::parse_internal(",es-MX;q=1.000 , en; q=0.125 , ,").unwrap(), AcceptLanguage( vec![ Item { tag: LanguageTag::parse("es-MX").unwrap(), weight: Weight(Some(1.0)) }, Item { tag: LanguageTag::parse("en").unwrap(), weight: Weight(Some(0.125)) }, ] .into_boxed_slice() ) ); } #[test] fn empty_lists() { assert!(matches!( AcceptLanguage::parse_internal(""), Err(AcceptLanguageError::NoElements) )); assert!(matches!( AcceptLanguage::parse_internal(","), Err(AcceptLanguageError::NoElements) )); assert!(matches!( AcceptLanguage::parse_internal(", , ,,,"), Err(AcceptLanguageError::NoElements) )); } #[test] fn ascii_only() { assert!(matches!( AcceptLanguage::parse_internal("ës"), Err(AcceptLanguageError::InvalidCharacters) )); } #[test] fn invalid_tag() { assert!(matches!( AcceptLanguage::parse_internal("no_underscores"), Err(AcceptLanguageError::InvalidLanguageTag(_)) )); } #[test] fn invalid_weight() { assert!(matches!( AcceptLanguage::parse_internal("es;"), Err(AcceptLanguageError::InvalidWeight) )); assert!(matches!( AcceptLanguage::parse_internal("es;q"), Err(AcceptLanguageError::InvalidWeight) )); assert!(matches!( AcceptLanguage::parse_internal("es;q="), Err(AcceptLanguageError::InvalidWeight) )); assert!(matches!( AcceptLanguage::parse_internal("es;q=2"), Err(AcceptLanguageError::InvalidWeight) )); assert!(matches!( AcceptLanguage::parse_internal("es;q=1.1"), Err(AcceptLanguageError::InvalidWeight) )); assert!(matches!( AcceptLanguage::parse_internal("es;q=1.12"), Err(AcceptLanguageError::InvalidWeight) )); assert!(matches!( AcceptLanguage::parse_internal("es;q=1.123"), Err(AcceptLanguageError::InvalidWeight) )); // Up to three decimals allowed per RFC 7231 assert!(matches!( AcceptLanguage::parse_internal("es;q=0.1234"), Err(AcceptLanguageError::InvalidWeight) )); } #[test] fn iter() { let accept_language = AcceptLanguage::parse_internal("es-MX, en; q=0.5").unwrap(); let mut iter = accept_language.iter(); let (tag, weight) = iter.next().unwrap(); assert_eq!(*tag, LanguageTag::parse("es-MX").unwrap()); assert_eq!(weight, 1.0); let (tag, weight) = iter.next().unwrap(); assert_eq!(*tag, LanguageTag::parse("en").unwrap()); assert_eq!(weight, 0.5); assert!(iter.next().is_none()); } } librsvg-2.59.0/src/angle.rs000064400000000000000000000123151046102023000136160ustar 00000000000000//! CSS angle values. use std::f64::consts::*; use cssparser::{Parser, Token}; use float_cmp::approx_eq; use crate::error::*; use crate::parsers::{finite_f32, Parse}; #[derive(Debug, Copy, Clone, PartialEq)] pub struct Angle(f64); impl Angle { pub fn new(rad: f64) -> Angle { Angle(Angle::normalize(rad)) } pub fn from_degrees(deg: f64) -> Angle { Angle(Angle::normalize(deg.to_radians())) } pub fn from_vector(vx: f64, vy: f64) -> Angle { let rad = vy.atan2(vx); if rad.is_nan() { Angle(0.0) } else { Angle(Angle::normalize(rad)) } } pub fn radians(self) -> f64 { self.0 } pub fn bisect(self, other: Angle) -> Angle { let half_delta = (other.0 - self.0) * 0.5; if FRAC_PI_2 < half_delta.abs() { Angle(Angle::normalize(self.0 + half_delta - PI)) } else { Angle(Angle::normalize(self.0 + half_delta)) } } //Flips an angle to be 180deg or PI radians rotated pub fn flip(self) -> Angle { Angle::new(self.radians() + PI) } // Normalizes an angle to [0.0, 2*PI) fn normalize(rad: f64) -> f64 { let res = rad % (PI * 2.0); if approx_eq!(f64, res, 0.0) { 0.0 } else if res < 0.0 { res + PI * 2.0 } else { res } } } // angle: // https://www.w3.org/TR/SVG/types.html#DataTypeAngle // // angle ::= number ("deg" | "grad" | "rad")? // impl Parse for Angle { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let angle = { let loc = parser.current_source_location(); let token = parser.next()?; match *token { Token::Number { value, .. } => { let degrees = finite_f32(value).map_err(|e| loc.new_custom_error(e))?; Angle::from_degrees(f64::from(degrees)) } Token::Dimension { value, ref unit, .. } => { let value = f64::from(finite_f32(value).map_err(|e| loc.new_custom_error(e))?); match unit.as_ref() { "deg" => Angle::from_degrees(value), "grad" => Angle::from_degrees(value * 360.0 / 400.0), "rad" => Angle::new(value), "turn" => Angle::from_degrees(value * 360.0), _ => { return Err(loc.new_unexpected_token_error(token.clone())); } } } _ => return Err(loc.new_unexpected_token_error(token.clone())), } }; Ok(angle) } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_angle() { assert_eq!(Angle::parse_str("0").unwrap(), Angle::new(0.0)); assert_eq!(Angle::parse_str("15").unwrap(), Angle::from_degrees(15.0)); assert_eq!( Angle::parse_str("180.5deg").unwrap(), Angle::from_degrees(180.5) ); assert_eq!(Angle::parse_str("1rad").unwrap(), Angle::new(1.0)); assert_eq!( Angle::parse_str("-400grad").unwrap(), Angle::from_degrees(-360.0) ); assert_eq!( Angle::parse_str("0.25turn").unwrap(), Angle::from_degrees(90.0) ); assert!(Angle::parse_str("").is_err()); assert!(Angle::parse_str("foo").is_err()); assert!(Angle::parse_str("300foo").is_err()); } fn test_bisection_angle( expected: f64, incoming_vx: f64, incoming_vy: f64, outgoing_vx: f64, outgoing_vy: f64, ) { let i = Angle::from_vector(incoming_vx, incoming_vy); let o = Angle::from_vector(outgoing_vx, outgoing_vy); let bisected = i.bisect(o); assert!(approx_eq!(f64, expected, bisected.radians())); } #[test] fn bisection_angle_is_correct_from_incoming_counterclockwise_to_outgoing() { // 1st quadrant test_bisection_angle(FRAC_PI_4, 1.0, 0.0, 0.0, 1.0); // 2nd quadrant test_bisection_angle(FRAC_PI_2 + FRAC_PI_4, 0.0, 1.0, -1.0, 0.0); // 3rd quadrant test_bisection_angle(PI + FRAC_PI_4, -1.0, 0.0, 0.0, -1.0); // 4th quadrant test_bisection_angle(PI + FRAC_PI_2 + FRAC_PI_4, 0.0, -1.0, 1.0, 0.0); } #[test] fn bisection_angle_is_correct_from_incoming_clockwise_to_outgoing() { // 1st quadrant test_bisection_angle(FRAC_PI_4, 0.0, 1.0, 1.0, 0.0); // 2nd quadrant test_bisection_angle(FRAC_PI_2 + FRAC_PI_4, -1.0, 0.0, 0.0, 1.0); // 3rd quadrant test_bisection_angle(PI + FRAC_PI_4, 0.0, -1.0, -1.0, 0.0); // 4th quadrant test_bisection_angle(PI + FRAC_PI_2 + FRAC_PI_4, 1.0, 0.0, 0.0, -1.0); } #[test] fn bisection_angle_is_correct_for_more_than_quarter_turn_angle() { test_bisection_angle(0.0, 0.1, -1.0, 0.1, 1.0); test_bisection_angle(FRAC_PI_2, 1.0, 0.1, -1.0, 0.1); test_bisection_angle(PI, -0.1, 1.0, -0.1, -1.0); test_bisection_angle(PI + FRAC_PI_2, -1.0, -0.1, 1.0, -0.1); } } librsvg-2.59.0/src/api.rs000064400000000000000000000772341046102023000133140ustar 00000000000000//! Public Rust API for librsvg. //! //! This gets re-exported from the toplevel `lib.rs`. #![warn(missing_docs)] use std::fmt; // Here we only re-export stuff in the public API. pub use crate::{ accept_language::{AcceptLanguage, Language}, drawing_ctx::Viewport, error::{DefsLookupErrorKind, ImplementationLimit, LoadingError}, length::{LengthUnit, RsvgLength as Length}, }; // Don't merge these in the "pub use" above! They are not part of the public API! use crate::{ accept_language::{LanguageTags, UserLanguage}, css::{Origin, Stylesheet}, document::{Document, LoadOptions, NodeId, RenderingOptions}, dpi::Dpi, drawing_ctx::SvgNesting, error::InternalRenderingError, length::NormalizeParams, node::{CascadedValues, Node}, rsvg_log, session::Session, url_resolver::UrlResolver, }; use url::Url; use std::path::Path; use std::sync::Arc; use gio::prelude::*; // Re-exposes glib's prelude as well use gio::Cancellable; use locale_config::{LanguageRange, Locale}; /// Errors that can happen while rendering or measuring an SVG document. #[non_exhaustive] #[derive(Debug, Clone)] pub enum RenderingError { /// An error from the rendering backend. Rendering(String), /// A particular implementation-defined limit was exceeded. LimitExceeded(ImplementationLimit), /// Tried to reference an SVG element that does not exist. IdNotFound, /// Tried to reference an SVG element from a fragment identifier that is incorrect. InvalidId(String), /// Not enough memory was available for rendering. OutOfMemory(String), /// The rendering was interrupted via a [`gio::Cancellable`]. /// /// See the documentation for [`CairoRenderer::with_cancellable`]. Cancelled, } impl std::error::Error for RenderingError {} impl From for RenderingError { fn from(e: cairo::Error) -> RenderingError { RenderingError::Rendering(format!("{e:?}")) } } impl From for RenderingError { fn from(e: InternalRenderingError) -> RenderingError { // These enums are mostly the same, except for cases that should definitely // not bubble up to the public API. So, we just move each variant, and for the // others, we emit a catch-all value as a safeguard. (We ought to panic in that case, // maybe.) match e { InternalRenderingError::Rendering(s) => RenderingError::Rendering(s), InternalRenderingError::LimitExceeded(l) => RenderingError::LimitExceeded(l), InternalRenderingError::InvalidTransform => { RenderingError::Rendering("invalid transform".to_string()) } InternalRenderingError::CircularReference(c) => { RenderingError::Rendering(format!("circular reference in node {c}")) } InternalRenderingError::IdNotFound => RenderingError::IdNotFound, InternalRenderingError::InvalidId(s) => RenderingError::InvalidId(s), InternalRenderingError::OutOfMemory(s) => RenderingError::OutOfMemory(s), InternalRenderingError::Cancelled => RenderingError::Cancelled, } } } impl fmt::Display for RenderingError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { RenderingError::Rendering(ref s) => write!(f, "rendering error: {s}"), RenderingError::LimitExceeded(ref l) => write!(f, "{l}"), RenderingError::IdNotFound => write!(f, "element id not found"), RenderingError::InvalidId(ref s) => write!(f, "invalid id: {s:?}"), RenderingError::OutOfMemory(ref s) => write!(f, "out of memory: {s}"), RenderingError::Cancelled => write!(f, "rendering cancelled"), } } } /// Builder for loading an [`SvgHandle`]. /// /// This is the starting point for using librsvg. This struct /// implements a builder pattern for configuring an [`SvgHandle`]'s /// options, and then loading the SVG data. You can call the methods /// of `Loader` in sequence to configure how SVG data should be /// loaded, and finally use one of the loading functions to load an /// [`SvgHandle`]. pub struct Loader { unlimited_size: bool, keep_image_data: bool, session: Session, } impl Loader { /// Creates a `Loader` with the default flags. /// /// * [`unlimited_size`](#method.with_unlimited_size) defaults to `false`, as malicious /// SVG documents could cause the XML parser to consume very large amounts of memory. /// /// * [`keep_image_data`](#method.keep_image_data) defaults to /// `false`. You may only need this if rendering to Cairo /// surfaces that support including image data in compressed /// formats, like PDF. /// /// # Example: /// /// ``` /// use rsvg; /// /// let svg_handle = rsvg::Loader::new() /// .read_path("example.svg") /// .unwrap(); /// ``` #[allow(clippy::new_without_default)] pub fn new() -> Self { Self { unlimited_size: false, keep_image_data: false, session: Session::default(), } } /// Creates a `Loader` from a pre-created [`Session`]. /// /// This is useful when a `Loader` must be created by the C API, which should already /// have created a session for logging. #[cfg(feature = "capi")] pub fn new_with_session(session: Session) -> Self { Self { unlimited_size: false, keep_image_data: false, session, } } /// Controls safety limits used in the XML parser. /// /// Internally, librsvg uses libxml2, which has set limits for things like the /// maximum length of XML element names, the size of accumulated buffers /// using during parsing of deeply-nested XML files, and the maximum size /// of embedded XML entities. /// /// Set this to `true` only if loading a trusted SVG fails due to size limits. /// /// # Example: /// ``` /// use rsvg; /// /// let svg_handle = rsvg::Loader::new() /// .with_unlimited_size(true) /// .read_path("example.svg") // presumably a trusted huge file /// .unwrap(); /// ``` pub fn with_unlimited_size(mut self, unlimited: bool) -> Self { self.unlimited_size = unlimited; self } /// Controls embedding of compressed image data into the renderer. /// /// Normally, Cairo expects one to pass it uncompressed (decoded) /// images as surfaces. However, when using a PDF rendering /// context to render SVG documents that reference raster images /// (e.g. those which include a bitmap as part of the SVG image), /// it may be more efficient to embed the original, compressed raster /// images into the PDF. /// /// Set this to `true` if you are using a Cairo PDF context, or any other type /// of context which allows embedding compressed images. /// /// # Example: /// /// ``` /// # use std::env; /// let svg_handle = rsvg::Loader::new() /// .keep_image_data(true) /// .read_path("example.svg") /// .unwrap(); /// /// let mut output = env::temp_dir(); /// output.push("output.pdf"); /// let surface = cairo::PdfSurface::new(640.0, 480.0, output)?; /// let cr = cairo::Context::new(&surface).expect("Failed to create a cairo context"); /// /// let renderer = rsvg::CairoRenderer::new(&svg_handle); /// renderer.render_document( /// &cr, /// &cairo::Rectangle::new(0.0, 0.0, 640.0, 480.0), /// )?; /// # Ok::<(), rsvg::RenderingError>(()) /// ``` pub fn keep_image_data(mut self, keep: bool) -> Self { self.keep_image_data = keep; self } /// Reads an SVG document from `path`. /// /// # Example: /// /// ``` /// let svg_handle = rsvg::Loader::new() /// .read_path("example.svg") /// .unwrap(); /// ``` pub fn read_path>(self, path: P) -> Result { let file = gio::File::for_path(path); self.read_file(&file, None::<&Cancellable>) } /// Reads an SVG document from a `gio::File`. /// /// The `cancellable` can be used to cancel loading from another thread. /// /// # Example: /// ``` /// let svg_handle = rsvg::Loader::new() /// .read_file(&gio::File::for_path("example.svg"), None::<&gio::Cancellable>) /// .unwrap(); /// ``` pub fn read_file, P: IsA>( self, file: &F, cancellable: Option<&P>, ) -> Result { let stream = file.read(cancellable)?; self.read_stream(&stream, Some(file), cancellable) } /// Reads an SVG stream from a `gio::InputStream`. /// /// The `base_file`, if it is not `None`, is used to extract the /// [base URL][crate#the-base-file-and-resolving-references-to-external-files] for this stream. /// /// Reading an SVG document may involve resolving relative URLs if the /// SVG references things like raster images, or other SVG files. /// In this case, pass the `base_file` that correspondds to the /// URL where this SVG got loaded from. /// /// The `cancellable` can be used to cancel loading from another thread. /// /// # Example /// /// ``` /// use gio::prelude::*; /// /// let file = gio::File::for_path("example.svg"); /// /// let stream = file.read(None::<&gio::Cancellable>).unwrap(); /// /// let svg_handle = rsvg::Loader::new() /// .read_stream(&stream, Some(&file), None::<&gio::Cancellable>) /// .unwrap(); /// ``` pub fn read_stream, F: IsA, P: IsA>( self, stream: &S, base_file: Option<&F>, cancellable: Option<&P>, ) -> Result { let base_file = base_file.map(|f| f.as_ref()); let base_url = if let Some(base_file) = base_file { Some(url_from_file(base_file)?) } else { None }; let load_options = LoadOptions::new(UrlResolver::new(base_url)) .with_unlimited_size(self.unlimited_size) .keep_image_data(self.keep_image_data); Ok(SvgHandle { document: Document::load_from_stream( self.session.clone(), Arc::new(load_options), stream.as_ref(), cancellable.map(|c| c.as_ref()), )?, session: self.session, }) } } fn url_from_file(file: &gio::File) -> Result { Url::parse(&file.uri()).map_err(|_| LoadingError::BadUrl) } /// Handle used to hold SVG data in memory. /// /// You can create this from one of the `read` methods in /// [`Loader`]. pub struct SvgHandle { session: Session, pub(crate) document: Document, } // Public API goes here impl SvgHandle { /// Checks if the SVG has an element with the specified `id`. /// /// Note that the `id` must be a plain fragment identifier like `#foo`, with /// a leading `#` character. /// /// The purpose of the `Err()` case in the return value is to indicate an /// incorrectly-formatted `id` argument. pub fn has_element_with_id(&self, id: &str) -> Result { let node_id = self.get_node_id(id)?; match self.lookup_node(&node_id) { Ok(_) => Ok(true), Err(InternalRenderingError::IdNotFound) => Ok(false), Err(e) => Err(e.into()), } } /// Sets a CSS stylesheet to use for an SVG document. /// /// During the CSS cascade, the specified stylesheet will be used /// with a "User" [origin]. /// /// Note that `@import` rules will not be resolved, except for `data:` URLs. /// /// [origin]: https://drafts.csswg.org/css-cascade-3/#cascading-origins pub fn set_stylesheet(&mut self, css: &str) -> Result<(), LoadingError> { let stylesheet = Stylesheet::from_data( css, &UrlResolver::new(None), Origin::User, self.session.clone(), )?; self.document.cascade(&[stylesheet], &self.session); Ok(()) } } // Private methods go here impl SvgHandle { fn get_node_id_or_root(&self, id: Option<&str>) -> Result, RenderingError> { match id { None => Ok(None), Some(s) => Ok(Some(self.get_node_id(s)?)), } } fn get_node_id(&self, id: &str) -> Result { let node_id = NodeId::parse(id).map_err(|_| RenderingError::InvalidId(id.to_string()))?; // The public APIs to get geometries of individual elements, or to render // them, should only allow referencing elements within the main handle's // SVG file; that is, only plain "#foo" fragment IDs are allowed here. // Otherwise, a calling program could request "another-file#foo" and cause // another-file to be loaded, even if it is not part of the set of // resources that the main SVG actually references. In the future we may // relax this requirement to allow lookups within that set, but not to // other random files. match node_id { NodeId::Internal(_) => Ok(node_id), NodeId::External(_, _) => { rsvg_log!( self.session, "the public API is not allowed to look up external references: {}", node_id ); Err(RenderingError::InvalidId( "cannot lookup references to elements in external files".to_string(), )) } } } fn get_node_or_root(&self, node_id: &Option) -> Result { if let Some(ref node_id) = *node_id { Ok(self.lookup_node(node_id)?) } else { Ok(self.document.root()) } } fn lookup_node(&self, node_id: &NodeId) -> Result { // The public APIs to get geometries of individual elements, or to render // them, should only allow referencing elements within the main handle's // SVG file; that is, only plain "#foo" fragment IDs are allowed here. // Otherwise, a calling program could request "another-file#foo" and cause // another-file to be loaded, even if it is not part of the set of // resources that the main SVG actually references. In the future we may // relax this requirement to allow lookups within that set, but not to // other random files. match node_id { NodeId::Internal(id) => self .document .lookup_internal_node(id) .ok_or(InternalRenderingError::IdNotFound), NodeId::External(_, _) => { unreachable!("caller should already have validated internal node IDs only") } } } } /// Can render an `SvgHandle` to a Cairo context. pub struct CairoRenderer<'a> { pub(crate) handle: &'a SvgHandle, pub(crate) dpi: Dpi, user_language: UserLanguage, cancellable: Option, is_testing: bool, } // Note that these are different than the C API's default, which is 90. const DEFAULT_DPI_X: f64 = 96.0; const DEFAULT_DPI_Y: f64 = 96.0; #[derive(Debug, Copy, Clone, PartialEq)] /// Contains the computed values of the `` element's `width`, `height`, and `viewBox`. /// /// An SVG document has a toplevel `` element, with optional attributes `width`, /// `height`, and `viewBox`. This structure contains the values for those attributes; you /// can obtain the struct from [`CairoRenderer::intrinsic_dimensions`]. /// /// Since librsvg 2.54.0, there is support for [geometry /// properties](https://www.w3.org/TR/SVG2/geometry.html) from SVG2. This means that /// `width` and `height` are no longer attributes; they are instead CSS properties that /// default to `auto`. The computed value for `auto` is `100%`, so for a `` that /// does not have these attributes/properties, the `width`/`height` fields will be /// returned as a [`Length`] of 100%. /// /// As an example, the following SVG element has a `width` of 100 pixels /// and a `height` of 400 pixels, but no `viewBox`. /// /// ```xml /// /// ``` /// /// In this case, the length fields will be set to the corresponding /// values with [`LengthUnit::Px`] units, and the `vbox` field will be /// set to to `None`. pub struct IntrinsicDimensions { /// Computed value of the `width` property of the ``. pub width: Length, /// Computed value of the `height` property of the ``. pub height: Length, /// `viewBox` attribute of the ``, if present. pub vbox: Option, } /// Gets the user's preferred locale from the environment and /// translates it to a `Locale` with `LanguageRange` fallbacks. /// /// The `Locale::current()` call only contemplates a single language, /// but glib is smarter, and `g_get_langauge_names()` can provide /// fallbacks, for example, when LC_MESSAGES="en_US.UTF-8:de" (USA /// English and German). This function converts the output of /// `g_get_language_names()` into a `Locale` with appropriate /// fallbacks. fn locale_from_environment() -> Locale { let mut locale = Locale::invariant(); for name in glib::language_names() { let name = name.as_str(); if let Ok(range) = LanguageRange::from_unix(name) { locale.add(&range); } } locale } impl UserLanguage { fn new(language: &Language, session: &Session) -> UserLanguage { match *language { Language::FromEnvironment => UserLanguage::LanguageTags( LanguageTags::from_locale(&locale_from_environment()) .map_err(|s| { rsvg_log!(session, "could not convert locale to language tags: {}", s); }) .unwrap_or_else(|_| LanguageTags::empty()), ), Language::AcceptLanguage(ref a) => UserLanguage::AcceptLanguage(a.clone()), } } } impl<'a> CairoRenderer<'a> { /// Creates a `CairoRenderer` for the specified `SvgHandle`. /// /// The default dots-per-inch (DPI) value is set to 96; you can change it /// with the [`with_dpi`] method. /// /// [`with_dpi`]: #method.with_dpi pub fn new(handle: &'a SvgHandle) -> Self { let session = &handle.session; CairoRenderer { handle, dpi: Dpi::new(DEFAULT_DPI_X, DEFAULT_DPI_Y), user_language: UserLanguage::new(&Language::FromEnvironment, session), cancellable: None, is_testing: false, } } /// Configures the dots-per-inch for resolving physical lengths. /// /// If an SVG document has physical units like `5cm`, they must be resolved /// to pixel-based values. The default pixel density is 96 DPI in /// both dimensions. pub fn with_dpi(self, dpi_x: f64, dpi_y: f64) -> Self { assert!(dpi_x > 0.0); assert!(dpi_y > 0.0); CairoRenderer { dpi: Dpi::new(dpi_x, dpi_y), ..self } } /// Configures the set of languages used for rendering. /// /// SVG documents can use the `` element, whose children have a /// `systemLanguage` attribute; only the first child which has a `systemLanguage` that /// matches the preferred languages will be rendered. /// /// This function sets the preferred languages. The default is /// `Language::FromEnvironment`, which means that the set of preferred languages will /// be obtained from the program's environment. To set an explicit list of languages, /// you can use `Language::AcceptLanguage` instead. pub fn with_language(self, language: &Language) -> Self { let user_language = UserLanguage::new(language, &self.handle.session); CairoRenderer { user_language, ..self } } /// Sets a cancellable to be able to interrupt rendering. /// /// The rendering functions like [`render_document`] will normally render the whole /// SVG document tree. However, they can be interrupted if you set a `cancellable` /// object with this method. To interrupt rendering, you can call /// [`gio::CancellableExt::cancel()`] from a different thread than where the rendering /// is happening. /// /// Since rendering happens as a side-effect on the Cairo context (`cr`) that is /// passed to the rendering functions, it may be that the `cr`'s target surface is in /// an undefined state if the rendering is cancelled. The surface may have not yet /// been painted on, or it may contain a partially-rendered document. For this /// reason, if your application does not want to leave the target surface in an /// inconsistent state, you may prefer to use a temporary surface for rendering, which /// can be discarded if your code cancels the rendering. /// /// [`render_document`]: #method.render_document pub fn with_cancellable>(self, cancellable: &C) -> Self { CairoRenderer { cancellable: Some(cancellable.clone().into()), ..self } } /// Queries the `width`, `height`, and `viewBox` attributes in an SVG document. /// /// If you are calling this function to compute a scaling factor to render the SVG, /// consider simply using [`render_document`] instead; it will do the scaling /// computations automatically. /// /// See also [`intrinsic_size_in_pixels`], which does the conversion to pixels if /// possible. /// /// [`render_document`]: #method.render_document /// [`intrinsic_size_in_pixels`]: #method.intrinsic_size_in_pixels pub fn intrinsic_dimensions(&self) -> IntrinsicDimensions { let d = self.handle.document.get_intrinsic_dimensions(); IntrinsicDimensions { width: Into::into(d.width), height: Into::into(d.height), vbox: d.vbox.map(|v| cairo::Rectangle::from(*v)), } } /// Converts the SVG document's intrinsic dimensions to pixels, if possible. /// /// Returns `Some(width, height)` in pixel units if the SVG document has `width` and /// `height` attributes with physical dimensions (CSS pixels, cm, in, etc.) or /// font-based dimensions (em, ex). /// /// Note that the dimensions are floating-point numbers, so your application can know /// the exact size of an SVG document. To get integer dimensions, you should use /// [`f64::ceil()`] to round up to the nearest integer (just using [`f64::round()`], /// may may chop off pixels with fractional coverage). /// /// If the SVG document has percentage-based `width` and `height` attributes, or if /// either of those attributes are not present, returns `None`. Dimensions of that /// kind require more information to be resolved to pixels; for example, the calling /// application can use a viewport size to scale percentage-based dimensions. pub fn intrinsic_size_in_pixels(&self) -> Option<(f64, f64)> { let dim = self.intrinsic_dimensions(); let width = dim.width; let height = dim.height; if width.unit == LengthUnit::Percent || height.unit == LengthUnit::Percent { return None; } Some(self.width_height_to_user(self.dpi)) } fn rendering_options(&self) -> RenderingOptions { RenderingOptions { dpi: self.dpi, cancellable: self.cancellable.clone(), user_language: self.user_language.clone(), svg_nesting: SvgNesting::Standalone, testing: self.is_testing, } } /// Renders the whole SVG document fitted to a viewport /// /// The `viewport` gives the position and size at which the whole SVG /// document will be rendered. /// /// The `cr` must be in a `cairo::Status::Success` state, or this function /// will not render anything, and instead will return /// `RenderingError::Cairo` with the `cr`'s current error state. pub fn render_document( &self, cr: &cairo::Context, viewport: &cairo::Rectangle, ) -> Result<(), RenderingError> { Ok(self.handle.document.render_document( &self.handle.session, cr, viewport, &self.rendering_options(), )?) } /// Computes the (ink_rect, logical_rect) of an SVG element, as if /// the SVG were rendered to a specific viewport. /// /// Element IDs should look like an URL fragment identifier; for /// example, pass `Some("#foo")` to get the geometry of the /// element that has an `id="foo"` attribute. /// /// The "ink rectangle" is the bounding box that would be painted /// for fully- stroked and filled elements. /// /// The "logical rectangle" just takes into account the unstroked /// paths and text outlines. /// /// Note that these bounds are not minimum bounds; for example, /// clipping paths are not taken into account. /// /// You can pass `None` for the `id` if you want to measure all /// the elements in the SVG, i.e. to measure everything from the /// root element. /// /// This operation is not constant-time, as it involves going through all /// the child elements. /// /// FIXME: example pub fn geometry_for_layer( &self, id: Option<&str>, viewport: &cairo::Rectangle, ) -> Result<(cairo::Rectangle, cairo::Rectangle), RenderingError> { let node_id = self.handle.get_node_id_or_root(id)?; let node = self.handle.get_node_or_root(&node_id)?; Ok(self.handle.document.get_geometry_for_layer( &self.handle.session, node, viewport, &self.rendering_options(), )?) } /// Renders a single SVG element in the same place as for a whole SVG document /// /// This is equivalent to `render_document`, but renders only a single element and its /// children, as if they composed an individual layer in the SVG. The element is /// rendered with the same transformation matrix as it has within the whole SVG /// document. Applications can use this to re-render a single element and repaint it /// on top of a previously-rendered document, for example. /// /// Note that the `id` must be a plain fragment identifier like `#foo`, with /// a leading `#` character. /// /// The `viewport` gives the position and size at which the whole SVG /// document would be rendered. This function will effectively place the /// whole SVG within that viewport, but only render the element given by /// `id`. /// /// The `cr` must be in a `cairo::Status::Success` state, or this function /// will not render anything, and instead will return /// `RenderingError::Cairo` with the `cr`'s current error state. pub fn render_layer( &self, cr: &cairo::Context, id: Option<&str>, viewport: &cairo::Rectangle, ) -> Result<(), RenderingError> { let node_id = self.handle.get_node_id_or_root(id)?; let node = self.handle.get_node_or_root(&node_id)?; Ok(self.handle.document.render_layer( &self.handle.session, cr, node, viewport, &self.rendering_options(), )?) } /// Computes the (ink_rect, logical_rect) of a single SVG element /// /// While `geometry_for_layer` computes the geometry of an SVG element subtree with /// its transformation matrix, this other function will compute the element's geometry /// as if it were being rendered under an identity transformation by itself. That is, /// the resulting geometry is as if the element got extracted by itself from the SVG. /// /// This function is the counterpart to `render_element`. /// /// Element IDs should look like an URL fragment identifier; for /// example, pass `Some("#foo")` to get the geometry of the /// element that has an `id="foo"` attribute. /// /// The "ink rectangle" is the bounding box that would be painted /// for fully- stroked and filled elements. /// /// The "logical rectangle" just takes into account the unstroked /// paths and text outlines. /// /// Note that these bounds are not minimum bounds; for example, /// clipping paths are not taken into account. /// /// You can pass `None` for the `id` if you want to measure all /// the elements in the SVG, i.e. to measure everything from the /// root element. /// /// This operation is not constant-time, as it involves going through all /// the child elements. /// /// FIXME: example pub fn geometry_for_element( &self, id: Option<&str>, ) -> Result<(cairo::Rectangle, cairo::Rectangle), RenderingError> { let node_id = self.handle.get_node_id_or_root(id)?; let node = self.handle.get_node_or_root(&node_id)?; Ok(self.handle.document.get_geometry_for_element( &self.handle.session, node, &self.rendering_options(), )?) } /// Renders a single SVG element to a given viewport /// /// This function can be used to extract individual element subtrees and render them, /// scaled to a given `element_viewport`. This is useful for applications which have /// reusable objects in an SVG and want to render them individually; for example, an /// SVG full of icons that are meant to be be rendered independently of each other. /// /// Note that the `id` must be a plain fragment identifier like `#foo`, with /// a leading `#` character. /// /// The `element_viewport` gives the position and size at which the named element will /// be rendered. FIXME: mention proportional scaling. /// /// The `cr` must be in a `cairo::Status::Success` state, or this function /// will not render anything, and instead will return /// `RenderingError::Cairo` with the `cr`'s current error state. pub fn render_element( &self, cr: &cairo::Context, id: Option<&str>, element_viewport: &cairo::Rectangle, ) -> Result<(), RenderingError> { let node_id = self.handle.get_node_id_or_root(id)?; let node = self.handle.get_node_or_root(&node_id)?; Ok(self.handle.document.render_element( &self.handle.session, cr, node, element_viewport, &self.rendering_options(), )?) } #[doc(hidden)] #[cfg(feature = "capi")] pub fn dpi(&self) -> Dpi { self.dpi } /// Normalizes the svg's width/height properties with a 0-sized viewport /// /// This assumes that if one of the properties is in percentage units, then /// its corresponding value will not be used. E.g. if width=100%, the caller /// will ignore the resulting width value. #[doc(hidden)] pub fn width_height_to_user(&self, dpi: Dpi) -> (f64, f64) { let dimensions = self.handle.document.get_intrinsic_dimensions(); let width = dimensions.width; let height = dimensions.height; let viewport = Viewport::new(dpi, 0.0, 0.0); let root = self.handle.document.root(); let cascaded = CascadedValues::new_from_node(&root); let values = cascaded.get(); let params = NormalizeParams::new(values, &viewport); (width.to_user(¶ms), height.to_user(¶ms)) } #[doc(hidden)] #[cfg(feature = "capi")] pub fn test_mode(self, is_testing: bool) -> Self { CairoRenderer { is_testing, ..self } } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/aspect_ratio.rs������������������������������������������������������������������0000644�0000000�0000000�00000035462�10461020230�0015215�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Handling of `preserveAspectRatio` values. //! //! This module handles `preserveAspectRatio` values [per the SVG specification][spec]. //! We have an [`AspectRatio`] struct which encapsulates such a value. //! //! ``` //! # use rsvg::doctest_only::AspectRatio; //! # use rsvg::doctest_only::Parse; //! assert_eq!( //! AspectRatio::parse_str("xMidYMid").unwrap(), //! AspectRatio::default() //! ); //! ``` //! //! [spec]: https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute use cssparser::{BasicParseError, Parser}; use std::ops::Deref; use crate::error::*; use crate::parse_identifiers; use crate::parsers::Parse; use crate::rect::Rect; use crate::transform::{Transform, ValidTransform}; use crate::viewbox::ViewBox; #[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] enum FitMode { #[default] Meet, Slice, } #[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] enum Align1D { Min, #[default] Mid, Max, } #[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] struct X(Align1D); #[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] struct Y(Align1D); impl Deref for X { type Target = Align1D; fn deref(&self) -> &Align1D { &self.0 } } impl Deref for Y { type Target = Align1D; fn deref(&self) -> &Align1D { &self.0 } } impl Align1D { fn compute(self, dest_pos: f64, dest_size: f64, obj_size: f64) -> f64 { match self { Align1D::Min => dest_pos, Align1D::Mid => dest_pos + (dest_size - obj_size) / 2.0, Align1D::Max => dest_pos + dest_size - obj_size, } } } #[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] struct Align { x: X, y: Y, fit: FitMode, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct AspectRatio { defer: bool, align: Option, } impl Default for AspectRatio { fn default() -> AspectRatio { AspectRatio { defer: false, align: Some(Align::default()), } } } impl AspectRatio { /// Produces the equivalent of `preserveAspectRatio="none"`. pub fn none() -> AspectRatio { AspectRatio { defer: false, align: None, } } pub fn is_slice(&self) -> bool { matches!( self.align, Some(Align { fit: FitMode::Slice, .. }) ) } pub fn compute(&self, vbox: &ViewBox, viewport: &Rect) -> Rect { match self.align { None => *viewport, Some(Align { x, y, fit }) => { let (vb_width, vb_height) = vbox.size(); let (vp_width, vp_height) = viewport.size(); let w_factor = vp_width / vb_width; let h_factor = vp_height / vb_height; let factor = match fit { FitMode::Meet => w_factor.min(h_factor), FitMode::Slice => w_factor.max(h_factor), }; let w = vb_width * factor; let h = vb_height * factor; let xpos = x.compute(viewport.x0, vp_width, w); let ypos = y.compute(viewport.y0, vp_height, h); Rect::new(xpos, ypos, xpos + w, ypos + h) } } } /// Computes the viewport to viewbox transformation. /// /// Given a viewport, returns a transformation that will create a coordinate /// space inside it. The `(vbox.x0, vbox.y0)` will be mapped to the viewport's /// upper-left corner, and the `(vbox.x1, vbox.y1)` will be mapped to the viewport's /// lower-right corner. /// /// If the vbox or viewport are empty, returns `Ok(None)`. Per the SVG spec, either /// of those mean that the corresponding element should not be rendered. /// /// If the vbox would create an invalid transform (say, a vbox with huge numbers that /// leads to a near-zero scaling transform), returns an `Err(())`. pub fn viewport_to_viewbox_transform( &self, vbox: Option, viewport: &Rect, ) -> Result, InvalidTransform> { // width or height set to 0 disables rendering of the element // https://www.w3.org/TR/SVG/struct.html#SVGElementWidthAttribute // https://www.w3.org/TR/SVG/struct.html#UseElementWidthAttribute // https://www.w3.org/TR/SVG/struct.html#ImageElementWidthAttribute // https://www.w3.org/TR/SVG/painting.html#MarkerWidthAttribute if viewport.is_empty() { return Ok(None); } // the preserveAspectRatio attribute is only used if viewBox is specified // https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute let transform = if let Some(vbox) = vbox { if vbox.is_empty() { // Width or height of 0 for the viewBox disables rendering of the element // https://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute return Ok(None); } else { let r = self.compute(&vbox, viewport); Transform::new_translate(r.x0, r.y0) .pre_scale(r.width() / vbox.width(), r.height() / vbox.height()) .pre_translate(-vbox.x0, -vbox.y0) } } else { Transform::new_translate(viewport.x0, viewport.y0) }; ValidTransform::try_from(transform).map(Some) } } fn parse_align_xy<'i>(parser: &mut Parser<'i, '_>) -> Result, BasicParseError<'i>> { use self::Align1D::*; parse_identifiers!( parser, "none" => None, "xMinYMin" => Some((X(Min), Y(Min))), "xMidYMin" => Some((X(Mid), Y(Min))), "xMaxYMin" => Some((X(Max), Y(Min))), "xMinYMid" => Some((X(Min), Y(Mid))), "xMidYMid" => Some((X(Mid), Y(Mid))), "xMaxYMid" => Some((X(Max), Y(Mid))), "xMinYMax" => Some((X(Min), Y(Max))), "xMidYMax" => Some((X(Mid), Y(Max))), "xMaxYMax" => Some((X(Max), Y(Max))), ) } fn parse_fit_mode<'i>(parser: &mut Parser<'i, '_>) -> Result> { parse_identifiers!( parser, "meet" => FitMode::Meet, "slice" => FitMode::Slice, ) } impl Parse for AspectRatio { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let defer = parser .try_parse(|p| p.expect_ident_matching("defer")) .is_ok(); let align_xy = parser.try_parse(parse_align_xy)?; let fit = parser.try_parse(parse_fit_mode).unwrap_or_default(); let align = align_xy.map(|(x, y)| Align { x, y, fit }); Ok(AspectRatio { defer, align }) } } #[cfg(test)] mod tests { use super::*; use crate::{assert_approx_eq_cairo, float_eq_cairo::ApproxEqCairo}; #[test] fn aspect_ratio_none() { assert_eq!(AspectRatio::none(), AspectRatio::parse_str("none").unwrap()); } #[test] fn parsing_invalid_strings_yields_error() { assert!(AspectRatio::parse_str("").is_err()); assert!(AspectRatio::parse_str("defer").is_err()); assert!(AspectRatio::parse_str("defer foo").is_err()); assert!(AspectRatio::parse_str("defer xMidYMid foo").is_err()); assert!(AspectRatio::parse_str("xMidYMid foo").is_err()); assert!(AspectRatio::parse_str("defer xMidYMid meet foo").is_err()); } #[test] fn parses_valid_strings() { assert_eq!( AspectRatio::parse_str("defer none").unwrap(), AspectRatio { defer: true, align: None, } ); assert_eq!( AspectRatio::parse_str("xMidYMid").unwrap(), AspectRatio { defer: false, align: Some(Align { x: X(Align1D::Mid), y: Y(Align1D::Mid), fit: FitMode::Meet, },), } ); assert_eq!( AspectRatio::parse_str("defer xMidYMid").unwrap(), AspectRatio { defer: true, align: Some(Align { x: X(Align1D::Mid), y: Y(Align1D::Mid), fit: FitMode::Meet, },), } ); assert_eq!( AspectRatio::parse_str("defer xMinYMax").unwrap(), AspectRatio { defer: true, align: Some(Align { x: X(Align1D::Min), y: Y(Align1D::Max), fit: FitMode::Meet, },), } ); assert_eq!( AspectRatio::parse_str("defer xMaxYMid meet").unwrap(), AspectRatio { defer: true, align: Some(Align { x: X(Align1D::Max), y: Y(Align1D::Mid), fit: FitMode::Meet, },), } ); assert_eq!( AspectRatio::parse_str("defer xMinYMax slice").unwrap(), AspectRatio { defer: true, align: Some(Align { x: X(Align1D::Min), y: Y(Align1D::Max), fit: FitMode::Slice, },), } ); } fn assert_rect_equal(r1: &Rect, r2: &Rect) { assert_approx_eq_cairo!(r1.x0, r2.x0); assert_approx_eq_cairo!(r1.y0, r2.y0); assert_approx_eq_cairo!(r1.x1, r2.x1); assert_approx_eq_cairo!(r1.y1, r2.y1); } #[test] fn aligns() { let viewbox = ViewBox::from(Rect::from_size(1.0, 10.0)); let foo = AspectRatio::parse_str("xMinYMin meet").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::from_size(0.1, 1.0)); let foo = AspectRatio::parse_str("xMinYMin slice").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::from_size(10.0, 100.0)); let foo = AspectRatio::parse_str("xMinYMid meet").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::from_size(0.1, 1.0)); let foo = AspectRatio::parse_str("xMinYMid slice").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::new(0.0, -49.5, 10.0, 100.0 - 49.5)); let foo = AspectRatio::parse_str("xMinYMax meet").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::from_size(0.1, 1.0)); let foo = AspectRatio::parse_str("xMinYMax slice").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::new(0.0, -99.0, 10.0, 1.0)); let foo = AspectRatio::parse_str("xMidYMin meet").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::new(4.95, 0.0, 4.95 + 0.1, 1.0)); let foo = AspectRatio::parse_str("xMidYMin slice").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::from_size(10.0, 100.0)); let foo = AspectRatio::parse_str("xMidYMid meet").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::new(4.95, 0.0, 4.95 + 0.1, 1.0)); let foo = AspectRatio::parse_str("xMidYMid slice").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::new(0.0, -49.5, 10.0, 100.0 - 49.5)); let foo = AspectRatio::parse_str("xMidYMax meet").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::new(4.95, 0.0, 4.95 + 0.1, 1.0)); let foo = AspectRatio::parse_str("xMidYMax slice").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::new(0.0, -99.0, 10.0, 1.0)); let foo = AspectRatio::parse_str("xMaxYMin meet").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::new(9.9, 0.0, 10.0, 1.0)); let foo = AspectRatio::parse_str("xMaxYMin slice").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::from_size(10.0, 100.0)); let foo = AspectRatio::parse_str("xMaxYMid meet").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::new(9.9, 0.0, 10.0, 1.0)); let foo = AspectRatio::parse_str("xMaxYMid slice").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::new(0.0, -49.5, 10.0, 100.0 - 49.5)); let foo = AspectRatio::parse_str("xMaxYMax meet").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::new(9.9, 0.0, 10.0, 1.0)); let foo = AspectRatio::parse_str("xMaxYMax slice").unwrap(); let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); assert_rect_equal(&foo, &Rect::new(0.0, -99.0, 10.0, 1.0)); } #[test] fn empty_viewport() { let a = AspectRatio::default(); let t = a .viewport_to_viewbox_transform( Some(ViewBox::parse_str("10 10 40 40").unwrap()), &Rect::from_size(0.0, 0.0), ) .unwrap(); assert_eq!(t, None); } #[test] fn empty_viewbox() { let a = AspectRatio::default(); let t = a .viewport_to_viewbox_transform( Some(ViewBox::parse_str("10 10 0 0").unwrap()), &Rect::from_size(10.0, 10.0), ) .unwrap(); assert_eq!(t, None); } #[test] fn valid_viewport_and_viewbox() { let a = AspectRatio::default(); let t = a .viewport_to_viewbox_transform( Some(ViewBox::parse_str("10 10 40 40").unwrap()), &Rect::new(1.0, 1.0, 2.0, 2.0), ) .unwrap(); assert_eq!( t, Some( ValidTransform::try_from( Transform::identity() .pre_translate(1.0, 1.0) .pre_scale(0.025, 0.025) .pre_translate(-10.0, -10.0) ) .unwrap() ) ); } #[test] fn invalid_viewbox() { let a = AspectRatio::default(); let t = a.viewport_to_viewbox_transform( Some(ViewBox::parse_str("0 0 6E20 540").unwrap()), &Rect::new(1.0, 1.0, 2.0, 2.0), ); assert_eq!(t, Err(InvalidTransform)); } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/bbox.rs��������������������������������������������������������������������������0000644�0000000�0000000�00000006521�10461020230�0013464�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Bounding boxes that know their coordinate space. use crate::rect::Rect; use crate::transform::Transform; #[derive(Debug, Default, Copy, Clone)] pub struct BoundingBox { transform: Transform, pub rect: Option, // without stroke pub ink_rect: Option, // with stroke } impl BoundingBox { pub fn new() -> BoundingBox { Default::default() } pub fn with_transform(self, transform: Transform) -> BoundingBox { BoundingBox { transform, ..self } } pub fn with_rect(self, rect: Rect) -> BoundingBox { BoundingBox { rect: Some(rect), ..self } } pub fn with_ink_rect(self, ink_rect: Rect) -> BoundingBox { BoundingBox { ink_rect: Some(ink_rect), ..self } } pub fn clear(mut self) { self.rect = None; self.ink_rect = None; } fn combine(&mut self, src: &BoundingBox, clip: bool) { if src.rect.is_none() && src.ink_rect.is_none() { return; } // this will panic!() if it's not invertible... should we check on our own? let transform = self .transform .invert() .unwrap() .pre_transform(&src.transform); self.rect = combine_rects(self.rect, src.rect, &transform, clip); self.ink_rect = combine_rects(self.ink_rect, src.ink_rect, &transform, clip); } pub fn insert(&mut self, src: &BoundingBox) { self.combine(src, false); } pub fn clip(&mut self, src: &BoundingBox) { self.combine(src, true); } } fn combine_rects( r1: Option, r2: Option, transform: &Transform, clip: bool, ) -> Option { match (r1, r2, clip) { (r1, None, _) => r1, (None, Some(r2), _) => Some(transform.transform_rect(&r2)), (Some(r1), Some(r2), true) => transform .transform_rect(&r2) .intersection(&r1) .or_else(|| Some(Rect::default())), (Some(r1), Some(r2), false) => Some(transform.transform_rect(&r2).union(&r1)), } } #[cfg(test)] mod tests { use super::*; #[test] fn combine() { let r1 = Rect::new(1.0, 2.0, 3.0, 4.0); let r2 = Rect::new(1.5, 2.5, 3.5, 4.5); let r3 = Rect::new(10.0, 11.0, 12.0, 13.0); let t = Transform::new_unchecked(1.0, 0.0, 0.0, 1.0, 0.5, 0.5); let res = combine_rects(None, None, &t, true); assert_eq!(res, None); let res = combine_rects(None, None, &t, false); assert_eq!(res, None); let res = combine_rects(Some(r1), None, &t, true); assert_eq!(res, Some(r1)); let res = combine_rects(Some(r1), None, &t, false); assert_eq!(res, Some(r1)); let res = combine_rects(None, Some(r2), &t, true); assert_eq!(res, Some(Rect::new(2.0, 3.0, 4.0, 5.0))); let res = combine_rects(None, Some(r2), &t, false); assert_eq!(res, Some(Rect::new(2.0, 3.0, 4.0, 5.0))); let res = combine_rects(Some(r1), Some(r2), &t, true); assert_eq!(res, Some(Rect::new(2.0, 3.0, 3.0, 4.0))); let res = combine_rects(Some(r1), Some(r3), &t, true); assert_eq!(res, Some(Rect::default())); let res = combine_rects(Some(r1), Some(r2), &t, false); assert_eq!(res, Some(Rect::new(1.0, 2.0, 4.0, 5.0))); } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/color.rs�������������������������������������������������������������������������0000644�0000000�0000000�00000015217�10461020230�0013652�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! CSS color values. use cssparser::{hsl_to_rgb, hwb_to_rgb, Color, ParseErrorKind, Parser, RGBA}; use crate::error::*; use crate::parsers::Parse; /// Turn a short-lived [`cssparser::ParseError`] into a long-lived [`ParseError`]. /// /// cssparser's error type has a lifetime equal to the string being parsed. We want /// a long-lived error so we can store it away if needed. Basically, here we turn /// a `&str` into a `String`. fn map_color_parse_error(err: cssparser::ParseError<'_, ()>) -> ParseError<'_> { let string_err = match err.kind { ParseErrorKind::Basic(ref e) => format!("{}", e), ParseErrorKind::Custom(()) => { // In cssparser 0.31, the error type for Color::parse is defined like this: // // pub fn parse<'i>(input: &mut Parser<'i, '_>) -> Result> { // // The ParseError<'i, ()> means that the ParseErrorKind::Custom(T) variant will have // T be the () type. // // So, here we match for () inside the Custom variant. If cssparser // changes its error API, this match will hopefully catch errors. // // Implementation detail: Color::parse() does not ever return Custom errors, only // Basic ones. So the match for Basic above handles everything, and this one // for () is a dummy case. "could not parse color".to_string() } }; ParseError { kind: ParseErrorKind::Custom(ValueErrorKind::Parse(string_err)), location: err.location, } } fn parse_plain_color<'i>(parser: &mut Parser<'i, '_>) -> Result> { let loc = parser.current_source_location(); let color = cssparser::Color::parse(parser).map_err(map_color_parse_error)?; // Return only supported color types, and mark the others as errors. match color { Color::CurrentColor | Color::Rgba(_) | Color::Hsl(_) | Color::Hwb(_) => Ok(color), _ => Err(ParseError { kind: ParseErrorKind::Custom(ValueErrorKind::parse_error("unsupported color syntax")), location: loc, }), } } /// Parse a custom property name. /// /// fn parse_name(s: &str) -> Result<&str, ()> { if s.starts_with("--") && s.len() > 2 { Ok(&s[2..]) } else { Err(()) } } fn parse_var_with_fallback<'i>( parser: &mut Parser<'i, '_>, ) -> Result> { let name = parser.expect_ident_cloned()?; // ignore the name for now; we'll use it later when we actually // process the names of custom variables let _name = parse_name(&name).map_err(|()| { parser.new_custom_error(ValueErrorKind::parse_error(&format!( "unexpected identifier {}", name ))) })?; parser.expect_comma()?; // FIXME: when fixing #459 (full support for var()), note that // https://drafts.csswg.org/css-variables/#using-variables indicates that var(--a,) is // a valid function, which means that the fallback value is an empty set of tokens. // // Also, see Servo's extra code to handle semicolons and stuff in toplevel rules. // // Also, tweak the tests tagged with "FIXME: var()" below. parse_plain_color(parser) } impl Parse for cssparser::Color { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { if let Ok(c) = parser.try_parse(|p| { p.expect_function_matching("var")?; p.parse_nested_block(parse_var_with_fallback) }) { Ok(c) } else { parse_plain_color(parser) } } } /// Normalizes `h` (a hue value in degrees) to be in the interval `[0.0, 1.0]`. /// /// Rust-cssparser (the cssparser-color crate) provides /// [`hsl_to_rgb()`], but it assumes that the hue is between 0 and 1. /// `normalize_hue()` takes a value with respect to a scale of 0 to /// 360 degrees and converts it to that different scale. fn normalize_hue(h: f32) -> f32 { h.rem_euclid(360.0) / 360.0 } pub fn color_to_rgba(color: &Color) -> RGBA { match color { Color::Rgba(rgba) => *rgba, Color::Hsl(hsl) => { let hue = normalize_hue(hsl.hue.unwrap_or(0.0)); let (red, green, blue) = hsl_to_rgb( hue, hsl.saturation.unwrap_or(0.0), hsl.lightness.unwrap_or(0.0), ); RGBA::from_floats(Some(red), Some(green), Some(blue), hsl.alpha) } Color::Hwb(hwb) => { let hue = normalize_hue(hwb.hue.unwrap_or(0.0)); let (red, green, blue) = hwb_to_rgb( hue, hwb.whiteness.unwrap_or(0.0), hwb.blackness.unwrap_or(0.0), ); RGBA::from_floats(Some(red), Some(green), Some(blue), hwb.alpha) } _ => unimplemented!(), } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_plain_color() { assert_eq!( Color::parse_str("#112233").unwrap(), Color::Rgba(RGBA::new(Some(0x11), Some(0x22), Some(0x33), Some(1.0))) ); } #[test] fn var_with_fallback_parses_as_color() { assert_eq!( Color::parse_str("var(--foo, #112233)").unwrap(), Color::Rgba(RGBA::new(Some(0x11), Some(0x22), Some(0x33), Some(1.0))) ); assert_eq!( Color::parse_str("var(--foo, rgb(100% 50% 25%)").unwrap(), Color::Rgba(RGBA::new(Some(0xff), Some(0x80), Some(0x40), Some(1.0))) ); } // FIXME: var() - when fixing #459, see the note in the code above. All the syntaxes // in this test function will become valid once we have full support for var(). #[test] fn var_without_fallback_yields_error() { assert!(Color::parse_str("var(--foo)").is_err()); assert!(Color::parse_str("var(--foo,)").is_err()); assert!(Color::parse_str("var(--foo, )").is_err()); assert!(Color::parse_str("var(--foo, this is not a color)").is_err()); assert!(Color::parse_str("var(--foo, #112233, blah)").is_err()); } #[test] fn normalizes_hue() { assert_eq!(normalize_hue(0.0), 0.0); assert_eq!(normalize_hue(360.0), 0.0); assert_eq!(normalize_hue(90.0), 0.25); assert_eq!(normalize_hue(-90.0), 0.75); assert_eq!(normalize_hue(450.0), 0.25); // 360 + 90 degrees assert_eq!(normalize_hue(-450.0), 0.75); } // Bug #1117 #[test] fn large_hue_value() { let _ = color_to_rgba(&Color::parse_str("hsla(70000000000000,4%,10%,.2)").unwrap()); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/cond.rs��������������������������������������������������������������������������0000644�0000000�0000000�00000016634�10461020230�0013463�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Conditional processing attributes: `requiredExtensions`, `requiredFeatures`, `systemLanguage`. #[allow(unused_imports, deprecated)] use std::ascii::AsciiExt; use std::str::FromStr; use language_tags::LanguageTag; use crate::accept_language::{LanguageTags, UserLanguage}; use crate::error::*; use crate::rsvg_log; use crate::session::Session; // No extensions at the moment. static IMPLEMENTED_EXTENSIONS: &[&str] = &[]; #[derive(Debug, PartialEq)] pub struct RequiredExtensions(pub bool); impl RequiredExtensions { /// Parse a requiredExtensions attribute. /// /// pub fn from_attribute(s: &str) -> RequiredExtensions { RequiredExtensions( s.split_whitespace() .all(|f| IMPLEMENTED_EXTENSIONS.binary_search(&f).is_ok()), ) } /// Evaluate a requiredExtensions value for conditional processing. pub fn eval(&self) -> bool { self.0 } } // Keep these sorted alphabetically for binary_search. static IMPLEMENTED_FEATURES: &[&str] = &[ "http://www.w3.org/TR/SVG11/feature#BasicFilter", "http://www.w3.org/TR/SVG11/feature#BasicGraphicsAttribute", "http://www.w3.org/TR/SVG11/feature#BasicPaintAttribute", "http://www.w3.org/TR/SVG11/feature#BasicStructure", "http://www.w3.org/TR/SVG11/feature#BasicText", "http://www.w3.org/TR/SVG11/feature#ConditionalProcessing", "http://www.w3.org/TR/SVG11/feature#ContainerAttribute", "http://www.w3.org/TR/SVG11/feature#Filter", "http://www.w3.org/TR/SVG11/feature#Gradient", "http://www.w3.org/TR/SVG11/feature#Image", "http://www.w3.org/TR/SVG11/feature#Marker", "http://www.w3.org/TR/SVG11/feature#Mask", "http://www.w3.org/TR/SVG11/feature#OpacityAttribute", "http://www.w3.org/TR/SVG11/feature#Pattern", "http://www.w3.org/TR/SVG11/feature#SVG", "http://www.w3.org/TR/SVG11/feature#SVG-static", "http://www.w3.org/TR/SVG11/feature#Shape", "http://www.w3.org/TR/SVG11/feature#Structure", "http://www.w3.org/TR/SVG11/feature#Style", "http://www.w3.org/TR/SVG11/feature#View", "org.w3c.svg.static", // deprecated SVG 1.0 feature string ]; #[derive(Debug, PartialEq)] pub struct RequiredFeatures(pub bool); impl RequiredFeatures { // Parse a requiredFeatures attribute // http://www.w3.org/TR/SVG/struct.html#RequiredFeaturesAttribute pub fn from_attribute(s: &str) -> RequiredFeatures { RequiredFeatures( s.split_whitespace() .all(|f| IMPLEMENTED_FEATURES.binary_search(&f).is_ok()), ) } /// Evaluate a requiredFeatures value for conditional processing. pub fn eval(&self) -> bool { self.0 } } /// The systemLanguage attribute inside `` element's children. /// /// Parsing the value of a `systemLanguage` attribute may fail if the document supplies /// invalid BCP47 language tags. In that case, we store an `Invalid` variant. /// /// That variant is used later, during [`SystemLanguage::eval`], to see whether the /// `` should match or not. #[derive(Debug, PartialEq)] pub enum SystemLanguage { Valid(LanguageTags), Invalid, } impl SystemLanguage { /// Parse a `systemLanguage` attribute and match it against a given `Locale` /// /// The [`systemLanguage`] conditional attribute is a /// comma-separated list of [BCP47] Language Tags. This function /// parses the attribute and matches the result against a given /// `locale`. If there is a match, i.e. if the given locale /// supports one of the languages listed in the `systemLanguage` /// attribute, then the `SystemLanguage.0` will be `true`; /// otherwise it will be `false`. /// /// Normally, calling code will pass `&Locale::current()` for the /// `locale` attribute; this is the user's current locale. /// /// [`systemLanguage`]: https://www.w3.org/TR/SVG/struct.html#ConditionalProcessingSystemLanguageAttribute /// [BCP47]: http://www.ietf.org/rfc/bcp/bcp47.txt pub fn from_attribute(s: &str, session: &Session) -> SystemLanguage { let attribute_tags = s .split(',') .map(str::trim) .map(|s| { LanguageTag::from_str(s).map_err(|e| { ValueErrorKind::parse_error(&format!("invalid language tag \"{s}\": {e}")) }) }) .collect::, _>>(); match attribute_tags { Ok(tags) => SystemLanguage::Valid(LanguageTags::from(tags)), Err(e) => { rsvg_log!( session, "ignoring systemLanguage attribute with invalid value: {}", e ); SystemLanguage::Invalid } } } /// Evaluate a systemLanguage value for conditional processing. pub fn eval(&self, user_language: &UserLanguage) -> bool { match *self { SystemLanguage::Valid(ref tags) => user_language.any_matches(tags), SystemLanguage::Invalid => false, } } } #[cfg(test)] mod tests { use super::*; use locale_config::Locale; #[test] fn required_extensions() { assert_eq!( RequiredExtensions::from_attribute("http://test.org/NotExisting/1.0"), RequiredExtensions(false) ); } #[test] fn required_features() { assert_eq!( RequiredFeatures::from_attribute("http://www.w3.org/TR/SVG11/feature#NotExisting"), RequiredFeatures(false) ); assert_eq!( RequiredFeatures::from_attribute("http://www.w3.org/TR/SVG11/feature#BasicFilter"), RequiredFeatures(true) ); assert_eq!( RequiredFeatures::from_attribute( "http://www.w3.org/TR/SVG11/feature#BasicFilter \ http://www.w3.org/TR/SVG11/feature#NotExisting", ), RequiredFeatures(false) ); assert_eq!( RequiredFeatures::from_attribute( "http://www.w3.org/TR/SVG11/feature#BasicFilter \ http://www.w3.org/TR/SVG11/feature#BasicText", ), RequiredFeatures(true) ); } #[test] fn system_language() { let session = Session::new_for_test_suite(); let locale = Locale::new("de,en-US").unwrap(); let user_language = UserLanguage::LanguageTags(LanguageTags::from_locale(&locale).unwrap()); assert!(matches!( SystemLanguage::from_attribute("", &session), SystemLanguage::Invalid )); assert!(matches!( SystemLanguage::from_attribute("12345", &session), SystemLanguage::Invalid )); assert!(!SystemLanguage::from_attribute("fr", &session).eval(&user_language)); assert!(!SystemLanguage::from_attribute("en", &session).eval(&user_language)); assert!(SystemLanguage::from_attribute("de", &session).eval(&user_language)); assert!(SystemLanguage::from_attribute("en-US", &session).eval(&user_language)); assert!(!SystemLanguage::from_attribute("en-GB", &session).eval(&user_language)); assert!(SystemLanguage::from_attribute("DE", &session).eval(&user_language)); assert!(SystemLanguage::from_attribute("de-LU", &session).eval(&user_language)); assert!(SystemLanguage::from_attribute("fr, de", &session).eval(&user_language)); } } ����������������������������������������������������������������������������������������������������librsvg-2.59.0/src/coord_units.rs�������������������������������������������������������������������0000644�0000000�0000000�00000005467�10461020230�0015072�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! `userSpaceOnUse` or `objectBoundingBox` values. use cssparser::Parser; use crate::error::*; use crate::parse_identifiers; use crate::parsers::Parse; /// Defines the units to be used for things that can consider a /// coordinate system in terms of the current transformation, or in /// terms of the current object's bounding box. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum CoordUnits { UserSpaceOnUse, ObjectBoundingBox, } impl Parse for CoordUnits { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { Ok(parse_identifiers!( parser, "userSpaceOnUse" => CoordUnits::UserSpaceOnUse, "objectBoundingBox" => CoordUnits::ObjectBoundingBox, )?) } } /// Creates a newtype around `CoordUnits`, with a default value. /// /// SVG attributes that can take `userSpaceOnUse` or /// `objectBoundingBox` values often have different default values /// depending on the type of SVG element. We use this macro to create /// a newtype for each SVG element and attribute that requires values /// of this type. The newtype provides an `impl Default` with the /// specified `$default` value. #[doc(hidden)] #[macro_export] macro_rules! coord_units { ($name:ident, $default:expr) => { #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct $name(pub CoordUnits); impl Default for $name { fn default() -> Self { $name($default) } } impl From<$name> for CoordUnits { fn from(u: $name) -> Self { u.0 } } impl $crate::parsers::Parse for $name { fn parse<'i>( parser: &mut ::cssparser::Parser<'i, '_>, ) -> Result> { Ok($name($crate::coord_units::CoordUnits::parse(parser)?)) } } }; } #[cfg(test)] mod tests { use super::*; coord_units!(MyUnits, CoordUnits::ObjectBoundingBox); #[test] fn parsing_invalid_strings_yields_error() { assert!(MyUnits::parse_str("").is_err()); assert!(MyUnits::parse_str("foo").is_err()); } #[test] fn parses_paint_server_units() { assert_eq!( MyUnits::parse_str("userSpaceOnUse").unwrap(), MyUnits(CoordUnits::UserSpaceOnUse) ); assert_eq!( MyUnits::parse_str("objectBoundingBox").unwrap(), MyUnits(CoordUnits::ObjectBoundingBox) ); } #[test] fn has_correct_default() { assert_eq!(MyUnits::default(), MyUnits(CoordUnits::ObjectBoundingBox)); } #[test] fn converts_to_coord_units() { assert_eq!( CoordUnits::from(MyUnits(CoordUnits::ObjectBoundingBox)), CoordUnits::ObjectBoundingBox ); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/css.rs���������������������������������������������������������������������������0000644�0000000�0000000�00000110433�10461020230�0013320�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Representation of CSS types, and the CSS parsing and matching engine. //! //! # Terminology //! //! Consider a CSS **stylesheet** like this: //! //! ```css //! @import url("another.css"); //! //! foo, .bar { //! fill: red; //! stroke: green; //! } //! //! #baz { stroke-width: 42; } //! ``` //! The example contains three **rules**, the first one is an **at-rule*, //! the other two are **qualified rules**. //! //! Each rule is made of two parts, a **prelude** and an optional **block** //! The prelude is the part until the first `{` or until `;`, depending on //! whether a block is present. The block is the part between curly braces. //! //! Let's look at each rule: //! //! `@import` is an **at-rule**. This rule has a prelude, but no block. //! There are other at-rules like `@media` and some of them may have a block, //! but librsvg doesn't support those yet. //! //! The prelude of the following rule is `foo, .bar`. //! It is a **selector list** with two **selectors**, one for //! `foo` elements and one for elements that have the `bar` class. //! //! The content of the block between `{}` for a qualified rule is a //! **declaration list**. The block of the first qualified rule contains two //! **declarations**, one for the `fill` **property** and one for the //! `stroke` property. //! //! After the first qualified rule, we have a second qualified rule with //! a single selector for the `#baz` id, with a single declaration for the //! `stroke-width` property. //! //! # Helper crates we use //! //! * `cssparser` crate as a CSS tokenizer, and some utilities to //! parse CSS rules and declarations. //! //! * `selectors` crate for the representation of selectors and //! selector lists, and for the matching engine. //! //! Both crates provide very generic implementations of their concepts, //! and expect the caller to provide implementations of various traits, //! and to provide types that represent certain things. //! //! For example, `cssparser` expects one to provide representations of //! the following types: //! //! * A parsed CSS rule. For `fill: blue;` we have //! `ParsedProperty::Fill(...)`. //! //! * A parsed selector list; we use `SelectorList` from the //! `selectors` crate. //! //! In turn, the `selectors` crate needs a way to navigate and examine //! one's implementation of an element tree. We provide `impl //! selectors::Element for RsvgElement` for this. This implementation //! has methods like "does this element have the id `#foo`", or "give //! me the next sibling element". //! //! Finally, the matching engine ties all of this together with //! `matches_selector()`. This takes an opaque representation of an //! element, plus a selector, and returns a bool. We iterate through //! the rules in the stylesheets and gather the matches; then sort the //! matches by specificity and apply the result to each element. use cssparser::{ self, match_ignore_ascii_case, parse_important, AtRuleParser, BasicParseErrorKind, CowRcStr, DeclarationParser, Parser, ParserInput, ParserState, QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, SourceLocation, StyleSheetParser, ToCss, }; use data_url::mime::Mime; use language_tags::LanguageTag; use markup5ever::{self, namespace_url, ns, Namespace, QualName}; use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; use selectors::matching::{ ElementSelectorFlags, IgnoreNthChildForInvalidation, MatchingContext, MatchingMode, NeedsSelectorFlags, QuirksMode, }; use selectors::parser::ParseRelative; use selectors::{NthIndexCache, OpaqueElement, SelectorImpl, SelectorList}; use std::cmp::Ordering; use std::fmt; use std::str; use std::str::FromStr; use crate::element::Element; use crate::error::*; use crate::io::{self, BinaryData}; use crate::node::{Node, NodeBorrow, NodeCascade}; use crate::properties::{parse_value, ComputedValues, ParseAs, ParsedProperty}; use crate::rsvg_log; use crate::session::Session; use crate::url_resolver::{AllowedUrl, UrlResolver}; /// A parsed CSS declaration /// /// For example, in the declaration `fill: green !important`, the /// `prop_name` would be `fill`, the `property` would be /// `ParsedProperty::Fill(...)` with the green value, and `important` /// would be `true`. pub struct Declaration { pub prop_name: QualName, pub property: ParsedProperty, pub important: bool, } /// This enum represents the fact that a rule body can be either a /// declaration or a nested rule. pub enum RuleBodyItem { Decl(Declaration), #[allow(dead_code)] // We don't support nested rules yet Rule(Rule), } /// Dummy struct required to use `cssparser::DeclarationListParser` /// /// It implements `cssparser::DeclarationParser`, which knows how to parse /// the property/value pairs from a CSS declaration. pub struct DeclParser; impl<'i> DeclarationParser<'i> for DeclParser { type Declaration = RuleBodyItem; type Error = ValueErrorKind; /// Parses a CSS declaration like `name: input_value [!important]` fn parse_value<'t>( &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, ) -> Result> { let prop_name = QualName::new(None, ns!(), markup5ever::LocalName::from(name.as_ref())); let property = parse_value(&prop_name, input, ParseAs::Property)?; let important = input.try_parse(parse_important).is_ok(); Ok(RuleBodyItem::Decl(Declaration { prop_name, property, important, })) } } // cssparser's DeclarationListParser requires this; we just use the dummy // implementations from cssparser itself. We may want to provide a real // implementation in the future, although this may require keeping track of the // CSS parsing state like Servo does. impl<'i> AtRuleParser<'i> for DeclParser { type Prelude = (); type AtRule = RuleBodyItem; type Error = ValueErrorKind; } /// We need this dummy implementation as well. impl<'i> QualifiedRuleParser<'i> for DeclParser { type Prelude = (); type QualifiedRule = RuleBodyItem; type Error = ValueErrorKind; } impl<'i> RuleBodyItemParser<'i, RuleBodyItem, ValueErrorKind> for DeclParser { /// We want to parse declarations. fn parse_declarations(&self) -> bool { true } /// We don't wanto parse qualified rules though. fn parse_qualified(&self) -> bool { false } } /// Struct to implement cssparser::QualifiedRuleParser and cssparser::AtRuleParser pub struct RuleParser { session: Session, } /// Errors from the CSS parsing process #[allow(dead_code)] // looks like we are not actually using this yet? #[derive(Debug)] pub enum ParseErrorKind<'i> { Selector(selectors::parser::SelectorParseErrorKind<'i>), } impl<'i> From> for ParseErrorKind<'i> { fn from(e: selectors::parser::SelectorParseErrorKind<'_>) -> ParseErrorKind<'_> { ParseErrorKind::Selector(e) } } /// A CSS qualified rule (or ruleset) pub struct QualifiedRule { selectors: SelectorList, declarations: Vec, } /// Prelude of at-rule used in the AtRuleParser. pub enum AtRulePrelude { Import(String), } /// A CSS at-rule (or ruleset) pub enum AtRule { Import(String), } /// A CSS rule (or ruleset) pub enum Rule { AtRule(AtRule), QualifiedRule(QualifiedRule), } // Required to implement the `Prelude` associated type in `cssparser::QualifiedRuleParser` impl<'i> selectors::Parser<'i> for RuleParser { type Impl = Selector; type Error = ParseErrorKind<'i>; fn default_namespace(&self) -> Option<::NamespaceUrl> { Some(ns!(svg)) } fn namespace_for_prefix( &self, _prefix: &::NamespacePrefix, ) -> Option<::NamespaceUrl> { // FIXME: Do we need to keep a lookup table extracted from libxml2's // XML namespaces? // // Or are CSS namespaces completely different, declared elsewhere? None } fn parse_non_ts_pseudo_class( &self, location: SourceLocation, name: CowRcStr<'i>, ) -> Result> { match &*name { "link" => Ok(NonTSPseudoClass::Link), "visited" => Ok(NonTSPseudoClass::Visited), _ => Err(location.new_custom_error( selectors::parser::SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name), )), } } fn parse_non_ts_functional_pseudo_class( &self, name: CowRcStr<'i>, arguments: &mut Parser<'i, '_>, ) -> Result> { match &*name { "lang" => { // Comma-separated lists of languages are a Selectors 4 feature, // but a pretty stable one that hasn't changed in a long time. let tags = arguments.parse_comma_separated(|arg| { let language_tag = arg.expect_ident_or_string()?.clone(); LanguageTag::from_str(&language_tag).map_err(|_| { arg.new_custom_error(selectors::parser::SelectorParseErrorKind::UnsupportedPseudoClassOrElement(language_tag)) }) })?; arguments.expect_exhausted()?; Ok(NonTSPseudoClass::Lang(tags)) } _ => Err(arguments.new_custom_error( selectors::parser::SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name), )), } } } // `cssparser::StyleSheetParser` is a struct which requires that we provide a type that // implements `cssparser::QualifiedRuleParser` and `cssparser::AtRuleParser`. // // In turn, `cssparser::QualifiedRuleParser` requires that we // implement a way to parse the `Prelude` of a ruleset or rule. For // example, in this ruleset: // // ```css // foo, .bar { fill: red; stroke: green; } // ``` // // The prelude is the selector list with the `foo` and `.bar` selectors. // // The `parse_prelude` method just uses `selectors::SelectorList`. This // is what requires the `impl selectors::Parser for RuleParser`. // // Next, the `parse_block` method takes an already-parsed prelude (a selector list), // and tries to parse the block between braces. It creates a `Rule` out of // the selector list and the declaration list. impl<'i> QualifiedRuleParser<'i> for RuleParser { type Prelude = SelectorList; type QualifiedRule = Rule; type Error = ValueErrorKind; fn parse_prelude<'t>( &mut self, input: &mut Parser<'i, 't>, ) -> Result> { SelectorList::parse(self, input, ParseRelative::No).map_err(|e| ParseError { kind: cssparser::ParseErrorKind::Custom(ValueErrorKind::parse_error( "Could not parse selector", )), location: e.location, }) } fn parse_block<'t>( &mut self, prelude: Self::Prelude, _start: &ParserState, input: &mut Parser<'i, 't>, ) -> Result> { let declarations = RuleBodyParser::<_, _, Self::Error>::new(input, &mut DeclParser) .filter_map(|r| match r { Ok(RuleBodyItem::Decl(decl)) => Some(decl), Ok(RuleBodyItem::Rule(_)) => None, Err(e) => { rsvg_log!(self.session, "Invalid declaration; ignoring: {:?}", e); None } }) .collect(); Ok(Rule::QualifiedRule(QualifiedRule { selectors: prelude, declarations, })) } } // Required by `cssparser::StyleSheetParser`. // // This only handles the `@import` at-rule. impl<'i> AtRuleParser<'i> for RuleParser { type Prelude = AtRulePrelude; type AtRule = Rule; type Error = ValueErrorKind; #[allow(clippy::type_complexity)] fn parse_prelude<'t>( &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, ) -> Result> { match_ignore_ascii_case! { &name, // FIXME: at the moment we ignore media queries "import" => { let url = input.expect_url_or_string()?.as_ref().to_owned(); Ok(AtRulePrelude::Import(url)) }, _ => Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name))), } } fn rule_without_block( &mut self, prelude: Self::Prelude, _start: &ParserState, ) -> Result { let AtRulePrelude::Import(url) = prelude; Ok(Rule::AtRule(AtRule::Import(url))) } // When we implement at-rules with blocks, implement the trait's parse_block() method here. } /// Dummy type required by the SelectorImpl trait. #[allow(clippy::upper_case_acronyms)] #[derive(Clone, Debug, Eq, PartialEq)] pub enum NonTSPseudoClass { Link, Visited, Lang(Vec), } impl ToCss for NonTSPseudoClass { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { match self { NonTSPseudoClass::Link => write!(dest, "link"), NonTSPseudoClass::Visited => write!(dest, "visited"), NonTSPseudoClass::Lang(lang) => write!( dest, "lang(\"{}\")", lang.iter() .map(ToString::to_string) .collect::>() .join("\",\"") ), } } } impl selectors::parser::NonTSPseudoClass for NonTSPseudoClass { type Impl = Selector; fn is_active_or_hover(&self) -> bool { false } fn is_user_action_state(&self) -> bool { false } } /// Dummy type required by the SelectorImpl trait #[derive(Clone, Debug, Eq, PartialEq)] pub struct PseudoElement; impl ToCss for PseudoElement { fn to_css(&self, _dest: &mut W) -> fmt::Result where W: fmt::Write, { Ok(()) } } impl selectors::parser::PseudoElement for PseudoElement { type Impl = Selector; } /// Holds all the types for the SelectorImpl trait #[derive(Debug, Clone)] pub struct Selector; /// Wrapper for attribute values. /// /// We use a newtype because the associated type Selector::AttrValue /// must implement `From<&str>` and `ToCss`, which are foreign traits. /// /// The `derive` requirements come from the `selectors` crate. #[derive(Clone, PartialEq, Eq)] pub struct AttributeValue(String); impl From<&str> for AttributeValue { fn from(s: &str) -> AttributeValue { AttributeValue(s.to_owned()) } } impl ToCss for AttributeValue { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { use std::fmt::Write; write!(cssparser::CssStringWriter::new(dest), "{}", &self.0) } } impl AsRef for AttributeValue { fn as_ref(&self) -> &str { self.0.as_ref() } } /// Wrapper for identifier values. /// /// Used to implement `ToCss` on the `LocalName` foreign type. #[derive(Clone, PartialEq, Eq)] pub struct Identifier(markup5ever::LocalName); impl From<&str> for Identifier { fn from(s: &str) -> Identifier { Identifier(markup5ever::LocalName::from(s)) } } impl ToCss for Identifier { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { cssparser::serialize_identifier(&self.0, dest) } } /// Wrapper for local names. /// /// Used to implement `ToCss` on the `LocalName` foreign type. #[derive(Clone, PartialEq, Eq)] pub struct LocalName(markup5ever::LocalName); impl From<&str> for LocalName { fn from(s: &str) -> LocalName { LocalName(markup5ever::LocalName::from(s)) } } impl ToCss for LocalName { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { cssparser::serialize_identifier(&self.0, dest) } } /// Wrapper for namespace prefixes. /// /// Used to implement `ToCss` on the `markup5ever::Prefix` foreign type. #[derive(Clone, Default, PartialEq, Eq)] pub struct NamespacePrefix(markup5ever::Prefix); impl From<&str> for NamespacePrefix { fn from(s: &str) -> NamespacePrefix { NamespacePrefix(markup5ever::Prefix::from(s)) } } impl ToCss for NamespacePrefix { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { cssparser::serialize_identifier(&self.0, dest) } } impl SelectorImpl for Selector { type ExtraMatchingData<'a> = (); type AttrValue = AttributeValue; type Identifier = Identifier; type LocalName = LocalName; type NamespaceUrl = Namespace; type NamespacePrefix = NamespacePrefix; type BorrowedNamespaceUrl = Namespace; type BorrowedLocalName = LocalName; type NonTSPseudoClass = NonTSPseudoClass; type PseudoElement = PseudoElement; } /// Newtype wrapper around `Node` so we can implement [`selectors::Element`] for it. /// /// `Node` is an alias for [`rctree::Node`], so we can't implement /// `selectors::Element` directly on it. We implement it on the /// `RsvgElement` wrapper instead. #[derive(Clone, PartialEq)] pub struct RsvgElement(Node); impl From for RsvgElement { fn from(n: Node) -> RsvgElement { RsvgElement(n) } } impl fmt::Debug for RsvgElement { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0.borrow()) } } // The selectors crate uses this to examine our tree of elements. impl selectors::Element for RsvgElement { type Impl = Selector; /// Converts self into an opaque representation. fn opaque(&self) -> OpaqueElement { // The `selectors` crate uses this value just for pointer comparisons, to answer // the question, "is this element the same as that one?". So, we'll give it a // reference to our actual node's data, i.e. skip over whatever wrappers there // are in rctree. // // We use an explicit type here to make it clear what the type is; otherwise you // may be fooled by the fact that borrow_element() returns a Ref, not a // plain reference: &Ref is transient and would get dropped at the end of this // function, but we want something long-lived. let element: &Element = &self.0.borrow_element(); OpaqueElement::new::(element) } fn parent_element(&self) -> Option { self.0.parent().map(|n| n.into()) } /// Whether the parent node of this element is a shadow root. fn parent_node_is_shadow_root(&self) -> bool { // unsupported false } /// The host of the containing shadow root, if any. fn containing_shadow_host(&self) -> Option { // unsupported None } /// Whether we're matching on a pseudo-element. fn is_pseudo_element(&self) -> bool { // unsupported false } /// Skips non-element nodes fn prev_sibling_element(&self) -> Option { let mut sibling = self.0.previous_sibling(); while let Some(ref sib) = sibling { if sib.is_element() { return sibling.map(|n| n.into()); } sibling = sib.previous_sibling(); } None } /// Skips non-element nodes fn next_sibling_element(&self) -> Option { let mut sibling = self.0.next_sibling(); while let Some(ref sib) = sibling { if sib.is_element() { return sibling.map(|n| n.into()); } sibling = sib.next_sibling(); } None } fn is_html_element_in_html_document(&self) -> bool { false } fn has_local_name(&self, local_name: &LocalName) -> bool { self.0.borrow_element().element_name().local == local_name.0 } /// Empty string for no namespace fn has_namespace(&self, ns: &Namespace) -> bool { self.0.borrow_element().element_name().ns == *ns } /// Whether this element and the `other` element have the same local name and namespace. fn is_same_type(&self, other: &Self) -> bool { self.0.borrow_element().element_name() == other.0.borrow_element().element_name() } fn attr_matches( &self, ns: &NamespaceConstraint<&Namespace>, local_name: &LocalName, operation: &AttrSelectorOperation<&AttributeValue>, ) -> bool { self.0 .borrow_element() .get_attributes() .iter() .find(|(attr, _)| { // do we have an attribute that matches the namespace and local_name? match *ns { NamespaceConstraint::Any => local_name.0 == attr.local, NamespaceConstraint::Specific(ns) => { QualName::new(None, ns.clone(), local_name.0.clone()) == *attr } } }) .map(|(_, value)| { // we have one; does the attribute's value match the expected operation? operation.eval_str(value) }) .unwrap_or(false) } fn match_non_ts_pseudo_class( &self, pc: &::NonTSPseudoClass, _context: &mut MatchingContext<'_, Self::Impl>, ) -> bool where { match pc { NonTSPseudoClass::Link => self.is_link(), NonTSPseudoClass::Visited => false, NonTSPseudoClass::Lang(css_lang) => self .0 .borrow_element() .get_computed_values() .xml_lang() .0 .as_ref() .map_or(false, |e_lang| { css_lang .iter() .any(|l| l.is_language_range() && l.matches(e_lang)) }), } } fn match_pseudo_element( &self, _pe: &::PseudoElement, _context: &mut MatchingContext<'_, Self::Impl>, ) -> bool { // unsupported false } /// Whether this element is a `link`. fn is_link(&self) -> bool { // Style as link only if href is specified at all. // // The SVG and CSS specifications do not seem to clearly // say what happens when you have an `` tag with no // `(xlink:|svg:)href` attribute. However, both Firefox and Chromium // consider a bare `` element with no href to be NOT // a link, so to avoid nasty surprises, we do the same. // Empty href's, however, ARE considered links. self.0.is_element() && match *self.0.borrow_element_data() { crate::element::ElementData::Link(ref link) => link.link.is_some(), _ => false, } } /// Returns whether the element is an HTML `` element. fn is_html_slot_element(&self) -> bool { false } fn has_id(&self, id: &Identifier, case_sensitivity: CaseSensitivity) -> bool { self.0 .borrow_element() .get_id() .map(|self_id| case_sensitivity.eq(self_id.as_bytes(), id.0.as_bytes())) .unwrap_or(false) } fn has_class(&self, name: &Identifier, case_sensitivity: CaseSensitivity) -> bool { self.0 .borrow_element() .get_class() .map(|classes| { classes .split_whitespace() .any(|class| case_sensitivity.eq(class.as_bytes(), name.0.as_bytes())) }) .unwrap_or(false) } fn imported_part(&self, _name: &Identifier) -> Option { // unsupported None } fn is_part(&self, _name: &Identifier) -> bool { // unsupported false } /// Returns whether this element matches `:empty`. /// /// That is, whether it does not contain any child element or any non-zero-length text node. /// See . fn is_empty(&self) -> bool { // .all() returns true for the empty iterator self.0 .children() .all(|child| child.is_chars() && child.borrow_chars().is_empty()) } /// Returns whether this element matches `:root`, /// i.e. whether it is the root element of a document. /// /// Note: this can be false even if `.parent_element()` is `None` /// if the parent node is a `DocumentFragment`. fn is_root(&self) -> bool { self.0.parent().is_none() } /// Returns the first child element of this element. fn first_element_child(&self) -> Option { self.0 .children() .find(|child| child.is_element()) .map(|n| n.into()) } /// Applies the given selector flags to this element. fn apply_selector_flags(&self, _: ElementSelectorFlags) { todo!() } } /// Origin for a stylesheet, per CSS 2.2. /// /// This is used when sorting selector matches according to their origin and specificity. /// /// CSS2.2: #[derive(Copy, Clone, Eq, Ord, PartialEq, PartialOrd)] pub enum Origin { UserAgent, User, Author, } /// A parsed CSS stylesheet. pub struct Stylesheet { origin: Origin, qualified_rules: Vec, } /// A match during the selector matching process /// /// This struct comes from [`Stylesheet::get_matches`], and represents /// that a certain node matched a CSS rule which has a selector with a /// certain `specificity`. The stylesheet's `origin` is also given here. /// /// This type implements [`Ord`] so a list of `Match` can be sorted. /// That implementation does ordering based on origin and specificity /// as per . struct Match<'a> { specificity: u32, origin: Origin, declaration: &'a Declaration, } impl<'a> Ord for Match<'a> { fn cmp(&self, other: &Self) -> Ordering { match self.origin.cmp(&other.origin) { Ordering::Equal => self.specificity.cmp(&other.specificity), o => o, } } } impl<'a> PartialOrd for Match<'a> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl<'a> PartialEq for Match<'a> { fn eq(&self, other: &Self) -> bool { self.origin == other.origin && self.specificity == other.specificity } } impl<'a> Eq for Match<'a> {} impl Stylesheet { fn empty(origin: Origin) -> Stylesheet { Stylesheet { origin, qualified_rules: Vec::new(), } } /// Parses a new stylesheet from CSS data in a string. /// /// The `url_resolver_url` is required for `@import` rules, so that librsvg can determine if /// the requested path is allowed. pub fn from_data( buf: &str, url_resolver: &UrlResolver, origin: Origin, session: Session, ) -> Result { let mut stylesheet = Stylesheet::empty(origin); stylesheet.add_rules_from_string(buf, url_resolver, session)?; Ok(stylesheet) } /// Parses a new stylesheet by loading CSS data from a URL. pub fn from_href( aurl: &AllowedUrl, origin: Origin, session: Session, ) -> Result { let mut stylesheet = Stylesheet::empty(origin); stylesheet.load(aurl, session)?; Ok(stylesheet) } /// Parses the CSS rules in `buf` and appends them to the stylesheet. /// /// The `url_resolver_url` is required for `@import` rules, so that librsvg can determine if /// the requested path is allowed. /// /// If there is an `@import` rule, its rules will be recursively added into the /// stylesheet, in the order in which they appear. fn add_rules_from_string( &mut self, buf: &str, url_resolver: &UrlResolver, session: Session, ) -> Result<(), LoadingError> { let mut input = ParserInput::new(buf); let mut parser = Parser::new(&mut input); let mut rule_parser = RuleParser { session: session.clone(), }; StyleSheetParser::new(&mut parser, &mut rule_parser) .filter_map(|r| match r { Ok(rule) => Some(rule), Err(e) => { rsvg_log!(session, "Invalid rule; ignoring: {:?}", e); None } }) .for_each(|rule| match rule { Rule::AtRule(AtRule::Import(url)) => match url_resolver.resolve_href(&url) { Ok(aurl) => { // ignore invalid imports let _ = self.load(&aurl, session.clone()); } Err(e) => { rsvg_log!(session, "Not loading stylesheet from \"{}\": {}", url, e); } }, Rule::QualifiedRule(qr) => self.qualified_rules.push(qr), }); Ok(()) } /// Parses a stylesheet referenced by an URL fn load(&mut self, aurl: &AllowedUrl, session: Session) -> Result<(), LoadingError> { io::acquire_data(aurl, None) .map_err(LoadingError::from) .and_then(|data| { let BinaryData { data: bytes, mime_type, } = data; if is_text_css(&mime_type) { Ok(bytes) } else { rsvg_log!(session, "\"{}\" is not of type text/css; ignoring", aurl); Err(LoadingError::BadCss) } }) .and_then(|bytes| { String::from_utf8(bytes).map_err(|_| { rsvg_log!( session, "\"{}\" does not contain valid UTF-8 CSS data; ignoring", aurl ); LoadingError::BadCss }) }) .and_then(|utf8| { let url = (**aurl).clone(); self.add_rules_from_string(&utf8, &UrlResolver::new(Some(url)), session) }) } /// Appends the style declarations that match a specified node to a given vector fn get_matches<'a>( &'a self, node: &Node, match_ctx: &mut MatchingContext<'_, Selector>, acc: &mut Vec>, ) { for rule in &self.qualified_rules { for selector in &rule.selectors.0 { // This magic call is stolen from selectors::matching::matches_selector_list() let matches = selectors::matching::matches_selector( selector, 0, None, &RsvgElement(node.clone()), match_ctx, ); if matches { for decl in rule.declarations.iter() { acc.push(Match { declaration: decl, specificity: selector.specificity(), origin: self.origin, }); } } } } } } fn is_text_css(mime_type: &Mime) -> bool { mime_type.type_ == "text" && mime_type.subtype == "css" } /// Runs the CSS cascade on the specified tree from all the stylesheets pub fn cascade( root: &mut Node, ua_stylesheets: &[Stylesheet], author_stylesheets: &[Stylesheet], user_stylesheets: &[Stylesheet], session: &Session, ) { for mut node in root.descendants().filter(|n| n.is_element()) { let mut matches = Vec::new(); // xml:lang needs to be inherited before selector matching, so it // can't be done in the usual SpecifiedValues::to_computed_values, // which is called by cascade() and runs after matching. let parent = node.parent().clone(); node.borrow_element_mut().inherit_xml_lang(parent); let mut cache = NthIndexCache::default(); let mut match_ctx = MatchingContext::new( MatchingMode::Normal, // FIXME: how the fuck does one set up a bloom filter here? None, &mut cache, QuirksMode::NoQuirks, NeedsSelectorFlags::No, IgnoreNthChildForInvalidation::No, ); for s in ua_stylesheets .iter() .chain(author_stylesheets) .chain(user_stylesheets) { s.get_matches(&node, &mut match_ctx, &mut matches); } matches.as_mut_slice().sort(); let mut element = node.borrow_element_mut(); for m in matches { element.apply_style_declaration(m.declaration, m.origin); } element.set_style_attribute(session); } let values = ComputedValues::default(); root.cascade(&values); } #[cfg(test)] mod tests { use super::*; use selectors::Element; use crate::document::Document; use crate::is_element_of_type; #[test] fn xml_lang() { let document = Document::load_from_bytes( br#" "#, ); let a = document.lookup_internal_node("a").unwrap(); assert_eq!( a.borrow_element() .get_computed_values() .xml_lang() .0 .unwrap() .as_str(), "zh" ); let b = document.lookup_internal_node("b").unwrap(); assert_eq!( b.borrow_element() .get_computed_values() .xml_lang() .0 .unwrap() .as_str(), "en" ); } #[test] fn impl_element() { let document = Document::load_from_bytes( br#" "#, ); let a = document.lookup_internal_node("a").unwrap(); let b = document.lookup_internal_node("b").unwrap(); let c = document.lookup_internal_node("c").unwrap(); let d = document.lookup_internal_node("d").unwrap(); // Node types assert!(is_element_of_type!(a, Svg)); assert!(is_element_of_type!(b, Rect)); assert!(is_element_of_type!(c, Circle)); assert!(is_element_of_type!(d, Rect)); let a = RsvgElement(a); let b = RsvgElement(b); let c = RsvgElement(c); let d = RsvgElement(d); // Tree navigation assert_eq!(a.parent_element(), None); assert_eq!(b.parent_element(), Some(a.clone())); assert_eq!(c.parent_element(), Some(a.clone())); assert_eq!(d.parent_element(), Some(a.clone())); assert_eq!(b.next_sibling_element(), Some(c.clone())); assert_eq!(c.next_sibling_element(), Some(d.clone())); assert_eq!(d.next_sibling_element(), None); assert_eq!(b.prev_sibling_element(), None); assert_eq!(c.prev_sibling_element(), Some(b.clone())); assert_eq!(d.prev_sibling_element(), Some(c.clone())); // Other operations assert!(a.has_local_name(&LocalName::from("svg"))); assert!(a.has_namespace(&ns!(svg))); assert!(!a.is_same_type(&b)); assert!(b.is_same_type(&d)); assert!(a.has_id( &Identifier::from("a"), CaseSensitivity::AsciiCaseInsensitive )); assert!(!b.has_id( &Identifier::from("foo"), CaseSensitivity::AsciiCaseInsensitive )); assert!(d.has_class( &Identifier::from("foo"), CaseSensitivity::AsciiCaseInsensitive )); assert!(d.has_class( &Identifier::from("bar"), CaseSensitivity::AsciiCaseInsensitive )); assert!(!a.has_class( &Identifier::from("foo"), CaseSensitivity::AsciiCaseInsensitive )); assert!(d.is_empty()); assert!(!a.is_empty()); } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/dasharray.rs���������������������������������������������������������������������0000644�0000000�0000000�00000006414�10461020230�0014511�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Parser for the `stroke-dasharray` property. use cssparser::Parser; use crate::error::*; use crate::length::*; use crate::parsers::{optional_comma, Parse}; #[derive(Debug, Default, PartialEq, Clone)] pub enum Dasharray { #[default] None, Array(Box<[ULength]>), } impl Parse for Dasharray { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { if parser .try_parse(|p| p.expect_ident_matching("none")) .is_ok() { return Ok(Dasharray::None); } let mut dasharray = Vec::new(); loop { let d = ULength::::parse(parser)?; dasharray.push(d); if parser.is_exhausted() { break; } optional_comma(parser); } Ok(Dasharray::Array(dasharray.into_boxed_slice())) } } #[cfg(test)] mod tests { use super::*; fn dasharray(l: &[ULength]) -> Dasharray { Dasharray::Array(l.to_vec().into_boxed_slice()) } #[test] fn parses_dash_array() { // helper to cut down boilderplate let length_parse = |s| ULength::::parse_str(s).unwrap(); let expected = dasharray(&[ length_parse("1"), length_parse("2in"), length_parse("3"), length_parse("4%"), ]); let sample_1 = dasharray(&[length_parse("10"), length_parse("6")]); let sample_2 = dasharray(&[length_parse("5"), length_parse("5"), length_parse("20")]); let sample_3 = dasharray(&[ length_parse("10px"), length_parse("20px"), length_parse("20px"), ]); let sample_4 = dasharray(&[ length_parse("25"), length_parse("5"), length_parse("5"), length_parse("5"), ]); let sample_5 = dasharray(&[length_parse("3.1415926"), length_parse("8")]); let sample_6 = dasharray(&[length_parse("5"), length_parse("3.14")]); let sample_7 = dasharray(&[length_parse("2")]); assert_eq!(Dasharray::parse_str("none").unwrap(), Dasharray::None); assert_eq!(Dasharray::parse_str("1 2in,3 4%").unwrap(), expected); assert_eq!(Dasharray::parse_str("10,6").unwrap(), sample_1); assert_eq!(Dasharray::parse_str("5,5,20").unwrap(), sample_2); assert_eq!(Dasharray::parse_str("10px 20px 20px").unwrap(), sample_3); assert_eq!(Dasharray::parse_str("25 5 , 5 5").unwrap(), sample_4); assert_eq!(Dasharray::parse_str("3.1415926,8").unwrap(), sample_5); assert_eq!(Dasharray::parse_str("5, 3.14").unwrap(), sample_6); assert_eq!(Dasharray::parse_str("2").unwrap(), sample_7); // Negative numbers assert!(Dasharray::parse_str("20,40,-20").is_err()); // Empty dash_array assert!(Dasharray::parse_str("").is_err()); assert!(Dasharray::parse_str("\t \n ").is_err()); assert!(Dasharray::parse_str(",,,").is_err()); assert!(Dasharray::parse_str("10, \t, 20 \n").is_err()); // No trailing commas allowed, parse error assert!(Dasharray::parse_str("10,").is_err()); // A comma should be followed by a number assert!(Dasharray::parse_str("20,,10").is_err()); } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/document.rs����������������������������������������������������������������������0000644�0000000�0000000�00000077637�10461020230�0014370�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Main SVG document structure. use data_url::mime::Mime; use glib::prelude::*; use markup5ever::QualName; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fmt; use std::include_str; use std::io::Cursor; use std::rc::Rc; use std::str::FromStr; use std::sync::Arc; use std::{cell::RefCell, sync::OnceLock}; use crate::accept_language::UserLanguage; use crate::bbox::BoundingBox; use crate::borrow_element_as; use crate::css::{self, Origin, Stylesheet}; use crate::dpi::Dpi; use crate::drawing_ctx::{ draw_tree, with_saved_cr, DrawingMode, RenderingConfiguration, SvgNesting, }; use crate::error::{AcquireError, InternalRenderingError, LoadingError, NodeIdError}; use crate::io::{self, BinaryData}; use crate::is_element_of_type; use crate::limits; use crate::node::{CascadedValues, Node, NodeBorrow, NodeData}; use crate::rect::Rect; use crate::session::Session; use crate::structure::IntrinsicDimensions; use crate::surface_utils::shared_surface::SharedImageSurface; use crate::url_resolver::{AllowedUrl, UrlResolver}; use crate::xml::{xml_load_from_possibly_compressed_stream, Attributes}; /// Identifier of a node #[derive(Debug, PartialEq, Clone)] pub enum NodeId { /// element id Internal(String), /// url, element id External(String, String), } impl NodeId { pub fn parse(href: &str) -> Result { let (url, id) = match href.rfind('#') { None => (Some(href), None), Some(0) => (None, Some(&href[1..])), Some(p) => (Some(&href[..p]), Some(&href[(p + 1)..])), }; match (url, id) { (None, Some(id)) if !id.is_empty() => Ok(NodeId::Internal(String::from(id))), (Some(url), Some(id)) if !id.is_empty() => { Ok(NodeId::External(String::from(url), String::from(id))) } _ => Err(NodeIdError::NodeIdRequired), } } } impl fmt::Display for NodeId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { NodeId::Internal(id) => write!(f, "#{id}"), NodeId::External(url, id) => write!(f, "{url}#{id}"), } } } /// Loading options for SVG documents. pub struct LoadOptions { /// Load url resolver; all references will be resolved with respect to this. pub url_resolver: UrlResolver, /// Whether to turn off size limits in libxml2. pub unlimited_size: bool, /// Whether to keep original (undecoded) image data to embed in Cairo PDF surfaces. pub keep_image_data: bool, } impl LoadOptions { /// Creates a `LoadOptions` with defaults, and sets the `url resolver`. pub fn new(url_resolver: UrlResolver) -> Self { LoadOptions { url_resolver, unlimited_size: false, keep_image_data: false, } } /// Sets whether libxml2's limits on memory usage should be turned off. /// /// This should only be done for trusted data. pub fn with_unlimited_size(mut self, unlimited: bool) -> Self { self.unlimited_size = unlimited; self } /// Sets whether to keep the original compressed image data from referenced JPEG/PNG images. /// /// This is only useful for rendering to Cairo PDF /// surfaces, which can embed the original, compressed image data instead of uncompressed /// RGB buffers. pub fn keep_image_data(mut self, keep: bool) -> Self { self.keep_image_data = keep; self } /// Creates a new `LoadOptions` with a different `url resolver`. /// /// This is used when loading a referenced file that may in turn cause other files /// to be loaded, for example `` pub fn copy_with_base_url(&self, base_url: &AllowedUrl) -> Self { let mut url_resolver = self.url_resolver.clone(); url_resolver.base_url = Some((**base_url).clone()); LoadOptions { url_resolver, unlimited_size: self.unlimited_size, keep_image_data: self.keep_image_data, } } } /// Document-level rendering options. /// /// This gets then converted to a [`drawing_ctx::RenderingConfiguration`] when all the /// parameters are known. pub struct RenderingOptions { pub dpi: Dpi, pub cancellable: Option, pub user_language: UserLanguage, pub svg_nesting: SvgNesting, pub testing: bool, } impl RenderingOptions { /// Copies the options to a [`RenderingConfiguration`], and adds the `measuring` flag. fn to_rendering_configuration(&self, measuring: bool) -> RenderingConfiguration { RenderingConfiguration { dpi: self.dpi, cancellable: self.cancellable.clone(), user_language: self.user_language.clone(), svg_nesting: self.svg_nesting, testing: self.testing, measuring, } } } /// A loaded SVG file and its derived data. pub struct Document { /// Tree of nodes; the root is guaranteed to be an `` element. tree: Node, /// Metadata about the SVG handle. session: Session, /// Mapping from `id` attributes to nodes. ids: HashMap, /// Othewr SVG documents and images referenced from this document. /// /// This requires requires interior mutability because we load resources all over the /// place. Eventually we'll be able to do this once, at loading time, and keep this /// immutable. resources: RefCell, /// Used to load referenced resources. load_options: Arc, /// Stylesheets defined in the document. stylesheets: Vec, } impl Document { /// Constructs a `Document` by loading it from a stream. pub fn load_from_stream( session: Session, load_options: Arc, stream: &gio::InputStream, cancellable: Option<&gio::Cancellable>, ) -> Result { xml_load_from_possibly_compressed_stream( session.clone(), DocumentBuilder::new(session, load_options.clone()), load_options, stream, cancellable, ) } /// Utility function to load a document from a static string in tests. #[cfg(test)] pub fn load_from_bytes(input: &'static [u8]) -> Document { let bytes = glib::Bytes::from_static(input); let stream = gio::MemoryInputStream::from_bytes(&bytes); Document::load_from_stream( Session::new_for_test_suite(), Arc::new(LoadOptions::new(UrlResolver::new(None))), &stream.upcast(), None::<&gio::Cancellable>, ) .unwrap() } /// Gets the root node. This is guaranteed to be an `` element. pub fn root(&self) -> Node { self.tree.clone() } /// Looks up a node in this document or one of its resources by its `id` attribute. fn lookup_node(&self, node_id: &NodeId) -> Option { match node_id { NodeId::Internal(id) => self.lookup_internal_node(id), NodeId::External(url, id) => self .resources .borrow_mut() .lookup_node(&self.session, &self.load_options, url, id) .ok(), } } /// Looks up a node in this document by its `id` attribute. pub fn lookup_internal_node(&self, id: &str) -> Option { self.ids.get(id).map(|n| (*n).clone()) } /// Loads a resource by URL, or returns a pre-loaded one. fn lookup_resource(&self, url: &str) -> Result { let aurl = self .load_options .url_resolver .resolve_href(url) .map_err(|_| LoadingError::BadUrl)?; // FIXME: pass a cancellable to this. This function is called // at rendering time, so probably the cancellable should come // from cancellability in CairoRenderer - see #429 self.resources .borrow_mut() .lookup_resource(&self.session, &self.load_options, &aurl, None) } /// Runs the CSS cascade on the document tree /// /// This uses the default UserAgent stylesheet, the document's internal stylesheets, /// plus an extra set of stylesheets supplied by the caller. pub fn cascade(&mut self, extra: &[Stylesheet], session: &Session) { let stylesheets = { static UA_STYLESHEETS: OnceLock> = OnceLock::new(); UA_STYLESHEETS.get_or_init(|| { vec![Stylesheet::from_data( include_str!("ua.css"), &UrlResolver::new(None), Origin::UserAgent, Session::default(), ) .expect("could not parse user agent stylesheet for librsvg, there's a bug!")] }) }; css::cascade( &mut self.tree, stylesheets, &self.stylesheets, extra, session, ); } pub fn get_intrinsic_dimensions(&self) -> IntrinsicDimensions { let root = self.root(); let cascaded = CascadedValues::new_from_node(&root); let values = cascaded.get(); borrow_element_as!(self.root(), Svg).get_intrinsic_dimensions(values) } pub fn render_document( &self, session: &Session, cr: &cairo::Context, viewport: &cairo::Rectangle, options: &RenderingOptions, ) -> Result<(), InternalRenderingError> { let root = self.root(); self.render_layer(session, cr, root, viewport, options) } pub fn render_layer( &self, session: &Session, cr: &cairo::Context, node: Node, viewport: &cairo::Rectangle, options: &RenderingOptions, ) -> Result<(), InternalRenderingError> { cr.status()?; let root = self.root(); let viewport = Rect::from(*viewport); let config = options.to_rendering_configuration(false); with_saved_cr(cr, || { draw_tree( session.clone(), DrawingMode::LimitToStack { node, root }, cr, viewport, config, &mut AcquiredNodes::new(self), ) .map(|_bbox| ()) }) } fn geometry_for_layer( &self, session: &Session, node: Node, viewport: Rect, options: &RenderingOptions, ) -> Result<(Rect, Rect), InternalRenderingError> { let root = self.root(); let target = cairo::ImageSurface::create(cairo::Format::Rgb24, 1, 1)?; let cr = cairo::Context::new(&target)?; let config = options.to_rendering_configuration(true); let bbox = draw_tree( session.clone(), DrawingMode::LimitToStack { node, root }, &cr, viewport, config, &mut AcquiredNodes::new(self), )?; let ink_rect = bbox.ink_rect.unwrap_or_default(); let logical_rect = bbox.rect.unwrap_or_default(); Ok((ink_rect, logical_rect)) } pub fn get_geometry_for_layer( &self, session: &Session, node: Node, viewport: &cairo::Rectangle, options: &RenderingOptions, ) -> Result<(cairo::Rectangle, cairo::Rectangle), InternalRenderingError> { let viewport = Rect::from(*viewport); let (ink_rect, logical_rect) = self.geometry_for_layer(session, node, viewport, options)?; Ok(( cairo::Rectangle::from(ink_rect), cairo::Rectangle::from(logical_rect), )) } fn get_bbox_for_element( &self, session: &Session, node: &Node, options: &RenderingOptions, ) -> Result { let target = cairo::ImageSurface::create(cairo::Format::Rgb24, 1, 1)?; let cr = cairo::Context::new(&target)?; let node = node.clone(); let config = options.to_rendering_configuration(true); draw_tree( session.clone(), DrawingMode::OnlyNode(node), &cr, unit_rectangle(), config, &mut AcquiredNodes::new(self), ) } /// Returns (ink_rect, logical_rect) pub fn get_geometry_for_element( &self, session: &Session, node: Node, options: &RenderingOptions, ) -> Result<(cairo::Rectangle, cairo::Rectangle), InternalRenderingError> { let bbox = self.get_bbox_for_element(session, &node, options)?; let ink_rect = bbox.ink_rect.unwrap_or_default(); let logical_rect = bbox.rect.unwrap_or_default(); // Translate so ink_rect is always at offset (0, 0) let ofs = (-ink_rect.x0, -ink_rect.y0); Ok(( cairo::Rectangle::from(ink_rect.translate(ofs)), cairo::Rectangle::from(logical_rect.translate(ofs)), )) } pub fn render_element( &self, session: &Session, cr: &cairo::Context, node: Node, element_viewport: &cairo::Rectangle, options: &RenderingOptions, ) -> Result<(), InternalRenderingError> { cr.status()?; let bbox = self.get_bbox_for_element(session, &node, options)?; if bbox.ink_rect.is_none() || bbox.rect.is_none() { // Nothing to draw return Ok(()); } let ink_r = bbox.ink_rect.unwrap_or_default(); if ink_r.is_empty() { return Ok(()); } // Render, transforming so element is at the new viewport's origin with_saved_cr(cr, || { let factor = (element_viewport.width() / ink_r.width()) .min(element_viewport.height() / ink_r.height()); cr.translate(element_viewport.x(), element_viewport.y()); cr.scale(factor, factor); cr.translate(-ink_r.x0, -ink_r.y0); let config = options.to_rendering_configuration(false); draw_tree( session.clone(), DrawingMode::OnlyNode(node), cr, unit_rectangle(), config, &mut AcquiredNodes::new(self), ) .map(|_bbox| ()) }) } } fn unit_rectangle() -> Rect { Rect::from_size(1.0, 1.0) } /// Any kind of resource loaded while processing an SVG document: images, or SVGs themselves. #[derive(Clone)] pub enum Resource { Document(Rc), Image(SharedImageSurface), } struct Resources { resources: HashMap>, } impl Resources { fn new() -> Resources { Resources { resources: Default::default(), } } fn lookup_node( &mut self, session: &Session, load_options: &LoadOptions, url: &str, id: &str, ) -> Result { self.get_extern_document(session, load_options, url) .and_then(|resource| match resource { Resource::Document(doc) => doc.lookup_internal_node(id).ok_or(LoadingError::BadUrl), _ => unreachable!("get_extern_document() should already have ensured the document"), }) } fn get_extern_document( &mut self, session: &Session, load_options: &LoadOptions, href: &str, ) -> Result { let aurl = load_options .url_resolver .resolve_href(href) .map_err(|_| LoadingError::BadUrl)?; // FIXME: pass a cancellable to this. This function is called // at rendering time, so probably the cancellable should come // from cancellability in CairoRenderer - see #429 let resource = self.lookup_resource(session, load_options, &aurl, None)?; match resource { Resource::Document(_) => Ok(resource), _ => Err(LoadingError::Other(format!( "{href} is not an SVG document" ))), } } fn lookup_resource( &mut self, session: &Session, load_options: &LoadOptions, aurl: &AllowedUrl, cancellable: Option<&gio::Cancellable>, ) -> Result { match self.resources.entry(aurl.clone()) { Entry::Occupied(e) => e.get().clone(), Entry::Vacant(e) => { let resource_result = load_resource(session, load_options, aurl, cancellable); e.insert(resource_result.clone()); resource_result } } } } fn load_resource( session: &Session, load_options: &LoadOptions, aurl: &AllowedUrl, cancellable: Option<&gio::Cancellable>, ) -> Result { let data = io::acquire_data(aurl, cancellable)?; let svg_mime_type = Mime::from_str("image/svg+xml").unwrap(); if data.mime_type == svg_mime_type { load_svg_resource_from_bytes(session, load_options, aurl, data, cancellable) } else { load_image_resource_from_bytes(load_options, aurl, data) } } fn load_svg_resource_from_bytes( session: &Session, load_options: &LoadOptions, aurl: &AllowedUrl, data: BinaryData, cancellable: Option<&gio::Cancellable>, ) -> Result { let BinaryData { data: input_bytes, mime_type: _mime_type, } = data; let bytes = glib::Bytes::from_owned(input_bytes); let stream = gio::MemoryInputStream::from_bytes(&bytes); let document = Document::load_from_stream( session.clone(), Arc::new(load_options.copy_with_base_url(aurl)), &stream.upcast(), cancellable, )?; Ok(Resource::Document(Rc::new(document))) } fn load_image_resource_from_bytes( load_options: &LoadOptions, aurl: &AllowedUrl, data: BinaryData, ) -> Result { let BinaryData { data: bytes, mime_type, } = data; if bytes.is_empty() { return Err(LoadingError::Other(String::from("no image data"))); } let content_type = content_type_for_image(&mime_type); load_image_with_image_rs(aurl, bytes, content_type, load_options) } fn image_format(content_type: &str) -> Result { match content_type { "image/png" => Ok(image::ImageFormat::Png), "image/jpeg" => Ok(image::ImageFormat::Jpeg), "image/gif" => Ok(image::ImageFormat::Gif), "image/webp" => Ok(image::ImageFormat::WebP), #[cfg(feature = "avif")] "image/avif" => Ok(image::ImageFormat::Avif), _ => Err(LoadingError::Other(format!( "unsupported image format {content_type}" ))), } } fn load_image_with_image_rs( aurl: &AllowedUrl, bytes: Vec, content_type: Option, load_options: &LoadOptions, ) -> Result { let cursor = Cursor::new(&bytes); let reader = if let Some(ref content_type) = content_type { let format = image_format(content_type)?; image::ImageReader::with_format(cursor, format) } else { image::ImageReader::new(cursor) .with_guessed_format() .map_err(|_| LoadingError::Other(String::from("unknown image format")))? }; let image = reader .decode() .map_err(|e| LoadingError::Other(format!("error decoding image: {e}")))?; let bytes = if load_options.keep_image_data { Some(bytes) } else { None }; let surface = SharedImageSurface::from_image(&image, content_type.as_deref(), bytes) .map_err(|e| image_loading_error_from_cairo(e, aurl))?; Ok(Resource::Image(surface)) } fn content_type_for_image(mime_type: &Mime) -> Option { // See issue #548 - data: URLs without a MIME-type automatically // fall back to "text/plain;charset=US-ASCII". Some (old?) versions of // Adobe Illustrator generate data: URLs without MIME-type for image // data. We'll catch this and fall back to sniffing by unsetting the // content_type. let unspecified_mime_type = Mime::from_str("text/plain;charset=US-ASCII").unwrap(); if *mime_type == unspecified_mime_type { None } else { Some(format!("{}/{}", mime_type.type_, mime_type.subtype)) } } fn human_readable_url(aurl: &AllowedUrl) -> &str { if aurl.scheme() == "data" { // avoid printing a huge data: URL for image data "data URL" } else { aurl.as_ref() } } fn image_loading_error_from_cairo(status: cairo::Error, aurl: &AllowedUrl) -> LoadingError { let url = human_readable_url(aurl); match status { cairo::Error::NoMemory => LoadingError::OutOfMemory(format!("loading image: {url}")), cairo::Error::InvalidSize => LoadingError::Other(format!("image too big: {url}")), _ => LoadingError::Other(format!("cairo error: {status}")), } } pub struct AcquiredNode { stack: Option>>, node: Node, } impl Drop for AcquiredNode { fn drop(&mut self) { if let Some(ref stack) = self.stack { let mut stack = stack.borrow_mut(); let last = stack.pop().unwrap(); assert!(last == self.node); } } } impl AcquiredNode { pub fn get(&self) -> &Node { &self.node } } /// Detects circular references between nodes, and enforces referencing limits. /// /// Consider this fragment of SVG: /// /// ```xml /// /// /// /// ``` /// /// The pattern has a child element that references the pattern itself. This kind of circular /// reference is invalid. The `AcquiredNodes` struct is passed around /// wherever it may be necessary to resolve references to nodes, or to access nodes /// "elsewhere" in the DOM that is not the current subtree. /// /// Also, such constructs that reference other elements can be maliciously arranged like /// in the [billion laughs attack][lol], to cause huge amounts of CPU to be consumed through /// creating an exponential number of references. `AcquiredNodes` imposes a hard limit on /// the number of references that can be resolved for typical, well-behaved SVG documents. /// /// The [`Self::acquire()`] and [`Self::acquire_ref()`] methods return an [`AcquiredNode`], which /// acts like a smart pointer for a [`Node`]. Once a node has been acquired, it cannot be /// acquired again until its [`AcquiredNode`] is dropped. In the example above, a graphic element /// would acquire the `pattern`, which would then acquire its `rect` child, which then would fail /// to re-acquired the `pattern` — thus signaling a circular reference. /// /// Those methods return an [`AcquireError`] to signal circular references. Also, they /// can return [`AcquireError::MaxReferencesExceeded`] if the aforementioned referencing limit /// is exceeded. /// /// [lol]: https://bitbucket.org/tiran/defusedxml pub struct AcquiredNodes<'i> { document: &'i Document, num_elements_acquired: usize, node_stack: Rc>, nodes_with_cycles: Vec, } impl<'i> AcquiredNodes<'i> { pub fn new(document: &Document) -> AcquiredNodes<'_> { AcquiredNodes { document, num_elements_acquired: 0, node_stack: Rc::new(RefCell::new(NodeStack::new())), nodes_with_cycles: Vec::new(), } } pub fn lookup_resource(&self, url: &str) -> Result { self.document.lookup_resource(url) } /// Acquires a node by its id. /// /// This is typically used during an "early resolution" stage, when XML `id`s are being /// resolved to node references. pub fn acquire(&mut self, node_id: &NodeId) -> Result { self.num_elements_acquired += 1; // This is a mitigation for SVG files that try to instance a huge number of // elements via , recursive patterns, etc. See limits.rs for details. if self.num_elements_acquired > limits::MAX_REFERENCED_ELEMENTS { return Err(AcquireError::MaxReferencesExceeded); } // FIXME: callers shouldn't have to know that get_node() can initiate a file load. // Maybe we should have the following stages: // - load main SVG XML // // - load secondary resources: SVG XML and other files like images // // - Now that all files are loaded, resolve URL references let node = self .document .lookup_node(node_id) .ok_or_else(|| AcquireError::LinkNotFound(node_id.clone()))?; if self.nodes_with_cycles.contains(&node) { return Err(AcquireError::CircularReference(node.clone())); } if node.borrow_element().is_accessed_by_reference() { self.acquire_ref(&node) } else { Ok(AcquiredNode { stack: None, node }) } } /// Acquires a node whose reference is already known. /// /// This is useful for cases where a node is initially referenced by its id with /// [`Self::acquire`] and kept around for later use. During the later use, the node /// needs to be re-acquired with this method. For example: /// /// * At an "early resolution" stage, `acquire()` a pattern by its id, and keep around its /// [`Node`] reference. /// /// * At the drawing stage, `acquire_ref()` the pattern node that we already had, so that /// its child elements that reference other paint servers will be able to detect circular /// references to the pattern. pub fn acquire_ref(&mut self, node: &Node) -> Result { if self.nodes_with_cycles.contains(node) { Err(AcquireError::CircularReference(node.clone())) } else if self.node_stack.borrow().contains(node) { self.nodes_with_cycles.push(node.clone()); Err(AcquireError::CircularReference(node.clone())) } else { self.node_stack.borrow_mut().push(node); Ok(AcquiredNode { stack: Some(self.node_stack.clone()), node: node.clone(), }) } } } /// Keeps a stack of nodes and can check if a certain node is contained in the stack /// /// Sometimes parts of the code cannot plainly use the implicit stack of acquired /// nodes as maintained by DrawingCtx::acquire_node(), and they must keep their /// own stack of nodes to test for reference cycles. NodeStack can be used to do that. pub struct NodeStack(Vec); impl NodeStack { pub fn new() -> NodeStack { NodeStack(Vec::new()) } pub fn push(&mut self, node: &Node) { self.0.push(node.clone()); } pub fn pop(&mut self) -> Option { self.0.pop() } pub fn contains(&self, node: &Node) -> bool { self.0.iter().any(|n| *n == *node) } } /// Used to build a tree of SVG nodes while an XML document is being read. /// /// This struct holds the document-related state while loading an SVG document from XML: /// the loading options, the partially-built tree of nodes, the CSS stylesheets that /// appear while loading the document. /// /// The XML loader asks a `DocumentBuilder` to /// [`append_element`][DocumentBuilder::append_element], /// [`append_characters`][DocumentBuilder::append_characters], etc. When all the XML has /// been consumed, the caller can use [`build`][DocumentBuilder::build] to get a /// fully-loaded [`Document`]. pub struct DocumentBuilder { /// Metadata for the document's lifetime. session: Session, /// Loading options; mainly the URL resolver. load_options: Arc, /// Root node of the tree. tree: Option, /// Mapping from `id` attributes to nodes. ids: HashMap, /// Stylesheets defined in the document. stylesheets: Vec, } impl DocumentBuilder { pub fn new(session: Session, load_options: Arc) -> DocumentBuilder { DocumentBuilder { session, load_options, tree: None, ids: HashMap::new(), stylesheets: Vec::new(), } } /// Adds a stylesheet in order to the document. /// /// Stylesheets will later be matched in the order in which they were added. pub fn append_stylesheet(&mut self, stylesheet: Stylesheet) { self.stylesheets.push(stylesheet); } /// Creates an element of the specified `name` as a child of `parent`. /// /// This is the main function to create new SVG elements while parsing XML. /// /// `name` is the XML element's name, for example `rect`. /// /// `attrs` has the XML element's attributes, e.g. cx/cy/r for ``. /// /// If `parent` is `None` it means that we are creating the root node in the tree of /// elements. The code will later validate that this is indeed an `` element. pub fn append_element( &mut self, name: &QualName, attrs: Attributes, parent: Option, ) -> Node { let node = Node::new(NodeData::new_element(&self.session, name, attrs)); if let Some(id) = node.borrow_element().get_id() { // This is so we don't overwrite an existing id self.ids .entry(id.to_string()) .or_insert_with(|| node.clone()); } if let Some(parent) = parent { parent.append(node.clone()); } else if self.tree.is_none() { self.tree = Some(node.clone()); } else { panic!("The tree root has already been set"); } node } /// Creates a node for an XML text element as a child of `parent`. pub fn append_characters(&mut self, text: &str, parent: &mut Node) { if !text.is_empty() { // When the last child is a Chars node we can coalesce // the text and avoid screwing up the Pango layouts if let Some(child) = parent.last_child().filter(|c| c.is_chars()) { child.borrow_chars().append(text); } else { parent.append(Node::new(NodeData::new_chars(text))); }; } } /// Does the final validation on the `Document` being read, and returns it. pub fn build(self) -> Result { let DocumentBuilder { load_options, session, tree, ids, stylesheets, .. } = self; match tree { Some(root) if root.is_element() => { if is_element_of_type!(root, Svg) { let mut document = Document { tree: root, session: session.clone(), ids, resources: RefCell::new(Resources::new()), load_options, stylesheets, }; document.cascade(&[], &session); Ok(document) } else { Err(LoadingError::NoSvgRoot) } } _ => Err(LoadingError::NoSvgRoot), } } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_node_id() { assert_eq!( NodeId::parse("#foo").unwrap(), NodeId::Internal("foo".to_string()) ); assert_eq!( NodeId::parse("uri#foo").unwrap(), NodeId::External("uri".to_string(), "foo".to_string()) ); assert!(matches!( NodeId::parse("uri"), Err(NodeIdError::NodeIdRequired) )); } #[test] fn unspecified_mime_type_yields_no_content_type() { // Issue #548 let mime = Mime::from_str("text/plain;charset=US-ASCII").unwrap(); assert!(content_type_for_image(&mime).is_none()); } #[test] fn strips_mime_type_parameters() { // Issue #699 let mime = Mime::from_str("image/png;charset=utf-8").unwrap(); assert_eq!( content_type_for_image(&mime), Some(String::from("image/png")) ); } } �������������������������������������������������������������������������������������������������librsvg-2.59.0/src/dpi.rs���������������������������������������������������������������������������0000644�0000000�0000000�00000000327�10461020230�0013304�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Resolution for rendering (dots per inch = DPI). #[derive(Debug, Copy, Clone)] pub struct Dpi { pub x: f64, pub y: f64, } impl Dpi { pub fn new(x: f64, y: f64) -> Dpi { Dpi { x, y } } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/drawing_ctx.rs�������������������������������������������������������������������0000644�0000000�0000000�00000253461�10461020230�0015052�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! The main context structure which drives the drawing process. use float_cmp::approx_eq; use gio::prelude::*; use glib::translate::*; use pango::ffi::PangoMatrix; use pango::prelude::FontMapExt; use regex::{Captures, Regex}; use std::cell::RefCell; use std::convert::TryFrom; use std::f64::consts::*; use std::rc::Rc; use std::{borrow::Cow, sync::OnceLock}; use crate::accept_language::UserLanguage; use crate::bbox::BoundingBox; use crate::color::color_to_rgba; use crate::coord_units::CoordUnits; use crate::document::{AcquiredNodes, NodeId, RenderingOptions}; use crate::dpi::Dpi; use crate::element::{Element, ElementData}; use crate::error::{AcquireError, ImplementationLimit, InternalRenderingError}; use crate::filters::{self, FilterSpec}; use crate::float_eq_cairo::ApproxEqCairo; use crate::gradient::{GradientVariant, SpreadMethod, UserSpaceGradient}; use crate::layout::{ Filter, Group, Image, Layer, LayerKind, LayoutViewport, Shape, StackingContext, Stroke, Text, TextSpan, }; use crate::length::*; use crate::marker; use crate::node::{CascadedValues, Node, NodeBorrow, NodeDraw}; use crate::paint_server::{PaintSource, UserSpacePaintSource}; use crate::path_builder::*; use crate::pattern::UserSpacePattern; use crate::properties::{ ClipRule, ComputedValues, FillRule, ImageRendering, MaskType, MixBlendMode, Opacity, PaintTarget, ShapeRendering, StrokeLinecap, StrokeLinejoin, TextRendering, }; use crate::rect::{rect_to_transform, IRect, Rect}; use crate::rsvg_log; use crate::session::Session; use crate::surface_utils::shared_surface::{ ExclusiveImageSurface, Interpolation, SharedImageSurface, SurfaceType, }; use crate::transform::{Transform, ValidTransform}; use crate::unit_interval::UnitInterval; use crate::viewbox::ViewBox; use crate::{borrow_element_as, is_element_of_type}; /// Opaque font options for a DrawingCtx. /// /// This is used for DrawingCtx::create_pango_context. pub struct FontOptions { options: cairo::FontOptions, } /// Set path on the cairo context, or clear it. /// This helper object keeps track whether the path has been set already, /// so that it isn't recalculated every so often. struct PathHelper<'a> { cr: &'a cairo::Context, transform: ValidTransform, path: &'a Path, is_square_linecap: bool, has_path: Option, } impl<'a> PathHelper<'a> { pub fn new( cr: &'a cairo::Context, transform: ValidTransform, path: &'a Path, linecap: StrokeLinecap, ) -> Self { PathHelper { cr, transform, path, is_square_linecap: linecap == StrokeLinecap::Square, has_path: None, } } pub fn set(&mut self) -> Result<(), InternalRenderingError> { match self.has_path { Some(false) | None => { self.has_path = Some(true); self.cr.set_matrix(self.transform.into()); self.path.to_cairo(self.cr, self.is_square_linecap) } Some(true) => Ok(()), } } pub fn unset(&mut self) { match self.has_path { Some(true) | None => { self.has_path = Some(false); self.cr.new_path(); } Some(false) => {} } } } /// Holds the size of the current viewport in the user's coordinate system. #[derive(Clone)] pub struct Viewport { pub dpi: Dpi, /// Corners of the current coordinate space. pub vbox: ViewBox, /// The viewport's coordinate system, or "user coordinate system" in SVG terms. transform: Transform, } impl Viewport { /// FIXME: this is just used in Handle::with_height_to_user(), and in length.rs's test suite. /// Find a way to do this without involving a default identity transform. pub fn new(dpi: Dpi, view_box_width: f64, view_box_height: f64) -> Viewport { Viewport { dpi, vbox: ViewBox::from(Rect::from_size(view_box_width, view_box_height)), transform: Default::default(), } } /// Creates a new viewport suitable for a certain kind of units. /// /// For `objectBoundingBox`, CSS lengths which are in percentages /// refer to the size of the current viewport. Librsvg implements /// that by keeping the same current transformation matrix, and /// setting a viewport size of (1.0, 1.0). /// /// For `userSpaceOnUse`, we just duplicate the current viewport, /// since that kind of units means to use the current coordinate /// system unchanged. pub fn with_units(&self, units: CoordUnits) -> Viewport { match units { CoordUnits::ObjectBoundingBox => Viewport { dpi: self.dpi, vbox: ViewBox::from(Rect::from_size(1.0, 1.0)), transform: self.transform, }, CoordUnits::UserSpaceOnUse => Viewport { dpi: self.dpi, vbox: self.vbox, transform: self.transform, }, } } /// Returns a viewport with a new size for normalizing `Length` values. pub fn with_view_box(&self, width: f64, height: f64) -> Viewport { Viewport { dpi: self.dpi, vbox: ViewBox::from(Rect::from_size(width, height)), transform: self.transform, } } } /// Values that stay constant during rendering with a DrawingCtx. #[derive(Clone)] pub struct RenderingConfiguration { pub dpi: Dpi, pub cancellable: Option, pub user_language: UserLanguage, pub svg_nesting: SvgNesting, pub measuring: bool, pub testing: bool, } pub struct DrawingCtx { session: Session, initial_viewport: Viewport, cr_stack: Rc>>, cr: cairo::Context, drawsub_stack: Vec, config: RenderingConfiguration, } pub enum DrawingMode { LimitToStack { node: Node, root: Node }, OnlyNode(Node), } /// Whether an SVG document is being rendered standalone or referenced from an `` element. /// /// Normally, the coordinate system used when rendering a toplevel SVG is determined from the /// initial viewport and the `` element's `viewBox` and `preserveAspectRatio` attributes. /// However, when an SVG document is referenced from an `` element, as in ``, /// its `preserveAspectRatio` needs to be ignored so that the one from the `` element can /// be used instead. This lets the parent document (the one with the `` element) specify /// how it wants the child SVG to be scaled into the viewport. #[derive(Copy, Clone)] pub enum SvgNesting { Standalone, ReferencedFromImageElement, } /// The toplevel drawing routine. /// /// This creates a DrawingCtx internally and starts drawing at the specified `node`. pub fn draw_tree( session: Session, mode: DrawingMode, cr: &cairo::Context, viewport_rect: Rect, config: RenderingConfiguration, acquired_nodes: &mut AcquiredNodes<'_>, ) -> Result { let (drawsub_stack, node) = match mode { DrawingMode::LimitToStack { node, root } => (node.ancestors().collect(), root), DrawingMode::OnlyNode(node) => (Vec::new(), node), }; let cascaded = CascadedValues::new_from_node(&node); // Preserve the user's transform and use it for the outermost bounding box. All bounds/extents // will be converted to this transform in the end. let user_transform = Transform::from(cr.matrix()); let mut user_bbox = BoundingBox::new().with_transform(user_transform); // https://www.w3.org/TR/SVG2/coords.html#InitialCoordinateSystem // // "For the outermost svg element, the SVG user agent must // determine an initial viewport coordinate system and an // initial user coordinate system such that the two // coordinates systems are identical. The origin of both // coordinate systems must be at the origin of the SVG // viewport." // // "... the initial viewport coordinate system (and therefore // the initial user coordinate system) must have its origin at // the top/left of the viewport" // Translate so (0, 0) is at the viewport's upper-left corner. let transform = user_transform.pre_translate(viewport_rect.x0, viewport_rect.y0); // Here we exit immediately if the transform is not valid, since we are in the // toplevel drawing function. Downstream cases would simply not render the current // element and ignore the error. let valid_transform = ValidTransform::try_from(transform)?; cr.set_matrix(valid_transform.into()); // Per the spec, so the viewport has (0, 0) as upper-left. let viewport_rect = viewport_rect.translate((-viewport_rect.x0, -viewport_rect.y0)); let initial_viewport = Viewport { dpi: config.dpi, vbox: ViewBox::from(viewport_rect), transform, }; let mut draw_ctx = DrawingCtx::new(session, cr, &initial_viewport, config, drawsub_stack); let content_bbox = draw_ctx.draw_node_from_stack( &node, acquired_nodes, &cascaded, &initial_viewport, false, )?; user_bbox.insert(&content_bbox); if draw_ctx.is_rendering_cancelled() { Err(InternalRenderingError::Cancelled) } else { Ok(user_bbox) } } pub fn with_saved_cr(cr: &cairo::Context, f: F) -> Result where F: FnOnce() -> Result, { cr.save()?; match f() { Ok(o) => { cr.restore()?; Ok(o) } Err(e) => Err(e), } } impl Drop for DrawingCtx { fn drop(&mut self) { self.cr_stack.borrow_mut().pop(); } } const CAIRO_TAG_LINK: &str = "Link"; impl DrawingCtx { fn new( session: Session, cr: &cairo::Context, initial_viewport: &Viewport, config: RenderingConfiguration, drawsub_stack: Vec, ) -> DrawingCtx { DrawingCtx { session, initial_viewport: initial_viewport.clone(), cr_stack: Rc::new(RefCell::new(Vec::new())), cr: cr.clone(), drawsub_stack, config, } } /// Copies a `DrawingCtx` for temporary use on a Cairo surface. /// /// `DrawingCtx` maintains state using during the drawing process, and sometimes we /// would like to use that same state but on a different Cairo surface and context /// than the ones being used on `self`. This function copies the `self` state into a /// new `DrawingCtx`, and ties the copied one to the supplied `cr`. fn nested(&self, cr: cairo::Context) -> DrawingCtx { let cr_stack = self.cr_stack.clone(); cr_stack.borrow_mut().push(self.cr.clone()); DrawingCtx { session: self.session.clone(), initial_viewport: self.initial_viewport.clone(), cr_stack, cr, drawsub_stack: self.drawsub_stack.clone(), config: self.config.clone(), } } pub fn session(&self) -> &Session { &self.session } /// Returns the `RenderingOptions` being used for rendering. pub fn rendering_options(&self, svg_nesting: SvgNesting) -> RenderingOptions { RenderingOptions { dpi: self.config.dpi, cancellable: self.config.cancellable.clone(), user_language: self.config.user_language.clone(), svg_nesting, testing: self.config.testing, } } pub fn user_language(&self) -> &UserLanguage { &self.config.user_language } pub fn toplevel_viewport(&self) -> Rect { *self.initial_viewport.vbox } /// Gets the transform that will be used on the target surface, /// whether using an isolated stacking context or not. /// /// This is only used in the text code, and we should probably try /// to remove it. pub fn get_transform_for_stacking_ctx( &self, stacking_ctx: &StackingContext, clipping: bool, ) -> Result { if stacking_ctx.should_isolate() && !clipping { let affines = CompositingAffines::new( *self.get_transform(), self.initial_viewport.transform, self.cr_stack.borrow().len(), ); Ok(ValidTransform::try_from(affines.for_temporary_surface)?) } else { Ok(self.get_transform()) } } pub fn svg_nesting(&self) -> SvgNesting { self.config.svg_nesting } pub fn is_measuring(&self) -> bool { self.config.measuring } pub fn is_testing(&self) -> bool { self.config.testing } pub fn get_transform(&self) -> ValidTransform { let t = Transform::from(self.cr.matrix()); ValidTransform::try_from(t) .expect("Cairo should already have checked that its current transform is valid") } pub fn empty_bbox(&self) -> BoundingBox { BoundingBox::new().with_transform(*self.get_transform()) } fn size_for_temporary_surface(&self) -> (i32, i32) { let rect = self.toplevel_viewport(); let (viewport_width, viewport_height) = (rect.width(), rect.height()); let (width, height) = self .initial_viewport .transform .transform_distance(viewport_width, viewport_height); // We need a size in whole pixels, so use ceil() to ensure the whole viewport fits // into the temporary surface. (width.ceil().abs() as i32, height.ceil().abs() as i32) } pub fn create_surface_for_toplevel_viewport( &self, ) -> Result { let (w, h) = self.size_for_temporary_surface(); Ok(cairo::ImageSurface::create(cairo::Format::ARgb32, w, h)?) } fn create_similar_surface_for_toplevel_viewport( &self, surface: &cairo::Surface, ) -> Result { let (w, h) = self.size_for_temporary_surface(); Ok(cairo::Surface::create_similar( surface, cairo::Content::ColorAlpha, w, h, )?) } /// Creates a new coordinate space inside a viewport and sets a clipping rectangle. /// /// Note that this actually changes the `draw_ctx.cr`'s transformation to match /// the new coordinate space, but the old one is not restored after the /// result's `Viewport` is dropped. Thus, this function must be called /// inside `with_saved_cr` or `draw_ctx.with_discrete_layer`. pub fn push_new_viewport( &self, current_viewport: &Viewport, layout_viewport: &LayoutViewport, ) -> Option { let LayoutViewport { geometry, vbox, preserve_aspect_ratio, overflow, } = *layout_viewport; if !overflow.overflow_allowed() || (vbox.is_some() && preserve_aspect_ratio.is_slice()) { clip_to_rectangle(&self.cr, &geometry); } preserve_aspect_ratio .viewport_to_viewbox_transform(vbox, &geometry) .unwrap_or_else(|_e| { match vbox { None => unreachable!( "viewport_to_viewbox_transform only returns errors when vbox != None" ), Some(v) => { rsvg_log!( self.session, "ignoring viewBox ({}, {}, {}, {}) since it is not usable", v.x0, v.y0, v.width(), v.height() ); } } None }) .map(|t| { self.cr.transform(t.into()); Viewport { dpi: self.config.dpi, vbox: vbox.unwrap_or(current_viewport.vbox), transform: current_viewport.transform.post_transform(&t), } }) } fn clip_to_node( &mut self, clip_node: &Option, acquired_nodes: &mut AcquiredNodes<'_>, viewport: &Viewport, bbox: &BoundingBox, ) -> Result<(), InternalRenderingError> { if clip_node.is_none() { return Ok(()); } let node = clip_node.as_ref().unwrap(); let units = borrow_element_as!(node, ClipPath).get_units(); if let Ok(transform) = rect_to_transform(&bbox.rect, units) { let cascaded = CascadedValues::new_from_node(node); let values = cascaded.get(); let node_transform = values.transform().post_transform(&transform); let transform_for_clip = ValidTransform::try_from(node_transform)?; let orig_transform = self.get_transform(); self.cr.transform(transform_for_clip.into()); for child in node.children().filter(|c| { c.is_element() && element_can_be_used_inside_clip_path(&c.borrow_element()) }) { child.draw( acquired_nodes, &CascadedValues::clone_with_node(&cascaded, &child), viewport, self, true, )?; } self.cr.clip(); self.cr.set_matrix(orig_transform.into()); } Ok(()) } fn generate_cairo_mask( &mut self, mask_node: &Node, viewport: &Viewport, transform: Transform, bbox: &BoundingBox, acquired_nodes: &mut AcquiredNodes<'_>, ) -> Result, InternalRenderingError> { if bbox.rect.is_none() { // The node being masked is empty / doesn't have a // bounding box, so there's nothing to mask! return Ok(None); } let _mask_acquired = match acquired_nodes.acquire_ref(mask_node) { Ok(n) => n, Err(AcquireError::CircularReference(_)) => { rsvg_log!(self.session, "circular reference in element {}", mask_node); return Ok(None); } _ => unreachable!(), }; let mask_element = mask_node.borrow_element(); let mask = borrow_element_as!(mask_node, Mask); let bbox_rect = bbox.rect.as_ref().unwrap(); let cascaded = CascadedValues::new_from_node(mask_node); let values = cascaded.get(); let mask_units = mask.get_units(); let mask_rect = { let params = NormalizeParams::new(values, &viewport.with_units(mask_units)); mask.get_rect(¶ms) }; let mask_transform = values.transform().post_transform(&transform); let transform_for_mask = ValidTransform::try_from(mask_transform)?; let mask_content_surface = self.create_surface_for_toplevel_viewport()?; // Use a scope because mask_cr needs to release the // reference to the surface before we access the pixels { let mask_cr = cairo::Context::new(&mask_content_surface)?; mask_cr.set_matrix(transform_for_mask.into()); let bbtransform = Transform::new_unchecked( bbox_rect.width(), 0.0, 0.0, bbox_rect.height(), bbox_rect.x0, bbox_rect.y0, ); let clip_rect = if mask_units == CoordUnits::ObjectBoundingBox { bbtransform.transform_rect(&mask_rect) } else { mask_rect }; clip_to_rectangle(&mask_cr, &clip_rect); if mask.get_content_units() == CoordUnits::ObjectBoundingBox { if bbox_rect.is_empty() { return Ok(None); } mask_cr.transform(ValidTransform::try_from(bbtransform)?.into()); } let mask_viewport = viewport.with_units(mask.get_content_units()); let mut mask_draw_ctx = self.nested(mask_cr); let stacking_ctx = StackingContext::new( self.session(), acquired_nodes, &mask_element, Transform::identity(), None, values, ); rsvg_log!(self.session, "(mask {}", mask_element); let res = mask_draw_ctx.with_discrete_layer( &stacking_ctx, acquired_nodes, &mask_viewport, None, false, &mut |an, dc, new_viewport| { mask_node.draw_children(an, &cascaded, new_viewport, dc, false) }, ); rsvg_log!(self.session, ")"); res?; } 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()?; Ok(Some(mask)) } fn is_rendering_cancelled(&self) -> bool { match &self.config.cancellable { None => false, Some(cancellable) => cancellable.is_cancelled(), } } pub fn with_discrete_layer( &mut self, stacking_ctx: &StackingContext, acquired_nodes: &mut AcquiredNodes<'_>, viewport: &Viewport, layout_viewport: Option, clipping: bool, draw_fn: &mut dyn FnMut( &mut AcquiredNodes<'_>, &mut DrawingCtx, &Viewport, ) -> Result, ) -> Result { if self.is_rendering_cancelled() { return Err(InternalRenderingError::Cancelled); } let stacking_ctx_transform = ValidTransform::try_from(stacking_ctx.transform)?; let orig_transform = self.get_transform(); self.cr.transform(stacking_ctx_transform.into()); let res = if clipping { if let Some(layout_viewport) = layout_viewport.as_ref() { // FIXME: here we ignore the Some() result of push_new_viewport(). We do that because // the returned one is just a copy of the one that got passeed in, but with a changed // transform. However, we are in fact not using that transform anywhere! // // In case push_new_viewport() returns None, we just don't draw anything. // // Note that push_new_viewport() changes the cr's transform. However it will be restored // at the end of this function with set_matrix. if let Some(new_viewport) = self.push_new_viewport(viewport, layout_viewport) { draw_fn(acquired_nodes, self, &new_viewport) } else { Ok(self.empty_bbox()) } } else { draw_fn(acquired_nodes, self, viewport) } } else { with_saved_cr(&self.cr.clone(), || { if let Some(ref link_target) = stacking_ctx.link_target { self.link_tag_begin(link_target); } let Opacity(UnitInterval(opacity)) = stacking_ctx.opacity; let affine_at_start = self.get_transform(); if let Some(rect) = stacking_ctx.clip_rect.as_ref() { clip_to_rectangle(&self.cr, rect); } // Here we are clipping in user space, so the bbox doesn't matter self.clip_to_node( &stacking_ctx.clip_in_user_space, acquired_nodes, viewport, &self.empty_bbox(), )?; let should_isolate = stacking_ctx.should_isolate(); let res = if should_isolate { // Compute our assortment of affines let affines = CompositingAffines::new( *affine_at_start, self.initial_viewport.transform, self.cr_stack.borrow().len(), ); // Create temporary surface and its cr let cr = match stacking_ctx.filter { None => cairo::Context::new( &self .create_similar_surface_for_toplevel_viewport(&self.cr.target())?, )?, Some(_) => { cairo::Context::new(self.create_surface_for_toplevel_viewport()?)? } }; cr.set_matrix(ValidTransform::try_from(affines.for_temporary_surface)?.into()); let (source_surface, mut res, bbox) = { let mut temporary_draw_ctx = self.nested(cr.clone()); // Draw! let res = with_saved_cr(&cr, || { if let Some(layout_viewport) = layout_viewport.as_ref() { // FIXME: here we ignore the Some() result of push_new_viewport(). We do that because // the returned one is just a copy of the one that got passeed in, but with a changed // transform. However, we are in fact not using that transform anywhere! // // In case push_new_viewport() returns None, we just don't draw anything. if let Some(new_viewport) = temporary_draw_ctx.push_new_viewport(viewport, layout_viewport) { draw_fn(acquired_nodes, &mut temporary_draw_ctx, &new_viewport) } else { Ok(self.empty_bbox()) } } else { draw_fn(acquired_nodes, &mut temporary_draw_ctx, viewport) } }); let bbox = if let Ok(ref bbox) = res { *bbox } else { BoundingBox::new().with_transform(affines.for_temporary_surface) }; if let Some(ref filter) = stacking_ctx.filter { let surface_to_filter = SharedImageSurface::copy_from_surface( &cairo::ImageSurface::try_from(temporary_draw_ctx.cr.target()) .unwrap(), )?; let stroke_paint_source = Rc::new(filter.stroke_paint_source.to_user_space( &bbox.rect, viewport, &filter.normalize_values, )); let fill_paint_source = Rc::new(filter.fill_paint_source.to_user_space( &bbox.rect, viewport, &filter.normalize_values, )); // Filter functions (like "blend()", not the element) require // being resolved in userSpaceonUse units, since that is the default // for primitive_units. So, get the corresponding NormalizeParams // here and pass them down. let user_space_params = NormalizeParams::from_values( &filter.normalize_values, &viewport.with_units(CoordUnits::UserSpaceOnUse), ); let filtered_surface = temporary_draw_ctx .run_filters( viewport, surface_to_filter, filter, acquired_nodes, &stacking_ctx.element_name, &user_space_params, stroke_paint_source, fill_paint_source, bbox, )? .into_image_surface()?; let generic_surface: &cairo::Surface = &filtered_surface; // deref to Surface (generic_surface.clone(), res, bbox) } else { (temporary_draw_ctx.cr.target(), res, bbox) } }; // Set temporary surface as source self.cr .set_matrix(ValidTransform::try_from(affines.compositing)?.into()); self.cr.set_source_surface(&source_surface, 0.0, 0.0)?; // Clip self.cr.set_matrix( ValidTransform::try_from(affines.outside_temporary_surface)?.into(), ); self.clip_to_node( &stacking_ctx.clip_in_object_space, acquired_nodes, viewport, &bbox, )?; // Mask if let Some(ref mask_node) = stacking_ctx.mask { res = res.and_then(|bbox| { self.generate_cairo_mask( mask_node, viewport, affines.for_temporary_surface, &bbox, acquired_nodes, ) .and_then(|mask_surf| { if let Some(surf) = mask_surf { self.cr.push_group(); self.cr.set_matrix( ValidTransform::try_from(affines.compositing)?.into(), ); self.cr.mask_surface(&surf, 0.0, 0.0)?; Ok(self.cr.pop_group_to_source()?) } else { Ok(()) } }) .map(|_: ()| bbox) }); } { // Composite the temporary surface self.cr .set_matrix(ValidTransform::try_from(affines.compositing)?.into()); self.cr.set_operator(stacking_ctx.mix_blend_mode.into()); if opacity < 1.0 { self.cr.paint_with_alpha(opacity)?; } else { self.cr.paint()?; } } self.cr.set_matrix(affine_at_start.into()); res } else if let Some(layout_viewport) = layout_viewport.as_ref() { // FIXME: here we ignore the Some() result of push_new_viewport(). We do that because // the returned one is just a copy of the one that got passeed in, but with a changed // transform. However, we are in fact not using that transform anywhere! // // In case push_new_viewport() returns None, we just don't draw anything. // // Note that push_new_viewport() changes the cr's transform. However it will be restored // at the end of this function with set_matrix. if let Some(new_viewport) = self.push_new_viewport(viewport, layout_viewport) { draw_fn(acquired_nodes, self, &new_viewport) } else { self.cr.set_matrix(orig_transform.into()); Ok(self.empty_bbox()) } } else { draw_fn(acquired_nodes, self, viewport) }; if stacking_ctx.link_target.is_some() { self.link_tag_end(); } res }) }; self.cr.set_matrix(orig_transform.into()); res } /// Run the drawing function with the specified opacity fn with_alpha( &mut self, opacity: UnitInterval, draw_fn: &mut dyn FnMut(&mut DrawingCtx) -> Result, ) -> Result { let res; let UnitInterval(o) = opacity; if o < 1.0 { self.cr.push_group(); res = draw_fn(self); self.cr.pop_group_to_source()?; self.cr.paint_with_alpha(o)?; } else { res = draw_fn(self); } res } /// Start a Cairo tag for PDF links fn link_tag_begin(&mut self, link_target: &str) { let attributes = format!("uri='{}'", escape_link_target(link_target)); let cr = self.cr.clone(); cr.tag_begin(CAIRO_TAG_LINK, &attributes); } /// End a Cairo tag for PDF links fn link_tag_end(&mut self) { self.cr.tag_end(CAIRO_TAG_LINK); } fn run_filters( &mut self, viewport: &Viewport, surface_to_filter: SharedImageSurface, filter: &Filter, acquired_nodes: &mut AcquiredNodes<'_>, node_name: &str, user_space_params: &NormalizeParams, stroke_paint_source: Rc, fill_paint_source: Rc, node_bbox: BoundingBox, ) -> Result { let session = self.session(); // We try to convert each item in the filter_list to a FilterSpec. // // However, the spec mentions, "If the filter references a non-existent object or // the referenced object is not a filter element, then the whole filter chain is // ignored." - https://www.w3.org/TR/filter-effects/#FilterProperty // // So, run through the filter_list and collect into a Result>. // This will return an Err if any of the conversions failed. let filter_specs = filter .filter_list .iter() .map(|filter_value| { filter_value.to_filter_spec( acquired_nodes, user_space_params, filter.current_color, viewport, session, node_name, ) }) .collect::, _>>(); match filter_specs { Ok(specs) => { // Start with the surface_to_filter, and apply each filter spec in turn; // the final result is our return value. specs.iter().try_fold(surface_to_filter, |surface, spec| { filters::render( spec, stroke_paint_source.clone(), fill_paint_source.clone(), surface, acquired_nodes, self, *self.get_transform(), node_bbox, ) }) } Err(e) => { rsvg_log!( self.session, "not rendering filter list on node {} because it was in error: {}", node_name, e ); // just return the original surface without filtering it Ok(surface_to_filter) } } } fn set_gradient(&mut self, gradient: &UserSpaceGradient) -> Result<(), InternalRenderingError> { let g = match gradient.variant { GradientVariant::Linear { x1, y1, x2, y2 } => { cairo::Gradient::clone(&cairo::LinearGradient::new(x1, y1, x2, y2)) } GradientVariant::Radial { cx, cy, r, fx, fy, fr, } => cairo::Gradient::clone(&cairo::RadialGradient::new(fx, fy, fr, cx, cy, r)), }; g.set_matrix(ValidTransform::try_from(gradient.transform)?.into()); g.set_extend(cairo::Extend::from(gradient.spread)); for stop in &gradient.stops { let UnitInterval(stop_offset) = stop.offset; let rgba = color_to_rgba(&stop.color); g.add_color_stop_rgba( stop_offset, f64::from(rgba.red.unwrap_or(0)) / 255.0, f64::from(rgba.green.unwrap_or(0)) / 255.0, f64::from(rgba.blue.unwrap_or(0)) / 255.0, f64::from(rgba.alpha.unwrap_or(0.0)), ); } Ok(self.cr.set_source(&g)?) } fn set_pattern( &mut self, pattern: &UserSpacePattern, acquired_nodes: &mut AcquiredNodes<'_>, ) -> Result { // Bail out early if the pattern has zero size, per the spec if approx_eq!(f64, pattern.width, 0.0) || approx_eq!(f64, pattern.height, 0.0) { return Ok(false); } // Bail out early if this pattern has a circular reference let pattern_node_acquired = match pattern.acquire_pattern_node(acquired_nodes) { Ok(n) => n, Err(AcquireError::CircularReference(ref node)) => { rsvg_log!(self.session, "circular reference in element {}", node); return Ok(false); } _ => unreachable!(), }; let pattern_node = pattern_node_acquired.get(); let taffine = self.get_transform().pre_transform(&pattern.transform); let mut scwscale = (taffine.xx.powi(2) + taffine.xy.powi(2)).sqrt(); let mut schscale = (taffine.yx.powi(2) + taffine.yy.powi(2)).sqrt(); let pw: i32 = (pattern.width * scwscale) as i32; let ph: i32 = (pattern.height * schscale) as i32; if pw < 1 || ph < 1 { return Ok(false); } scwscale = f64::from(pw) / pattern.width; schscale = f64::from(ph) / pattern.height; // Apply the pattern transform let (affine, caffine) = if scwscale.approx_eq_cairo(1.0) && schscale.approx_eq_cairo(1.0) { (pattern.coord_transform, pattern.content_transform) } else { ( pattern .coord_transform .pre_scale(1.0 / scwscale, 1.0 / schscale), pattern.content_transform.post_scale(scwscale, schscale), ) }; // Draw to another surface let surface = self .cr .target() .create_similar(cairo::Content::ColorAlpha, pw, ph)?; let cr_pattern = cairo::Context::new(&surface)?; // Set up transformations to be determined by the contents units let transform = ValidTransform::try_from(caffine)?; cr_pattern.set_matrix(transform.into()); // Draw everything { let mut pattern_draw_ctx = self.nested(cr_pattern); let pattern_viewport = Viewport { dpi: self.config.dpi, vbox: ViewBox::from(Rect::from_size(pattern.width, pattern.height)), transform: *transform, }; pattern_draw_ctx .with_alpha(pattern.opacity, &mut |dc| { let pattern_cascaded = CascadedValues::new_from_node(pattern_node); let pattern_values = pattern_cascaded.get(); let elt = pattern_node.borrow_element(); let stacking_ctx = StackingContext::new( self.session(), acquired_nodes, &elt, Transform::identity(), None, pattern_values, ); dc.with_discrete_layer( &stacking_ctx, acquired_nodes, &pattern_viewport, None, false, &mut |an, dc, new_viewport| { pattern_node.draw_children( an, &pattern_cascaded, new_viewport, dc, false, ) }, ) }) .map(|_| ())?; } // Set the final surface as a Cairo pattern into the Cairo context let pattern = cairo::SurfacePattern::create(&surface); if let Some(m) = affine.invert() { pattern.set_matrix(ValidTransform::try_from(m)?.into()); pattern.set_extend(cairo::Extend::Repeat); pattern.set_filter(cairo::Filter::Best); self.cr.set_source(&pattern)?; } Ok(true) } fn set_paint_source( &mut self, paint_source: &UserSpacePaintSource, acquired_nodes: &mut AcquiredNodes<'_>, ) -> Result { match *paint_source { UserSpacePaintSource::Gradient(ref gradient, _c) => { self.set_gradient(gradient)?; Ok(true) } UserSpacePaintSource::Pattern(ref pattern, ref c) => { if self.set_pattern(pattern, acquired_nodes)? { Ok(true) } else if let Some(c) = c { set_source_color_on_cairo(&self.cr, c); Ok(true) } else { Ok(false) } } UserSpacePaintSource::SolidColor(ref c) => { set_source_color_on_cairo(&self.cr, c); Ok(true) } UserSpacePaintSource::None => Ok(false), } } /// Computes and returns a surface corresponding to the given paint server. pub fn get_paint_source_surface( &mut self, width: i32, height: i32, acquired_nodes: &mut AcquiredNodes<'_>, paint_source: &UserSpacePaintSource, ) -> Result { let mut surface = ExclusiveImageSurface::new(width, height, SurfaceType::SRgb)?; surface.draw(&mut |cr| { let mut temporary_draw_ctx = self.nested(cr); // FIXME: we are ignoring any error let had_paint_server = temporary_draw_ctx.set_paint_source(paint_source, acquired_nodes)?; if had_paint_server { temporary_draw_ctx.cr.paint()?; } Ok(()) })?; Ok(surface.share()?) } fn stroke( &mut self, cr: &cairo::Context, acquired_nodes: &mut AcquiredNodes<'_>, paint_source: &UserSpacePaintSource, ) -> Result<(), InternalRenderingError> { let had_paint_server = self.set_paint_source(paint_source, acquired_nodes)?; if had_paint_server { cr.stroke_preserve()?; } Ok(()) } fn fill( &mut self, cr: &cairo::Context, acquired_nodes: &mut AcquiredNodes<'_>, paint_source: &UserSpacePaintSource, ) -> Result<(), InternalRenderingError> { let had_paint_server = self.set_paint_source(paint_source, acquired_nodes)?; if had_paint_server { cr.fill_preserve()?; } Ok(()) } pub fn draw_layer( &mut self, layer: &Layer, acquired_nodes: &mut AcquiredNodes<'_>, clipping: bool, viewport: &Viewport, ) -> Result { match &layer.kind { LayerKind::Shape(shape) => self.draw_shape( shape, &layer.stacking_ctx, acquired_nodes, clipping, viewport, ), LayerKind::Text(text) => self.draw_text( text, &layer.stacking_ctx, acquired_nodes, clipping, viewport, ), LayerKind::Image(image) => self.draw_image( image, &layer.stacking_ctx, acquired_nodes, clipping, viewport, ), LayerKind::Group(group) => self.draw_group( group, &layer.stacking_ctx, acquired_nodes, clipping, viewport, ), } } fn draw_shape( &mut self, shape: &Shape, stacking_ctx: &StackingContext, acquired_nodes: &mut AcquiredNodes<'_>, clipping: bool, viewport: &Viewport, ) -> Result { if shape.extents.is_none() { return Ok(self.empty_bbox()); } self.with_discrete_layer( stacking_ctx, acquired_nodes, viewport, None, clipping, &mut |an, dc, new_viewport| { let cr = dc.cr.clone(); let transform = dc.get_transform_for_stacking_ctx(stacking_ctx, clipping)?; let mut path_helper = PathHelper::new(&cr, transform, &shape.path, shape.stroke.line_cap); if clipping { if shape.is_visible { cr.set_fill_rule(cairo::FillRule::from(shape.clip_rule)); path_helper.set()?; } return Ok(dc.empty_bbox()); } cr.set_antialias(cairo::Antialias::from(shape.shape_rendering)); setup_cr_for_stroke(&cr, &shape.stroke); cr.set_fill_rule(cairo::FillRule::from(shape.fill_rule)); path_helper.set()?; let bbox = compute_stroke_and_fill_box( &cr, &shape.stroke, &shape.stroke_paint, &dc.initial_viewport, )?; if shape.is_visible { for &target in &shape.paint_order.targets { // fill and stroke operations will preserve the path. // markers operation will clear the path. match target { PaintTarget::Fill => { path_helper.set()?; dc.fill(&cr, an, &shape.fill_paint)?; } PaintTarget::Stroke => { path_helper.set()?; let backup_matrix = if shape.stroke.non_scaling { let matrix = cr.matrix(); cr.set_matrix( ValidTransform::try_from(dc.initial_viewport.transform)? .into(), ); Some(matrix) } else { None }; dc.stroke(&cr, an, &shape.stroke_paint)?; if let Some(matrix) = backup_matrix { cr.set_matrix(matrix); } } PaintTarget::Markers => { path_helper.unset(); marker::render_markers_for_shape( shape, new_viewport, dc, an, clipping, )?; } } } } path_helper.unset(); Ok(bbox) }, ) } fn paint_surface( &mut self, surface: &SharedImageSurface, width: f64, height: f64, image_rendering: ImageRendering, ) -> Result<(), cairo::Error> { let cr = self.cr.clone(); // We need to set extend appropriately, so can't use cr.set_source_surface(). // // If extend is left at its default value (None), then bilinear scaling uses // transparency outside of the image producing incorrect results. // For example, in svg1.1/filters-blend-01-b.svgthere's a completely // opaque 100×1 image of a gradient scaled to 100×98 which ends up // transparent almost everywhere without this fix (which it shouldn't). let ptn = surface.to_cairo_pattern(); ptn.set_extend(cairo::Extend::Pad); let interpolation = Interpolation::from(image_rendering); ptn.set_filter(cairo::Filter::from(interpolation)); cr.set_source(&ptn)?; // Clip is needed due to extend being set to pad. clip_to_rectangle(&cr, &Rect::from_size(width, height)); cr.paint() } fn draw_image( &mut self, image: &Image, stacking_ctx: &StackingContext, acquired_nodes: &mut AcquiredNodes<'_>, clipping: bool, viewport: &Viewport, ) -> Result { let image_width = image.surface.width(); let image_height = image.surface.height(); if clipping || image.rect.is_empty() || image_width == 0 || image_height == 0 { return Ok(self.empty_bbox()); } let image_width = f64::from(image_width); let image_height = f64::from(image_height); let vbox = ViewBox::from(Rect::from_size(image_width, image_height)); // The bounding box for is decided by the values of the image's x, y, w, h // and not by the final computed image bounds. let bounds = self.empty_bbox().with_rect(image.rect); let layout_viewport = LayoutViewport { vbox: Some(vbox), geometry: image.rect, preserve_aspect_ratio: image.aspect, overflow: image.overflow, }; if image.is_visible { self.with_discrete_layer( stacking_ctx, acquired_nodes, viewport, Some(layout_viewport), clipping, &mut |_an, dc, _new_viewport| { dc.paint_surface( &image.surface, image_width, image_height, image.image_rendering, )?; Ok(bounds) }, ) } else { Ok(bounds) } } fn draw_group( &mut self, _group: &Group, _stacking_ctx: &StackingContext, _acquired_nodes: &mut AcquiredNodes<'_>, _clipping: bool, _viewport: &Viewport, ) -> Result { unimplemented!() } fn draw_text_span( &mut self, span: &TextSpan, acquired_nodes: &mut AcquiredNodes<'_>, clipping: bool, ) -> Result { let path = pango_layout_to_path(span.x, span.y, &span.layout, span.gravity)?; if path.is_empty() { // Empty strings, or only-whitespace text, get turned into empty paths. // In that case, we really want to return "no bounds" rather than an // empty rectangle. return Ok(self.empty_bbox()); } // #851 - We can't just render all text as paths for PDF; it // needs the actual text content so text is selectable by PDF // viewers. let can_use_text_as_path = self.cr.target().type_() != cairo::SurfaceType::Pdf; with_saved_cr(&self.cr.clone(), || { self.cr .set_antialias(cairo::Antialias::from(span.text_rendering)); setup_cr_for_stroke(&self.cr, &span.stroke); if clipping { path.to_cairo(&self.cr, false)?; return Ok(self.empty_bbox()); } path.to_cairo(&self.cr, false)?; let bbox = compute_stroke_and_fill_box( &self.cr, &span.stroke, &span.stroke_paint, &self.initial_viewport, )?; self.cr.new_path(); if span.is_visible { if let Some(ref link_target) = span.link_target { self.link_tag_begin(link_target); } for &target in &span.paint_order.targets { match target { PaintTarget::Fill => { let had_paint_server = self.set_paint_source(&span.fill_paint, acquired_nodes)?; if had_paint_server { if can_use_text_as_path { path.to_cairo(&self.cr, false)?; self.cr.fill()?; self.cr.new_path(); } else { self.cr.move_to(span.x, span.y); let matrix = self.cr.matrix(); let rotation_from_gravity = span.gravity.to_rotation(); if !rotation_from_gravity.approx_eq_cairo(0.0) { self.cr.rotate(-rotation_from_gravity); } pangocairo::functions::update_layout(&self.cr, &span.layout); pangocairo::functions::show_layout(&self.cr, &span.layout); self.cr.set_matrix(matrix); } } } PaintTarget::Stroke => { let had_paint_server = self.set_paint_source(&span.stroke_paint, acquired_nodes)?; if had_paint_server { path.to_cairo(&self.cr, false)?; self.cr.stroke()?; self.cr.new_path(); } } PaintTarget::Markers => {} } } if span.link_target.is_some() { self.link_tag_end(); } } Ok(bbox) }) } fn draw_text( &mut self, text: &Text, stacking_ctx: &StackingContext, acquired_nodes: &mut AcquiredNodes<'_>, clipping: bool, viewport: &Viewport, ) -> Result { self.with_discrete_layer( stacking_ctx, acquired_nodes, viewport, None, clipping, &mut |an, dc, _new_viewport| { let mut bbox = dc.empty_bbox(); for span in &text.spans { let span_bbox = dc.draw_text_span(span, an, clipping)?; bbox.insert(&span_bbox); } Ok(bbox) }, ) } pub fn get_snapshot( &self, width: i32, height: i32, ) -> Result { // TODO: as far as I can tell this should not render elements past the last (topmost) one // with enable-background: new (because technically we shouldn't have been caching them). // Right now there are no enable-background checks whatsoever. // // Addendum: SVG 2 has deprecated the enable-background property, and replaced it with an // "isolation" property from the CSS Compositing and Blending spec. // // Deprecation: // https://www.w3.org/TR/filter-effects-1/#AccessBackgroundImage // // BackgroundImage, BackgroundAlpha in the "in" attribute of filter primitives: // https://www.w3.org/TR/filter-effects-1/#attr-valuedef-in-backgroundimage // // CSS Compositing and Blending, "isolation" property: // https://www.w3.org/TR/compositing-1/#isolation let mut surface = ExclusiveImageSurface::new(width, height, SurfaceType::SRgb)?; surface.draw(&mut |cr| { // TODO: apparently DrawingCtx.cr_stack is just a way to store pairs of // (surface, transform). Can we turn it into a DrawingCtx.surface_stack // instead? See what CSS isolation would like to call that; are the pairs just // stacking contexts instead, or the result of rendering stacking contexts? for (depth, draw) in self.cr_stack.borrow().iter().enumerate() { let affines = CompositingAffines::new( Transform::from(draw.matrix()), self.initial_viewport.transform, depth, ); cr.set_matrix(ValidTransform::try_from(affines.for_snapshot)?.into()); cr.set_source_surface(&draw.target(), 0.0, 0.0)?; cr.paint()?; } Ok(()) })?; Ok(surface.share()?) } pub fn draw_node_to_surface( &mut self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, affine: Transform, width: i32, height: i32, ) -> Result { let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, width, height)?; let save_cr = self.cr.clone(); { let cr = cairo::Context::new(&surface)?; cr.set_matrix(ValidTransform::try_from(affine)?.into()); self.cr = cr; let viewport = Viewport { dpi: self.config.dpi, transform: affine, vbox: ViewBox::from(Rect::from_size(f64::from(width), f64::from(height))), }; let _ = self.draw_node_from_stack(node, acquired_nodes, cascaded, &viewport, false)?; } self.cr = save_cr; Ok(SharedImageSurface::wrap(surface, SurfaceType::SRgb)?) } pub fn draw_node_from_stack( &mut self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, clipping: bool, ) -> Result { let stack_top = self.drawsub_stack.pop(); let draw = if let Some(ref top) = stack_top { top == node } else { true }; let res = if draw { node.draw(acquired_nodes, cascaded, viewport, self, clipping) } else { Ok(self.empty_bbox()) }; if let Some(top) = stack_top { self.drawsub_stack.push(top); } res } pub fn draw_from_use_node( &mut self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, values: &ComputedValues, use_rect: Rect, link: &NodeId, clipping: bool, viewport: &Viewport, fill_paint: Rc, stroke_paint: Rc, ) -> Result { // is an element that is used directly, unlike // , which is used through a fill="url(#...)" // reference. However, will always reference another // element, potentially itself or an ancestor of itself (or // another which references the first one, etc.). So, // we acquire the element itself so that circular // references can be caught. let _self_acquired = match acquired_nodes.acquire_ref(node) { Ok(n) => n, Err(AcquireError::CircularReference(circular)) => { rsvg_log!(self.session, "circular reference in element {}", circular); return Err(InternalRenderingError::CircularReference(circular)); } _ => unreachable!(), }; let acquired = match acquired_nodes.acquire(link) { Ok(acquired) => acquired, Err(AcquireError::CircularReference(circular)) => { rsvg_log!( self.session, "circular reference from {} to element {}", node, circular ); return Err(InternalRenderingError::CircularReference(circular)); } Err(AcquireError::MaxReferencesExceeded) => { return Err(InternalRenderingError::LimitExceeded( ImplementationLimit::TooManyReferencedElements, )); } Err(AcquireError::InvalidLinkType(_)) => unreachable!(), Err(AcquireError::LinkNotFound(node_id)) => { rsvg_log!( self.session, "element {} references nonexistent \"{}\"", node, node_id ); return Ok(self.empty_bbox()); } }; // width or height set to 0 disables rendering of the element // https://www.w3.org/TR/SVG/struct.html#UseElementWidthAttribute if use_rect.is_empty() { return Ok(self.empty_bbox()); } let child = acquired.get(); if clipping && !element_can_be_used_inside_use_inside_clip_path(&child.borrow_element()) { return Ok(self.empty_bbox()); } let orig_transform = self.get_transform(); self.cr .transform(ValidTransform::try_from(values.transform())?.into()); let use_element = node.borrow_element(); let defines_a_viewport = if is_element_of_type!(child, Symbol) { let symbol = borrow_element_as!(child, Symbol); Some((symbol.get_viewbox(), symbol.get_preserve_aspect_ratio())) } else if is_element_of_type!(child, Svg) { let svg = borrow_element_as!(child, Svg); Some((svg.get_viewbox(), svg.get_preserve_aspect_ratio())) } else { None }; let res = if let Some((vbox, preserve_aspect_ratio)) = defines_a_viewport { // and define a viewport, as described in the specification: // https://www.w3.org/TR/SVG2/struct.html#UseElement // https://gitlab.gnome.org/GNOME/librsvg/-/issues/875#note_1482705 let elt = child.borrow_element(); let child_values = elt.get_computed_values(); let stacking_ctx = StackingContext::new( self.session(), acquired_nodes, &use_element, Transform::identity(), None, values, ); let layout_viewport = LayoutViewport { vbox, geometry: use_rect, preserve_aspect_ratio, overflow: child_values.overflow(), }; self.with_discrete_layer( &stacking_ctx, acquired_nodes, viewport, Some(layout_viewport), clipping, &mut |an, dc, new_viewport| { child.draw_children( an, &CascadedValues::new_from_values( child, values, Some(fill_paint.clone()), Some(stroke_paint.clone()), ), new_viewport, dc, clipping, ) }, ) } else { // otherwise the referenced node is not a ; process it generically let stacking_ctx = StackingContext::new( self.session(), acquired_nodes, &use_element, Transform::new_translate(use_rect.x0, use_rect.y0), None, values, ); self.with_discrete_layer( &stacking_ctx, acquired_nodes, viewport, None, clipping, &mut |an, dc, new_viewport| { child.draw( an, &CascadedValues::new_from_values( child, values, Some(fill_paint.clone()), Some(stroke_paint.clone()), ), new_viewport, dc, clipping, ) }, ) }; self.cr.set_matrix(orig_transform.into()); if let Ok(bbox) = res { let mut res_bbox = BoundingBox::new().with_transform(*orig_transform); res_bbox.insert(&bbox); Ok(res_bbox) } else { res } } /// Extracts the font options for the current state of the DrawingCtx. /// /// You can use the font options later with create_pango_context(). pub fn get_font_options(&self) -> FontOptions { let mut options = cairo::FontOptions::new().unwrap(); if self.config.testing { options.set_antialias(cairo::Antialias::Gray); } options.set_hint_style(cairo::HintStyle::None); options.set_hint_metrics(cairo::HintMetrics::Off); FontOptions { options } } } impl From for Interpolation { fn from(r: ImageRendering) -> Interpolation { match r { ImageRendering::Pixelated | ImageRendering::CrispEdges | ImageRendering::OptimizeSpeed => Interpolation::Nearest, ImageRendering::Smooth | ImageRendering::OptimizeQuality | ImageRendering::HighQuality | ImageRendering::Auto => Interpolation::Smooth, } } } pub fn compute_path_extents(path: &Path) -> Result, InternalRenderingError> { if path.is_empty() { return Ok(None); } let surface = cairo::RecordingSurface::create(cairo::Content::ColorAlpha, None)?; let cr = cairo::Context::new(&surface)?; path.to_cairo(&cr, false)?; let (x0, y0, x1, y1) = cr.path_extents()?; Ok(Some(Rect::new(x0, y0, x1, y1))) } /// Create a Pango context with a particular configuration. pub fn create_pango_context(font_options: &FontOptions, transform: &Transform) -> pango::Context { let font_map = pangocairo::FontMap::default(); let context = font_map.create_context(); context.set_round_glyph_positions(false); let pango_matrix = PangoMatrix { xx: transform.xx, xy: transform.xy, yx: transform.yx, yy: transform.yy, x0: transform.x0, y0: transform.y0, }; let pango_matrix_ptr: *const PangoMatrix = &pango_matrix; let matrix = unsafe { pango::Matrix::from_glib_none(pango_matrix_ptr) }; context.set_matrix(Some(&matrix)); pangocairo::functions::context_set_font_options(&context, Some(&font_options.options)); // Pango says this about pango_cairo_context_set_resolution(): // // Sets the resolution for the context. This is a scale factor between // points specified in a #PangoFontDescription and Cairo units. The // default value is 96, meaning that a 10 point font will be 13 // units high. (10 * 96. / 72. = 13.3). // // I.e. Pango font sizes in a PangoFontDescription are in *points*, not pixels. // However, we are normalizing everything to userspace units, which amount to // pixels. So, we will use 72.0 here to make Pango not apply any further scaling // to the size values we give it. // // An alternative would be to divide our font sizes by (dpi_y / 72) to effectively // cancel out Pango's scaling, but it's probably better to deal with Pango-isms // right here, instead of spreading them out through our Length normalization // code. pangocairo::functions::context_set_resolution(&context, 72.0); context } pub fn set_source_color_on_cairo(cr: &cairo::Context, color: &cssparser::Color) { let rgba = color_to_rgba(color); cr.set_source_rgba( f64::from(rgba.red.unwrap_or(0)) / 255.0, f64::from(rgba.green.unwrap_or(0)) / 255.0, f64::from(rgba.blue.unwrap_or(0)) / 255.0, f64::from(rgba.alpha.unwrap_or(0.0)), ); } /// Converts a Pango layout to a Cairo path on the specified cr starting at (x, y). /// Does not clear the current path first. fn pango_layout_to_cairo( x: f64, y: f64, layout: &pango::Layout, gravity: pango::Gravity, cr: &cairo::Context, ) { let rotation_from_gravity = gravity.to_rotation(); let rotation = if !rotation_from_gravity.approx_eq_cairo(0.0) { Some(-rotation_from_gravity) } else { None }; cr.move_to(x, y); let matrix = cr.matrix(); if let Some(rot) = rotation { cr.rotate(rot); } pangocairo::functions::update_layout(cr, layout); pangocairo::functions::layout_path(cr, layout); cr.set_matrix(matrix); } /// Converts a Pango layout to a Path starting at (x, y). pub fn pango_layout_to_path( x: f64, y: f64, layout: &pango::Layout, gravity: pango::Gravity, ) -> Result { let surface = cairo::RecordingSurface::create(cairo::Content::ColorAlpha, None)?; let cr = cairo::Context::new(&surface)?; pango_layout_to_cairo(x, y, layout, gravity, &cr); let cairo_path = cr.copy_path()?; Ok(Path::from_cairo(cairo_path)) } // https://www.w3.org/TR/css-masking-1/#ClipPathElement fn element_can_be_used_inside_clip_path(element: &Element) -> bool { use ElementData::*; matches!( element.element_data, Circle(_) | Ellipse(_) | Line(_) | Path(_) | Polygon(_) | Polyline(_) | Rect(_) | Text(_) | Use(_) ) } // https://www.w3.org/TR/css-masking-1/#ClipPathElement fn element_can_be_used_inside_use_inside_clip_path(element: &Element) -> bool { use ElementData::*; matches!( element.element_data, Circle(_) | Ellipse(_) | Line(_) | Path(_) | Polygon(_) | Polyline(_) | Rect(_) | Text(_) ) } #[derive(Debug)] struct CompositingAffines { pub outside_temporary_surface: Transform, #[allow(unused)] pub initial: Transform, pub for_temporary_surface: Transform, pub compositing: Transform, pub for_snapshot: Transform, } impl CompositingAffines { fn new(current: Transform, initial: Transform, cr_stack_depth: usize) -> CompositingAffines { let is_topmost_temporary_surface = cr_stack_depth == 0; let initial_inverse = initial.invert().unwrap(); let outside_temporary_surface = if is_topmost_temporary_surface { current } else { current.post_transform(&initial_inverse) }; let (scale_x, scale_y) = initial.transform_distance(1.0, 1.0); let for_temporary_surface = if is_topmost_temporary_surface { current .post_transform(&initial_inverse) .post_scale(scale_x, scale_y) } else { current }; let compositing = if is_topmost_temporary_surface { initial.pre_scale(1.0 / scale_x, 1.0 / scale_y) } else { Transform::identity() }; let for_snapshot = compositing.invert().unwrap(); CompositingAffines { outside_temporary_surface, initial, for_temporary_surface, compositing, for_snapshot, } } } fn compute_stroke_and_fill_extents( cr: &cairo::Context, stroke: &Stroke, stroke_paint_source: &UserSpacePaintSource, initial_viewport: &Viewport, ) -> Result { // Dropping the precision of cairo's bezier subdivision, yielding 2x // _rendering_ time speedups, are these rather expensive operations // really needed here? */ let backup_tolerance = cr.tolerance(); cr.set_tolerance(1.0); // Bounding box for fill // // Unlike the case for stroke, for fills we always compute the bounding box. // In GNOME we have SVGs for symbolic icons where each icon has a bounding // rectangle with no fill and no stroke, and inside it there are the actual // paths for the icon's shape. We need to be able to compute the bounding // rectangle's extents, even when it has no fill nor stroke. let (x0, y0, x1, y1) = cr.fill_extents()?; let fill_extents = if x0 != 0.0 || y0 != 0.0 || x1 != 0.0 || y1 != 0.0 { Some(Rect::new(x0, y0, x1, y1)) } else { None }; // Bounding box for stroke // // When presented with a line width of 0, Cairo returns a // stroke_extents rectangle of (0, 0, 0, 0). This would cause the // bbox to include a lone point at the origin, which is wrong, as a // stroke of zero width should not be painted, per // https://www.w3.org/TR/SVG2/painting.html#StrokeWidth // // So, see if the stroke width is 0 and just not include the stroke in the // bounding box if so. let stroke_extents = if !stroke.width.approx_eq_cairo(0.0) && !matches!(stroke_paint_source, UserSpacePaintSource::None) { let backup_matrix = if stroke.non_scaling { let matrix = cr.matrix(); cr.set_matrix(ValidTransform::try_from(initial_viewport.transform)?.into()); Some(matrix) } else { None }; let (x0, y0, x1, y1) = cr.stroke_extents()?; if let Some(matrix) = backup_matrix { cr.set_matrix(matrix); } Some(Rect::new(x0, y0, x1, y1)) } else { None }; // objectBoundingBox let (x0, y0, x1, y1) = cr.path_extents()?; let path_extents = Some(Rect::new(x0, y0, x1, y1)); // restore tolerance cr.set_tolerance(backup_tolerance); Ok(PathExtents { path_only: path_extents, fill: fill_extents, stroke: stroke_extents, }) } fn compute_stroke_and_fill_box( cr: &cairo::Context, stroke: &Stroke, stroke_paint_source: &UserSpacePaintSource, initial_viewport: &Viewport, ) -> Result { let extents = compute_stroke_and_fill_extents(cr, stroke, stroke_paint_source, initial_viewport)?; let ink_rect = match (extents.fill, extents.stroke) { (None, None) => None, (Some(f), None) => Some(f), (None, Some(s)) => Some(s), (Some(f), Some(s)) => Some(f.union(&s)), }; let mut bbox = BoundingBox::new().with_transform(Transform::from(cr.matrix())); if let Some(rect) = extents.path_only { bbox = bbox.with_rect(rect); } if let Some(ink_rect) = ink_rect { bbox = bbox.with_ink_rect(ink_rect); } Ok(bbox) } fn setup_cr_for_stroke(cr: &cairo::Context, stroke: &Stroke) { cr.set_line_width(stroke.width); cr.set_miter_limit(stroke.miter_limit.0); cr.set_line_cap(cairo::LineCap::from(stroke.line_cap)); cr.set_line_join(cairo::LineJoin::from(stroke.line_join)); let total_length: f64 = stroke.dashes.iter().sum(); if total_length > 0.0 { cr.set_dash(&stroke.dashes, stroke.dash_offset); } else { cr.set_dash(&[], 0.0); } } /// escape quotes and backslashes with backslash fn escape_link_target(value: &str) -> Cow<'_, str> { let regex = { static REGEX: OnceLock = OnceLock::new(); REGEX.get_or_init(|| Regex::new(r"['\\]").unwrap()) }; regex.replace_all(value, |caps: &Captures<'_>| { match caps.get(0).unwrap().as_str() { "'" => "\\'".to_owned(), "\\" => "\\\\".to_owned(), _ => unreachable!(), } }) } fn clip_to_rectangle(cr: &cairo::Context, r: &Rect) { cr.rectangle(r.x0, r.y0, r.width(), r.height()); cr.clip(); } impl From for cairo::Extend { fn from(s: SpreadMethod) -> cairo::Extend { match s { SpreadMethod::Pad => cairo::Extend::Pad, SpreadMethod::Reflect => cairo::Extend::Reflect, SpreadMethod::Repeat => cairo::Extend::Repeat, } } } impl From for cairo::LineJoin { fn from(j: StrokeLinejoin) -> cairo::LineJoin { match j { StrokeLinejoin::Miter => cairo::LineJoin::Miter, StrokeLinejoin::Round => cairo::LineJoin::Round, StrokeLinejoin::Bevel => cairo::LineJoin::Bevel, } } } impl From for cairo::LineCap { fn from(j: StrokeLinecap) -> cairo::LineCap { match j { StrokeLinecap::Butt => cairo::LineCap::Butt, StrokeLinecap::Round => cairo::LineCap::Round, StrokeLinecap::Square => cairo::LineCap::Square, } } } impl From for cairo::Operator { fn from(m: MixBlendMode) -> cairo::Operator { use cairo::Operator; match m { MixBlendMode::Normal => Operator::Over, MixBlendMode::Multiply => Operator::Multiply, MixBlendMode::Screen => Operator::Screen, MixBlendMode::Overlay => Operator::Overlay, MixBlendMode::Darken => Operator::Darken, MixBlendMode::Lighten => Operator::Lighten, MixBlendMode::ColorDodge => Operator::ColorDodge, MixBlendMode::ColorBurn => Operator::ColorBurn, MixBlendMode::HardLight => Operator::HardLight, MixBlendMode::SoftLight => Operator::SoftLight, MixBlendMode::Difference => Operator::Difference, MixBlendMode::Exclusion => Operator::Exclusion, MixBlendMode::Hue => Operator::HslHue, MixBlendMode::Saturation => Operator::HslSaturation, MixBlendMode::Color => Operator::HslColor, MixBlendMode::Luminosity => Operator::HslLuminosity, } } } impl From for cairo::FillRule { fn from(c: ClipRule) -> cairo::FillRule { match c { ClipRule::NonZero => cairo::FillRule::Winding, ClipRule::EvenOdd => cairo::FillRule::EvenOdd, } } } impl From for cairo::FillRule { fn from(f: FillRule) -> cairo::FillRule { match f { FillRule::NonZero => cairo::FillRule::Winding, FillRule::EvenOdd => cairo::FillRule::EvenOdd, } } } impl From for cairo::Antialias { fn from(sr: ShapeRendering) -> cairo::Antialias { match sr { ShapeRendering::Auto | ShapeRendering::GeometricPrecision => cairo::Antialias::Default, ShapeRendering::OptimizeSpeed | ShapeRendering::CrispEdges => cairo::Antialias::None, } } } impl From for cairo::Antialias { fn from(tr: TextRendering) -> cairo::Antialias { match tr { TextRendering::Auto | TextRendering::OptimizeLegibility | TextRendering::GeometricPrecision => cairo::Antialias::Default, TextRendering::OptimizeSpeed => cairo::Antialias::None, } } } impl From for Transform { #[inline] fn from(m: cairo::Matrix) -> Self { Self::new_unchecked(m.xx(), m.yx(), m.xy(), m.yy(), m.x0(), m.y0()) } } impl From for cairo::Matrix { #[inline] fn from(t: ValidTransform) -> cairo::Matrix { cairo::Matrix::new(t.xx, t.yx, t.xy, t.yy, t.x0, t.y0) } } /// Extents for a path in its current coordinate system. /// /// Normally you'll want to convert this to a BoundingBox, which has knowledge about just /// what that coordinate system is. pub struct PathExtents { /// Extents of the "plain", unstroked path, or `None` if the path is empty. pub path_only: Option, /// Extents of just the fill, or `None` if the path is empty. pub fill: Option, /// Extents for the stroked path, or `None` if the path is empty or zero-width. pub stroke: Option, } impl Path { pub fn to_cairo( &self, cr: &cairo::Context, is_square_linecap: bool, ) -> Result<(), InternalRenderingError> { assert!(!self.is_empty()); for subpath in self.iter_subpath() { // If a subpath is empty and the linecap is a square, then draw a square centered on // the origin of the subpath. See #165. if is_square_linecap { let (x, y) = subpath.origin(); if subpath.is_zero_length() { let stroke_size = 0.002; cr.move_to(x - stroke_size / 2., y); cr.line_to(x + stroke_size / 2., y); } } for cmd in subpath.iter_commands() { cmd.to_cairo(cr); } } // We check the cr's status right after feeding it a new path for a few reasons: // // * Any of the individual path commands may cause the cr to enter an error state, for // example, if they come with coordinates outside of Cairo's supported range. // // * The *next* call to the cr will probably be something that actually checks the status // (i.e. in cairo-rs), and we don't want to panic there. cr.status().map_err(|e| e.into()) } /// Converts a `cairo::Path` to a librsvg `Path`. fn from_cairo(cairo_path: cairo::Path) -> Path { let mut builder = PathBuilder::default(); // Cairo has the habit of appending a MoveTo to some paths, but we don't want a // path for empty text to generate that lone point. So, strip out paths composed // only of MoveTo. if !cairo_path_is_only_move_tos(&cairo_path) { for segment in cairo_path.iter() { match segment { cairo::PathSegment::MoveTo((x, y)) => builder.move_to(x, y), cairo::PathSegment::LineTo((x, y)) => builder.line_to(x, y), cairo::PathSegment::CurveTo((x2, y2), (x3, y3), (x4, y4)) => { builder.curve_to(x2, y2, x3, y3, x4, y4) } cairo::PathSegment::ClosePath => builder.close_path(), } } } builder.into_path() } } fn cairo_path_is_only_move_tos(path: &cairo::Path) -> bool { path.iter() .all(|seg| matches!(seg, cairo::PathSegment::MoveTo((_, _)))) } impl PathCommand { fn to_cairo(&self, cr: &cairo::Context) { match *self { PathCommand::MoveTo(x, y) => cr.move_to(x, y), PathCommand::LineTo(x, y) => cr.line_to(x, y), PathCommand::CurveTo(ref curve) => curve.to_cairo(cr), PathCommand::Arc(ref arc) => arc.to_cairo(cr), PathCommand::ClosePath => cr.close_path(), } } } impl EllipticalArc { fn to_cairo(&self, cr: &cairo::Context) { match self.center_parameterization() { ArcParameterization::CenterParameters { center, radii, theta1, delta_theta, } => { let n_segs = (delta_theta / (PI * 0.5 + 0.001)).abs().ceil() as u32; let d_theta = delta_theta / f64::from(n_segs); let mut theta = theta1; for _ in 0..n_segs { arc_segment(center, radii, self.x_axis_rotation, theta, theta + d_theta) .to_cairo(cr); theta += d_theta; } } ArcParameterization::LineTo => { let (x2, y2) = self.to; cr.line_to(x2, y2); } ArcParameterization::Omit => {} } } } impl CubicBezierCurve { fn to_cairo(&self, cr: &cairo::Context) { let Self { pt1, pt2, to } = *self; cr.curve_to(pt1.0, pt1.1, pt2.0, pt2.1, to.0, to.1); } } #[cfg(test)] mod tests { use super::*; #[test] fn rsvg_path_from_cairo_path() { let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 10, 10).unwrap(); let cr = cairo::Context::new(&surface).unwrap(); cr.move_to(1.0, 2.0); cr.line_to(3.0, 4.0); cr.curve_to(5.0, 6.0, 7.0, 8.0, 9.0, 10.0); cr.close_path(); let cairo_path = cr.copy_path().unwrap(); let path = Path::from_cairo(cairo_path); assert_eq!( path.iter().collect::>(), vec![ PathCommand::MoveTo(1.0, 2.0), PathCommand::LineTo(3.0, 4.0), PathCommand::CurveTo(CubicBezierCurve { pt1: (5.0, 6.0), pt2: (7.0, 8.0), to: (9.0, 10.0), }), PathCommand::ClosePath, PathCommand::MoveTo(1.0, 2.0), // cairo inserts a MoveTo after ClosePath ], ); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/element.rs�����������������������������������������������������������������������0000644�0000000�0000000�00000072664�10461020230�0014176�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! SVG Elements. use markup5ever::{expanded_name, local_name, namespace_url, ns, QualName}; use std::collections::{HashMap, HashSet}; use std::fmt; use std::sync::OnceLock; use crate::accept_language::UserLanguage; use crate::bbox::BoundingBox; use crate::cond::{RequiredExtensions, RequiredFeatures, SystemLanguage}; use crate::css::{Declaration, Origin}; use crate::document::AcquiredNodes; use crate::drawing_ctx::{DrawingCtx, Viewport}; use crate::error::*; use crate::filter::Filter; use crate::filters::{ blend::FeBlend, color_matrix::FeColorMatrix, component_transfer::{FeComponentTransfer, FeFuncA, FeFuncB, FeFuncG, FeFuncR}, composite::FeComposite, convolve_matrix::FeConvolveMatrix, displacement_map::FeDisplacementMap, drop_shadow::FeDropShadow, flood::FeFlood, gaussian_blur::FeGaussianBlur, image::FeImage, lighting::{FeDiffuseLighting, FeDistantLight, FePointLight, FeSpecularLighting, FeSpotLight}, merge::{FeMerge, FeMergeNode}, morphology::FeMorphology, offset::FeOffset, tile::FeTile, turbulence::FeTurbulence, FilterEffect, }; use crate::gradient::{LinearGradient, RadialGradient, Stop}; use crate::image::Image; use crate::layout::Layer; use crate::marker::Marker; use crate::node::*; use crate::pattern::Pattern; use crate::properties::{ComputedValues, SpecifiedValues}; use crate::rsvg_log; use crate::session::Session; use crate::shapes::{Circle, Ellipse, Line, Path, Polygon, Polyline, Rect}; use crate::structure::{ClipPath, Group, Link, Mask, NonRendering, Svg, Switch, Symbol, Use}; use crate::style::Style; use crate::text::{TRef, TSpan, Text}; use crate::xml::Attributes; pub trait ElementTrait { /// Sets per-element attributes. /// /// Each element is supposed to iterate the `attributes`, and parse any ones it needs. /// SVG specifies that unknown attributes should be ignored, and known attributes with invalid /// values should be ignored so that the attribute ends up with its "initial value". /// /// You can use the [`set_attribute`] function to do that. fn set_attributes(&mut self, _attributes: &Attributes, _session: &Session) {} /// Draw an element. /// /// Each element is supposed to draw itself as needed. fn draw( &self, _node: &Node, _acquired_nodes: &mut AcquiredNodes<'_>, _cascaded: &CascadedValues<'_>, _viewport: &Viewport, draw_ctx: &mut DrawingCtx, _clipping: bool, ) -> Result { // by default elements don't draw themselves Ok(draw_ctx.empty_bbox()) } /// Create a layout object for the current element. /// /// This resolves property values, coordinates, lengths, etc. and produces a layout /// item for rendering. fn layout( &self, _node: &Node, _acquired_nodes: &mut AcquiredNodes<'_>, _cascaded: &CascadedValues<'_>, _viewport: &Viewport, _draw_ctx: &mut DrawingCtx, _clipping: bool, ) -> Result, InternalRenderingError> { Ok(None) } } /// Sets `dest` if `parse_result` is `Ok()`, otherwise just logs the error. /// /// Implementations of the [`ElementTrait`] trait generally scan a list of attributes /// for the ones they can handle, and parse their string values. Per the SVG spec, an attribute /// with an invalid value should be ignored, and it should fall back to the default value. /// /// In librsvg, those default values are set in each element's implementation of the [`Default`] trait: /// at element creation time, each element gets initialized to its `Default`, and then each attribute /// gets parsed. This function will set that attribute's value only if parsing was successful. /// /// In case the `parse_result` is an error, this function will log an appropriate notice /// via the [`Session`]. pub fn set_attribute(dest: &mut T, parse_result: Result, session: &Session) { match parse_result { Ok(v) => *dest = v, Err(e) => { // FIXME: this does not provide a clue of what was the problematic element. // We need tracking of the current parsing position to do that. rsvg_log!(session, "ignoring attribute with invalid value: {}", e); } } } pub struct Element { element_name: QualName, attributes: Attributes, specified_values: SpecifiedValues, important_styles: HashSet, values: ComputedValues, required_extensions: Option, required_features: Option, system_language: Option, pub element_data: ElementData, } impl fmt::Display for Element { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.element_name().local)?; write!(f, " id={}", self.get_id().unwrap_or("None"))?; Ok(()) } } /// Parsed contents of an element node in the DOM. /// /// This enum uses `Box` in order to make each variant the size of /// a pointer. pub enum ElementData { Circle(Box), ClipPath(Box), Ellipse(Box), Filter(Box), Group(Box), Image(Box), Line(Box), LinearGradient(Box), Link(Box), Marker(Box), Mask(Box), NonRendering(Box), Path(Box), Pattern(Box), Polygon(Box), Polyline(Box), RadialGradient(Box), Rect(Box), Stop(Box), Style(Box(bounds, d, d / 2)?; } surface } else { // Even kernel sizes have a more interesting scheme. let surface = surface.box_blur::(bounds, d, d / 2)?; let surface = surface.box_blur::(bounds, d, d / 2 - 1)?; let d = d + 1; surface.box_blur::(bounds, d, d / 2)? }; Ok(surface) } /// Applies the gaussian blur. /// /// This is intended to be used in two steps, horizontal and vertical. fn gaussian_blur( input_surface: &SharedImageSurface, bounds: IRect, std_deviation: f64, edge_mode: EdgeMode, vertical: bool, ) -> Result { let kernel = gaussian_kernel(std_deviation); let (rows, cols) = if vertical { (kernel.len(), 1) } else { (1, kernel.len()) }; let kernel = DMatrix::from_data(VecStorage::new(Dyn(rows), Dyn(cols), kernel)); Ok(input_surface.convolve( bounds, ((cols / 2) as i32, (rows / 2) as i32), &kernel, edge_mode, )?) } impl GaussianBlur { pub fn render( &self, bounds_builder: BoundsBuilder, ctx: &FilterContext, acquired_nodes: &mut AcquiredNodes<'_>, draw_ctx: &mut DrawingCtx, ) -> Result { let input_1 = ctx.get_input( acquired_nodes, draw_ctx, &self.in1, self.color_interpolation_filters, )?; let bounds: IRect = bounds_builder .add_input(&input_1) .compute(ctx) .clipped .into(); let NumberOptionalNumber(std_x, std_y) = self.std_deviation; // "A negative value or a value of zero disables the effect of // the given filter primitive (i.e., the result is the filter // input image)." if std_x <= 0.0 && std_y <= 0.0 { return Ok(FilterOutput { surface: input_1.surface().clone(), bounds, }); } let (std_x, std_y) = ctx.paffine().transform_distance(std_x, std_y); // The deviation can become negative here due to the transform. let std_x = std_x.abs(); let std_y = std_y.abs(); // Performance TODO: gaussian blur is frequently used for shadows, operating on SourceAlpha // (so the image is alpha-only). We can use this to not waste time processing the other // channels. // Horizontal convolution. let horiz_result_surface = if std_x >= 2.0 { // The spec says for deviation >= 2.0 three box blurs can be used as an optimization. three_box_blurs::(input_1.surface(), bounds, std_x)? } else if std_x != 0.0 { gaussian_blur(input_1.surface(), bounds, std_x, self.edge_mode, false)? } else { input_1.surface().clone() }; // Vertical convolution. let output_surface = if std_y >= 2.0 { // The spec says for deviation >= 2.0 three box blurs can be used as an optimization. three_box_blurs::(&horiz_result_surface, bounds, std_y)? } else if std_y != 0.0 { gaussian_blur(&horiz_result_surface, bounds, std_y, self.edge_mode, true)? } else { horiz_result_surface }; Ok(FilterOutput { surface: output_surface, bounds, }) } } impl FilterEffect for FeGaussianBlur { fn resolve( &self, _acquired_nodes: &mut AcquiredNodes<'_>, node: &Node, ) -> Result, FilterResolveError> { let cascaded = CascadedValues::new_from_node(node); let values = cascaded.get(); let mut params = self.params.clone(); params.color_interpolation_filters = values.color_interpolation_filters(); Ok(vec![ResolvedPrimitive { primitive: self.base.clone(), params: PrimitiveParams::GaussianBlur(params), }]) } } librsvg-2.59.0/src/filters/image.rs000064400000000000000000000245411046102023000152660ustar 00000000000000use markup5ever::{expanded_name, local_name, namespace_url, ns}; use crate::aspect_ratio::AspectRatio; use crate::document::{AcquiredNodes, Document, NodeId, Resource}; use crate::drawing_ctx::{DrawingCtx, SvgNesting}; use crate::element::{set_attribute, ElementTrait}; use crate::href::{is_href, set_href}; use crate::image::checked_i32; use crate::node::{CascadedValues, Node}; use crate::parsers::ParseValue; use crate::properties::ComputedValues; use crate::rect::Rect; use crate::rsvg_log; use crate::session::Session; use crate::surface_utils::shared_surface::{Interpolation, SharedImageSurface, SurfaceType}; use crate::viewbox::ViewBox; use crate::xml::Attributes; use super::bounds::{Bounds, BoundsBuilder}; use super::context::{FilterContext, FilterOutput}; use super::{ FilterEffect, FilterError, FilterResolveError, Primitive, PrimitiveParams, ResolvedPrimitive, }; /// The `feImage` filter primitive. #[derive(Default)] pub struct FeImage { base: Primitive, params: ImageParams, } #[derive(Clone, Default)] struct ImageParams { aspect: AspectRatio, href: Option, } /// Resolved `feImage` primitive for rendering. pub struct Image { aspect: AspectRatio, source: Source, feimage_values: Box, } /// What a feImage references for rendering. enum Source { /// Nothing is referenced; ignore the filter. None, /// Reference to a node. Node(Node, String), /// Reference to an external image. This is just a URL. ExternalImage(String), } impl ElementTrait for FeImage { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { self.base.parse_no_inputs(attrs, session); for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "preserveAspectRatio") => { set_attribute(&mut self.params.aspect, attr.parse(value), session); } // "path" is used by some older Adobe Illustrator versions ref a if is_href(a) || *a == expanded_name!("", "path") => { set_href(a, &mut self.params.href, Some(value.to_string())); } _ => (), } } } } impl Image { pub fn render( &self, bounds_builder: BoundsBuilder, ctx: &FilterContext, acquired_nodes: &mut AcquiredNodes<'_>, draw_ctx: &mut DrawingCtx, ) -> Result { let bounds = bounds_builder.compute(ctx); let surface = match &self.source { Source::None => return Err(FilterError::InvalidInput), Source::Node(node, ref name) => { if let Ok(acquired) = acquired_nodes.acquire_ref(node) { rsvg_log!(draw_ctx.session(), "(feImage \"{}\"", name); let res = self.render_node( ctx, acquired_nodes, draw_ctx, bounds.clipped, acquired.get(), ); rsvg_log!(draw_ctx.session(), ")"); res? } else { return Err(FilterError::InvalidInput); } } Source::ExternalImage(ref href) => { self.render_external_image(ctx, acquired_nodes, draw_ctx, &bounds, href)? } }; Ok(FilterOutput { surface, bounds: bounds.clipped.into(), }) } /// Renders the filter if the source is an existing node. fn render_node( &self, ctx: &FilterContext, acquired_nodes: &mut AcquiredNodes<'_>, draw_ctx: &mut DrawingCtx, bounds: Rect, referenced_node: &Node, ) -> Result { // https://www.w3.org/TR/filter-effects/#feImageElement // // The filters spec says, "... otherwise [rendering a referenced object], the // referenced resource is rendered according to the behavior of the use element." // I think this means that we use the same cascading mode as , i.e. the // referenced object inherits its properties from the feImage element. let cascaded = CascadedValues::new_from_values(referenced_node, &self.feimage_values, None, None); let interpolation = Interpolation::from(self.feimage_values.image_rendering()); let image = draw_ctx.draw_node_to_surface( referenced_node, acquired_nodes, &cascaded, ctx.paffine(), ctx.source_graphic().width(), ctx.source_graphic().height(), )?; let surface = ctx .source_graphic() .paint_image(bounds, &image, None, interpolation)?; Ok(surface) } /// Renders the filter if the source is an external image. fn render_external_image( &self, ctx: &FilterContext, acquired_nodes: &mut AcquiredNodes<'_>, draw_ctx: &DrawingCtx, bounds: &Bounds, url: &str, ) -> Result { match acquired_nodes.lookup_resource(url) { Ok(Resource::Image(surface)) => { self.render_surface_from_raster_image(&surface, ctx, bounds) } Ok(Resource::Document(document)) => { self.render_surface_from_svg(&document, ctx, bounds, draw_ctx) } Err(e) => { rsvg_log!( draw_ctx.session(), "could not load image \"{}\" for feImage: {}", url, e ); Err(FilterError::InvalidInput) } } } fn render_surface_from_raster_image( &self, image: &SharedImageSurface, ctx: &FilterContext, bounds: &Bounds, ) -> Result { let rect = self.aspect.compute( &ViewBox::from(Rect::from_size( f64::from(image.width()), f64::from(image.height()), )), &bounds.unclipped, ); // FIXME: overflow is not used but it should be // let overflow = self.feimage_values.overflow(); let interpolation = Interpolation::from(self.feimage_values.image_rendering()); let surface = ctx.source_graphic() .paint_image(bounds.clipped, image, Some(rect), interpolation)?; Ok(surface) } fn render_surface_from_svg( &self, document: &Document, ctx: &FilterContext, bounds: &Bounds, draw_ctx: &DrawingCtx, ) -> Result { // Strategy: // // Render the document at the size needed for the filter primitive // subregion, and then paste that as if we were handling the case for a raster imge. // // Note that for feImage, x/y/width/height are *attributes*, not the geometry // properties from the normal element , and have special handling: // // - They don't take "auto" as a value. The defaults are "0 0 100% 100%" but those // are with respect to the filter primitive subregion. let x = bounds.x.unwrap_or(0.0); let y = bounds.y.unwrap_or(0.0); let w = bounds.width.unwrap_or(1.0); // default is 100% let h = bounds.height.unwrap_or(1.0); // https://www.w3.org/TR/filter-effects/#FilterPrimitiveSubRegion // "If the filter primitive subregion has a negative or zero width or height, the // effect of the filter primitive is disabled." if w <= 0.0 || h < 0.0 { // In this case just return an empty image the size of the SourceGraphic return Ok(SharedImageSurface::empty( ctx.source_graphic().width(), ctx.source_graphic().height(), SurfaceType::SRgb, )?); } let dest_rect = Rect { x0: bounds.clipped.x0 + bounds.clipped.width() * x, y0: bounds.clipped.y0 + bounds.clipped.height() * y, x1: bounds.clipped.x0 + bounds.clipped.width() * w, y1: bounds.clipped.y0 + bounds.clipped.height() * h, }; let dest_size = dest_rect.size(); let surface_dest_rect = Rect::from_size(dest_size.0, dest_size.1); // We use ceil() to avoid chopping off the last pixel if it is partially covered. let surface_width = checked_i32(dest_size.0.ceil())?; let surface_height = checked_i32(dest_size.1.ceil())?; let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, surface_width, surface_height)?; { let cr = cairo::Context::new(&surface)?; let options = draw_ctx.rendering_options(SvgNesting::ReferencedFromImageElement); document.render_document( draw_ctx.session(), &cr, &cairo::Rectangle::from(surface_dest_rect), &options, )?; } // Now paste that image as a normal raster image let surface = SharedImageSurface::wrap(surface, SurfaceType::SRgb)?; self.render_surface_from_raster_image(&surface, ctx, bounds) } } impl FilterEffect for FeImage { fn resolve( &self, acquired_nodes: &mut AcquiredNodes<'_>, node: &Node, ) -> Result, FilterResolveError> { let cascaded = CascadedValues::new_from_node(node); let feimage_values = cascaded.get().clone(); let source = match self.params.href { None => Source::None, Some(ref s) => { if let Ok(node_id) = NodeId::parse(s) { acquired_nodes .acquire(&node_id) .map(|acquired| Source::Node(acquired.get().clone(), s.clone())) .unwrap_or(Source::None) } else { Source::ExternalImage(s.to_string()) } } }; Ok(vec![ResolvedPrimitive { primitive: self.base.clone(), params: PrimitiveParams::Image(Image { aspect: self.params.aspect, source, feimage_values: Box::new(feimage_values), }), }]) } } librsvg-2.59.0/src/filters/lighting.rs000064400000000000000000001123001046102023000160000ustar 00000000000000//! Lighting filters and light nodes. use cssparser::{Color, RGBA}; use float_cmp::approx_eq; use markup5ever::{expanded_name, local_name, namespace_url, ns}; use nalgebra::{Vector2, Vector3}; use num_traits::identities::Zero; use rayon::prelude::*; use std::cmp::max; use crate::color::color_to_rgba; use crate::document::AcquiredNodes; use crate::drawing_ctx::DrawingCtx; use crate::element::{set_attribute, ElementData, ElementTrait}; use crate::filters::{ bounds::BoundsBuilder, context::{FilterContext, FilterOutput}, FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, ResolvedPrimitive, }; use crate::node::{CascadedValues, Node, NodeBorrow}; use crate::paint_server::resolve_color; use crate::parsers::{NonNegative, NumberOptionalNumber, ParseValue}; use crate::properties::ColorInterpolationFilters; use crate::rect::IRect; use crate::rsvg_log; use crate::session::Session; use crate::surface_utils::{ shared_surface::{ExclusiveImageSurface, SharedImageSurface, SurfaceType}, ImageSurfaceDataExt, Pixel, }; use crate::transform::Transform; use crate::unit_interval::UnitInterval; use crate::util::clamp; use crate::xml::Attributes; /// The `feDiffuseLighting` filter primitives. #[derive(Default)] pub struct FeDiffuseLighting { base: Primitive, params: DiffuseLightingParams, } #[derive(Clone)] pub struct DiffuseLightingParams { in1: Input, surface_scale: f64, kernel_unit_length: Option<(f64, f64)>, diffuse_constant: NonNegative, } impl Default for DiffuseLightingParams { fn default() -> Self { Self { in1: Default::default(), surface_scale: 1.0, kernel_unit_length: None, diffuse_constant: NonNegative(1.0), } } } /// The `feSpecularLighting` filter primitives. #[derive(Default)] pub struct FeSpecularLighting { base: Primitive, params: SpecularLightingParams, } #[derive(Clone)] pub struct SpecularLightingParams { in1: Input, surface_scale: f64, kernel_unit_length: Option<(f64, f64)>, specular_constant: NonNegative, specular_exponent: f64, } impl Default for SpecularLightingParams { fn default() -> Self { Self { in1: Default::default(), surface_scale: 1.0, kernel_unit_length: None, specular_constant: NonNegative(1.0), specular_exponent: 1.0, } } } /// Resolved `feDiffuseLighting` primitive for rendering. pub struct DiffuseLighting { params: DiffuseLightingParams, light: Light, } /// Resolved `feSpecularLighting` primitive for rendering. pub struct SpecularLighting { params: SpecularLightingParams, light: Light, } /// A light source before applying affine transformations, straight out of the SVG. #[derive(Debug, PartialEq)] enum UntransformedLightSource { Distant(FeDistantLight), Point(FePointLight), Spot(FeSpotLight), } /// A light source with affine transformations applied. enum LightSource { Distant { azimuth: f64, elevation: f64, }, Point { origin: Vector3, }, Spot { origin: Vector3, direction: Vector3, specular_exponent: f64, limiting_cone_angle: Option, }, } impl UntransformedLightSource { fn transform(&self, paffine: Transform) -> LightSource { match *self { UntransformedLightSource::Distant(ref l) => l.transform(), UntransformedLightSource::Point(ref l) => l.transform(paffine), UntransformedLightSource::Spot(ref l) => l.transform(paffine), } } } struct Light { source: UntransformedLightSource, lighting_color: Color, color_interpolation_filters: ColorInterpolationFilters, } /// Returns the color and unit (or null) vector from the image sample to the light. #[inline] fn color_and_vector( lighting_color: &RGBA, source: &LightSource, x: f64, y: f64, z: f64, ) -> (cssparser::RGBA, Vector3) { let vector = match *source { LightSource::Distant { azimuth, elevation } => { let azimuth = azimuth.to_radians(); let elevation = elevation.to_radians(); Vector3::new( azimuth.cos() * elevation.cos(), azimuth.sin() * elevation.cos(), elevation.sin(), ) } LightSource::Point { origin } | LightSource::Spot { origin, .. } => { let mut v = origin - Vector3::new(x, y, z); let _ = v.try_normalize_mut(0.0); v } }; let color = match *source { LightSource::Spot { direction, specular_exponent, limiting_cone_angle, .. } => { let transparent_color = cssparser::RGBA::new(Some(0), Some(0), Some(0), Some(0.0)); let minus_l_dot_s = -vector.dot(&direction); match limiting_cone_angle { _ if minus_l_dot_s <= 0.0 => transparent_color, Some(a) if minus_l_dot_s < a.to_radians().cos() => transparent_color, _ => { let factor = minus_l_dot_s.powf(specular_exponent); let compute = |x| (clamp(f64::from(x) * factor, 0.0, 255.0) + 0.5) as u8; cssparser::RGBA { red: Some(compute(lighting_color.red.unwrap_or(0))), green: Some(compute(lighting_color.green.unwrap_or(0))), blue: Some(compute(lighting_color.blue.unwrap_or(0))), alpha: Some(1.0), } } } } _ => *lighting_color, }; (color, vector) } #[derive(Clone, Debug, Default, PartialEq)] pub struct FeDistantLight { azimuth: f64, elevation: f64, } impl FeDistantLight { fn transform(&self) -> LightSource { LightSource::Distant { azimuth: self.azimuth, elevation: self.elevation, } } } impl ElementTrait for FeDistantLight { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "azimuth") => { set_attribute(&mut self.azimuth, attr.parse(value), session) } expanded_name!("", "elevation") => { set_attribute(&mut self.elevation, attr.parse(value), session) } _ => (), } } } } #[derive(Clone, Debug, Default, PartialEq)] pub struct FePointLight { x: f64, y: f64, z: f64, } impl FePointLight { fn transform(&self, paffine: Transform) -> LightSource { let (x, y) = paffine.transform_point(self.x, self.y); let z = transform_dist(paffine, self.z); LightSource::Point { origin: Vector3::new(x, y, z), } } } impl ElementTrait for FePointLight { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session), expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session), expanded_name!("", "z") => set_attribute(&mut self.z, attr.parse(value), session), _ => (), } } } } #[derive(Clone, Debug, PartialEq)] pub struct FeSpotLight { x: f64, y: f64, z: f64, points_at_x: f64, points_at_y: f64, points_at_z: f64, specular_exponent: f64, limiting_cone_angle: Option, } // We need this because, per the spec, the initial values for all fields are 0.0 // except for specular_exponent, which is 1. impl Default for FeSpotLight { fn default() -> FeSpotLight { FeSpotLight { x: 0.0, y: 0.0, z: 0.0, points_at_x: 0.0, points_at_y: 0.0, points_at_z: 0.0, specular_exponent: 1.0, limiting_cone_angle: None, } } } impl FeSpotLight { fn transform(&self, paffine: Transform) -> LightSource { let (x, y) = paffine.transform_point(self.x, self.y); let z = transform_dist(paffine, self.z); let (points_at_x, points_at_y) = paffine.transform_point(self.points_at_x, self.points_at_y); let points_at_z = transform_dist(paffine, self.points_at_z); let origin = Vector3::new(x, y, z); let mut direction = Vector3::new(points_at_x, points_at_y, points_at_z) - origin; let _ = direction.try_normalize_mut(0.0); LightSource::Spot { origin, direction, specular_exponent: self.specular_exponent, limiting_cone_angle: self.limiting_cone_angle, } } } impl ElementTrait for FeSpotLight { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session), expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session), expanded_name!("", "z") => set_attribute(&mut self.z, attr.parse(value), session), expanded_name!("", "pointsAtX") => { set_attribute(&mut self.points_at_x, attr.parse(value), session) } expanded_name!("", "pointsAtY") => { set_attribute(&mut self.points_at_y, attr.parse(value), session) } expanded_name!("", "pointsAtZ") => { set_attribute(&mut self.points_at_z, attr.parse(value), session) } expanded_name!("", "specularExponent") => { set_attribute(&mut self.specular_exponent, attr.parse(value), session); } expanded_name!("", "limitingConeAngle") => { set_attribute(&mut self.limiting_cone_angle, attr.parse(value), session); } _ => (), } } } } /// Applies the `primitiveUnits` coordinate transformation to a non-x or y distance. #[inline] fn transform_dist(t: Transform, d: f64) -> f64 { d * (t.xx.powi(2) + t.yy.powi(2)).sqrt() / std::f64::consts::SQRT_2 } impl ElementTrait for FeDiffuseLighting { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { self.params.in1 = self.base.parse_one_input(attrs, session); for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "surfaceScale") => { set_attribute(&mut self.params.surface_scale, attr.parse(value), session); } expanded_name!("", "kernelUnitLength") => { let v: Result, _> = attr.parse(value); match v { Ok(NumberOptionalNumber(x, y)) => { self.params.kernel_unit_length = Some((x, y)); } Err(e) => { rsvg_log!(session, "ignoring attribute with invalid value: {}", e); } } } expanded_name!("", "diffuseConstant") => { set_attribute( &mut self.params.diffuse_constant, attr.parse(value), session, ); } _ => (), } } } } impl DiffuseLighting { #[inline] fn compute_factor(&self, normal: Normal, light_vector: Vector3) -> f64 { let k = if normal.normal.is_zero() { // Common case of (0, 0, 1) normal. light_vector.z } else { let mut n = normal .normal .map(|x| f64::from(x) * self.params.surface_scale / 255.); n.component_mul_assign(&normal.factor); let normal = Vector3::new(n.x, n.y, 1.0); normal.dot(&light_vector) / normal.norm() }; self.params.diffuse_constant.0 * k } } impl ElementTrait for FeSpecularLighting { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { self.params.in1 = self.base.parse_one_input(attrs, session); for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "surfaceScale") => { set_attribute(&mut self.params.surface_scale, attr.parse(value), session); } expanded_name!("", "kernelUnitLength") => { let v: Result, _> = attr.parse(value); match v { Ok(NumberOptionalNumber(x, y)) => { self.params.kernel_unit_length = Some((x, y)); } Err(e) => { rsvg_log!(session, "ignoring attribute with invalid value: {}", e); } } } expanded_name!("", "specularConstant") => { set_attribute( &mut self.params.specular_constant, attr.parse(value), session, ); } expanded_name!("", "specularExponent") => { set_attribute( &mut self.params.specular_exponent, attr.parse(value), session, ); } _ => (), } } } } impl SpecularLighting { #[inline] fn compute_factor(&self, normal: Normal, light_vector: Vector3) -> f64 { let h = light_vector + Vector3::new(0.0, 0.0, 1.0); let h_norm = h.norm(); if h_norm == 0.0 { return 0.0; } let n_dot_h = if normal.normal.is_zero() { // Common case of (0, 0, 1) normal. h.z / h_norm } else { let mut n = normal .normal .map(|x| f64::from(x) * self.params.surface_scale / 255.); n.component_mul_assign(&normal.factor); let normal = Vector3::new(n.x, n.y, 1.0); normal.dot(&h) / normal.norm() / h_norm }; if approx_eq!(f64, self.params.specular_exponent, 1.0) { self.params.specular_constant.0 * n_dot_h } else { self.params.specular_constant.0 * n_dot_h.powf(self.params.specular_exponent) } } } macro_rules! impl_lighting_filter { ($lighting_type:ty, $params_name:ident, $alpha_func:ident) => { impl $params_name { pub fn render( &self, bounds_builder: BoundsBuilder, ctx: &FilterContext, acquired_nodes: &mut AcquiredNodes<'_>, draw_ctx: &mut DrawingCtx, ) -> Result { let input_1 = ctx.get_input( acquired_nodes, draw_ctx, &self.params.in1, self.light.color_interpolation_filters, )?; let mut bounds: IRect = bounds_builder .add_input(&input_1) .compute(ctx) .clipped .into(); let original_bounds = bounds; let scale = self .params .kernel_unit_length .and_then(|(x, y)| { if x <= 0.0 || y <= 0.0 { None } else { Some((x, y)) } }) .map(|(dx, dy)| ctx.paffine().transform_distance(dx, dy)); let mut input_surface = input_1.surface().clone(); if let Some((ox, oy)) = scale { // Scale the input surface to match kernel_unit_length. let (new_surface, new_bounds) = input_surface.scale(bounds, 1.0 / ox, 1.0 / oy)?; input_surface = new_surface; bounds = new_bounds; } let (bounds_w, bounds_h) = bounds.size(); // Check if the surface is too small for normal computation. This case is // unspecified; WebKit doesn't render anything in this case. if bounds_w < 2 || bounds_h < 2 { return Err(FilterError::LightingInputTooSmall); } let (ox, oy) = scale.unwrap_or((1.0, 1.0)); let source = self.light.source.transform(ctx.paffine()); let mut surface = ExclusiveImageSurface::new( input_surface.width(), input_surface.height(), SurfaceType::from(self.light.color_interpolation_filters), )?; let lighting_color = color_to_rgba(&self.light.lighting_color); { let output_stride = surface.stride() as usize; let mut output_data = surface.data(); let output_slice = &mut *output_data; let compute_output_pixel = |output_slice: &mut [u8], base_y, x, y, normal: Normal| { let pixel = input_surface.get_pixel(x, y); let scaled_x = f64::from(x) * ox; let scaled_y = f64::from(y) * oy; let z = f64::from(pixel.a) / 255.0 * self.params.surface_scale; let (color, vector) = color_and_vector(&lighting_color, &source, scaled_x, scaled_y, z); // compute the factor just once for the three colors let factor = self.compute_factor(normal, vector); let compute = |x| (clamp(factor * f64::from(x), 0.0, 255.0) + 0.5) as u8; let r = compute(color.red.unwrap_or(0)); let g = compute(color.green.unwrap_or(0)); let b = compute(color.blue.unwrap_or(0)); let a = $alpha_func(r, g, b); let output_pixel = Pixel { r, g, b, a }; output_slice.set_pixel(output_stride, output_pixel, x, y - base_y); }; // Top left. compute_output_pixel( output_slice, 0, bounds.x0 as u32, bounds.y0 as u32, Normal::top_left(&input_surface, bounds), ); // Top right. compute_output_pixel( output_slice, 0, bounds.x1 as u32 - 1, bounds.y0 as u32, Normal::top_right(&input_surface, bounds), ); // Bottom left. compute_output_pixel( output_slice, 0, bounds.x0 as u32, bounds.y1 as u32 - 1, Normal::bottom_left(&input_surface, bounds), ); // Bottom right. compute_output_pixel( output_slice, 0, bounds.x1 as u32 - 1, bounds.y1 as u32 - 1, Normal::bottom_right(&input_surface, bounds), ); if bounds_w >= 3 { // Top row. for x in bounds.x0 as u32 + 1..bounds.x1 as u32 - 1 { compute_output_pixel( output_slice, 0, x, bounds.y0 as u32, Normal::top_row(&input_surface, bounds, x), ); } // Bottom row. for x in bounds.x0 as u32 + 1..bounds.x1 as u32 - 1 { compute_output_pixel( output_slice, 0, x, bounds.y1 as u32 - 1, Normal::bottom_row(&input_surface, bounds, x), ); } } if bounds_h >= 3 { // Left column. for y in bounds.y0 as u32 + 1..bounds.y1 as u32 - 1 { compute_output_pixel( output_slice, 0, bounds.x0 as u32, y, Normal::left_column(&input_surface, bounds, y), ); } // Right column. for y in bounds.y0 as u32 + 1..bounds.y1 as u32 - 1 { compute_output_pixel( output_slice, 0, bounds.x1 as u32 - 1, y, Normal::right_column(&input_surface, bounds, y), ); } } if bounds_w >= 3 && bounds_h >= 3 { // Interior pixels. let first_row = bounds.y0 as u32 + 1; let one_past_last_row = bounds.y1 as u32 - 1; let first_pixel = (first_row as usize) * output_stride; let one_past_last_pixel = (one_past_last_row as usize) * output_stride; output_slice[first_pixel..one_past_last_pixel] .par_chunks_mut(output_stride) .zip(first_row..one_past_last_row) .for_each(|(slice, y)| { for x in bounds.x0 as u32 + 1..bounds.x1 as u32 - 1 { compute_output_pixel( slice, y, x, y, Normal::interior(&input_surface, bounds, x, y), ); } }); } } let mut surface = surface.share()?; if let Some((ox, oy)) = scale { // Scale the output surface back. surface = surface.scale_to( ctx.source_graphic().width(), ctx.source_graphic().height(), original_bounds, ox, oy, )?; bounds = original_bounds; } Ok(FilterOutput { surface, bounds }) } } impl FilterEffect for $lighting_type { fn resolve( &self, _acquired_nodes: &mut AcquiredNodes<'_>, node: &Node, ) -> Result, FilterResolveError> { let mut sources = node.children().rev().filter(|c| { c.is_element() && matches!( *c.borrow_element_data(), ElementData::FeDistantLight(_) | ElementData::FePointLight(_) | ElementData::FeSpotLight(_) ) }); let source_node = sources.next(); if source_node.is_none() || sources.next().is_some() { return Err(FilterResolveError::InvalidLightSourceCount); } let source_node = source_node.unwrap(); let source = match &*source_node.borrow_element_data() { ElementData::FeDistantLight(l) => { UntransformedLightSource::Distant((**l).clone()) } ElementData::FePointLight(l) => UntransformedLightSource::Point((**l).clone()), ElementData::FeSpotLight(l) => UntransformedLightSource::Spot((**l).clone()), _ => unreachable!(), }; let cascaded = CascadedValues::new_from_node(node); let values = cascaded.get(); Ok(vec![ResolvedPrimitive { primitive: self.base.clone(), params: PrimitiveParams::$params_name($params_name { params: self.params.clone(), light: Light { source, lighting_color: resolve_color( &values.lighting_color().0, UnitInterval::clamp(1.0), &values.color().0, ), color_interpolation_filters: values.color_interpolation_filters(), }, }), }]) } } }; } const fn diffuse_alpha(_r: u8, _g: u8, _b: u8) -> u8 { 255 } fn specular_alpha(r: u8, g: u8, b: u8) -> u8 { max(max(r, g), b) } impl_lighting_filter!(FeDiffuseLighting, DiffuseLighting, diffuse_alpha); impl_lighting_filter!(FeSpecularLighting, SpecularLighting, specular_alpha); /// 2D normal and factor stored separately. /// /// The normal needs to be multiplied by `surface_scale * factor / 255` and /// normalized with 1 as the z component. /// pub for the purpose of accessing this from benchmarks. #[derive(Debug, Clone, Copy)] pub struct Normal { pub factor: Vector2, pub normal: Vector2, } impl Normal { #[inline] fn new(factor_x: f64, nx: i16, factor_y: f64, ny: i16) -> Normal { // Negative nx and ny to account for the different coordinate system. Normal { factor: Vector2::new(factor_x, factor_y), normal: Vector2::new(-nx, -ny), } } /// Computes and returns the normal vector for the top left pixel for light filters. #[inline] pub fn top_left(surface: &SharedImageSurface, bounds: IRect) -> Normal { // Surface needs to be at least 2×2. assert!(bounds.width() >= 2); assert!(bounds.height() >= 2); let get = |x, y| i16::from(surface.get_pixel(x, y).a); let (x, y) = (bounds.x0 as u32, bounds.y0 as u32); let center = get(x, y); let right = get(x + 1, y); let bottom = get(x, y + 1); let bottom_right = get(x + 1, y + 1); Self::new( 2. / 3., -2 * center + 2 * right - bottom + bottom_right, 2. / 3., -2 * center - right + 2 * bottom + bottom_right, ) } /// Computes and returns the normal vector for the top row pixels for light filters. #[inline] pub fn top_row(surface: &SharedImageSurface, bounds: IRect, x: u32) -> Normal { assert!(x as i32 > bounds.x0); assert!((x as i32) + 1 < bounds.x1); assert!(bounds.height() >= 2); let get = |x, y| i16::from(surface.get_pixel(x, y).a); let y = bounds.y0 as u32; let left = get(x - 1, y); let center = get(x, y); let right = get(x + 1, y); let bottom_left = get(x - 1, y + 1); let bottom = get(x, y + 1); let bottom_right = get(x + 1, y + 1); Self::new( 1. / 3., -2 * left + 2 * right - bottom_left + bottom_right, 1. / 2., -left - 2 * center - right + bottom_left + 2 * bottom + bottom_right, ) } /// Computes and returns the normal vector for the top right pixel for light filters. #[inline] pub fn top_right(surface: &SharedImageSurface, bounds: IRect) -> Normal { // Surface needs to be at least 2×2. assert!(bounds.width() >= 2); assert!(bounds.height() >= 2); let get = |x, y| i16::from(surface.get_pixel(x, y).a); let (x, y) = (bounds.x1 as u32 - 1, bounds.y0 as u32); let left = get(x - 1, y); let center = get(x, y); let bottom_left = get(x - 1, y + 1); let bottom = get(x, y + 1); Self::new( 2. / 3., -2 * left + 2 * center - bottom_left + bottom, 2. / 3., -left - 2 * center + bottom_left + 2 * bottom, ) } /// Computes and returns the normal vector for the left column pixels for light filters. #[inline] pub fn left_column(surface: &SharedImageSurface, bounds: IRect, y: u32) -> Normal { assert!(y as i32 > bounds.y0); assert!((y as i32) + 1 < bounds.y1); assert!(bounds.width() >= 2); let get = |x, y| i16::from(surface.get_pixel(x, y).a); let x = bounds.x0 as u32; let top = get(x, y - 1); let top_right = get(x + 1, y - 1); let center = get(x, y); let right = get(x + 1, y); let bottom = get(x, y + 1); let bottom_right = get(x + 1, y + 1); Self::new( 1. / 2., -top + top_right - 2 * center + 2 * right - bottom + bottom_right, 1. / 3., -2 * top - top_right + 2 * bottom + bottom_right, ) } /// Computes and returns the normal vector for the interior pixels for light filters. #[inline] pub fn interior(surface: &SharedImageSurface, bounds: IRect, x: u32, y: u32) -> Normal { assert!(x as i32 > bounds.x0); assert!((x as i32) + 1 < bounds.x1); assert!(y as i32 > bounds.y0); assert!((y as i32) + 1 < bounds.y1); let get = |x, y| i16::from(surface.get_pixel(x, y).a); let top_left = get(x - 1, y - 1); let top = get(x, y - 1); let top_right = get(x + 1, y - 1); let left = get(x - 1, y); let right = get(x + 1, y); let bottom_left = get(x - 1, y + 1); let bottom = get(x, y + 1); let bottom_right = get(x + 1, y + 1); Self::new( 1. / 4., -top_left + top_right - 2 * left + 2 * right - bottom_left + bottom_right, 1. / 4., -top_left - 2 * top - top_right + bottom_left + 2 * bottom + bottom_right, ) } /// Computes and returns the normal vector for the right column pixels for light filters. #[inline] pub fn right_column(surface: &SharedImageSurface, bounds: IRect, y: u32) -> Normal { assert!(y as i32 > bounds.y0); assert!((y as i32) + 1 < bounds.y1); assert!(bounds.width() >= 2); let get = |x, y| i16::from(surface.get_pixel(x, y).a); let x = bounds.x1 as u32 - 1; let top_left = get(x - 1, y - 1); let top = get(x, y - 1); let left = get(x - 1, y); let center = get(x, y); let bottom_left = get(x - 1, y + 1); let bottom = get(x, y + 1); Self::new( 1. / 2., -top_left + top - 2 * left + 2 * center - bottom_left + bottom, 1. / 3., -top_left - 2 * top + bottom_left + 2 * bottom, ) } /// Computes and returns the normal vector for the bottom left pixel for light filters. #[inline] pub fn bottom_left(surface: &SharedImageSurface, bounds: IRect) -> Normal { // Surface needs to be at least 2×2. assert!(bounds.width() >= 2); assert!(bounds.height() >= 2); let get = |x, y| i16::from(surface.get_pixel(x, y).a); let (x, y) = (bounds.x0 as u32, bounds.y1 as u32 - 1); let top = get(x, y - 1); let top_right = get(x + 1, y - 1); let center = get(x, y); let right = get(x + 1, y); Self::new( 2. / 3., -top + top_right - 2 * center + 2 * right, 2. / 3., -2 * top - top_right + 2 * center + right, ) } /// Computes and returns the normal vector for the bottom row pixels for light filters. #[inline] pub fn bottom_row(surface: &SharedImageSurface, bounds: IRect, x: u32) -> Normal { assert!(x as i32 > bounds.x0); assert!((x as i32) + 1 < bounds.x1); assert!(bounds.height() >= 2); let get = |x, y| i16::from(surface.get_pixel(x, y).a); let y = bounds.y1 as u32 - 1; let top_left = get(x - 1, y - 1); let top = get(x, y - 1); let top_right = get(x + 1, y - 1); let left = get(x - 1, y); let center = get(x, y); let right = get(x + 1, y); Self::new( 1. / 3., -top_left + top_right - 2 * left + 2 * right, 1. / 2., -top_left - 2 * top - top_right + left + 2 * center + right, ) } /// Computes and returns the normal vector for the bottom right pixel for light filters. #[inline] pub fn bottom_right(surface: &SharedImageSurface, bounds: IRect) -> Normal { // Surface needs to be at least 2×2. assert!(bounds.width() >= 2); assert!(bounds.height() >= 2); let get = |x, y| i16::from(surface.get_pixel(x, y).a); let (x, y) = (bounds.x1 as u32 - 1, bounds.y1 as u32 - 1); let top_left = get(x - 1, y - 1); let top = get(x, y - 1); let left = get(x - 1, y); let center = get(x, y); Self::new( 2. / 3., -top_left + top - 2 * left + 2 * center, 2. / 3., -top_left - 2 * top + left + 2 * center, ) } } #[cfg(test)] mod tests { use super::*; use crate::borrow_element_as; use crate::document::Document; #[test] fn extracts_light_source() { let document = Document::load_from_bytes( br#" "#, ); let mut acquired_nodes = AcquiredNodes::new(&document); let node = document.lookup_internal_node("diffuse_distant").unwrap(); let lighting = borrow_element_as!(node, FeDiffuseLighting); let resolved = lighting.resolve(&mut acquired_nodes, &node).unwrap(); let ResolvedPrimitive { params, .. } = resolved.first().unwrap(); let diffuse_lighting = match params { PrimitiveParams::DiffuseLighting(l) => l, _ => unreachable!(), }; assert_eq!( diffuse_lighting.light.source, UntransformedLightSource::Distant(FeDistantLight { azimuth: 0.0, elevation: 45.0, }) ); let node = document.lookup_internal_node("specular_point").unwrap(); let lighting = borrow_element_as!(node, FeSpecularLighting); let resolved = lighting.resolve(&mut acquired_nodes, &node).unwrap(); let ResolvedPrimitive { params, .. } = resolved.first().unwrap(); let specular_lighting = match params { PrimitiveParams::SpecularLighting(l) => l, _ => unreachable!(), }; assert_eq!( specular_lighting.light.source, UntransformedLightSource::Point(FePointLight { x: 1.0, y: 2.0, z: 3.0, }) ); let node = document.lookup_internal_node("diffuse_spot").unwrap(); let lighting = borrow_element_as!(node, FeDiffuseLighting); let resolved = lighting.resolve(&mut acquired_nodes, &node).unwrap(); let ResolvedPrimitive { params, .. } = resolved.first().unwrap(); let diffuse_lighting = match params { PrimitiveParams::DiffuseLighting(l) => l, _ => unreachable!(), }; assert_eq!( diffuse_lighting.light.source, UntransformedLightSource::Spot(FeSpotLight { x: 1.0, y: 2.0, z: 3.0, points_at_x: 4.0, points_at_y: 5.0, points_at_z: 6.0, specular_exponent: 7.0, limiting_cone_angle: Some(8.0), }) ); } } librsvg-2.59.0/src/filters/merge.rs000064400000000000000000000145371046102023000153070ustar 00000000000000use markup5ever::{expanded_name, local_name, namespace_url, ns}; use crate::document::AcquiredNodes; use crate::drawing_ctx::DrawingCtx; use crate::element::{set_attribute, ElementData, ElementTrait}; use crate::node::{CascadedValues, Node, NodeBorrow}; use crate::parsers::ParseValue; use crate::properties::ColorInterpolationFilters; use crate::rect::IRect; use crate::session::Session; use crate::surface_utils::shared_surface::{Operator, SharedImageSurface, SurfaceType}; use crate::xml::Attributes; use super::bounds::BoundsBuilder; use super::context::{FilterContext, FilterOutput}; use super::{ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, ResolvedPrimitive, }; /// The `feMerge` filter primitive. pub struct FeMerge { base: Primitive, } /// The `` element. #[derive(Clone, Default)] pub struct FeMergeNode { in1: Input, } /// Resolved `feMerge` primitive for rendering. pub struct Merge { pub merge_nodes: Vec, } /// Resolved `feMergeNode` for rendering. #[derive(Debug, Default, PartialEq)] pub struct MergeNode { pub in1: Input, pub color_interpolation_filters: ColorInterpolationFilters, } impl Default for FeMerge { /// Constructs a new `Merge` with empty properties. #[inline] fn default() -> FeMerge { FeMerge { base: Default::default(), } } } impl ElementTrait for FeMerge { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { self.base.parse_no_inputs(attrs, session); } } impl ElementTrait for FeMergeNode { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { if let expanded_name!("", "in") = attr.expanded() { set_attribute(&mut self.in1, attr.parse(value), session); } } } } impl MergeNode { fn render( &self, ctx: &FilterContext, acquired_nodes: &mut AcquiredNodes<'_>, draw_ctx: &mut DrawingCtx, bounds: IRect, output_surface: Option, ) -> Result { let input = ctx.get_input( acquired_nodes, draw_ctx, &self.in1, self.color_interpolation_filters, )?; if output_surface.is_none() { return Ok(input.surface().clone()); } input .surface() .compose(&output_surface.unwrap(), bounds, Operator::Over) .map_err(FilterError::CairoError) } } impl Merge { pub fn render( &self, bounds_builder: BoundsBuilder, ctx: &FilterContext, acquired_nodes: &mut AcquiredNodes<'_>, draw_ctx: &mut DrawingCtx, ) -> Result { // Compute the filter bounds, taking each feMergeNode's input into account. let mut bounds_builder = bounds_builder; for merge_node in &self.merge_nodes { let input = ctx.get_input( acquired_nodes, draw_ctx, &merge_node.in1, merge_node.color_interpolation_filters, )?; bounds_builder = bounds_builder.add_input(&input); } let bounds: IRect = bounds_builder.compute(ctx).clipped.into(); // Now merge them all. let mut output_surface = None; for merge_node in &self.merge_nodes { output_surface = merge_node .render(ctx, acquired_nodes, draw_ctx, bounds, output_surface) .ok(); } let surface = match output_surface { Some(s) => s, None => SharedImageSurface::empty( ctx.source_graphic().width(), ctx.source_graphic().height(), SurfaceType::AlphaOnly, )?, }; Ok(FilterOutput { surface, bounds }) } } impl FilterEffect for FeMerge { fn resolve( &self, _acquired_nodes: &mut AcquiredNodes<'_>, node: &Node, ) -> Result, FilterResolveError> { Ok(vec![ResolvedPrimitive { primitive: self.base.clone(), params: PrimitiveParams::Merge(Merge { merge_nodes: resolve_merge_nodes(node)?, }), }]) } } /// Takes a feMerge and walks its children to produce a list of feMergeNode arguments. fn resolve_merge_nodes(node: &Node) -> Result, FilterResolveError> { let mut merge_nodes = Vec::new(); for child in node.children().filter(|c| c.is_element()) { let cascaded = CascadedValues::new_from_node(&child); let values = cascaded.get(); if let ElementData::FeMergeNode(merge_node) = &*child.borrow_element_data() { merge_nodes.push(MergeNode { in1: merge_node.in1.clone(), color_interpolation_filters: values.color_interpolation_filters(), }); } } Ok(merge_nodes) } #[cfg(test)] mod tests { use super::*; use crate::borrow_element_as; use crate::document::Document; #[test] fn extracts_parameters() { let document = Document::load_from_bytes( br#" "#, ); let mut acquired_nodes = AcquiredNodes::new(&document); let node = document.lookup_internal_node("merge").unwrap(); let merge = borrow_element_as!(node, FeMerge); let resolved = merge.resolve(&mut acquired_nodes, &node).unwrap(); let ResolvedPrimitive { params, .. } = resolved.first().unwrap(); let params = match params { PrimitiveParams::Merge(m) => m, _ => unreachable!(), }; assert_eq!( ¶ms.merge_nodes[..], vec![ MergeNode { in1: Input::SourceGraphic, color_interpolation_filters: Default::default(), }, MergeNode { in1: Input::SourceAlpha, color_interpolation_filters: ColorInterpolationFilters::Srgb, }, ] ); } } librsvg-2.59.0/src/filters/mod.rs000064400000000000000000000331041046102023000147560ustar 00000000000000//! Entry point for the CSS filters infrastructure. use cssparser::{BasicParseError, Parser}; use markup5ever::{expanded_name, local_name, namespace_url, ns}; use std::rc::Rc; use std::time::Instant; use crate::bbox::BoundingBox; use crate::document::AcquiredNodes; use crate::drawing_ctx::DrawingCtx; use crate::element::{set_attribute, ElementTrait}; use crate::error::{InternalRenderingError, ParseError}; use crate::filter::UserSpaceFilter; use crate::length::*; use crate::node::Node; use crate::paint_server::UserSpacePaintSource; use crate::parse_identifiers; use crate::parsers::{CustomIdent, Parse, ParseValue}; use crate::properties::ColorInterpolationFilters; use crate::rsvg_log; use crate::session::Session; use crate::surface_utils::{ shared_surface::{SharedImageSurface, SurfaceType}, EdgeMode, }; use crate::transform::Transform; use crate::xml::Attributes; mod bounds; use self::bounds::BoundsBuilder; pub mod context; use self::context::{FilterContext, FilterOutput, FilterResult}; mod error; use self::error::FilterError; pub use self::error::FilterResolveError; /// A filter primitive interface. pub trait FilterEffect: ElementTrait { fn resolve( &self, acquired_nodes: &mut AcquiredNodes<'_>, node: &Node, ) -> Result, FilterResolveError>; } pub mod blend; pub mod color_matrix; pub mod component_transfer; pub mod composite; pub mod convolve_matrix; pub mod displacement_map; pub mod drop_shadow; pub mod flood; pub mod gaussian_blur; pub mod image; pub mod lighting; pub mod merge; pub mod morphology; pub mod offset; pub mod tile; pub mod turbulence; pub struct FilterSpec { pub name: String, pub user_space_filter: UserSpaceFilter, pub primitives: Vec, } /// Resolved parameters for each filter primitive. /// /// These gather all the data that a primitive may need during rendering: /// the `feFoo` element's attributes, any computed values from its properties, /// and parameters extracted from the element's children (for example, /// `feMerge` gathers info from its `feMergNode` children). pub enum PrimitiveParams { Blend(blend::Blend), ColorMatrix(color_matrix::ColorMatrix), ComponentTransfer(component_transfer::ComponentTransfer), Composite(composite::Composite), ConvolveMatrix(convolve_matrix::ConvolveMatrix), DiffuseLighting(lighting::DiffuseLighting), DisplacementMap(displacement_map::DisplacementMap), Flood(flood::Flood), GaussianBlur(gaussian_blur::GaussianBlur), Image(image::Image), Merge(merge::Merge), Morphology(morphology::Morphology), Offset(offset::Offset), SpecularLighting(lighting::SpecularLighting), Tile(tile::Tile), Turbulence(turbulence::Turbulence), } impl PrimitiveParams { /// Returns a human-readable name for a primitive. #[rustfmt::skip] fn name(&self) -> &'static str { use PrimitiveParams::*; match self { Blend(..) => "feBlend", ColorMatrix(..) => "feColorMatrix", ComponentTransfer(..) => "feComponentTransfer", Composite(..) => "feComposite", ConvolveMatrix(..) => "feConvolveMatrix", DiffuseLighting(..) => "feDiffuseLighting", DisplacementMap(..) => "feDisplacementMap", Flood(..) => "feFlood", GaussianBlur(..) => "feGaussianBlur", Image(..) => "feImage", Merge(..) => "feMerge", Morphology(..) => "feMorphology", Offset(..) => "feOffset", SpecularLighting(..) => "feSpecularLighting", Tile(..) => "feTile", Turbulence(..) => "feTurbulence", } } } /// The base filter primitive node containing common properties. #[derive(Default, Clone)] pub struct Primitive { pub x: Option>, pub y: Option>, pub width: Option>, pub height: Option>, pub result: Option, } pub struct ResolvedPrimitive { pub primitive: Primitive, pub params: PrimitiveParams, } /// A fully resolved filter primitive in user-space coordinates. pub struct UserSpacePrimitive { x: Option, y: Option, width: Option, height: Option, result: Option, params: PrimitiveParams, } /// An enumeration of possible inputs for a filter primitive. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub enum Input { #[default] Unspecified, SourceGraphic, SourceAlpha, BackgroundImage, BackgroundAlpha, FillPaint, StrokePaint, FilterOutput(CustomIdent), } impl Parse for Input { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { parser .try_parse(|p| { parse_identifiers!( p, "SourceGraphic" => Input::SourceGraphic, "SourceAlpha" => Input::SourceAlpha, "BackgroundImage" => Input::BackgroundImage, "BackgroundAlpha" => Input::BackgroundAlpha, "FillPaint" => Input::FillPaint, "StrokePaint" => Input::StrokePaint, ) }) .or_else(|_: BasicParseError<'_>| { let ident = CustomIdent::parse(parser)?; Ok(Input::FilterOutput(ident)) }) } } impl ResolvedPrimitive { pub fn into_user_space(self, params: &NormalizeParams) -> UserSpacePrimitive { let x = self.primitive.x.map(|l| l.to_user(params)); let y = self.primitive.y.map(|l| l.to_user(params)); let width = self.primitive.width.map(|l| l.to_user(params)); let height = self.primitive.height.map(|l| l.to_user(params)); UserSpacePrimitive { x, y, width, height, result: self.primitive.result, params: self.params, } } } impl UserSpacePrimitive { /// Validates attributes and returns the `BoundsBuilder` for bounds computation. #[inline] fn get_bounds(&self, ctx: &FilterContext) -> BoundsBuilder { BoundsBuilder::new(self.x, self.y, self.width, self.height, ctx.paffine()) } } impl Primitive { fn parse_standard_attributes( &mut self, attrs: &Attributes, session: &Session, ) -> (Input, Input) { let mut input_1 = Input::Unspecified; let mut input_2 = Input::Unspecified; for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session), expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session), expanded_name!("", "width") => { set_attribute(&mut self.width, attr.parse(value), session) } expanded_name!("", "height") => { set_attribute(&mut self.height, attr.parse(value), session) } expanded_name!("", "result") => { set_attribute(&mut self.result, attr.parse(value), session) } expanded_name!("", "in") => set_attribute(&mut input_1, attr.parse(value), session), expanded_name!("", "in2") => { set_attribute(&mut input_2, attr.parse(value), session) } _ => (), } } (input_1, input_2) } pub fn parse_no_inputs(&mut self, attrs: &Attributes, session: &Session) { let (_, _) = self.parse_standard_attributes(attrs, session); } pub fn parse_one_input(&mut self, attrs: &Attributes, session: &Session) -> Input { let (input_1, _) = self.parse_standard_attributes(attrs, session); input_1 } pub fn parse_two_inputs(&mut self, attrs: &Attributes, session: &Session) -> (Input, Input) { self.parse_standard_attributes(attrs, session) } } /// Applies a filter and returns the resulting surface. pub fn render( filter: &FilterSpec, stroke_paint_source: Rc, fill_paint_source: Rc, source_surface: SharedImageSurface, acquired_nodes: &mut AcquiredNodes<'_>, draw_ctx: &mut DrawingCtx, transform: Transform, node_bbox: BoundingBox, ) -> Result { let session = draw_ctx.session().clone(); FilterContext::new( &filter.user_space_filter, stroke_paint_source, fill_paint_source, &source_surface, transform, node_bbox, ) .and_then(|mut filter_ctx| { // the message has an unclosed parenthesis; we'll close it below. rsvg_log!( session, "(filter \"{}\" with effects_region={:?}", filter.name, filter_ctx.effects_region() ); for user_space_primitive in &filter.primitives { let start = Instant::now(); match render_primitive(user_space_primitive, &filter_ctx, acquired_nodes, draw_ctx) { Ok(output) => { let elapsed = start.elapsed(); rsvg_log!( session, "(rendered filter primitive {} in {} seconds)", user_space_primitive.params.name(), elapsed.as_secs() as f64 + f64::from(elapsed.subsec_nanos()) / 1e9 ); filter_ctx.store_result(FilterResult { name: user_space_primitive.result.clone(), output, }); } Err(err) => { rsvg_log!( session, "(filter primitive {} returned an error: {})", user_space_primitive.params.name(), err ); // close the opening parenthesis from the message at the start of this function rsvg_log!(session, ")"); // Exit early on Cairo errors. Continue rendering otherwise. if let FilterError::CairoError(status) = err { return Err(FilterError::CairoError(status)); } } } } // close the opening parenthesis from the message at the start of this function rsvg_log!(session, ")"); Ok(filter_ctx.into_output()?) }) .or_else(|err| match err { FilterError::CairoError(status) => { // Exit early on Cairo errors Err(InternalRenderingError::from(status)) } _ => { // ignore other filter errors and just return an empty surface Ok(SharedImageSurface::empty( source_surface.width(), source_surface.height(), SurfaceType::AlphaOnly, )?) } }) } #[rustfmt::skip] fn render_primitive( primitive: &UserSpacePrimitive, ctx: &FilterContext, acquired_nodes: &mut AcquiredNodes<'_>, draw_ctx: &mut DrawingCtx, ) -> Result { use PrimitiveParams::*; let bounds_builder = primitive.get_bounds(ctx); // Note that feDropShadow is not handled here. When its FilterElement::resolve() is called, // it returns a series of lower-level primitives (flood, blur, offset, etc.) that make up // the drop-shadow effect. match primitive.params { Blend(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), ColorMatrix(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), ComponentTransfer(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), Composite(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), ConvolveMatrix(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), DiffuseLighting(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), DisplacementMap(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), Flood(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), GaussianBlur(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), Image(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), Merge(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), Morphology(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), Offset(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), SpecularLighting(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), Tile(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), Turbulence(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), } } impl From for SurfaceType { fn from(c: ColorInterpolationFilters) -> Self { match c { ColorInterpolationFilters::LinearRgb => SurfaceType::LinearRgb, _ => SurfaceType::SRgb, } } } impl Parse for EdgeMode { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { Ok(parse_identifiers!( parser, "duplicate" => EdgeMode::Duplicate, "wrap" => EdgeMode::Wrap, "none" => EdgeMode::None, )?) } } librsvg-2.59.0/src/filters/morphology.rs000064400000000000000000000145141046102023000164020ustar 00000000000000use std::cmp::{max, min}; use cssparser::Parser; use markup5ever::{expanded_name, local_name, namespace_url, ns}; use crate::document::AcquiredNodes; use crate::drawing_ctx::DrawingCtx; use crate::element::{set_attribute, ElementTrait}; use crate::error::*; use crate::node::Node; use crate::parse_identifiers; use crate::parsers::{NumberOptionalNumber, Parse, ParseValue}; use crate::properties::ColorInterpolationFilters; use crate::rect::IRect; use crate::session::Session; use crate::surface_utils::{ iterators::{PixelRectangle, Pixels}, shared_surface::ExclusiveImageSurface, EdgeMode, ImageSurfaceDataExt, Pixel, }; use crate::xml::Attributes; use super::bounds::BoundsBuilder; use super::context::{FilterContext, FilterOutput}; use super::{ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, ResolvedPrimitive, }; /// Enumeration of the possible morphology operations. #[derive(Default, Clone)] enum Operator { #[default] Erode, Dilate, } /// The `feMorphology` filter primitive. #[derive(Default)] pub struct FeMorphology { base: Primitive, params: Morphology, } /// Resolved `feMorphology` primitive for rendering. #[derive(Clone)] pub struct Morphology { in1: Input, operator: Operator, radius: NumberOptionalNumber, } // We need this because NumberOptionalNumber doesn't impl Default impl Default for Morphology { fn default() -> Morphology { Morphology { in1: Default::default(), operator: Default::default(), radius: NumberOptionalNumber(0.0, 0.0), } } } impl ElementTrait for FeMorphology { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { self.params.in1 = self.base.parse_one_input(attrs, session); for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "operator") => { set_attribute(&mut self.params.operator, attr.parse(value), session); } expanded_name!("", "radius") => { set_attribute(&mut self.params.radius, attr.parse(value), session); } _ => (), } } } } impl Morphology { pub fn render( &self, bounds_builder: BoundsBuilder, ctx: &FilterContext, acquired_nodes: &mut AcquiredNodes<'_>, draw_ctx: &mut DrawingCtx, ) -> Result { // Although https://www.w3.org/TR/filter-effects/#propdef-color-interpolation-filters does not mention // feMorphology as being one of the primitives that does *not* use that property, // the SVG1.1 test for filters-morph-01-f.svg fails if we pass the value from the ComputedValues here (that // document does not specify the color-interpolation-filters property, so it defaults to linearRGB). // So, we pass Auto, which will get resolved to SRGB, and that makes that test pass. // // I suppose erosion/dilation doesn't care about the color space of the source image? let input_1 = ctx.get_input( acquired_nodes, draw_ctx, &self.in1, ColorInterpolationFilters::Auto, )?; let bounds: IRect = bounds_builder .add_input(&input_1) .compute(ctx) .clipped .into(); let NumberOptionalNumber(rx, ry) = self.radius; if rx <= 0.0 && ry <= 0.0 { return Ok(FilterOutput { surface: input_1.surface().clone(), bounds, }); } let (rx, ry) = ctx.paffine().transform_distance(rx, ry); // The radii can become negative here due to the transform. // Additionally The radii being excessively large causes cpu hangups let (rx, ry) = (rx.abs().min(10.0), ry.abs().min(10.0)); let mut surface = ExclusiveImageSurface::new( ctx.source_graphic().width(), ctx.source_graphic().height(), input_1.surface().surface_type(), )?; surface.modify(&mut |data, stride| { for (x, y, _pixel) in Pixels::within(input_1.surface(), bounds) { // Compute the kernel rectangle bounds. let kernel_bounds = IRect::new( (f64::from(x) - rx).floor() as i32, (f64::from(y) - ry).floor() as i32, (f64::from(x) + rx).ceil() as i32 + 1, (f64::from(y) + ry).ceil() as i32 + 1, ); // Compute the new pixel values. let initial = match self.operator { Operator::Erode => u8::MAX, Operator::Dilate => u8::MIN, }; let mut output_pixel = Pixel { r: initial, g: initial, b: initial, a: initial, }; for (_x, _y, pixel) in PixelRectangle::within(input_1.surface(), bounds, kernel_bounds, EdgeMode::None) { let op = match self.operator { Operator::Erode => min, Operator::Dilate => max, }; output_pixel.r = op(output_pixel.r, pixel.r); output_pixel.g = op(output_pixel.g, pixel.g); output_pixel.b = op(output_pixel.b, pixel.b); output_pixel.a = op(output_pixel.a, pixel.a); } data.set_pixel(stride, output_pixel, x, y); } }); Ok(FilterOutput { surface: surface.share()?, bounds, }) } } impl FilterEffect for FeMorphology { fn resolve( &self, _acquired_nodes: &mut AcquiredNodes<'_>, _node: &Node, ) -> Result, FilterResolveError> { Ok(vec![ResolvedPrimitive { primitive: self.base.clone(), params: PrimitiveParams::Morphology(self.params.clone()), }]) } } impl Parse for Operator { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { Ok(parse_identifiers!( parser, "erode" => Operator::Erode, "dilate" => Operator::Dilate, )?) } } librsvg-2.59.0/src/filters/offset.rs000064400000000000000000000057271046102023000154770ustar 00000000000000use markup5ever::{expanded_name, local_name, namespace_url, ns}; use crate::document::AcquiredNodes; use crate::drawing_ctx::DrawingCtx; use crate::element::{set_attribute, ElementTrait}; use crate::node::Node; use crate::parsers::ParseValue; use crate::properties::ColorInterpolationFilters; use crate::rect::IRect; use crate::rsvg_log; use crate::session::Session; use crate::xml::Attributes; use super::bounds::BoundsBuilder; use super::context::{FilterContext, FilterOutput}; use super::{ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, ResolvedPrimitive, }; /// The `feOffset` filter primitive. #[derive(Default)] pub struct FeOffset { base: Primitive, params: Offset, } /// Resolved `feOffset` primitive for rendering. #[derive(Clone, Default)] pub struct Offset { pub in1: Input, pub dx: f64, pub dy: f64, } impl ElementTrait for FeOffset { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { self.params.in1 = self.base.parse_one_input(attrs, session); for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "dx") => { set_attribute(&mut self.params.dx, attr.parse(value), session) } expanded_name!("", "dy") => { set_attribute(&mut self.params.dy, attr.parse(value), session) } _ => (), } } } } impl Offset { pub fn render( &self, bounds_builder: BoundsBuilder, ctx: &FilterContext, acquired_nodes: &mut AcquiredNodes<'_>, draw_ctx: &mut DrawingCtx, ) -> Result { // https://www.w3.org/TR/filter-effects/#ColorInterpolationFiltersProperty // // "Note: The color-interpolation-filters property just has an // effect on filter operations. Therefore, it has no effect on // filter primitives like feOffset" // // This is why we pass Auto here. let input_1 = ctx.get_input( acquired_nodes, draw_ctx, &self.in1, ColorInterpolationFilters::Auto, )?; let bounds = bounds_builder.add_input(&input_1).compute(ctx).clipped; rsvg_log!(draw_ctx.session(), "(feOffset bounds={:?}", bounds); let (dx, dy) = ctx.paffine().transform_distance(self.dx, self.dy); let surface = input_1.surface().offset(bounds, dx, dy)?; let ibounds: IRect = bounds.into(); Ok(FilterOutput { surface, bounds: ibounds, }) } } impl FilterEffect for FeOffset { fn resolve( &self, _acquired_nodes: &mut AcquiredNodes<'_>, _node: &Node, ) -> Result, FilterResolveError> { Ok(vec![ResolvedPrimitive { primitive: self.base.clone(), params: PrimitiveParams::Offset(self.params.clone()), }]) } } librsvg-2.59.0/src/filters/tile.rs000064400000000000000000000064761046102023000151500ustar 00000000000000use crate::document::AcquiredNodes; use crate::drawing_ctx::DrawingCtx; use crate::element::ElementTrait; use crate::node::Node; use crate::properties::ColorInterpolationFilters; use crate::rect::IRect; use crate::rsvg_log; use crate::session::Session; use crate::xml::Attributes; use super::bounds::BoundsBuilder; use super::context::{FilterContext, FilterInput, FilterOutput}; use super::{ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, ResolvedPrimitive, }; /// The `feTile` filter primitive. #[derive(Default)] pub struct FeTile { base: Primitive, params: Tile, } /// Resolved `feTile` primitive for rendering. #[derive(Clone, Default)] pub struct Tile { in1: Input, } impl ElementTrait for FeTile { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { self.params.in1 = self.base.parse_one_input(attrs, session); } } impl Tile { pub fn render( &self, bounds_builder: BoundsBuilder, ctx: &FilterContext, acquired_nodes: &mut AcquiredNodes<'_>, draw_ctx: &mut DrawingCtx, ) -> Result { // https://www.w3.org/TR/filter-effects/#ColorInterpolationFiltersProperty // // "Note: The color-interpolation-filters property just has an // effect on filter operations. Therefore, it has no effect on // filter primitives like [...], feTile" // // This is why we pass Auto here. let input_1 = ctx.get_input( acquired_nodes, draw_ctx, &self.in1, ColorInterpolationFilters::Auto, )?; // feTile doesn't consider its inputs in the filter primitive subregion calculation. let bounds: IRect = bounds_builder.compute(ctx).clipped.into(); let surface = match input_1 { FilterInput::StandardInput(input_surface) => input_surface, FilterInput::PrimitiveOutput(FilterOutput { surface: input_surface, bounds: input_bounds, }) => { if input_bounds.is_empty() { rsvg_log!( draw_ctx.session(), "(feTile with empty input_bounds; returning just the input surface)" ); input_surface } else { rsvg_log!( draw_ctx.session(), "(feTile bounds={:?}, input_bounds={:?})", bounds, input_bounds ); let tile_surface = input_surface.tile(input_bounds)?; ctx.source_graphic().paint_image_tiled( bounds, &tile_surface, input_bounds.x0, input_bounds.y0, )? } } }; Ok(FilterOutput { surface, bounds }) } } impl FilterEffect for FeTile { fn resolve( &self, _acquired_nodes: &mut AcquiredNodes<'_>, _node: &Node, ) -> Result, FilterResolveError> { Ok(vec![ResolvedPrimitive { primitive: self.base.clone(), params: PrimitiveParams::Tile(self.params.clone()), }]) } } librsvg-2.59.0/src/filters/turbulence.rs000064400000000000000000000407671046102023000163640ustar 00000000000000use cssparser::Parser; use markup5ever::{expanded_name, local_name, namespace_url, ns}; use crate::document::AcquiredNodes; use crate::drawing_ctx::DrawingCtx; use crate::element::{set_attribute, ElementTrait}; use crate::error::*; use crate::node::{CascadedValues, Node}; use crate::parse_identifiers; use crate::parsers::{NumberOptionalNumber, Parse, ParseValue}; use crate::properties::ColorInterpolationFilters; use crate::rect::IRect; use crate::rsvg_log; use crate::session::Session; use crate::surface_utils::{ shared_surface::{ExclusiveImageSurface, SurfaceType}, ImageSurfaceDataExt, Pixel, PixelOps, }; use crate::util::clamp; use crate::xml::Attributes; use super::bounds::BoundsBuilder; use super::context::{FilterContext, FilterOutput}; use super::{ FilterEffect, FilterError, FilterResolveError, Primitive, PrimitiveParams, ResolvedPrimitive, }; /// Limit the `numOctaves` parameter to avoid unbounded CPU consumption. /// /// https://drafts.fxtf.org/filter-effects/#element-attrdef-feturbulence-numoctaves const MAX_OCTAVES: i32 = 9; /// Enumeration of the tile stitching modes. #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)] enum StitchTiles { Stitch, #[default] NoStitch, } /// Enumeration of the noise types. #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)] enum NoiseType { FractalNoise, #[default] Turbulence, } /// The `feTurbulence` filter primitive. #[derive(Default)] pub struct FeTurbulence { base: Primitive, params: Turbulence, } /// Resolved `feTurbulence` primitive for rendering. #[derive(Clone)] pub struct Turbulence { base_frequency: NumberOptionalNumber, num_octaves: i32, seed: f64, stitch_tiles: StitchTiles, type_: NoiseType, color_interpolation_filters: ColorInterpolationFilters, } impl Default for Turbulence { /// Constructs a new `Turbulence` with empty properties. #[inline] fn default() -> Turbulence { Turbulence { base_frequency: NumberOptionalNumber(0.0, 0.0), num_octaves: 1, seed: 0.0, stitch_tiles: Default::default(), type_: Default::default(), color_interpolation_filters: Default::default(), } } } impl ElementTrait for FeTurbulence { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { self.base.parse_no_inputs(attrs, session); for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "baseFrequency") => { set_attribute(&mut self.params.base_frequency, attr.parse(value), session); } expanded_name!("", "numOctaves") => { set_attribute(&mut self.params.num_octaves, attr.parse(value), session); if self.params.num_octaves > MAX_OCTAVES { let n = self.params.num_octaves; rsvg_log!( session, "ignoring numOctaves={n}, setting it to {MAX_OCTAVES}" ); self.params.num_octaves = MAX_OCTAVES; } } // Yes, seed needs to be parsed as a number and then truncated. expanded_name!("", "seed") => { set_attribute(&mut self.params.seed, attr.parse(value), session); } expanded_name!("", "stitchTiles") => { set_attribute(&mut self.params.stitch_tiles, attr.parse(value), session); } expanded_name!("", "type") => { set_attribute(&mut self.params.type_, attr.parse(value), session) } _ => (), } } } } // Produces results in the range [1, 2**31 - 2]. // Algorithm is: r = (a * r) mod m // where a = 16807 and m = 2**31 - 1 = 2147483647 // See [Park & Miller], CACM vol. 31 no. 10 p. 1195, Oct. 1988 // To test: the algorithm should produce the result 1043618065 // as the 10,000th generated number if the original seed is 1. const RAND_M: i32 = 2147483647; // 2**31 - 1 const RAND_A: i32 = 16807; // 7**5; primitive root of m const RAND_Q: i32 = 127773; // m / a const RAND_R: i32 = 2836; // m % a fn setup_seed(mut seed: i32) -> i32 { if seed <= 0 { seed = -(seed % (RAND_M - 1)) + 1; } if seed > RAND_M - 1 { seed = RAND_M - 1; } seed } fn random(seed: i32) -> i32 { let mut result = RAND_A * (seed % RAND_Q) - RAND_R * (seed / RAND_Q); if result <= 0 { result += RAND_M; } result } const B_SIZE: usize = 0x100; const PERLIN_N: i32 = 0x1000; #[derive(Clone, Copy)] struct NoiseGenerator { base_frequency: (f64, f64), num_octaves: i32, stitch_tiles: StitchTiles, type_: NoiseType, tile_width: f64, tile_height: f64, lattice_selector: [usize; B_SIZE + B_SIZE + 2], gradient: [[[f64; 2]; B_SIZE + B_SIZE + 2]; 4], } #[derive(Clone, Copy)] struct StitchInfo { width: usize, // How much to subtract to wrap for stitching. height: usize, wrap_x: usize, // Minimum value to wrap. wrap_y: usize, } impl NoiseGenerator { fn new( seed: i32, base_frequency: (f64, f64), num_octaves: i32, type_: NoiseType, stitch_tiles: StitchTiles, tile_width: f64, tile_height: f64, ) -> Self { let mut rv = Self { base_frequency, num_octaves, type_, stitch_tiles, tile_width, tile_height, lattice_selector: [0; B_SIZE + B_SIZE + 2], gradient: [[[0.0; 2]; B_SIZE + B_SIZE + 2]; 4], }; let mut seed = setup_seed(seed); for k in 0..4 { for i in 0..B_SIZE { rv.lattice_selector[i] = i; for j in 0..2 { seed = random(seed); rv.gradient[k][i][j] = ((seed % (B_SIZE + B_SIZE) as i32) - B_SIZE as i32) as f64 / B_SIZE as f64; } let s = (rv.gradient[k][i][0] * rv.gradient[k][i][0] + rv.gradient[k][i][1] * rv.gradient[k][i][1]) .sqrt(); rv.gradient[k][i][0] /= s; rv.gradient[k][i][1] /= s; } } for i in (1..B_SIZE).rev() { let k = rv.lattice_selector[i]; seed = random(seed); let j = seed as usize % B_SIZE; rv.lattice_selector[i] = rv.lattice_selector[j]; rv.lattice_selector[j] = k; } for i in 0..B_SIZE + 2 { rv.lattice_selector[B_SIZE + i] = rv.lattice_selector[i]; for k in 0..4 { for j in 0..2 { rv.gradient[k][B_SIZE + i][j] = rv.gradient[k][i][j]; } } } rv } fn noise2(&self, color_channel: usize, vec: [f64; 2], stitch_info: Option) -> f64 { #![allow(clippy::many_single_char_names)] const BM: usize = 0xff; let s_curve = |t| t * t * (3. - 2. * t); let lerp = |t, a, b| a + t * (b - a); let t = vec[0] + f64::from(PERLIN_N); let mut bx0 = t as usize; let mut bx1 = bx0 + 1; let rx0 = t.fract(); let rx1 = rx0 - 1.0; let t = vec[1] + f64::from(PERLIN_N); let mut by0 = t as usize; let mut by1 = by0 + 1; let ry0 = t.fract(); let ry1 = ry0 - 1.0; // If stitching, adjust lattice points accordingly. if let Some(stitch_info) = stitch_info { if bx0 >= stitch_info.wrap_x { bx0 -= stitch_info.width; } if bx1 >= stitch_info.wrap_x { bx1 -= stitch_info.width; } if by0 >= stitch_info.wrap_y { by0 -= stitch_info.height; } if by1 >= stitch_info.wrap_y { by1 -= stitch_info.height; } } bx0 &= BM; bx1 &= BM; by0 &= BM; by1 &= BM; let i = self.lattice_selector[bx0]; let j = self.lattice_selector[bx1]; let b00 = self.lattice_selector[i + by0]; let b10 = self.lattice_selector[j + by0]; let b01 = self.lattice_selector[i + by1]; let b11 = self.lattice_selector[j + by1]; let sx = s_curve(rx0); let sy = s_curve(ry0); let q = self.gradient[color_channel][b00]; let u = rx0 * q[0] + ry0 * q[1]; let q = self.gradient[color_channel][b10]; let v = rx1 * q[0] + ry0 * q[1]; let a = lerp(sx, u, v); let q = self.gradient[color_channel][b01]; let u = rx0 * q[0] + ry1 * q[1]; let q = self.gradient[color_channel][b11]; let v = rx1 * q[0] + ry1 * q[1]; let b = lerp(sx, u, v); lerp(sy, a, b) } fn turbulence(&self, color_channel: usize, point: [f64; 2], tile_x: f64, tile_y: f64) -> f64 { let mut stitch_info = None; let mut base_frequency = self.base_frequency; // Adjust the base frequencies if necessary for stitching. if self.stitch_tiles == StitchTiles::Stitch { // When stitching tiled turbulence, the frequencies must be adjusted // so that the tile borders will be continuous. if base_frequency.0 != 0.0 { let freq_lo = (self.tile_width * base_frequency.0).floor() / self.tile_width; let freq_hi = (self.tile_width * base_frequency.0).ceil() / self.tile_width; if base_frequency.0 / freq_lo < freq_hi / base_frequency.0 { base_frequency.0 = freq_lo; } else { base_frequency.0 = freq_hi; } } if base_frequency.1 != 0.0 { let freq_lo = (self.tile_height * base_frequency.1).floor() / self.tile_height; let freq_hi = (self.tile_height * base_frequency.1).ceil() / self.tile_height; if base_frequency.1 / freq_lo < freq_hi / base_frequency.1 { base_frequency.1 = freq_lo; } else { base_frequency.1 = freq_hi; } } // Set up initial stitch values. let width = (self.tile_width * base_frequency.0 + 0.5) as usize; let height = (self.tile_height * base_frequency.1 + 0.5) as usize; stitch_info = Some(StitchInfo { width, wrap_x: (tile_x * base_frequency.0) as usize + PERLIN_N as usize + width, height, wrap_y: (tile_y * base_frequency.1) as usize + PERLIN_N as usize + height, }); } let mut sum = 0.0; let mut vec = [point[0] * base_frequency.0, point[1] * base_frequency.1]; let mut ratio = 1.0; for _ in 0..self.num_octaves { if self.type_ == NoiseType::FractalNoise { sum += self.noise2(color_channel, vec, stitch_info) / ratio; } else { sum += (self.noise2(color_channel, vec, stitch_info)).abs() / ratio; } vec[0] *= 2.0; vec[1] *= 2.0; ratio *= 2.0; if let Some(stitch_info) = stitch_info.as_mut() { // Update stitch values. Subtracting PerlinN before the multiplication and // adding it afterward simplifies to subtracting it once. stitch_info.width *= 2; stitch_info.wrap_x = 2 * stitch_info.wrap_x - PERLIN_N as usize; stitch_info.height *= 2; stitch_info.wrap_y = 2 * stitch_info.wrap_y - PERLIN_N as usize; } } sum } } impl Turbulence { pub fn render( &self, bounds_builder: BoundsBuilder, ctx: &FilterContext, _acquired_nodes: &mut AcquiredNodes<'_>, _draw_ctx: &mut DrawingCtx, ) -> Result { let bounds: IRect = bounds_builder.compute(ctx).clipped.into(); let affine = ctx.paffine().invert().unwrap(); let seed = clamp( self.seed.trunc(), // per the spec, round towards zero f64::from(i32::MIN), f64::from(i32::MAX), ) as i32; // "Negative values are unsupported" -> set to the initial value which is 0.0 // // https://drafts.fxtf.org/filter-effects/#element-attrdef-feturbulence-basefrequency // // Later in the algorithm, the base_frequency gets multiplied by the coordinates within the // tile. So, limit the base_frequency to avoid overflow later. We impose an arbitrary // upper limit for the frequency. If it crosses that limit, we consider it invalid // and revert back to the initial value. See bug #1115. let base_frequency = { let NumberOptionalNumber(base_freq_x, base_freq_y) = self.base_frequency; let x = if base_freq_x > 32768.0 { 0.0 } else { base_freq_x.max(0.0) }; let y = if base_freq_y > 32768.0 { 0.0 } else { base_freq_y.max(0.0) }; (x, y) }; let noise_generator = NoiseGenerator::new( seed, base_frequency, self.num_octaves, self.type_, self.stitch_tiles, f64::from(bounds.width()), f64::from(bounds.height()), ); // The generated color values are in the color space determined by // color-interpolation-filters. let surface_type = SurfaceType::from(self.color_interpolation_filters); let mut surface = ExclusiveImageSurface::new( ctx.source_graphic().width(), ctx.source_graphic().height(), surface_type, )?; surface.modify(&mut |data, stride| { for y in bounds.y_range() { for x in bounds.x_range() { let point = affine.transform_point(f64::from(x), f64::from(y)); let point = [point.0, point.1]; let generate = |color_channel| { let v = noise_generator.turbulence( color_channel, point, f64::from(x - bounds.x0), f64::from(y - bounds.y0), ); let v = match self.type_ { NoiseType::FractalNoise => (v * 255.0 + 255.0) / 2.0, NoiseType::Turbulence => v * 255.0, }; (clamp(v, 0.0, 255.0) + 0.5) as u8 }; let pixel = Pixel { r: generate(0), g: generate(1), b: generate(2), a: generate(3), } .premultiply(); data.set_pixel(stride, pixel, x as u32, y as u32); } } }); Ok(FilterOutput { surface: surface.share()?, bounds, }) } } impl FilterEffect for FeTurbulence { fn resolve( &self, _acquired_nodes: &mut AcquiredNodes<'_>, node: &Node, ) -> Result, FilterResolveError> { let cascaded = CascadedValues::new_from_node(node); let values = cascaded.get(); let mut params = self.params.clone(); params.color_interpolation_filters = values.color_interpolation_filters(); Ok(vec![ResolvedPrimitive { primitive: self.base.clone(), params: PrimitiveParams::Turbulence(params), }]) } } impl Parse for StitchTiles { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { Ok(parse_identifiers!( parser, "stitch" => StitchTiles::Stitch, "noStitch" => StitchTiles::NoStitch, )?) } } impl Parse for NoiseType { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { Ok(parse_identifiers!( parser, "fractalNoise" => NoiseType::FractalNoise, "turbulence" => NoiseType::Turbulence, )?) } } #[cfg(test)] mod tests { use super::*; #[test] fn turbulence_rng() { let mut r = 1; r = setup_seed(r); for _ in 0..10_000 { r = random(r); } assert_eq!(r, 1043618065); } } librsvg-2.59.0/src/float_eq_cairo.rs000064400000000000000000000130401046102023000154730ustar 00000000000000//! Utilities to compare floating-point numbers. use float_cmp::ApproxEq; // The following are copied from cairo/src/{cairo-fixed-private.h, // cairo-fixed-type-private.h} const CAIRO_FIXED_FRAC_BITS: u64 = 8; const CAIRO_MAGIC_NUMBER_FIXED: f64 = (1u64 << (52 - CAIRO_FIXED_FRAC_BITS)) as f64 * 1.5; fn cairo_magic_double(d: f64) -> f64 { d + CAIRO_MAGIC_NUMBER_FIXED } fn cairo_fixed_from_double(d: f64) -> i32 { let bits = cairo_magic_double(d).to_bits(); let lower = bits & 0xffffffff; lower as i32 } /// Implements a method to check whether two `f64` numbers would have /// the same fixed-point representation in Cairo. /// /// This generally means that the absolute difference between them, /// when taken as floating-point numbers, is less than the smallest /// representable fraction that Cairo can represent in fixed-point. /// /// Implementation detail: Cairo fixed-point numbers use 24 bits for /// the integral part, and 8 bits for the fractional part. That is, /// the smallest fraction they can represent is 1/256. pub trait FixedEqCairo { #[allow(dead_code)] // https://github.com/rust-lang/rust/issues/120770 fn fixed_eq_cairo(&self, other: &Self) -> bool; } impl FixedEqCairo for f64 { fn fixed_eq_cairo(&self, other: &f64) -> bool { // FIXME: Here we have the same problem as Cairo itself: we // don't check for overflow in the conversion of double to // fixed-point. cairo_fixed_from_double(*self) == cairo_fixed_from_double(*other) } } /// Checks whether two floating-point numbers are approximately equal, /// considering Cairo's limitations on numeric representation. /// /// Cairo uses fixed-point numbers internally. We implement this /// trait for `f64`, so that two numbers can be considered "close /// enough to equal" if their absolute difference is smaller than the /// smallest fixed-point fraction that Cairo can represent. /// /// Note that this trait is reliable even if the given numbers are /// outside of the range that Cairo's fixed-point numbers can /// represent. In that case, we check for the absolute difference, /// and finally allow a difference of 1 unit-in-the-last-place (ULP) /// for very large f64 values. pub trait ApproxEqCairo: ApproxEq { fn approx_eq_cairo(self, other: Self) -> bool; } impl ApproxEqCairo for f64 { fn approx_eq_cairo(self, other: f64) -> bool { let cairo_smallest_fraction = 1.0 / f64::from(1 << CAIRO_FIXED_FRAC_BITS); self.approx_eq(other, (cairo_smallest_fraction, 1)) } } // Macro for usage in unit tests #[doc(hidden)] #[macro_export] macro_rules! assert_approx_eq_cairo { ($left:expr, $right:expr) => {{ match ($left, $right) { (l, r) => { if !l.approx_eq_cairo(r) { panic!( r#"assertion failed: `(left == right)` left: `{:?}`, right: `{:?}`"#, l, r ) } } } }}; } #[cfg(test)] mod tests { use super::*; #[test] fn numbers_equal_in_cairo_fixed_point() { assert!(1.0_f64.fixed_eq_cairo(&1.0_f64)); assert!(1.0_f64.fixed_eq_cairo(&1.001953125_f64)); // 1 + 1/512 - cairo rounds to 1 assert!(!1.0_f64.fixed_eq_cairo(&1.00390625_f64)); // 1 + 1/256 - cairo can represent it } #[test] fn numbers_approx_equal() { // 0 == 1/256 - cairo can represent it, so not equal assert!(!0.0_f64.approx_eq_cairo(0.00390635_f64)); // 1 == 1 + 1/256 - cairo can represent it, so not equal assert!(!1.0_f64.approx_eq_cairo(1.00390635_f64)); // 0 == 1/256 - cairo can represent it, so not equal assert!(!0.0_f64.approx_eq_cairo(-0.00390635_f64)); // 1 == 1 - 1/256 - cairo can represent it, so not equal assert!(!1.0_f64.approx_eq_cairo(0.99609365_f64)); // 0 == 1/512 - cairo approximates to 0, so equal assert!(0.0_f64.approx_eq_cairo(0.001953125_f64)); // 1 == 1 + 1/512 - cairo approximates to 1, so equal assert!(1.0_f64.approx_eq_cairo(1.001953125_f64)); // 0 == -1/512 - cairo approximates to 0, so equal assert!(0.0_f64.approx_eq_cairo(-0.001953125_f64)); // 1 == 1 - 1/512 - cairo approximates to 1, so equal assert!(1.0_f64.approx_eq_cairo(0.998046875_f64)); // This is 2^53 compared to (2^53 + 2). When represented as // f64, they are 1 unit-in-the-last-place (ULP) away from each // other, since the mantissa has 53 bits (52 bits plus 1 // "hidden" bit). The first number is an exact double, and // the second one is the next biggest double. We consider a // difference of 1 ULP to mean that numbers are "equal", to // account for slight imprecision in floating-point // calculations. Most of the time, for small values, we will // be using the cairo_smallest_fraction from the // implementation of approx_eq_cairo() above. For large // values, we want the ULPs. // // In the second assertion, we compare 2^53 with (2^53 + 4). Those are // 2 ULPs away, and we don't consider them equal. assert!(9_007_199_254_740_992.0.approx_eq_cairo(9_007_199_254_740_994.0)); assert!(!9_007_199_254_740_992.0.approx_eq_cairo(9_007_199_254_740_996.0)); } #[test] fn assert_approx_eq_cairo_should_not_panic() { assert_approx_eq_cairo!(42_f64, 42_f64); } #[test] #[should_panic] fn assert_approx_eq_cairo_should_panic() { assert_approx_eq_cairo!(3_f64, 42_f64); } } librsvg-2.59.0/src/font_props.rs000064400000000000000000000733541046102023000147330ustar 00000000000000//! CSS font properties. //! //! Do not import things directly from this module; use the `properties` module instead, //! which re-exports things from here. use cast::{f64, u16}; use cssparser::{Parser, Token}; use crate::error::*; use crate::length::*; use crate::parse_identifiers; use crate::parsers::{finite_f32, Parse}; use crate::properties::ComputedValues; use crate::property_defs::{FontStretch, FontStyle, FontVariant}; /// `font` shorthand property. /// /// CSS2: /// /// CSS Fonts 3: /// /// CSS Fonts 4: /// /// This is a shorthand, which expands to the longhands `font-family`, `font-size`, etc. // servo/components/style/properties/shorthands/font.mako.rs is a good reference for this #[derive(Debug, Clone, PartialEq)] pub enum Font { Caption, Icon, Menu, MessageBox, SmallCaption, StatusBar, Spec(FontSpec), } /// Parameters from the `font` shorthand property. #[derive(Debug, Default, Clone, PartialEq)] pub struct FontSpec { pub style: FontStyle, pub variant: FontVariant, pub weight: FontWeight, pub stretch: FontStretch, pub size: FontSize, pub line_height: LineHeight, pub family: FontFamily, } impl Parse for Font { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { if let Ok(f) = parse_font_spec_identifiers(parser) { return Ok(f); } // The following is stolen from servo/components/style/properties/shorthands/font.mako.rs let mut nb_normals = 0; let mut style = None; let mut variant_caps = None; let mut weight = None; let mut stretch = None; let size; loop { // Special-case 'normal' because it is valid in each of // font-style, font-weight, font-variant and font-stretch. // Leaves the values to None, 'normal' is the initial value for each of them. if parser .try_parse(|input| input.expect_ident_matching("normal")) .is_ok() { nb_normals += 1; continue; } if style.is_none() { if let Ok(value) = parser.try_parse(FontStyle::parse) { style = Some(value); continue; } } if weight.is_none() { if let Ok(value) = parser.try_parse(FontWeight::parse) { weight = Some(value); continue; } } if variant_caps.is_none() { if let Ok(value) = parser.try_parse(FontVariant::parse) { variant_caps = Some(value); continue; } } if stretch.is_none() { if let Ok(value) = parser.try_parse(FontStretch::parse) { stretch = Some(value); continue; } } size = FontSize::parse(parser)?; break; } let line_height = if parser.try_parse(|input| input.expect_delim('/')).is_ok() { Some(LineHeight::parse(parser)?) } else { None }; #[inline] fn count(opt: &Option) -> u8 { if opt.is_some() { 1 } else { 0 } } if (count(&style) + count(&weight) + count(&variant_caps) + count(&stretch) + nb_normals) > 4 { return Err(parser.new_custom_error(ValueErrorKind::parse_error( "invalid syntax for 'font' property", ))); } let family = FontFamily::parse(parser)?; Ok(Font::Spec(FontSpec { style: style.unwrap_or_default(), variant: variant_caps.unwrap_or_default(), weight: weight.unwrap_or_default(), stretch: stretch.unwrap_or_default(), size, line_height: line_height.unwrap_or_default(), family, })) } } impl Font { pub fn to_font_spec(&self) -> FontSpec { match *self { Font::Caption | Font::Icon | Font::Menu | Font::MessageBox | Font::SmallCaption | Font::StatusBar => { // We don't actually pick up the systme fonts, so reduce them to a default. FontSpec::default() } Font::Spec(ref spec) => spec.clone(), } } } /// Parses identifiers used for system fonts. #[rustfmt::skip] fn parse_font_spec_identifiers<'i>(parser: &mut Parser<'i, '_>) -> Result> { Ok(parser.try_parse(|p| { parse_identifiers! { p, "caption" => Font::Caption, "icon" => Font::Icon, "menu" => Font::Menu, "message-box" => Font::MessageBox, "small-caption" => Font::SmallCaption, "status-bar" => Font::StatusBar, } })?) } /// `font-size` property. /// /// SVG1.1: /// /// CSS2: /// /// CSS Fonts 3: #[allow(clippy::upper_case_acronyms)] #[derive(Debug, Clone, PartialEq)] pub enum FontSize { Smaller, Larger, XXSmall, XSmall, Small, Medium, Large, XLarge, XXLarge, Value(Length), } impl FontSize { pub fn value(&self) -> Length { match self { FontSize::Value(s) => *s, _ => unreachable!(), } } pub fn compute(&self, v: &ComputedValues) -> Self { let compute_points = |p| 12.0 * 1.2f64.powf(p) / POINTS_PER_INCH; let parent = v.font_size().value(); // The parent must already have resolved to an absolute unit assert!( parent.unit != LengthUnit::Percent && parent.unit != LengthUnit::Em && parent.unit != LengthUnit::Ex ); use FontSize::*; #[rustfmt::skip] let new_size = match self { Smaller => Length::::new(parent.length / 1.2, parent.unit), Larger => Length::::new(parent.length * 1.2, parent.unit), XXSmall => Length::::new(compute_points(-3.0), LengthUnit::In), XSmall => Length::::new(compute_points(-2.0), LengthUnit::In), Small => Length::::new(compute_points(-1.0), LengthUnit::In), Medium => Length::::new(compute_points(0.0), LengthUnit::In), Large => Length::::new(compute_points(1.0), LengthUnit::In), XLarge => Length::::new(compute_points(2.0), LengthUnit::In), XXLarge => Length::::new(compute_points(3.0), LengthUnit::In), Value(s) if s.unit == LengthUnit::Percent => { Length::::new(parent.length * s.length, parent.unit) } Value(s) if s.unit == LengthUnit::Em => { Length::::new(parent.length * s.length, parent.unit) } Value(s) if s.unit == LengthUnit::Ex => { // FIXME: it would be nice to know the actual Ex-height // of the font. Length::::new(parent.length * s.length / 2.0, parent.unit) } Value(s) => *s, }; FontSize::Value(new_size) } pub fn to_user(&self, params: &NormalizeParams) -> f64 { self.value().to_user(params) } } impl Parse for FontSize { #[rustfmt::skip] fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { parser .try_parse(|p| Length::::parse(p)) .map(FontSize::Value) .or_else(|_| { Ok(parse_identifiers!( parser, "smaller" => FontSize::Smaller, "larger" => FontSize::Larger, "xx-small" => FontSize::XXSmall, "x-small" => FontSize::XSmall, "small" => FontSize::Small, "medium" => FontSize::Medium, "large" => FontSize::Large, "x-large" => FontSize::XLarge, "xx-large" => FontSize::XXLarge, )?) }) } } /// `font-weight` property. /// /// CSS Fonts 3: /// /// CSS Fonts 4: #[derive(Debug, Copy, Clone, PartialEq)] pub enum FontWeight { Normal, Bold, Bolder, Lighter, Weight(u16), } impl Parse for FontWeight { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { parser .try_parse(|p| { Ok(parse_identifiers!( p, "normal" => FontWeight::Normal, "bold" => FontWeight::Bold, "bolder" => FontWeight::Bolder, "lighter" => FontWeight::Lighter, )?) }) .or_else(|_: ParseError<'_>| { let loc = parser.current_source_location(); let i = parser.expect_integer()?; if (1..=1000).contains(&i) { Ok(FontWeight::Weight(u16(i).unwrap())) } else { Err(loc.new_custom_error(ValueErrorKind::value_error( "value must be between 1 and 1000 inclusive", ))) } }) } } impl FontWeight { #[rustfmt::skip] pub fn compute(&self, v: &Self) -> Self { use FontWeight::*; // Here, note that we assume that Normal=W400 and Bold=W700, per the spec. Also, // this must match `impl From for pango::Weight`. // // See the table at https://drafts.csswg.org/css-fonts-4/#relative-weights match *self { Bolder => match v.numeric_weight() { w if ( 1..100).contains(&w) => Weight(400), w if (100..350).contains(&w) => Weight(400), w if (350..550).contains(&w) => Weight(700), w if (550..750).contains(&w) => Weight(900), w if (750..900).contains(&w) => Weight(900), w if 900 <= w => Weight(w), _ => unreachable!(), } Lighter => match v.numeric_weight() { w if ( 1..100).contains(&w) => Weight(w), w if (100..350).contains(&w) => Weight(100), w if (350..550).contains(&w) => Weight(100), w if (550..750).contains(&w) => Weight(400), w if (750..900).contains(&w) => Weight(700), w if 900 <= w => Weight(700), _ => unreachable!(), } _ => *self, } } // Converts the symbolic weights to numeric weights. Will panic on `Bolder` or `Lighter`. pub fn numeric_weight(self) -> u16 { use FontWeight::*; // Here, note that we assume that Normal=W400 and Bold=W700, per the spec. Also, // this must match `impl From for pango::Weight`. match self { Normal => 400, Bold => 700, Bolder | Lighter => unreachable!(), Weight(w) => w, } } } /// `letter-spacing` property. /// /// SVG1.1: /// /// CSS Text 3: #[derive(Debug, Clone, PartialEq)] pub enum LetterSpacing { Normal, Value(Length), } impl LetterSpacing { pub fn value(&self) -> Length { match self { LetterSpacing::Value(s) => *s, _ => unreachable!(), } } pub fn compute(&self) -> Self { let spacing = match self { LetterSpacing::Normal => Length::::new(0.0, LengthUnit::Px), LetterSpacing::Value(s) => *s, }; LetterSpacing::Value(spacing) } pub fn to_user(&self, params: &NormalizeParams) -> f64 { self.value().to_user(params) } } impl Parse for LetterSpacing { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { parser .try_parse(|p| Length::::parse(p)) .map(LetterSpacing::Value) .or_else(|_| { Ok(parse_identifiers!( parser, "normal" => LetterSpacing::Normal, )?) }) } } /// `line-height` property. /// /// CSS2: #[derive(Debug, Clone, PartialEq)] pub enum LineHeight { Normal, Number(f32), Length(Length), Percentage(f32), } impl LineHeight { pub fn value(&self) -> Length { match self { LineHeight::Length(l) => *l, _ => unreachable!(), } } pub fn compute(&self, values: &ComputedValues) -> Self { let font_size = values.font_size().value(); match *self { LineHeight::Normal => LineHeight::Length(font_size), LineHeight::Number(f) | LineHeight::Percentage(f) => { LineHeight::Length(Length::new(font_size.length * f64(f), font_size.unit)) } LineHeight::Length(l) => LineHeight::Length(l), } } pub fn to_user(&self, params: &NormalizeParams) -> f64 { self.value().to_user(params) } } impl Parse for LineHeight { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let state = parser.state(); let loc = parser.current_source_location(); let token = parser.next()?.clone(); match token { Token::Ident(ref cow) => { if cow.eq_ignore_ascii_case("normal") { Ok(LineHeight::Normal) } else { Err(parser .new_basic_unexpected_token_error(token.clone()) .into()) } } Token::Number { value, .. } => Ok(LineHeight::Number( finite_f32(value).map_err(|e| loc.new_custom_error(e))?, )), Token::Percentage { unit_value, .. } => Ok(LineHeight::Percentage(unit_value)), Token::Dimension { .. } => { parser.reset(&state); Ok(LineHeight::Length(Length::::parse(parser)?)) } _ => Err(parser.new_basic_unexpected_token_error(token).into()), } } } /// `font-family` property. /// /// SVG1.1: /// /// CSS 2: /// /// CSS Fonts 3: #[derive(Debug, Clone, PartialEq)] pub struct FontFamily(pub String); impl Parse for FontFamily { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let loc = parser.current_source_location(); let fonts = parser.parse_comma_separated(|parser| { if let Ok(cow) = parser.try_parse(|p| p.expect_string_cloned()) { if cow == "" { return Err(loc.new_custom_error(ValueErrorKind::value_error( "empty string is not a valid font family name", ))); } return Ok(cow); } let first_ident = parser.expect_ident()?.clone(); let mut value = first_ident.as_ref().to_owned(); while let Ok(cow) = parser.try_parse(|p| p.expect_ident_cloned()) { value.push(' '); value.push_str(&cow); } Ok(cssparser::CowRcStr::from(value)) })?; Ok(FontFamily(fonts.join(","))) } } impl FontFamily { pub fn as_str(&self) -> &str { &self.0 } } /// `glyph-orientation-vertical` property. /// /// Note that in SVG1.1 this could be `auto` or ``, but in SVG2/CSS3 it is /// deprecated, and turned into a shorthand for the `text-orientation` property. Also, /// now it only takes values `auto`, `0deg`, `90deg`, `0`, `90`. At parsing time, this /// gets translated to fixed enum values. /// /// CSS Writing Modes 3: /// /// Obsolete SVG1.1: #[derive(Debug, Copy, Clone, PartialEq)] pub enum GlyphOrientationVertical { Auto, Angle0, Angle90, } impl Parse for GlyphOrientationVertical { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let loc = parser.current_source_location(); if parser .try_parse(|p| p.expect_ident_matching("auto")) .is_ok() { return Ok(GlyphOrientationVertical::Auto); } let token = parser.next()?.clone(); // Apart from "auto" (handled above), // https://www.w3.org/TR/css-writing-modes-3/#propdef-glyph-orientation-vertical // only allows the values "0", "90", "0deg", "90deg". So, we will look at // individual tokens. We'll reject non-integer numbers or non-integer dimensions. match token { Token::Number { int_value: Some(0), .. } => Ok(GlyphOrientationVertical::Angle0), Token::Number { int_value: Some(90), .. } => Ok(GlyphOrientationVertical::Angle90), Token::Dimension { int_value: Some(0), unit, .. } if unit.eq_ignore_ascii_case("deg") => Ok(GlyphOrientationVertical::Angle0), Token::Dimension { int_value: Some(90), unit, .. } if unit.eq_ignore_ascii_case("deg") => Ok(GlyphOrientationVertical::Angle90), _ => Err(loc.new_unexpected_token_error(token.clone())), } } } #[cfg(test)] mod tests { use super::*; use crate::properties::{ParsedProperty, SpecifiedValue, SpecifiedValues}; #[test] fn parses_font_shorthand() { assert_eq!( Font::parse_str("small-caption").unwrap(), Font::SmallCaption, ); assert_eq!( Font::parse_str("italic bold 12px sans").unwrap(), Font::Spec(FontSpec { style: FontStyle::Italic, variant: Default::default(), weight: FontWeight::Bold, stretch: Default::default(), size: FontSize::Value(Length::new(12.0, LengthUnit::Px)), line_height: Default::default(), family: FontFamily("sans".to_string()), }), ); assert_eq!( Font::parse_str("bold 14cm/2 serif").unwrap(), Font::Spec(FontSpec { style: Default::default(), variant: Default::default(), weight: FontWeight::Bold, stretch: Default::default(), size: FontSize::Value(Length::new(14.0, LengthUnit::Cm)), line_height: LineHeight::Number(2.0), family: FontFamily("serif".to_string()), }), ); assert_eq!( Font::parse_str("small-caps condensed 12pt serif").unwrap(), Font::Spec(FontSpec { style: Default::default(), variant: FontVariant::SmallCaps, weight: FontWeight::Normal, stretch: FontStretch::Condensed, size: FontSize::Value(Length::new(12.0, LengthUnit::Pt)), line_height: Default::default(), family: FontFamily("serif".to_string()), }), ); } #[test] fn parses_font_shorthand_with_normal_values() { let expected_font = Font::Spec(FontSpec { style: Default::default(), variant: Default::default(), weight: Default::default(), stretch: Default::default(), size: FontSize::Value(Length::new(12.0, LengthUnit::Pt)), line_height: Default::default(), family: FontFamily("serif".to_string()), }); // One through four instances of "normal" - they all resolve to default values for // each property. assert_eq!(Font::parse_str("normal 12pt serif").unwrap(), expected_font,); assert_eq!( Font::parse_str("normal normal 12pt serif").unwrap(), expected_font, ); assert_eq!( Font::parse_str("normal normal normal 12pt serif").unwrap(), expected_font, ); assert_eq!( Font::parse_str("normal normal normal normal 12pt serif").unwrap(), expected_font, ); // But more than four "normal" is an error. assert!(Font::parse_str("normal normal normal normal normal 12pt serif").is_err()); // Let's throw in an actual keyword in the middle assert_eq!( Font::parse_str("normal bold normal 12pt serif").unwrap(), Font::Spec(FontSpec { style: Default::default(), variant: Default::default(), weight: FontWeight::Bold, stretch: Default::default(), size: FontSize::Value(Length::new(12.0, LengthUnit::Pt)), line_height: Default::default(), family: FontFamily("serif".to_string()), }), ); } #[test] fn detects_invalid_invalid_font_size() { assert!(FontSize::parse_str("furlong").is_err()); } #[test] fn computes_parent_relative_font_size() { let mut specified = SpecifiedValues::default(); specified.set_parsed_property(&ParsedProperty::FontSize(SpecifiedValue::Specified( FontSize::parse_str("10px").unwrap(), ))); let mut values = ComputedValues::default(); specified.to_computed_values(&mut values); assert_eq!( FontSize::parse_str("150%").unwrap().compute(&values), FontSize::parse_str("15px").unwrap() ); assert_eq!( FontSize::parse_str("1.5em").unwrap().compute(&values), FontSize::parse_str("15px").unwrap() ); assert_eq!( FontSize::parse_str("1ex").unwrap().compute(&values), FontSize::parse_str("5px").unwrap() ); let smaller = FontSize::parse_str("smaller").unwrap().compute(&values); if let FontSize::Value(v) = smaller { assert!(v.length < 10.0); assert_eq!(v.unit, LengthUnit::Px); } else { unreachable!(); } let larger = FontSize::parse_str("larger").unwrap().compute(&values); if let FontSize::Value(v) = larger { assert!(v.length > 10.0); assert_eq!(v.unit, LengthUnit::Px); } else { unreachable!(); } } #[test] fn parses_font_weight() { assert_eq!( ::parse_str("normal").unwrap(), FontWeight::Normal ); assert_eq!( ::parse_str("bold").unwrap(), FontWeight::Bold ); assert_eq!( ::parse_str("100").unwrap(), FontWeight::Weight(100) ); } #[test] fn detects_invalid_font_weight() { assert!(::parse_str("").is_err()); assert!(::parse_str("strange").is_err()); assert!(::parse_str("0").is_err()); assert!(::parse_str("-1").is_err()); assert!(::parse_str("1001").is_err()); assert!(::parse_str("3.14").is_err()); } #[test] fn parses_letter_spacing() { assert_eq!( ::parse_str("normal").unwrap(), LetterSpacing::Normal ); assert_eq!( ::parse_str("10em").unwrap(), LetterSpacing::Value(Length::::new(10.0, LengthUnit::Em,)) ); } #[test] fn computes_letter_spacing() { assert_eq!( ::parse_str("normal") .map(|s| s.compute()) .unwrap(), LetterSpacing::Value(Length::::new(0.0, LengthUnit::Px,)) ); assert_eq!( ::parse_str("10em") .map(|s| s.compute()) .unwrap(), LetterSpacing::Value(Length::::new(10.0, LengthUnit::Em,)) ); } #[test] fn detects_invalid_invalid_letter_spacing() { assert!(LetterSpacing::parse_str("furlong").is_err()); } #[test] fn parses_font_family() { assert_eq!( ::parse_str("'Hello world'").unwrap(), FontFamily("Hello world".to_owned()) ); assert_eq!( ::parse_str("\"Hello world\"").unwrap(), FontFamily("Hello world".to_owned()) ); assert_eq!( ::parse_str("\"Hello world with spaces\"").unwrap(), FontFamily("Hello world with spaces".to_owned()) ); assert_eq!( ::parse_str(" Hello world ").unwrap(), FontFamily("Hello world".to_owned()) ); assert_eq!( ::parse_str("Plonk").unwrap(), FontFamily("Plonk".to_owned()) ); } #[test] fn parses_multiple_font_family() { assert_eq!( ::parse_str("serif,monospace,\"Hello world\", with spaces ") .unwrap(), FontFamily("serif,monospace,Hello world,with spaces".to_owned()) ); } #[test] fn detects_invalid_font_family() { assert!(::parse_str("").is_err()); assert!(::parse_str("''").is_err()); assert!(::parse_str("42").is_err()); } #[test] fn parses_line_height() { assert_eq!( ::parse_str("normal").unwrap(), LineHeight::Normal ); assert_eq!( ::parse_str("2").unwrap(), LineHeight::Number(2.0) ); assert_eq!( ::parse_str("2cm").unwrap(), LineHeight::Length(Length::new(2.0, LengthUnit::Cm)) ); assert_eq!( ::parse_str("150%").unwrap(), LineHeight::Percentage(1.5) ); } #[test] fn detects_invalid_line_height() { assert!(::parse_str("").is_err()); assert!(::parse_str("florp").is_err()); assert!(::parse_str("3florp").is_err()); } #[test] fn computes_line_height() { let mut specified = SpecifiedValues::default(); specified.set_parsed_property(&ParsedProperty::FontSize(SpecifiedValue::Specified( FontSize::parse_str("10px").unwrap(), ))); let mut values = ComputedValues::default(); specified.to_computed_values(&mut values); assert_eq!( LineHeight::Normal.compute(&values), LineHeight::Length(Length::new(10.0, LengthUnit::Px)), ); assert_eq!( LineHeight::Number(2.0).compute(&values), LineHeight::Length(Length::new(20.0, LengthUnit::Px)), ); assert_eq!( LineHeight::Length(Length::new(3.0, LengthUnit::Cm)).compute(&values), LineHeight::Length(Length::new(3.0, LengthUnit::Cm)), ); assert_eq!( LineHeight::parse_str("150%").unwrap().compute(&values), LineHeight::Length(Length::new(15.0, LengthUnit::Px)), ); } #[test] fn parses_glyph_orientation_vertical() { assert_eq!( ::parse_str("auto").unwrap(), GlyphOrientationVertical::Auto ); assert_eq!( ::parse_str("0").unwrap(), GlyphOrientationVertical::Angle0 ); assert_eq!( ::parse_str("0deg").unwrap(), GlyphOrientationVertical::Angle0 ); assert_eq!( ::parse_str("90").unwrap(), GlyphOrientationVertical::Angle90 ); assert_eq!( ::parse_str("90deg").unwrap(), GlyphOrientationVertical::Angle90 ); } #[test] fn detects_invalid_glyph_orientation_vertical() { assert!(::parse_str("").is_err()); assert!(::parse_str("0.0").is_err()); assert!(::parse_str("90.0").is_err()); assert!(::parse_str("0.0deg").is_err()); assert!(::parse_str("90.0deg").is_err()); assert!(::parse_str("0rad").is_err()); assert!(::parse_str("0.0rad").is_err()); } } librsvg-2.59.0/src/gradient.rs000064400000000000000000000562041046102023000143320ustar 00000000000000//! Gradient paint servers; the `linearGradient` and `radialGradient` elements. use cssparser::{Color, Parser}; use markup5ever::{ expanded_name, local_name, namespace_url, ns, ExpandedName, LocalName, Namespace, }; use crate::coord_units; use crate::coord_units::CoordUnits; use crate::document::{AcquiredNodes, NodeId, NodeStack}; use crate::drawing_ctx::Viewport; use crate::element::{set_attribute, ElementData, ElementTrait}; use crate::error::*; use crate::href::{is_href, set_href}; use crate::length::*; use crate::node::{CascadedValues, Node, NodeBorrow}; use crate::paint_server::resolve_color; use crate::parse_identifiers; use crate::parsers::{Parse, ParseValue}; use crate::rect::{rect_to_transform, Rect}; use crate::session::Session; use crate::transform::{Transform, TransformAttribute}; use crate::unit_interval::UnitInterval; use crate::xml::Attributes; /// Contents of a `` element for gradient color stops #[derive(Copy, Clone)] pub struct ColorStop { /// `` pub offset: UnitInterval, /// `` pub color: Color, } // gradientUnits attribute; its default is objectBoundingBox coord_units!(GradientUnits, CoordUnits::ObjectBoundingBox); /// spreadMethod attribute for gradients #[derive(Debug, Default, Copy, Clone, PartialEq)] pub enum SpreadMethod { #[default] Pad, Reflect, Repeat, } impl Parse for SpreadMethod { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { Ok(parse_identifiers!( parser, "pad" => SpreadMethod::Pad, "reflect" => SpreadMethod::Reflect, "repeat" => SpreadMethod::Repeat, )?) } } /// Node for the `` element #[derive(Default)] pub struct Stop { /// `` offset: UnitInterval, /* stop-color and stop-opacity are not attributes; they are properties, so * they go into property_defs.rs */ } impl ElementTrait for Stop { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { if attr.expanded() == expanded_name!("", "offset") { set_attribute(&mut self.offset, attr.parse(value), session); } } } } /// Parameters specific to each gradient type, before being resolved. /// These will be composed together with UnreseolvedVariant from fallback /// nodes (referenced with e.g. ``) to form /// a final, resolved Variant. #[derive(Copy, Clone)] enum UnresolvedVariant { Linear { x1: Option>, y1: Option>, x2: Option>, y2: Option>, }, Radial { cx: Option>, cy: Option>, r: Option>, fx: Option>, fy: Option>, fr: Option>, }, } /// Parameters specific to each gradient type, after resolving. #[derive(Clone)] enum ResolvedGradientVariant { Linear { x1: Length, y1: Length, x2: Length, y2: Length, }, Radial { cx: Length, cy: Length, r: Length, fx: Length, fy: Length, fr: Length, }, } /// Parameters specific to each gradient type, after normalizing to user-space units. pub enum GradientVariant { Linear { x1: f64, y1: f64, x2: f64, y2: f64, }, Radial { cx: f64, cy: f64, r: f64, fx: f64, fy: f64, fr: f64, }, } impl UnresolvedVariant { fn into_resolved(self) -> ResolvedGradientVariant { assert!(self.is_resolved()); match self { UnresolvedVariant::Linear { x1, y1, x2, y2 } => ResolvedGradientVariant::Linear { x1: x1.unwrap(), y1: y1.unwrap(), x2: x2.unwrap(), y2: y2.unwrap(), }, UnresolvedVariant::Radial { cx, cy, r, fx, fy, fr, } => ResolvedGradientVariant::Radial { cx: cx.unwrap(), cy: cy.unwrap(), r: r.unwrap(), fx: fx.unwrap(), fy: fy.unwrap(), fr: fr.unwrap(), }, } } fn is_resolved(&self) -> bool { match *self { UnresolvedVariant::Linear { x1, y1, x2, y2 } => { x1.is_some() && y1.is_some() && x2.is_some() && y2.is_some() } UnresolvedVariant::Radial { cx, cy, r, fx, fy, fr, } => { cx.is_some() && cy.is_some() && r.is_some() && fx.is_some() && fy.is_some() && fr.is_some() } } } fn resolve_from_fallback(&self, fallback: &UnresolvedVariant) -> UnresolvedVariant { match (*self, *fallback) { ( UnresolvedVariant::Linear { x1, y1, x2, y2 }, UnresolvedVariant::Linear { x1: fx1, y1: fy1, x2: fx2, y2: fy2, }, ) => UnresolvedVariant::Linear { x1: x1.or(fx1), y1: y1.or(fy1), x2: x2.or(fx2), y2: y2.or(fy2), }, ( UnresolvedVariant::Radial { cx, cy, r, fx, fy, fr, }, UnresolvedVariant::Radial { cx: f_cx, cy: f_cy, r: f_r, fx: f_fx, fy: f_fy, fr: f_fr, }, ) => UnresolvedVariant::Radial { cx: cx.or(f_cx), cy: cy.or(f_cy), r: r.or(f_r), fx: fx.or(f_fx), fy: fy.or(f_fy), fr: fr.or(f_fr), }, _ => *self, // If variants are of different types, then nothing to resolve } } // https://www.w3.org/TR/SVG/pservers.html#LinearGradients // https://www.w3.org/TR/SVG/pservers.html#RadialGradients fn resolve_from_defaults(&self) -> UnresolvedVariant { match self { UnresolvedVariant::Linear { x1, y1, x2, y2 } => UnresolvedVariant::Linear { x1: x1.or_else(|| Some(Length::::parse_str("0%").unwrap())), y1: y1.or_else(|| Some(Length::::parse_str("0%").unwrap())), x2: x2.or_else(|| Some(Length::::parse_str("100%").unwrap())), y2: y2.or_else(|| Some(Length::::parse_str("0%").unwrap())), }, UnresolvedVariant::Radial { cx, cy, r, fx, fy, fr, } => { let cx = cx.or_else(|| Some(Length::::parse_str("50%").unwrap())); let cy = cy.or_else(|| Some(Length::::parse_str("50%").unwrap())); let r = r.or_else(|| Some(Length::::parse_str("50%").unwrap())); // fx and fy fall back to the presentational value of cx and cy let fx = fx.or(cx); let fy = fy.or(cy); let fr = fr.or_else(|| Some(Length::::parse_str("0%").unwrap())); UnresolvedVariant::Radial { cx, cy, r, fx, fy, fr, } } } } } /// Fields shared by all gradient nodes #[derive(Default)] struct Common { units: Option, transform: Option, spread: Option, fallback: Option, } /// Node for the `` element #[derive(Default)] pub struct LinearGradient { common: Common, x1: Option>, y1: Option>, x2: Option>, y2: Option>, } /// Node for the `` element #[derive(Default)] pub struct RadialGradient { common: Common, cx: Option>, cy: Option>, r: Option>, fx: Option>, fy: Option>, fr: Option>, } /// Main structure used during gradient resolution. For unresolved /// gradients, we store all fields as `Option` - if `None`, it means /// that the field is not specified; if `Some(T)`, it means that the /// field was specified. struct UnresolvedGradient { units: Option, transform: Option, spread: Option, stops: Option>, variant: UnresolvedVariant, } /// Resolved gradient; this is memoizable after the initial resolution. #[derive(Clone)] pub struct ResolvedGradient { units: GradientUnits, transform: TransformAttribute, spread: SpreadMethod, stops: Vec, variant: ResolvedGradientVariant, } /// Gradient normalized to user-space units. pub struct UserSpaceGradient { pub transform: Transform, pub spread: SpreadMethod, pub stops: Vec, pub variant: GradientVariant, } impl UnresolvedGradient { fn into_resolved(self) -> ResolvedGradient { assert!(self.is_resolved()); let UnresolvedGradient { units, transform, spread, stops, variant, } = self; match variant { UnresolvedVariant::Linear { .. } => ResolvedGradient { units: units.unwrap(), transform: transform.unwrap(), spread: spread.unwrap(), stops: stops.unwrap(), variant: variant.into_resolved(), }, UnresolvedVariant::Radial { .. } => ResolvedGradient { units: units.unwrap(), transform: transform.unwrap(), spread: spread.unwrap(), stops: stops.unwrap(), variant: variant.into_resolved(), }, } } /// Helper for add_color_stops_from_node() fn add_color_stop(&mut self, offset: UnitInterval, color: Color) { if self.stops.is_none() { self.stops = Some(Vec::::new()); } if let Some(ref mut stops) = self.stops { let last_offset = if !stops.is_empty() { stops[stops.len() - 1].offset } else { UnitInterval(0.0) }; let offset = if offset > last_offset { offset } else { last_offset }; stops.push(ColorStop { offset, color }); } else { unreachable!(); } } /// Looks for `` children inside a linearGradient or radialGradient node, /// and adds their info to the UnresolvedGradient &self. fn add_color_stops_from_node(&mut self, node: &Node, opacity: UnitInterval) { assert!(matches!( *node.borrow_element_data(), ElementData::LinearGradient(_) | ElementData::RadialGradient(_) )); for child in node.children().filter(|c| c.is_element()) { if let ElementData::Stop(ref stop) = &*child.borrow_element_data() { let cascaded = CascadedValues::new_from_node(&child); let values = cascaded.get(); let UnitInterval(stop_opacity) = values.stop_opacity().0; let UnitInterval(o) = opacity; let composed_opacity = UnitInterval(stop_opacity * o); let stop_color = resolve_color(&values.stop_color().0, composed_opacity, &values.color().0); self.add_color_stop(stop.offset, stop_color); } } } fn is_resolved(&self) -> bool { self.units.is_some() && self.transform.is_some() && self.spread.is_some() && self.stops.is_some() && self.variant.is_resolved() } fn resolve_from_fallback(&self, fallback: &UnresolvedGradient) -> UnresolvedGradient { let units = self.units.or(fallback.units); let transform = self.transform.or(fallback.transform); let spread = self.spread.or(fallback.spread); let stops = self.stops.clone().or_else(|| fallback.stops.clone()); let variant = self.variant.resolve_from_fallback(&fallback.variant); UnresolvedGradient { units, transform, spread, stops, variant, } } fn resolve_from_defaults(&self) -> UnresolvedGradient { let units = self.units.or_else(|| Some(GradientUnits::default())); let transform = self .transform .or_else(|| Some(TransformAttribute::default())); let spread = self.spread.or_else(|| Some(SpreadMethod::default())); let stops = self.stops.clone().or_else(|| Some(Vec::::new())); let variant = self.variant.resolve_from_defaults(); UnresolvedGradient { units, transform, spread, stops, variant, } } } /// State used during the gradient resolution process /// /// This is the current node's gradient information, plus the fallback /// that should be used in case that information is not complete for a /// resolved gradient yet. struct Unresolved { gradient: UnresolvedGradient, fallback: Option, } impl LinearGradient { fn get_unresolved_variant(&self) -> UnresolvedVariant { UnresolvedVariant::Linear { x1: self.x1, y1: self.y1, x2: self.x2, y2: self.y2, } } } impl RadialGradient { fn get_unresolved_variant(&self) -> UnresolvedVariant { UnresolvedVariant::Radial { cx: self.cx, cy: self.cy, r: self.r, fx: self.fx, fy: self.fy, fr: self.fr, } } } impl Common { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "gradientUnits") => { set_attribute(&mut self.units, attr.parse(value), session) } expanded_name!("", "gradientTransform") => { set_attribute(&mut self.transform, attr.parse(value), session); } expanded_name!("", "spreadMethod") => { set_attribute(&mut self.spread, attr.parse(value), session) } ref a if is_href(a) => { let mut href = None; set_attribute( &mut href, NodeId::parse(value).map(Some).attribute(attr.clone()), session, ); set_href(a, &mut self.fallback, href); } _ => (), } } } } impl ElementTrait for LinearGradient { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { self.common.set_attributes(attrs, session); for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "x1") => set_attribute(&mut self.x1, attr.parse(value), session), expanded_name!("", "y1") => set_attribute(&mut self.y1, attr.parse(value), session), expanded_name!("", "x2") => set_attribute(&mut self.x2, attr.parse(value), session), expanded_name!("", "y2") => set_attribute(&mut self.y2, attr.parse(value), session), _ => (), } } } } macro_rules! impl_gradient { ($gradient_type:ident, $other_type:ident) => { impl $gradient_type { fn get_unresolved(&self, node: &Node, opacity: UnitInterval) -> Unresolved { let mut gradient = UnresolvedGradient { units: self.common.units, transform: self.common.transform, spread: self.common.spread, stops: None, variant: self.get_unresolved_variant(), }; gradient.add_color_stops_from_node(node, opacity); Unresolved { gradient, fallback: self.common.fallback.clone(), } } pub fn resolve( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, opacity: UnitInterval, ) -> Result { let Unresolved { mut gradient, mut fallback, } = self.get_unresolved(node, opacity); let mut stack = NodeStack::new(); while !gradient.is_resolved() { if let Some(node_id) = fallback { let acquired = acquired_nodes.acquire(&node_id)?; let acquired_node = acquired.get(); if stack.contains(acquired_node) { return Err(AcquireError::CircularReference(acquired_node.clone())); } let unresolved = match *acquired_node.borrow_element_data() { ElementData::$gradient_type(ref g) => { g.get_unresolved(&acquired_node, opacity) } ElementData::$other_type(ref g) => { g.get_unresolved(&acquired_node, opacity) } _ => return Err(AcquireError::InvalidLinkType(node_id.clone())), }; gradient = gradient.resolve_from_fallback(&unresolved.gradient); fallback = unresolved.fallback; stack.push(acquired_node); } else { gradient = gradient.resolve_from_defaults(); break; } } Ok(gradient.into_resolved()) } } }; } impl_gradient!(LinearGradient, RadialGradient); impl_gradient!(RadialGradient, LinearGradient); impl ElementTrait for RadialGradient { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { self.common.set_attributes(attrs, session); // Create a local expanded name for "fr" because markup5ever doesn't have built-in let expanded_name_fr = ExpandedName { ns: &Namespace::from(""), local: &LocalName::from("fr"), }; for (attr, value) in attrs.iter() { let attr_expanded = attr.expanded(); match attr_expanded { expanded_name!("", "cx") => set_attribute(&mut self.cx, attr.parse(value), session), expanded_name!("", "cy") => set_attribute(&mut self.cy, attr.parse(value), session), expanded_name!("", "r") => set_attribute(&mut self.r, attr.parse(value), session), expanded_name!("", "fx") => set_attribute(&mut self.fx, attr.parse(value), session), expanded_name!("", "fy") => set_attribute(&mut self.fy, attr.parse(value), session), a if a == expanded_name_fr => { set_attribute(&mut self.fr, attr.parse(value), session) } _ => (), } } } } impl ResolvedGradient { pub fn to_user_space( &self, object_bbox: &Option, viewport: &Viewport, values: &NormalizeValues, ) -> Option { let units = self.units.0; let transform = rect_to_transform(object_bbox, units).ok()?; let viewport = viewport.with_units(units); let params = NormalizeParams::from_values(values, &viewport); let gradient_transform = self.transform.to_transform(); let transform = transform.pre_transform(&gradient_transform).invert()?; let variant = match self.variant { ResolvedGradientVariant::Linear { x1, y1, x2, y2 } => GradientVariant::Linear { x1: x1.to_user(¶ms), y1: y1.to_user(¶ms), x2: x2.to_user(¶ms), y2: y2.to_user(¶ms), }, ResolvedGradientVariant::Radial { cx, cy, r, fx, fy, fr, } => GradientVariant::Radial { cx: cx.to_user(¶ms), cy: cy.to_user(¶ms), r: r.to_user(¶ms), fx: fx.to_user(¶ms), fy: fy.to_user(¶ms), fr: fr.to_user(¶ms), }, }; Some(UserSpaceGradient { transform, spread: self.spread, stops: self.stops.clone(), variant, }) } } #[cfg(test)] mod tests { use super::*; use markup5ever::{namespace_url, ns, QualName}; use crate::borrow_element_as; use crate::node::{Node, NodeData}; #[test] fn parses_spread_method() { assert_eq!(SpreadMethod::parse_str("pad").unwrap(), SpreadMethod::Pad); assert_eq!( SpreadMethod::parse_str("reflect").unwrap(), SpreadMethod::Reflect ); assert_eq!( SpreadMethod::parse_str("repeat").unwrap(), SpreadMethod::Repeat ); assert!(SpreadMethod::parse_str("foobar").is_err()); } #[test] fn gradient_resolved_from_defaults_is_really_resolved() { let session = Session::default(); let node = Node::new(NodeData::new_element( &session, &QualName::new(None, ns!(svg), local_name!("linearGradient")), Attributes::new(), )); let unresolved = borrow_element_as!(node, LinearGradient) .get_unresolved(&node, UnitInterval::clamp(1.0)); let gradient = unresolved.gradient.resolve_from_defaults(); assert!(gradient.is_resolved()); let node = Node::new(NodeData::new_element( &session, &QualName::new(None, ns!(svg), local_name!("radialGradient")), Attributes::new(), )); let unresolved = borrow_element_as!(node, RadialGradient) .get_unresolved(&node, UnitInterval::clamp(1.0)); let gradient = unresolved.gradient.resolve_from_defaults(); assert!(gradient.is_resolved()); } } librsvg-2.59.0/src/href.rs000064400000000000000000000032251046102023000134540ustar 00000000000000//! Handling of `xlink:href` and `href` attributes //! //! In SVG1.1, links to elements are done with the `xlink:href` attribute. However, SVG2 //! reduced this to just plain `href` with no namespace: //! //! //! If an element has both `xlink:href` and `href` attributes, the `href` overrides the //! other. We implement that logic in this module. use markup5ever::{expanded_name, local_name, namespace_url, ns, ExpandedName}; /// Returns whether the attribute is either of `xlink:href` or `href`. /// /// # Example /// /// Use with an `if` pattern inside a `match`: /// /// ``` /// # use markup5ever::{expanded_name, local_name, namespace_url, ns, QualName, Prefix, Namespace, LocalName, ExpandedName}; /// # use rsvg::doctest_only::{is_href,set_href}; /// /// let qual_name = QualName::new( /// Some(Prefix::from("xlink")), /// Namespace::from("http://www.w3.org/1999/xlink"), /// LocalName::from("href"), /// ); /// /// let value = "some_url"; /// let mut my_field: Option = None; /// /// match qual_name.expanded() { /// ref name if is_href(name) => set_href(name, &mut my_field, Some(value.to_string())), /// _ => unreachable!(), /// } /// ``` pub fn is_href(name: &ExpandedName<'_>) -> bool { matches!( *name, expanded_name!(xlink "href") | expanded_name!("", "href") ) } /// Sets an `href` attribute in preference over an `xlink:href` one. /// /// See [`is_href`] for example usage. pub fn set_href(name: &ExpandedName<'_>, dest: &mut Option, href: Option) { if dest.is_none() || *name != expanded_name!(xlink "href") { *dest = href; } } librsvg-2.59.0/src/image.rs000064400000000000000000000207461046102023000136210ustar 00000000000000//! The `image` element. use markup5ever::{expanded_name, local_name, namespace_url, ns}; use crate::aspect_ratio::AspectRatio; use crate::bbox::BoundingBox; use crate::document::{AcquiredNodes, Document, Resource}; use crate::drawing_ctx::{DrawingCtx, SvgNesting, Viewport}; use crate::element::{set_attribute, ElementTrait}; use crate::error::*; use crate::href::{is_href, set_href}; use crate::layout::{self, Layer, LayerKind, StackingContext}; use crate::length::*; use crate::node::{CascadedValues, Node, NodeBorrow}; use crate::parsers::ParseValue; use crate::rect::Rect; use crate::rsvg_log; use crate::session::Session; use crate::surface_utils::shared_surface::{SharedImageSurface, SurfaceType}; use crate::xml::Attributes; /// The `` element. /// /// Note that its x/y/width/height are properties in SVG2, so they are /// defined as part of [the properties machinery](properties.rs). #[derive(Default)] pub struct Image { aspect: AspectRatio, href: Option, } impl ElementTrait for Image { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "preserveAspectRatio") => { set_attribute(&mut self.aspect, attr.parse(value), session) } // "path" is used by some older Adobe Illustrator versions ref a if is_href(a) || *a == expanded_name!("", "path") => { set_href(a, &mut self.href, Some(value.to_string())) } _ => (), } } } fn layout( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, _clipping: bool, ) -> Result, InternalRenderingError> { if let Some(ref url) = self.href { self.layout_from_url(url, node, acquired_nodes, cascaded, viewport, draw_ctx) } else { Ok(None) } } fn draw( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, clipping: bool, ) -> Result { let layer = self.layout(node, acquired_nodes, cascaded, viewport, draw_ctx, clipping)?; if let Some(layer) = layer { draw_ctx.draw_layer(&layer, acquired_nodes, clipping, viewport) } else { Ok(draw_ctx.empty_bbox()) } } } impl Image { fn layout_from_url( &self, url: &str, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, ) -> Result, InternalRenderingError> { match acquired_nodes.lookup_resource(url) { Ok(Resource::Image(surface)) => self.layout_from_surface( &surface, node, acquired_nodes, cascaded, viewport, draw_ctx, ), Ok(Resource::Document(document)) => self.layout_from_svg( &document, node, acquired_nodes, cascaded, viewport, draw_ctx, ), Err(e) => { rsvg_log!( draw_ctx.session(), "could not load image \"{}\": {}", url, e ); Ok(None) } } } /// Draw an `` from a raster image. fn layout_from_surface( &self, surface: &SharedImageSurface, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, ) -> Result, InternalRenderingError> { let values = cascaded.get(); let params = NormalizeParams::new(values, viewport); let x = values.x().0.to_user(¶ms); let y = values.y().0.to_user(¶ms); let w = match values.width().0 { LengthOrAuto::Length(l) => l.to_user(¶ms), LengthOrAuto::Auto => surface.width() as f64, }; let h = match values.height().0 { LengthOrAuto::Length(l) => l.to_user(¶ms), LengthOrAuto::Auto => surface.height() as f64, }; let is_visible = values.is_visible(); let rect = Rect::new(x, y, x + w, y + h); let overflow = values.overflow(); let image = Box::new(layout::Image { surface: surface.clone(), is_visible, rect, aspect: self.aspect, overflow, image_rendering: values.image_rendering(), }); let elt = node.borrow_element(); let stacking_ctx = StackingContext::new( draw_ctx.session(), acquired_nodes, &elt, values.transform(), None, values, ); let layer = Layer { kind: LayerKind::Image(image), stacking_ctx, }; Ok(Some(layer)) } /// Draw an `` from an SVG image. /// /// Per the [spec], we need to rasterize the SVG ("The result of processing an ‘image’ /// is always a four-channel RGBA result.") and then composite it as if it were a PNG /// or JPEG. /// /// [spec]: https://www.w3.org/TR/SVG2/embedded.html#ImageElement fn layout_from_svg( &self, document: &Document, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, ) -> Result, InternalRenderingError> { let dimensions = document.get_intrinsic_dimensions(); let values = cascaded.get(); let params = NormalizeParams::new(values, viewport); let x = values.x().0.to_user(¶ms); let y = values.y().0.to_user(¶ms); let w = match values.width().0 { LengthOrAuto::Length(l) => l.to_user(¶ms), LengthOrAuto::Auto => dimensions.width.to_user(¶ms), }; let h = match values.height().0 { LengthOrAuto::Length(l) => l.to_user(¶ms), LengthOrAuto::Auto => dimensions.height.to_user(¶ms), }; let is_visible = values.is_visible(); let rect = Rect::new(x, y, x + w, y + h); let overflow = values.overflow(); let dest_rect = match dimensions.vbox { None => Rect::from_size(w, h), Some(vbox) => self.aspect.compute(&vbox, &Rect::new(x, y, x + w, y + h)), }; let dest_size = dest_rect.size(); let surface_dest_rect = Rect::from_size(dest_size.0, dest_size.1); // We use ceil() to avoid chopping off the last pixel if it is partially covered. let surface_width = checked_i32(dest_size.0.ceil())?; let surface_height = checked_i32(dest_size.1.ceil())?; let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, surface_width, surface_height)?; { let cr = cairo::Context::new(&surface)?; let options = draw_ctx.rendering_options(SvgNesting::ReferencedFromImageElement); document.render_document( draw_ctx.session(), &cr, &cairo::Rectangle::from(surface_dest_rect), &options, )?; } let surface = SharedImageSurface::wrap(surface, SurfaceType::SRgb)?; let image = Box::new(layout::Image { surface, is_visible, rect, aspect: self.aspect, overflow, image_rendering: values.image_rendering(), }); let elt = node.borrow_element(); let stacking_ctx = StackingContext::new( draw_ctx.session(), acquired_nodes, &elt, values.transform(), None, values, ); let layer = Layer { kind: LayerKind::Image(image), stacking_ctx, }; Ok(Some(layer)) } } pub fn checked_i32(x: f64) -> Result { cast::i32(x).map_err(|_| cairo::Error::InvalidSize) } librsvg-2.59.0/src/io.rs000064400000000000000000000067621046102023000131500ustar 00000000000000//! Utilities to acquire streams and data from from URLs. use data_url::{mime::Mime, DataUrl}; use gio::{ prelude::{FileExt, FileExtManual}, Cancellable, File as GFile, InputStream, MemoryInputStream, }; use glib::{self, object::Cast, Bytes as GBytes}; use std::fmt; use std::str::FromStr; use crate::url_resolver::AllowedUrl; pub enum IoError { BadDataUrl, Glib(glib::Error), } impl From for IoError { fn from(e: glib::Error) -> IoError { IoError::Glib(e) } } impl fmt::Display for IoError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { IoError::BadDataUrl => write!(f, "invalid data: URL"), IoError::Glib(ref e) => e.fmt(f), } } } pub struct BinaryData { pub data: Vec, pub mime_type: Mime, } fn decode_data_uri(uri: &str) -> Result { let data_url = DataUrl::process(uri).map_err(|_| IoError::BadDataUrl)?; let mime = data_url.mime_type(); // data_url::mime::Mime doesn't impl Clone, so do it by hand let mime_type = Mime { type_: mime.type_.clone(), subtype: mime.subtype.clone(), parameters: mime.parameters.clone(), }; let (bytes, fragment_id) = data_url.decode_to_vec().map_err(|_| IoError::BadDataUrl)?; // See issue #377 - per the data: URL spec // (https://fetch.spec.whatwg.org/#data-urls), those URLs cannot // have fragment identifiers. So, just return an error if we find // one. This probably indicates mis-quoted SVG data inside the // data: URL. if fragment_id.is_some() { return Err(IoError::BadDataUrl); } Ok(BinaryData { data: bytes, mime_type, }) } /// Creates a stream for reading. The url can be a data: URL or a plain URI. pub fn acquire_stream( aurl: &AllowedUrl, cancellable: Option<&Cancellable>, ) -> Result { let uri = aurl.as_str(); if uri.starts_with("data:") { let BinaryData { data, .. } = decode_data_uri(uri)?; // { // use std::fs::File; // use std::io::prelude::*; // // let mut file = File::create("data.bin").unwrap(); // file.write_all(&data).unwrap(); // } let stream = MemoryInputStream::from_bytes(&GBytes::from_owned(data)); Ok(stream.upcast::()) } else { let file = GFile::for_uri(uri); let stream = file.read(cancellable)?; Ok(stream.upcast::()) } } /// Reads the entire contents pointed by an URL. The url can be a data: URL or a plain URI. pub fn acquire_data( aurl: &AllowedUrl, cancellable: Option<&Cancellable>, ) -> Result { let uri = aurl.as_str(); if uri.starts_with("data:") { Ok(decode_data_uri(uri)?) } else { let file = GFile::for_uri(uri); let (contents, _etag) = file.load_contents(cancellable)?; let (content_type, _uncertain) = gio::content_type_guess(Some(uri), &contents); let mime_type = if let Some(mime_type_str) = gio::content_type_get_mime_type(&content_type) { Mime::from_str(&mime_type_str) .expect("gio::content_type_get_mime_type returned an invalid MIME-type!?") } else { Mime::from_str("application/octet-stream").unwrap() }; Ok(BinaryData { data: contents.to_vec(), mime_type, }) } } librsvg-2.59.0/src/iri.rs000064400000000000000000000046201046102023000133130ustar 00000000000000//! CSS funciri values. use cssparser::Parser; use crate::document::NodeId; use crate::error::*; use crate::parsers::Parse; /// Used where style properties take a funciri or "none" /// /// This is not to be used for values which don't come from properties. /// For example, the `xlink:href` attribute in the `` element /// does not take a funciri value (which looks like `url(...)`), but rather /// it takes a plain URL. #[derive(Debug, Clone, PartialEq)] pub enum Iri { None, Resource(Box), } impl Iri { /// Returns the contents of an `IRI::Resource`, or `None` pub fn get(&self) -> Option<&NodeId> { match *self { Iri::None => None, Iri::Resource(ref f) => Some(f), } } } impl Parse for Iri { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { if parser .try_parse(|i| i.expect_ident_matching("none")) .is_ok() { Ok(Iri::None) } else { let loc = parser.current_source_location(); let url = parser.expect_url()?; let node_id = NodeId::parse(&url).map_err(|e| loc.new_custom_error(ValueErrorKind::from(e)))?; Ok(Iri::Resource(Box::new(node_id))) } } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_none() { assert_eq!(Iri::parse_str("none").unwrap(), Iri::None); } #[test] fn parses_url() { assert_eq!( Iri::parse_str("url(#bar)").unwrap(), Iri::Resource(Box::new(NodeId::Internal("bar".to_string()))) ); assert_eq!( Iri::parse_str("url(foo#bar)").unwrap(), Iri::Resource(Box::new(NodeId::External( "foo".to_string(), "bar".to_string() ))) ); // be permissive if the closing ) is missing assert_eq!( Iri::parse_str("url(#bar").unwrap(), Iri::Resource(Box::new(NodeId::Internal("bar".to_string()))) ); assert_eq!( Iri::parse_str("url(foo#bar").unwrap(), Iri::Resource(Box::new(NodeId::External( "foo".to_string(), "bar".to_string() ))) ); assert!(Iri::parse_str("").is_err()); assert!(Iri::parse_str("foo").is_err()); assert!(Iri::parse_str("url(foo)bar").is_err()); } } librsvg-2.59.0/src/layout.rs000064400000000000000000000311551046102023000140500ustar 00000000000000//! Layout tree. //! //! The idea is to take the DOM tree and produce a layout tree with SVG concepts. use std::rc::Rc; use cssparser::Color; use float_cmp::approx_eq; use crate::aspect_ratio::AspectRatio; use crate::bbox::BoundingBox; use crate::coord_units::CoordUnits; use crate::dasharray::Dasharray; use crate::document::AcquiredNodes; use crate::element::{Element, ElementData}; use crate::filter::FilterValueList; use crate::length::*; use crate::node::*; use crate::paint_server::{PaintSource, UserSpacePaintSource}; use crate::path_builder::Path; use crate::properties::{ self, ClipRule, ComputedValues, Direction, FillRule, FontFamily, FontStretch, FontStyle, FontVariant, FontWeight, ImageRendering, Isolation, MixBlendMode, Opacity, Overflow, PaintOrder, ShapeRendering, StrokeDasharray, StrokeLinecap, StrokeLinejoin, StrokeMiterlimit, TextDecoration, TextRendering, UnicodeBidi, VectorEffect, XmlLang, }; use crate::rect::Rect; use crate::rsvg_log; use crate::session::Session; use crate::surface_utils::shared_surface::SharedImageSurface; use crate::transform::Transform; use crate::unit_interval::UnitInterval; use crate::viewbox::ViewBox; use crate::{borrow_element_as, is_element_of_type}; /// SVG Stacking context, an inner node in the layout tree. /// /// /// /// This is not strictly speaking an SVG2 stacking context, but a /// looser version of it. For example. the SVG spec mentions that a /// an element should establish a stacking context if the `filter` /// property applies to the element and is not `none`. In that case, /// the element is rendered as an "isolated group" - /// /// /// Here we store all the parameters that may lead to the decision to actually /// render an element as an isolated group. pub struct StackingContext { pub element_name: String, pub transform: Transform, pub opacity: Opacity, pub filter: Option, pub clip_rect: Option, pub clip_in_user_space: Option, pub clip_in_object_space: Option, pub mask: Option, pub mix_blend_mode: MixBlendMode, pub isolation: Isolation, /// Target from an `` element pub link_target: Option, } /// The item being rendered inside a stacking context. pub struct Layer { pub kind: LayerKind, pub stacking_ctx: StackingContext, } pub enum LayerKind { Shape(Box), Text(Box), Image(Box), Group(Box), } pub struct Group { pub children: Vec, pub is_visible: bool, // FIXME: move to Layer? All of them have this... pub establish_viewport: Option, } /// Used for elements that need to establish a new viewport, like ``. pub struct LayoutViewport { // transform goes in the group's layer's StackingContext /// Position and size of the element, per its x/y/width/height properties. /// For markers, this is markerWidth/markerHeight. pub geometry: Rect, /// viewBox attribute pub vbox: Option, /// preserveAspectRatio attribute pub preserve_aspect_ratio: AspectRatio, /// overflow property pub overflow: Overflow, } /// Stroke parameters in user-space coordinates. pub struct Stroke { pub width: f64, pub miter_limit: StrokeMiterlimit, pub line_cap: StrokeLinecap, pub line_join: StrokeLinejoin, pub dash_offset: f64, pub dashes: Box<[f64]>, // https://svgwg.org/svg2-draft/painting.html#non-scaling-stroke pub non_scaling: bool, } /// Paths and basic shapes resolved to a path. pub struct Shape { pub path: Rc, pub extents: Option, pub is_visible: bool, pub paint_order: PaintOrder, pub stroke: Stroke, pub stroke_paint: UserSpacePaintSource, pub fill_paint: UserSpacePaintSource, pub fill_rule: FillRule, pub clip_rule: ClipRule, pub shape_rendering: ShapeRendering, pub marker_start: Marker, pub marker_mid: Marker, pub marker_end: Marker, } pub struct Marker { pub node_ref: Option, pub context_stroke: Rc, pub context_fill: Rc, } /// Image in user-space coordinates. pub struct Image { pub surface: SharedImageSurface, pub is_visible: bool, pub rect: Rect, pub aspect: AspectRatio, pub overflow: Overflow, pub image_rendering: ImageRendering, } /// A single text span in user-space coordinates. pub struct TextSpan { pub layout: pango::Layout, pub gravity: pango::Gravity, pub bbox: Option, pub is_visible: bool, pub x: f64, pub y: f64, pub paint_order: PaintOrder, pub stroke: Stroke, pub stroke_paint: UserSpacePaintSource, pub fill_paint: UserSpacePaintSource, pub text_rendering: TextRendering, pub link_target: Option, } /// Fully laid-out text in user-space coordinates. pub struct Text { pub spans: Vec, } /// Font-related properties extracted from `ComputedValues`. pub struct FontProperties { pub xml_lang: XmlLang, pub unicode_bidi: UnicodeBidi, pub direction: Direction, pub font_family: FontFamily, pub font_style: FontStyle, pub font_variant: FontVariant, pub font_weight: FontWeight, pub font_stretch: FontStretch, pub font_size: f64, pub letter_spacing: f64, pub text_decoration: TextDecoration, } pub struct Filter { pub filter_list: FilterValueList, pub current_color: Color, pub stroke_paint_source: Rc, pub fill_paint_source: Rc, pub normalize_values: NormalizeValues, } fn get_filter( values: &ComputedValues, acquired_nodes: &mut AcquiredNodes<'_>, session: &Session, ) -> Option { match values.filter() { properties::Filter::None => None, properties::Filter::List(filter_list) => Some(get_filter_from_filter_list( filter_list, acquired_nodes, values, session, )), } } fn get_filter_from_filter_list( filter_list: FilterValueList, acquired_nodes: &mut AcquiredNodes<'_>, values: &ComputedValues, session: &Session, ) -> Filter { let current_color = values.color().0; let stroke_paint_source = values.stroke().0.resolve( acquired_nodes, values.stroke_opacity().0, current_color, None, None, session, ); let fill_paint_source = values.fill().0.resolve( acquired_nodes, values.fill_opacity().0, current_color, None, None, session, ); let normalize_values = NormalizeValues::new(values); Filter { filter_list, current_color, stroke_paint_source, fill_paint_source, normalize_values, } } impl StackingContext { pub fn new( session: &Session, acquired_nodes: &mut AcquiredNodes<'_>, element: &Element, transform: Transform, clip_rect: Option, values: &ComputedValues, ) -> StackingContext { let element_name = format!("{element}"); let opacity; let filter; match element.element_data { // "The opacity, filter and display properties do not apply to the mask element" // https://drafts.fxtf.org/css-masking-1/#MaskElement ElementData::Mask(_) => { opacity = Opacity(UnitInterval::clamp(1.0)); filter = None; } _ => { opacity = values.opacity(); filter = get_filter(values, acquired_nodes, session); } } let clip_path = values.clip_path(); let clip_uri = clip_path.0.get(); let (clip_in_user_space, clip_in_object_space) = clip_uri .and_then(|node_id| { acquired_nodes .acquire(node_id) .ok() .filter(|a| is_element_of_type!(*a.get(), ClipPath)) }) .map(|acquired| { let clip_node = acquired.get().clone(); let units = borrow_element_as!(clip_node, ClipPath).get_units(); match units { CoordUnits::UserSpaceOnUse => (Some(clip_node), None), CoordUnits::ObjectBoundingBox => (None, Some(clip_node)), } }) .unwrap_or((None, None)); let mask = values.mask().0.get().and_then(|mask_id| { if let Ok(acquired) = acquired_nodes.acquire(mask_id) { let node = acquired.get(); match *node.borrow_element_data() { ElementData::Mask(_) => Some(node.clone()), _ => { rsvg_log!( session, "element {} references \"{}\" which is not a mask", element, mask_id ); None } } } else { rsvg_log!( session, "element {} references nonexistent mask \"{}\"", element, mask_id ); None } }); let mix_blend_mode = values.mix_blend_mode(); let isolation = values.isolation(); StackingContext { element_name, transform, opacity, filter, clip_rect, clip_in_user_space, clip_in_object_space, mask, mix_blend_mode, isolation, link_target: None, } } pub fn new_with_link( session: &Session, acquired_nodes: &mut AcquiredNodes<'_>, element: &Element, transform: Transform, values: &ComputedValues, link_target: Option, ) -> StackingContext { // Note that the clip_rect=Some(...) argument is only used by the markers code, // hence it is None here. Something to refactor later. let mut ctx = Self::new(session, acquired_nodes, element, transform, None, values); ctx.link_target = link_target; ctx } pub fn should_isolate(&self) -> bool { let Opacity(UnitInterval(opacity)) = self.opacity; match self.isolation { Isolation::Auto => { let is_opaque = approx_eq!(f64, opacity, 1.0); !(is_opaque && self.filter.is_none() && self.mask.is_none() && self.mix_blend_mode == MixBlendMode::Normal && self.clip_in_object_space.is_none()) } Isolation::Isolate => true, } } } impl Stroke { pub fn new(values: &ComputedValues, params: &NormalizeParams) -> Stroke { let width = values.stroke_width().0.to_user(params); let miter_limit = values.stroke_miterlimit(); let line_cap = values.stroke_line_cap(); let line_join = values.stroke_line_join(); let dash_offset = values.stroke_dashoffset().0.to_user(params); let non_scaling = values.vector_effect() == VectorEffect::NonScalingStroke; let dashes = match values.stroke_dasharray() { StrokeDasharray(Dasharray::None) => Box::new([]), StrokeDasharray(Dasharray::Array(dashes)) => dashes .iter() .map(|l| l.to_user(params)) .collect::>(), }; Stroke { width, miter_limit, line_cap, line_join, dash_offset, dashes, non_scaling, } } } impl FontProperties { /// Collects font properties from a `ComputedValues`. /// /// The `writing-mode` property is passed separately, as it must come from the `` element, /// not the `` whose computed values are being passed. pub fn new(values: &ComputedValues, params: &NormalizeParams) -> FontProperties { FontProperties { xml_lang: values.xml_lang(), unicode_bidi: values.unicode_bidi(), direction: values.direction(), font_family: values.font_family(), font_style: values.font_style(), font_variant: values.font_variant(), font_weight: values.font_weight(), font_stretch: values.font_stretch(), font_size: values.font_size().to_user(params), letter_spacing: values.letter_spacing().to_user(params), text_decoration: values.text_decoration(), } } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/length.rs������������������������������������������������������������������������0000644�0000000�0000000�00000060711�10461020230�0014014�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! CSS length values. //! //! [`CssLength`] is the struct librsvg uses to represent CSS lengths. //! See its documentation for examples of how to construct it. //! //! `CssLength` values need to know whether they will be normalized with respect to the width, //! height, or both dimensions of the current viewport. `CssLength` values can be signed or //! unsigned. So, a `CssLength` has two type parameters, [`Normalize`] and [`Validate`]; //! the full type is `CssLength`. We provide [`Horizontal`], //! [`Vertical`], and [`Both`] implementations of [`Normalize`]; these let length values know //! how to normalize themselves with respect to the current viewport. We also provide //! [`Signed`] and [`Unsigned`] implementations of [`Validate`]. //! //! For ease of use, we define two type aliases [`Length`] and [`ULength`] corresponding to //! signed and unsigned. //! //! For example, the implementation of [`Circle`][crate::shapes::Circle] defines this //! structure with fields for the `(center_x, center_y, radius)`: //! //! ``` //! # use rsvg::doctest_only::{Length,ULength,Horizontal,Vertical,Both}; //! pub struct Circle { //! cx: Length, //! cy: Length, //! r: ULength, //! } //! ``` //! //! This means that: //! //! * `cx` and `cy` define the center of the circle, they can be positive or negative, and //! they will be normalized with respect to the current viewport's width and height, //! respectively. If the SVG document specified ``, the values //! would be normalized to be at 50% of the the viewport's width, and 30% of the viewport's //! height. //! //! * `r` is non-negative and needs to be resolved against the [normalized diagonal][diag] //! of the current viewport. //! //! The `N` type parameter of `CssLength` is enough to know how to normalize a length //! value; the [`CssLength::to_user`] method will handle it automatically. //! //! [diag]: https://www.w3.org/TR/SVG/coords.html#Units use cssparser::{match_ignore_ascii_case, Parser, Token}; use std::f64::consts::*; use std::fmt; use std::marker::PhantomData; use crate::dpi::Dpi; use crate::drawing_ctx::Viewport; use crate::error::*; use crate::parsers::{finite_f32, Parse}; use crate::properties::{ComputedValues, FontSize, TextOrientation, WritingMode}; use crate::rect::Rect; use crate::viewbox::ViewBox; /// Units for length values. // This needs to be kept in sync with `rsvg.h:RsvgUnit`. #[non_exhaustive] #[repr(C)] #[derive(Debug, PartialEq, Copy, Clone)] pub enum LengthUnit { /// `1.0` means 100% Percent, /// Pixels, or the CSS default unit Px, /// Size of the current font Em, /// x-height of the current font Ex, /// Inches (25.4 mm) In, /// Centimeters Cm, /// Millimeters Mm, /// Points (1/72 inch) Pt, /// Picas (12 points) Pc, /// Advance measure of a '0' character (depends on the text orientation) Ch, } /// A CSS length value. /// /// This is equivalent to [CSS lengths]. /// /// [CSS lengths]: https://www.w3.org/TR/CSS22/syndata.html#length-units /// /// It is up to the calling application to convert lengths in non-pixel units (i.e. those /// where the [`unit`][RsvgLength::unit] field is not [`LengthUnit::Px`]) into something /// meaningful to the application. For example, if your application knows the /// dots-per-inch (DPI) it is using, it can convert lengths with [`unit`] in /// [`LengthUnit::In`] or other physical units. // Keep this in sync with rsvg.h:RsvgLength #[repr(C)] #[derive(Debug, PartialEq, Copy, Clone)] pub struct RsvgLength { /// Numeric part of the length pub length: f64, /// Unit part of the length pub unit: LengthUnit, } impl RsvgLength { /// Constructs a CSS length value. pub fn new(l: f64, unit: LengthUnit) -> RsvgLength { RsvgLength { length: l, unit } } } /// Used for the `N` type parameter of `CssLength`. pub trait Normalize { /// Computes an orientation-based scaling factor. /// /// This is used in the [`CssLength::to_user`] method to resolve lengths with percentage /// units; they need to be resolved with respect to the width, height, or [normalized /// diagonal][diag] of the current viewport. /// /// [diag]: https://www.w3.org/TR/SVG/coords.html#Units fn normalize(x: f64, y: f64) -> f64; } /// Allows declaring `CssLength`. #[derive(Debug, PartialEq, Copy, Clone)] pub struct Horizontal; /// Allows declaring `CssLength`. #[derive(Debug, PartialEq, Copy, Clone)] pub struct Vertical; /// Allows declaring `CssLength`. #[derive(Debug, PartialEq, Copy, Clone)] pub struct Both; impl Normalize for Horizontal { #[inline] fn normalize(x: f64, _y: f64) -> f64 { x } } impl Normalize for Vertical { #[inline] fn normalize(_x: f64, y: f64) -> f64 { y } } impl Normalize for Both { #[inline] fn normalize(x: f64, y: f64) -> f64 { viewport_percentage(x, y) } } /// Used for the `V` type parameter of `CssLength`. pub trait Validate { /// Checks if the specified value is acceptable /// /// This is used when parsing a length value fn validate(v: f64) -> Result { Ok(v) } } #[derive(Debug, PartialEq, Copy, Clone)] pub struct Signed; impl Validate for Signed {} #[derive(Debug, PartialEq, Copy, Clone)] pub struct Unsigned; impl Validate for Unsigned { fn validate(v: f64) -> Result { if v >= 0.0 { Ok(v) } else { Err(ValueErrorKind::Value( "value must be non-negative".to_string(), )) } } } /// A CSS length value. /// /// This is equivalent to [CSS lengths]. /// /// [CSS lengths]: https://www.w3.org/TR/CSS22/syndata.html#length-units /// /// `CssLength` implements the [`Parse`] trait, so it can be parsed out of a /// [`cssparser::Parser`]. /// /// This type will be normally used through the type aliases [`Length`] and [`ULength`] /// /// Examples of construction: /// /// ``` /// # use rsvg::doctest_only::{Length,ULength,LengthUnit,Horizontal,Vertical,Both}; /// # use rsvg::doctest_only::Parse; /// // Explicit type /// let width: Length = Length::new(42.0, LengthUnit::Cm); /// /// // Inferred type /// let height = Length::::new(42.0, LengthUnit::Cm); /// /// // Parsed /// let radius = ULength::::parse_str("5px").unwrap(); /// ``` /// /// During the rendering phase, a `CssLength` needs to be converted to user-space /// coordinates with the [`CssLength::to_user`] method. #[derive(Debug, PartialEq, Copy, Clone)] pub struct CssLength { /// Numeric part of the length pub length: f64, /// Unit part of the length pub unit: LengthUnit, /// Dummy; used internally for the type parameter `N` orientation: PhantomData, /// Dummy; used internally for the type parameter `V` validation: PhantomData, } impl From> for RsvgLength { fn from(l: CssLength) -> RsvgLength { RsvgLength { length: l.length, unit: l.unit, } } } impl Default for CssLength { fn default() -> Self { CssLength::new(0.0, LengthUnit::Px) } } pub const POINTS_PER_INCH: f64 = 72.0; const CM_PER_INCH: f64 = 2.54; const MM_PER_INCH: f64 = 25.4; const PICA_PER_INCH: f64 = 6.0; impl Parse for CssLength { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result, ParseError<'i>> { let l_value; let l_unit; let token = parser.next()?.clone(); match token { Token::Number { value, .. } => { l_value = value; l_unit = LengthUnit::Px; } Token::Percentage { unit_value, .. } => { l_value = unit_value; l_unit = LengthUnit::Percent; } Token::Dimension { value, ref unit, .. } => { l_value = value; l_unit = match_ignore_ascii_case! {unit.as_ref(), "px" => LengthUnit::Px, "em" => LengthUnit::Em, "ex" => LengthUnit::Ex, "in" => LengthUnit::In, "cm" => LengthUnit::Cm, "mm" => LengthUnit::Mm, "pt" => LengthUnit::Pt, "pc" => LengthUnit::Pc, "ch" => LengthUnit::Ch, _ => return Err(parser.new_unexpected_token_error(token)), }; } _ => return Err(parser.new_unexpected_token_error(token)), } let l_value = f64::from(finite_f32(l_value).map_err(|e| parser.new_custom_error(e))?); ::validate(l_value) .map_err(|e| parser.new_custom_error(e)) .map(|l_value| CssLength::new(l_value, l_unit)) } } /// Parameters for length normalization extracted from [`ComputedValues`]. /// /// This is a precursor to [`NormalizeParams::from_values`], for cases where it is inconvenient /// to keep a [`ComputedValues`] around. pub struct NormalizeValues { font_size: FontSize, is_vertical_text: bool, } impl NormalizeValues { pub fn new(values: &ComputedValues) -> NormalizeValues { let is_vertical_text = matches!( (values.writing_mode(), values.text_orientation()), (WritingMode::VerticalLr, TextOrientation::Upright) | (WritingMode::VerticalRl, TextOrientation::Upright) ); NormalizeValues { font_size: values.font_size(), is_vertical_text, } } } /// Parameters to normalize [`Length`] values to user-space distances. pub struct NormalizeParams { vbox: ViewBox, font_size: f64, dpi: Dpi, is_vertical_text: bool, } impl NormalizeParams { /// Extracts the information needed to normalize [`Length`] values from a set of /// [`ComputedValues`] and the viewport size in [`Viewport`]. pub fn new(values: &ComputedValues, viewport: &Viewport) -> NormalizeParams { let v = NormalizeValues::new(values); NormalizeParams::from_values(&v, viewport) } pub fn from_values(v: &NormalizeValues, viewport: &Viewport) -> NormalizeParams { NormalizeParams { vbox: viewport.vbox, font_size: font_size_from_values(v, viewport.dpi), dpi: viewport.dpi, is_vertical_text: v.is_vertical_text, } } /// Just used by rsvg-convert, where there is no font size nor viewport. pub fn from_dpi(dpi: Dpi) -> NormalizeParams { NormalizeParams { vbox: ViewBox::from(Rect::default()), font_size: 1.0, dpi, is_vertical_text: false, } } } impl CssLength { /// Creates a CssLength. /// /// The compiler needs to know the type parameters `N` and `V` which represents the /// length's orientation and validation. /// You can specify them explicitly, or call the parametrized method: /// /// ``` /// # use rsvg::doctest_only::{Length,LengthUnit,Horizontal,Vertical}; /// // Explicit type /// let width: Length = Length::new(42.0, LengthUnit::Cm); /// /// // Inferred type /// let height = Length::::new(42.0, LengthUnit::Cm); /// ``` pub fn new(l: f64, unit: LengthUnit) -> CssLength { CssLength { length: l, unit, orientation: PhantomData, validation: PhantomData, } } /// Convert a Length with units into user-space coordinates. /// /// Lengths may come with non-pixel units, and when rendering, they need to be normalized /// to pixels based on the current viewport (e.g. for lengths with percent units), and /// based on the current element's set of [`ComputedValues`] (e.g. for lengths with `Em` /// units that need to be resolved against the current font size). /// /// Those parameters can be obtained with [`NormalizeParams::new()`]. pub fn to_user(&self, params: &NormalizeParams) -> f64 { match self.unit { LengthUnit::Px => self.length, LengthUnit::Percent => { self.length * ::normalize(params.vbox.width(), params.vbox.height()) } LengthUnit::Em => self.length * params.font_size, LengthUnit::Ex => self.length * params.font_size / 2.0, // how far "0" advances the text, so it varies depending on orientation // we're using the 0.5em or 1.0em (based on orientation) fallback from the spec LengthUnit::Ch => { if params.is_vertical_text { self.length * params.font_size } else { self.length * params.font_size / 2.0 } } LengthUnit::In => self.length * ::normalize(params.dpi.x, params.dpi.y), LengthUnit::Cm => { self.length * ::normalize(params.dpi.x, params.dpi.y) / CM_PER_INCH } LengthUnit::Mm => { self.length * ::normalize(params.dpi.x, params.dpi.y) / MM_PER_INCH } LengthUnit::Pt => { self.length * ::normalize(params.dpi.x, params.dpi.y) / POINTS_PER_INCH } LengthUnit::Pc => { self.length * ::normalize(params.dpi.x, params.dpi.y) / PICA_PER_INCH } } } /// Converts a Length to points. Pixels are taken to be respect with the DPI. /// /// # Panics /// /// Will panic if the length is in Percent, Em, or Ex units. pub fn to_points(&self, params: &NormalizeParams) -> f64 { match self.unit { LengthUnit::Px => { self.length / ::normalize(params.dpi.x, params.dpi.y) * 72.0 } LengthUnit::Percent => { panic!("Cannot convert a percentage length into an absolute length"); } LengthUnit::Em => { panic!("Cannot convert an Em length into an absolute length"); } LengthUnit::Ex => { panic!("Cannot convert an Ex length into an absolute length"); } LengthUnit::In => self.length * POINTS_PER_INCH, LengthUnit::Cm => self.length / CM_PER_INCH * POINTS_PER_INCH, LengthUnit::Mm => self.length / MM_PER_INCH * POINTS_PER_INCH, LengthUnit::Pt => self.length, LengthUnit::Pc => self.length / PICA_PER_INCH * POINTS_PER_INCH, LengthUnit::Ch => { panic!("Cannot convert a Ch length into an absolute length"); } } } pub fn to_inches(&self, params: &NormalizeParams) -> f64 { self.to_points(params) / POINTS_PER_INCH } pub fn to_cm(&self, params: &NormalizeParams) -> f64 { self.to_inches(params) * CM_PER_INCH } pub fn to_mm(&self, params: &NormalizeParams) -> f64 { self.to_inches(params) * MM_PER_INCH } pub fn to_picas(&self, params: &NormalizeParams) -> f64 { self.to_inches(params) * PICA_PER_INCH } } fn font_size_from_values(values: &NormalizeValues, dpi: Dpi) -> f64 { let v = values.font_size.value(); match v.unit { LengthUnit::Percent => unreachable!("ComputedValues can't have a relative font size"), LengthUnit::Px => v.length, // The following implies that our default font size is 12, which // matches the default from the FontSize property. LengthUnit::Em => v.length * 12.0, LengthUnit::Ex => v.length * 12.0 / 2.0, LengthUnit::Ch => v.length * 12.0 / 2.0, // FontSize always is a Both, per properties.rs LengthUnit::In => v.length * Both::normalize(dpi.x, dpi.y), LengthUnit::Cm => v.length * Both::normalize(dpi.x, dpi.y) / CM_PER_INCH, LengthUnit::Mm => v.length * Both::normalize(dpi.x, dpi.y) / MM_PER_INCH, LengthUnit::Pt => v.length * Both::normalize(dpi.x, dpi.y) / POINTS_PER_INCH, LengthUnit::Pc => v.length * Both::normalize(dpi.x, dpi.y) / PICA_PER_INCH, } } fn viewport_percentage(x: f64, y: f64) -> f64 { // https://www.w3.org/TR/SVG/coords.html#Units // "For any other length value expressed as a percentage of the viewport, the // percentage is calculated as the specified percentage of // sqrt((actual-width)**2 + (actual-height)**2))/sqrt(2)." (x * x + y * y).sqrt() / SQRT_2 } /// Alias for `CssLength` types that can have negative values pub type Length = CssLength; /// Alias for `CssLength` types that are non negative pub type ULength = CssLength; #[derive(Debug, Default, PartialEq, Copy, Clone)] pub enum LengthOrAuto { #[default] Auto, Length(CssLength), } impl Parse for LengthOrAuto { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result, ParseError<'i>> { if parser .try_parse(|i| i.expect_ident_matching("auto")) .is_ok() { Ok(LengthOrAuto::Auto) } else { Ok(LengthOrAuto::Length(CssLength::parse(parser)?)) } } } impl fmt::Display for LengthUnit { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let unit = match &self { LengthUnit::Percent => "%", LengthUnit::Px => "px", LengthUnit::Em => "em", LengthUnit::Ex => "ex", LengthUnit::In => "in", LengthUnit::Cm => "cm", LengthUnit::Mm => "mm", LengthUnit::Pt => "pt", LengthUnit::Pc => "pc", LengthUnit::Ch => "ch", }; write!(f, "{unit}") } } #[cfg(test)] mod tests { use super::*; use crate::properties::{ParsedProperty, SpecifiedValue, SpecifiedValues}; use crate::{assert_approx_eq_cairo, float_eq_cairo::ApproxEqCairo}; #[test] fn parses_default() { assert_eq!( Length::::parse_str("42").unwrap(), Length::::new(42.0, LengthUnit::Px) ); assert_eq!( Length::::parse_str("-42px").unwrap(), Length::::new(-42.0, LengthUnit::Px) ); } #[test] fn parses_percent() { assert_eq!( Length::::parse_str("50.0%").unwrap(), Length::::new(0.5, LengthUnit::Percent) ); } #[test] fn parses_font_em() { assert_eq!( Length::::parse_str("22.5em").unwrap(), Length::::new(22.5, LengthUnit::Em) ); } #[test] fn parses_font_ex() { assert_eq!( Length::::parse_str("22.5ex").unwrap(), Length::::new(22.5, LengthUnit::Ex) ); } #[test] fn parses_font_ch() { assert_eq!( Length::::parse_str("22.5ch").unwrap(), Length::::new(22.5, LengthUnit::Ch) ); } #[test] fn parses_physical_units() { assert_eq!( Length::::parse_str("72pt").unwrap(), Length::::new(72.0, LengthUnit::Pt) ); assert_eq!( Length::::parse_str("-22.5in").unwrap(), Length::::new(-22.5, LengthUnit::In) ); assert_eq!( Length::::parse_str("-254cm").unwrap(), Length::::new(-254.0, LengthUnit::Cm) ); assert_eq!( Length::::parse_str("254mm").unwrap(), Length::::new(254.0, LengthUnit::Mm) ); assert_eq!( Length::::parse_str("60pc").unwrap(), Length::::new(60.0, LengthUnit::Pc) ); } #[test] fn parses_unsigned() { assert_eq!( ULength::::parse_str("42").unwrap(), ULength::::new(42.0, LengthUnit::Px) ); assert_eq!( ULength::::parse_str("0pt").unwrap(), ULength::::new(0.0, LengthUnit::Pt) ); assert!(ULength::::parse_str("-42px").is_err()); } #[test] fn empty_length_yields_error() { assert!(Length::::parse_str("").is_err()); } #[test] fn invalid_unit_yields_error() { assert!(Length::::parse_str("8furlong").is_err()); } #[test] fn normalize_default_works() { let viewport = Viewport::new(Dpi::new(40.0, 40.0), 100.0, 100.0); let values = ComputedValues::default(); let params = NormalizeParams::new(&values, &viewport); assert_approx_eq_cairo!( Length::::new(10.0, LengthUnit::Px).to_user(¶ms), 10.0 ); } #[test] fn normalize_absolute_units_works() { let viewport = Viewport::new(Dpi::new(40.0, 50.0), 100.0, 100.0); let values = ComputedValues::default(); let params = NormalizeParams::new(&values, &viewport); assert_approx_eq_cairo!( Length::::new(10.0, LengthUnit::In).to_user(¶ms), 400.0 ); assert_approx_eq_cairo!( Length::::new(10.0, LengthUnit::In).to_user(¶ms), 500.0 ); assert_approx_eq_cairo!( Length::::new(10.0, LengthUnit::Cm).to_user(¶ms), 400.0 / CM_PER_INCH ); assert_approx_eq_cairo!( Length::::new(10.0, LengthUnit::Mm).to_user(¶ms), 400.0 / MM_PER_INCH ); assert_approx_eq_cairo!( Length::::new(10.0, LengthUnit::Pt).to_user(¶ms), 400.0 / POINTS_PER_INCH ); assert_approx_eq_cairo!( Length::::new(10.0, LengthUnit::Pc).to_user(¶ms), 400.0 / PICA_PER_INCH ); } #[test] fn normalize_percent_works() { let viewport = Viewport::new(Dpi::new(40.0, 40.0), 100.0, 200.0); let values = ComputedValues::default(); let params = NormalizeParams::new(&values, &viewport); assert_approx_eq_cairo!( Length::::new(0.05, LengthUnit::Percent).to_user(¶ms), 5.0 ); assert_approx_eq_cairo!( Length::::new(0.05, LengthUnit::Percent).to_user(¶ms), 10.0 ); } #[test] fn normalize_font_em_ex_ch_works() { let mut values = ComputedValues::default(); let viewport = Viewport::new(Dpi::new(40.0, 40.0), 100.0, 200.0); let mut params = NormalizeParams::new(&values, &viewport); // These correspond to the default size for the font-size // property and the way we compute Em/Ex from that. assert_approx_eq_cairo!( Length::::new(1.0, LengthUnit::Em).to_user(¶ms), 12.0 ); assert_approx_eq_cairo!( Length::::new(1.0, LengthUnit::Ex).to_user(¶ms), 6.0 ); assert_approx_eq_cairo!( Length::::new(1.0, LengthUnit::Ch).to_user(¶ms), 6.0 ); // check for vertical upright text let mut specified = SpecifiedValues::default(); specified.set_parsed_property(&ParsedProperty::TextOrientation(SpecifiedValue::Specified( TextOrientation::Upright, ))); specified.set_parsed_property(&ParsedProperty::WritingMode(SpecifiedValue::Specified( WritingMode::VerticalLr, ))); specified.to_computed_values(&mut values); params = NormalizeParams::new(&values, &viewport); assert_approx_eq_cairo!( Length::::new(1.0, LengthUnit::Ch).to_user(¶ms), 12.0 ); } #[test] fn to_points_works() { let params = NormalizeParams::from_dpi(Dpi::new(40.0, 96.0)); assert_approx_eq_cairo!( Length::::new(80.0, LengthUnit::Px).to_points(¶ms), 2.0 * 72.0 ); assert_approx_eq_cairo!( Length::::new(192.0, LengthUnit::Px).to_points(¶ms), 2.0 * 72.0 ); } } �������������������������������������������������������librsvg-2.59.0/src/lib.rs���������������������������������������������������������������������������0000644�0000000�0000000�00000021230�10461020230�0013272�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Load and render SVG images into Cairo surfaces. //! //! This crate can load SVG images and render them to Cairo surfaces, //! using a mixture of SVG's [static mode] and [secure static mode]. //! Librsvg does not do animation nor scripting, and can load //! references to external data only in some situations; see below. //! //! Librsvg supports reading [SVG 1.1] data, and is gradually adding //! support for features in [SVG 2]. Librsvg also supports SVGZ //! files, which are just an SVG stream compressed with the GZIP //! algorithm. //! //! # Basic usage //! //! * Create a [`Loader`] struct. //! * Get an [`SvgHandle`] from the [`Loader`]. //! * Create a [`CairoRenderer`] for the [`SvgHandle`] and render to a Cairo context. //! //! You can put the following in your `Cargo.toml`: //! //! ```toml //! [dependencies] //! librsvg = "2.59.0" //! cairo-rs = "0.20" //! gio = "0.20" # only if you need streams //! ``` //! //! # Example //! //! ``` //! const WIDTH: i32 = 640; //! const HEIGHT: i32 = 480; //! //! fn main() { //! // Loading from a file //! //! let handle = rsvg::Loader::new().read_path("example.svg").unwrap(); //! //! let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, WIDTH, HEIGHT).unwrap(); //! let cr = cairo::Context::new(&surface).expect("Failed to create a cairo context"); //! //! let renderer = rsvg::CairoRenderer::new(&handle); //! renderer.render_document( //! &cr, //! &cairo::Rectangle::new(0.0, 0.0, f64::from(WIDTH), f64::from(HEIGHT)) //! ).unwrap(); //! //! // Loading from a static SVG asset //! //! let bytes = glib::Bytes::from_static( //! br#" //! //! //! //! "# //! ); //! let stream = gio::MemoryInputStream::from_bytes(&bytes); //! //! let handle = rsvg::Loader::new().read_stream( //! &stream, //! None::<&gio::File>, // no base file as this document has no references //! None::<&gio::Cancellable>, // no cancellable //! ).unwrap(); //! } //! ``` //! //! # The "base file" and resolving references to external files //! //! When you load an SVG, librsvg needs to know the location of the "base file" //! for it. This is so that librsvg can determine the location of referenced //! entities. For example, say you have an SVG in /foo/bar/foo.svg //! and that it has an image element like this: //! //! ```xml //! //! ``` //! //! In this case, librsvg needs to know the location of the toplevel //! `/foo/bar/foo.svg` so that it can generate the appropriate //! reference to `/foo/bar/resources/foo.png`. //! //! ## Security and locations of referenced files //! //! When processing an SVG, librsvg will only load referenced files if //! they are in the same directory as the base file, or in a //! subdirectory of it. That is, if the base file is //! `/foo/bar/baz.svg`, then librsvg will only try to load referenced //! files (from SVG's `` element, for example, or from content //! included through XML entities) if those files are in `/foo/bar/*` //! or in `/foo/bar/*/.../*`. This is so that malicious SVG documents //! cannot include files that are in a directory above. //! //! The full set of rules for deciding which URLs may be loaded is as follows; //! they are applied in order. A referenced URL will not be loaded as soon as //! one of these rules fails: //! //! 1. All `data:` URLs may be loaded. These are sometimes used to //! include raster image data, encoded as base-64, directly in an SVG //! file. //! //! 2. URLs with queries ("?") or fragment identifiers ("#") are not allowed. //! //! 3. All URL schemes other than data: in references require a base URL. For //! example, this means that if you load an SVG with [`Loader::read_stream`] //! without providing a `base_file`, then any referenced files will not //! be allowed (e.g. raster images to be loaded from other files will //! not work). //! //! 4. If referenced URLs are absolute, rather than relative, then //! they must have the same scheme as the base URL. For example, if //! the base URL has a "`file`" scheme, then all URL references inside //! the SVG must also have the "`file`" scheme, or be relative //! references which will be resolved against the base URL. //! //! 5. If referenced URLs have a "`resource`" scheme, that is, if they //! are included into your binary program with GLib's resource //! mechanism, they are allowed to be loaded (provided that the base //! URL is also a "`resource`", per the previous rule). //! //! 6. Otherwise, non-`file` schemes are not allowed. For example, //! librsvg will not load `http` resources, to keep malicious SVG data //! from "phoning home". //! //! 7. A relative URL must resolve to the same directory as the base //! URL, or to one of its subdirectories. Librsvg will canonicalize //! filenames, by removing "`..`" path components and resolving symbolic //! links, to decide whether files meet these conditions. //! //! [static mode]: https://www.w3.org/TR/SVG2/conform.html#static-mode //! [secure static mode]: https://www.w3.org/TR/SVG2/conform.html#secure-static-mode //! [SVG 1.1]: https://www.w3.org/TR/SVG11/ //! [SVG 2]: https://www.w3.org/TR/SVG2/ #![doc(html_logo_url = "https://gnome.pages.gitlab.gnome.org/librsvg/Rsvg-2.0/librsvg-r.svg")] #![allow(rustdoc::private_intra_doc_links)] #![allow(clippy::clone_on_ref_ptr)] #![allow(clippy::not_unsafe_ptr_arg_deref)] #![allow(clippy::too_many_arguments)] #![allow(clippy::derive_partial_eq_without_eq)] #![warn(nonstandard_style, rust_2018_idioms, unused)] // Some lints no longer exist #![warn(renamed_and_removed_lints)] // Standalone lints #![warn(trivial_casts, trivial_numeric_casts)] // The public API is exported here pub use crate::api::*; mod accept_language; mod angle; mod api; mod aspect_ratio; mod bbox; mod color; mod cond; mod coord_units; mod css; mod dasharray; mod document; mod dpi; mod drawing_ctx; mod element; mod error; mod filter; mod filter_func; mod filters; mod float_eq_cairo; mod font_props; mod gradient; mod href; mod image; mod io; mod iri; mod layout; mod length; mod limits; mod log; mod marker; mod node; mod paint_server; mod parsers; mod path_builder; mod path_parser; mod pattern; mod properties; mod property_defs; mod property_macros; mod rect; mod session; mod shapes; mod space; mod structure; mod style; mod surface_utils; mod text; mod transform; mod unit_interval; mod url_resolver; mod util; mod viewbox; mod xml; #[cfg(feature = "test-utils")] #[doc(hidden)] pub mod test_utils; #[doc(hidden)] pub mod bench_only { pub use crate::filters::lighting::Normal; pub use crate::path_builder::PathBuilder; pub use crate::path_parser::Lexer; pub use crate::rect::IRect; pub use crate::surface_utils::{ iterators::{PixelRectangle, Pixels}, shared_surface::{ composite_arithmetic, AlphaOnly, ExclusiveImageSurface, Horizontal, NotAlphaOnly, SharedImageSurface, SurfaceType, Vertical, }, srgb::{linearize, map_unpremultiplied_components_loop}, EdgeMode, ImageSurfaceDataExt, Pixel, PixelOps, }; } #[doc(hidden)] #[cfg(feature = "capi")] pub mod c_api_only { pub use crate::dpi::Dpi; pub use crate::rsvg_log; pub use crate::session::Session; pub use crate::surface_utils::shared_surface::{SharedImageSurface, SurfaceType}; pub use crate::surface_utils::{Pixel, PixelOps, ToPixel}; } #[doc(hidden)] pub mod doctest_only { pub use crate::aspect_ratio::AspectRatio; pub use crate::error::AttributeResultExt; pub use crate::error::ElementError; pub use crate::error::ValueErrorKind; pub use crate::href::is_href; pub use crate::href::set_href; pub use crate::length::{Both, CssLength, Horizontal, Length, LengthUnit, ULength, Vertical}; pub use crate::parsers::{Parse, ParseValue}; } #[doc(hidden)] pub mod rsvg_convert_only { pub use crate::aspect_ratio::AspectRatio; pub use crate::dpi::Dpi; pub use crate::drawing_ctx::set_source_color_on_cairo; pub use crate::error::ParseError; pub use crate::length::{ CssLength, Horizontal, Length, Normalize, NormalizeParams, Signed, ULength, Unsigned, Validate, Vertical, }; pub use crate::parsers::{Parse, ParseValue}; pub use crate::rect::Rect; pub use crate::surface_utils::shared_surface::{SharedImageSurface, SurfaceType}; pub use crate::viewbox::ViewBox; } #[doc(hidden)] pub mod tests_only { pub use crate::rect::Rect; pub use crate::surface_utils::shared_surface::{SharedImageSurface, SurfaceType}; } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/limits.rs������������������������������������������������������������������������0000644�0000000�0000000�00000004442�10461020230�0014033�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Processing limits to mitigate malicious SVGs. /// Maximum number of times that elements can be referenced through URL fragments. /// /// This is a mitigation for the security-related bugs: /// /// /// /// Imagine the XML [billion laughs attack], but done in SVG's terms: /// /// - #323 above creates deeply nested groups of `` elements. /// The first one references the second one ten times, the second one /// references the third one ten times, and so on. In the file given, /// this causes 10^17 objects to be rendered. While this does not /// exhaust memory, it would take a really long time. /// /// - #515 has deeply nested references of `` elements. Each /// object inside each pattern has an attribute /// fill="url(#next_pattern)", so the number of final rendered objects /// grows exponentially. /// /// We deal with both cases by placing a limit on how many references /// will be resolved during the SVG rendering process, that is, /// how many `url(#foo)` will be resolved. /// /// [billion laughs attack]: https://bitbucket.org/tiran/defusedxml pub const MAX_REFERENCED_ELEMENTS: usize = 500_000; /// Maximum number of elements loadable per document. /// /// This is a mitigation for SVG files which create millions of elements /// in an attempt to exhaust memory. We don't allow loading more than /// this number of elements during the initial streaming load process. pub const MAX_LOADED_ELEMENTS: usize = 1_000_000; /// Maximum number of attributes per XML element. /// /// A malicious file could have ``, /// etc. Librsvg puts this limit on how many attributes an element can have. pub const MAX_LOADED_ATTRIBUTES: usize = u16::MAX as usize; /// Maximum level of nesting for XInclude (XML Include) files. /// /// See . With /// the use of XML like ``, an /// SVG document can recursively include other XML files. This value /// defines a maximum level of nesting for XInclude, to prevent cases /// where the base document is included within itself, or when two /// documents recursively include each other. pub const MAX_XINCLUDE_DEPTH: usize = 20; ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/log.rs���������������������������������������������������������������������������0000644�0000000�0000000�00000004255�10461020230�0013315�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Utilities for logging messages from the library. #[doc(hidden)] #[macro_export] macro_rules! rsvg_log { ( $session:expr, $($arg:tt)+ ) => { if $session.log_enabled() { println!("{}", format_args!($($arg)+)); } }; } /// Captures the basic state of a [`cairo::Context`] for logging purposes. /// /// A librsvg "transaction" like rendering a /// [`crate::api::SvgHandle`], which takes a Cairo context, depends on the state of the /// context as it was passed in by the caller. For example, librsvg may decide to /// operate differently depending on the context's target surface type, or its current /// transformation matrix. This struct captures that sort of information. #[allow(dead_code)] // this is never constructed yet; allow building on newer rustc which warns about this #[derive(Copy, Clone, Debug, PartialEq)] struct CairoContextState { surface_type: cairo::SurfaceType, matrix: cairo::Matrix, } impl CairoContextState { #[cfg(test)] fn new(cr: &cairo::Context) -> Self { let surface_type = cr.target().type_(); let matrix = cr.matrix(); Self { surface_type, matrix, } } } #[cfg(test)] mod tests { use super::*; #[test] fn captures_cr_state() { let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 10, 10).unwrap(); let cr = cairo::Context::new(&surface).unwrap(); let state = CairoContextState::new(&cr); assert_eq!( CairoContextState { surface_type: cairo::SurfaceType::Image, matrix: cairo::Matrix::identity(), }, state, ); let surface = cairo::RecordingSurface::create(cairo::Content::ColorAlpha, None).unwrap(); let cr = cairo::Context::new(&surface).unwrap(); cr.scale(2.0, 3.0); let state = CairoContextState::new(&cr); let mut matrix = cairo::Matrix::identity(); matrix.scale(2.0, 3.0); assert_eq!( CairoContextState { surface_type: cairo::SurfaceType::Recording, matrix, }, state, ); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/marker.rs������������������������������������������������������������������������0000644�0000000�0000000�00000121463�10461020230�0014016�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! The `marker` element, and geometry computations for markers. use std::f64::consts::*; use std::ops::Deref; use cssparser::Parser; use markup5ever::{expanded_name, local_name, namespace_url, ns}; use crate::angle::Angle; use crate::aspect_ratio::*; use crate::bbox::BoundingBox; use crate::borrow_element_as; use crate::document::AcquiredNodes; use crate::drawing_ctx::{DrawingCtx, Viewport}; use crate::element::{set_attribute, ElementTrait}; use crate::error::*; use crate::float_eq_cairo::ApproxEqCairo; use crate::layout::{self, Shape, StackingContext}; use crate::length::*; use crate::node::{CascadedValues, Node, NodeBorrow, NodeDraw}; use crate::parse_identifiers; use crate::parsers::{Parse, ParseValue}; use crate::path_builder::{arc_segment, ArcParameterization, CubicBezierCurve, Path, PathCommand}; use crate::rect::Rect; use crate::rsvg_log; use crate::session::Session; use crate::transform::Transform; use crate::viewbox::*; use crate::xml::Attributes; // markerUnits attribute: https://www.w3.org/TR/SVG/painting.html#MarkerElement #[derive(Debug, Default, Copy, Clone, PartialEq)] enum MarkerUnits { UserSpaceOnUse, #[default] StrokeWidth, } impl Parse for MarkerUnits { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { Ok(parse_identifiers!( parser, "userSpaceOnUse" => MarkerUnits::UserSpaceOnUse, "strokeWidth" => MarkerUnits::StrokeWidth, )?) } } // orient attribute: https://www.w3.org/TR/SVG/painting.html#MarkerElement #[derive(Debug, Copy, Clone, PartialEq)] enum MarkerOrient { Auto, AutoStartReverse, Angle(Angle), } impl Default for MarkerOrient { #[inline] fn default() -> MarkerOrient { MarkerOrient::Angle(Angle::new(0.0)) } } impl Parse for MarkerOrient { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { if parser .try_parse(|p| p.expect_ident_matching("auto")) .is_ok() { return Ok(MarkerOrient::Auto); } if parser .try_parse(|p| p.expect_ident_matching("auto-start-reverse")) .is_ok() { Ok(MarkerOrient::AutoStartReverse) } else { Angle::parse(parser).map(MarkerOrient::Angle) } } } pub struct Marker { units: MarkerUnits, ref_x: Length, ref_y: Length, width: ULength, height: ULength, orient: MarkerOrient, aspect: AspectRatio, vbox: Option, } impl Default for Marker { fn default() -> Marker { Marker { units: MarkerUnits::default(), ref_x: Default::default(), ref_y: Default::default(), // the following two are per the spec width: ULength::::parse_str("3").unwrap(), height: ULength::::parse_str("3").unwrap(), orient: MarkerOrient::default(), aspect: AspectRatio::default(), vbox: None, } } } impl Marker { fn render( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, xpos: f64, ypos: f64, computed_angle: Angle, line_width: f64, clipping: bool, marker_type: MarkerType, marker: &layout::Marker, ) -> Result { let mut cascaded = CascadedValues::new_from_node(node); cascaded.context_fill = Some(marker.context_fill.clone()); cascaded.context_stroke = Some(marker.context_stroke.clone()); let values = cascaded.get(); let params = NormalizeParams::new(values, viewport); let marker_width = self.width.to_user(¶ms); let marker_height = self.height.to_user(¶ms); if marker_width.approx_eq_cairo(0.0) || marker_height.approx_eq_cairo(0.0) { // markerWidth or markerHeight set to 0 disables rendering of the element // https://www.w3.org/TR/SVG/painting.html#MarkerWidthAttribute return Ok(draw_ctx.empty_bbox()); } let rotation = match self.orient { MarkerOrient::Auto => computed_angle, MarkerOrient::AutoStartReverse => { if marker_type == MarkerType::Start { computed_angle.flip() } else { computed_angle } } MarkerOrient::Angle(a) => a, }; let mut transform = Transform::new_translate(xpos, ypos).pre_rotate(rotation); if self.units == MarkerUnits::StrokeWidth { transform = transform.pre_scale(line_width, line_width); } let content_viewport = if let Some(vbox) = self.vbox { if vbox.is_empty() { return Ok(draw_ctx.empty_bbox()); } let r = self .aspect .compute(&vbox, &Rect::from_size(marker_width, marker_height)); let (vb_width, vb_height) = vbox.size(); transform = transform.pre_scale(r.width() / vb_width, r.height() / vb_height); viewport.with_view_box(vb_width, vb_height) } else { viewport.with_view_box(marker_width, marker_height) }; let content_params = NormalizeParams::new(values, &content_viewport); transform = transform.pre_translate( -self.ref_x.to_user(&content_params), -self.ref_y.to_user(&content_params), ); // FIXME: This is the only place in the code where we pass a Some(rect) to // StackingContext::new() for its clip_rect argument. The effect is to clip to // the viewport that the current marker should establish, but this code for // drawing markers does not yet use the viewports machinery and instead does // things by hand. We should encode the information about the overflow property // in the viewport, so it knows whether to clip or not. let clip_rect = if values.is_overflow() { None } else if let Some(vbox) = self.vbox { Some(*vbox) } else { Some(Rect::from_size(marker_width, marker_height)) }; let elt = node.borrow_element(); let stacking_ctx = StackingContext::new( draw_ctx.session(), acquired_nodes, &elt, transform, clip_rect, values, ); draw_ctx.with_discrete_layer( &stacking_ctx, acquired_nodes, &content_viewport, None, clipping, &mut |an, dc, new_viewport| { node.draw_children(an, &cascaded, new_viewport, dc, clipping) }, // content_viewport ) } } impl ElementTrait for Marker { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "markerUnits") => { set_attribute(&mut self.units, attr.parse(value), session) } expanded_name!("", "refX") => { set_attribute(&mut self.ref_x, attr.parse(value), session) } expanded_name!("", "refY") => { set_attribute(&mut self.ref_y, attr.parse(value), session) } expanded_name!("", "markerWidth") => { set_attribute(&mut self.width, attr.parse(value), session) } expanded_name!("", "markerHeight") => { set_attribute(&mut self.height, attr.parse(value), session) } expanded_name!("", "orient") => { set_attribute(&mut self.orient, attr.parse(value), session) } expanded_name!("", "preserveAspectRatio") => { set_attribute(&mut self.aspect, attr.parse(value), session) } expanded_name!("", "viewBox") => { set_attribute(&mut self.vbox, attr.parse(value), session) } _ => (), } } } } // Machinery to figure out marker orientations #[derive(Debug, PartialEq)] enum Segment { Degenerate { // A single lone point x: f64, y: f64, }, LineOrCurve { x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64, }, } impl Segment { fn degenerate(x: f64, y: f64) -> Segment { Segment::Degenerate { x, y } } fn curve(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) -> Segment { Segment::LineOrCurve { x1, y1, x2, y2, x3, y3, x4, y4, } } fn line(x1: f64, y1: f64, x2: f64, y2: f64) -> Segment { Segment::curve(x1, y1, x2, y2, x1, y1, x2, y2) } // If the segment has directionality, returns two vectors (v1x, v1y, v2x, v2y); otherwise, // returns None. The vectors are the tangents at the beginning and at the end of the segment, // respectively. A segment does not have directionality if it is degenerate (i.e. a single // point) or a zero-length segment, i.e. where all four control points are coincident (the first // and last control points may coincide, but the others may define a loop - thus nonzero length) fn get_directionalities(&self) -> Option<(f64, f64, f64, f64)> { match *self { Segment::Degenerate { .. } => None, Segment::LineOrCurve { x1, y1, x2, y2, x3, y3, x4, y4, } => { let coincide_1_and_2 = points_equal(x1, y1, x2, y2); let coincide_1_and_3 = points_equal(x1, y1, x3, y3); let coincide_1_and_4 = points_equal(x1, y1, x4, y4); let coincide_2_and_3 = points_equal(x2, y2, x3, y3); let coincide_2_and_4 = points_equal(x2, y2, x4, y4); let coincide_3_and_4 = points_equal(x3, y3, x4, y4); if coincide_1_and_2 && coincide_1_and_3 && coincide_1_and_4 { None } else if coincide_1_and_2 && coincide_1_and_3 { Some((x4 - x1, y4 - y1, x4 - x3, y4 - y3)) } else if coincide_1_and_2 && coincide_3_and_4 { Some((x4 - x1, y4 - y1, x4 - x1, y4 - y1)) } else if coincide_2_and_3 && coincide_2_and_4 { Some((x2 - x1, y2 - y1, x4 - x1, y4 - y1)) } else if coincide_1_and_2 { Some((x3 - x1, y3 - y1, x4 - x3, y4 - y3)) } else if coincide_3_and_4 { Some((x2 - x1, y2 - y1, x4 - x2, y4 - y2)) } else { Some((x2 - x1, y2 - y1, x4 - x3, y4 - y3)) } } } } } fn points_equal(x1: f64, y1: f64, x2: f64, y2: f64) -> bool { x1.approx_eq_cairo(x2) && y1.approx_eq_cairo(y2) } enum SegmentState { Initial, NewSubpath, InSubpath, ClosedSubpath, } #[derive(Debug, PartialEq)] struct Segments(Vec); impl Deref for Segments { type Target = [Segment]; fn deref(&self) -> &[Segment] { &self.0 } } // This converts a path builder into a vector of curveto-like segments. // Each segment can be: // // 1. Segment::Degenerate => the segment is actually a single point (x, y) // // 2. Segment::LineOrCurve => either a lineto or a curveto (or the effective // lineto that results from a closepath). // We have the following points: // P1 = (x1, y1) // P2 = (x2, y2) // P3 = (x3, y3) // P4 = (x4, y4) // // The start and end points are P1 and P4, respectively. // The tangent at the start point is given by the vector (P2 - P1). // The tangent at the end point is given by the vector (P4 - P3). // The tangents also work if the segment refers to a lineto (they will // both just point in the same direction). impl From<&Path> for Segments { fn from(path: &Path) -> Segments { let mut last_x: f64; let mut last_y: f64; let mut cur_x: f64 = 0.0; let mut cur_y: f64 = 0.0; let mut subpath_start_x: f64 = 0.0; let mut subpath_start_y: f64 = 0.0; let mut segments = Vec::new(); let mut state = SegmentState::Initial; for path_command in path.iter() { last_x = cur_x; last_y = cur_y; match path_command { PathCommand::MoveTo(x, y) => { cur_x = x; cur_y = y; subpath_start_x = cur_x; subpath_start_y = cur_y; match state { SegmentState::Initial | SegmentState::InSubpath => { // Ignore the very first moveto in a sequence (Initial state), // or if we were already drawing within a subpath, start // a new subpath. state = SegmentState::NewSubpath; } SegmentState::NewSubpath => { // We had just begun a new subpath (i.e. from a moveto) and we got // another moveto? Output a stray point for the // previous moveto. segments.push(Segment::degenerate(last_x, last_y)); state = SegmentState::NewSubpath; } SegmentState::ClosedSubpath => { // Cairo outputs a moveto after every closepath, so that subsequent // lineto/curveto commands will start at the closed vertex. // We don't want to actually emit a point (a degenerate segment) in // that artificial-moveto case. // // We'll reset to the Initial state so that a subsequent "real" // moveto will be handled as the beginning of a new subpath, or a // degenerate point, as usual. state = SegmentState::Initial; } } } PathCommand::LineTo(x, y) => { cur_x = x; cur_y = y; segments.push(Segment::line(last_x, last_y, cur_x, cur_y)); state = SegmentState::InSubpath; } PathCommand::CurveTo(curve) => { let CubicBezierCurve { pt1: (x2, y2), pt2: (x3, y3), to, } = curve; cur_x = to.0; cur_y = to.1; segments.push(Segment::curve(last_x, last_y, x2, y2, x3, y3, cur_x, cur_y)); state = SegmentState::InSubpath; } PathCommand::Arc(arc) => { cur_x = arc.to.0; cur_y = arc.to.1; match arc.center_parameterization() { ArcParameterization::CenterParameters { center, radii, theta1, delta_theta, } => { let rot = arc.x_axis_rotation; let theta2 = theta1 + delta_theta; let n_segs = (delta_theta / (PI * 0.5 + 0.001)).abs().ceil() as u32; let d_theta = delta_theta / f64::from(n_segs); let segment1 = arc_segment(center, radii, rot, theta1, theta1 + d_theta); let segment2 = arc_segment(center, radii, rot, theta2 - d_theta, theta2); let (x2, y2) = segment1.pt1; let (x3, y3) = segment2.pt2; segments .push(Segment::curve(last_x, last_y, x2, y2, x3, y3, cur_x, cur_y)); state = SegmentState::InSubpath; } ArcParameterization::LineTo => { segments.push(Segment::line(last_x, last_y, cur_x, cur_y)); state = SegmentState::InSubpath; } ArcParameterization::Omit => {} } } PathCommand::ClosePath => { cur_x = subpath_start_x; cur_y = subpath_start_y; segments.push(Segment::line(last_x, last_y, cur_x, cur_y)); state = SegmentState::ClosedSubpath; } } } if let SegmentState::NewSubpath = state { // Output a lone point if we started a subpath with a moveto // command, but there are no subsequent commands. segments.push(Segment::degenerate(cur_x, cur_y)); }; Segments(segments) } } // The SVG spec 1.1 says http://www.w3.org/TR/SVG/implnote.html#PathElementImplementationNotes // Certain line-capping and line-joining situations and markers // require that a path segment have directionality at its start and // end points. Zero-length path segments have no directionality. In // these cases, the following algorithm is used to establish // directionality: to determine the directionality of the start // point of a zero-length path segment, go backwards in the path // data specification within the current subpath until you find a // segment which has directionality at its end point (e.g., a path // segment with non-zero length) and use its ending direction; // otherwise, temporarily consider the start point to lack // directionality. Similarly, to determine the directionality of the // end point of a zero-length path segment, go forwards in the path // data specification within the current subpath until you find a // segment which has directionality at its start point (e.g., a path // segment with non-zero length) and use its starting direction; // otherwise, temporarily consider the end point to lack // directionality. If the start point has directionality but the end // point doesn't, then the end point uses the start point's // directionality. If the end point has directionality but the start // point doesn't, then the start point uses the end point's // directionality. Otherwise, set the directionality for the path // segment's start and end points to align with the positive x-axis // in user space. impl Segments { fn find_incoming_angle_backwards(&self, start_index: usize) -> Option { // "go backwards ... within the current subpath until ... segment which has directionality // at its end point" for segment in self[..=start_index].iter().rev() { match *segment { Segment::Degenerate { .. } => { return None; // reached the beginning of the subpath as we ran into a standalone point } Segment::LineOrCurve { .. } => match segment.get_directionalities() { Some((_, _, v2x, v2y)) => { return Some(Angle::from_vector(v2x, v2y)); } None => { continue; } }, } } None } fn find_outgoing_angle_forwards(&self, start_index: usize) -> Option { // "go forwards ... within the current subpath until ... segment which has directionality at // its start point" for segment in &self[start_index..] { match *segment { Segment::Degenerate { .. } => { return None; // reached the end of a subpath as we ran into a standalone point } Segment::LineOrCurve { .. } => match segment.get_directionalities() { Some((v1x, v1y, _, _)) => { return Some(Angle::from_vector(v1x, v1y)); } None => { continue; } }, } } None } } // From SVG's marker-start, marker-mid, marker-end properties #[derive(Debug, Copy, Clone, PartialEq)] enum MarkerType { Start, Middle, End, } fn emit_marker_by_node( viewport: &Viewport, draw_ctx: &mut DrawingCtx, acquired_nodes: &mut AcquiredNodes<'_>, marker: &layout::Marker, xpos: f64, ypos: f64, computed_angle: Angle, line_width: f64, clipping: bool, marker_type: MarkerType, ) -> Result { match acquired_nodes.acquire_ref(marker.node_ref.as_ref().unwrap()) { Ok(acquired) => { let node = acquired.get(); let marker_elt = borrow_element_as!(node, Marker); marker_elt.render( node, acquired_nodes, viewport, draw_ctx, xpos, ypos, computed_angle, line_width, clipping, marker_type, marker, ) } Err(e) => { rsvg_log!(draw_ctx.session(), "could not acquire marker: {}", e); Ok(draw_ctx.empty_bbox()) } } } #[derive(Debug, Copy, Clone, PartialEq)] enum MarkerEndpoint { Start, End, } fn emit_marker( segment: &Segment, endpoint: MarkerEndpoint, marker_type: MarkerType, orient: Angle, emit_fn: &mut E, ) -> Result where E: FnMut(MarkerType, f64, f64, Angle) -> Result, { let (x, y) = match *segment { Segment::Degenerate { x, y } => (x, y), Segment::LineOrCurve { x1, y1, x4, y4, .. } => match endpoint { MarkerEndpoint::Start => (x1, y1), MarkerEndpoint::End => (x4, y4), }, }; emit_fn(marker_type, x, y, orient) } pub fn render_markers_for_shape( shape: &Shape, viewport: &Viewport, draw_ctx: &mut DrawingCtx, acquired_nodes: &mut AcquiredNodes<'_>, clipping: bool, ) -> Result { if shape.stroke.width.approx_eq_cairo(0.0) { return Ok(draw_ctx.empty_bbox()); } if shape.marker_start.node_ref.is_none() && shape.marker_mid.node_ref.is_none() && shape.marker_end.node_ref.is_none() { return Ok(draw_ctx.empty_bbox()); } emit_markers_for_path( &shape.path, draw_ctx.empty_bbox(), &mut |marker_type: MarkerType, x: f64, y: f64, computed_angle: Angle| { let marker = match marker_type { MarkerType::Start => &shape.marker_start, MarkerType::Middle => &shape.marker_mid, MarkerType::End => &shape.marker_end, }; if marker.node_ref.is_some() { emit_marker_by_node( viewport, draw_ctx, acquired_nodes, marker, x, y, computed_angle, shape.stroke.width, clipping, marker_type, ) } else { Ok(draw_ctx.empty_bbox()) } }, ) } fn emit_markers_for_path( path: &Path, empty_bbox: BoundingBox, emit_fn: &mut E, ) -> Result where E: FnMut(MarkerType, f64, f64, Angle) -> Result, { enum SubpathState { NoSubpath, InSubpath, } let mut bbox = empty_bbox; // Convert the path to a list of segments and bare points let segments = Segments::from(path); let mut subpath_state = SubpathState::NoSubpath; for (i, segment) in segments.iter().enumerate() { match *segment { Segment::Degenerate { .. } => { if let SubpathState::InSubpath = subpath_state { assert!(i > 0); // Got a lone point after a subpath; render the subpath's end marker first let angle = segments .find_incoming_angle_backwards(i - 1) .unwrap_or_else(|| Angle::new(0.0)); let marker_bbox = emit_marker( &segments[i - 1], MarkerEndpoint::End, MarkerType::End, angle, emit_fn, )?; bbox.insert(&marker_bbox); } // Render marker for the lone point; no directionality let marker_bbox = emit_marker( segment, MarkerEndpoint::Start, MarkerType::Middle, Angle::new(0.0), emit_fn, )?; bbox.insert(&marker_bbox); subpath_state = SubpathState::NoSubpath; } Segment::LineOrCurve { .. } => { // Not a degenerate segment match subpath_state { SubpathState::NoSubpath => { let angle = segments .find_outgoing_angle_forwards(i) .unwrap_or_else(|| Angle::new(0.0)); let marker_bbox = emit_marker( segment, MarkerEndpoint::Start, MarkerType::Start, angle, emit_fn, )?; bbox.insert(&marker_bbox); subpath_state = SubpathState::InSubpath; } SubpathState::InSubpath => { assert!(i > 0); let incoming = segments.find_incoming_angle_backwards(i - 1); let outgoing = segments.find_outgoing_angle_forwards(i); let angle = match (incoming, outgoing) { (Some(incoming), Some(outgoing)) => incoming.bisect(outgoing), (Some(incoming), _) => incoming, (_, Some(outgoing)) => outgoing, _ => Angle::new(0.0), }; let marker_bbox = emit_marker( segment, MarkerEndpoint::Start, MarkerType::Middle, angle, emit_fn, )?; bbox.insert(&marker_bbox); } } } } } // Finally, render the last point if !segments.is_empty() { let segment = &segments[segments.len() - 1]; if let Segment::LineOrCurve { .. } = *segment { let incoming = segments .find_incoming_angle_backwards(segments.len() - 1) .unwrap_or_else(|| Angle::new(0.0)); let angle = { if let PathCommand::ClosePath = path.iter().nth(segments.len()).unwrap() { let outgoing = segments .find_outgoing_angle_forwards(0) .unwrap_or_else(|| Angle::new(0.0)); incoming.bisect(outgoing) } else { incoming } }; let marker_bbox = emit_marker( segment, MarkerEndpoint::End, MarkerType::End, angle, emit_fn, )?; bbox.insert(&marker_bbox); } } Ok(bbox) } #[cfg(test)] mod parser_tests { use super::*; #[test] fn parsing_invalid_marker_units_yields_error() { assert!(MarkerUnits::parse_str("").is_err()); assert!(MarkerUnits::parse_str("foo").is_err()); } #[test] fn parses_marker_units() { assert_eq!( MarkerUnits::parse_str("userSpaceOnUse").unwrap(), MarkerUnits::UserSpaceOnUse ); assert_eq!( MarkerUnits::parse_str("strokeWidth").unwrap(), MarkerUnits::StrokeWidth ); } #[test] fn parsing_invalid_marker_orient_yields_error() { assert!(MarkerOrient::parse_str("").is_err()); assert!(MarkerOrient::parse_str("blah").is_err()); assert!(MarkerOrient::parse_str("45blah").is_err()); } #[test] fn parses_marker_orient() { assert_eq!(MarkerOrient::parse_str("auto").unwrap(), MarkerOrient::Auto); assert_eq!( MarkerOrient::parse_str("auto-start-reverse").unwrap(), MarkerOrient::AutoStartReverse ); assert_eq!( MarkerOrient::parse_str("0").unwrap(), MarkerOrient::Angle(Angle::new(0.0)) ); assert_eq!( MarkerOrient::parse_str("180").unwrap(), MarkerOrient::Angle(Angle::from_degrees(180.0)) ); assert_eq!( MarkerOrient::parse_str("180deg").unwrap(), MarkerOrient::Angle(Angle::from_degrees(180.0)) ); assert_eq!( MarkerOrient::parse_str("-400grad").unwrap(), MarkerOrient::Angle(Angle::from_degrees(-360.0)) ); assert_eq!( MarkerOrient::parse_str("1rad").unwrap(), MarkerOrient::Angle(Angle::new(1.0)) ); } } #[cfg(test)] mod directionality_tests { use super::*; use crate::path_builder::PathBuilder; // Single open path; the easy case fn setup_open_path() -> Segments { let mut builder = PathBuilder::default(); builder.move_to(10.0, 10.0); builder.line_to(20.0, 10.0); builder.line_to(20.0, 20.0); Segments::from(&builder.into_path()) } #[test] fn path_to_segments_handles_open_path() { let expected_segments: Segments = Segments(vec![ Segment::line(10.0, 10.0, 20.0, 10.0), Segment::line(20.0, 10.0, 20.0, 20.0), ]); assert_eq!(setup_open_path(), expected_segments); } fn setup_multiple_open_subpaths() -> Segments { let mut builder = PathBuilder::default(); builder.move_to(10.0, 10.0); builder.line_to(20.0, 10.0); builder.line_to(20.0, 20.0); builder.move_to(30.0, 30.0); builder.line_to(40.0, 30.0); builder.curve_to(50.0, 35.0, 60.0, 60.0, 70.0, 70.0); builder.line_to(80.0, 90.0); Segments::from(&builder.into_path()) } #[test] fn path_to_segments_handles_multiple_open_subpaths() { let expected_segments: Segments = Segments(vec![ Segment::line(10.0, 10.0, 20.0, 10.0), Segment::line(20.0, 10.0, 20.0, 20.0), Segment::line(30.0, 30.0, 40.0, 30.0), Segment::curve(40.0, 30.0, 50.0, 35.0, 60.0, 60.0, 70.0, 70.0), Segment::line(70.0, 70.0, 80.0, 90.0), ]); assert_eq!(setup_multiple_open_subpaths(), expected_segments); } // Closed subpath; must have a line segment back to the first point fn setup_closed_subpath() -> Segments { let mut builder = PathBuilder::default(); builder.move_to(10.0, 10.0); builder.line_to(20.0, 10.0); builder.line_to(20.0, 20.0); builder.close_path(); Segments::from(&builder.into_path()) } #[test] fn path_to_segments_handles_closed_subpath() { let expected_segments: Segments = Segments(vec![ Segment::line(10.0, 10.0, 20.0, 10.0), Segment::line(20.0, 10.0, 20.0, 20.0), Segment::line(20.0, 20.0, 10.0, 10.0), ]); assert_eq!(setup_closed_subpath(), expected_segments); } // Multiple closed subpaths; each must have a line segment back to their // initial points, with no degenerate segments between subpaths. fn setup_multiple_closed_subpaths() -> Segments { let mut builder = PathBuilder::default(); builder.move_to(10.0, 10.0); builder.line_to(20.0, 10.0); builder.line_to(20.0, 20.0); builder.close_path(); builder.move_to(30.0, 30.0); builder.line_to(40.0, 30.0); builder.curve_to(50.0, 35.0, 60.0, 60.0, 70.0, 70.0); builder.line_to(80.0, 90.0); builder.close_path(); Segments::from(&builder.into_path()) } #[test] fn path_to_segments_handles_multiple_closed_subpaths() { let expected_segments: Segments = Segments(vec![ Segment::line(10.0, 10.0, 20.0, 10.0), Segment::line(20.0, 10.0, 20.0, 20.0), Segment::line(20.0, 20.0, 10.0, 10.0), Segment::line(30.0, 30.0, 40.0, 30.0), Segment::curve(40.0, 30.0, 50.0, 35.0, 60.0, 60.0, 70.0, 70.0), Segment::line(70.0, 70.0, 80.0, 90.0), Segment::line(80.0, 90.0, 30.0, 30.0), ]); assert_eq!(setup_multiple_closed_subpaths(), expected_segments); } // A lineto follows the first closed subpath, with no moveto to start the second subpath. // The lineto must start at the first point of the first subpath. fn setup_no_moveto_after_closepath() -> Segments { let mut builder = PathBuilder::default(); builder.move_to(10.0, 10.0); builder.line_to(20.0, 10.0); builder.line_to(20.0, 20.0); builder.close_path(); builder.line_to(40.0, 30.0); Segments::from(&builder.into_path()) } #[test] fn path_to_segments_handles_no_moveto_after_closepath() { let expected_segments: Segments = Segments(vec![ Segment::line(10.0, 10.0, 20.0, 10.0), Segment::line(20.0, 10.0, 20.0, 20.0), Segment::line(20.0, 20.0, 10.0, 10.0), Segment::line(10.0, 10.0, 40.0, 30.0), ]); assert_eq!(setup_no_moveto_after_closepath(), expected_segments); } // Sequence of moveto; should generate degenerate points. // This test is not enabled right now! We create the // path fixtures with Cairo, and Cairo compresses // sequences of moveto into a single one. So, we can't // really test this, as we don't get the fixture we want. // // Eventually we'll probably have to switch librsvg to // its own internal path representation which should // allow for unelided path commands, and which should // only build a cairo_path_t for the final rendering step. // // fn setup_sequence_of_moveto () -> Segments { // let mut builder = PathBuilder::default (); // // builder.move_to (10.0, 10.0); // builder.move_to (20.0, 20.0); // builder.move_to (30.0, 30.0); // builder.move_to (40.0, 40.0); // // Segments::from(&builder.into_path()) // } // // #[test] // fn path_to_segments_handles_sequence_of_moveto () { // let expected_segments: Segments = Segments(vec! [ // Segment::degenerate(10.0, 10.0), // Segment::degenerate(20.0, 20.0), // Segment::degenerate(30.0, 30.0), // Segment::degenerate(40.0, 40.0), // ]); // // assert_eq!(setup_sequence_of_moveto(), expected_segments); // } #[test] fn degenerate_segment_has_no_directionality() { let s = Segment::degenerate(1.0, 2.0); assert!(s.get_directionalities().is_none()); } #[test] fn line_segment_has_directionality() { let s = Segment::line(1.0, 2.0, 3.0, 4.0); let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); assert_eq!((2.0, 2.0), (v1x, v1y)); assert_eq!((2.0, 2.0), (v2x, v2y)); } #[test] fn line_segment_with_coincident_ends_has_no_directionality() { let s = Segment::line(1.0, 2.0, 1.0, 2.0); assert!(s.get_directionalities().is_none()); } #[test] fn curve_has_directionality() { let s = Segment::curve(1.0, 2.0, 3.0, 5.0, 8.0, 13.0, 20.0, 33.0); let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); assert_eq!((2.0, 3.0), (v1x, v1y)); assert_eq!((12.0, 20.0), (v2x, v2y)); } #[test] fn curves_with_loops_and_coincident_ends_have_directionality() { let s = Segment::curve(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0, 2.0); let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); assert_eq!((2.0, 2.0), (v1x, v1y)); assert_eq!((-4.0, -4.0), (v2x, v2y)); let s = Segment::curve(1.0, 2.0, 1.0, 2.0, 3.0, 4.0, 1.0, 2.0); let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); assert_eq!((2.0, 2.0), (v1x, v1y)); assert_eq!((-2.0, -2.0), (v2x, v2y)); let s = Segment::curve(1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 1.0, 2.0); let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); assert_eq!((2.0, 2.0), (v1x, v1y)); assert_eq!((-2.0, -2.0), (v2x, v2y)); } #[test] fn curve_with_coincident_control_points_has_no_directionality() { let s = Segment::curve(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0); assert!(s.get_directionalities().is_none()); } #[test] fn curve_with_123_coincident_has_directionality() { let s = Segment::curve(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 40.0); let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); assert_eq!((20.0, 40.0), (v1x, v1y)); assert_eq!((20.0, 40.0), (v2x, v2y)); } #[test] fn curve_with_234_coincident_has_directionality() { let s = Segment::curve(20.0, 40.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); assert_eq!((-20.0, -40.0), (v1x, v1y)); assert_eq!((-20.0, -40.0), (v2x, v2y)); } #[test] fn curve_with_12_34_coincident_has_directionality() { let s = Segment::curve(20.0, 40.0, 20.0, 40.0, 60.0, 70.0, 60.0, 70.0); let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); assert_eq!((40.0, 30.0), (v1x, v1y)); assert_eq!((40.0, 30.0), (v2x, v2y)); } } #[cfg(test)] mod marker_tests { use super::*; use crate::path_builder::PathBuilder; #[test] fn emits_for_open_subpath() { let mut builder = PathBuilder::default(); builder.move_to(0.0, 0.0); builder.line_to(1.0, 0.0); builder.line_to(1.0, 1.0); builder.line_to(0.0, 1.0); let mut v = Vec::new(); assert!(emit_markers_for_path( &builder.into_path(), BoundingBox::new(), &mut |marker_type: MarkerType, x: f64, y: f64, computed_angle: Angle| -> Result { v.push((marker_type, x, y, computed_angle)); Ok(BoundingBox::new()) } ) .is_ok()); assert_eq!( v, vec![ (MarkerType::Start, 0.0, 0.0, Angle::new(0.0)), (MarkerType::Middle, 1.0, 0.0, Angle::from_vector(1.0, 1.0)), (MarkerType::Middle, 1.0, 1.0, Angle::from_vector(-1.0, 1.0)), (MarkerType::End, 0.0, 1.0, Angle::from_vector(-1.0, 0.0)), ] ); } #[test] fn emits_for_closed_subpath() { let mut builder = PathBuilder::default(); builder.move_to(0.0, 0.0); builder.line_to(1.0, 0.0); builder.line_to(1.0, 1.0); builder.line_to(0.0, 1.0); builder.close_path(); let mut v = Vec::new(); assert!(emit_markers_for_path( &builder.into_path(), BoundingBox::new(), &mut |marker_type: MarkerType, x: f64, y: f64, computed_angle: Angle| -> Result { v.push((marker_type, x, y, computed_angle)); Ok(BoundingBox::new()) } ) .is_ok()); assert_eq!( v, vec![ (MarkerType::Start, 0.0, 0.0, Angle::new(0.0)), (MarkerType::Middle, 1.0, 0.0, Angle::from_vector(1.0, 1.0)), (MarkerType::Middle, 1.0, 1.0, Angle::from_vector(-1.0, 1.0)), (MarkerType::Middle, 0.0, 1.0, Angle::from_vector(-1.0, -1.0)), (MarkerType::End, 0.0, 0.0, Angle::from_vector(1.0, -1.0)), ] ); } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/node.rs��������������������������������������������������������������������������0000644�0000000�0000000�00000031264�10461020230�0013461�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Tree nodes, the representation of SVG elements. //! //! Librsvg uses the [rctree crate][rctree] to represent the SVG tree of elements. //! Its [`rctree::Node`] struct provides a generic wrapper over nodes in a tree. //! Librsvg puts a [`NodeData`] as the type parameter of [`rctree::Node`]. For convenience, //! librsvg has a type alias [`Node`]` = rctree::Node`. //! //! Nodes are not constructed directly by callers; use markup5ever::QualName; use std::cell::{Ref, RefMut}; use std::fmt; use std::rc::Rc; use crate::bbox::BoundingBox; use crate::document::AcquiredNodes; use crate::drawing_ctx::{DrawingCtx, Viewport}; use crate::element::*; use crate::error::*; use crate::paint_server::PaintSource; use crate::properties::ComputedValues; use crate::rsvg_log; use crate::session::Session; use crate::text::Chars; use crate::xml::Attributes; /// Strong reference to an element in the SVG tree. /// /// See the [module documentation][self] for more information. pub type Node = rctree::Node; /// Weak reference to an element in the SVG tree. /// /// See the [module documentation][self] for more information. pub type WeakNode = rctree::WeakNode; /// Data for a single DOM node. /// /// ## Memory consumption /// /// SVG files look like this, roughly: /// /// ```xml /// /// /// /// Hello /// /// /// ``` /// /// Each element has a bunch of data, including the styles, which is /// the biggest consumer of memory within the `Element` struct. But /// between each element there is a text node; in the example above /// there are a bunch of text nodes with just whitespace (newlines and /// spaces), and a single text node with "`Hello`" in it from the /// `` element. /// /// ## Accessing the node's contents /// /// Code that traverses the DOM tree needs to find out at runtime what /// each node stands for. First, use the `is_chars` or `is_element` /// methods from the `NodeBorrow` trait to see if you can then call /// `borrow_chars`, `borrow_element`, or `borrow_element_mut`. pub enum NodeData { Element(Box), Text(Box), } impl NodeData { pub fn new_element(session: &Session, name: &QualName, attrs: Attributes) -> NodeData { NodeData::Element(Box::new(Element::new(session, name, attrs))) } pub fn new_chars(initial_text: &str) -> NodeData { NodeData::Text(Box::new(Chars::new(initial_text))) } } impl fmt::Display for NodeData { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { NodeData::Element(ref e) => { write!(f, "{e}")?; } NodeData::Text(_) => { write!(f, "Chars")?; } } Ok(()) } } /// Can obtain computed values from a node /// /// In our tree of SVG elements (Node in our parlance), each node stores a `ComputedValues` that /// gets computed during the initial CSS cascade. However, sometimes nodes need to be rendered /// outside the normal hierarchy. For example, the `` element can "instance" a subtree from /// elsewhere in the SVG; it causes the instanced subtree to re-cascade from the computed values for /// the `` element. /// /// You can then call the `get()` method on the resulting `CascadedValues` to get a /// `&ComputedValues` whose fields you can access. pub struct CascadedValues<'a> { inner: CascadedInner<'a>, pub context_stroke: Option>, pub context_fill: Option>, } enum CascadedInner<'a> { FromNode(Ref<'a, Element>), FromValues(Box), } impl<'a> CascadedValues<'a> { /// Creates a `CascadedValues` that has the same cascading mode as &self /// /// This is what nodes should normally use to draw their children from their `draw()` method. /// Nodes that need to override the cascade for their children can use `new_from_values()` /// instead. pub fn clone_with_node(&self, node: &'a Node) -> CascadedValues<'a> { match self.inner { CascadedInner::FromNode(_) => CascadedValues { inner: CascadedInner::FromNode(node.borrow_element()), context_fill: self.context_fill.clone(), context_stroke: self.context_stroke.clone(), }, CascadedInner::FromValues(ref v) => CascadedValues::new_from_values( node, v, self.context_fill.clone(), self.context_stroke.clone(), ), } } /// Creates a `CascadedValues` that will hold the `node`'s computed values /// /// This is to be used only in the toplevel drawing function, or in elements like `` /// that don't propagate their parent's cascade to their children. All others should use /// `new()` to derive the cascade from an existing one. pub fn new_from_node(node: &Node) -> CascadedValues<'_> { CascadedValues { inner: CascadedInner::FromNode(node.borrow_element()), context_fill: None, context_stroke: None, } } /// Creates a `CascadedValues` that will override the `node`'s cascade with the specified /// `values` /// /// This is for the `` element, which draws the element which it references with the /// ``'s own cascade, not with the element's original cascade. pub fn new_from_values( node: &'a Node, values: &ComputedValues, fill: Option>, stroke: Option>, ) -> CascadedValues<'a> { let mut v = Box::new(values.clone()); node.borrow_element() .get_specified_values() .to_computed_values(&mut v); CascadedValues { inner: CascadedInner::FromValues(v), context_fill: fill, context_stroke: stroke, } } /// Returns the cascaded `ComputedValues`. /// /// Nodes should use this from their `Draw::draw()` implementation to get the /// `ComputedValues` from the `CascadedValues` that got passed to `draw()`. pub fn get(&'a self) -> &'a ComputedValues { match self.inner { CascadedInner::FromNode(ref e) => e.get_computed_values(), CascadedInner::FromValues(ref v) => v, } // if values.fill == "context-fill" { // values.fill=self.context_fill // } // if values.stroke == "context-stroke" { // values.stroke=self.context_stroke // } } } /// Helper trait to get different NodeData variants pub trait NodeBorrow { /// Returns `false` for NodeData::Text, `true` otherwise. fn is_element(&self) -> bool; /// Returns `true` for NodeData::Text, `false` otherwise. fn is_chars(&self) -> bool; /// Borrows a `Chars` reference. /// /// Panics: will panic if `&self` is not a `NodeData::Text` node fn borrow_chars(&self) -> Ref<'_, Chars>; /// Borrows an `Element` reference /// /// Panics: will panic if `&self` is not a `NodeData::Element` node fn borrow_element(&self) -> Ref<'_, Element>; /// Borrows an `Element` reference mutably /// /// Panics: will panic if `&self` is not a `NodeData::Element` node fn borrow_element_mut(&mut self) -> RefMut<'_, Element>; /// Borrows an `ElementData` reference to the concrete element type. /// /// Panics: will panic if `&self` is not a `NodeData::Element` node fn borrow_element_data(&self) -> Ref<'_, ElementData>; } impl NodeBorrow for Node { fn is_element(&self) -> bool { matches!(*self.borrow(), NodeData::Element(_)) } fn is_chars(&self) -> bool { matches!(*self.borrow(), NodeData::Text(_)) } fn borrow_chars(&self) -> Ref<'_, Chars> { Ref::map(self.borrow(), |n| match n { NodeData::Text(c) => &**c, _ => panic!("tried to borrow_chars for a non-text node"), }) } fn borrow_element(&self) -> Ref<'_, Element> { Ref::map(self.borrow(), |n| match n { NodeData::Element(e) => &**e, _ => panic!("tried to borrow_element for a non-element node"), }) } fn borrow_element_mut(&mut self) -> RefMut<'_, Element> { RefMut::map(self.borrow_mut(), |n| match &mut *n { NodeData::Element(e) => &mut **e, _ => panic!("tried to borrow_element_mut for a non-element node"), }) } fn borrow_element_data(&self) -> Ref<'_, ElementData> { Ref::map(self.borrow(), |n| match n { NodeData::Element(e) => &e.element_data, _ => panic!("tried to borrow_element_data for a non-element node"), }) } } #[doc(hidden)] #[macro_export] macro_rules! is_element_of_type { ($node:expr, $element_type:ident) => { matches!( $node.borrow_element().element_data, $crate::element::ElementData::$element_type(_) ) }; } #[doc(hidden)] #[macro_export] macro_rules! borrow_element_as { ($node:expr, $element_type:ident) => { std::cell::Ref::map($node.borrow_element_data(), |d| match d { $crate::element::ElementData::$element_type(ref e) => &*e, _ => panic!("tried to borrow_element_as {}", stringify!($element_type)), }) }; } /// Helper trait for cascading recursively pub trait NodeCascade { fn cascade(&mut self, values: &ComputedValues); } impl NodeCascade for Node { fn cascade(&mut self, values: &ComputedValues) { let mut values = values.clone(); { let mut elt = self.borrow_element_mut(); elt.get_specified_values().to_computed_values(&mut values); elt.set_computed_values(&values); } for mut child in self.children().filter(|c| c.is_element()) { child.cascade(&values); } } } /// Helper trait for drawing recursively. /// /// This is a trait because [`Node`] is a type alias over [`rctree::Node`], not a concrete type. pub trait NodeDraw { fn draw( &self, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, clipping: bool, ) -> Result; fn draw_children( &self, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, clipping: bool, ) -> Result; } impl NodeDraw for Node { fn draw( &self, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, clipping: bool, ) -> Result { match *self.borrow() { NodeData::Element(ref e) => { rsvg_log!(draw_ctx.session(), "({}", e); let res = match e.draw(self, acquired_nodes, cascaded, viewport, draw_ctx, clipping) { Ok(bbox) => Ok(bbox), // https://www.w3.org/TR/css-transforms-1/#transform-function-lists // // "If a transform function causes the current transformation matrix of an // object to be non-invertible, the object and its content do not get // displayed." Err(InternalRenderingError::InvalidTransform) => Ok(draw_ctx.empty_bbox()), Err(InternalRenderingError::CircularReference(node)) => { if node != *self { return Ok(draw_ctx.empty_bbox()); } else { return Err(InternalRenderingError::CircularReference(node)); } } Err(e) => Err(e), }; rsvg_log!(draw_ctx.session(), ")"); res } _ => Ok(draw_ctx.empty_bbox()), } } fn draw_children( &self, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, clipping: bool, ) -> Result { let mut bbox = draw_ctx.empty_bbox(); for child in self.children().filter(|c| c.is_element()) { let child_bbox = draw_ctx.draw_node_from_stack( &child, acquired_nodes, &CascadedValues::clone_with_node(cascaded, &child), viewport, clipping, )?; bbox.insert(&child_bbox); } Ok(bbox) } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/paint_server.rs������������������������������������������������������������������0000644�0000000�0000000�00000040754�10461020230�0015241�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! SVG paint servers. use std::rc::Rc; use cssparser::{ Color, ColorFunction, Hsl, Hwb, Lab, Lch, Oklab, Oklch, ParseErrorKind, Parser, RGBA, }; use crate::document::{AcquiredNodes, NodeId}; use crate::drawing_ctx::Viewport; use crate::element::ElementData; use crate::error::{AcquireError, NodeIdError, ParseError, ValueErrorKind}; use crate::gradient::{ResolvedGradient, UserSpaceGradient}; use crate::length::NormalizeValues; use crate::node::NodeBorrow; use crate::parsers::Parse; use crate::pattern::{ResolvedPattern, UserSpacePattern}; use crate::rect::Rect; use crate::rsvg_log; use crate::session::Session; use crate::unit_interval::UnitInterval; use crate::util; /// Unresolved SVG paint server straight from the DOM data. /// /// This is either a solid color (which if `currentColor` needs to be extracted from the /// `ComputedValues`), or a paint server like a gradient or pattern which is referenced by /// a URL that points to a certain document node. /// /// Use [`PaintServer.resolve`](#method.resolve) to turn this into a [`PaintSource`]. #[derive(Debug, Clone, PartialEq)] pub enum PaintServer { /// For example, `fill="none"`. None, /// For example, `fill="url(#some_gradient) fallback_color"`. Iri { iri: Box, alternate: Option, }, /// For example, `fill="blue"`. SolidColor(cssparser::Color), /// For example, `fill="context-fill"` ContextFill, /// For example, `fill="context-stroke"` ContextStroke, } /// Paint server with resolved references, with unnormalized lengths. /// /// Use [`PaintSource.to_user_space`](#method.to_user_space) to turn this into a /// [`UserSpacePaintSource`]. pub enum PaintSource { None, Gradient(ResolvedGradient, Option), Pattern(ResolvedPattern, Option), SolidColor(Color), } /// Fully resolved paint server, in user-space units. /// /// This has everything required for rendering. pub enum UserSpacePaintSource { None, Gradient(UserSpaceGradient, Option), Pattern(UserSpacePattern, Option), SolidColor(Color), } impl Parse for PaintServer { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { if parser .try_parse(|i| i.expect_ident_matching("none")) .is_ok() { Ok(PaintServer::None) } else if parser .try_parse(|i| i.expect_ident_matching("context-fill")) .is_ok() { Ok(PaintServer::ContextFill) } else if parser .try_parse(|i| i.expect_ident_matching("context-stroke")) .is_ok() { Ok(PaintServer::ContextStroke) } else if let Ok(url) = parser.try_parse(|i| i.expect_url()) { let loc = parser.current_source_location(); let alternate = if !parser.is_exhausted() { if parser .try_parse(|i| i.expect_ident_matching("none")) .is_ok() { None } else { Some( parser .try_parse(cssparser::Color::parse) .map_err(|e| ParseError { kind: ParseErrorKind::Custom(ValueErrorKind::parse_error( "Could not parse color", )), location: e.location, })?, ) } } else { None }; Ok(PaintServer::Iri { iri: Box::new( NodeId::parse(&url) .map_err(|e: NodeIdError| -> ValueErrorKind { e.into() }) .map_err(|e| loc.new_custom_error(e))?, ), alternate, }) } else { ::parse(parser).map(PaintServer::SolidColor) } } } impl PaintServer { /// Resolves colors, plus node references for gradients and patterns. /// /// `opacity` depends on `strokeOpacity` or `fillOpacity` depending on whether /// the paint server is for the `stroke` or `fill` properties. /// /// `current_color` should be the value of `ComputedValues.color()`. /// /// After a paint server is resolved, the resulting [`PaintSource`] can be used in /// many places: for an actual shape, or for the `context-fill` of a marker for that /// shape. Therefore, this returns an [`Rc`] so that the `PaintSource` may be shared /// easily. pub fn resolve( &self, acquired_nodes: &mut AcquiredNodes<'_>, opacity: UnitInterval, current_color: Color, context_fill: Option>, context_stroke: Option>, session: &Session, ) -> Rc { match self { PaintServer::Iri { ref iri, ref alternate, } => acquired_nodes .acquire(iri) .and_then(|acquired| { let node = acquired.get(); assert!(node.is_element()); match *node.borrow_element_data() { ElementData::LinearGradient(ref g) => { g.resolve(node, acquired_nodes, opacity).map(|g| { Rc::new(PaintSource::Gradient( g, alternate.map(|c| resolve_color(&c, opacity, ¤t_color)), )) }) } ElementData::Pattern(ref p) => { p.resolve(node, acquired_nodes, opacity, session).map(|p| { Rc::new(PaintSource::Pattern( p, alternate.map(|c| resolve_color(&c, opacity, ¤t_color)), )) }) } ElementData::RadialGradient(ref g) => { g.resolve(node, acquired_nodes, opacity).map(|g| { Rc::new(PaintSource::Gradient( g, alternate.map(|c| resolve_color(&c, opacity, ¤t_color)), )) }) } _ => Err(AcquireError::InvalidLinkType(iri.as_ref().clone())), } }) .unwrap_or_else(|_| match alternate { // The following cases catch AcquireError::CircularReference and // AcquireError::MaxReferencesExceeded. // // Circular references mean that there is a pattern or gradient with a // reference cycle in its "href" attribute. This is an invalid paint // server, and per // https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint we should // try to fall back to the alternate color. // // Exceeding the maximum number of references will get caught again // later in the drawing code, so it should be fine to translate this // condition to that for an invalid paint server. Some(color) => { rsvg_log!( session, "could not resolve paint server \"{}\", using alternate color", iri ); Rc::new(PaintSource::SolidColor(resolve_color( color, opacity, ¤t_color, ))) } None => { rsvg_log!( session, "could not resolve paint server \"{}\", no alternate color specified", iri ); Rc::new(PaintSource::None) } }), PaintServer::SolidColor(color) => Rc::new(PaintSource::SolidColor(resolve_color( color, opacity, ¤t_color, ))), PaintServer::ContextFill => { if let Some(paint) = context_fill { paint } else { Rc::new(PaintSource::None) } } PaintServer::ContextStroke => { if let Some(paint) = context_stroke { paint } else { Rc::new(PaintSource::None) } } PaintServer::None => Rc::new(PaintSource::None), } } } impl PaintSource { /// Converts lengths to user-space. pub fn to_user_space( &self, object_bbox: &Option, viewport: &Viewport, values: &NormalizeValues, ) -> UserSpacePaintSource { match *self { PaintSource::None => UserSpacePaintSource::None, PaintSource::SolidColor(c) => UserSpacePaintSource::SolidColor(c), PaintSource::Gradient(ref g, c) => { match (g.to_user_space(object_bbox, viewport, values), c) { (Some(gradient), c) => UserSpacePaintSource::Gradient(gradient, c), (None, Some(c)) => UserSpacePaintSource::SolidColor(c), (None, None) => UserSpacePaintSource::None, } } PaintSource::Pattern(ref p, c) => { match (p.to_user_space(object_bbox, viewport, values), c) { (Some(pattern), c) => UserSpacePaintSource::Pattern(pattern, c), (None, Some(c)) => UserSpacePaintSource::SolidColor(c), (None, None) => UserSpacePaintSource::None, } } } } } /// Takes the `opacity` property and an alpha value from a CSS `` and returns a resulting /// alpha for a computed value. /// /// `alpha` is `Option` because that is what cssparser uses everywhere. fn resolve_alpha(opacity: UnitInterval, alpha: Option) -> Option { let UnitInterval(o) = opacity; let alpha = f64::from(alpha.unwrap_or(0.0)) * o; let alpha = util::clamp(alpha, 0.0, 1.0); let alpha = cast::f32(alpha).unwrap(); Some(alpha) } fn black() -> Color { Color::Rgba(RGBA::new(Some(0), Some(0), Some(0), Some(1.0))) } /// Resolves a CSS color from itself, an `opacity` property, and a `color` property (to resolve `currentColor`). /// /// A CSS color can be `currentColor`, in which case the computed value comes from /// the `color` property. You should pass the `color` property's value for `current_color`. /// /// Note that `currrent_color` can itself have a value of `currentColor`. In that case, we /// consider it to be opaque black. pub fn resolve_color(color: &Color, opacity: UnitInterval, current_color: &Color) -> Color { let without_opacity_applied = match color { Color::CurrentColor => { if let Color::CurrentColor = current_color { black() } else { *current_color } } _ => *color, }; match without_opacity_applied { Color::CurrentColor => unreachable!(), Color::Rgba(rgba) => Color::Rgba(RGBA { alpha: resolve_alpha(opacity, rgba.alpha), ..rgba }), Color::Hsl(hsl) => Color::Hsl(Hsl { alpha: resolve_alpha(opacity, hsl.alpha), ..hsl }), Color::Hwb(hwb) => Color::Hwb(Hwb { alpha: resolve_alpha(opacity, hwb.alpha), ..hwb }), Color::Lab(lab) => Color::Lab(Lab { alpha: resolve_alpha(opacity, lab.alpha), ..lab }), Color::Lch(lch) => Color::Lch(Lch { alpha: resolve_alpha(opacity, lch.alpha), ..lch }), Color::Oklab(oklab) => Color::Oklab(Oklab { alpha: resolve_alpha(opacity, oklab.alpha), ..oklab }), Color::Oklch(oklch) => Color::Oklch(Oklch { alpha: resolve_alpha(opacity, oklch.alpha), ..oklch }), Color::ColorFunction(cf) => Color::ColorFunction(ColorFunction { alpha: resolve_alpha(opacity, cf.alpha), ..cf }), } } impl std::fmt::Debug for PaintSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { match *self { PaintSource::None => f.write_str("PaintSource::None"), PaintSource::Gradient(_, _) => f.write_str("PaintSource::Gradient"), PaintSource::Pattern(_, _) => f.write_str("PaintSource::Pattern"), PaintSource::SolidColor(_) => f.write_str("PaintSource::SolidColor"), } } } #[cfg(test)] mod tests { use super::*; #[test] fn catches_invalid_syntax() { assert!(PaintServer::parse_str("").is_err()); assert!(PaintServer::parse_str("42").is_err()); assert!(PaintServer::parse_str("invalid").is_err()); } #[test] fn parses_none() { assert_eq!(PaintServer::parse_str("none").unwrap(), PaintServer::None); } #[test] fn parses_solid_color() { assert_eq!( PaintServer::parse_str("rgb(255, 128, 64, 0.5)").unwrap(), PaintServer::SolidColor(cssparser::Color::Rgba(cssparser::RGBA::new( Some(255), Some(128), Some(64), Some(0.5) ))) ); assert_eq!( PaintServer::parse_str("currentColor").unwrap(), PaintServer::SolidColor(cssparser::Color::CurrentColor) ); } #[test] fn parses_iri() { assert_eq!( PaintServer::parse_str("url(#link)").unwrap(), PaintServer::Iri { iri: Box::new(NodeId::Internal("link".to_string())), alternate: None, } ); assert_eq!( PaintServer::parse_str("url(foo#link) none").unwrap(), PaintServer::Iri { iri: Box::new(NodeId::External("foo".to_string(), "link".to_string())), alternate: None, } ); assert_eq!( PaintServer::parse_str("url(#link) #ff8040").unwrap(), PaintServer::Iri { iri: Box::new(NodeId::Internal("link".to_string())), alternate: Some(cssparser::Color::Rgba(cssparser::RGBA::new( Some(255), Some(128), Some(64), Some(1.0) ))), } ); assert_eq!( PaintServer::parse_str("url(#link) rgb(255, 128, 64, 0.5)").unwrap(), PaintServer::Iri { iri: Box::new(NodeId::Internal("link".to_string())), alternate: Some(cssparser::Color::Rgba(cssparser::RGBA::new( Some(255), Some(128), Some(64), Some(0.5) ))), } ); assert_eq!( PaintServer::parse_str("url(#link) currentColor").unwrap(), PaintServer::Iri { iri: Box::new(NodeId::Internal("link".to_string())), alternate: Some(cssparser::Color::CurrentColor), } ); assert!(PaintServer::parse_str("url(#link) invalid").is_err()); } #[test] fn resolves_explicit_color() { assert_eq!( resolve_color( &Color::Rgba(RGBA::new(Some(255), Some(0), Some(0), Some(0.5))), UnitInterval::clamp(0.5), &Color::Rgba(RGBA::new(Some(0), Some(255), Some(0), Some(1.0))), ), Color::Rgba(RGBA::new(Some(255), Some(0), Some(0), Some(0.25))), ); } #[test] fn resolves_current_color() { assert_eq!( resolve_color( &Color::CurrentColor, UnitInterval::clamp(0.5), &Color::Rgba(RGBA::new(Some(0), Some(255), Some(0), Some(0.5))), ), Color::Rgba(RGBA::new(Some(0), Some(255), Some(0), Some(0.25))), ); } } ��������������������librsvg-2.59.0/src/parsers.rs�����������������������������������������������������������������������0000644�0000000�0000000�00000032506�10461020230�0014213�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! The `Parse` trait for CSS properties, and utilities for parsers. use cssparser::{Parser, ParserInput, Token}; use markup5ever::QualName; use std::str; use crate::error::*; /// Trait to parse values using `cssparser::Parser`. pub trait Parse: Sized { /// Parses a value out of the `parser`. /// /// All value types should implement this for composability. fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result>; /// Convenience function to parse a value out of a `&str`. /// /// This is useful mostly for tests which want to avoid creating a /// `cssparser::Parser` by hand. Property types do not need to reimplement this. fn parse_str(s: &str) -> Result> { let mut input = ParserInput::new(s); let mut parser = Parser::new(&mut input); let res = Self::parse(&mut parser)?; parser.expect_exhausted()?; Ok(res) } } /// Consumes a comma if it exists, or does nothing. pub fn optional_comma(parser: &mut Parser<'_, '_>) { let _ = parser.try_parse(|p| p.expect_comma()); } /// Parses an `f32` and ensures that it is not an infinity or NaN. pub fn finite_f32(n: f32) -> Result { if n.is_finite() { Ok(n) } else { Err(ValueErrorKind::Value("expected finite number".to_string())) } } pub trait ParseValue { /// Parses a `value` string into a type `T`. fn parse(&self, value: &str) -> Result; } impl ParseValue for QualName { fn parse(&self, value: &str) -> Result { let mut input = ParserInput::new(value); let mut parser = Parser::new(&mut input); T::parse(&mut parser).attribute(self.clone()) } } impl Parse for Option { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { T::parse(parser).map(Some) } } impl Parse for f64 { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let loc = parser.current_source_location(); let n = parser.expect_number()?; if n.is_finite() { Ok(f64::from(n)) } else { Err(loc.new_custom_error(ValueErrorKind::value_error("expected finite number"))) } } } /// Non-Negative number #[derive(Debug, Copy, Clone, PartialEq)] pub struct NonNegative(pub f64); impl Parse for NonNegative { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let loc = parser.current_source_location(); let n = Parse::parse(parser)?; if n >= 0.0 { Ok(NonNegative(n)) } else { Err(loc.new_custom_error(ValueErrorKind::value_error("expected non negative number"))) } } } /// CSS number-optional-number /// /// SVG1.1: #[derive(Debug, Copy, Clone, PartialEq)] pub struct NumberOptionalNumber(pub T, pub T); impl Parse for NumberOptionalNumber { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let x = Parse::parse(parser)?; if !parser.is_exhausted() { optional_comma(parser); let y = Parse::parse(parser)?; Ok(NumberOptionalNumber(x, y)) } else { Ok(NumberOptionalNumber(x, x)) } } } /// CSS number-percentage /// /// CSS Values and Units 3: #[derive(Debug, Copy, Clone, PartialEq)] pub struct NumberOrPercentage { pub value: f64, } impl Parse for NumberOrPercentage { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let loc = parser.current_source_location(); let value = match parser.next()? { Token::Number { value, .. } => Ok(*value), Token::Percentage { unit_value, .. } => Ok(*unit_value), tok => Err(loc.new_unexpected_token_error(tok.clone())), }?; let v = finite_f32(value).map_err(|e| parser.new_custom_error(e))?; Ok(NumberOrPercentage { value: f64::from(v), }) } } impl Parse for i32 { /// CSS integer /// /// SVG1.1: fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { Ok(parser.expect_integer()?) } } impl Parse for u32 { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let loc = parser.current_source_location(); let n = parser.expect_integer()?; if n >= 0 { Ok(n as u32) } else { Err(loc.new_custom_error(ValueErrorKind::value_error("expected unsigned number"))) } } } /// Number lists with bounds for the required and maximum number of items. #[derive(Clone, Debug, PartialEq)] pub struct NumberList(pub Vec); impl Parse for NumberList { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let loc = parser.current_source_location(); let mut v = Vec::::with_capacity(MAX); for i in 0..MAX { if i != 0 { optional_comma(parser); } v.push(f64::parse(parser)?); if parser.is_exhausted() { break; } } if REQUIRED > 0 && v.len() < REQUIRED { Err(loc.new_custom_error(ValueErrorKind::value_error("expected more numbers"))) } else { Ok(NumberList(v)) } } } /// Parses a list of identifiers from a `cssparser::Parser` /// /// # Example /// /// ``` /// # use cssparser::{ParserInput, Parser}; /// # use rsvg::parse_identifiers; /// # fn main() -> Result<(), cssparser::BasicParseError<'static>> { /// # let mut input = ParserInput::new("true"); /// # let mut parser = Parser::new(&mut input); /// let my_boolean = parse_identifiers!( /// parser, /// "true" => true, /// "false" => false, /// )?; /// # Ok(()) /// # } /// ``` #[doc(hidden)] #[macro_export] macro_rules! parse_identifiers { ($parser:expr, $($str:expr => $val:expr,)+) => { { let loc = $parser.current_source_location(); let token = $parser.next()?; match token { $(cssparser::Token::Ident(ref cow) if cow.eq_ignore_ascii_case($str) => Ok($val),)+ _ => Err(loc.new_basic_unexpected_token_error(token.clone())) } } }; } /// CSS Custom identifier. /// /// CSS Values and Units 4: #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct CustomIdent(pub String); impl Parse for CustomIdent { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let loc = parser.current_source_location(); let token = parser.next()?; match token { // CSS-wide keywords and "default" are errors here // https://www.w3.org/TR/css-values-4/#css-wide-keywords Token::Ident(ref cow) => { for s in &["initial", "inherit", "unset", "default"] { if cow.eq_ignore_ascii_case(s) { return Err(loc.new_basic_unexpected_token_error(token.clone()).into()); } } Ok(CustomIdent(cow.as_ref().to_string())) } _ => Err(loc.new_basic_unexpected_token_error(token.clone()).into()), } } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_number_optional_number() { assert_eq!( NumberOptionalNumber::parse_str("1, 2").unwrap(), NumberOptionalNumber(1.0, 2.0) ); assert_eq!( NumberOptionalNumber::parse_str("1 2").unwrap(), NumberOptionalNumber(1.0, 2.0) ); assert_eq!( NumberOptionalNumber::parse_str("1").unwrap(), NumberOptionalNumber(1.0, 1.0) ); assert_eq!( NumberOptionalNumber::parse_str("-1, -2").unwrap(), NumberOptionalNumber(-1.0, -2.0) ); assert_eq!( NumberOptionalNumber::parse_str("-1 -2").unwrap(), NumberOptionalNumber(-1.0, -2.0) ); assert_eq!( NumberOptionalNumber::parse_str("-1").unwrap(), NumberOptionalNumber(-1.0, -1.0) ); } #[test] fn invalid_number_optional_number() { assert!(NumberOptionalNumber::::parse_str("").is_err()); assert!(NumberOptionalNumber::::parse_str("1x").is_err()); assert!(NumberOptionalNumber::::parse_str("x1").is_err()); assert!(NumberOptionalNumber::::parse_str("1 x").is_err()); assert!(NumberOptionalNumber::::parse_str("1 , x").is_err()); assert!(NumberOptionalNumber::::parse_str("1 , 2x").is_err()); assert!(NumberOptionalNumber::::parse_str("1 2 x").is_err()); } #[test] fn parses_integer() { assert_eq!(i32::parse_str("0").unwrap(), 0); assert_eq!(i32::parse_str("1").unwrap(), 1); assert_eq!(i32::parse_str("-1").unwrap(), -1); assert_eq!(u32::parse_str("0").unwrap(), 0); assert_eq!(u32::parse_str("1").unwrap(), 1); } #[test] fn invalid_integer() { assert!(i32::parse_str("").is_err()); assert!(i32::parse_str("1x").is_err()); assert!(i32::parse_str("1.5").is_err()); assert!(u32::parse_str("").is_err()); assert!(u32::parse_str("1x").is_err()); assert!(u32::parse_str("1.5").is_err()); assert!(u32::parse_str("-1").is_err()); } #[test] fn parses_integer_optional_integer() { assert_eq!( NumberOptionalNumber::parse_str("1, 2").unwrap(), NumberOptionalNumber(1, 2) ); assert_eq!( NumberOptionalNumber::parse_str("1 2").unwrap(), NumberOptionalNumber(1, 2) ); assert_eq!( NumberOptionalNumber::parse_str("1").unwrap(), NumberOptionalNumber(1, 1) ); assert_eq!( NumberOptionalNumber::parse_str("-1, -2").unwrap(), NumberOptionalNumber(-1, -2) ); assert_eq!( NumberOptionalNumber::parse_str("-1 -2").unwrap(), NumberOptionalNumber(-1, -2) ); assert_eq!( NumberOptionalNumber::parse_str("-1").unwrap(), NumberOptionalNumber(-1, -1) ); } #[test] fn invalid_integer_optional_integer() { assert!(NumberOptionalNumber::::parse_str("").is_err()); assert!(NumberOptionalNumber::::parse_str("1x").is_err()); assert!(NumberOptionalNumber::::parse_str("x1").is_err()); assert!(NumberOptionalNumber::::parse_str("1 x").is_err()); assert!(NumberOptionalNumber::::parse_str("1 , x").is_err()); assert!(NumberOptionalNumber::::parse_str("1 , 2x").is_err()); assert!(NumberOptionalNumber::::parse_str("1 2 x").is_err()); assert!(NumberOptionalNumber::::parse_str("1.5").is_err()); assert!(NumberOptionalNumber::::parse_str("1 2.5").is_err()); assert!(NumberOptionalNumber::::parse_str("1, 2.5").is_err()); } #[test] fn parses_number_list() { assert_eq!( NumberList::<1, 1>::parse_str("5").unwrap(), NumberList(vec![5.0]) ); assert_eq!( NumberList::<4, 4>::parse_str("1 2 3 4").unwrap(), NumberList(vec![1.0, 2.0, 3.0, 4.0]) ); assert_eq!( NumberList::<0, 5>::parse_str("1 2 3 4 5").unwrap(), NumberList(vec![1.0, 2.0, 3.0, 4.0, 5.0]) ); assert_eq!( NumberList::<0, 5>::parse_str("1 2 3").unwrap(), NumberList(vec![1.0, 2.0, 3.0]) ); } #[test] fn errors_on_invalid_number_list() { // empty assert!(NumberList::<1, 1>::parse_str("").is_err()); assert!(NumberList::<0, 1>::parse_str("").is_err()); // garbage assert!(NumberList::<1, 1>::parse_str("foo").is_err()); assert!(NumberList::<2, 2>::parse_str("1foo").is_err()); assert!(NumberList::<2, 2>::parse_str("1 foo").is_err()); assert!(NumberList::<2, 2>::parse_str("1 foo 2").is_err()); assert!(NumberList::<2, 2>::parse_str("1,foo").is_err()); // too many assert!(NumberList::<1, 1>::parse_str("1 2").is_err()); // extra token assert!(NumberList::<1, 1>::parse_str("1,").is_err()); assert!(NumberList::<0, 1>::parse_str("1,").is_err()); // too few assert!(NumberList::<2, 2>::parse_str("1").is_err()); assert!(NumberList::<3, 3>::parse_str("1 2").is_err()); } #[test] fn parses_custom_ident() { assert_eq!( CustomIdent::parse_str("hello").unwrap(), CustomIdent("hello".to_string()) ); } #[test] fn invalid_custom_ident_yields_error() { assert!(CustomIdent::parse_str("initial").is_err()); assert!(CustomIdent::parse_str("inherit").is_err()); assert!(CustomIdent::parse_str("unset").is_err()); assert!(CustomIdent::parse_str("default").is_err()); assert!(CustomIdent::parse_str("").is_err()); } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/path_builder.rs������������������������������������������������������������������0000644�0000000�0000000�00000067161�10461020230�0015203�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Representation of Bézier paths. //! //! Path data can consume a significant amount of memory in complex SVG documents. This //! module deals with this as follows: //! //! * The path parser pushes commands into a [`PathBuilder`]. This is a mutable, //! temporary storage for path data. //! //! * Then, the [`PathBuilder`] gets turned into a long-term, immutable [`Path`] that has //! a more compact representation. //! //! The code tries to reduce work in the allocator, by using a [`TinyVec`] with space for at //! least 32 commands on the stack for `PathBuilder`; most paths in SVGs in the wild have //! fewer than 32 commands, and larger ones will spill to the heap. //! //! See these blog posts for details and profiles: //! //! * [Compact representation for path data](https://people.gnome.org/~federico/blog/reducing-memory-consumption-in-librsvg-4.html) //! * [Reducing slack space and allocator work](https://people.gnome.org/~federico/blog/reducing-memory-consumption-in-librsvg-3.html) use tinyvec::TinyVec; use std::f64; use std::f64::consts::*; use std::slice; use crate::float_eq_cairo::ApproxEqCairo; use crate::path_parser::{ParseError, PathParser}; use crate::util::clamp; /// Whether an arc's sweep should be >= 180 degrees, or smaller. #[derive(Debug, Copy, Clone, PartialEq)] pub struct LargeArc(pub bool); /// Angular direction in which an arc is drawn. #[derive(Debug, Copy, Clone, PartialEq)] pub enum Sweep { Negative, Positive, } /// "c" command for paths; describes a cubic Bézier segment. #[derive(Debug, Clone, PartialEq, Default)] pub struct CubicBezierCurve { /// The (x, y) coordinates of the first control point. pub pt1: (f64, f64), /// The (x, y) coordinates of the second control point. pub pt2: (f64, f64), /// The (x, y) coordinates of the end point of this path segment. pub to: (f64, f64), } impl CubicBezierCurve { /// Consumes 6 coordinates and creates a curve segment. fn from_coords(coords: &mut slice::Iter<'_, f64>) -> CubicBezierCurve { let pt1 = take_two(coords); let pt2 = take_two(coords); let to = take_two(coords); CubicBezierCurve { pt1, pt2, to } } /// Pushes 6 coordinates to `coords` and returns `PackedCommand::CurveTo`. fn to_packed_and_coords(&self, coords: &mut Vec) -> PackedCommand { coords.push(self.pt1.0); coords.push(self.pt1.1); coords.push(self.pt2.0); coords.push(self.pt2.1); coords.push(self.to.0); coords.push(self.to.1); PackedCommand::CurveTo } } /// Conversion from endpoint parameterization to center parameterization. /// /// SVG path data specifies elliptical arcs in terms of their endpoints, but /// they are easier to process if they are converted to a center parameterization. /// /// When attempting to compute the center parameterization of the arc, /// out of range parameters may see an arc omitted or treated as a line. pub enum ArcParameterization { /// Center parameterization of the arc. CenterParameters { /// Center of the ellipse. center: (f64, f64), /// Radii of the ellipse (corrected). radii: (f64, f64), /// Angle of the start point. theta1: f64, /// Delta angle to the end point. delta_theta: f64, }, /// Treat the arc as a line to the end point. LineTo, /// Omit the arc. Omit, } /// "a" command for paths; describes an elliptical arc in terms of its endpoints. #[derive(Debug, Clone, PartialEq)] pub struct EllipticalArc { /// The (x-axis, y-axis) radii for the ellipse. pub r: (f64, f64), /// The rotation angle in degrees for the ellipse's x-axis /// relative to the x-axis of the user coordinate system. pub x_axis_rotation: f64, /// Flag indicating whether the arc sweep should be /// greater than or equal to 180 degrees, or smaller than 180 degrees. pub large_arc: LargeArc, /// Flag indicating the angular direction in which the arc is drawn. pub sweep: Sweep, /// The (x, y) coordinates for the start point of this path segment. pub from: (f64, f64), /// The (x, y) coordinates for the end point of this path segment. pub to: (f64, f64), } impl EllipticalArc { /// Calculates a center parameterization from the endpoint parameterization. /// /// Radii may be adjusted if there is no solution. /// /// See section [B.2.4. Conversion from endpoint to center /// parameterization](https://www.w3.org/TR/SVG2/implnote.html#ArcConversionEndpointToCenter) pub(crate) fn center_parameterization(&self) -> ArcParameterization { let Self { r: (mut rx, mut ry), x_axis_rotation, large_arc, sweep, from: (x1, y1), to: (x2, y2), } = *self; // Ensure radii are non-zero. // Otherwise this arc is treated as a line segment joining the end points. // // A bit further down we divide by the square of the radii. // Check that we won't divide by zero. // See http://bugs.debian.org/508443 if rx * rx < f64::EPSILON || ry * ry < f64::EPSILON { return ArcParameterization::LineTo; } let is_large_arc = large_arc.0; let is_positive_sweep = sweep == Sweep::Positive; let phi = x_axis_rotation * PI / 180.0; let (sin_phi, cos_phi) = phi.sin_cos(); // Ensure radii are positive. rx = rx.abs(); ry = ry.abs(); // The equations simplify after a translation which places // the origin at the midpoint of the line joining (x1, y1) to (x2, y2), // followed by a rotation to line up the coordinate axes // with the axes of the ellipse. // All transformed coordinates will be written with primes. // // Compute (x1', y1'). let mid_x = (x1 - x2) / 2.0; let mid_y = (y1 - y2) / 2.0; let x1_ = cos_phi * mid_x + sin_phi * mid_y; let y1_ = -sin_phi * mid_x + cos_phi * mid_y; // Ensure radii are large enough. let lambda = (x1_ / rx).powi(2) + (y1_ / ry).powi(2); if lambda > 1.0 { // If not, scale up the ellipse uniformly // until there is exactly one solution. rx *= lambda.sqrt(); ry *= lambda.sqrt(); } // Compute the transformed center (cx', cy'). let d = (rx * y1_).powi(2) + (ry * x1_).powi(2); if d == 0.0 { return ArcParameterization::Omit; } let k = { let mut k = ((rx * ry).powi(2) / d - 1.0).abs().sqrt(); if is_positive_sweep == is_large_arc { k = -k; } k }; let cx_ = k * rx * y1_ / ry; let cy_ = -k * ry * x1_ / rx; // Compute the center (cx, cy). let cx = cos_phi * cx_ - sin_phi * cy_ + (x1 + x2) / 2.0; let cy = sin_phi * cx_ + cos_phi * cy_ + (y1 + y2) / 2.0; // Compute the start angle θ1. let ux = (x1_ - cx_) / rx; let uy = (y1_ - cy_) / ry; let u_len = (ux * ux + uy * uy).abs().sqrt(); if u_len == 0.0 { return ArcParameterization::Omit; } let cos_theta1 = clamp(ux / u_len, -1.0, 1.0); let theta1 = { let mut theta1 = cos_theta1.acos(); if uy < 0.0 { theta1 = -theta1; } theta1 }; // Compute the total delta angle Δθ. let vx = (-x1_ - cx_) / rx; let vy = (-y1_ - cy_) / ry; let v_len = (vx * vx + vy * vy).abs().sqrt(); if v_len == 0.0 { return ArcParameterization::Omit; } let dp_uv = ux * vx + uy * vy; let cos_delta_theta = clamp(dp_uv / (u_len * v_len), -1.0, 1.0); let delta_theta = { let mut delta_theta = cos_delta_theta.acos(); if ux * vy - uy * vx < 0.0 { delta_theta = -delta_theta; } if is_positive_sweep && delta_theta < 0.0 { delta_theta += PI * 2.0; } else if !is_positive_sweep && delta_theta > 0.0 { delta_theta -= PI * 2.0; } delta_theta }; ArcParameterization::CenterParameters { center: (cx, cy), radii: (rx, ry), theta1, delta_theta, } } /// Consumes 7 coordinates and creates an arc segment. fn from_coords( large_arc: LargeArc, sweep: Sweep, coords: &mut slice::Iter<'_, f64>, ) -> EllipticalArc { let r = take_two(coords); let x_axis_rotation = take_one(coords); let from = take_two(coords); let to = take_two(coords); EllipticalArc { r, x_axis_rotation, large_arc, sweep, from, to, } } /// Pushes 7 coordinates to `coords` and returns one of `PackedCommand::Arc*`. fn to_packed_and_coords(&self, coords: &mut Vec) -> PackedCommand { coords.push(self.r.0); coords.push(self.r.1); coords.push(self.x_axis_rotation); coords.push(self.from.0); coords.push(self.from.1); coords.push(self.to.0); coords.push(self.to.1); match (self.large_arc, self.sweep) { (LargeArc(false), Sweep::Negative) => PackedCommand::ArcSmallNegative, (LargeArc(false), Sweep::Positive) => PackedCommand::ArcSmallPositive, (LargeArc(true), Sweep::Negative) => PackedCommand::ArcLargeNegative, (LargeArc(true), Sweep::Positive) => PackedCommand::ArcLargePositive, } } } /// Turns an arc segment into a cubic bezier curve. /// /// Takes the center, the radii and the x-axis rotation of the ellipse, /// the angles of the start and end points, /// and returns cubic bezier curve parameters. pub(crate) fn arc_segment( c: (f64, f64), r: (f64, f64), x_axis_rotation: f64, th0: f64, th1: f64, ) -> CubicBezierCurve { let (cx, cy) = c; let (rx, ry) = r; let phi = x_axis_rotation * PI / 180.0; let (sin_phi, cos_phi) = phi.sin_cos(); let (sin_th0, cos_th0) = th0.sin_cos(); let (sin_th1, cos_th1) = th1.sin_cos(); let th_half = 0.5 * (th1 - th0); let t = (8.0 / 3.0) * (th_half * 0.5).sin().powi(2) / th_half.sin(); let x1 = rx * (cos_th0 - t * sin_th0); let y1 = ry * (sin_th0 + t * cos_th0); let x3 = rx * cos_th1; let y3 = ry * sin_th1; let x2 = x3 + rx * (t * sin_th1); let y2 = y3 + ry * (-t * cos_th1); CubicBezierCurve { pt1: ( cx + cos_phi * x1 - sin_phi * y1, cy + sin_phi * x1 + cos_phi * y1, ), pt2: ( cx + cos_phi * x2 - sin_phi * y2, cy + sin_phi * x2 + cos_phi * y2, ), to: ( cx + cos_phi * x3 - sin_phi * y3, cy + sin_phi * x3 + cos_phi * y3, ), } } /// Long-form version of a single path command. /// /// This is returned from iterators on paths and subpaths. #[derive(Clone, Default, Debug, PartialEq)] pub enum PathCommand { MoveTo(f64, f64), LineTo(f64, f64), CurveTo(CubicBezierCurve), Arc(EllipticalArc), // The #[default] is just so we can use TinyVec, whose type // parameter requires T: Default. There is no actual default for // path commands in the SVG spec; this is just our implementation // detail. #[default] ClosePath, } impl PathCommand { /// Returns the number of coordinate values that this command will generate in a `Path`. fn num_coordinates(&self) -> usize { match *self { PathCommand::MoveTo(..) => 2, PathCommand::LineTo(..) => 2, PathCommand::CurveTo(_) => 6, PathCommand::Arc(_) => 7, PathCommand::ClosePath => 0, } } /// Pushes a command's coordinates to `coords` and returns the corresponding `PackedCommand`. fn to_packed(&self, coords: &mut Vec) -> PackedCommand { match *self { PathCommand::MoveTo(x, y) => { coords.push(x); coords.push(y); PackedCommand::MoveTo } PathCommand::LineTo(x, y) => { coords.push(x); coords.push(y); PackedCommand::LineTo } PathCommand::CurveTo(ref c) => c.to_packed_and_coords(coords), PathCommand::Arc(ref a) => a.to_packed_and_coords(coords), PathCommand::ClosePath => PackedCommand::ClosePath, } } /// Consumes a packed command's coordinates from the `coords` iterator and returns the rehydrated `PathCommand`. fn from_packed(packed: PackedCommand, coords: &mut slice::Iter<'_, f64>) -> PathCommand { match packed { PackedCommand::MoveTo => { let x = take_one(coords); let y = take_one(coords); PathCommand::MoveTo(x, y) } PackedCommand::LineTo => { let x = take_one(coords); let y = take_one(coords); PathCommand::LineTo(x, y) } PackedCommand::CurveTo => PathCommand::CurveTo(CubicBezierCurve::from_coords(coords)), PackedCommand::ClosePath => PathCommand::ClosePath, PackedCommand::ArcSmallNegative => PathCommand::Arc(EllipticalArc::from_coords( LargeArc(false), Sweep::Negative, coords, )), PackedCommand::ArcSmallPositive => PathCommand::Arc(EllipticalArc::from_coords( LargeArc(false), Sweep::Positive, coords, )), PackedCommand::ArcLargeNegative => PathCommand::Arc(EllipticalArc::from_coords( LargeArc(true), Sweep::Negative, coords, )), PackedCommand::ArcLargePositive => PathCommand::Arc(EllipticalArc::from_coords( LargeArc(true), Sweep::Positive, coords, )), } } } /// Constructs a path out of commands. /// /// Create this with `PathBuilder::default`; you can then add commands to it or call the /// `parse` method. When you are finished constructing a path builder, turn it into a /// `Path` with `into_path`. You can then iterate on that `Path`'s commands with its /// methods. #[derive(Default)] pub struct PathBuilder { path_commands: TinyVec<[PathCommand; 32]>, } /// An immutable path with a compact representation. /// /// This is constructed from a `PathBuilder` once it is finished. You /// can get an iterator for the path's commands with the `iter` /// method, or an iterator for its subpaths (subsequences of commands that /// start with a MoveTo) with the `iter_subpath` method. /// /// The variants in `PathCommand` have different sizes, so a simple array of `PathCommand` /// would have a lot of slack space. We reduce this to a minimum by separating the /// commands from their coordinates. Then, we can have two dense arrays: one with a compact /// representation of commands, and another with a linear list of the coordinates for each /// command. /// /// Both `PathCommand` and `PackedCommand` know how many coordinates they ought to /// produce, with their `num_coordinates` methods. /// /// This struct implements `Default`, and it yields an empty path. #[derive(Default)] pub struct Path { commands: Box<[PackedCommand]>, coords: Box<[f64]>, } /// Packed version of a `PathCommand`, used in `Path`. /// /// MoveTo/LineTo/CurveTo have only pairs of coordinates, while ClosePath has no coordinates, /// and EllipticalArc has a bunch of coordinates plus two flags. Here we represent the flags /// as four variants. /// /// This is `repr(u8)` to keep it as small as possible. #[repr(u8)] #[derive(Debug, Clone, Copy)] enum PackedCommand { MoveTo, LineTo, CurveTo, ArcSmallNegative, ArcSmallPositive, ArcLargeNegative, ArcLargePositive, ClosePath, } impl PackedCommand { // Returns the number of coordinate values that this command will generate in a `Path`. fn num_coordinates(&self) -> usize { match *self { PackedCommand::MoveTo => 2, PackedCommand::LineTo => 2, PackedCommand::CurveTo => 6, PackedCommand::ArcSmallNegative | PackedCommand::ArcSmallPositive | PackedCommand::ArcLargeNegative | PackedCommand::ArcLargePositive => 7, PackedCommand::ClosePath => 0, } } } impl PathBuilder { pub fn parse(&mut self, path_str: &str) -> Result<(), ParseError> { let mut parser = PathParser::new(self, path_str); parser.parse() } /// Consumes the `PathBuilder` and returns a compact, immutable representation as a `Path`. pub fn into_path(self) -> Path { let num_coords = self .path_commands .iter() .map(PathCommand::num_coordinates) .sum(); let mut coords = Vec::with_capacity(num_coords); let packed_commands: Vec<_> = self .path_commands .iter() .map(|cmd| cmd.to_packed(&mut coords)) .collect(); Path { commands: packed_commands.into_boxed_slice(), coords: coords.into_boxed_slice(), } } /// Adds a MoveTo command to the path. pub fn move_to(&mut self, x: f64, y: f64) { self.path_commands.push(PathCommand::MoveTo(x, y)); } /// Adds a LineTo command to the path. pub fn line_to(&mut self, x: f64, y: f64) { self.path_commands.push(PathCommand::LineTo(x, y)); } /// Adds a CurveTo command to the path. pub fn curve_to(&mut self, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) { let curve = CubicBezierCurve { pt1: (x2, y2), pt2: (x3, y3), to: (x4, y4), }; self.path_commands.push(PathCommand::CurveTo(curve)); } /// Adds an EllipticalArc command to the path. pub fn arc( &mut self, x1: f64, y1: f64, rx: f64, ry: f64, x_axis_rotation: f64, large_arc: LargeArc, sweep: Sweep, x2: f64, y2: f64, ) { let arc = EllipticalArc { r: (rx, ry), x_axis_rotation, large_arc, sweep, from: (x1, y1), to: (x2, y2), }; self.path_commands.push(PathCommand::Arc(arc)); } /// Adds a ClosePath command to the path. pub fn close_path(&mut self) { self.path_commands.push(PathCommand::ClosePath); } } /// An iterator over the subpaths of a `Path`. pub struct SubPathIter<'a> { path: &'a Path, commands_start: usize, coords_start: usize, } /// A slice of commands and coordinates with a single `MoveTo` at the beginning. pub struct SubPath<'a> { commands: &'a [PackedCommand], coords: &'a [f64], } /// An iterator over the commands/coordinates of a subpath. pub struct SubPathCommandsIter<'a> { commands_iter: slice::Iter<'a, PackedCommand>, coords_iter: slice::Iter<'a, f64>, } impl<'a> SubPath<'a> { /// Returns an iterator over the subpath's commands. pub fn iter_commands(&self) -> SubPathCommandsIter<'_> { SubPathCommandsIter { commands_iter: self.commands.iter(), coords_iter: self.coords.iter(), } } /// Each subpath starts with a MoveTo; this returns its `(x, y)` coordinates. pub fn origin(&self) -> (f64, f64) { let first = *self.commands.first().unwrap(); assert!(matches!(first, PackedCommand::MoveTo)); let command = PathCommand::from_packed(first, &mut self.coords.iter()); match command { PathCommand::MoveTo(x, y) => (x, y), _ => unreachable!(), } } /// Returns whether the length of a subpath is approximately zero. pub fn is_zero_length(&self) -> bool { let (cur_x, cur_y) = self.origin(); for cmd in self.iter_commands().skip(1) { let (end_x, end_y) = match cmd { PathCommand::MoveTo(_, _) => unreachable!( "A MoveTo cannot appear in a subpath if it's not the first element" ), PathCommand::LineTo(x, y) => (x, y), PathCommand::CurveTo(curve) => curve.to, PathCommand::Arc(arc) => arc.to, // If we get a `ClosePath and haven't returned yet then we haven't moved at all making // it an empty subpath` PathCommand::ClosePath => return true, }; if !end_x.approx_eq_cairo(cur_x) || !end_y.approx_eq_cairo(cur_y) { return false; } } true } } impl<'a> Iterator for SubPathIter<'a> { type Item = SubPath<'a>; fn next(&mut self) -> Option { // If we ended on our last command in the previous iteration, we're done here if self.commands_start >= self.path.commands.len() { return None; } // Otherwise we have at least one command left, we setup the slice to be all the remaining // commands. let commands = &self.path.commands[self.commands_start..]; assert!(matches!(commands.first().unwrap(), PackedCommand::MoveTo)); let mut num_coords = PackedCommand::MoveTo.num_coordinates(); // Skip over the initial MoveTo for (i, cmd) in commands.iter().enumerate().skip(1) { // If we encounter a MoveTo , we ended our current subpath, we // return the commands until this command and set commands_start to be the index of the // next command if let PackedCommand::MoveTo = cmd { let subpath_coords_start = self.coords_start; self.commands_start += i; self.coords_start += num_coords; return Some(SubPath { commands: &commands[..i], coords: &self.path.coords [subpath_coords_start..subpath_coords_start + num_coords], }); } else { num_coords += cmd.num_coordinates(); } } // If we didn't find any MoveTo, we're done here. We return the rest of the path // and set commands_start so next iteration will return None. self.commands_start = self.path.commands.len(); let subpath_coords_start = self.coords_start; assert!(subpath_coords_start + num_coords == self.path.coords.len()); self.coords_start = self.path.coords.len(); Some(SubPath { commands, coords: &self.path.coords[subpath_coords_start..], }) } } impl<'a> Iterator for SubPathCommandsIter<'a> { type Item = PathCommand; fn next(&mut self) -> Option { self.commands_iter .next() .map(|packed| PathCommand::from_packed(*packed, &mut self.coords_iter)) } } impl Path { /// Get an iterator over a path `Subpath`s. pub fn iter_subpath(&self) -> SubPathIter<'_> { SubPathIter { path: self, commands_start: 0, coords_start: 0, } } /// Get an iterator over a path's commands. pub fn iter(&self) -> impl Iterator + '_ { let commands = self.commands.iter(); let mut coords = self.coords.iter(); commands.map(move |cmd| PathCommand::from_packed(*cmd, &mut coords)) } /// Returns whether there are no commands in the path. pub fn is_empty(&self) -> bool { self.commands.is_empty() } } fn take_one(iter: &mut slice::Iter<'_, f64>) -> f64 { *iter.next().unwrap() } fn take_two(iter: &mut slice::Iter<'_, f64>) -> (f64, f64) { (take_one(iter), take_one(iter)) } #[cfg(test)] mod tests { use super::*; #[test] fn empty_builder() { let builder = PathBuilder::default(); let path = builder.into_path(); assert!(path.is_empty()); assert_eq!(path.iter().count(), 0); } #[test] fn empty_path() { let path = Path::default(); assert!(path.is_empty()); assert_eq!(path.iter().count(), 0); } #[test] fn all_commands() { let mut builder = PathBuilder::default(); builder.move_to(42.0, 43.0); builder.line_to(42.0, 43.0); builder.curve_to(42.0, 43.0, 44.0, 45.0, 46.0, 47.0); builder.arc( 42.0, 43.0, 44.0, 45.0, 46.0, LargeArc(true), Sweep::Positive, 47.0, 48.0, ); builder.close_path(); let path = builder.into_path(); assert!(path.iter().eq(vec![ PathCommand::MoveTo(42.0, 43.0), PathCommand::LineTo(42.0, 43.0), PathCommand::CurveTo(CubicBezierCurve { pt1: (42.0, 43.0), pt2: (44.0, 45.0), to: (46.0, 47.0), }), PathCommand::Arc(EllipticalArc { from: (42.0, 43.0), r: (44.0, 45.0), to: (47.0, 48.0), x_axis_rotation: 46.0, large_arc: LargeArc(true), sweep: Sweep::Positive, }), PathCommand::ClosePath, ])); } #[test] fn subpath_iter() { let mut builder = PathBuilder::default(); builder.move_to(42.0, 43.0); builder.line_to(42.0, 43.0); builder.close_path(); builder.move_to(22.0, 22.0); builder.curve_to(22.0, 22.0, 44.0, 45.0, 46.0, 47.0); builder.move_to(69.0, 69.0); builder.line_to(42.0, 43.0); let path = builder.into_path(); let subpaths = path .iter_subpath() .map(|subpath| { ( subpath.origin(), subpath.iter_commands().collect::>(), ) }) .collect::)>>(); assert_eq!( subpaths, vec![ ( (42.0, 43.0), vec![ PathCommand::MoveTo(42.0, 43.0), PathCommand::LineTo(42.0, 43.0), PathCommand::ClosePath ] ), ( (22.0, 22.0), vec![ PathCommand::MoveTo(22.0, 22.0), PathCommand::CurveTo(CubicBezierCurve { pt1: (22.0, 22.0), pt2: (44.0, 45.0), to: (46.0, 47.0) }) ] ), ( (69.0, 69.0), vec![ PathCommand::MoveTo(69.0, 69.0), PathCommand::LineTo(42.0, 43.0) ] ) ] ); } #[test] fn zero_length_subpaths() { let mut builder = PathBuilder::default(); builder.move_to(42.0, 43.0); builder.move_to(44.0, 45.0); builder.close_path(); builder.move_to(46.0, 47.0); builder.line_to(48.0, 49.0); let path = builder.into_path(); let subpaths = path .iter_subpath() .map(|subpath| (subpath.is_zero_length(), subpath.origin())) .collect::>(); assert_eq!( subpaths, vec![ (true, (42.0, 43.0)), (true, (44.0, 45.0)), (false, (46.0, 47.0)), ] ); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/path_parser.rs�������������������������������������������������������������������0000644�0000000�0000000�00000162662�10461020230�0015053�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Parser for SVG path data. use std::fmt; use std::iter::Enumerate; use std::str; use std::str::Bytes; use crate::path_builder::*; #[derive(Debug, PartialEq, Copy, Clone)] pub enum Token { // pub to allow benchmarking Number(f64), Flag(bool), Command(u8), Comma, } use crate::path_parser::Token::{Comma, Command, Flag, Number}; #[derive(Debug)] pub struct Lexer<'a> { // pub to allow benchmarking input: &'a [u8], ci: Enumerate>, current: Option<(usize, u8)>, flags_required: u8, } #[derive(Debug, PartialEq, Copy, Clone)] pub enum LexError { // pub to allow benchmarking ParseFloatError, UnexpectedByte(u8), UnexpectedEof, } impl<'a> Lexer<'_> { pub fn new(input: &'a str) -> Lexer<'a> { let mut ci = input.bytes().enumerate(); let current = ci.next(); Lexer { input: input.as_bytes(), ci, current, flags_required: 0, } } // The way Flag tokens work is a little annoying. We don't have // any way to distinguish between numbers and flags without context // from the parser. The only time we need to return flags is within the // argument sequence of an elliptical arc, and then we need 2 in a row // or it's an error. So, when the parser gets to that point, it calls // this method and we switch from our usual mode of handling digits as // numbers to looking for two 'flag' characters (either 0 or 1) in a row // (with optional intervening whitespace, and possibly comma tokens.) // Every time we find a flag we decrement flags_required. pub fn require_flags(&mut self) { self.flags_required = 2; } fn current_pos(&mut self) -> usize { match self.current { None => self.input.len(), Some((pos, _)) => pos, } } fn advance(&mut self) { self.current = self.ci.next(); } fn advance_over_whitespace(&mut self) -> bool { let mut found_some = false; while self.current.is_some() && self.current.unwrap().1.is_ascii_whitespace() { found_some = true; self.current = self.ci.next(); } found_some } fn advance_over_optional(&mut self, needle: u8) -> bool { match self.current { Some((_, c)) if c == needle => { self.advance(); true } _ => false, } } fn advance_over_digits(&mut self) -> bool { let mut found_some = false; while self.current.is_some() && self.current.unwrap().1.is_ascii_digit() { found_some = true; self.current = self.ci.next(); } found_some } fn advance_over_simple_number(&mut self) -> bool { let _ = self.advance_over_optional(b'-') || self.advance_over_optional(b'+'); let found_digit = self.advance_over_digits(); let _ = self.advance_over_optional(b'.'); self.advance_over_digits() || found_digit } fn match_number(&mut self) -> Result { // remember the beginning let (start_pos, _) = self.current.unwrap(); if !self.advance_over_simple_number() && start_pos != self.current_pos() { match self.current { None => return Err(LexError::UnexpectedEof), Some((_pos, c)) => return Err(LexError::UnexpectedByte(c)), } } if self.advance_over_optional(b'e') || self.advance_over_optional(b'E') { let _ = self.advance_over_optional(b'-') || self.advance_over_optional(b'+'); let _ = self.advance_over_digits(); } let end_pos = match self.current { None => self.input.len(), Some((i, _)) => i, }; // If you need path parsing to be faster, you can do from_utf8_unchecked to // avoid re-validating all the chars, and std::str::parse calls are // faster than std::str::parse for numbers that are not floats. // bare unwrap here should be safe since we've already checked all the bytes // in the range match std::str::from_utf8(&self.input[start_pos..end_pos]) .unwrap() .parse::() { Ok(n) => Ok(Number(n)), Err(_e) => Err(LexError::ParseFloatError), } } } impl Iterator for Lexer<'_> { type Item = (usize, Result); fn next(&mut self) -> Option { // eat whitespace self.advance_over_whitespace(); match self.current { // commas are separators Some((pos, b',')) => { self.advance(); Some((pos, Ok(Comma))) } // alphabetic chars are commands Some((pos, c)) if c.is_ascii_alphabetic() => { let token = Command(c); self.advance(); Some((pos, Ok(token))) } Some((pos, c)) if self.flags_required > 0 && c.is_ascii_digit() => match c { b'0' => { self.flags_required -= 1; self.advance(); Some((pos, Ok(Flag(false)))) } b'1' => { self.flags_required -= 1; self.advance(); Some((pos, Ok(Flag(true)))) } _ => Some((pos, Err(LexError::UnexpectedByte(c)))), }, Some((pos, c)) if c.is_ascii_digit() || c == b'-' || c == b'+' || c == b'.' => { Some((pos, self.match_number())) } Some((pos, c)) => { self.advance(); Some((pos, Err(LexError::UnexpectedByte(c)))) } None => None, } } } pub struct PathParser<'b> { tokens: Lexer<'b>, current_pos_and_token: Option<(usize, Result)>, builder: &'b mut PathBuilder, // Current point; adjusted at every command current_x: f64, current_y: f64, // Last control point from previous cubic curve command, used to reflect // the new control point for smooth cubic curve commands. cubic_reflection_x: f64, cubic_reflection_y: f64, // Last control point from previous quadratic curve command, used to reflect // the new control point for smooth quadratic curve commands. quadratic_reflection_x: f64, quadratic_reflection_y: f64, // Start point of current subpath (i.e. position of last moveto); // used for closepath. subpath_start_x: f64, subpath_start_y: f64, } // This is a recursive descent parser for path data in SVG files, // as specified in https://www.w3.org/TR/SVG/paths.html#PathDataBNF // Some peculiarities: // // - SVG allows optional commas inside coordinate pairs, and between // coordinate pairs. So, for example, these are equivalent: // // M 10 20 30 40 // M 10, 20 30, 40 // M 10, 20, 30, 40 // // - Whitespace is optional. These are equivalent: // // M10,20 30,40 // M10,20,30,40 // // These are also equivalent: // // M-10,20-30-40 // M -10 20 -30 -40 // // M.1-2,3E2-4 // M 0.1 -2 300 -4 impl<'b> PathParser<'b> { pub fn new(builder: &'b mut PathBuilder, path_str: &'b str) -> PathParser<'b> { let mut lexer = Lexer::new(path_str); let pt = lexer.next(); PathParser { tokens: lexer, current_pos_and_token: pt, builder, current_x: 0.0, current_y: 0.0, cubic_reflection_x: 0.0, cubic_reflection_y: 0.0, quadratic_reflection_x: 0.0, quadratic_reflection_y: 0.0, subpath_start_x: 0.0, subpath_start_y: 0.0, } } // Our match_* methods all either consume the token we requested // and return the unwrapped value, or return an error without // advancing the token stream. // // You can safely use them to probe for a particular kind of token, // fail to match it, and try some other type. fn match_command(&mut self) -> Result { let result = match &self.current_pos_and_token { Some((_, Ok(Command(c)))) => Ok(*c), Some((pos, Ok(t))) => Err(ParseError::new(*pos, UnexpectedToken(*t))), Some((pos, Err(e))) => Err(ParseError::new(*pos, LexError(*e))), None => Err(ParseError::new(self.tokens.input.len(), UnexpectedEof)), }; if result.is_ok() { self.current_pos_and_token = self.tokens.next(); } result } fn match_number(&mut self) -> Result { let result = match &self.current_pos_and_token { Some((_, Ok(Number(n)))) => Ok(*n), Some((pos, Ok(t))) => Err(ParseError::new(*pos, UnexpectedToken(*t))), Some((pos, Err(e))) => Err(ParseError::new(*pos, LexError(*e))), None => Err(ParseError::new(self.tokens.input.len(), UnexpectedEof)), }; if result.is_ok() { self.current_pos_and_token = self.tokens.next(); } result } fn match_number_and_flags(&mut self) -> Result<(f64, bool, bool), ParseError> { // We can't just do self.match_number() here, because we have to // tell the lexer, if we do find a number, to switch to looking for flags // before we advance it to the next token. Otherwise it will treat the flag // characters as numbers. // // So, first we do the guts of match_number... let n = match &self.current_pos_and_token { Some((_, Ok(Number(n)))) => Ok(*n), Some((pos, Ok(t))) => Err(ParseError::new(*pos, UnexpectedToken(*t))), Some((pos, Err(e))) => Err(ParseError::new(*pos, LexError(*e))), None => Err(ParseError::new(self.tokens.input.len(), UnexpectedEof)), }?; // Then we tell the lexer that we're going to need to find Flag tokens, // *then* we can advance the token stream. self.tokens.require_flags(); self.current_pos_and_token = self.tokens.next(); self.eat_optional_comma(); let f1 = self.match_flag()?; self.eat_optional_comma(); let f2 = self.match_flag()?; Ok((n, f1, f2)) } fn match_comma(&mut self) -> Result<(), ParseError> { let result = match &self.current_pos_and_token { Some((_, Ok(Comma))) => Ok(()), Some((pos, Ok(t))) => Err(ParseError::new(*pos, UnexpectedToken(*t))), Some((pos, Err(e))) => Err(ParseError::new(*pos, LexError(*e))), None => Err(ParseError::new(self.tokens.input.len(), UnexpectedEof)), }; if result.is_ok() { self.current_pos_and_token = self.tokens.next(); } result } fn eat_optional_comma(&mut self) { let _ = self.match_comma(); } // Convenience function; like match_number, but eats a leading comma if present. fn match_comma_number(&mut self) -> Result { self.eat_optional_comma(); self.match_number() } fn match_flag(&mut self) -> Result { let result = match self.current_pos_and_token { Some((_, Ok(Flag(f)))) => Ok(f), Some((pos, Ok(t))) => Err(ParseError::new(pos, UnexpectedToken(t))), Some((pos, Err(e))) => Err(ParseError::new(pos, LexError(e))), None => Err(ParseError::new(self.tokens.input.len(), UnexpectedEof)), }; if result.is_ok() { self.current_pos_and_token = self.tokens.next(); } result } // peek_* methods are the twins of match_*, but don't consume the token, and so // can't return ParseError fn peek_command(&mut self) -> Option { match &self.current_pos_and_token { Some((_, Ok(Command(c)))) => Some(*c), _ => None, } } fn peek_number(&mut self) -> Option { match &self.current_pos_and_token { Some((_, Ok(Number(n)))) => Some(*n), _ => None, } } // This is the entry point for parsing a given blob of path data. // All the parsing just uses various match_* methods to consume tokens // and retrieve the values. pub fn parse(&mut self) -> Result<(), ParseError> { if self.current_pos_and_token.is_none() { return Ok(()); } self.moveto_drawto_command_groups() } fn error(&self, kind: ErrorKind) -> ParseError { match self.current_pos_and_token { Some((pos, _)) => ParseError { position: pos, kind, }, None => ParseError { position: 0, kind }, // FIXME: ??? } } fn coordinate_pair(&mut self) -> Result<(f64, f64), ParseError> { Ok((self.match_number()?, self.match_comma_number()?)) } fn set_current_point(&mut self, x: f64, y: f64) { self.current_x = x; self.current_y = y; self.cubic_reflection_x = self.current_x; self.cubic_reflection_y = self.current_y; self.quadratic_reflection_x = self.current_x; self.quadratic_reflection_y = self.current_y; } fn set_cubic_reflection_and_current_point(&mut self, x3: f64, y3: f64, x4: f64, y4: f64) { self.cubic_reflection_x = x3; self.cubic_reflection_y = y3; self.current_x = x4; self.current_y = y4; self.quadratic_reflection_x = self.current_x; self.quadratic_reflection_y = self.current_y; } fn set_quadratic_reflection_and_current_point(&mut self, a: f64, b: f64, c: f64, d: f64) { self.quadratic_reflection_x = a; self.quadratic_reflection_y = b; self.current_x = c; self.current_y = d; self.cubic_reflection_x = self.current_x; self.cubic_reflection_y = self.current_y; } fn emit_move_to(&mut self, x: f64, y: f64) { self.set_current_point(x, y); self.subpath_start_x = self.current_x; self.subpath_start_y = self.current_y; self.builder.move_to(self.current_x, self.current_y); } fn emit_line_to(&mut self, x: f64, y: f64) { self.set_current_point(x, y); self.builder.line_to(self.current_x, self.current_y); } fn emit_curve_to(&mut self, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) { self.set_cubic_reflection_and_current_point(x3, y3, x4, y4); self.builder.curve_to(x2, y2, x3, y3, x4, y4); } fn emit_quadratic_curve_to(&mut self, a: f64, b: f64, c: f64, d: f64) { // raise quadratic Bézier to cubic let x2 = (self.current_x + 2.0 * a) / 3.0; let y2 = (self.current_y + 2.0 * b) / 3.0; let x4 = c; let y4 = d; let x3 = (x4 + 2.0 * a) / 3.0; let y3 = (y4 + 2.0 * b) / 3.0; self.set_quadratic_reflection_and_current_point(a, b, c, d); self.builder.curve_to(x2, y2, x3, y3, x4, y4); } fn emit_arc( &mut self, rx: f64, ry: f64, x_axis_rotation: f64, large_arc: LargeArc, sweep: Sweep, x: f64, y: f64, ) { let (start_x, start_y) = (self.current_x, self.current_y); self.set_current_point(x, y); self.builder.arc( start_x, start_y, rx, ry, x_axis_rotation, large_arc, sweep, self.current_x, self.current_y, ); } fn moveto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { let (mut x, mut y) = self.coordinate_pair()?; if !absolute { x += self.current_x; y += self.current_y; } self.emit_move_to(x, y); if self.match_comma().is_ok() || self.peek_number().is_some() { self.lineto_argument_sequence(absolute) } else { Ok(()) } } fn moveto(&mut self) -> Result<(), ParseError> { match self.match_command()? { b'M' => self.moveto_argument_sequence(true), b'm' => self.moveto_argument_sequence(false), c => Err(self.error(ErrorKind::UnexpectedCommand(c))), } } fn moveto_drawto_command_group(&mut self) -> Result<(), ParseError> { self.moveto()?; self.optional_drawto_commands().map(|_| ()) } fn moveto_drawto_command_groups(&mut self) -> Result<(), ParseError> { loop { self.moveto_drawto_command_group()?; if self.current_pos_and_token.is_none() { break; } } Ok(()) } fn optional_drawto_commands(&mut self) -> Result { while self.drawto_command()? { // everything happens in the drawto_command() calls. } Ok(false) } // FIXME: This should not just fail to match 'M' and 'm', but make sure the // command is in the set of drawto command characters. fn match_if_drawto_command_with_absolute(&mut self) -> Option<(u8, bool)> { let cmd = self.peek_command(); let result = match cmd { Some(b'M') => None, Some(b'm') => None, Some(c) => { let c_up = c.to_ascii_uppercase(); if c == c_up { Some((c_up, true)) } else { Some((c_up, false)) } } _ => None, }; if result.is_some() { let _ = self.match_command(); } result } fn drawto_command(&mut self) -> Result { match self.match_if_drawto_command_with_absolute() { Some((b'Z', _)) => { self.emit_close_path(); Ok(true) } Some((b'L', abs)) => { self.lineto_argument_sequence(abs)?; Ok(true) } Some((b'H', abs)) => { self.horizontal_lineto_argument_sequence(abs)?; Ok(true) } Some((b'V', abs)) => { self.vertical_lineto_argument_sequence(abs)?; Ok(true) } Some((b'C', abs)) => { self.curveto_argument_sequence(abs)?; Ok(true) } Some((b'S', abs)) => { self.smooth_curveto_argument_sequence(abs)?; Ok(true) } Some((b'Q', abs)) => { self.quadratic_curveto_argument_sequence(abs)?; Ok(true) } Some((b'T', abs)) => { self.smooth_quadratic_curveto_argument_sequence(abs)?; Ok(true) } Some((b'A', abs)) => { self.elliptical_arc_argument_sequence(abs)?; Ok(true) } _ => Ok(false), } } fn emit_close_path(&mut self) { let (x, y) = (self.subpath_start_x, self.subpath_start_y); self.set_current_point(x, y); self.builder.close_path(); } fn should_break_arg_sequence(&mut self) -> bool { if self.match_comma().is_ok() { // if there is a comma (indicating we should continue to loop), eat the comma // so we're ready at the next start of the loop to process the next token. false } else { // continue to process args in the sequence unless the next token is a comma self.peek_number().is_none() } } fn lineto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { loop { let (mut x, mut y) = self.coordinate_pair()?; if !absolute { x += self.current_x; y += self.current_y; } self.emit_line_to(x, y); if self.should_break_arg_sequence() { break; } } Ok(()) } fn horizontal_lineto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { loop { let mut x = self.match_number()?; if !absolute { x += self.current_x; } let y = self.current_y; self.emit_line_to(x, y); if self.should_break_arg_sequence() { break; } } Ok(()) } fn vertical_lineto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { loop { let mut y = self.match_number()?; if !absolute { y += self.current_y; } let x = self.current_x; self.emit_line_to(x, y); if self.should_break_arg_sequence() { break; } } Ok(()) } fn curveto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { loop { let (mut x2, mut y2) = self.coordinate_pair()?; self.eat_optional_comma(); let (mut x3, mut y3) = self.coordinate_pair()?; self.eat_optional_comma(); let (mut x4, mut y4) = self.coordinate_pair()?; if !absolute { x2 += self.current_x; y2 += self.current_y; x3 += self.current_x; y3 += self.current_y; x4 += self.current_x; y4 += self.current_y; } self.emit_curve_to(x2, y2, x3, y3, x4, y4); if self.should_break_arg_sequence() { break; } } Ok(()) } fn smooth_curveto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { loop { let (mut x3, mut y3) = self.coordinate_pair()?; self.eat_optional_comma(); let (mut x4, mut y4) = self.coordinate_pair()?; if !absolute { x3 += self.current_x; y3 += self.current_y; x4 += self.current_x; y4 += self.current_y; } let (x2, y2) = ( self.current_x + self.current_x - self.cubic_reflection_x, self.current_y + self.current_y - self.cubic_reflection_y, ); self.emit_curve_to(x2, y2, x3, y3, x4, y4); if self.should_break_arg_sequence() { break; } } Ok(()) } fn quadratic_curveto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { loop { let (mut a, mut b) = self.coordinate_pair()?; self.eat_optional_comma(); let (mut c, mut d) = self.coordinate_pair()?; if !absolute { a += self.current_x; b += self.current_y; c += self.current_x; d += self.current_y; } self.emit_quadratic_curve_to(a, b, c, d); if self.should_break_arg_sequence() { break; } } Ok(()) } fn smooth_quadratic_curveto_argument_sequence( &mut self, absolute: bool, ) -> Result<(), ParseError> { loop { let (mut c, mut d) = self.coordinate_pair()?; if !absolute { c += self.current_x; d += self.current_y; } let (a, b) = ( self.current_x + self.current_x - self.quadratic_reflection_x, self.current_y + self.current_y - self.quadratic_reflection_y, ); self.emit_quadratic_curve_to(a, b, c, d); if self.should_break_arg_sequence() { break; } } Ok(()) } fn elliptical_arc_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { loop { let rx = self.match_number()?.abs(); let ry = self.match_comma_number()?.abs(); self.eat_optional_comma(); let (x_axis_rotation, f1, f2) = self.match_number_and_flags()?; let large_arc = LargeArc(f1); let sweep = if f2 { Sweep::Positive } else { Sweep::Negative }; self.eat_optional_comma(); let (mut x, mut y) = self.coordinate_pair()?; if !absolute { x += self.current_x; y += self.current_y; } self.emit_arc(rx, ry, x_axis_rotation, large_arc, sweep, x, y); if self.should_break_arg_sequence() { break; } } Ok(()) } } #[derive(Debug, PartialEq)] pub enum ErrorKind { UnexpectedToken(Token), UnexpectedCommand(u8), UnexpectedEof, LexError(LexError), } #[derive(Debug, PartialEq)] pub struct ParseError { pub position: usize, pub kind: ErrorKind, } impl ParseError { fn new(pos: usize, k: ErrorKind) -> ParseError { ParseError { position: pos, kind: k, } } } use crate::path_parser::ErrorKind::*; impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let description = match self.kind { UnexpectedToken(_t) => "unexpected token", UnexpectedCommand(_c) => "unexpected command", UnexpectedEof => "unexpected end of data", LexError(_le) => "error processing token", }; write!(f, "error at position {}: {}", self.position, description) } } #[cfg(test)] #[rustfmt::skip] mod tests { use super::*; fn find_error_pos(s: &str) -> Option { s.find('^') } fn make_parse_result( error_pos_str: &str, error_kind: Option, ) -> Result<(), ParseError> { if let Some(pos) = find_error_pos(error_pos_str) { Err(ParseError { position: pos, kind: error_kind.unwrap(), }) } else { assert!(error_kind.is_none()); Ok(()) } } fn test_parser( path_str: &str, error_pos_str: &str, expected_commands: &[PathCommand], expected_error_kind: Option, ) { let expected_result = make_parse_result(error_pos_str, expected_error_kind); let mut builder = PathBuilder::default(); let result = builder.parse(path_str); let path = builder.into_path(); let commands = path.iter().collect::>(); assert_eq!(expected_commands, commands.as_slice()); assert_eq!(expected_result, result); } fn moveto(x: f64, y: f64) -> PathCommand { PathCommand::MoveTo(x, y) } fn lineto(x: f64, y: f64) -> PathCommand { PathCommand::LineTo(x, y) } fn curveto(x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) -> PathCommand { PathCommand::CurveTo(CubicBezierCurve { pt1: (x2, y2), pt2: (x3, y3), to: (x4, y4), }) } fn arc(x2: f64, y2: f64, xr: f64, large_arc: bool, sweep: bool, x3: f64, y3: f64, x4: f64, y4: f64) -> PathCommand { PathCommand::Arc(EllipticalArc { r: (x2, y2), x_axis_rotation: xr, large_arc: LargeArc(large_arc), sweep: match sweep { true => Sweep::Positive, false => Sweep::Negative, }, from: (x3, y3), to: (x4, y4), }) } fn closepath() -> PathCommand { PathCommand::ClosePath } #[test] fn handles_empty_data() { test_parser( "", "", &Vec::::new(), None, ); } #[test] fn handles_numbers() { test_parser( "M 10 20", "", &[moveto(10.0, 20.0)], None, ); test_parser( "M -10 -20", "", &[moveto(-10.0, -20.0)], None, ); test_parser( "M .10 0.20", "", &[moveto(0.10, 0.20)], None, ); test_parser( "M -.10 -0.20", "", &[moveto(-0.10, -0.20)], None, ); test_parser( "M-.10-0.20", "", &[moveto(-0.10, -0.20)], None, ); test_parser( "M10.5.50", "", &[moveto(10.5, 0.50)], None, ); test_parser( "M.10.20", "", &[moveto(0.10, 0.20)], None, ); test_parser( "M .10E1 .20e-4", "", &[moveto(1.0, 0.000020)], None, ); test_parser( "M-.10E1-.20", "", &[moveto(-1.0, -0.20)], None, ); test_parser( "M10.10E2 -0.20e3", "", &[moveto(1010.0, -200.0)], None, ); test_parser( "M-10.10E2-0.20e-3", "", &[moveto(-1010.0, -0.00020)], None, ); test_parser( "M1e2.5", // a decimal after exponent start the next number "", &[moveto(100.0, 0.5)], None, ); test_parser( "M1e-2.5", // but we are allowed a sign after exponent "", &[moveto(0.01, 0.5)], None, ); test_parser( "M1e+2.5", // but we are allowed a sign after exponent "", &[moveto(100.0, 0.5)], None, ); } #[test] fn detects_bogus_numbers() { test_parser( "M+", " ^", &[], Some(ErrorKind::LexError(LexError::UnexpectedEof)), ); test_parser( "M-", " ^", &[], Some(ErrorKind::LexError(LexError::UnexpectedEof)), ); test_parser( "M+x", " ^", &[], Some(ErrorKind::LexError(LexError::UnexpectedByte(b'x'))), ); test_parser( "M10e", " ^", &[], Some(ErrorKind::LexError(LexError::ParseFloatError)), ); test_parser( "M10ex", " ^", &[], Some(ErrorKind::LexError(LexError::ParseFloatError)), ); test_parser( "M10e-", " ^", &[], Some(ErrorKind::LexError(LexError::ParseFloatError)), ); test_parser( "M10e+x", " ^", &[], Some(ErrorKind::LexError(LexError::ParseFloatError)), ); } #[test] fn handles_numbers_with_comma() { test_parser( "M 10, 20", "", &[moveto(10.0, 20.0)], None, ); test_parser( "M -10,-20", "", &[moveto(-10.0, -20.0)], None, ); test_parser( "M.10 , 0.20", "", &[moveto(0.10, 0.20)], None, ); test_parser( "M -.10, -0.20 ", "", &[moveto(-0.10, -0.20)], None, ); test_parser( "M-.10-0.20", "", &[moveto(-0.10, -0.20)], None, ); test_parser( "M.10.20", "", &[moveto(0.10, 0.20)], None, ); test_parser( "M .10E1,.20e-4", "", &[moveto(1.0, 0.000020)], None, ); test_parser( "M-.10E-2,-.20", "", &[moveto(-0.0010, -0.20)], None, ); test_parser( "M10.10E2,-0.20e3", "", &[moveto(1010.0, -200.0)], None, ); test_parser( "M-10.10E2,-0.20e-3", "", &[moveto(-1010.0, -0.00020)], None, ); } #[test] fn handles_single_moveto() { test_parser( "M 10 20 ", "", &[moveto(10.0, 20.0)], None, ); test_parser( "M10,20 ", "", &[moveto(10.0, 20.0)], None, ); test_parser( "M10 20 ", "", &[moveto(10.0, 20.0)], None, ); test_parser( " M10,20 ", "", &[moveto(10.0, 20.0)], None, ); } #[test] fn handles_relative_moveto() { test_parser( "m10 20", "", &[moveto(10.0, 20.0)], None, ); } #[test] fn handles_absolute_moveto_with_implicit_lineto() { test_parser( "M10 20 30 40", "", &[moveto(10.0, 20.0), lineto(30.0, 40.0)], None, ); test_parser( "M10,20,30,40", "", &[moveto(10.0, 20.0), lineto(30.0, 40.0)], None, ); test_parser( "M.1-2,3E2-4", "", &[moveto(0.1, -2.0), lineto(300.0, -4.0)], None, ); } #[test] fn handles_relative_moveto_with_implicit_lineto() { test_parser( "m10 20 30 40", "", &[moveto(10.0, 20.0), lineto(40.0, 60.0)], None, ); } #[test] fn handles_relative_moveto_with_relative_lineto_sequence() { test_parser( // 1 2 3 4 5 "m 46,447 l 0,0.5 -1,0 -1,0 0,1 0,12", "", &vec![moveto(46.0, 447.0), lineto(46.0, 447.5), lineto(45.0, 447.5), lineto(44.0, 447.5), lineto(44.0, 448.5), lineto(44.0, 460.5)], None, ); } #[test] fn handles_absolute_moveto_with_implicit_linetos() { test_parser( "M10,20 30,40,50 60", "", &[moveto(10.0, 20.0), lineto(30.0, 40.0), lineto(50.0, 60.0)], None, ); } #[test] fn handles_relative_moveto_with_implicit_linetos() { test_parser( "m10 20 30 40 50 60", "", &[moveto(10.0, 20.0), lineto(40.0, 60.0), lineto(90.0, 120.0)], None, ); } #[test] fn handles_absolute_moveto_moveto() { test_parser( "M10 20 M 30 40", "", &[moveto(10.0, 20.0), moveto(30.0, 40.0)], None, ); } #[test] fn handles_relative_moveto_moveto() { test_parser( "m10 20 m 30 40", "", &[moveto(10.0, 20.0), moveto(40.0, 60.0)], None, ); } #[test] fn handles_relative_moveto_lineto_moveto() { test_parser( "m10 20 30 40 m 50 60", "", &[moveto(10.0, 20.0), lineto(40.0, 60.0), moveto(90.0, 120.0)], None, ); } #[test] fn handles_absolute_moveto_lineto() { test_parser( "M10 20 L30,40", "", &[moveto(10.0, 20.0), lineto(30.0, 40.0)], None, ); } #[test] fn handles_relative_moveto_lineto() { test_parser( "m10 20 l30,40", "", &[moveto(10.0, 20.0), lineto(40.0, 60.0)], None, ); } #[test] fn handles_relative_moveto_lineto_lineto_abs_lineto() { test_parser( "m10 20 30 40l30,40,50 60L200,300", "", &vec![ moveto(10.0, 20.0), lineto(40.0, 60.0), lineto(70.0, 100.0), lineto(120.0, 160.0), lineto(200.0, 300.0), ], None, ); } #[test] fn handles_horizontal_lineto() { test_parser( "M10 20 H30", "", &[moveto(10.0, 20.0), lineto(30.0, 20.0)], None, ); test_parser( "M10 20 H30 40", "", &[moveto(10.0, 20.0), lineto(30.0, 20.0), lineto(40.0, 20.0)], None, ); test_parser( "M10 20 H30,40-50", "", &vec![ moveto(10.0, 20.0), lineto(30.0, 20.0), lineto(40.0, 20.0), lineto(-50.0, 20.0), ], None, ); test_parser( "m10 20 h30,40-50", "", &vec![ moveto(10.0, 20.0), lineto(40.0, 20.0), lineto(80.0, 20.0), lineto(30.0, 20.0), ], None, ); } #[test] fn handles_vertical_lineto() { test_parser( "M10 20 V30", "", &[moveto(10.0, 20.0), lineto(10.0, 30.0)], None, ); test_parser( "M10 20 V30 40", "", &[moveto(10.0, 20.0), lineto(10.0, 30.0), lineto(10.0, 40.0)], None, ); test_parser( "M10 20 V30,40-50", "", &vec![ moveto(10.0, 20.0), lineto(10.0, 30.0), lineto(10.0, 40.0), lineto(10.0, -50.0), ], None, ); test_parser( "m10 20 v30,40-50", "", &vec![ moveto(10.0, 20.0), lineto(10.0, 50.0), lineto(10.0, 90.0), lineto(10.0, 40.0), ], None, ); } #[test] fn handles_curveto() { test_parser( "M10 20 C 30,40 50 60-70,80", "", &[moveto(10.0, 20.0), curveto(30.0, 40.0, 50.0, 60.0, -70.0, 80.0)], None, ); test_parser( "M10 20 C 30,40 50 60-70,80,90 100,110 120,130,140", "", &[moveto(10.0, 20.0), curveto(30.0, 40.0, 50.0, 60.0, -70.0, 80.0), curveto(90.0, 100.0, 110.0, 120.0, 130.0, 140.0)], None, ); test_parser( "m10 20 c 30,40 50 60-70,80,90 100,110 120,130,140", "", &[moveto(10.0, 20.0), curveto(40.0, 60.0, 60.0, 80.0, -60.0, 100.0), curveto(30.0, 200.0, 50.0, 220.0, 70.0, 240.0)], None, ); test_parser( "m10 20 c 30,40 50 60-70,80,90 100,110 120,130,140", "", &[moveto(10.0, 20.0), curveto(40.0, 60.0, 60.0, 80.0, -60.0, 100.0), curveto(30.0, 200.0, 50.0, 220.0, 70.0, 240.0)], None, ); } #[test] fn handles_smooth_curveto() { test_parser( "M10 20 S 30,40-50,60", "", &[moveto(10.0, 20.0), curveto(10.0, 20.0, 30.0, 40.0, -50.0, 60.0)], None, ); test_parser( "M10 20 S 30,40 50 60-70,80,90 100", "", &[moveto(10.0, 20.0), curveto(10.0, 20.0, 30.0, 40.0, 50.0, 60.0), curveto(70.0, 80.0, -70.0, 80.0, 90.0, 100.0)], None, ); test_parser( "m10 20 s 30,40 50 60-70,80,90 100", "", &[moveto(10.0, 20.0), curveto(10.0, 20.0, 40.0, 60.0, 60.0, 80.0), curveto(80.0, 100.0, -10.0, 160.0, 150.0, 180.0)], None, ); } #[test] fn handles_quadratic_curveto() { test_parser( "M10 20 Q30 40 50 60", "", &[moveto(10.0, 20.0), curveto( 70.0 / 3.0, 100.0 / 3.0, 110.0 / 3.0, 140.0 / 3.0, 50.0, 60.0, )], None, ); test_parser( "M10 20 Q30 40 50 60,70,80-90 100", "", &[moveto(10.0, 20.0), curveto( 70.0 / 3.0, 100.0 / 3.0, 110.0 / 3.0, 140.0 / 3.0, 50.0, 60.0, ), curveto( 190.0 / 3.0, 220.0 / 3.0, 50.0 / 3.0, 260.0 / 3.0, -90.0, 100.0, )], None, ); test_parser( "m10 20 q 30,40 50 60-70,80 90 100", "", &[moveto(10.0, 20.0), curveto( 90.0 / 3.0, 140.0 / 3.0, 140.0 / 3.0, 200.0 / 3.0, 60.0, 80.0, ), curveto( 40.0 / 3.0, 400.0 / 3.0, 130.0 / 3.0, 500.0 / 3.0, 150.0, 180.0, )], None, ); } #[test] fn handles_smooth_quadratic_curveto() { test_parser( "M10 20 T30 40", "", &[moveto(10.0, 20.0), curveto(10.0, 20.0, 50.0 / 3.0, 80.0 / 3.0, 30.0, 40.0)], None, ); test_parser( "M10 20 Q30 40 50 60 T70 80", "", &[moveto(10.0, 20.0), curveto( 70.0 / 3.0, 100.0 / 3.0, 110.0 / 3.0, 140.0 / 3.0, 50.0, 60.0, ), curveto(190.0 / 3.0, 220.0 / 3.0, 70.0, 80.0, 70.0, 80.0)], None, ); test_parser( "m10 20 q 30,40 50 60t-70,80", "", &[moveto(10.0, 20.0), curveto( 90.0 / 3.0, 140.0 / 3.0, 140.0 / 3.0, 200.0 / 3.0, 60.0, 80.0, ), curveto(220.0 / 3.0, 280.0 / 3.0, 50.0, 120.0, -10.0, 160.0)], None, ); } #[test] fn handles_elliptical_arc() { // no space required between arc flags test_parser("M 1 2 A 1 2 3 00 6 7", "", &[moveto(1.0, 2.0), arc(1.0, 2.0, 3.0, false, false, 1.0, 2.0, 6.0, 7.0)], None); // or after... test_parser("M 1 2 A 1 2 3 016 7", "", &[moveto(1.0, 2.0), arc(1.0, 2.0, 3.0, false, true, 1.0, 2.0, 6.0, 7.0)], None); // commas and whitespace are optionally allowed test_parser("M 1 2 A 1 2 3 10,6 7", "", &[moveto(1.0, 2.0), arc(1.0, 2.0, 3.0, true, false, 1.0, 2.0, 6.0, 7.0)], None); test_parser("M 1 2 A 1 2 3 1,16, 7", "", &[moveto(1.0, 2.0), arc(1.0, 2.0, 3.0, true, true, 1.0, 2.0, 6.0, 7.0)], None); test_parser("M 1 2 A 1 2 3 1,1 6 7", "", &[moveto(1.0, 2.0), arc(1.0, 2.0, 3.0, true, true, 1.0, 2.0, 6.0, 7.0)], None); test_parser("M 1 2 A 1 2 3 1 1 6 7", "", &[moveto(1.0, 2.0), arc(1.0, 2.0, 3.0, true, true, 1.0, 2.0, 6.0, 7.0)], None); test_parser("M 1 2 A 1 2 3 1 16 7", "", &[moveto(1.0, 2.0), arc(1.0, 2.0, 3.0, true, true, 1.0, 2.0, 6.0, 7.0)], None); } #[test] fn handles_close_path() { test_parser("M10 20 Z", "", &[moveto(10.0, 20.0), closepath()], None); test_parser( "m10 20 30 40 m 50 60 70 80 90 100z", "", &vec![ moveto(10.0, 20.0), lineto(40.0, 60.0), moveto(90.0, 120.0), lineto(160.0, 200.0), lineto(250.0, 300.0), closepath(), ], None, ); } #[test] fn first_command_must_be_moveto() { test_parser( " L10 20", " ^", // FIXME: why is this not at position 2? &[], Some(ErrorKind::UnexpectedCommand(b'L')), ); } #[test] fn moveto_args() { test_parser( "M", " ^", &[], Some(ErrorKind::UnexpectedEof), ); test_parser( "M,", " ^", &[], Some(ErrorKind::UnexpectedToken(Comma)), ); test_parser( "M10", " ^", &[], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10,", " ^", &[], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10x", " ^", &[], Some(ErrorKind::UnexpectedToken(Command(b'x'))), ); test_parser( "M10,x", " ^", &[], Some(ErrorKind::UnexpectedToken(Command(b'x'))), ); } #[test] fn moveto_implicit_lineto_args() { test_parser( "M10-20,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20-30", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20-30 x", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedToken(Command(b'x'))), ); } #[test] fn closepath_no_args() { test_parser( "M10-20z10", " ^", &[moveto(10.0, -20.0), closepath()], Some(ErrorKind::UnexpectedToken(Number(10.0))), ); test_parser( "M10-20z,", " ^", &[moveto(10.0, -20.0), closepath()], Some(ErrorKind::UnexpectedToken(Comma)), ); } #[test] fn lineto_args() { test_parser( "M10-20L10", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M 10,10 L 20,20,30", " ^", &[moveto(10.0, 10.0), lineto(20.0, 20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M 10,10 L 20,20,", " ^", &[moveto(10.0, 10.0), lineto(20.0, 20.0)], Some(ErrorKind::UnexpectedEof), ); } #[test] fn horizontal_lineto_args() { test_parser( "M10-20H", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20H,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedToken(Comma)), ); test_parser( "M10-20H30,", " ^", &[moveto(10.0, -20.0), lineto(30.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); } #[test] fn vertical_lineto_args() { test_parser( "M10-20v", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20v,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedToken(Comma)), ); test_parser( "M10-20v30,", " ^", &[moveto(10.0, -20.0), lineto(10.0, 10.0)], Some(ErrorKind::UnexpectedEof), ); } #[test] fn curveto_args() { test_parser( "M10-20C1", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20C1,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20C1 2", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20C1,2,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20C1 2 3", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20C1,2,3", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20C1,2,3,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20C1 2 3 4", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20C1,2,3,4", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20C1,2,3,4,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20C1 2 3 4 5", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20C1,2,3,4,5", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20C1,2,3,4,5,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20C1,2,3,4,5,6,", " ^", &[moveto(10.0, -20.0), curveto(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)], Some(ErrorKind::UnexpectedEof), ); } #[test] fn smooth_curveto_args() { test_parser( "M10-20S1", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20S1,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20S1 2", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20S1,2,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20S1 2 3", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20S1,2,3", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20S1,2,3,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20S1,2,3,4,", " ^", &[moveto(10.0, -20.0), curveto(10.0, -20.0, 1.0, 2.0, 3.0, 4.0)], Some(ErrorKind::UnexpectedEof), ); } #[test] fn quadratic_bezier_curveto_args() { test_parser( "M10-20Q1", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20Q1,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20Q1 2", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20Q1,2,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20Q1 2 3", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20Q1,2,3", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20Q1,2,3,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10 20 Q30 40 50 60,", " ^", &[moveto(10.0, 20.0), curveto( 70.0 / 3.0, 100.0 / 3.0, 110.0 / 3.0, 140.0 / 3.0, 50.0, 60.0, )], Some(ErrorKind::UnexpectedEof), ); } #[test] fn smooth_quadratic_bezier_curveto_args() { test_parser( "M10-20T1", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20T1,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10 20 T30 40,", " ^", &[moveto(10.0, 20.0), curveto(10.0, 20.0, 50.0 / 3.0, 80.0 / 3.0, 30.0, 40.0)], Some(ErrorKind::UnexpectedEof), ); } #[test] fn elliptical_arc_args() { test_parser( "M10-20A1", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20A1,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20A1 2", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20A1 2,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20A1 2 3", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20A1 2 3,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20A1 2 3 4", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::LexError(LexError::UnexpectedByte(b'4'))), ); test_parser( "M10-20A1 2 3 1", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20A1 2 3,1,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20A1 2 3 1 5", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::LexError(LexError::UnexpectedByte(b'5'))), ); test_parser( "M10-20A1 2 3 1 1", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20A1 2 3,1,1,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20A1 2 3 1 1 6", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); test_parser( "M10-20A1 2 3,1,1,6,", " ^", &[moveto(10.0, -20.0)], Some(ErrorKind::UnexpectedEof), ); // no non 0|1 chars allowed for flags test_parser("M 1 2 A 1 2 3 1.0 0.0 6 7", " ^", &[moveto(1.0, 2.0)], Some(ErrorKind::UnexpectedToken(Number(0.0)))); test_parser("M10-20A1 2 3,1,1,6,7,", " ^", &[moveto(10.0, -20.0), arc(1.0, 2.0, 3.0, true, true, 10.0, -20.0, 6.0, 7.0)], Some(ErrorKind::UnexpectedEof)); } #[test] fn bugs() { // https://gitlab.gnome.org/GNOME/librsvg/issues/345 test_parser( "M.. 1,0 0,100000", " ^", // FIXME: we have to report position of error in lexer errors to make this right &[], Some(ErrorKind::LexError(LexError::UnexpectedByte(b'.'))), ); } } ������������������������������������������������������������������������������librsvg-2.59.0/src/pattern.rs�����������������������������������������������������������������������0000644�0000000�0000000�00000043067�10461020230�0014215�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! The `pattern` element. use markup5ever::{expanded_name, local_name, namespace_url, ns}; use crate::coord_units; use crate::coord_units::CoordUnits; use crate::aspect_ratio::*; use crate::document::{AcquiredNode, AcquiredNodes, NodeId, NodeStack}; use crate::drawing_ctx::Viewport; use crate::element::{set_attribute, ElementData, ElementTrait}; use crate::error::*; use crate::href::{is_href, set_href}; use crate::length::*; use crate::node::{Node, NodeBorrow, WeakNode}; use crate::parsers::ParseValue; use crate::rect::Rect; use crate::rsvg_log; use crate::session::Session; use crate::transform::{Transform, TransformAttribute}; use crate::unit_interval::UnitInterval; use crate::viewbox::*; use crate::xml::Attributes; coord_units!(PatternUnits, CoordUnits::ObjectBoundingBox); coord_units!(PatternContentUnits, CoordUnits::UserSpaceOnUse); #[derive(Clone, Default)] struct Common { units: Option, content_units: Option, // This Option> is a bit strange. We want a field // with value None to mean, "this field isn't resolved yet". However, // the vbox can very well be *not* specified in the SVG file. // In that case, the fully resolved pattern will have a .vbox=Some(None) value. vbox: Option>, preserve_aspect_ratio: Option, transform: Option, x: Option>, y: Option>, width: Option>, height: Option>, } /// State used during the pattern resolution process /// /// This is the current node's pattern information, plus the fallback /// that should be used in case that information is not complete for a /// resolved pattern yet. struct Unresolved { pattern: UnresolvedPattern, fallback: Option, } /// Keeps track of which Pattern provided a non-empty set of children during pattern resolution #[derive(Clone)] enum UnresolvedChildren { /// Points back to the original Pattern if it had no usable children Unresolved, /// Points back to the original Pattern, as no pattern in the /// chain of fallbacks had usable children. This only gets returned /// by resolve_from_defaults(). ResolvedEmpty, /// Points back to the Pattern that had usable children. WithChildren(WeakNode), } /// Keeps track of which Pattern provided a non-empty set of children during pattern resolution #[derive(Clone)] enum Children { Empty, /// Points back to the Pattern that had usable children WithChildren(WeakNode), } /// Main structure used during pattern resolution. For unresolved /// patterns, we store all fields as `Option` - if `None`, it means /// that the field is not specified; if `Some(T)`, it means that the /// field was specified. struct UnresolvedPattern { common: Common, // Point back to our corresponding node, or to the fallback node which has children. // If the value is None, it means we are fully resolved and didn't find any children // among the fallbacks. children: UnresolvedChildren, } #[derive(Clone)] pub struct ResolvedPattern { units: PatternUnits, content_units: PatternContentUnits, vbox: Option, preserve_aspect_ratio: AspectRatio, transform: TransformAttribute, x: Length, y: Length, width: ULength, height: ULength, opacity: UnitInterval, // Link to the node whose children are the pattern's resolved children. children: Children, } /// Pattern normalized to user-space units. pub struct UserSpacePattern { pub width: f64, pub height: f64, pub transform: Transform, pub coord_transform: Transform, pub content_transform: Transform, pub opacity: UnitInterval, // This one is private so the caller has to go through fn acquire_pattern_node() node_with_children: Node, } #[derive(Default)] pub struct Pattern { common: Common, fallback: Option, } impl ElementTrait for Pattern { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "patternUnits") => { set_attribute(&mut self.common.units, attr.parse(value), session) } expanded_name!("", "patternContentUnits") => { set_attribute(&mut self.common.content_units, attr.parse(value), session); } expanded_name!("", "viewBox") => { set_attribute(&mut self.common.vbox, attr.parse(value), session) } expanded_name!("", "preserveAspectRatio") => { set_attribute( &mut self.common.preserve_aspect_ratio, attr.parse(value), session, ); } expanded_name!("", "patternTransform") => { set_attribute(&mut self.common.transform, attr.parse(value), session); } ref a if is_href(a) => { let mut href = None; set_attribute( &mut href, NodeId::parse(value).map(Some).attribute(attr.clone()), session, ); set_href(a, &mut self.fallback, href); } expanded_name!("", "x") => { set_attribute(&mut self.common.x, attr.parse(value), session) } expanded_name!("", "y") => { set_attribute(&mut self.common.y, attr.parse(value), session) } expanded_name!("", "width") => { set_attribute(&mut self.common.width, attr.parse(value), session) } expanded_name!("", "height") => { set_attribute(&mut self.common.height, attr.parse(value), session) } _ => (), } } } } impl UnresolvedPattern { fn into_resolved(self, opacity: UnitInterval) -> ResolvedPattern { assert!(self.is_resolved()); ResolvedPattern { units: self.common.units.unwrap(), content_units: self.common.content_units.unwrap(), vbox: self.common.vbox.unwrap(), preserve_aspect_ratio: self.common.preserve_aspect_ratio.unwrap(), transform: self.common.transform.unwrap(), x: self.common.x.unwrap(), y: self.common.y.unwrap(), width: self.common.width.unwrap(), height: self.common.height.unwrap(), opacity, children: self.children.to_resolved(), } } fn is_resolved(&self) -> bool { self.common.units.is_some() && self.common.content_units.is_some() && self.common.vbox.is_some() && self.common.preserve_aspect_ratio.is_some() && self.common.transform.is_some() && self.common.x.is_some() && self.common.y.is_some() && self.common.width.is_some() && self.common.height.is_some() && self.children.is_resolved() } fn resolve_from_fallback(&self, fallback: &UnresolvedPattern) -> UnresolvedPattern { let units = self.common.units.or(fallback.common.units); let content_units = self.common.content_units.or(fallback.common.content_units); let vbox = self.common.vbox.or(fallback.common.vbox); let preserve_aspect_ratio = self .common .preserve_aspect_ratio .or(fallback.common.preserve_aspect_ratio); let transform = self.common.transform.or(fallback.common.transform); let x = self.common.x.or(fallback.common.x); let y = self.common.y.or(fallback.common.y); let width = self.common.width.or(fallback.common.width); let height = self.common.height.or(fallback.common.height); let children = self.children.resolve_from_fallback(&fallback.children); UnresolvedPattern { common: Common { units, content_units, vbox, preserve_aspect_ratio, transform, x, y, width, height, }, children, } } fn resolve_from_defaults(&self) -> UnresolvedPattern { let units = self.common.units.or_else(|| Some(PatternUnits::default())); let content_units = self .common .content_units .or_else(|| Some(PatternContentUnits::default())); let vbox = self.common.vbox.or(Some(None)); let preserve_aspect_ratio = self .common .preserve_aspect_ratio .or_else(|| Some(AspectRatio::default())); let transform = self .common .transform .or_else(|| Some(TransformAttribute::default())); let x = self.common.x.or_else(|| Some(Default::default())); let y = self.common.y.or_else(|| Some(Default::default())); let width = self.common.width.or_else(|| Some(Default::default())); let height = self.common.height.or_else(|| Some(Default::default())); let children = self.children.resolve_from_defaults(); UnresolvedPattern { common: Common { units, content_units, vbox, preserve_aspect_ratio, transform, x, y, width, height, }, children, } } } impl UnresolvedChildren { fn from_node(node: &Node) -> UnresolvedChildren { let weak = node.downgrade(); if node.children().any(|child| child.is_element()) { UnresolvedChildren::WithChildren(weak) } else { UnresolvedChildren::Unresolved } } fn is_resolved(&self) -> bool { !matches!(*self, UnresolvedChildren::Unresolved) } fn resolve_from_fallback(&self, fallback: &UnresolvedChildren) -> UnresolvedChildren { use UnresolvedChildren::*; match (self, fallback) { (&Unresolved, &Unresolved) => Unresolved, (WithChildren(wc), _) => WithChildren(wc.clone()), (_, WithChildren(wc)) => WithChildren(wc.clone()), (_, _) => unreachable!(), } } fn resolve_from_defaults(&self) -> UnresolvedChildren { use UnresolvedChildren::*; match *self { Unresolved => ResolvedEmpty, _ => (*self).clone(), } } fn to_resolved(&self) -> Children { use UnresolvedChildren::*; assert!(self.is_resolved()); match *self { ResolvedEmpty => Children::Empty, WithChildren(ref wc) => Children::WithChildren(wc.clone()), _ => unreachable!(), } } } fn nonempty_rect(bbox: &Option) -> Option { match *bbox { None => None, Some(r) if r.is_empty() => None, Some(r) => Some(r), } } impl ResolvedPattern { fn node_with_children(&self) -> Option { match self.children { // This means we didn't find any children among the fallbacks, // so there is nothing to render. Children::Empty => None, Children::WithChildren(ref wc) => Some(wc.upgrade().unwrap()), } } fn get_rect(&self, params: &NormalizeParams) -> Rect { let x = self.x.to_user(params); let y = self.y.to_user(params); let w = self.width.to_user(params); let h = self.height.to_user(params); Rect::new(x, y, x + w, y + h) } pub fn to_user_space( &self, object_bbox: &Option, viewport: &Viewport, values: &NormalizeValues, ) -> Option { let node_with_children = self.node_with_children()?; let viewport = viewport.with_units(self.units.0); let params = NormalizeParams::from_values(values, &viewport); let rect = self.get_rect(¶ms); // Create the pattern coordinate system let (width, height, coord_transform) = match self.units { PatternUnits(CoordUnits::ObjectBoundingBox) => { let bbrect = nonempty_rect(object_bbox)?; ( rect.width() * bbrect.width(), rect.height() * bbrect.height(), Transform::new_translate( bbrect.x0 + rect.x0 * bbrect.width(), bbrect.y0 + rect.y0 * bbrect.height(), ), ) } PatternUnits(CoordUnits::UserSpaceOnUse) => ( rect.width(), rect.height(), Transform::new_translate(rect.x0, rect.y0), ), }; let pattern_transform = self.transform.to_transform(); let coord_transform = coord_transform.post_transform(&pattern_transform); // Create the pattern contents coordinate system let content_transform = if let Some(vbox) = self.vbox { // If there is a vbox, use that let r = self .preserve_aspect_ratio .compute(&vbox, &Rect::from_size(width, height)); let sw = r.width() / vbox.width(); let sh = r.height() / vbox.height(); let x = r.x0 - vbox.x0 * sw; let y = r.y0 - vbox.y0 * sh; Transform::new_scale(sw, sh).pre_translate(x, y) } else { match self.content_units { PatternContentUnits(CoordUnits::ObjectBoundingBox) => { let bbrect = nonempty_rect(object_bbox)?; Transform::new_scale(bbrect.width(), bbrect.height()) } PatternContentUnits(CoordUnits::UserSpaceOnUse) => Transform::identity(), } }; Some(UserSpacePattern { width, height, transform: pattern_transform, coord_transform, content_transform, opacity: self.opacity, node_with_children, }) } } impl UserSpacePattern { /// Gets the `` node that contains the children to be drawn for the pattern's contents. /// /// This has to go through [AcquiredNodes] to catch circular references among /// patterns and their children. pub fn acquire_pattern_node( &self, acquired_nodes: &mut AcquiredNodes<'_>, ) -> Result { acquired_nodes.acquire_ref(&self.node_with_children) } } impl Pattern { fn get_unresolved(&self, node: &Node) -> Unresolved { let pattern = UnresolvedPattern { common: self.common.clone(), children: UnresolvedChildren::from_node(node), }; Unresolved { pattern, fallback: self.fallback.clone(), } } pub fn resolve( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, opacity: UnitInterval, session: &Session, ) -> Result { let Unresolved { mut pattern, mut fallback, } = self.get_unresolved(node); let mut stack = NodeStack::new(); while !pattern.is_resolved() { if let Some(ref node_id) = fallback { match acquired_nodes.acquire(node_id) { Ok(acquired) => { let acquired_node = acquired.get(); if stack.contains(acquired_node) { return Err(AcquireError::CircularReference(acquired_node.clone())); } match *acquired_node.borrow_element_data() { ElementData::Pattern(ref p) => { let unresolved = p.get_unresolved(acquired_node); pattern = pattern.resolve_from_fallback(&unresolved.pattern); fallback = unresolved.fallback; stack.push(acquired_node); } _ => return Err(AcquireError::InvalidLinkType(node_id.clone())), } } Err(AcquireError::MaxReferencesExceeded) => { return Err(AcquireError::MaxReferencesExceeded) } Err(e) => { rsvg_log!(session, "Stopping pattern resolution: {}", e); pattern = pattern.resolve_from_defaults(); break; } } } else { pattern = pattern.resolve_from_defaults(); break; } } Ok(pattern.into_resolved(opacity)) } } #[cfg(test)] mod tests { use super::*; use markup5ever::{namespace_url, ns, QualName}; use crate::borrow_element_as; use crate::node::NodeData; #[test] fn pattern_resolved_from_defaults_is_really_resolved() { let node = Node::new(NodeData::new_element( &Session::default(), &QualName::new(None, ns!(svg), local_name!("pattern")), Attributes::new(), )); let unresolved = borrow_element_as!(node, Pattern).get_unresolved(&node); let pattern = unresolved.pattern.resolve_from_defaults(); assert!(pattern.is_resolved()); } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/properties.rs��������������������������������������������������������������������0000644�0000000�0000000�00000131261�10461020230�0014726�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! CSS properties, specified values, computed values. //! //! To implement support for a CSS property, do the following: //! //! * Create a type that will hold the property's values. Please do this in the file //! `property_defs.rs`; you should cut-and-paste from the existing property definitions or //! read the documentation of the [`make_property`] macro. You should read the //! documentation for the [`property_defs`][crate::property_defs] module to see all that //! is involved in creating a type for a property. //! //! * Modify the call to the `make_properties` macro in this module to include the new //! property's name. //! //! * Modify the rest of librsvg wherever the computed value of the property needs to be used. //! This is available in methods that take an argument of type [`ComputedValues`]. use cssparser::{ self, BasicParseErrorKind, ParseErrorKind, Parser, ParserInput, RuleBodyParser, ToCss, }; use markup5ever::{ expanded_name, local_name, namespace_url, ns, ExpandedName, LocalName, QualName, }; use std::collections::HashSet; #[cfg(doc)] use crate::make_property; use crate::css::{DeclParser, Declaration, Origin, RuleBodyItem}; use crate::error::*; use crate::parsers::{Parse, ParseValue}; use crate::property_macros::Property; use crate::rsvg_log; use crate::session::Session; use crate::transform::{Transform, TransformAttribute, TransformProperty}; use crate::xml::Attributes; // Re-export the actual properties so they are easy to find from a single place `properties::*`. pub use crate::font_props::*; pub use crate::property_defs::*; /// Representation of a single CSS property value. /// /// `Unspecified` is the `Default`; it means that the corresponding property is not present. /// /// `Inherit` means that the property is explicitly set to inherit /// from the parent element. This is useful for properties which the /// SVG or CSS specs mandate that should not be inherited by default. /// /// `Specified` is a value given by the SVG or CSS stylesheet. This will later be /// resolved into part of a `ComputedValues` struct. #[derive(Clone)] pub enum SpecifiedValue where T: Property + Clone + Default, { Unspecified, Inherit, Specified(T), } impl SpecifiedValue where T: Property + Clone + Default, { pub fn compute(&self, src: &T, src_values: &ComputedValues) -> T { let value: T = match *self { SpecifiedValue::Unspecified => { if ::inherits_automatically() { src.clone() } else { Default::default() } } SpecifiedValue::Inherit => src.clone(), SpecifiedValue::Specified(ref v) => v.clone(), }; value.compute(src_values) } } /// Whether a property also has a presentation attribute. /// /// #[derive(PartialEq)] enum PresentationAttr { No, Yes, } /// How to parse a value, whether it comes from a property or from a presentation attribute #[derive(PartialEq)] pub enum ParseAs { Property, PresentationAttr, } impl PropertyId { fn as_u8(&self) -> u8 { *self as u8 } fn as_usize(&self) -> usize { *self as usize } } /// Holds the specified values for the CSS properties of an element. #[derive(Clone)] pub struct SpecifiedValues { indices: [u8; PropertyId::UnsetProperty as usize], props: Vec, transform: Option, } impl Default for SpecifiedValues { fn default() -> Self { SpecifiedValues { // this many elements, with the same value indices: [PropertyId::UnsetProperty.as_u8(); PropertyId::UnsetProperty as usize], props: Vec::new(), transform: None, } } } impl ComputedValues { // TODO for madds: this function will go away, to be replaced by the one generated // automatically by the macros. pub fn transform(&self) -> Transform { self.transform } pub fn is_overflow(&self) -> bool { matches!(self.overflow(), Overflow::Auto | Overflow::Visible) } /// Whether we should draw the element or skip both space allocation /// and drawing. /// pub fn is_displayed(&self) -> bool { self.display() != Display::None } /// Whether we should draw the element or allocate its space but /// skip drawing. /// pub fn is_visible(&self) -> bool { self.visibility() == Visibility::Visible } } /// Macro to generate all the machinery for properties. /// /// This generates the following: /// /// * `PropertyId`, an fieldless enum with simple values to identify all the properties. /// * `ParsedProperty`, a variant enum for all the specified property values. /// * `ComputedValue`, a variant enum for all the computed values. /// * `parse_value`, the main function to parse a property or attribute value from user input. /// /// There is a lot of repetitive code, for example, because sometimes /// we need to operate on `PropertyId::Foo`, `ParsedProperty::Foo` and /// `ComputedValue::Foo` together. This is why all this is done with a macro. /// /// See the only invocation of this macro to see how it is used; it is just /// a declarative list of property names. /// /// **NOTE:** If you get a compiler error similar to this: /// /// ```text /// 362 | "mix-blend-mode" => mix_blend_mode : MixBlendMode, /// | ^^^^^^^^^^^^^^^^ no rules expected this token in macro call /// ``` /// /// Then it may be that you put the name inside the `longhands` block, when it should be /// inside the `longhands_not_supported_by_markup5ever` block. This is because the /// [`markup5ever`] crate does not have predefined names for every single property out /// there; just the common ones. /// /// [`markup5ever`]: https://docs.rs/markup5ever macro_rules! make_properties { { shorthands: { $($short_str:tt => ( $short_presentation_attr:expr, $short_field:ident: $short_name:ident ),)* } longhands: { $($long_str:tt => ( $long_presentation_attr:expr, $long_field:ident: $long_name:ident ),)+ } // These are for when expanded_name!("" "foo") is not defined yet // in markup5ever. We create an ExpandedName by hand in that case. longhands_not_supported_by_markup5ever: { $($long_m5e_str:tt => ($long_m5e_presentation_attr:expr, $long_m5e_field:ident: $long_m5e_name:ident ),)+ } non_properties: { $($nonprop_field:ident: $nonprop_name:ident,)+ } }=> { /// Used to match `ParsedProperty` to their discriminant /// /// The `PropertyId::UnsetProperty` can be used as a sentinel value, as /// it does not match any `ParsedProperty` discriminant; it is really the /// number of valid values in this enum. #[repr(u8)] #[derive(Copy, Clone, PartialEq)] enum PropertyId { $($short_name,)+ $($long_name,)+ $($long_m5e_name,)+ $($nonprop_name,)+ UnsetProperty, } impl PropertyId { fn is_shorthand(self) -> bool { match self { $(PropertyId::$short_name => true,)+ _ => false, } } } /// Embodies "which property is this" plus the property's value #[derive(Clone)] pub enum ParsedProperty { // we put all the properties here; these are for SpecifiedValues $($short_name(SpecifiedValue<$short_name>),)+ $($long_name(SpecifiedValue<$long_name>),)+ $($long_m5e_name(SpecifiedValue<$long_m5e_name>),)+ $($nonprop_name(SpecifiedValue<$nonprop_name>),)+ } enum ComputedValue { $( $long_name($long_name), )+ $( $long_m5e_name($long_m5e_name), )+ $( $nonprop_name($nonprop_name), )+ } /// Holds the computed values for the CSS properties of an element. #[derive(Debug, Default, Clone)] pub struct ComputedValues { $( $long_field: $long_name, )+ $( $long_m5e_field: $long_m5e_name, )+ $( $nonprop_field: $nonprop_name, )+ transform: Transform, } impl ParsedProperty { fn get_property_id(&self) -> PropertyId { match *self { $(ParsedProperty::$long_name(_) => PropertyId::$long_name,)+ $(ParsedProperty::$long_m5e_name(_) => PropertyId::$long_m5e_name,)+ $(ParsedProperty::$short_name(_) => PropertyId::$short_name,)+ $(ParsedProperty::$nonprop_name(_) => PropertyId::$nonprop_name,)+ } } fn unspecified(id: PropertyId) -> Self { use SpecifiedValue::Unspecified; match id { $(PropertyId::$long_name => ParsedProperty::$long_name(Unspecified),)+ $(PropertyId::$long_m5e_name => ParsedProperty::$long_m5e_name(Unspecified),)+ $(PropertyId::$short_name => ParsedProperty::$short_name(Unspecified),)+ $(PropertyId::$nonprop_name => ParsedProperty::$nonprop_name(Unspecified),)+ PropertyId::UnsetProperty => unreachable!(), } } } impl ComputedValues { $( pub fn $long_field(&self) -> $long_name { if let ComputedValue::$long_name(v) = self.get_value(PropertyId::$long_name) { v } else { unreachable!(); } } )+ $( pub fn $long_m5e_field(&self) -> $long_m5e_name { if let ComputedValue::$long_m5e_name(v) = self.get_value(PropertyId::$long_m5e_name) { v } else { unreachable!(); } } )+ $( pub fn $nonprop_field(&self) -> $nonprop_name { if let ComputedValue::$nonprop_name(v) = self.get_value(PropertyId::$nonprop_name) { v } else { unreachable!(); } } )+ fn set_value(&mut self, computed: ComputedValue) { match computed { $(ComputedValue::$long_name(v) => self.$long_field = v,)+ $(ComputedValue::$long_m5e_name(v) => self.$long_m5e_field = v,)+ $(ComputedValue::$nonprop_name(v) => self.$nonprop_field = v,)+ } } fn get_value(&self, id: PropertyId) -> ComputedValue { assert!(!id.is_shorthand()); match id { $( PropertyId::$long_name => ComputedValue::$long_name(self.$long_field.clone()), )+ $( PropertyId::$long_m5e_name => ComputedValue::$long_m5e_name(self.$long_m5e_field.clone()), )+ $( PropertyId::$nonprop_name => ComputedValue::$nonprop_name(self.$nonprop_field.clone()), )+ _ => unreachable!(), } } } /// Parses a value from either a style property or from an element's attribute. pub fn parse_value<'i>( prop_name: &QualName, input: &mut Parser<'i, '_>, parse_as: ParseAs, ) -> Result> { match prop_name.expanded() { $( expanded_name!("", $long_str) if !(parse_as == ParseAs::PresentationAttr && $long_presentation_attr == PresentationAttr::No) => { Ok(ParsedProperty::$long_name(parse_input(input)?)) } )+ $( e if e == ExpandedName { ns: &ns!(), local: &LocalName::from($long_m5e_str), } && !(parse_as == ParseAs::PresentationAttr && $long_m5e_presentation_attr == PresentationAttr::No) => { Ok(ParsedProperty::$long_m5e_name(parse_input(input)?)) } )+ $( expanded_name!("", $short_str) if parse_as == ParseAs::Property => { // No shorthand has a presentation attribute. assert!($short_presentation_attr == PresentationAttr::No); Ok(ParsedProperty::$short_name(parse_input(input)?)) } )+ _ => { let loc = input.current_source_location(); Err(loc.new_custom_error(ValueErrorKind::UnknownProperty)) } } } }; } #[rustfmt::skip] make_properties! { shorthands: { // No shorthand has a presentation attribute. "font" => (PresentationAttr::No, font : Font), "marker" => (PresentationAttr::No, marker : Marker), } // longhands that are presentation attributes right now, but need to be turned into properties: // "d" - applies only to path longhands: { // "alignment-baseline" => (PresentationAttr::Yes, unimplemented), "baseline-shift" => (PresentationAttr::Yes, baseline_shift : BaselineShift), "clip-path" => (PresentationAttr::Yes, clip_path : ClipPath), "clip-rule" => (PresentationAttr::Yes, clip_rule : ClipRule), "color" => (PresentationAttr::Yes, color : Color), // "color-interpolation" => (PresentationAttr::Yes, unimplemented), "color-interpolation-filters" => (PresentationAttr::Yes, color_interpolation_filters : ColorInterpolationFilters), // "cursor" => (PresentationAttr::Yes, unimplemented), "cx" => (PresentationAttr::Yes, cx: CX), "cy" => (PresentationAttr::Yes, cy: CY), "direction" => (PresentationAttr::Yes, direction : Direction), "display" => (PresentationAttr::Yes, display : Display), // "dominant-baseline" => (PresentationAttr::Yes, unimplemented), "enable-background" => (PresentationAttr::Yes, enable_background : EnableBackground), // "applies to any element except animation elements" // https://www.w3.org/TR/SVG2/styling.html#PresentationAttributes "fill" => (PresentationAttr::Yes, fill : Fill), "fill-opacity" => (PresentationAttr::Yes, fill_opacity : FillOpacity), "fill-rule" => (PresentationAttr::Yes, fill_rule : FillRule), "filter" => (PresentationAttr::Yes, filter : Filter), "flood-color" => (PresentationAttr::Yes, flood_color : FloodColor), "flood-opacity" => (PresentationAttr::Yes, flood_opacity : FloodOpacity), "font-family" => (PresentationAttr::Yes, font_family : FontFamily), "font-size" => (PresentationAttr::Yes, font_size : FontSize), // "font-size-adjust" => (PresentationAttr::Yes, unimplemented), "font-stretch" => (PresentationAttr::Yes, font_stretch : FontStretch), "font-style" => (PresentationAttr::Yes, font_style : FontStyle), "font-variant" => (PresentationAttr::Yes, font_variant : FontVariant), "font-weight" => (PresentationAttr::Yes, font_weight : FontWeight), // "glyph-orientation-horizontal" - obsolete, removed from SVG2 // "glyph-orientation-vertical" - obsolete, now shorthand - // https://svgwg.org/svg2-draft/text.html#GlyphOrientationVerticalProperty // https://www.w3.org/TR/css-writing-modes-3/#propdef-glyph-orientation-vertical // // Note that even though CSS Writing Modes 3 turned glyph-orientation-vertical // into a shorthand, SVG1.1 still makes it available as a presentation attribute. // So, we put the property here, not in the shorthands, and deal with it as a // special case in the text handling code. "glyph-orientation-vertical" => (PresentationAttr::Yes, glyph_orientation_vertical : GlyphOrientationVertical), "height" => (PresentationAttr::Yes, height: Height), "image-rendering" => (PresentationAttr::Yes, image_rendering : ImageRendering), "letter-spacing" => (PresentationAttr::Yes, letter_spacing : LetterSpacing), "lighting-color" => (PresentationAttr::Yes, lighting_color : LightingColor), "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), "opacity" => (PresentationAttr::Yes, opacity : Opacity), "overflow" => (PresentationAttr::Yes, overflow : Overflow), // "pointer-events" => (PresentationAttr::Yes, unimplemented), "r" => (PresentationAttr::Yes, r: R), "rx" => (PresentationAttr::Yes, rx: RX), "ry" => (PresentationAttr::Yes, ry: RY), "shape-rendering" => (PresentationAttr::Yes, shape_rendering : ShapeRendering), "stop-color" => (PresentationAttr::Yes, stop_color : StopColor), "stop-opacity" => (PresentationAttr::Yes, stop_opacity : StopOpacity), "stroke" => (PresentationAttr::Yes, stroke : Stroke), "stroke-dasharray" => (PresentationAttr::Yes, stroke_dasharray : StrokeDasharray), "stroke-dashoffset" => (PresentationAttr::Yes, stroke_dashoffset : StrokeDashoffset), "stroke-linecap" => (PresentationAttr::Yes, stroke_line_cap : StrokeLinecap), "stroke-linejoin" => (PresentationAttr::Yes, stroke_line_join : StrokeLinejoin), "stroke-miterlimit" => (PresentationAttr::Yes, stroke_miterlimit : StrokeMiterlimit), "stroke-opacity" => (PresentationAttr::Yes, stroke_opacity : StrokeOpacity), "stroke-width" => (PresentationAttr::Yes, stroke_width : StrokeWidth), "text-anchor" => (PresentationAttr::Yes, text_anchor : TextAnchor), "text-decoration" => (PresentationAttr::Yes, text_decoration : TextDecoration), // "text-overflow" => (PresentationAttr::Yes, unimplemented), "text-rendering" => (PresentationAttr::Yes, text_rendering : TextRendering), // "transform" - Special case as presentation attribute: // The SVG1.1 "transform" attribute has a different grammar than the // SVG2 "transform" property. Here we define for the properties machinery, // and it is handled specially as an attribute in parse_presentation_attributes(). "transform" => (PresentationAttr::No, transform_property : TransformProperty), // "transform-box" => (PresentationAttr::Yes, unimplemented), // "transform-origin" => (PresentationAttr::Yes, unimplemented), "unicode-bidi" => (PresentationAttr::Yes, unicode_bidi : UnicodeBidi), "visibility" => (PresentationAttr::Yes, visibility : Visibility), // "white-space" => (PresentationAttr::Yes, unimplemented), // "word-spacing" => (PresentationAttr::Yes, unimplemented), "width" => (PresentationAttr::Yes, width: Width), "writing-mode" => (PresentationAttr::Yes, writing_mode : WritingMode), "x" => (PresentationAttr::Yes, x: X), "y" => (PresentationAttr::Yes, y: Y), } longhands_not_supported_by_markup5ever: { "isolation" => (PresentationAttr::No, isolation : Isolation), "line-height" => (PresentationAttr::No, line_height : LineHeight), "mask-type" => (PresentationAttr::Yes, mask_type : MaskType), "mix-blend-mode" => (PresentationAttr::No, mix_blend_mode : MixBlendMode), "paint-order" => (PresentationAttr::Yes, paint_order : PaintOrder), "text-orientation" => (PresentationAttr::No, text_orientation : TextOrientation), "vector-effect" => (PresentationAttr::Yes, vector_effect : VectorEffect), } // These are not properties, but presentation attributes. However, // both xml:lang and xml:space *do* inherit. We are abusing the // property inheritance code for these XML-specific attributes. non_properties: { xml_lang: XmlLang, xml_space: XmlSpace, } } impl SpecifiedValues { fn property_index(&self, id: PropertyId) -> Option { let v = self.indices[id.as_usize()]; if v == PropertyId::UnsetProperty.as_u8() { None } else { Some(v as usize) } } fn set_property(&mut self, prop: &ParsedProperty, replace: bool) { let id = prop.get_property_id(); assert!(!id.is_shorthand()); if let Some(index) = self.property_index(id) { if replace { self.props[index] = prop.clone(); } } else { self.props.push(prop.clone()); let pos = self.props.len() - 1; self.indices[id.as_usize()] = pos as u8; } } fn get_property(&self, id: PropertyId) -> ParsedProperty { assert!(!id.is_shorthand()); if let Some(index) = self.property_index(id) { self.props[index].clone() } else { ParsedProperty::unspecified(id) } } fn set_property_expanding_shorthands(&mut self, prop: &ParsedProperty, replace: bool) { match *prop { ParsedProperty::Font(SpecifiedValue::Specified(ref f)) => { self.expand_font_shorthand(f, replace) } ParsedProperty::Marker(SpecifiedValue::Specified(ref m)) => { self.expand_marker_shorthand(m, replace) } ParsedProperty::Font(SpecifiedValue::Inherit) => { self.expand_font_shorthand_inherit(replace) } ParsedProperty::Marker(SpecifiedValue::Inherit) => { self.expand_marker_shorthand_inherit(replace) } _ => self.set_property(prop, replace), } } fn expand_font_shorthand(&mut self, font: &Font, replace: bool) { let FontSpec { style, variant, weight, stretch, size, line_height, family, } = font.to_font_spec(); self.set_property( &ParsedProperty::FontStyle(SpecifiedValue::Specified(style)), replace, ); self.set_property( &ParsedProperty::FontVariant(SpecifiedValue::Specified(variant)), replace, ); self.set_property( &ParsedProperty::FontWeight(SpecifiedValue::Specified(weight)), replace, ); self.set_property( &ParsedProperty::FontStretch(SpecifiedValue::Specified(stretch)), replace, ); self.set_property( &ParsedProperty::FontSize(SpecifiedValue::Specified(size)), replace, ); self.set_property( &ParsedProperty::LineHeight(SpecifiedValue::Specified(line_height)), replace, ); self.set_property( &ParsedProperty::FontFamily(SpecifiedValue::Specified(family)), replace, ); } fn expand_marker_shorthand(&mut self, marker: &Marker, replace: bool) { let Marker(v) = marker; self.set_property( &ParsedProperty::MarkerStart(SpecifiedValue::Specified(MarkerStart(v.clone()))), replace, ); self.set_property( &ParsedProperty::MarkerMid(SpecifiedValue::Specified(MarkerMid(v.clone()))), replace, ); self.set_property( &ParsedProperty::MarkerEnd(SpecifiedValue::Specified(MarkerEnd(v.clone()))), replace, ); } fn expand_font_shorthand_inherit(&mut self, replace: bool) { self.set_property(&ParsedProperty::FontStyle(SpecifiedValue::Inherit), replace); self.set_property( &ParsedProperty::FontVariant(SpecifiedValue::Inherit), replace, ); self.set_property( &ParsedProperty::FontWeight(SpecifiedValue::Inherit), replace, ); self.set_property( &ParsedProperty::FontStretch(SpecifiedValue::Inherit), replace, ); self.set_property(&ParsedProperty::FontSize(SpecifiedValue::Inherit), replace); self.set_property( &ParsedProperty::LineHeight(SpecifiedValue::Inherit), replace, ); self.set_property( &ParsedProperty::FontFamily(SpecifiedValue::Inherit), replace, ); } fn expand_marker_shorthand_inherit(&mut self, replace: bool) { self.set_property( &ParsedProperty::MarkerStart(SpecifiedValue::Inherit), replace, ); self.set_property(&ParsedProperty::MarkerMid(SpecifiedValue::Inherit), replace); self.set_property(&ParsedProperty::MarkerEnd(SpecifiedValue::Inherit), replace); } pub fn set_parsed_property(&mut self, prop: &ParsedProperty) { self.set_property_expanding_shorthands(prop, true); } /* user agent property have less priority than presentation attributes */ pub fn set_parsed_property_user_agent(&mut self, prop: &ParsedProperty) { self.set_property_expanding_shorthands(prop, false); } pub fn to_computed_values(&self, computed: &mut ComputedValues) { macro_rules! compute { ($name:ident, $field:ident) => {{ // This extra block --------^ // is so that prop_val will be dropped within the macro invocation; // otherwise all the temporary values cause this function to use // an unreasonably large amount of stack space. let prop_val = self.get_property(PropertyId::$name); if let ParsedProperty::$name(s) = prop_val { computed.set_value(ComputedValue::$name( s.compute(&computed.$field(), computed), )); } else { unreachable!(); } }}; } // First, compute font_size. It needs to be done before everything // else, so that properties that depend on its computed value // will be able to use it. For example, baseline-shift // depends on font-size. compute!(FontSize, font_size); // Then, do all the other properties. compute!(BaselineShift, baseline_shift); compute!(ClipPath, clip_path); compute!(ClipRule, clip_rule); compute!(Color, color); compute!(ColorInterpolationFilters, color_interpolation_filters); compute!(CX, cx); compute!(CY, cy); compute!(Direction, direction); compute!(Display, display); compute!(EnableBackground, enable_background); compute!(Fill, fill); compute!(FillOpacity, fill_opacity); compute!(FillRule, fill_rule); compute!(Filter, filter); compute!(FloodColor, flood_color); compute!(FloodOpacity, flood_opacity); compute!(FontFamily, font_family); compute!(FontStretch, font_stretch); compute!(FontStyle, font_style); compute!(FontVariant, font_variant); compute!(FontWeight, font_weight); compute!(GlyphOrientationVertical, glyph_orientation_vertical); compute!(Height, height); compute!(ImageRendering, image_rendering); compute!(Isolation, isolation); compute!(LetterSpacing, letter_spacing); compute!(LightingColor, lighting_color); compute!(MarkerEnd, marker_end); compute!(MarkerMid, marker_mid); compute!(MarkerStart, marker_start); compute!(Mask, mask); compute!(MaskType, mask_type); compute!(MixBlendMode, mix_blend_mode); compute!(Opacity, opacity); compute!(Overflow, overflow); compute!(PaintOrder, paint_order); compute!(R, r); compute!(RX, rx); compute!(RY, ry); compute!(ShapeRendering, shape_rendering); compute!(StopColor, stop_color); compute!(StopOpacity, stop_opacity); compute!(Stroke, stroke); compute!(StrokeDasharray, stroke_dasharray); compute!(StrokeDashoffset, stroke_dashoffset); compute!(StrokeLinecap, stroke_line_cap); compute!(StrokeLinejoin, stroke_line_join); compute!(StrokeOpacity, stroke_opacity); compute!(StrokeMiterlimit, stroke_miterlimit); compute!(StrokeWidth, stroke_width); compute!(TextAnchor, text_anchor); compute!(TextDecoration, text_decoration); compute!(TextOrientation, text_orientation); compute!(TextRendering, text_rendering); compute!(TransformProperty, transform_property); compute!(UnicodeBidi, unicode_bidi); compute!(VectorEffect, vector_effect); compute!(Visibility, visibility); compute!(Width, width); compute!(WritingMode, writing_mode); compute!(X, x); compute!(XmlSpace, xml_space); compute!(XmlLang, xml_lang); compute!(Y, y); computed.transform = self.transform.unwrap_or_else(|| { match self.get_property(PropertyId::TransformProperty) { ParsedProperty::TransformProperty(SpecifiedValue::Specified(ref t)) => { t.to_transform() } _ => Transform::identity(), } }); } /// This is a somewhat egregious hack to allow xml:lang to be stored as a presentational /// attribute. Presentational attributes can often be influenced by stylesheets, /// so they're cascaded after selector matching is done, but xml:lang can be queried by /// CSS selectors, so they need to be cascaded *first*. pub fn inherit_xml_lang( &self, computed: &mut ComputedValues, parent: Option, ) { use crate::node::NodeBorrow; let prop_val = self.get_property(PropertyId::XmlLang); if let ParsedProperty::XmlLang(s) = prop_val { if let Some(parent) = parent { computed.set_value(ComputedValue::XmlLang( parent.borrow_element().get_computed_values().xml_lang(), )); } computed.set_value(ComputedValue::XmlLang( s.compute(&computed.xml_lang(), computed), )); } else { unreachable!(); } } pub fn is_overflow(&self) -> bool { if let Some(overflow_index) = self.property_index(PropertyId::Overflow) { match self.props[overflow_index] { ParsedProperty::Overflow(SpecifiedValue::Specified(Overflow::Auto)) => true, ParsedProperty::Overflow(SpecifiedValue::Specified(Overflow::Visible)) => true, ParsedProperty::Overflow(_) => false, _ => unreachable!(), } } else { false } } fn parse_one_presentation_attribute(&mut self, session: &Session, attr: QualName, value: &str) { let mut input = ParserInput::new(value); let mut parser = Parser::new(&mut input); match parse_value(&attr, &mut parser, ParseAs::PresentationAttr) { Ok(prop) => { if parser.expect_exhausted().is_ok() { self.set_parsed_property(&prop); } else { rsvg_log!( session, "(ignoring invalid presentation attribute {:?}\n value=\"{}\")\n", attr.expanded(), value, ); } } // not a presentation attribute; just ignore it Err(ParseError { kind: ParseErrorKind::Custom(ValueErrorKind::UnknownProperty), .. }) => (), // https://www.w3.org/TR/CSS2/syndata.html#unsupported-values // For all the following cases, ignore illegal values; don't set the whole node to // be in error in that case. Err(ParseError { kind: ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(ref t)), .. }) => { let mut tok = String::new(); t.to_css(&mut tok).unwrap(); // FIXME: what do we do with a fmt::Error? rsvg_log!( session, "(ignoring invalid presentation attribute {:?}\n value=\"{}\"\n \ unexpected token '{}')", attr.expanded(), value, tok, ); } Err(ParseError { kind: ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput), .. }) => { rsvg_log!( session, "(ignoring invalid presentation attribute {:?}\n value=\"{}\"\n \ unexpected end of input)", attr.expanded(), value, ); } Err(ParseError { kind: ParseErrorKind::Basic(_), .. }) => { rsvg_log!( session, "(ignoring invalid presentation attribute {:?}\n value=\"{}\"\n \ unexpected error)", attr.expanded(), value, ); } Err(ParseError { kind: ParseErrorKind::Custom(ref v), .. }) => { rsvg_log!( session, "(ignoring invalid presentation attribute {:?}\n value=\"{}\"\n {})", attr.expanded(), value, v ); } } } pub fn parse_presentation_attributes(&mut self, session: &Session, attrs: &Attributes) { for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "transform") => { // FIXME: we parse the transform attribute here because we don't yet have // a better way to distinguish attributes whose values have different // grammars than properties. let transform_attr = TransformAttribute::parse_str(value) .unwrap_or_else(|_| TransformAttribute::default()); self.transform = Some(transform_attr.to_transform()); } expanded_name!(xml "lang") => { // xml:lang is a non-presentation attribute and as such cannot have the // "inherit" value. So, we don't call parse_one_presentation_attribute() // for it, but rather call its parser directly. let parse_result: Result = attr.parse(value); match parse_result { Ok(lang) => { self.set_parsed_property(&ParsedProperty::XmlLang( SpecifiedValue::Specified(lang), )); } Err(e) => { rsvg_log!(session, "ignoring attribute with invalid value: {}", e); } } } expanded_name!(xml "space") => { // xml:space is a non-presentation attribute and as such cannot have the // "inherit" value. So, we don't call parse_one_presentation_attribute() // for it, but rather call its parser directly. let parse_result: Result = attr.parse(value); match parse_result { Ok(space) => { self.set_parsed_property(&ParsedProperty::XmlSpace( SpecifiedValue::Specified(space), )); } Err(e) => { rsvg_log!(session, "ignoring attribute with invalid value: {}", e); } } } _ => self.parse_one_presentation_attribute(session, attr, value), } } } pub fn set_property_from_declaration( &mut self, declaration: &Declaration, origin: Origin, important_styles: &mut HashSet, ) { if !declaration.important && important_styles.contains(&declaration.prop_name) { return; } if declaration.important { important_styles.insert(declaration.prop_name.clone()); } if origin == Origin::UserAgent { self.set_parsed_property_user_agent(&declaration.property); } else { self.set_parsed_property(&declaration.property); } } pub fn parse_style_declarations( &mut self, declarations: &str, origin: Origin, important_styles: &mut HashSet, session: &Session, ) { let mut input = ParserInput::new(declarations); let mut parser = Parser::new(&mut input); RuleBodyParser::new(&mut parser, &mut DeclParser) .filter_map(|r| match r { Ok(RuleBodyItem::Decl(decl)) => Some(decl), Ok(RuleBodyItem::Rule(_)) => None, Err(e) => { rsvg_log!(session, "Invalid declaration; ignoring: {:?}", e); None } }) .for_each(|decl| self.set_property_from_declaration(&decl, origin, important_styles)); } } // Parses the value for the type `T` of the property out of the Parser, including `inherit` values. fn parse_input<'i, T>(input: &mut Parser<'i, '_>) -> Result, ParseError<'i>> where T: Property + Clone + Default + Parse, { if input .try_parse(|p| p.expect_ident_matching("inherit")) .is_ok() { Ok(SpecifiedValue::Inherit) } else { Parse::parse(input).map(SpecifiedValue::Specified) } } #[cfg(test)] mod tests { use super::*; use crate::iri::Iri; use crate::length::*; #[test] fn empty_values_computes_to_defaults() { let specified = SpecifiedValues::default(); let mut computed = ComputedValues::default(); specified.to_computed_values(&mut computed); assert_eq!(computed.stroke_width(), StrokeWidth::default()); } #[test] fn set_one_property() { let length = Length::::new(42.0, LengthUnit::Px); let mut specified = SpecifiedValues::default(); specified.set_parsed_property(&ParsedProperty::StrokeWidth(SpecifiedValue::Specified( StrokeWidth(length), ))); let mut computed = ComputedValues::default(); specified.to_computed_values(&mut computed); assert_eq!(computed.stroke_width(), StrokeWidth(length)); } #[test] fn replace_existing_property() { let length1 = Length::::new(42.0, LengthUnit::Px); let length2 = Length::::new(42.0, LengthUnit::Px); let mut specified = SpecifiedValues::default(); specified.set_parsed_property(&ParsedProperty::StrokeWidth(SpecifiedValue::Specified( StrokeWidth(length1), ))); specified.set_parsed_property(&ParsedProperty::StrokeWidth(SpecifiedValue::Specified( StrokeWidth(length2), ))); let mut computed = ComputedValues::default(); specified.to_computed_values(&mut computed); assert_eq!(computed.stroke_width(), StrokeWidth(length2)); } #[test] fn expands_marker_shorthand() { let mut specified = SpecifiedValues::default(); let iri = Iri::parse_str("url(#foo)").unwrap(); let marker = Marker(iri.clone()); specified.set_parsed_property(&ParsedProperty::Marker(SpecifiedValue::Specified(marker))); let mut computed = ComputedValues::default(); specified.to_computed_values(&mut computed); assert_eq!(computed.marker_start(), MarkerStart(iri.clone())); assert_eq!(computed.marker_mid(), MarkerMid(iri.clone())); assert_eq!(computed.marker_end(), MarkerEnd(iri.clone())); } #[test] fn replaces_marker_shorthand() { let mut specified = SpecifiedValues::default(); let iri1 = Iri::parse_str("url(#foo)").unwrap(); let iri2 = Iri::None; let marker1 = Marker(iri1.clone()); specified.set_parsed_property(&ParsedProperty::Marker(SpecifiedValue::Specified(marker1))); let marker2 = Marker(iri2.clone()); specified.set_parsed_property(&ParsedProperty::Marker(SpecifiedValue::Specified(marker2))); let mut computed = ComputedValues::default(); specified.to_computed_values(&mut computed); assert_eq!(computed.marker_start(), MarkerStart(iri2.clone())); assert_eq!(computed.marker_mid(), MarkerMid(iri2.clone())); assert_eq!(computed.marker_end(), MarkerEnd(iri2.clone())); } #[test] fn computes_property_that_does_not_inherit_automatically() { assert!(!::inherits_automatically()); let half_opacity = Opacity::parse_str("0.5").unwrap(); // first level, as specified with opacity let mut with_opacity = SpecifiedValues::default(); with_opacity.set_parsed_property(&ParsedProperty::Opacity(SpecifiedValue::Specified( half_opacity.clone(), ))); let mut computed_0_5 = ComputedValues::default(); with_opacity.to_computed_values(&mut computed_0_5); assert_eq!(computed_0_5.opacity(), half_opacity.clone()); // second level, no opacity specified, and it doesn't inherit let without_opacity = SpecifiedValues::default(); let mut computed = computed_0_5.clone(); without_opacity.to_computed_values(&mut computed); assert_eq!(computed.opacity(), Opacity::default()); // another at second level, opacity set to explicitly inherit let mut with_inherit_opacity = SpecifiedValues::default(); with_inherit_opacity.set_parsed_property(&ParsedProperty::Opacity(SpecifiedValue::Inherit)); let mut computed = computed_0_5.clone(); with_inherit_opacity.to_computed_values(&mut computed); assert_eq!(computed.opacity(), half_opacity.clone()); } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/property_defs.rs�����������������������������������������������������������������0000644�0000000�0000000�00000117155�10461020230�0015425�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Definitions for CSS property types. //! //! Do not import things directly from this module; use the `properties` module instead, //! which re-exports things from here. //! //! This module defines most of the CSS property types that librsvg supports. Each //! property requires a Rust type that will hold its values, and that type should //! implement a few traits, as follows. //! //! # Requirements for a property type //! //! You should call the [`make_property`] macro to take care of most of these requirements //! automatically: //! //! * A name for the type. For example, the `fill` property has a [`Fill`] type defined //! in this module. //! //! * An initial value per the CSS or SVG specs, given through an implementation of the //! [`Default`] trait. //! //! * Whether the property's computed value inherits to child elements, given through an //! implementation of the [`Property`] trait and its //! [`inherits_automatically`][Property::inherits_automatically] method. //! //! * A way to derive the CSS *computed value* for the property, given through an //! implementation of the [`Property`] trait and its [`compute`][Property::compute] method. //! //! * The actual underlying type. For example, the [`make_property`] macro can generate a //! field-less enum for properties like the `clip-rule` property, which just has //! identifier-based values like `nonzero` and `evenodd`. For general-purpose types like //! [`Length`], the macro can wrap them in a newtype like `struct` //! [`StrokeWidth`]`(`[`Length`]`)`. For custom types, the macro call can be used just to //! define the initial/default value and whether the property inherits automatically; you //! should provide the other required trait implementations separately. //! //! * An implementation of the [`Parse`] trait for the underlying type. use std::convert::TryInto; use std::str::FromStr; use cssparser::{Parser, Token}; use language_tags::LanguageTag; use crate::dasharray::Dasharray; use crate::error::*; use crate::filter::FilterValueList; use crate::font_props::{ Font, FontFamily, FontSize, FontWeight, GlyphOrientationVertical, LetterSpacing, LineHeight, }; use crate::iri::Iri; use crate::length::*; use crate::paint_server::PaintServer; use crate::parse_identifiers; use crate::parsers::Parse; use crate::properties::ComputedValues; use crate::property_macros::Property; use crate::rect::Rect; use crate::transform::TransformProperty; use crate::unit_interval::UnitInterval; use crate::{impl_default, impl_property, make_property}; make_property!( /// `baseline-shift` property. /// /// SVG1.1: /// /// SVG2: BaselineShift, default: Length::::parse_str("0.0").unwrap(), newtype: Length, property_impl: { impl Property for BaselineShift { fn inherits_automatically() -> bool { false } fn compute(&self, v: &ComputedValues) -> Self { let font_size = v.font_size().value(); let parent = v.baseline_shift(); match (self.0.unit, parent.0.unit) { (LengthUnit::Percent, _) => { BaselineShift(Length::::new(self.0.length * font_size.length + parent.0.length, font_size.unit)) } (x, y) if x == y || parent.0.length == 0.0 => { BaselineShift(Length::::new(self.0.length + parent.0.length, self.0.unit)) } _ => { // FIXME: the limitation here is that the parent's baseline_shift // and ours have different units. We should be able to normalize // the lengths and add them even if they have different units, but // at the moment that requires access to the draw_ctx, which we // don't have here. // // So for now we won't add to the parent's baseline_shift. parent } } } } }, parse_impl: { impl Parse for BaselineShift { // These values come from Inkscape's SP_CSS_BASELINE_SHIFT_(SUB/SUPER/BASELINE); // see sp_style_merge_baseline_shift_from_parent() fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { parser.try_parse(|p| Ok(BaselineShift(Length::::parse(p)?))) .or_else(|_: ParseError<'_>| { Ok(parse_identifiers!( parser, "baseline" => BaselineShift(Length::::new(0.0, LengthUnit::Percent)), "sub" => BaselineShift(Length::::new(-0.2, LengthUnit::Percent)), "super" => BaselineShift(Length::::new(0.4, LengthUnit::Percent)), )?) }) } } } ); make_property!( /// `clip-path` property. /// /// SVG1.1: /// /// CSS Masking 1: ClipPath, default: Iri::None, inherits_automatically: false, newtype_parse: Iri, ); make_property!( /// `clip-rule` property. /// /// SVG1.1: /// /// CSS Masking 1: ClipRule, default: NonZero, inherits_automatically: true, identifiers: "nonzero" => NonZero, "evenodd" => EvenOdd, ); make_property!( /// `color` property, the fallback for `currentColor` values. /// /// SVG1.1: /// /// SVG2: /// /// The SVG spec allows the user agent to choose its own initial value for the "color" /// property. Here we start with opaque black for the initial value. Clients can /// override this by specifing a custom CSS stylesheet. /// /// Most of the time the `color` property is used to call /// [`crate::paint_server::resolve_color`]. Color, default: cssparser::Color::Rgba(cssparser::RGBA::new(Some(0), Some(0), Some(0), Some(1.0))), inherits_automatically: true, newtype_parse: cssparser::Color, ); make_property!( /// `color-interpolation-filters` property. /// /// SVG1.1: /// /// Filter Effects 1: ColorInterpolationFilters, default: LinearRgb, inherits_automatically: true, identifiers: "auto" => Auto, "linearRGB" => LinearRgb, "sRGB" => Srgb, ); make_property!( /// `cx` property. /// /// SVG2: /// /// Note that in SVG1.1, this was an attribute, not a property. CX, default: Length::::parse_str("0").unwrap(), inherits_automatically: false, newtype_parse: Length, ); make_property!( /// `cy` attribute. /// /// SVG2: /// /// Note that in SVG1.1, this was an attribute, not a property. CY, default: Length::::parse_str("0").unwrap(), inherits_automatically: false, newtype_parse: Length, ); make_property!( /// `direction` property. /// /// SVG1.1: /// /// SVG2: Direction, default: Ltr, inherits_automatically: true, identifiers: "ltr" => Ltr, "rtl" => Rtl, ); make_property!( /// `display` property. /// /// SVG1.1: /// /// SVG2: Display, default: Inline, inherits_automatically: false, identifiers: "inline" => Inline, "block" => Block, "list-item" => ListItem, "run-in" => RunIn, "compact" => Compact, "marker" => Marker, "table" => Table, "inline-table" => InlineTable, "table-row-group" => TableRowGroup, "table-header-group" => TableHeaderGroup, "table-footer-group" => TableFooterGroup, "table-row" => TableRow, "table-column-group" => TableColumnGroup, "table-column" => TableColumn, "table-cell" => TableCell, "table-caption" => TableCaption, "none" => None, ); /// `enable-background` property. /// /// SVG1.1: /// /// This is deprecated in SVG2. We just have a parser for it to avoid setting elements in /// error if they have this property. Librsvg does not use the value of this property. #[derive(Debug, Clone, Copy, PartialEq)] pub enum EnableBackground { Accumulate, New(Option), } make_property!( EnableBackground, default: EnableBackground::Accumulate, inherits_automatically: false, parse_impl: { impl Parse for EnableBackground { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let loc = parser.current_source_location(); if parser .try_parse(|p| p.expect_ident_matching("accumulate")) .is_ok() { return Ok(EnableBackground::Accumulate); } if parser.try_parse(|p| p.expect_ident_matching("new")).is_ok() { parser.try_parse(|p| -> Result<_, ParseError<'_>> { let x = f64::parse(p)?; let y = f64::parse(p)?; let w = f64::parse(p)?; let h = f64::parse(p)?; Ok(EnableBackground::New(Some(Rect::new(x, y, x + w, y + h)))) }).or(Ok(EnableBackground::New(None))) } else { Err(loc.new_custom_error(ValueErrorKind::parse_error("invalid syntax for 'enable-background' property"))) } } } } ); #[cfg(test)] #[test] fn parses_enable_background() { assert_eq!( EnableBackground::parse_str("accumulate").unwrap(), EnableBackground::Accumulate ); assert_eq!( EnableBackground::parse_str("new").unwrap(), EnableBackground::New(None) ); assert_eq!( EnableBackground::parse_str("new 1 2 3 4").unwrap(), EnableBackground::New(Some(Rect::new(1.0, 2.0, 4.0, 6.0))) ); assert!(EnableBackground::parse_str("new foo").is_err()); assert!(EnableBackground::parse_str("plonk").is_err()); } make_property!( /// `fill` property. /// /// SVG1.1: /// /// SVG2: Fill, default: PaintServer::parse_str("#000").unwrap(), inherits_automatically: true, newtype_parse: PaintServer, ); make_property!( /// `fill-opacity` property. /// /// SVG1.1: /// /// SVG2: FillOpacity, default: UnitInterval(1.0), inherits_automatically: true, newtype_parse: UnitInterval, ); make_property!( /// `fill-rule` property. /// /// SVG1.1: /// /// SVG2: FillRule, default: NonZero, inherits_automatically: true, identifiers: "nonzero" => NonZero, "evenodd" => EvenOdd, ); /// `filter` property. /// /// SVG1.1: /// /// Filter Effects 1: /// /// Note that in SVG2, the filters got offloaded to the [Filter Effects Module Level /// 1](https://www.w3.org/TR/filter-effects/) specification. #[derive(Debug, Clone, PartialEq)] pub enum Filter { None, List(FilterValueList), } make_property!( Filter, default: Filter::None, inherits_automatically: false, parse_impl: { impl Parse for Filter { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { if parser .try_parse(|p| p.expect_ident_matching("none")) .is_ok() { return Ok(Filter::None); } Ok(Filter::List(FilterValueList::parse(parser)?)) } } } ); make_property!( /// `flood-color` property, for `feFlood` and `feDropShadow` filter elements. /// /// SVG1.1: /// /// Filter Effects 1: FloodColor, default: cssparser::Color::Rgba(cssparser::RGBA::new(Some(0), Some(0), Some(0), Some(1.0))), inherits_automatically: false, newtype_parse: cssparser::Color, ); make_property!( /// `flood-opacity` property, for `feFlood` and `feDropShadow` filter elements. /// /// SVG1.1: /// /// Filter Effects 1: FloodOpacity, default: UnitInterval(1.0), inherits_automatically: false, newtype_parse: UnitInterval, ); make_property!( // docs are in font_props.rs Font, default: Font::Spec(Default::default()), inherits_automatically: true, ); make_property!( // docs are in font_props.rs FontFamily, default: FontFamily("Times New Roman".to_string()), inherits_automatically: true, ); make_property!( // docs are in font_props.rs FontSize, default: FontSize::Value(Length::::parse_str("12.0").unwrap()), property_impl: { impl Property for FontSize { fn inherits_automatically() -> bool { true } fn compute(&self, v: &ComputedValues) -> Self { self.compute(v) } } } ); make_property!( /// `font-stretch` property. /// /// SVG1.1: /// /// CSS Fonts 3: FontStretch, default: Normal, inherits_automatically: true, identifiers: "normal" => Normal, "wider" => Wider, "narrower" => Narrower, "ultra-condensed" => UltraCondensed, "extra-condensed" => ExtraCondensed, "condensed" => Condensed, "semi-condensed" => SemiCondensed, "semi-expanded" => SemiExpanded, "expanded" => Expanded, "extra-expanded" => ExtraExpanded, "ultra-expanded" => UltraExpanded, ); make_property!( /// `font-style` property. /// /// SVG1.1: /// /// CSS Fonts 3: FontStyle, default: Normal, inherits_automatically: true, identifiers: "normal" => Normal, "italic" => Italic, "oblique" => Oblique, ); make_property!( /// `font-variant` property. /// /// SVG1.1: /// /// CSS Fonts 3: /// /// Note that in CSS3, this is a lot more complex than CSS2.1 / SVG1.1. FontVariant, default: Normal, inherits_automatically: true, identifiers: "normal" => Normal, "small-caps" => SmallCaps, ); make_property!( // docs are in font_props.rs FontWeight, default: FontWeight::Normal, property_impl: { impl Property for FontWeight { fn inherits_automatically() -> bool { true } fn compute(&self, v: &ComputedValues) -> Self { self.compute(&v.font_weight()) } } } ); make_property!( // docs are in font_props.rs // // Although https://www.w3.org/TR/css-writing-modes-3/#propdef-glyph-orientation-vertical specifies // "n/a" for both the initial value (default) and inheritance, we'll use Auto here for the default, // since it translates to TextOrientation::Mixed - which is text-orientation's initial value. GlyphOrientationVertical, default: GlyphOrientationVertical::Auto, inherits_automatically: false, ); make_property!( /// `height` property. /// /// SVG2: /// /// Note that in SVG1.1, this was an attribute, not a property. Height, default: LengthOrAuto::::Auto, inherits_automatically: false, newtype_parse: LengthOrAuto, ); make_property!( /// `image-rendering` property. /// /// CSS Images Module Level 3: /// /// Note that this property previously accepted the values optimizeSpeed and optimizeQuality. /// These are now deprecated; a user agent must accept them as valid values but must treat /// them as having the same behavior as crisp-edges and smooth respectively. ImageRendering, default: Auto, inherits_automatically: true, identifiers: "auto" => Auto, "smooth" => Smooth, "optimizeQuality" => OptimizeQuality, "high-quality" => HighQuality, "crisp-edges" => CrispEdges, "optimizeSpeed" => OptimizeSpeed, "pixelated" => Pixelated, ); make_property!( /// `isolation` property. /// /// CSS Compositing and Blending 1: Isolation, default: Auto, inherits_automatically: false, identifiers: "auto" => Auto, "isolate" => Isolate, ); make_property!( // docs are in font_props.rs LetterSpacing, default: LetterSpacing::Normal, property_impl: { impl Property for LetterSpacing { fn inherits_automatically() -> bool { true } fn compute(&self, _v: &ComputedValues) -> Self { self.compute() } } } ); make_property!( // docs are in font_props.rs LineHeight, default: LineHeight::Normal, inherits_automatically: true, ); make_property!( /// `lighting-color` property for `feDiffuseLighting` and `feSpecularLighting` filter elements. /// /// SVG1.1: /// /// Filter Effects 1: LightingColor, default: cssparser::Color::Rgba(cssparser::RGBA::new(Some(255), Some(255), Some(255), Some(1.0))), inherits_automatically: false, newtype_parse: cssparser::Color, ); make_property!( /// `marker` shorthand property. /// /// SVG2: /// /// This is a shorthand, which expands to the `marker-start`, `marker-mid`, /// `marker-end` longhand properties. Marker, default: Iri::None, inherits_automatically: true, newtype_parse: Iri, ); make_property!( /// `marker-end` property. /// /// SVG2: MarkerEnd, default: Iri::None, inherits_automatically: true, newtype_parse: Iri, ); make_property!( /// `marker-mid` property. /// /// SVG2: MarkerMid, default: Iri::None, inherits_automatically: true, newtype_parse: Iri, ); make_property!( /// `marker-start` property. /// /// SVG2: MarkerStart, default: Iri::None, inherits_automatically: true, newtype_parse: Iri, ); make_property!( /// `mask` shorthand property. /// /// SVG1.1: /// /// CSS Masking 1: /// /// Note that librsvg implements SVG1.1 semantics, where this is not a shorthand. Mask, default: Iri::None, inherits_automatically: false, newtype_parse: Iri, ); make_property!( /// `mask-type` property. /// /// CSS Masking 1: MaskType, default: Luminance, inherits_automatically: false, identifiers: "luminance" => Luminance, "alpha" => Alpha, ); make_property!( /// `mix-blend-mode` property. /// /// Compositing and Blending 1: MixBlendMode, default: Normal, inherits_automatically: false, identifiers: "normal" => Normal, "multiply" => Multiply, "screen" => Screen, "overlay" => Overlay, "darken" => Darken, "lighten" => Lighten, "color-dodge" => ColorDodge, "color-burn" => ColorBurn, "hard-light" => HardLight, "soft-light" => SoftLight, "difference" => Difference, "exclusion" => Exclusion, "hue" => Hue, "saturation" => Saturation, "color" => Color, "luminosity" => Luminosity, ); make_property!( /// `opacity` property. /// /// CSS Color 3: Opacity, default: UnitInterval(1.0), inherits_automatically: false, newtype_parse: UnitInterval, ); make_property!( /// `overflow` shorthand property. /// /// CSS2: /// /// CSS Overflow 3: /// /// Note that librsvg implements SVG1.1 semantics, where this is not a shorthand. Overflow, default: Visible, inherits_automatically: false, identifiers: "visible" => Visible, "hidden" => Hidden, "scroll" => Scroll, "auto" => Auto, ); impl Overflow { pub fn overflow_allowed(&self) -> bool { matches!(*self, Overflow::Auto | Overflow::Visible) } } /// One of the three operations for the `paint-order` property; see [`PaintOrder`]. #[repr(u8)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum PaintTarget { Fill, Stroke, Markers, } make_property!( /// `paint-order` property. /// /// SVG2: /// /// The `targets` field specifies the order in which graphic elements should be filled/stroked. /// Instead of hard-coding an order of fill/stroke/markers, use the order specified by the `targets`. PaintOrder, inherits_automatically: true, fields: { targets: [PaintTarget; 3], default: [PaintTarget::Fill, PaintTarget::Stroke, PaintTarget::Markers], } parse_impl: { impl Parse for PaintOrder { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let allowed_targets = 3; let mut targets = Vec::with_capacity(allowed_targets); if parser.try_parse(|p| p.expect_ident_matching("normal")).is_ok() { return Ok(PaintOrder::default()); } while !parser.is_exhausted() { let loc = parser.current_source_location(); let token = parser.next()?; let value = match token { Token::Ident(ref cow) if cow.eq_ignore_ascii_case("fill") && !targets.contains(&PaintTarget::Fill) => PaintTarget::Fill, Token::Ident(ref cow) if cow.eq_ignore_ascii_case("stroke") && !targets.contains(&PaintTarget::Stroke) => PaintTarget::Stroke, Token::Ident(ref cow) if cow.eq_ignore_ascii_case("markers") && !targets.contains(&PaintTarget::Markers) => PaintTarget::Markers, _ => return Err(loc.new_basic_unexpected_token_error(token.clone()).into()), }; targets.push(value); }; // any values which were not specfied should be painted in default order // (fill, stroke, markers) following the values which were explicitly specified. for &target in &[PaintTarget::Fill, PaintTarget::Stroke, PaintTarget::Markers] { if !targets.contains(&target) { targets.push(target); } } Ok(PaintOrder { targets: targets[..].try_into().expect("Incorrect number of targets in paint-order") }) } } } ); #[cfg(test)] #[test] fn parses_paint_order() { assert_eq!( PaintOrder::parse_str("normal").unwrap(), PaintOrder { targets: [PaintTarget::Fill, PaintTarget::Stroke, PaintTarget::Markers] } ); assert_eq!( PaintOrder::parse_str("markers fill").unwrap(), PaintOrder { targets: [PaintTarget::Markers, PaintTarget::Fill, PaintTarget::Stroke] } ); assert_eq!( PaintOrder::parse_str("stroke").unwrap(), PaintOrder { targets: [PaintTarget::Stroke, PaintTarget::Fill, PaintTarget::Markers] } ); assert!(PaintOrder::parse_str("stroke stroke").is_err()); assert!(PaintOrder::parse_str("markers stroke fill hello").is_err()); } make_property!( /// `r` property. /// /// SVG2: /// /// Note that in SVG1.1, this was an attribute, not a property. R, default: ULength::::parse_str("0").unwrap(), inherits_automatically: false, newtype_parse: ULength, ); make_property!( /// `rx` property. /// /// SVG2: /// /// Note that in SVG1.1, this was an attribute, not a property. RX, default: LengthOrAuto::::Auto, inherits_automatically: false, newtype_parse: LengthOrAuto, ); make_property!( /// `ry` property. /// /// SVG2: /// /// Note that in SVG1.1, this was an attribute, not a property. RY, default: LengthOrAuto::::Auto, inherits_automatically: false, newtype_parse: LengthOrAuto, ); make_property!( /// `shape-rendering` property. /// /// SVG2: ShapeRendering, default: Auto, inherits_automatically: true, identifiers: "auto" => Auto, "optimizeSpeed" => OptimizeSpeed, "geometricPrecision" => GeometricPrecision, "crispEdges" => CrispEdges, ); make_property!( /// `stop-color` property for gradient stops. /// /// SVG2: StopColor, default: cssparser::Color::Rgba(cssparser::RGBA::new(Some(0), Some(0), Some(0), Some(1.0))), inherits_automatically: false, newtype_parse: cssparser::Color, ); make_property!( /// `stop-opacity` property for gradient stops. /// /// SVG2: StopOpacity, default: UnitInterval(1.0), inherits_automatically: false, newtype_parse: UnitInterval, ); make_property!( /// `stroke` property. /// /// SVG2: Stroke, default: PaintServer::None, inherits_automatically: true, newtype_parse: PaintServer, ); make_property!( /// `stroke-dasharray` property. /// /// SVG2: StrokeDasharray, default: Dasharray::default(), inherits_automatically: true, newtype_parse: Dasharray, ); make_property!( /// `stroke-dashoffset` property. /// /// SVG2: StrokeDashoffset, default: Length::::default(), inherits_automatically: true, newtype_parse: Length, ); make_property!( /// `stroke-linecap` property. /// /// SVG2: StrokeLinecap, default: Butt, inherits_automatically: true, identifiers: "butt" => Butt, "round" => Round, "square" => Square, ); make_property!( /// `stroke-linejoin` property. /// /// SVG2: StrokeLinejoin, default: Miter, inherits_automatically: true, identifiers: "miter" => Miter, "round" => Round, "bevel" => Bevel, ); make_property!( /// `stroke-miterlimit` property. /// /// SVG2: StrokeMiterlimit, default: 4f64, inherits_automatically: true, newtype_parse: f64, ); make_property!( /// `stroke-opacity` property. /// /// SVG2: StrokeOpacity, default: UnitInterval(1.0), inherits_automatically: true, newtype_parse: UnitInterval, ); make_property!( /// `stroke-width` property. /// /// SVG2: StrokeWidth, default: Length::::parse_str("1.0").unwrap(), inherits_automatically: true, newtype_parse: Length::, ); make_property!( /// `text-anchor` property. /// /// SVG1.1: TextAnchor, default: Start, inherits_automatically: true, identifiers: "start" => Start, "middle" => Middle, "end" => End, ); make_property!( /// `text-decoration` shorthand property. /// /// SVG1.1: /// /// CSS Text Decoration 3: /// /// Note that librsvg implements SVG1.1 semantics, where this is not a shorthand. TextDecoration, inherits_automatically: false, fields: { overline: bool, default: false, underline: bool, default: false, strike: bool, default: false, } parse_impl: { impl Parse for TextDecoration { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let mut overline = false; let mut underline = false; let mut strike = false; if parser.try_parse(|p| p.expect_ident_matching("none")).is_ok() { return Ok(TextDecoration::default()); } while !parser.is_exhausted() { let loc = parser.current_source_location(); let token = parser.next()?; match token { Token::Ident(ref cow) if cow.eq_ignore_ascii_case("overline") => overline = true, Token::Ident(ref cow) if cow.eq_ignore_ascii_case("underline") => underline = true, Token::Ident(ref cow) if cow.eq_ignore_ascii_case("line-through") => strike = true, _ => return Err(loc.new_basic_unexpected_token_error(token.clone()).into()), } } Ok(TextDecoration { overline, underline, strike, }) } } } ); #[cfg(test)] #[test] fn parses_text_decoration() { assert_eq!( TextDecoration::parse_str("none").unwrap(), TextDecoration { overline: false, underline: false, strike: false, } ); assert_eq!( TextDecoration::parse_str("overline").unwrap(), TextDecoration { overline: true, underline: false, strike: false, } ); assert_eq!( TextDecoration::parse_str("underline").unwrap(), TextDecoration { overline: false, underline: true, strike: false, } ); assert_eq!( TextDecoration::parse_str("line-through").unwrap(), TextDecoration { overline: false, underline: false, strike: true, } ); assert_eq!( TextDecoration::parse_str("underline overline").unwrap(), TextDecoration { overline: true, underline: true, strike: false, } ); assert!(TextDecoration::parse_str("airline").is_err()) } make_property!( /// `text-orientation` property. /// /// CSS Writing Modes 3: TextOrientation, default: Mixed, inherits_automatically: true, identifiers: "mixed" => Mixed, "upright" => Upright, "sideways" => Sideways, ); impl From for TextOrientation { /// Converts the `glyph-orientation-vertical` shorthand to a `text-orientation` longhand. /// /// See for the conversion table. fn from(o: GlyphOrientationVertical) -> TextOrientation { match o { GlyphOrientationVertical::Auto => TextOrientation::Mixed, GlyphOrientationVertical::Angle0 => TextOrientation::Upright, GlyphOrientationVertical::Angle90 => TextOrientation::Sideways, } } } make_property!( /// `text-rendering` property. /// /// SVG1.1: TextRendering, default: Auto, inherits_automatically: true, identifiers: "auto" => Auto, "optimizeSpeed" => OptimizeSpeed, "optimizeLegibility" => OptimizeLegibility, "geometricPrecision" => GeometricPrecision, ); make_property!( /// `transform` property. /// /// CSS Transforms 1: Transform, default: TransformProperty::None, inherits_automatically: false, newtype_parse: TransformProperty, ); make_property!( /// `unicode-bidi` property. /// /// CSS Writing Modes 3: UnicodeBidi, default: Normal, inherits_automatically: false, identifiers: "normal" => Normal, "embed" => Embed, "isolate" => Isolate, "bidi-override" => BidiOverride, "isolate-override" => IsolateOverride, "plaintext" => Plaintext, ); make_property!( /// `vector-effect` property. /// /// SVG2: VectorEffect, default: None, inherits_automatically: false, identifiers: "none" => None, "non-scaling-stroke" => NonScalingStroke, // non-scaling-size, non-rotation, fixed-position not implemented ); make_property!( /// `visibility` property. /// /// CSS2: Visibility, default: Visible, inherits_automatically: true, identifiers: "visible" => Visible, "hidden" => Hidden, "collapse" => Collapse, ); make_property!( /// `width` property. /// /// SVG2: /// /// Note that in SVG1.1, this was an attribute, not a property. Width, default: LengthOrAuto::::Auto, inherits_automatically: false, newtype_parse: LengthOrAuto, ); make_property!( /// `writing-mode` property. /// /// SVG1.1: /// /// SVG2: /// /// CSS Writing Modes 3: /// /// See the comments in the SVG2 spec for how the SVG1.1 values must be translated /// into CSS Writing Modes 3 values. WritingMode, default: HorizontalTb, identifiers: { "horizontal-tb" => HorizontalTb, "vertical-rl" => VerticalRl, "vertical-lr" => VerticalLr, "lr" => Lr, "lr-tb" => LrTb, "rl" => Rl, "rl-tb" => RlTb, "tb" => Tb, "tb-rl" => TbRl, }, property_impl: { impl Property for WritingMode { fn inherits_automatically() -> bool { true } fn compute(&self, _v: &ComputedValues) -> Self { use WritingMode::*; // Translate SVG1.1 compatibility values to SVG2 / CSS Writing Modes 3. match *self { Lr | LrTb | Rl | RlTb => HorizontalTb, Tb | TbRl => VerticalRl, _ => *self, } } } } ); impl WritingMode { pub fn is_horizontal(self) -> bool { use WritingMode::*; matches!(self, HorizontalTb | Lr | LrTb | Rl | RlTb) } } make_property!( /// `x` property. /// /// SVG2: /// /// Note that in SVG1.1, this was an attribute, not a property. X, default: Length::::parse_str("0").unwrap(), inherits_automatically: false, newtype_parse: Length, ); make_property!( /// `xml:lang` attribute. /// /// XML1.0: /// /// Similar to `XmlSpace`, this is a hack in librsvg: the `xml:lang` attribute is /// supposed to apply to an element and all its children. This more or less matches /// CSS property inheritance, so librsvg reuses the machinery for property inheritance /// to propagate down the value of the `xml:lang` attribute to an element's children. XmlLang, default: None, inherits_automatically: true, newtype: Option>, parse_impl: { impl Parse for XmlLang { fn parse<'i>( parser: &mut Parser<'i, '_>, ) -> Result> { let language_tag = parser.expect_ident()?; let language_tag = LanguageTag::from_str(language_tag).map_err(|_| { parser.new_custom_error(ValueErrorKind::parse_error("invalid syntax for 'xml:lang' parameter")) })?; Ok(XmlLang(Some(Box::new(language_tag)))) } } }, ); #[cfg(test)] #[test] fn parses_xml_lang() { assert_eq!( XmlLang::parse_str("es-MX").unwrap(), XmlLang(Some(Box::new(LanguageTag::from_str("es-MX").unwrap()))) ); assert!(XmlLang::parse_str("").is_err()); } make_property!( /// `xml:space` attribute. /// /// XML1.0: /// /// Similar to `XmlLang`, this is a hack in librsvg. The `xml:space` attribute is /// supposed to be applied to all the children of the element in which it appears, so /// it works more or less the same as CSS property inheritance. Librsvg reuses the /// machinery for CSS property inheritance to propagate down the value of `xml:space` /// to an element's children. XmlSpace, default: Default, inherits_automatically: true, identifiers: "default" => Default, "preserve" => Preserve, ); make_property!( /// `y` property. /// /// SVG2: /// /// Note that in SVG1.1, this was an attribute, not a property. Y, default: Length::::parse_str("0").unwrap(), inherits_automatically: false, newtype_parse: Length, ); �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/property_macros.rs���������������������������������������������������������������0000644�0000000�0000000�00000021077�10461020230�0015765�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Macros to define CSS properties. use crate::properties::ComputedValues; /// Trait which all CSS property types should implement. pub trait Property { /// Whether the property's computed value inherits from parent to child elements. /// /// For each property, the CSS or SVG specs say whether the property inherits /// automatically. When a property is not specified in an element, the return value /// of this method determines whether the property's value is copied from the parent /// element (`true`), or whether it resets to the initial/default value (`false`). fn inherits_automatically() -> bool; /// Derive the CSS computed value from the parent element's /// [`ComputedValues`][crate::properties::ComputedValues] and the /// `self` value. /// /// The CSS or SVG specs say how to derive this for each property. fn compute(&self, _: &ComputedValues) -> Self; } /// Generates a type for a CSS property. /// /// Writing a property by hand takes a bit of boilerplate: /// /// * Define a type to represent the property's values. /// /// * A [`Parse`] implementation to parse the property. /// /// * A [`Default`] implementation to define the property's *initial* value. /// /// * A [`Property`] implementation to define whether the property /// inherits from the parent element, and how the property derives its /// computed value. /// /// When going from [`SpecifiedValues`] to [`ComputedValues`], /// properties which inherit automatically from the parent element /// will just have their values cloned. Properties which do not /// inherit will be reset back to their initial value (i.e. their /// [`Default`]). /// /// The default implementation of [`Property::compute()`] is to just /// clone the property's value. Properties which need more /// sophisticated computation can override this. /// /// This macro allows defining properties of different kinds; see the following /// sections for examples. /// /// # Simple identifiers /// /// Many properties are just sets of identifiers and can be represented /// by simple enums. In this case, you can use the following: /// /// ```text /// make_property!( /// /// Documentation here. /// StrokeLinejoin, /// default: Miter, /// inherits_automatically: true, /// /// identifiers: /// "miter" => Miter, /// "round" => Round, /// "bevel" => Bevel, /// ); /// ``` /// /// This generates a simple enum like the following, with implementations of [`Parse`], /// [`Default`], and [`Property`]. /// /// ``` /// pub enum StrokeLinejoin { Miter, Round, Bevel } /// ``` /// /// # Properties from an existing, general-purpose type /// /// For example, both the `lightingColor` and `floodColor` properties can be represented /// with a `cssparser::Color`, but their intial values are different. In this case, the macro /// can generate a newtype around `cssparser::Color` for each case: /// /// ```text /// make_property!( /// /// Documentation here. /// FloodColor, /// default: cssparser::Color::RGBA(cssparser::RGBA::new(0, 0, 0, 0)), /// inherits_automatically: false, /// newtype_parse: cssparser::Color, /// ); /// ``` /// /// # Properties from custom specific types /// /// For example, font-related properties have custom, complex types that require an /// implentation of `Property::compute` that is more than a simple `clone`. In this case, /// define the custom type separately, and use the macro to specify the default value and /// the `Property` implementation. /// /// [`Parse`]: crate::parsers::Parse /// [`Property`]: crate::property_macros::Property /// [`ComputedValues`]: crate::properties::ComputedValues /// [`SpecifiedValues`]: crate::properties::SpecifiedValues /// [`Property::compute()`]: crate::property_macros::Property::compute /// #[doc(hidden)] #[macro_export] macro_rules! make_property { ($(#[$attr:meta])* $name: ident, default: $default: ident, inherits_automatically: $inherits_automatically: expr, identifiers: $($str_prop: expr => $variant: ident,)+ ) => { $(#[$attr])* #[derive(Debug, Copy, Clone, PartialEq)] #[repr(C)] pub enum $name { $($variant),+ } impl_default!($name, $name::$default); impl_property!($name, $inherits_automatically); impl $crate::parsers::Parse for $name { fn parse<'i>(parser: &mut ::cssparser::Parser<'i, '_>) -> Result<$name, $crate::error::ParseError<'i>> { Ok(parse_identifiers!( parser, $($str_prop => $name::$variant,)+ )?) } } }; ($(#[$attr:meta])* $name: ident, default: $default: ident, identifiers: { $($str_prop: expr => $variant: ident,)+ }, property_impl: { $prop: item } ) => { $(#[$attr])* #[derive(Debug, Copy, Clone, PartialEq)] #[repr(C)] pub enum $name { $($variant),+ } impl_default!($name, $name::$default); $prop impl $crate::parsers::Parse for $name { fn parse<'i>(parser: &mut ::cssparser::Parser<'i, '_>) -> Result<$name, $crate::error::ParseError<'i>> { Ok(parse_identifiers!( parser, $($str_prop => $name::$variant,)+ )?) } } }; ($(#[$attr:meta])* $name: ident, default: $default: expr, inherits_automatically: $inherits_automatically: expr, newtype_parse: $type: ty, ) => { $(#[$attr])* #[derive(Debug, Clone, PartialEq)] pub struct $name(pub $type); impl_default!($name, $name($default)); impl_property!($name, $inherits_automatically); impl $crate::parsers::Parse for $name { fn parse<'i>(parser: &mut ::cssparser::Parser<'i, '_>) -> Result<$name, $crate::error::ParseError<'i>> { Ok($name(<$type as $crate::parsers::Parse>::parse(parser)?)) } } }; ($(#[$attr:meta])* $name: ident, default: $default: expr, property_impl: { $prop: item } ) => { impl_default!($name, $default); $prop }; ($name: ident, default: $default: expr, inherits_automatically: $inherits_automatically: expr, ) => { impl_default!($name, $default); impl_property!($name, $inherits_automatically); }; ($name: ident, default: $default: expr, inherits_automatically: $inherits_automatically: expr, parse_impl: { $parse: item } ) => { impl_default!($name, $default); impl_property!($name, $inherits_automatically); $parse }; ($(#[$attr:meta])* $name: ident, default: $default: expr, newtype: $type: ty, property_impl: { $prop: item }, parse_impl: { $parse: item } ) => { $(#[$attr])* #[derive(Debug, Clone, PartialEq)] pub struct $name(pub $type); impl_default!($name, $name($default)); $prop $parse }; // pending - only XmlLang ($(#[$attr:meta])* $name: ident, default: $default: expr, inherits_automatically: $inherits_automatically: expr, newtype: $type: ty, parse_impl: { $parse: item }, ) => { $(#[$attr])* #[derive(Debug, Clone, PartialEq)] pub struct $name(pub $type); impl_default!($name, $name($default)); impl_property!($name, $inherits_automatically); $parse }; ($(#[$attr:meta])* $name: ident, inherits_automatically: $inherits_automatically: expr, fields: { $($field_name: ident : $field_type: ty, default: $field_default : expr,)+ } parse_impl: { $parse: item } ) => { $(#[$attr])* #[derive(Debug, Clone, PartialEq)] pub struct $name { $(pub $field_name: $field_type),+ } impl_default!($name, $name { $($field_name: $field_default),+ }); impl_property!($name, $inherits_automatically); $parse }; } #[doc(hidden)] #[macro_export] macro_rules! impl_default { ($name:ident, $default:expr) => { impl Default for $name { fn default() -> $name { $default } } }; } #[doc(hidden)] #[macro_export] macro_rules! impl_property { ($name:ident, $inherits_automatically:expr) => { impl $crate::property_macros::Property for $name { fn inherits_automatically() -> bool { $inherits_automatically } fn compute(&self, _v: &$crate::properties::ComputedValues) -> Self { self.clone() } } }; } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/rect.rs��������������������������������������������������������������������������0000644�0000000�0000000�00000016235�10461020230�0013472�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Types for rectangles. use crate::coord_units::CoordUnits; use crate::transform::Transform; #[allow(clippy::module_inception)] mod rect { use crate::float_eq_cairo::ApproxEqCairo; use core::ops::{Add, Range, Sub}; use float_cmp::approx_eq; use num_traits::Zero; // Use our own min() and max() that are acceptable for floating point fn min(x: T, y: T) -> T { if x <= y { x } else { y } } fn max(x: T, y: T) -> T { if x >= y { x } else { y } } #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub struct Rect { pub x0: T, pub y0: T, pub x1: T, pub y1: T, } impl Rect { #[inline] pub fn new(x0: T, y0: T, x1: T, y1: T) -> Self { Self { x0, y0, x1, y1 } } } impl Rect where T: Copy + PartialOrd + PartialEq + Add + Sub + Zero, { #[inline] pub fn from_size(w: T, h: T) -> Self { Self { x0: Zero::zero(), y0: Zero::zero(), x1: w, y1: h, } } #[inline] pub fn width(&self) -> T { self.x1 - self.x0 } #[inline] pub fn height(&self) -> T { self.y1 - self.y0 } #[inline] pub fn size(&self) -> (T, T) { (self.width(), self.height()) } #[inline] pub fn x_range(&self) -> Range { self.x0..self.x1 } #[inline] pub fn y_range(&self) -> Range { self.y0..self.y1 } #[inline] pub fn contains(self, x: T, y: T) -> bool { x >= self.x0 && x < self.x1 && y >= self.y0 && y < self.y1 } #[inline] pub fn translate(&self, by: (T, T)) -> Self { Self { x0: self.x0 + by.0, y0: self.y0 + by.1, x1: self.x1 + by.0, y1: self.y1 + by.1, } } #[inline] pub fn intersection(&self, rect: &Self) -> Option { let (x0, y0, x1, y1) = ( max(self.x0, rect.x0), max(self.y0, rect.y0), min(self.x1, rect.x1), min(self.y1, rect.y1), ); if x1 > x0 && y1 > y0 { Some(Self { x0, y0, x1, y1 }) } else { None } } #[inline] pub fn union(&self, rect: &Self) -> Self { Self { x0: min(self.x0, rect.x0), y0: min(self.y0, rect.y0), x1: max(self.x1, rect.x1), y1: max(self.y1, rect.y1), } } } impl Rect { #[inline] pub fn is_empty(&self) -> bool { // Give an explicit type to the right hand side of the ==, since sometimes // type inference fails to figure it out. I have no idea why. self.width() == ::zero() || self.height() == ::zero() } #[inline] pub fn scale(self, x: f64, y: f64) -> Self { Self { x0: (f64::from(self.x0) * x).floor() as i32, y0: (f64::from(self.y0) * y).floor() as i32, x1: (f64::from(self.x1) * x).ceil() as i32, y1: (f64::from(self.y1) * y).ceil() as i32, } } } impl Rect { #[inline] pub fn is_empty(&self) -> bool { self.width().approx_eq_cairo(0.0) || self.height().approx_eq_cairo(0.0) } #[inline] pub fn scale(self, x: f64, y: f64) -> Self { Self { x0: self.x0 * x, y0: self.y0 * y, x1: self.x1 * x, y1: self.y1 * y, } } pub fn approx_eq(&self, other: &Self) -> bool { // FIXME: this is super fishy; shouldn't we be using 2x the epsilon against the width/height // instead of the raw coordinates? approx_eq!(f64, self.x0, other.x0, epsilon = 0.0001) && approx_eq!(f64, self.y0, other.y0, epsilon = 0.0001) && approx_eq!(f64, self.x1, other.x1, epsilon = 0.0001) && approx_eq!(f64, self.y1, other.y1, epsilon = 0.00012) } } } pub type Rect = rect::Rect; impl From for IRect { #[inline] fn from(r: Rect) -> Self { Self { x0: r.x0.floor() as i32, y0: r.y0.floor() as i32, x1: r.x1.ceil() as i32, y1: r.y1.ceil() as i32, } } } impl From for Rect { #[inline] fn from(r: cairo::Rectangle) -> Self { Self { x0: r.x(), y0: r.y(), x1: r.x() + r.width(), y1: r.y() + r.height(), } } } impl From for cairo::Rectangle { #[inline] fn from(r: Rect) -> Self { Self::new(r.x0, r.y0, r.x1 - r.x0, r.y1 - r.y0) } } /// Creates a transform to map to a rectangle. /// /// The rectangle is an `Option` to indicate the possibility that there is no /// bounding box from where the rectangle could be obtained. /// /// This depends on a `CoordUnits` parameter. When this is /// `CoordUnits::ObjectBoundingBox`, the bounding box must not be empty, since the calling /// code would then not have a usable size to work with. In that case, if the bbox is /// empty, this function returns `Err(())`. /// /// Usually calling code can simply ignore the action it was about to take if this /// function returns an error. pub fn rect_to_transform(rect: &Option, units: CoordUnits) -> Result { match units { CoordUnits::UserSpaceOnUse => Ok(Transform::identity()), CoordUnits::ObjectBoundingBox => { if rect.as_ref().map_or(true, |r| r.is_empty()) { Err(()) } else { let r = rect.as_ref().unwrap(); let t = Transform::new_unchecked(r.width(), 0.0, 0.0, r.height(), r.x0, r.y0); if t.is_invertible() { Ok(t) } else { Err(()) } } } } } pub type IRect = rect::Rect; impl From for Rect { #[inline] fn from(r: IRect) -> Self { Self { x0: f64::from(r.x0), y0: f64::from(r.y0), x1: f64::from(r.x1), y1: f64::from(r.y1), } } } impl From for IRect { #[inline] fn from(r: cairo::Rectangle) -> Self { Self { x0: r.x().floor() as i32, y0: r.y().floor() as i32, x1: (r.x() + r.width()).ceil() as i32, y1: (r.y() + r.height()).ceil() as i32, } } } impl From for cairo::Rectangle { #[inline] fn from(r: IRect) -> Self { Self::new( f64::from(r.x0), f64::from(r.y0), f64::from(r.x1 - r.x0), f64::from(r.y1 - r.y0), ) } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/session.rs�����������������������������������������������������������������������0000644�0000000�0000000�00000002026�10461020230�0014211�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Tracks metadata for a loading/rendering session. use std::sync::Arc; /// Metadata for a loading/rendering session. /// /// When the calling program first uses one of the API entry points (e.g. `Loader::new()` /// in the Rust API, or `rsvg_handle_new()` in the C API), there is no context yet where /// librsvg's code may start to track things. This struct provides that context. #[derive(Clone)] pub struct Session { inner: Arc, } struct SessionInner { log_enabled: bool, } fn log_enabled_via_env_var() -> bool { ::std::env::var_os("RSVG_LOG").is_some() } impl Default for Session { fn default() -> Self { Self { inner: Arc::new(SessionInner { log_enabled: log_enabled_via_env_var(), }), } } } impl Session { #[cfg(test)] pub fn new_for_test_suite() -> Self { Self { inner: Arc::new(SessionInner { log_enabled: false }), } } pub fn log_enabled(&self) -> bool { self.inner.log_enabled } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/shapes.rs������������������������������������������������������������������������0000644�0000000�0000000�00000047576�10461020230�0014034�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Basic SVG shapes: the `path`, `polygon`, `polyline`, `line`, //! `rect`, `circle`, `ellipse` elements. use cssparser::{Parser, Token}; use markup5ever::{expanded_name, local_name, namespace_url, ns}; use std::ops::Deref; use std::rc::Rc; use crate::bbox::BoundingBox; use crate::document::AcquiredNodes; use crate::drawing_ctx::{compute_path_extents, DrawingCtx, Viewport}; use crate::element::{set_attribute, ElementTrait}; use crate::error::*; use crate::iri::Iri; use crate::is_element_of_type; use crate::layout::{Layer, LayerKind, Marker, Shape, StackingContext, Stroke}; use crate::length::*; use crate::node::{CascadedValues, Node, NodeBorrow}; use crate::parsers::{optional_comma, Parse, ParseValue}; use crate::path_builder::{LargeArc, Path as SvgPath, PathBuilder, Sweep}; use crate::properties::ComputedValues; use crate::rsvg_log; use crate::session::Session; use crate::xml::Attributes; #[derive(PartialEq)] enum Markers { No, Yes, } struct ShapeDef { path: Rc, markers: Markers, } impl ShapeDef { fn new(path: Rc, markers: Markers) -> ShapeDef { ShapeDef { path, markers } } } trait BasicShape { fn make_shape(&self, params: &NormalizeParams, values: &ComputedValues) -> ShapeDef; } fn draw_basic_shape( basic_shape: &dyn BasicShape, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, session: &Session, ) -> Result { let values = cascaded.get(); let params = NormalizeParams::new(values, viewport); let shape_def = basic_shape.make_shape(¶ms, values); let is_visible = values.is_visible(); let paint_order = values.paint_order(); let stroke = Stroke::new(values, ¶ms); let stroke_paint = values.stroke().0.resolve( acquired_nodes, values.stroke_opacity().0, values.color().0, cascaded.context_fill.clone(), cascaded.context_stroke.clone(), session, ); let fill_paint = values.fill().0.resolve( acquired_nodes, values.fill_opacity().0, values.color().0, cascaded.context_fill.clone(), cascaded.context_stroke.clone(), session, ); let fill_rule = values.fill_rule(); let clip_rule = values.clip_rule(); let shape_rendering = values.shape_rendering(); let marker_start_node; let marker_mid_node; let marker_end_node; if shape_def.markers == Markers::Yes { marker_start_node = acquire_marker(session, acquired_nodes, &values.marker_start().0); marker_mid_node = acquire_marker(session, acquired_nodes, &values.marker_mid().0); marker_end_node = acquire_marker(session, acquired_nodes, &values.marker_end().0); } else { marker_start_node = None; marker_mid_node = None; marker_end_node = None; } let marker_start = Marker { node_ref: marker_start_node, context_stroke: stroke_paint.clone(), context_fill: fill_paint.clone(), }; let marker_mid = Marker { node_ref: marker_mid_node, context_stroke: stroke_paint.clone(), context_fill: fill_paint.clone(), }; let marker_end = Marker { node_ref: marker_end_node, context_stroke: stroke_paint.clone(), context_fill: fill_paint.clone(), }; let extents = compute_path_extents(&shape_def.path)?; let normalize_values = NormalizeValues::new(values); let stroke_paint = stroke_paint.to_user_space(&extents, viewport, &normalize_values); let fill_paint = fill_paint.to_user_space(&extents, viewport, &normalize_values); let shape = Box::new(Shape { path: shape_def.path, extents, is_visible, paint_order, stroke, stroke_paint, fill_paint, fill_rule, clip_rule, shape_rendering, marker_start, marker_mid, marker_end, }); let elt = node.borrow_element(); let stacking_ctx = StackingContext::new( session, acquired_nodes, &elt, values.transform(), None, values, ); Ok(Layer { kind: LayerKind::Shape(shape), stacking_ctx, }) } macro_rules! impl_draw { () => { fn layout( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, _clipping: bool, ) -> Result, InternalRenderingError> { draw_basic_shape( self, node, acquired_nodes, cascaded, viewport, &draw_ctx.session().clone(), ) .map(Some) } fn draw( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, clipping: bool, ) -> Result { self.layout(node, acquired_nodes, cascaded, viewport, draw_ctx, clipping) .and_then(|layer| { draw_ctx.draw_layer(layer.as_ref().unwrap(), acquired_nodes, clipping, viewport) }) } }; } fn acquire_marker( session: &Session, acquired_nodes: &mut AcquiredNodes<'_>, iri: &Iri, ) -> Option { iri.get().and_then(|id| { acquired_nodes .acquire(id) .map_err(|e| { rsvg_log!(session, "cannot render marker: {}", e); }) .ok() .and_then(|acquired| { let node = acquired.get(); if is_element_of_type!(node, Marker) { Some(node.clone()) } else { rsvg_log!(session, "{} is not a marker element", id); None } }) }) } fn make_ellipse(cx: f64, cy: f64, rx: f64, ry: f64) -> SvgPath { let mut builder = PathBuilder::default(); // Per the spec, rx and ry must be nonnegative if rx <= 0.0 || ry <= 0.0 { return builder.into_path(); } // 4/3 * (1-cos 45°)/sin 45° = 4/3 * sqrt(2) - 1 let arc_magic: f64 = 0.5522847498; // approximate an ellipse using 4 Bézier curves builder.move_to(cx + rx, cy); builder.curve_to( cx + rx, cy + arc_magic * ry, cx + arc_magic * rx, cy + ry, cx, cy + ry, ); builder.curve_to( cx - arc_magic * rx, cy + ry, cx - rx, cy + arc_magic * ry, cx - rx, cy, ); builder.curve_to( cx - rx, cy - arc_magic * ry, cx - arc_magic * rx, cy - ry, cx, cy - ry, ); builder.curve_to( cx + arc_magic * rx, cy - ry, cx + rx, cy - arc_magic * ry, cx + rx, cy, ); builder.close_path(); builder.into_path() } #[derive(Default)] pub struct Path { path: Rc, } impl ElementTrait for Path { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { if attr.expanded() == expanded_name!("", "d") { let mut builder = PathBuilder::default(); if let Err(e) = builder.parse(value) { // Creating a partial path is OK per the spec; we don't throw away the partial // result in case of an error. rsvg_log!(session, "could not parse path: {}", e); } self.path = Rc::new(builder.into_path()); } } } impl_draw!(); } impl BasicShape for Path { fn make_shape(&self, _params: &NormalizeParams, _values: &ComputedValues) -> ShapeDef { ShapeDef::new(self.path.clone(), Markers::Yes) } } /// List-of-points for polyline and polygon elements. /// /// SVG1.1: /// /// SVG2: #[derive(Debug, Default, PartialEq)] struct Points(Vec<(f64, f64)>); impl Deref for Points { type Target = [(f64, f64)]; fn deref(&self) -> &[(f64, f64)] { &self.0 } } impl Parse for Points { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let mut v = Vec::new(); loop { let x = f64::parse(parser)?; optional_comma(parser); let y = f64::parse(parser)?; v.push((x, y)); if parser.is_exhausted() { break; } match parser.next_including_whitespace() { Ok(&Token::WhiteSpace(_)) => (), _ => optional_comma(parser), } } Ok(Points(v)) } } fn make_poly(points: &Points, closed: bool) -> SvgPath { let mut builder = PathBuilder::default(); for (i, &(x, y)) in points.iter().enumerate() { if i == 0 { builder.move_to(x, y); } else { builder.line_to(x, y); } } if closed && !points.is_empty() { builder.close_path(); } builder.into_path() } #[derive(Default)] pub struct Polygon { points: Points, } impl ElementTrait for Polygon { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { if attr.expanded() == expanded_name!("", "points") { set_attribute(&mut self.points, attr.parse(value), session); } } } impl_draw!(); } impl BasicShape for Polygon { fn make_shape(&self, _params: &NormalizeParams, _values: &ComputedValues) -> ShapeDef { ShapeDef::new(Rc::new(make_poly(&self.points, true)), Markers::Yes) } } #[derive(Default)] pub struct Polyline { points: Points, } impl ElementTrait for Polyline { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { if attr.expanded() == expanded_name!("", "points") { set_attribute(&mut self.points, attr.parse(value), session); } } } impl_draw!(); } impl BasicShape for Polyline { fn make_shape(&self, _params: &NormalizeParams, _values: &ComputedValues) -> ShapeDef { ShapeDef::new(Rc::new(make_poly(&self.points, false)), Markers::Yes) } } #[derive(Default)] pub struct Line { x1: Length, y1: Length, x2: Length, y2: Length, } impl ElementTrait for Line { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "x1") => set_attribute(&mut self.x1, attr.parse(value), session), expanded_name!("", "y1") => set_attribute(&mut self.y1, attr.parse(value), session), expanded_name!("", "x2") => set_attribute(&mut self.x2, attr.parse(value), session), expanded_name!("", "y2") => set_attribute(&mut self.y2, attr.parse(value), session), _ => (), } } } impl_draw!(); } impl BasicShape for Line { fn make_shape(&self, params: &NormalizeParams, _values: &ComputedValues) -> ShapeDef { let mut builder = PathBuilder::default(); let x1 = self.x1.to_user(params); let y1 = self.y1.to_user(params); let x2 = self.x2.to_user(params); let y2 = self.y2.to_user(params); builder.move_to(x1, y1); builder.line_to(x2, y2); ShapeDef::new(Rc::new(builder.into_path()), Markers::Yes) } } /// The `` element. /// /// Note that its x/y/width/height/rx/ry are properties in SVG2, so they are /// defined as part of [the properties machinery](properties.rs). #[derive(Default)] pub struct Rect {} impl ElementTrait for Rect { impl_draw!(); } impl BasicShape for Rect { #[allow(clippy::many_single_char_names)] fn make_shape(&self, params: &NormalizeParams, values: &ComputedValues) -> ShapeDef { let x = values.x().0.to_user(params); let y = values.y().0.to_user(params); let w = match values.width().0 { LengthOrAuto::Length(l) => l.to_user(params), LengthOrAuto::Auto => 0.0, }; let h = match values.height().0 { LengthOrAuto::Length(l) => l.to_user(params), LengthOrAuto::Auto => 0.0, }; let norm_rx = match values.rx().0 { LengthOrAuto::Length(l) => Some(l.to_user(params)), LengthOrAuto::Auto => None, }; let norm_ry = match values.ry().0 { LengthOrAuto::Length(l) => Some(l.to_user(params)), LengthOrAuto::Auto => None, }; let mut rx; let mut ry; match (norm_rx, norm_ry) { (None, None) => { rx = 0.0; ry = 0.0; } (Some(_rx), None) => { rx = _rx; ry = _rx; } (None, Some(_ry)) => { rx = _ry; ry = _ry; } (Some(_rx), Some(_ry)) => { rx = _rx; ry = _ry; } } let mut builder = PathBuilder::default(); // Per the spec, w,h must be >= 0 if w <= 0.0 || h <= 0.0 { return ShapeDef::new(Rc::new(builder.into_path()), Markers::No); } let half_w = w / 2.0; let half_h = h / 2.0; if rx > half_w { rx = half_w; } if ry > half_h { ry = half_h; } if rx == 0.0 { ry = 0.0; } else if ry == 0.0 { rx = 0.0; } if rx == 0.0 { // Easy case, no rounded corners builder.move_to(x, y); builder.line_to(x + w, y); builder.line_to(x + w, y + h); builder.line_to(x, y + h); builder.line_to(x, y); } else { /* Hard case, rounded corners * * (top_x1, top_y) (top_x2, top_y) * *--------------------------------* * / \ * * (left_x, left_y1) * (right_x, right_y1) * | | * | | * | | * | | * | | * | | * | | * | | * | | * * (left_x, left_y2) * (right_x, right_y2) * \ / * *--------------------------------* * (bottom_x1, bottom_y) (bottom_x2, bottom_y) */ let top_x1 = x + rx; let top_x2 = x + w - rx; let top_y = y; let bottom_x1 = top_x1; let bottom_x2 = top_x2; let bottom_y = y + h; let left_x = x; let left_y1 = y + ry; let left_y2 = y + h - ry; let right_x = x + w; let right_y1 = left_y1; let right_y2 = left_y2; builder.move_to(top_x1, top_y); builder.line_to(top_x2, top_y); builder.arc( top_x2, top_y, rx, ry, 0.0, LargeArc(false), Sweep::Positive, right_x, right_y1, ); builder.line_to(right_x, right_y2); builder.arc( right_x, right_y2, rx, ry, 0.0, LargeArc(false), Sweep::Positive, bottom_x2, bottom_y, ); builder.line_to(bottom_x1, bottom_y); builder.arc( bottom_x1, bottom_y, rx, ry, 0.0, LargeArc(false), Sweep::Positive, left_x, left_y2, ); builder.line_to(left_x, left_y1); builder.arc( left_x, left_y1, rx, ry, 0.0, LargeArc(false), Sweep::Positive, top_x1, top_y, ); } builder.close_path(); ShapeDef::new(Rc::new(builder.into_path()), Markers::No) } } /// The `` element. /// /// Note that its cx/cy/r are properties in SVG2, so they are /// defined as part of [the properties machinery](properties.rs). #[derive(Default)] pub struct Circle {} impl ElementTrait for Circle { impl_draw!(); } impl BasicShape for Circle { fn make_shape(&self, params: &NormalizeParams, values: &ComputedValues) -> ShapeDef { let cx = values.cx().0.to_user(params); let cy = values.cy().0.to_user(params); let r = values.r().0.to_user(params); ShapeDef::new(Rc::new(make_ellipse(cx, cy, r, r)), Markers::No) } } /// The `` element. /// /// Note that its cx/cy/rx/ry are properties in SVG2, so they are /// defined as part of [the properties machinery](properties.rs). #[derive(Default)] pub struct Ellipse {} impl ElementTrait for Ellipse { impl_draw!(); } impl BasicShape for Ellipse { fn make_shape(&self, params: &NormalizeParams, values: &ComputedValues) -> ShapeDef { let cx = values.cx().0.to_user(params); let cy = values.cy().0.to_user(params); let norm_rx = match values.rx().0 { LengthOrAuto::Length(l) => Some(l.to_user(params)), LengthOrAuto::Auto => None, }; let norm_ry = match values.ry().0 { LengthOrAuto::Length(l) => Some(l.to_user(params)), LengthOrAuto::Auto => None, }; let rx; let ry; match (norm_rx, norm_ry) { (None, None) => { rx = 0.0; ry = 0.0; } (Some(_rx), None) => { rx = _rx; ry = _rx; } (None, Some(_ry)) => { rx = _ry; ry = _ry; } (Some(_rx), Some(_ry)) => { rx = _rx; ry = _ry; } } ShapeDef::new(Rc::new(make_ellipse(cx, cy, rx, ry)), Markers::No) } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_points() { assert_eq!( Points::parse_str(" 1 2 ").unwrap(), Points(vec![(1.0, 2.0)]) ); assert_eq!( Points::parse_str("1 2 3 4").unwrap(), Points(vec![(1.0, 2.0), (3.0, 4.0)]) ); assert_eq!( Points::parse_str("1,2,3,4").unwrap(), Points(vec![(1.0, 2.0), (3.0, 4.0)]) ); assert_eq!( Points::parse_str("1,2 3,4").unwrap(), Points(vec![(1.0, 2.0), (3.0, 4.0)]) ); assert_eq!( Points::parse_str("1,2 -3,4").unwrap(), Points(vec![(1.0, 2.0), (-3.0, 4.0)]) ); assert_eq!( Points::parse_str("1,2,-3,4").unwrap(), Points(vec![(1.0, 2.0), (-3.0, 4.0)]) ); } #[test] fn errors_on_invalid_points() { assert!(Points::parse_str("-1-2-3-4").is_err()); assert!(Points::parse_str("1 2-3,-4").is_err()); } } ����������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/space.rs�������������������������������������������������������������������������0000644�0000000�0000000�00000013271�10461020230�0013625�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Processing of the `xml:space` attribute. use itertools::Itertools; pub struct NormalizeDefault { pub has_element_before: bool, pub has_element_after: bool, } pub enum XmlSpaceNormalize { Default(NormalizeDefault), Preserve, } /// Implements `xml:space` handling per the SVG spec /// /// Normalizes a string as it comes out of the XML parser's handler /// for character data according to the SVG rules in /// pub fn xml_space_normalize(mode: XmlSpaceNormalize, s: &str) -> String { match mode { XmlSpaceNormalize::Default(d) => normalize_default(d, s), XmlSpaceNormalize::Preserve => normalize_preserve(s), } } // From https://www.w3.org/TR/SVG/text.html#WhiteSpace // // When xml:space="default", the SVG user agent will do the following // using a copy of the original character data content. First, it will // remove all newline characters. Then it will convert all tab // characters into space characters. Then, it will strip off all // leading and trailing space characters. Then, all contiguous space // characters will be consolidated. fn normalize_default(elements: NormalizeDefault, mut s: &str) -> String { if !elements.has_element_before { s = s.trim_start(); } if !elements.has_element_after { s = s.trim_end(); } s.chars() .filter(|ch| *ch != '\n') .map(|ch| match ch { '\t' => ' ', c => c, }) .coalesce(|current, next| match (current, next) { (' ', ' ') => Ok(' '), (_, _) => Err((current, next)), }) .collect::() } // From https://www.w3.org/TR/SVG/text.html#WhiteSpace // // When xml:space="preserve", the SVG user agent will do the following // using a copy of the original character data content. It will // convert all newline and tab characters into space characters. Then, // it will draw all space characters, including leading, trailing and // multiple contiguous space characters. Thus, when drawn with // xml:space="preserve", the string "a b" (three spaces between "a" // and "b") will produce a larger separation between "a" and "b" than // "a b" (one space between "a" and "b"). fn normalize_preserve(s: &str) -> String { s.chars() .map(|ch| match ch { '\n' | '\t' => ' ', c => c, }) .collect() } #[cfg(test)] mod tests { use super::*; #[test] fn xml_space_default() { assert_eq!( xml_space_normalize( XmlSpaceNormalize::Default(NormalizeDefault { has_element_before: false, has_element_after: false, }), "\n WS example\n indented lines\n " ), "WS example indented lines" ); assert_eq!( xml_space_normalize( XmlSpaceNormalize::Default(NormalizeDefault { has_element_before: false, has_element_after: false, }), "\n \t \tWS \t\t\texample\n \t indented lines\t\t \n " ), "WS example indented lines" ); assert_eq!( xml_space_normalize( XmlSpaceNormalize::Default(NormalizeDefault { has_element_before: false, has_element_after: false, }), "\n \t \tWS \t\t\texample\n \t duplicate letters\t\t \n " ), "WS example duplicate letters" ); assert_eq!( xml_space_normalize( XmlSpaceNormalize::Default(NormalizeDefault { has_element_before: false, has_element_after: false, }), "\nWS example\nnon-indented lines\n " ), "WS examplenon-indented lines" ); assert_eq!( xml_space_normalize( XmlSpaceNormalize::Default(NormalizeDefault { has_element_before: false, has_element_after: false, }), "\nWS example\tnon-indented lines\n " ), "WS example non-indented lines" ); } #[test] fn xml_space_default_with_elements() { assert_eq!( xml_space_normalize( XmlSpaceNormalize::Default(NormalizeDefault { has_element_before: true, has_element_after: false, }), " foo \n\t bar " ), " foo bar" ); assert_eq!( xml_space_normalize( XmlSpaceNormalize::Default(NormalizeDefault { has_element_before: false, has_element_after: true, }), " foo \nbar " ), "foo bar " ); } #[test] fn xml_space_preserve() { assert_eq!( xml_space_normalize( XmlSpaceNormalize::Preserve, "\n WS example\n indented lines\n " ), " WS example indented lines " ); assert_eq!( xml_space_normalize( XmlSpaceNormalize::Preserve, "\n \t \tWS \t\t\texample\n \t indented lines\t\t \n " ), " WS example indented lines " ); assert_eq!( xml_space_normalize( XmlSpaceNormalize::Preserve, "\n \t \tWS \t\t\texample\n \t duplicate letters\t\t \n " ), " WS example duplicate letters " ); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������librsvg-2.59.0/src/structure.rs���������������������������������������������������������������������0000644�0000000�0000000�00000046640�10461020230�0014600�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Structural elements in SVG: the `g`, `switch`, `svg`, `use`, `symbol`, `clip_path`, `mask`, `link` elements. use markup5ever::{expanded_name, local_name, namespace_url, ns}; use crate::aspect_ratio::*; use crate::bbox::BoundingBox; use crate::coord_units; use crate::coord_units::CoordUnits; use crate::document::{AcquiredNodes, NodeId}; use crate::drawing_ctx::{DrawingCtx, SvgNesting, Viewport}; use crate::element::{set_attribute, ElementData, ElementTrait}; use crate::error::*; use crate::href::{is_href, set_href}; use crate::layout::{LayoutViewport, StackingContext}; use crate::length::*; use crate::node::{CascadedValues, Node, NodeBorrow, NodeDraw}; use crate::parsers::{Parse, ParseValue}; use crate::properties::ComputedValues; use crate::rect::Rect; use crate::session::Session; use crate::viewbox::*; use crate::xml::Attributes; #[derive(Default)] pub struct Group(); impl ElementTrait for Group { fn draw( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, clipping: bool, ) -> Result { let values = cascaded.get(); let elt = node.borrow_element(); let stacking_ctx = StackingContext::new( draw_ctx.session(), acquired_nodes, &elt, values.transform(), None, values, ); draw_ctx.with_discrete_layer( &stacking_ctx, acquired_nodes, viewport, None, clipping, &mut |an, dc, new_viewport| { node.draw_children(an, cascaded, new_viewport, dc, clipping) }, ) } } /// A no-op node that does not render anything /// /// Sometimes we just need a node that can contain children, but doesn't /// render itself or its children. This is just that kind of node. #[derive(Default)] pub struct NonRendering; impl ElementTrait for NonRendering {} /// The `` element. #[derive(Default)] pub struct Switch(); impl ElementTrait for Switch { fn draw( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, clipping: bool, ) -> Result { let values = cascaded.get(); let elt = node.borrow_element(); let stacking_ctx = StackingContext::new( draw_ctx.session(), acquired_nodes, &elt, values.transform(), None, values, ); draw_ctx.with_discrete_layer( &stacking_ctx, acquired_nodes, viewport, None, clipping, &mut |an, dc, new_viewport| { if let Some(child) = node.children().filter(|c| c.is_element()).find(|c| { let elt = c.borrow_element(); elt.get_cond(dc.user_language()) }) { child.draw( an, &CascadedValues::clone_with_node(cascaded, &child), new_viewport, dc, clipping, ) } else { Ok(dc.empty_bbox()) } }, ) } } /// Intrinsic dimensions of an SVG document fragment: its `width/height` properties and `viewBox` attribute. /// /// Note that in SVG2, `width` and `height` are properties, not /// attributes. If either is omitted, it defaults to `auto`. which /// computes to `100%`. /// /// The `viewBox` attribute can also be omitted, hence an `Option`. #[derive(Debug, Copy, Clone, PartialEq)] pub struct IntrinsicDimensions { /// Computed value of the `width` property. pub width: ULength, /// Computed value of the `height` property. pub height: ULength, /// Contents of the `viewBox` attribute. pub vbox: Option, } /// The `` element. /// /// Note that its x/y/width/height are properties in SVG2, so they are /// defined as part of [the properties machinery](properties.rs). #[derive(Default)] pub struct Svg { preserve_aspect_ratio: AspectRatio, vbox: Option, } impl Svg { pub fn get_intrinsic_dimensions(&self, values: &ComputedValues) -> IntrinsicDimensions { let w = match values.width().0 { LengthOrAuto::Auto => ULength::::parse_str("100%").unwrap(), LengthOrAuto::Length(l) => l, }; let h = match values.height().0 { LengthOrAuto::Auto => ULength::::parse_str("100%").unwrap(), LengthOrAuto::Length(l) => l, }; IntrinsicDimensions { width: w, height: h, vbox: self.vbox, } } fn get_unnormalized_offset( &self, values: &ComputedValues, ) -> (Length, Length) { // these defaults are per the spec let x = values.x().0; let y = values.y().0; (x, y) } fn get_unnormalized_size( &self, values: &ComputedValues, ) -> (ULength, ULength) { // these defaults are per the spec let w = match values.width().0 { LengthOrAuto::Auto => ULength::::parse_str("100%").unwrap(), LengthOrAuto::Length(l) => l, }; let h = match values.height().0 { LengthOrAuto::Auto => ULength::::parse_str("100%").unwrap(), LengthOrAuto::Length(l) => l, }; (w, h) } fn get_viewport( &self, params: &NormalizeParams, values: &ComputedValues, outermost: bool, ) -> Rect { // x & y attributes have no effect on outermost svg // http://www.w3.org/TR/SVG/struct.html#SVGElement let (nx, ny) = if outermost { (0.0, 0.0) } else { let (x, y) = self.get_unnormalized_offset(values); (x.to_user(params), y.to_user(params)) }; let (w, h) = self.get_unnormalized_size(values); let (nw, nh) = (w.to_user(params), h.to_user(params)); Rect::new(nx, ny, nx + nw, ny + nh) } pub fn get_viewbox(&self) -> Option { self.vbox } pub fn get_preserve_aspect_ratio(&self) -> AspectRatio { self.preserve_aspect_ratio } fn make_svg_viewport( &self, node: &Node, cascaded: &CascadedValues<'_>, current_viewport: &Viewport, draw_ctx: &mut DrawingCtx, ) -> LayoutViewport { let values = cascaded.get(); let params = NormalizeParams::new(values, current_viewport); let has_parent = node.parent().is_some(); // From https://www.w3.org/TR/SVG2/embedded.html#ImageElement: // // For `image` elements embedding an SVG image, the `preserveAspectRatio` // attribute on the root element in the referenced SVG image must be ignored, // and instead treated as if it had a value of `none`. (see // `preserveAspectRatio` for details). This ensures that the // `preserveAspectRatio` attribute on the referencing `image` has its // intended effect, even if it is none. // let preserve_aspect_ratio = match (has_parent, draw_ctx.svg_nesting()) { // we are a toplevel, and referenced from => preserveAspectRatio=none (false, SvgNesting::ReferencedFromImageElement) => AspectRatio::none(), // otherwise just use our specified preserveAspectRatio _ => self.preserve_aspect_ratio, }; let svg_viewport = self.get_viewport(¶ms, values, !has_parent); let is_measuring_toplevel_svg = !has_parent && draw_ctx.is_measuring(); let (geometry, vbox) = if is_measuring_toplevel_svg { // We are obtaining the toplevel SVG's geometry. This means, don't care about the // DrawingCtx's viewport, just use the SVG's intrinsic dimensions and see how far // it wants to extend. (svg_viewport, self.vbox) } else { ( // The client's viewport overrides the toplevel's x/y/w/h viewport if has_parent { svg_viewport } else { draw_ctx.toplevel_viewport() }, // Use our viewBox if available, or try to derive one from // the intrinsic dimensions. self.vbox.or_else(|| { Some(ViewBox::from(Rect::from_size( svg_viewport.width(), svg_viewport.height(), ))) }), ) }; LayoutViewport { geometry, vbox, preserve_aspect_ratio, overflow: values.overflow(), } } } impl ElementTrait for Svg { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "preserveAspectRatio") => { set_attribute(&mut self.preserve_aspect_ratio, attr.parse(value), session) } expanded_name!("", "viewBox") => { set_attribute(&mut self.vbox, attr.parse(value), session) } _ => (), } } } fn draw( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, clipping: bool, ) -> Result { let values = cascaded.get(); let elt = node.borrow_element(); let stacking_ctx = StackingContext::new( draw_ctx.session(), acquired_nodes, &elt, values.transform(), None, values, ); let layout_viewport = self.make_svg_viewport(node, cascaded, viewport, draw_ctx); draw_ctx.with_discrete_layer( &stacking_ctx, acquired_nodes, viewport, Some(layout_viewport), clipping, &mut |an, dc, new_viewport| { node.draw_children(an, cascaded, new_viewport, dc, clipping) }, ) } } /// The `` element. pub struct Use { link: Option, x: Length, y: Length, width: ULength, height: ULength, } impl Use { fn get_rect(&self, params: &NormalizeParams) -> Rect { let x = self.x.to_user(params); let y = self.y.to_user(params); let w = self.width.to_user(params); let h = self.height.to_user(params); Rect::new(x, y, x + w, y + h) } } impl Default for Use { fn default() -> Use { Use { link: None, x: Default::default(), y: Default::default(), width: ULength::::parse_str("100%").unwrap(), height: ULength::::parse_str("100%").unwrap(), } } } impl ElementTrait for Use { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { match attr.expanded() { ref a if is_href(a) => { let mut href = None; set_attribute( &mut href, NodeId::parse(value).map(Some).attribute(attr.clone()), session, ); set_href(a, &mut self.link, href); } expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session), expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session), expanded_name!("", "width") => { set_attribute(&mut self.width, attr.parse(value), session) } expanded_name!("", "height") => { set_attribute(&mut self.height, attr.parse(value), session) } _ => (), } } } fn draw( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, clipping: bool, ) -> Result { if let Some(link) = self.link.as_ref() { let values = cascaded.get(); let params = NormalizeParams::new(values, viewport); let rect = self.get_rect(¶ms); let stroke_paint = values.stroke().0.resolve( acquired_nodes, values.stroke_opacity().0, values.color().0, cascaded.context_fill.clone(), cascaded.context_stroke.clone(), draw_ctx.session(), ); let fill_paint = values.fill().0.resolve( acquired_nodes, values.fill_opacity().0, values.color().0, cascaded.context_fill.clone(), cascaded.context_stroke.clone(), draw_ctx.session(), ); draw_ctx.draw_from_use_node( node, acquired_nodes, values, rect, link, clipping, viewport, fill_paint, stroke_paint, ) } else { Ok(draw_ctx.empty_bbox()) } } } /// The `` element. #[derive(Default)] pub struct Symbol { preserve_aspect_ratio: AspectRatio, vbox: Option, } impl Symbol { pub fn get_viewbox(&self) -> Option { self.vbox } pub fn get_preserve_aspect_ratio(&self) -> AspectRatio { self.preserve_aspect_ratio } } impl ElementTrait for Symbol { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "preserveAspectRatio") => { set_attribute(&mut self.preserve_aspect_ratio, attr.parse(value), session) } expanded_name!("", "viewBox") => { set_attribute(&mut self.vbox, attr.parse(value), session) } _ => (), } } } } coord_units!(ClipPathUnits, CoordUnits::UserSpaceOnUse); /// The `` element. #[derive(Default)] pub struct ClipPath { units: ClipPathUnits, } impl ClipPath { pub fn get_units(&self) -> CoordUnits { CoordUnits::from(self.units) } } impl ElementTrait for ClipPath { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { if attr.expanded() == expanded_name!("", "clipPathUnits") { set_attribute(&mut self.units, attr.parse(value), session); } } } } coord_units!(MaskUnits, CoordUnits::ObjectBoundingBox); coord_units!(MaskContentUnits, CoordUnits::UserSpaceOnUse); /// The `` element. pub struct Mask { x: Length, y: Length, width: ULength, height: ULength, units: MaskUnits, content_units: MaskContentUnits, } impl Default for Mask { fn default() -> Mask { Mask { // these values are per the spec x: Length::::parse_str("-10%").unwrap(), y: Length::::parse_str("-10%").unwrap(), width: ULength::::parse_str("120%").unwrap(), height: ULength::::parse_str("120%").unwrap(), units: MaskUnits::default(), content_units: MaskContentUnits::default(), } } } impl Mask { pub fn get_units(&self) -> CoordUnits { CoordUnits::from(self.units) } pub fn get_content_units(&self) -> CoordUnits { CoordUnits::from(self.content_units) } pub fn get_rect(&self, params: &NormalizeParams) -> Rect { let x = self.x.to_user(params); let y = self.y.to_user(params); let w = self.width.to_user(params); let h = self.height.to_user(params); Rect::new(x, y, x + w, y + h) } } impl ElementTrait for Mask { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session), expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session), expanded_name!("", "width") => { set_attribute(&mut self.width, attr.parse(value), session) } expanded_name!("", "height") => { set_attribute(&mut self.height, attr.parse(value), session) } expanded_name!("", "maskUnits") => { set_attribute(&mut self.units, attr.parse(value), session) } expanded_name!("", "maskContentUnits") => { set_attribute(&mut self.content_units, attr.parse(value), session) } _ => (), } } } } /// The `` element. #[derive(Default)] pub struct Link { pub link: Option, } impl ElementTrait for Link { fn set_attributes(&mut self, attrs: &Attributes, _session: &Session) { for (attr, value) in attrs.iter() { let expanded = attr.expanded(); if is_href(&expanded) { set_href(&expanded, &mut self.link, Some(value.to_owned())); } } } fn draw( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, clipping: bool, ) -> Result { // If this element is inside of , do not draw it. // The takes care of it. for an in node.ancestors() { if matches!(&*an.borrow_element_data(), ElementData::Text(_)) { return Ok(draw_ctx.empty_bbox()); } } let cascaded = CascadedValues::clone_with_node(cascaded, node); let values = cascaded.get(); let elt = node.borrow_element(); let link_is_empty = self.link.as_ref().map(|l| l.is_empty()).unwrap_or(true); let link_target = if link_is_empty { None } else { self.link.clone() }; let stacking_ctx = StackingContext::new_with_link( draw_ctx.session(), acquired_nodes, &elt, values.transform(), values, link_target, ); draw_ctx.with_discrete_layer( &stacking_ctx, acquired_nodes, viewport, None, clipping, &mut |an, dc, new_viewport| { node.draw_children(an, &cascaded, new_viewport, dc, clipping) }, ) } } ������������������������������������������������������������������������������������������������librsvg-2.59.0/src/style.rs�������������������������������������������������������������������������0000644�0000000�0000000�00000004327�10461020230�0013674�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! The `style` element. use markup5ever::{expanded_name, local_name, namespace_url, ns}; use crate::element::{set_attribute, ElementTrait}; use crate::error::*; use crate::session::Session; use crate::xml::Attributes; /// Represents the syntax used in the `

(path: P) -> Self where P: AsRef, { let msg = format!("read reference PNG {}", path.as_ref().to_string_lossy()); let file = File::open(path).expect(&msg); let mut reader = BufReader::new(file); let surface = surface_from_png(&mut reader).expect("decode reference PNG"); Self::from_surface(surface) } pub fn from_surface(surface: cairo::ImageSurface) -> Self { let shared = SharedImageSurface::wrap(surface, SurfaceType::SRgb) .expect("wrap Cairo surface with SharedImageSurface"); Self(shared) } } pub trait Compare { fn compare(self, surface: &SharedImageSurface) -> Result; } impl Compare for &Reference { fn compare(self, surface: &SharedImageSurface) -> Result { compare_surfaces(&self.0, surface).map_err(cairo::IoError::from) } } pub trait Evaluate { fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str); } impl Evaluate for BufferDiff { /// Evaluates a BufferDiff and panics if there are relevant differences /// /// The `output_base_name` is used to write test results if the /// surfaces are different. If this is `foo`, this will write /// `foo-out.png` with the `output_surf` and `foo-diff.png` with a /// visual diff between `output_surf` and the `Reference` that this /// diff was created from. /// /// # Panics /// /// Will panic if the surfaces are too different to be acceptable. fn evaluate(&self, output_surf: &SharedImageSurface, output_base_name: &str) { match self { BufferDiff::DifferentSizes => unreachable!("surfaces should be of the same size"), BufferDiff::Diff(diff) => { if diff.distinguishable() { println!( "{}: {} pixels changed with maximum difference of {}", output_base_name, diff.num_pixels_changed, diff.max_diff, ); write_to_file(output_surf, output_base_name, "out"); write_to_file(&diff.surface, output_base_name, "diff"); if diff.inacceptable() { panic!("surfaces are too different"); } } } } } } impl Evaluate for Result { fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str) { self.as_ref() .map(|diff| diff.evaluate(output_surface, output_base_name)) .unwrap(); } } fn write_to_file(input: &SharedImageSurface, output_base_name: &str, suffix: &str) { let path = output_dir().join(format!("{}-{}.png", output_base_name, suffix)); println!("{}: {}", suffix, path.to_string_lossy()); let mut output_file = File::create(path).unwrap(); input .clone() .into_image_surface() .unwrap() .write_to_png(&mut output_file) .unwrap(); } /// Creates a directory for test output and returns its path. /// /// The location for the output directory is taken from the `TESTS_OUTPUT_DIR` environment /// variable if that is set. Otherwise std::env::temp_dir() will be used, which is /// a platform dependent location for temporary files. /// /// # Panics /// /// Will panic if the output directory can not be created. pub fn output_dir() -> PathBuf { let tempdir = || { let mut path = env::temp_dir(); path.push("rsvg-test-output"); path }; let path = env::var_os("TESTS_OUTPUT_DIR").map_or_else(tempdir, PathBuf::from); fs::create_dir_all(&path).expect("could not create output directory for tests"); path } fn tolerable_difference() -> u8 { static mut TOLERANCE: u8 = 8; static ONCE: Once = Once::new(); ONCE.call_once(|| unsafe { if let Ok(str) = env::var("RSVG_TEST_TOLERANCE") { let value: usize = str .parse() .expect("Can not parse RSVG_TEST_TOLERANCE as a number"); TOLERANCE = u8::try_from(value).expect("RSVG_TEST_TOLERANCE should be between 0 and 255"); } }); unsafe { TOLERANCE } } pub trait Deviation { fn distinguishable(&self) -> bool; fn inacceptable(&self) -> bool; } impl Deviation for Diff { fn distinguishable(&self) -> bool { self.max_diff > 2 } fn inacceptable(&self) -> bool { self.max_diff > tolerable_difference() } } /// Creates a cairo::ImageSurface from a stream of PNG data. /// /// The surface is converted to ARGB if needed. Use this helper function with `Reference`. pub fn surface_from_png(stream: &mut R) -> Result where R: Read, { let png = cairo::ImageSurface::create_from_png(stream)?; let argb = cairo::ImageSurface::create(cairo::Format::ARgb32, png.width(), png.height())?; { // convert to ARGB; the PNG may come as Rgb24 let cr = cairo::Context::new(&argb).expect("Failed to create a cairo context"); cr.set_source_surface(&png, 0.0, 0.0).unwrap(); cr.paint().unwrap(); } Ok(argb) } /// Macro test that compares render outputs /// /// Takes in SurfaceSize width and height, setting the cairo surface #[macro_export] macro_rules! test_compare_render_output { ($test_name:ident, $width:expr, $height:expr, $test:expr, $reference:expr $(,)?) => { #[test] fn $test_name() { $crate::test_utils::reference_utils::compare_render_output( stringify!($test_name), $width, $height, $test, $reference, ); } }; } pub fn compare_render_output( test_name: &str, width: i32, height: i32, test: &'static [u8], reference: &'static [u8], ) { setup_font_map(); let svg = load_svg(test).unwrap(); let output_surf = render_document( &svg, SurfaceSize(width, height), |_| (), cairo::Rectangle::new(0.0, 0.0, f64::from(width), f64::from(height)), ) .unwrap(); let reference = load_svg(reference).unwrap(); let reference_surf = render_document( &reference, SurfaceSize(width, height), |_| (), cairo::Rectangle::new(0.0, 0.0, f64::from(width), f64::from(height)), ) .unwrap(); Reference::from_surface(reference_surf.into_image_surface().unwrap()) .compare(&output_surf) .evaluate(&output_surf, test_name); } /// Render two SVG files and compare them. /// /// This is used to implement reference tests, or reftests. Use it like this: /// /// ```no_run /// use rsvg::test_svg_reference; /// test_svg_reference!(test_name, "tests/fixtures/blah/foo.svg", "tests/fixtures/blah/foo-ref.svg"); /// ``` /// /// This will ensure that `foo.svg` and `foo-ref.svg` have exactly the same intrinsic dimensions, /// and that they produce the same rendered output. #[macro_export] macro_rules! test_svg_reference { ($test_name:ident, $test_filename:expr, $reference_filename:expr) => { #[test] fn $test_name() { $crate::test_utils::reference_utils::svg_reference_test( stringify!($test_name), $test_filename, $reference_filename, ); } }; } pub fn svg_reference_test(test_name: &str, test_filename: &str, reference_filename: &str) { setup_font_map(); let svg = Loader::new() .read_path(test_filename) .expect("reading SVG test file"); let reference = Loader::new() .read_path(reference_filename) .expect("reading reference file"); let svg_renderer = CairoRenderer::new(&svg); let ref_renderer = CairoRenderer::new(&reference); let svg_dim = svg_renderer.intrinsic_dimensions(); let ref_dim = ref_renderer.intrinsic_dimensions(); assert_eq!( svg_dim, ref_dim, "sizes of SVG document and reference file are different" ); let pixels = svg_renderer .intrinsic_size_in_pixels() .unwrap_or((100.0, 100.0)); let output_surf = render_document( &svg, SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32), |_| (), cairo::Rectangle::new(0.0, 0.0, pixels.0, pixels.1), ) .unwrap(); let reference_surf = render_document( &reference, SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32), |_| (), cairo::Rectangle::new(0.0, 0.0, pixels.0, pixels.1), ) .unwrap(); Reference::from_surface(reference_surf.into_image_surface().unwrap()) .compare(&output_surf) .evaluate(&output_surf, test_name); } librsvg-2.59.0/src/text.rs000064400000000000000000001274071046102023000135250ustar 00000000000000//! Text elements: `text`, `tspan`, `tref`. use markup5ever::{expanded_name, local_name, namespace_url, ns}; use pango::IsAttribute; use std::cell::RefCell; use std::convert::TryFrom; use std::rc::Rc; use crate::bbox::BoundingBox; use crate::document::{AcquiredNodes, NodeId}; use crate::drawing_ctx::{create_pango_context, DrawingCtx, FontOptions, Viewport}; use crate::element::{set_attribute, ElementData, ElementTrait}; use crate::error::*; use crate::layout::{self, FontProperties, Layer, LayerKind, StackingContext, Stroke, TextSpan}; use crate::length::*; use crate::node::{CascadedValues, Node, NodeBorrow}; use crate::paint_server::PaintSource; use crate::parsers::ParseValue; use crate::properties::{ ComputedValues, Direction, FontStretch, FontStyle, FontVariant, FontWeight, PaintOrder, TextAnchor, TextRendering, UnicodeBidi, WritingMode, XmlLang, XmlSpace, }; use crate::rect::Rect; use crate::rsvg_log; use crate::session::Session; use crate::space::{xml_space_normalize, NormalizeDefault, XmlSpaceNormalize}; use crate::transform::{Transform, ValidTransform}; use crate::xml::Attributes; /// The state of a text layout operation. struct LayoutContext { /// `writing-mode` property from the `` element. writing_mode: WritingMode, /// Current transform in the DrawingCtx. transform: ValidTransform, /// Font options from the DrawingCtx. font_options: FontOptions, /// For normalizing lengths. viewport: Viewport, /// Session metadata for the document session: Session, } /// An absolutely-positioned array of `Span`s /// /// SVG defines a "[text chunk]" to occur when a text-related element /// has an absolute position adjustment, that is, `x` or `y` /// attributes. /// /// A `` element always starts with an absolute position from /// such attributes, or (0, 0) if they are not specified. /// /// Subsequent children of the `` element will create new chunks /// whenever they have `x` or `y` attributes. /// /// [text chunk]: https://www.w3.org/TR/SVG11/text.html#TextLayoutIntroduction struct Chunk { values: Rc, x: Option, y: Option, spans: Vec, } struct MeasuredChunk { values: Rc, x: Option, y: Option, dx: f64, dy: f64, spans: Vec, } struct PositionedChunk { next_chunk_x: f64, next_chunk_y: f64, spans: Vec, } struct Span { values: Rc, text: String, dx: f64, dy: f64, _depth: usize, link_target: Option, } struct MeasuredSpan { values: Rc, layout: pango::Layout, layout_size: (f64, f64), advance: (f64, f64), dx: f64, dy: f64, link_target: Option, } struct PositionedSpan { layout: pango::Layout, values: Rc, rendered_position: (f64, f64), next_span_position: (f64, f64), link_target: Option, } /// A laid-out and resolved text span. /// /// The only thing not in user-space units are the `stroke_paint` and `fill_paint`. /// /// This is the non-user-space version of `layout::TextSpan`. struct LayoutSpan { layout: pango::Layout, gravity: pango::Gravity, bbox: Option, is_visible: bool, x: f64, y: f64, paint_order: PaintOrder, stroke: Stroke, stroke_paint: Rc, fill_paint: Rc, text_rendering: TextRendering, link_target: Option, values: Rc, } impl Chunk { fn new(values: &ComputedValues, x: Option, y: Option) -> Chunk { Chunk { values: Rc::new(values.clone()), x, y, spans: Vec::new(), } } } impl MeasuredChunk { fn from_chunk(layout_context: &LayoutContext, chunk: &Chunk) -> MeasuredChunk { let mut measured_spans: Vec = chunk .spans .iter() .filter_map(|span| MeasuredSpan::from_span(layout_context, span)) .collect(); // The first span contains the (dx, dy) that will be applied to the whole chunk. // Make them 0 in the span, and extract the values to set them on the chunk. // This is a hack until librsvg adds support for multiple dx/dy values per text/tspan. let (chunk_dx, chunk_dy) = if let Some(first) = measured_spans.first_mut() { let dx = first.dx; let dy = first.dy; first.dx = 0.0; first.dy = 0.0; (dx, dy) } else { (0.0, 0.0) }; MeasuredChunk { values: chunk.values.clone(), x: chunk.x, y: chunk.y, dx: chunk_dx, dy: chunk_dy, spans: measured_spans, } } } impl PositionedChunk { fn from_measured( layout_context: &LayoutContext, measured: &MeasuredChunk, chunk_x: f64, chunk_y: f64, ) -> PositionedChunk { let chunk_direction = measured.values.direction(); // Position the spans relatively to each other, starting at (0, 0) let mut positioned = Vec::new(); // Start position of each span; gets advanced as each span is laid out. // This is the text's start position, not the bounding box. let mut x = 0.0; let mut y = 0.0; let mut chunk_bounds: Option = None; for mspan in &measured.spans { let params = NormalizeParams::new(&mspan.values, &layout_context.viewport); let layout = mspan.layout.clone(); let layout_size = mspan.layout_size; let values = mspan.values.clone(); let dx = mspan.dx; let dy = mspan.dy; let advance = mspan.advance; let baseline_offset = compute_baseline_offset(&layout, &values, ¶ms); let start_pos = match chunk_direction { Direction::Ltr => (x, y), Direction::Rtl => (x - advance.0, y), }; let span_advance = match chunk_direction { Direction::Ltr => (advance.0, advance.1), Direction::Rtl => (-advance.0, advance.1), }; let rendered_position = if layout_context.writing_mode.is_horizontal() { (start_pos.0 + dx, start_pos.1 - baseline_offset + dy) } else { (start_pos.0 + baseline_offset + dx, start_pos.1 + dy) }; let span_bounds = Rect::from_size(layout_size.0, layout_size.1).translate(rendered_position); if let Some(bounds) = chunk_bounds { chunk_bounds = Some(bounds.union(&span_bounds)); } else { chunk_bounds = Some(span_bounds); } x = x + span_advance.0 + dx; y = y + span_advance.1 + dy; let positioned_span = PositionedSpan { layout, values, rendered_position, next_span_position: (x, y), link_target: mspan.link_target.clone(), }; positioned.push(positioned_span); } // Compute the offsets needed to align the chunk per the text-anchor property (start, middle, end): let anchor_offset = text_anchor_offset( measured.values.text_anchor(), chunk_direction, layout_context.writing_mode, chunk_bounds.unwrap_or_default(), ); // Apply the text-anchor offset to each individually-positioned span, and compute the // start position of the next chunk. Also add in the chunk's dx/dy. let mut next_chunk_x = chunk_x; let mut next_chunk_y = chunk_y; for pspan in &mut positioned { // Add the chunk's position, plus the text-anchor offset, plus the chunk's dx/dy. // This last term is a hack until librsvg adds support for multiple dx/dy values per text/tspan; // see the corresponding part in MeasuredChunk::from_chunk(). pspan.rendered_position.0 += chunk_x + anchor_offset.0 + measured.dx; pspan.rendered_position.1 += chunk_y + anchor_offset.1 + measured.dy; next_chunk_x = chunk_x + pspan.next_span_position.0 + anchor_offset.0 + measured.dx; next_chunk_y = chunk_y + pspan.next_span_position.1 + anchor_offset.1 + measured.dy; } PositionedChunk { next_chunk_x, next_chunk_y, spans: positioned, } } } fn compute_baseline_offset( layout: &pango::Layout, values: &ComputedValues, params: &NormalizeParams, ) -> f64 { let baseline = f64::from(layout.baseline()) / f64::from(pango::SCALE); let baseline_shift = values.baseline_shift().0.to_user(params); baseline + baseline_shift } /// Computes the (x, y) offsets to be applied to spans after applying the text-anchor property (start, middle, end). #[rustfmt::skip] fn text_anchor_offset( anchor: TextAnchor, direction: Direction, writing_mode: WritingMode, chunk_bounds: Rect, ) -> (f64, f64) { let (w, h) = (chunk_bounds.width(), chunk_bounds.height()); let x0 = chunk_bounds.x0; if writing_mode.is_horizontal() { match (anchor, direction) { (TextAnchor::Start, Direction::Ltr) => (-x0, 0.0), (TextAnchor::Start, Direction::Rtl) => (-x0 - w, 0.0), (TextAnchor::Middle, Direction::Ltr) => (-x0 - w / 2.0, 0.0), (TextAnchor::Middle, Direction::Rtl) => (-x0 - w / 2.0, 0.0), (TextAnchor::End, Direction::Ltr) => (-x0 - w, 0.0), (TextAnchor::End, Direction::Rtl) => (-x0, 0.0), } } else { // FIXME: we don't deal with text direction for vertical text yet. match anchor { TextAnchor::Start => (0.0, 0.0), TextAnchor::Middle => (0.0, -h / 2.0), TextAnchor::End => (0.0, -h), } } } impl Span { fn new( text: &str, values: Rc, dx: f64, dy: f64, depth: usize, link_target: Option, ) -> Span { Span { values, text: text.to_string(), dx, dy, _depth: depth, link_target, } } } /// Use as `PangoUnits::from_pixels()` so that we can check for overflow. struct PangoUnits(i32); impl PangoUnits { fn from_pixels(v: f64) -> Option { // We want (v * f64::from(pango::SCALE) + 0.5) as i32 // // But check for overflow. cast::i32(v * f64::from(pango::SCALE) + 0.5) .ok() .map(PangoUnits) } } impl MeasuredSpan { fn from_span(layout_context: &LayoutContext, span: &Span) -> Option { let values = span.values.clone(); let params = NormalizeParams::new(&values, &layout_context.viewport); let properties = FontProperties::new(&values, ¶ms); let bidi_control = BidiControl::from_unicode_bidi_and_direction( properties.unicode_bidi, properties.direction, ); let with_control_chars = wrap_with_direction_control_chars(&span.text, &bidi_control); if let Some(layout) = create_pango_layout(layout_context, &properties, &with_control_chars) { let (w, h) = layout.size(); let w = f64::from(w) / f64::from(pango::SCALE); let h = f64::from(h) / f64::from(pango::SCALE); let advance = if layout_context.writing_mode.is_horizontal() { (w, 0.0) } else { (0.0, w) }; Some(MeasuredSpan { values, layout, layout_size: (w, h), advance, dx: span.dx, dy: span.dy, link_target: span.link_target.clone(), }) } else { None } } } // FIXME: should the pango crate provide this like PANGO_GRAVITY_IS_VERTICAL() ? fn gravity_is_vertical(gravity: pango::Gravity) -> bool { matches!(gravity, pango::Gravity::East | pango::Gravity::West) } fn compute_text_box( layout: &pango::Layout, x: f64, y: f64, transform: Transform, gravity: pango::Gravity, ) -> Option { #![allow(clippy::many_single_char_names)] let (ink, _) = layout.extents(); if ink.width() == 0 || ink.height() == 0 { return None; } let ink_x = f64::from(ink.x()); let ink_y = f64::from(ink.y()); let ink_width = f64::from(ink.width()); let ink_height = f64::from(ink.height()); let pango_scale = f64::from(pango::SCALE); let (x, y, w, h) = if gravity_is_vertical(gravity) { ( x + (ink_x - ink_height) / pango_scale, y + ink_y / pango_scale, ink_height / pango_scale, ink_width / pango_scale, ) } else { ( x + ink_x / pango_scale, y + ink_y / pango_scale, ink_width / pango_scale, ink_height / pango_scale, ) }; let r = Rect::new(x, y, x + w, y + h); let bbox = BoundingBox::new() .with_transform(transform) .with_rect(r) .with_ink_rect(r); Some(bbox) } impl PositionedSpan { fn layout( &self, layout_context: &LayoutContext, acquired_nodes: &mut AcquiredNodes<'_>, ) -> LayoutSpan { let params = NormalizeParams::new(&self.values, &layout_context.viewport); let layout = self.layout.clone(); let is_visible = self.values.is_visible(); let (x, y) = self.rendered_position; let stroke = Stroke::new(&self.values, ¶ms); let gravity = layout.context().gravity(); let bbox = compute_text_box(&layout, x, y, *layout_context.transform, gravity); let stroke_paint = self.values.stroke().0.resolve( acquired_nodes, self.values.stroke_opacity().0, self.values.color().0, None, None, &layout_context.session, ); let fill_paint = self.values.fill().0.resolve( acquired_nodes, self.values.fill_opacity().0, self.values.color().0, None, None, &layout_context.session, ); let paint_order = self.values.paint_order(); let text_rendering = self.values.text_rendering(); LayoutSpan { layout, gravity, bbox, is_visible, x, y, paint_order, stroke, stroke_paint, fill_paint, text_rendering, values: self.values.clone(), link_target: self.link_target.clone(), } } } /// Walks the children of a ``, ``, or `` element /// and appends chunks/spans from them into the specified `chunks` /// array. fn children_to_chunks( chunks: &mut Vec, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, layout_context: &LayoutContext, dx: f64, dy: f64, depth: usize, link: Option, ) { let mut dx = dx; let mut dy = dy; for child in node.children() { if child.is_chars() { let values = cascaded.get(); child.borrow_chars().to_chunks( &child, Rc::new(values.clone()), chunks, dx, dy, depth, link.clone(), ); } else { assert!(child.is_element()); match *child.borrow_element_data() { ElementData::TSpan(ref tspan) => { let cascaded = CascadedValues::clone_with_node(cascaded, &child); tspan.to_chunks( &child, acquired_nodes, &cascaded, layout_context, chunks, dx, dy, depth + 1, link.clone(), ); } ElementData::Link(ref link) => { // TSpan::default sets all offsets to 0, // which is what we want in links. // // FIXME: This is the only place in the code where an element's method (TSpan::to_chunks) // is called with a node that is not the element itself: here, `child` is a Link, not a TSpan. // // The code works because the `tspan` is dropped immediately after calling to_chunks and no // references are retained for it. let tspan = TSpan::default(); let cascaded = CascadedValues::clone_with_node(cascaded, &child); tspan.to_chunks( &child, acquired_nodes, &cascaded, layout_context, chunks, dx, dy, depth + 1, link.link.clone(), ); } ElementData::TRef(ref tref) => { let cascaded = CascadedValues::clone_with_node(cascaded, &child); tref.to_chunks( &child, acquired_nodes, &cascaded, chunks, depth + 1, layout_context, ); } _ => (), } } // After the first span, we don't need to carry over the parent's dx/dy. dx = 0.0; dy = 0.0; } } /// In SVG text elements, we use `Chars` to store character data. For example, /// an element like `Foo Bar` will be a `Text` with a single child, /// and the child will be a `Chars` with "Foo Bar" for its contents. /// /// Text elements can contain `` sub-elements. In this case, /// those `tspan` nodes will also contain `Chars` children. /// /// A text or tspan element can contain more than one `Chars` child, for example, /// if there is an XML comment that splits the character contents in two: /// /// ```xml /// /// This sentence will create a Chars. /// /// This sentence will cretea another Chars. /// /// ``` /// /// When rendering a text element, it will take care of concatenating the strings /// in its `Chars` children as appropriate, depending on the /// `xml:space="preserve"` attribute. A `Chars` stores the characters verbatim /// as they come out of the XML parser, after ensuring that they are valid UTF-8. #[derive(Default)] pub struct Chars { string: RefCell, space_normalized: RefCell>, } impl Chars { pub fn new(initial_text: &str) -> Chars { Chars { string: RefCell::new(String::from(initial_text)), space_normalized: RefCell::new(None), } } pub fn is_empty(&self) -> bool { self.string.borrow().is_empty() } pub fn append(&self, s: &str) { self.string.borrow_mut().push_str(s); *self.space_normalized.borrow_mut() = None; } fn ensure_normalized_string(&self, node: &Node, values: &ComputedValues) { let mut normalized = self.space_normalized.borrow_mut(); if (*normalized).is_none() { let mode = match values.xml_space() { XmlSpace::Default => XmlSpaceNormalize::Default(NormalizeDefault { has_element_before: node.previous_sibling().is_some(), has_element_after: node.next_sibling().is_some(), }), XmlSpace::Preserve => XmlSpaceNormalize::Preserve, }; *normalized = Some(xml_space_normalize(mode, &self.string.borrow())); } } fn make_span( &self, node: &Node, values: Rc, dx: f64, dy: f64, depth: usize, link_target: Option, ) -> Span { self.ensure_normalized_string(node, &values); Span::new( self.space_normalized.borrow().as_ref().unwrap(), values, dx, dy, depth, link_target, ) } fn to_chunks( &self, node: &Node, values: Rc, chunks: &mut [Chunk], dx: f64, dy: f64, depth: usize, link_target: Option, ) { let span = self.make_span(node, values, dx, dy, depth, link_target); let num_chunks = chunks.len(); assert!(num_chunks > 0); chunks[num_chunks - 1].spans.push(span); } pub fn get_string(&self) -> String { self.string.borrow().clone() } } #[derive(Default)] pub struct Text { x: Length, y: Length, dx: Length, dy: Length, } impl Text { fn make_chunks( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, layout_context: &LayoutContext, x: f64, y: f64, ) -> Vec { let mut chunks = Vec::new(); let values = cascaded.get(); let params = NormalizeParams::new(values, &layout_context.viewport); chunks.push(Chunk::new(values, Some(x), Some(y))); let dx = self.dx.to_user(¶ms); let dy = self.dy.to_user(¶ms); children_to_chunks( &mut chunks, node, acquired_nodes, cascaded, layout_context, dx, dy, 0, None, ); chunks } } impl ElementTrait for Text { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session), expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session), expanded_name!("", "dx") => set_attribute(&mut self.dx, attr.parse(value), session), expanded_name!("", "dy") => set_attribute(&mut self.dy, attr.parse(value), session), _ => (), } } } fn layout( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, clipping: bool, ) -> Result, InternalRenderingError> { let values = cascaded.get(); let params = NormalizeParams::new(values, viewport); let elt = node.borrow_element(); let session = draw_ctx.session().clone(); let stacking_ctx = StackingContext::new( &session, acquired_nodes, &elt, values.transform(), None, values, ); let layout_text = { let transform = draw_ctx.get_transform_for_stacking_ctx(&stacking_ctx, clipping)?; let layout_context = LayoutContext { writing_mode: values.writing_mode(), transform, font_options: draw_ctx.get_font_options(), viewport: viewport.clone(), session: session.clone(), }; let mut x = self.x.to_user(¶ms); let mut y = self.y.to_user(¶ms); let chunks = self.make_chunks(node, acquired_nodes, cascaded, &layout_context, x, y); let mut measured_chunks = Vec::new(); for chunk in &chunks { measured_chunks.push(MeasuredChunk::from_chunk(&layout_context, chunk)); } let mut positioned_chunks = Vec::new(); for chunk in &measured_chunks { let chunk_x = chunk.x.unwrap_or(x); let chunk_y = chunk.y.unwrap_or(y); let positioned = PositionedChunk::from_measured(&layout_context, chunk, chunk_x, chunk_y); x = positioned.next_chunk_x; y = positioned.next_chunk_y; positioned_chunks.push(positioned); } let mut layout_spans = Vec::new(); for chunk in &positioned_chunks { for span in &chunk.spans { layout_spans.push(span.layout(&layout_context, acquired_nodes)); } } let empty_bbox = BoundingBox::new().with_transform(*transform); let text_bbox = layout_spans.iter().fold(empty_bbox, |mut bbox, span| { if let Some(ref span_bbox) = span.bbox { bbox.insert(span_bbox); } bbox }); let mut text_spans = Vec::new(); for span in layout_spans { let normalize_values = NormalizeValues::new(&span.values); let stroke_paint = span.stroke_paint.to_user_space( &text_bbox.rect, &layout_context.viewport, &normalize_values, ); let fill_paint = span.fill_paint.to_user_space( &text_bbox.rect, &layout_context.viewport, &normalize_values, ); let text_span = TextSpan { layout: span.layout, gravity: span.gravity, bbox: span.bbox, is_visible: span.is_visible, x: span.x, y: span.y, paint_order: span.paint_order, stroke: span.stroke, stroke_paint, fill_paint, text_rendering: span.text_rendering, link_target: span.link_target, }; text_spans.push(text_span); } layout::Text { spans: text_spans } }; Ok(Some(Layer { kind: LayerKind::Text(Box::new(layout_text)), stacking_ctx, })) } fn draw( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, viewport: &Viewport, draw_ctx: &mut DrawingCtx, clipping: bool, ) -> Result { self.layout(node, acquired_nodes, cascaded, viewport, draw_ctx, clipping) .and_then(|layer| { draw_ctx.draw_layer(layer.as_ref().unwrap(), acquired_nodes, clipping, viewport) }) } } #[derive(Default)] pub struct TRef { link: Option, } impl TRef { fn to_chunks( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, chunks: &mut Vec, depth: usize, layout_context: &LayoutContext, ) { if self.link.is_none() { return; } let link = self.link.as_ref().unwrap(); let values = cascaded.get(); if !values.is_displayed() { return; } if let Ok(acquired) = acquired_nodes.acquire(link) { let c = acquired.get(); extract_chars_children_to_chunks_recursively(chunks, c, Rc::new(values.clone()), depth); } else { rsvg_log!( layout_context.session, "element {} references a nonexistent text source \"{}\"", node, link, ); } } } fn extract_chars_children_to_chunks_recursively( chunks: &mut Vec, node: &Node, values: Rc, depth: usize, ) { for child in node.children() { let values = values.clone(); if child.is_chars() { child .borrow_chars() .to_chunks(&child, values, chunks, 0.0, 0.0, depth, None) } else { extract_chars_children_to_chunks_recursively(chunks, &child, values, depth + 1) } } } impl ElementTrait for TRef { fn set_attributes(&mut self, attrs: &Attributes, _session: &Session) { self.link = attrs .iter() .find(|(attr, _)| attr.expanded() == expanded_name!(xlink "href")) // Unlike other elements which use `href` in SVG2 versus `xlink:href` in SVG1.1, // the element got removed in SVG2. So, here we still use a match // against the full namespaced version of the attribute. .and_then(|(attr, value)| NodeId::parse(value).attribute(attr).ok()); } } #[derive(Default)] pub struct TSpan { x: Option>, y: Option>, dx: Length, dy: Length, } impl TSpan { fn to_chunks( &self, node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, layout_context: &LayoutContext, chunks: &mut Vec, dx: f64, dy: f64, depth: usize, link: Option, ) { let values = cascaded.get(); if !values.is_displayed() { return; } let params = NormalizeParams::new(values, &layout_context.viewport); let x = self.x.map(|l| l.to_user(¶ms)); let y = self.y.map(|l| l.to_user(¶ms)); let span_dx = dx + self.dx.to_user(¶ms); let span_dy = dy + self.dy.to_user(¶ms); if x.is_some() || y.is_some() { chunks.push(Chunk::new(values, x, y)); } children_to_chunks( chunks, node, acquired_nodes, cascaded, layout_context, span_dx, span_dy, depth, link, ); } } impl ElementTrait for TSpan { fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { for (attr, value) in attrs.iter() { match attr.expanded() { expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session), expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session), expanded_name!("", "dx") => set_attribute(&mut self.dx, attr.parse(value), session), expanded_name!("", "dy") => set_attribute(&mut self.dy, attr.parse(value), session), _ => (), } } } } impl From for pango::Style { fn from(s: FontStyle) -> pango::Style { match s { FontStyle::Normal => pango::Style::Normal, FontStyle::Italic => pango::Style::Italic, FontStyle::Oblique => pango::Style::Oblique, } } } impl From for pango::Variant { fn from(v: FontVariant) -> pango::Variant { match v { FontVariant::Normal => pango::Variant::Normal, FontVariant::SmallCaps => pango::Variant::SmallCaps, } } } impl From for pango::Stretch { fn from(s: FontStretch) -> pango::Stretch { match s { FontStretch::Normal => pango::Stretch::Normal, FontStretch::Wider => pango::Stretch::Expanded, // not quite correct FontStretch::Narrower => pango::Stretch::Condensed, // not quite correct FontStretch::UltraCondensed => pango::Stretch::UltraCondensed, FontStretch::ExtraCondensed => pango::Stretch::ExtraCondensed, FontStretch::Condensed => pango::Stretch::Condensed, FontStretch::SemiCondensed => pango::Stretch::SemiCondensed, FontStretch::SemiExpanded => pango::Stretch::SemiExpanded, FontStretch::Expanded => pango::Stretch::Expanded, FontStretch::ExtraExpanded => pango::Stretch::ExtraExpanded, FontStretch::UltraExpanded => pango::Stretch::UltraExpanded, } } } impl From for pango::Weight { fn from(w: FontWeight) -> pango::Weight { pango::Weight::__Unknown(w.numeric_weight().into()) } } impl From for pango::Direction { fn from(d: Direction) -> pango::Direction { match d { Direction::Ltr => pango::Direction::Ltr, Direction::Rtl => pango::Direction::Rtl, } } } impl From for pango::Direction { fn from(m: WritingMode) -> pango::Direction { use WritingMode::*; match m { HorizontalTb | VerticalRl | VerticalLr | LrTb | Lr | Tb | TbRl => pango::Direction::Ltr, RlTb | Rl => pango::Direction::Rtl, } } } impl From for pango::Gravity { fn from(m: WritingMode) -> pango::Gravity { use WritingMode::*; match m { HorizontalTb | LrTb | Lr | RlTb | Rl => pango::Gravity::South, VerticalRl | Tb | TbRl => pango::Gravity::East, VerticalLr => pango::Gravity::West, } } } /// Constants with Unicode's directional formatting characters /// /// mod directional_formatting_characters { /// Left-to-Right Embedding /// /// Treat the following text as embedded left-to-right. pub const LRE: char = '\u{202a}'; /// Right-to-Left Embedding /// /// Treat the following text as embedded right-to-left. pub const RLE: char = '\u{202b}'; /// Left-to-Right Override /// /// Force following characters to be treated as strong left-to-right characters. pub const LRO: char = '\u{202d}'; /// Right-to-Left Override /// /// Force following characters to be treated as strong right-to-left characters. pub const RLO: char = '\u{202e}'; /// Pop Directional Formatting /// /// End the scope of the last LRE, RLE, RLO, or LRO. pub const PDF: char = '\u{202c}'; /// Left-to-Right Isolate /// /// Treat the following text as isolated and left-to-right. pub const LRI: char = '\u{2066}'; /// Right-to-Left Isolate /// /// Treat the following text as isolated and right-to-left. pub const RLI: char = '\u{2067}'; /// First Strong Isolate /// /// Treat the following text as isolated and in the direction of its first strong /// directional character that is not inside a nested isolate. pub const FSI: char = '\u{2068}'; /// Pop Directional Isolate /// /// End the scope of the last LRI, RLI, or FSI. pub const PDI: char = '\u{2069}'; } /// Unicode control characters to be inserted when `unicode-bidi` is specified. /// /// The `unicode-bidi` property is used to change the embedding of a text span within /// another. This struct contains slices with the control characters that must be /// inserted into the text stream at the span's limits so that the bidi/shaping engine /// will know what to do. struct BidiControl { start: &'static [char], end: &'static [char], } impl BidiControl { /// Creates a `BidiControl` from the properties that determine it. /// /// See the table titled "Bidi control codes injected..." in /// #[rustfmt::skip] fn from_unicode_bidi_and_direction(unicode_bidi: UnicodeBidi, direction: Direction) -> BidiControl { use UnicodeBidi::*; use Direction::*; use directional_formatting_characters::*; let (start, end) = match (unicode_bidi, direction) { (Normal, _) => (&[][..], &[][..]), (Embed, Ltr) => (&[LRE][..], &[PDF][..]), (Embed, Rtl) => (&[RLE][..], &[PDF][..]), (Isolate, Ltr) => (&[LRI][..], &[PDI][..]), (Isolate, Rtl) => (&[RLI][..], &[PDI][..]), (BidiOverride, Ltr) => (&[LRO][..], &[PDF][..]), (BidiOverride, Rtl) => (&[RLO][..], &[PDF][..]), (IsolateOverride, Ltr) => (&[FSI, LRO][..], &[PDF, PDI][..]), (IsolateOverride, Rtl) => (&[FSI, RLO][..], &[PDF, PDI][..]), (Plaintext, Ltr) => (&[FSI][..], &[PDI][..]), (Plaintext, Rtl) => (&[FSI][..], &[PDI][..]), }; BidiControl { start, end } } } /// Prepends and appends Unicode directional formatting characters. fn wrap_with_direction_control_chars(s: &str, bidi_control: &BidiControl) -> String { let mut res = String::with_capacity(s.len() + bidi_control.start.len() + bidi_control.end.len()); for &ch in bidi_control.start { res.push(ch); } res.push_str(s); for &ch in bidi_control.end { res.push(ch); } res } /// Returns `None` if the layout would be invalid due to, for example, out-of-bounds font sizes. fn create_pango_layout( layout_context: &LayoutContext, props: &FontProperties, text: &str, ) -> Option { let pango_context = create_pango_context(&layout_context.font_options, &layout_context.transform); if let XmlLang(Some(ref lang)) = props.xml_lang { pango_context.set_language(Some(&pango::Language::from_string(lang.as_str()))); } pango_context.set_base_gravity(pango::Gravity::from(layout_context.writing_mode)); match (props.unicode_bidi, props.direction) { (UnicodeBidi::BidiOverride, _) | (UnicodeBidi::Embed, _) => { pango_context.set_base_dir(pango::Direction::from(props.direction)); } (_, direction) if direction != Direction::Ltr => { pango_context.set_base_dir(pango::Direction::from(direction)); } (_, _) => { pango_context.set_base_dir(pango::Direction::from(layout_context.writing_mode)); } } let layout = pango::Layout::new(&pango_context); let font_size = PangoUnits::from_pixels(props.font_size); let letter_spacing = PangoUnits::from_pixels(props.letter_spacing); if font_size.is_none() { rsvg_log!( &layout_context.session, "font-size {} is out of bounds; ignoring span", props.font_size ); } if letter_spacing.is_none() { rsvg_log!( &layout_context.session, "letter-spacing {} is out of bounds; ignoring span", props.letter_spacing ); } if let (Some(font_size), Some(letter_spacing)) = (font_size, letter_spacing) { let attr_list = pango::AttrList::new(); add_pango_attributes(&attr_list, props, 0, text.len(), font_size, letter_spacing); layout.set_attributes(Some(&attr_list)); layout.set_text(text); layout.set_auto_dir(false); Some(layout) } else { None } } /// Adds Pango attributes, suitable for a span of text, to an `AttrList`. fn add_pango_attributes( attr_list: &pango::AttrList, props: &FontProperties, start_index: usize, end_index: usize, font_size: PangoUnits, letter_spacing: PangoUnits, ) { let start_index = u32::try_from(start_index).expect("Pango attribute index must fit in u32"); let end_index = u32::try_from(end_index).expect("Pango attribute index must fit in u32"); assert!(start_index <= end_index); let mut attributes = Vec::new(); let mut font_desc = pango::FontDescription::new(); font_desc.set_family(props.font_family.as_str()); font_desc.set_style(pango::Style::from(props.font_style)); font_desc.set_variant(pango::Variant::from(props.font_variant)); font_desc.set_weight(pango::Weight::from(props.font_weight)); font_desc.set_stretch(pango::Stretch::from(props.font_stretch)); font_desc.set_size(font_size.0); attributes.push(pango::AttrFontDesc::new(&font_desc).upcast()); attributes.push(pango::AttrInt::new_letter_spacing(letter_spacing.0).upcast()); if props.text_decoration.overline { attributes.push(pango::AttrInt::new_overline(pango::Overline::Single).upcast()); } if props.text_decoration.underline { attributes.push(pango::AttrInt::new_underline(pango::Underline::Single).upcast()); } if props.text_decoration.strike { attributes.push(pango::AttrInt::new_strikethrough(true).upcast()); } // Set the range in each attribute for attr in &mut attributes { attr.set_start_index(start_index); attr.set_end_index(end_index); } // Add the attributes to the attr_list for attr in attributes { attr_list.insert(attr); } } #[cfg(test)] mod tests { use super::*; #[test] fn chars_default() { let c = Chars::default(); assert!(c.is_empty()); assert!(c.space_normalized.borrow().is_none()); } #[test] fn chars_new() { let example = "Test 123"; let c = Chars::new(example); assert_eq!(c.get_string(), example); assert!(c.space_normalized.borrow().is_none()); } // This is called _horizontal because the property value in "CSS Writing Modes 3" // is `horizontal-tb`. Eventually we will support that and this will make more sense. #[test] fn adjusted_advance_horizontal_ltr() { use Direction::*; use TextAnchor::*; assert_eq!( text_anchor_offset( Start, Ltr, WritingMode::Lr, Rect::from_size(1.0, 2.0).translate((5.0, 6.0)) ), (-5.0, 0.0) ); assert_eq!( text_anchor_offset( Middle, Ltr, WritingMode::Lr, Rect::from_size(1.0, 2.0).translate((5.0, 6.0)) ), (-5.5, 0.0) ); assert_eq!( text_anchor_offset( End, Ltr, WritingMode::Lr, Rect::from_size(1.0, 2.0).translate((5.0, 6.0)) ), (-6.0, 0.0) ); } #[test] fn adjusted_advance_horizontal_rtl() { use Direction::*; use TextAnchor::*; assert_eq!( text_anchor_offset( Start, Rtl, WritingMode::Rl, Rect::from_size(1.0, 2.0).translate((5.0, 6.0)) ), (-6.0, 0.0) ); assert_eq!( text_anchor_offset( Middle, Rtl, WritingMode::Rl, Rect::from_size(1.0, 2.0).translate((5.0, 6.0)) ), (-5.5, 0.0) ); assert_eq!( text_anchor_offset( TextAnchor::End, Direction::Rtl, WritingMode::Rl, Rect::from_size(1.0, 2.0).translate((5.0, 6.0)) ), (-5.0, 0.0) ); } // This is called _vertical because "CSS Writing Modes 3" has both `vertical-rl` (East // Asia), and `vertical-lr` (Manchu, Mongolian), but librsvg does not support block // flow direction properly yet. Eventually we will support that and this will make // more sense. #[test] fn adjusted_advance_vertical() { use Direction::*; use TextAnchor::*; assert_eq!( text_anchor_offset(Start, Ltr, WritingMode::Tb, Rect::from_size(2.0, 4.0)), (0.0, 0.0) ); assert_eq!( text_anchor_offset(Middle, Ltr, WritingMode::Tb, Rect::from_size(2.0, 4.0)), (0.0, -2.0) ); assert_eq!( text_anchor_offset(End, Ltr, WritingMode::Tb, Rect::from_size(2.0, 4.0)), (0.0, -4.0) ); } #[test] fn pango_units_works() { assert_eq!(PangoUnits::from_pixels(10.0).unwrap().0, pango::SCALE * 10); } #[test] fn pango_units_detects_overflow() { assert!(PangoUnits::from_pixels(1e7).is_none()); } } librsvg-2.59.0/src/transform.rs000064400000000000000000001040571046102023000145500ustar 00000000000000//! Handling of transform values. //! //! This module contains the following: //! //! * [`Transform`] to represent 2D transforms in general; it's just a matrix. //! //! * [`TransformProperty`] for the [`transform` property][prop] in SVG2/CSS3. //! //! * [`Transform`] also handles the [`transform` attribute][attr] in SVG1.1, which has a different //! grammar than the `transform` property from SVG2. //! //! [prop]: https://www.w3.org/TR/css-transforms-1/#transform-property //! [attr]: https://www.w3.org/TR/SVG11/coords.html#TransformAttribute use cssparser::{Parser, Token}; use std::ops::Deref; use crate::angle::Angle; use crate::error::*; use crate::length::*; use crate::parsers::{optional_comma, Parse}; use crate::properties::ComputedValues; use crate::property_macros::Property; use crate::rect::Rect; /// A transform that has been checked to be invertible. /// /// We need to validate user-supplied transforms before setting them on Cairo, /// so we use this type for that. #[derive(Debug, Copy, Clone, PartialEq)] pub struct ValidTransform(Transform); impl TryFrom for ValidTransform { type Error = InvalidTransform; /// Validates a [`Transform`] before converting it to a [`ValidTransform`]. /// /// A transform is valid if it is invertible. For example, a /// matrix with all-zeros is not invertible, and it is invalid. fn try_from(t: Transform) -> Result { if t.is_invertible() { Ok(ValidTransform(t)) } else { Err(InvalidTransform) } } } impl Deref for ValidTransform { type Target = Transform; fn deref(&self) -> &Transform { &self.0 } } /// A 2D transformation matrix. #[derive(Debug, Copy, Clone, PartialEq)] pub struct Transform { pub xx: f64, pub yx: f64, pub xy: f64, pub yy: f64, pub x0: f64, pub y0: f64, } /// The `transform` property from the CSS Transforms Module Level 1. /// /// CSS Transforms 1: #[derive(Debug, Default, Clone, PartialEq)] pub enum TransformProperty { #[default] None, List(Vec), } /// The `transform` attribute from SVG1.1 /// /// SVG1.1: #[derive(Copy, Clone, Default, Debug, PartialEq)] pub struct TransformAttribute(Transform); impl Property for TransformProperty { fn inherits_automatically() -> bool { false } fn compute(&self, _v: &ComputedValues) -> Self { self.clone() } } impl TransformProperty { pub fn to_transform(&self) -> Transform { // From the spec (https://www.w3.org/TR/css-transforms-1/#current-transformation-matrix): // Start with the identity matrix. // TODO: implement (#685) - Translate by the computed X and Y of transform-origin // Multiply by each of the transform functions in transform property from left to right // TODO: implement - Translate by the negated computed X and Y values of transform-origin match self { TransformProperty::None => Transform::identity(), TransformProperty::List(l) => { let mut final_transform = Transform::identity(); for f in l.iter() { use TransformFunction::*; let transform_matrix = match f { Matrix(trans_matrix) => *trans_matrix, Translate(h, v) => Transform::new_translate(h.length, v.length), TranslateX(h) => Transform::new_translate(h.length, 0.0), TranslateY(v) => Transform::new_translate(0.0, v.length), Scale(x, y) => Transform::new_scale(*x, *y), ScaleX(x) => Transform::new_scale(*x, 1.0), ScaleY(y) => Transform::new_scale(1.0, *y), Rotate(a) => Transform::new_rotate(*a), Skew(ax, ay) => Transform::new_skew(*ax, *ay), SkewX(ax) => Transform::new_skew(*ax, Angle::new(0.0)), SkewY(ay) => Transform::new_skew(Angle::new(0.0), *ay), }; final_transform = transform_matrix.post_transform(&final_transform); } final_transform } } } } // https://www.w3.org/TR/css-transforms-1/#typedef-transform-function #[derive(Debug, Clone, PartialEq)] pub enum TransformFunction { Matrix(Transform), Translate(Length, Length), TranslateX(Length), TranslateY(Length), Scale(f64, f64), ScaleX(f64), ScaleY(f64), Rotate(Angle), Skew(Angle, Angle), SkewX(Angle), SkewY(Angle), } impl Parse for TransformProperty { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { if parser .try_parse(|p| p.expect_ident_matching("none")) .is_ok() { Ok(TransformProperty::None) } else { let t = parse_transform_prop_function_list(parser)?; Ok(TransformProperty::List(t)) } } } fn parse_transform_prop_function_list<'i>( parser: &mut Parser<'i, '_>, ) -> Result, ParseError<'i>> { let mut v = Vec::::new(); loop { v.push(parse_transform_prop_function_command(parser)?); if parser.is_exhausted() { break; } } Ok(v) } fn parse_transform_prop_function_command<'i>( parser: &mut Parser<'i, '_>, ) -> Result> { let loc = parser.current_source_location(); match parser.next()?.clone() { Token::Function(ref name) => parse_transform_prop_function_internal(name, parser), tok => Err(loc.new_unexpected_token_error(tok.clone())), } } fn parse_transform_prop_function_internal<'i>( name: &str, parser: &mut Parser<'i, '_>, ) -> Result> { let loc = parser.current_source_location(); match name { "matrix" => parse_prop_matrix_args(parser), "translate" => parse_prop_translate_args(parser), "translateX" => parse_prop_translate_x_args(parser), "translateY" => parse_prop_translate_y_args(parser), "scale" => parse_prop_scale_args(parser), "scaleX" => parse_prop_scale_x_args(parser), "scaleY" => parse_prop_scale_y_args(parser), "rotate" => parse_prop_rotate_args(parser), "skew" => parse_prop_skew_args(parser), "skewX" => parse_prop_skew_x_args(parser), "skewY" => parse_prop_skew_y_args(parser), _ => Err(loc.new_custom_error(ValueErrorKind::parse_error( "expected matrix|translate|translateX|translateY|scale|scaleX|scaleY|rotate|skewX|skewY", ))), } } fn parse_prop_matrix_args<'i>( parser: &mut Parser<'i, '_>, ) -> Result> { parser.parse_nested_block(|p| { let xx = f64::parse(p)?; p.expect_comma()?; let yx = f64::parse(p)?; p.expect_comma()?; let xy = f64::parse(p)?; p.expect_comma()?; let yy = f64::parse(p)?; p.expect_comma()?; let x0 = f64::parse(p)?; p.expect_comma()?; let y0 = f64::parse(p)?; Ok(TransformFunction::Matrix(Transform::new_unchecked( xx, yx, xy, yy, x0, y0, ))) }) } fn length_is_in_pixels(l: &Length) -> bool { l.unit == LengthUnit::Px } fn only_pixels_error<'i>(loc: cssparser::SourceLocation) -> ParseError<'i> { loc.new_custom_error(ValueErrorKind::parse_error( "only translations in pixels are supported for now", )) } fn parse_prop_translate_args<'i>( parser: &mut Parser<'i, '_>, ) -> Result> { parser.parse_nested_block(|p| { let loc = p.current_source_location(); let tx: Length = Length::parse(p)?; let ty: Length = if p.try_parse(|p| p.expect_comma()).is_ok() { Length::parse(p)? } else { Length::new(0.0, LengthUnit::Px) }; if !(length_is_in_pixels(&tx) && length_is_in_pixels(&ty)) { return Err(only_pixels_error(loc)); } Ok(TransformFunction::Translate(tx, ty)) }) } fn parse_prop_translate_x_args<'i>( parser: &mut Parser<'i, '_>, ) -> Result> { parser.parse_nested_block(|p| { let loc = p.current_source_location(); let tx: Length = Length::parse(p)?; if !length_is_in_pixels(&tx) { return Err(only_pixels_error(loc)); } Ok(TransformFunction::TranslateX(tx)) }) } fn parse_prop_translate_y_args<'i>( parser: &mut Parser<'i, '_>, ) -> Result> { parser.parse_nested_block(|p| { let loc = p.current_source_location(); let ty: Length = Length::parse(p)?; if !length_is_in_pixels(&ty) { return Err(only_pixels_error(loc)); } Ok(TransformFunction::TranslateY(ty)) }) } fn parse_prop_scale_args<'i>( parser: &mut Parser<'i, '_>, ) -> Result> { parser.parse_nested_block(|p| { let x = f64::parse(p)?; let y = if p.try_parse(|p| p.expect_comma()).is_ok() { f64::parse(p)? } else { x }; Ok(TransformFunction::Scale(x, y)) }) } fn parse_prop_scale_x_args<'i>( parser: &mut Parser<'i, '_>, ) -> Result> { parser.parse_nested_block(|p| { let x = f64::parse(p)?; Ok(TransformFunction::ScaleX(x)) }) } fn parse_prop_scale_y_args<'i>( parser: &mut Parser<'i, '_>, ) -> Result> { parser.parse_nested_block(|p| { let y = f64::parse(p)?; Ok(TransformFunction::ScaleY(y)) }) } fn parse_prop_rotate_args<'i>( parser: &mut Parser<'i, '_>, ) -> Result> { parser.parse_nested_block(|p| { let angle = Angle::parse(p)?; Ok(TransformFunction::Rotate(angle)) }) } fn parse_prop_skew_args<'i>( parser: &mut Parser<'i, '_>, ) -> Result> { parser.parse_nested_block(|p| { let ax = Angle::parse(p)?; let ay = if p.try_parse(|p| p.expect_comma()).is_ok() { Angle::parse(p)? } else { Angle::from_degrees(0.0) }; Ok(TransformFunction::Skew(ax, ay)) }) } fn parse_prop_skew_x_args<'i>( parser: &mut Parser<'i, '_>, ) -> Result> { parser.parse_nested_block(|p| { let angle = Angle::parse(p)?; Ok(TransformFunction::SkewX(angle)) }) } fn parse_prop_skew_y_args<'i>( parser: &mut Parser<'i, '_>, ) -> Result> { parser.parse_nested_block(|p| { let angle = Angle::parse(p)?; Ok(TransformFunction::SkewY(angle)) }) } impl Transform { #[inline] pub fn new_unchecked(xx: f64, yx: f64, xy: f64, yy: f64, x0: f64, y0: f64) -> Self { Self { xx, yx, xy, yy, x0, y0, } } #[inline] pub fn identity() -> Self { Self::new_unchecked(1.0, 0.0, 0.0, 1.0, 0.0, 0.0) } #[inline] pub fn new_translate(tx: f64, ty: f64) -> Self { Self::new_unchecked(1.0, 0.0, 0.0, 1.0, tx, ty) } #[inline] pub fn new_scale(sx: f64, sy: f64) -> Self { Self::new_unchecked(sx, 0.0, 0.0, sy, 0.0, 0.0) } #[inline] pub fn new_rotate(a: Angle) -> Self { let (s, c) = a.radians().sin_cos(); Self::new_unchecked(c, s, -s, c, 0.0, 0.0) } #[inline] pub fn new_skew(ax: Angle, ay: Angle) -> Self { Self::new_unchecked(1.0, ay.radians().tan(), ax.radians().tan(), 1.0, 0.0, 0.0) } #[must_use] pub fn multiply(t1: &Transform, t2: &Transform) -> Self { #[allow(clippy::suspicious_operation_groupings)] Transform { xx: t1.xx * t2.xx + t1.yx * t2.xy, yx: t1.xx * t2.yx + t1.yx * t2.yy, xy: t1.xy * t2.xx + t1.yy * t2.xy, yy: t1.xy * t2.yx + t1.yy * t2.yy, x0: t1.x0 * t2.xx + t1.y0 * t2.xy + t2.x0, y0: t1.x0 * t2.yx + t1.y0 * t2.yy + t2.y0, } } #[inline] pub fn pre_transform(&self, t: &Transform) -> Self { Self::multiply(t, self) } #[inline] pub fn post_transform(&self, t: &Transform) -> Self { Self::multiply(self, t) } #[inline] pub fn pre_translate(&self, x: f64, y: f64) -> Self { self.pre_transform(&Transform::new_translate(x, y)) } #[inline] pub fn pre_scale(&self, sx: f64, sy: f64) -> Self { self.pre_transform(&Transform::new_scale(sx, sy)) } #[inline] pub fn pre_rotate(&self, angle: Angle) -> Self { self.pre_transform(&Transform::new_rotate(angle)) } #[inline] pub fn post_translate(&self, x: f64, y: f64) -> Self { self.post_transform(&Transform::new_translate(x, y)) } #[inline] pub fn post_scale(&self, sx: f64, sy: f64) -> Self { self.post_transform(&Transform::new_scale(sx, sy)) } #[inline] pub fn post_rotate(&self, angle: Angle) -> Self { self.post_transform(&Transform::new_rotate(angle)) } #[inline] fn determinant(&self) -> f64 { self.xx * self.yy - self.xy * self.yx } #[inline] pub fn is_invertible(&self) -> bool { let det = self.determinant(); det != 0.0 && det.is_finite() } #[must_use] pub fn invert(&self) -> Option { let det = self.determinant(); if det == 0.0 || !det.is_finite() { return None; } let inv_det = 1.0 / det; Some(Transform::new_unchecked( inv_det * self.yy, inv_det * (-self.yx), inv_det * (-self.xy), inv_det * self.xx, inv_det * (self.xy * self.y0 - self.yy * self.x0), inv_det * (self.yx * self.x0 - self.xx * self.y0), )) } #[inline] pub fn transform_distance(&self, dx: f64, dy: f64) -> (f64, f64) { (dx * self.xx + dy * self.xy, dx * self.yx + dy * self.yy) } #[inline] pub fn transform_point(&self, px: f64, py: f64) -> (f64, f64) { let (x, y) = self.transform_distance(px, py); (x + self.x0, y + self.y0) } pub fn transform_rect(&self, rect: &Rect) -> Rect { let points = [ self.transform_point(rect.x0, rect.y0), self.transform_point(rect.x1, rect.y0), self.transform_point(rect.x0, rect.y1), self.transform_point(rect.x1, rect.y1), ]; let (mut xmin, mut ymin, mut xmax, mut ymax) = { let (x, y) = points[0]; (x, y, x, y) }; for &(x, y) in points.iter().skip(1) { if x < xmin { xmin = x; } if x > xmax { xmax = x; } if y < ymin { ymin = y; } if y > ymax { ymax = y; } } Rect { x0: xmin, y0: ymin, x1: xmax, y1: ymax, } } } impl Default for Transform { #[inline] fn default() -> Transform { Transform::identity() } } impl TransformAttribute { pub fn to_transform(self) -> Transform { self.0 } } impl Parse for TransformAttribute { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { Ok(TransformAttribute(parse_transform_list(parser)?)) } } fn parse_transform_list<'i>(parser: &mut Parser<'i, '_>) -> Result> { let mut t = Transform::identity(); loop { if parser.is_exhausted() { break; } t = parse_transform_command(parser)?.post_transform(&t); optional_comma(parser); } Ok(t) } fn parse_transform_command<'i>(parser: &mut Parser<'i, '_>) -> Result> { let loc = parser.current_source_location(); match parser.next()?.clone() { Token::Function(ref name) => parse_transform_function(name, parser), Token::Ident(ref name) => { parser.expect_parenthesis_block()?; parse_transform_function(name, parser) } tok => Err(loc.new_unexpected_token_error(tok.clone())), } } fn parse_transform_function<'i>( name: &str, parser: &mut Parser<'i, '_>, ) -> Result> { let loc = parser.current_source_location(); match name { "matrix" => parse_matrix_args(parser), "translate" => parse_translate_args(parser), "scale" => parse_scale_args(parser), "rotate" => parse_rotate_args(parser), "skewX" => parse_skew_x_args(parser), "skewY" => parse_skew_y_args(parser), _ => Err(loc.new_custom_error(ValueErrorKind::parse_error( "expected matrix|translate|scale|rotate|skewX|skewY", ))), } } fn parse_matrix_args<'i>(parser: &mut Parser<'i, '_>) -> Result> { parser.parse_nested_block(|p| { let xx = f64::parse(p)?; optional_comma(p); let yx = f64::parse(p)?; optional_comma(p); let xy = f64::parse(p)?; optional_comma(p); let yy = f64::parse(p)?; optional_comma(p); let x0 = f64::parse(p)?; optional_comma(p); let y0 = f64::parse(p)?; Ok(Transform::new_unchecked(xx, yx, xy, yy, x0, y0)) }) } fn parse_translate_args<'i>(parser: &mut Parser<'i, '_>) -> Result> { parser.parse_nested_block(|p| { let tx = f64::parse(p)?; let ty = p .try_parse(|p| { optional_comma(p); f64::parse(p) }) .unwrap_or(0.0); Ok(Transform::new_translate(tx, ty)) }) } fn parse_scale_args<'i>(parser: &mut Parser<'i, '_>) -> Result> { parser.parse_nested_block(|p| { let x = f64::parse(p)?; let y = p .try_parse(|p| { optional_comma(p); f64::parse(p) }) .unwrap_or(x); Ok(Transform::new_scale(x, y)) }) } fn parse_rotate_args<'i>(parser: &mut Parser<'i, '_>) -> Result> { parser.parse_nested_block(|p| { let angle = Angle::from_degrees(f64::parse(p)?); let (tx, ty) = p .try_parse(|p| -> Result<_, ParseError<'_>> { optional_comma(p); let tx = f64::parse(p)?; optional_comma(p); let ty = f64::parse(p)?; Ok((tx, ty)) }) .unwrap_or((0.0, 0.0)); Ok(Transform::new_translate(tx, ty) .pre_rotate(angle) .pre_translate(-tx, -ty)) }) } fn parse_skew_x_args<'i>(parser: &mut Parser<'i, '_>) -> Result> { parser.parse_nested_block(|p| { let angle = Angle::from_degrees(f64::parse(p)?); Ok(Transform::new_skew(angle, Angle::new(0.0))) }) } fn parse_skew_y_args<'i>(parser: &mut Parser<'i, '_>) -> Result> { parser.parse_nested_block(|p| { let angle = Angle::from_degrees(f64::parse(p)?); Ok(Transform::new_skew(Angle::new(0.0), angle)) }) } #[cfg(test)] mod tests { use super::*; use float_cmp::ApproxEq; use std::f64; fn rotation_transform(deg: f64, tx: f64, ty: f64) -> Transform { Transform::new_translate(tx, ty) .pre_rotate(Angle::from_degrees(deg)) .pre_translate(-tx, -ty) } fn parse_transform(s: &str) -> Result> { let transform_attr = TransformAttribute::parse_str(s)?; Ok(transform_attr.to_transform()) } fn parse_transform_prop(s: &str) -> Result> { TransformProperty::parse_str(s) } fn assert_transform_eq(t1: &Transform, t2: &Transform) { let epsilon = 8.0 * f64::EPSILON; // kind of arbitrary, but allow for some sloppiness assert!(t1.xx.approx_eq(t2.xx, (epsilon, 1))); assert!(t1.yx.approx_eq(t2.yx, (epsilon, 1))); assert!(t1.xy.approx_eq(t2.xy, (epsilon, 1))); assert!(t1.yy.approx_eq(t2.yy, (epsilon, 1))); assert!(t1.x0.approx_eq(t2.x0, (epsilon, 1))); assert!(t1.y0.approx_eq(t2.y0, (epsilon, 1))); } #[test] fn test_multiply() { let t1 = Transform::identity(); let t2 = Transform::new_unchecked(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); assert_transform_eq(&Transform::multiply(&t1, &t2), &t2); assert_transform_eq(&Transform::multiply(&t2, &t1), &t2); let t1 = Transform::new_unchecked(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); let t2 = Transform::new_unchecked(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); let r = Transform::new_unchecked(0.0, 0.0, 0.0, 0.0, 5.0, 6.0); assert_transform_eq(&Transform::multiply(&t1, &t2), &t2); assert_transform_eq(&Transform::multiply(&t2, &t1), &r); let t1 = Transform::new_unchecked(0.5, 0.0, 0.0, 0.5, 10.0, 10.0); let t2 = Transform::new_unchecked(1.0, 0.0, 0.0, 1.0, -10.0, -10.0); let r1 = Transform::new_unchecked(0.5, 0.0, 0.0, 0.5, 0.0, 0.0); let r2 = Transform::new_unchecked(0.5, 0.0, 0.0, 0.5, 5.0, 5.0); assert_transform_eq(&Transform::multiply(&t1, &t2), &r1); assert_transform_eq(&Transform::multiply(&t2, &t1), &r2); } #[test] fn test_invert() { let t = Transform::new_unchecked(2.0, 0.0, 0.0, 0.0, 0.0, 0.0); assert!(!t.is_invertible()); assert!(t.invert().is_none()); let t = Transform::identity(); assert!(t.is_invertible()); assert!(t.invert().is_some()); let i = t.invert().unwrap(); assert_transform_eq(&i, &Transform::identity()); let t = Transform::new_unchecked(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); assert!(t.is_invertible()); assert!(t.invert().is_some()); let i = t.invert().unwrap(); assert_transform_eq(&t.pre_transform(&i), &Transform::identity()); assert_transform_eq(&t.post_transform(&i), &Transform::identity()); } #[test] pub fn test_transform_point() { let t = Transform::new_translate(10.0, 10.0); assert_eq!((11.0, 11.0), t.transform_point(1.0, 1.0)); } #[test] pub fn test_transform_distance() { let t = Transform::new_translate(10.0, 10.0).pre_scale(2.0, 1.0); assert_eq!((2.0, 1.0), t.transform_distance(1.0, 1.0)); } #[test] fn parses_valid_transform() { let t = Transform::new_unchecked(1.0, 0.0, 0.0, 1.0, 20.0, 30.0); let s = Transform::new_unchecked(10.0, 0.0, 0.0, 10.0, 0.0, 0.0); let r = rotation_transform(30.0, 10.0, 10.0); let a = Transform::multiply(&s, &t); assert_transform_eq( &parse_transform("translate(20, 30), scale (10) rotate (30 10 10)").unwrap(), &Transform::multiply(&r, &a), ); } fn assert_parse_error(s: &str) { assert!(parse_transform(s).is_err()); } #[test] fn syntax_error_yields_parse_error() { assert_parse_error("foo"); assert_parse_error("matrix (1 2 3 4 5)"); assert_parse_error("translate(1 2 3 4 5)"); assert_parse_error("translate (1,)"); assert_parse_error("scale (1,)"); assert_parse_error("skewX (1,2)"); assert_parse_error("skewY ()"); assert_parse_error("skewY"); } #[test] fn parses_matrix() { assert_transform_eq( &parse_transform("matrix (1 2 3 4 5 6)").unwrap(), &Transform::new_unchecked(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), ); assert_transform_eq( &parse_transform("matrix(1,2,3,4 5 6)").unwrap(), &Transform::new_unchecked(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), ); assert_transform_eq( &parse_transform("matrix (1,2.25,-3.25e2,4 5 6)").unwrap(), &Transform::new_unchecked(1.0, 2.25, -325.0, 4.0, 5.0, 6.0), ); } #[test] fn parses_translate() { assert_transform_eq( &parse_transform("translate(-1 -2)").unwrap(), &Transform::new_unchecked(1.0, 0.0, 0.0, 1.0, -1.0, -2.0), ); assert_transform_eq( &parse_transform("translate(-1, -2)").unwrap(), &Transform::new_unchecked(1.0, 0.0, 0.0, 1.0, -1.0, -2.0), ); assert_transform_eq( &parse_transform("translate(-1)").unwrap(), &Transform::new_unchecked(1.0, 0.0, 0.0, 1.0, -1.0, 0.0), ); } #[test] fn parses_scale() { assert_transform_eq( &parse_transform("scale (-1)").unwrap(), &Transform::new_unchecked(-1.0, 0.0, 0.0, -1.0, 0.0, 0.0), ); assert_transform_eq( &parse_transform("scale(-1 -2)").unwrap(), &Transform::new_unchecked(-1.0, 0.0, 0.0, -2.0, 0.0, 0.0), ); assert_transform_eq( &parse_transform("scale(-1, -2)").unwrap(), &Transform::new_unchecked(-1.0, 0.0, 0.0, -2.0, 0.0, 0.0), ); } #[test] fn parses_rotate() { assert_transform_eq( &parse_transform("rotate (30)").unwrap(), &rotation_transform(30.0, 0.0, 0.0), ); assert_transform_eq( &parse_transform("rotate (30,-1,-2)").unwrap(), &rotation_transform(30.0, -1.0, -2.0), ); assert_transform_eq( &parse_transform("rotate(30, -1, -2)").unwrap(), &rotation_transform(30.0, -1.0, -2.0), ); } #[test] fn parses_skew_x() { assert_transform_eq( &parse_transform("skewX (30)").unwrap(), &Transform::new_skew(Angle::from_degrees(30.0), Angle::new(0.0)), ); } #[test] fn parses_skew_y() { assert_transform_eq( &parse_transform("skewY (30)").unwrap(), &Transform::new_skew(Angle::new(0.0), Angle::from_degrees(30.0)), ); } #[test] fn parses_transform_list() { let t = Transform::new_unchecked(1.0, 0.0, 0.0, 1.0, 20.0, 30.0); let s = Transform::new_unchecked(10.0, 0.0, 0.0, 10.0, 0.0, 0.0); let r = rotation_transform(30.0, 10.0, 10.0); assert_transform_eq( &parse_transform("scale(10)rotate(30, 10, 10)").unwrap(), &Transform::multiply(&r, &s), ); assert_transform_eq( &parse_transform("translate(20, 30), scale (10)").unwrap(), &Transform::multiply(&s, &t), ); let a = Transform::multiply(&s, &t); assert_transform_eq( &parse_transform("translate(20, 30), scale (10) rotate (30 10 10)").unwrap(), &Transform::multiply(&r, &a), ); } #[test] fn parses_empty() { assert_transform_eq(&parse_transform("").unwrap(), &Transform::identity()); } #[test] fn test_parse_transform_property_none() { assert_eq!( parse_transform_prop("none").unwrap(), TransformProperty::None ); } #[test] fn none_transform_is_identity() { assert_eq!( parse_transform_prop("none").unwrap().to_transform(), Transform::identity() ); } #[test] fn empty_transform_property_is_error() { // https://www.w3.org/TR/css-transforms-1/#transform-property // // = + // ^ one or more required assert!(parse_transform_prop("").is_err()); } #[test] fn test_parse_transform_property_matrix() { let tp = TransformProperty::List(vec![TransformFunction::Matrix( Transform::new_unchecked(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), )]); assert_eq!(&tp, &parse_transform_prop("matrix(1,2,3,4,5,6)").unwrap()); assert!(parse_transform_prop("matrix(1 2 3 4 5 6)").is_err()); assert!(parse_transform_prop("Matrix(1,2,3,4,5,6)").is_err()); } #[test] fn test_parse_transform_property_translate() { let tpt = TransformProperty::List(vec![TransformFunction::Translate( Length::::new(100.0, LengthUnit::Px), Length::::new(200.0, LengthUnit::Px), )]); assert_eq!( &tpt, &parse_transform_prop("translate(100px,200px)").unwrap() ); assert_eq!( parse_transform_prop("translate(1)").unwrap(), parse_transform_prop("translate(1, 0)").unwrap() ); assert!(parse_transform_prop("translate(100, foo)").is_err()); assert!(parse_transform_prop("translate(100, )").is_err()); assert!(parse_transform_prop("translate(100 200)").is_err()); assert!(parse_transform_prop("translate(1px,2px,3px,4px)").is_err()); } #[test] fn test_parse_transform_property_translate_x() { let tptx = TransformProperty::List(vec![TransformFunction::TranslateX( Length::::new(100.0, LengthUnit::Px), )]); assert_eq!(&tptx, &parse_transform_prop("translateX(100px)").unwrap()); assert!(parse_transform_prop("translateX(1)").is_ok()); assert!(parse_transform_prop("translateX(100 100)").is_err()); assert!(parse_transform_prop("translatex(1px)").is_err()); assert!(parse_transform_prop("translatex(1rad)").is_err()); } #[test] fn test_parse_transform_property_translate_y() { let tpty = TransformProperty::List(vec![TransformFunction::TranslateY( Length::::new(100.0, LengthUnit::Px), )]); assert_eq!(&tpty, &parse_transform_prop("translateY(100px)").unwrap()); assert!(parse_transform_prop("translateY(1)").is_ok()); assert!(parse_transform_prop("translateY(100 100)").is_err()); assert!(parse_transform_prop("translatey(1px)").is_err()); } #[test] fn test_translate_only_supports_pixel_units() { assert!(parse_transform_prop("translate(1in, 2)").is_err()); assert!(parse_transform_prop("translate(1, 2in)").is_err()); assert!(parse_transform_prop("translateX(1cm)").is_err()); assert!(parse_transform_prop("translateY(1cm)").is_err()); } #[test] fn test_parse_transform_property_scale() { let tps = TransformProperty::List(vec![TransformFunction::Scale(1.0, 10.0)]); assert_eq!(&tps, &parse_transform_prop("scale(1,10)").unwrap()); assert_eq!( parse_transform_prop("scale(2)").unwrap(), parse_transform_prop("scale(2, 2)").unwrap() ); assert!(parse_transform_prop("scale(100, foo)").is_err()); assert!(parse_transform_prop("scale(100, )").is_err()); assert!(parse_transform_prop("scale(1 10)").is_err()); assert!(parse_transform_prop("scale(1px,10px)").is_err()); assert!(parse_transform_prop("scale(1%)").is_err()); } #[test] fn test_parse_transform_property_scale_x() { let tpsx = TransformProperty::List(vec![TransformFunction::ScaleX(10.0)]); assert_eq!(&tpsx, &parse_transform_prop("scaleX(10)").unwrap()); assert!(parse_transform_prop("scaleX(100 100)").is_err()); assert!(parse_transform_prop("scalex(10)").is_err()); assert!(parse_transform_prop("scaleX(10px)").is_err()); } #[test] fn test_parse_transform_property_scale_y() { let tpsy = TransformProperty::List(vec![TransformFunction::ScaleY(10.0)]); assert_eq!(&tpsy, &parse_transform_prop("scaleY(10)").unwrap()); assert!(parse_transform_prop("scaleY(10 1)").is_err()); assert!(parse_transform_prop("scaleY(1px)").is_err()); } #[test] fn test_parse_transform_property_rotate() { let tpr = TransformProperty::List(vec![TransformFunction::Rotate(Angle::from_degrees(100.0))]); assert_eq!(&tpr, &parse_transform_prop("rotate(100deg)").unwrap()); assert!(parse_transform_prop("rotate(100deg 100)").is_err()); assert!(parse_transform_prop("rotate(3px)").is_err()); } #[test] fn test_parse_transform_property_skew() { let tpsk = TransformProperty::List(vec![TransformFunction::Skew( Angle::from_degrees(90.0), Angle::from_degrees(120.0), )]); assert_eq!(&tpsk, &parse_transform_prop("skew(90deg,120deg)").unwrap()); assert_eq!( parse_transform_prop("skew(45deg)").unwrap(), parse_transform_prop("skew(45deg, 0)").unwrap() ); assert!(parse_transform_prop("skew(1.0,1.0)").is_ok()); assert!(parse_transform_prop("skew(1rad,1rad)").is_ok()); assert!(parse_transform_prop("skew(100, foo)").is_err()); assert!(parse_transform_prop("skew(100, )").is_err()); assert!(parse_transform_prop("skew(1.0px)").is_err()); assert!(parse_transform_prop("skew(1.0,1.0,1deg)").is_err()); } #[test] fn test_parse_transform_property_skew_x() { let tpskx = TransformProperty::List(vec![TransformFunction::SkewX(Angle::from_degrees(90.0))]); assert_eq!(&tpskx, &parse_transform_prop("skewX(90deg)").unwrap()); assert!(parse_transform_prop("skewX(1.0)").is_ok()); assert!(parse_transform_prop("skewX(1rad)").is_ok()); assert!(parse_transform_prop("skewx(1.0)").is_err()); assert!(parse_transform_prop("skewX(1.0,1.0)").is_err()); } #[test] fn test_parse_transform_property_skew_y() { let tpsky = TransformProperty::List(vec![TransformFunction::SkewY(Angle::from_degrees(90.0))]); assert_eq!(&tpsky, &parse_transform_prop("skewY(90deg)").unwrap()); assert!(parse_transform_prop("skewY(1.0)").is_ok()); assert!(parse_transform_prop("skewY(1rad)").is_ok()); assert!(parse_transform_prop("skewy(1.0)").is_err()); assert!(parse_transform_prop("skewY(1.0,1.0)").is_err()); } } librsvg-2.59.0/src/ua.css000064400000000000000000000012511046102023000132760ustar 00000000000000/* See https://www.w3.org/TR/SVG/styling.html#UAStyleSheet * * Commented out rules cannot yet be parsed. */ /* @namespace url(http://www.w3.org/2000/svg); @namespace xml url(http://www.w3.org/XML/1998/namespace); */ svg:not(:root), image, marker, pattern, symbol { overflow: hidden; } /* *:not(svg), *:not(foreignObject) > svg { transform-origin: 0 0; } *[xml|space=preserve] { text-space-collapse: preserve-spaces; } */ defs, clipPath, mask, marker, desc, title, metadata, pattern, linearGradient, radialGradient, script, style, symbol { display: none !important; } :host(use) > symbol { display: inline !important; } /* :link, :visited { cursor: pointer; } * librsvg-2.59.0/src/unit_interval.rs000064400000000000000000000060551046102023000154170ustar 00000000000000//! Type for values in the [0.0, 1.0] range. use cssparser::Parser; use crate::error::*; use crate::length::*; use crate::parsers::Parse; use crate::util; #[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)] pub struct UnitInterval(pub f64); impl UnitInterval { pub fn clamp(val: f64) -> UnitInterval { UnitInterval(util::clamp(val, 0.0, 1.0)) } } impl Parse for UnitInterval { fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let loc = parser.current_source_location(); let l: Length = Parse::parse(parser)?; match l.unit { LengthUnit::Px | LengthUnit::Percent => Ok(UnitInterval::clamp(l.length)), _ => Err(loc.new_custom_error(ValueErrorKind::value_error( " must be in default or percent units", ))), } } } impl From for u8 { fn from(val: UnitInterval) -> u8 { let UnitInterval(x) = val; (x * 255.0 + 0.5).floor() as u8 } } #[cfg(test)] mod tests { use super::*; #[test] fn clamps() { assert_eq!(UnitInterval::clamp(-1.0), UnitInterval(0.0)); assert_eq!(UnitInterval::clamp(0.0), UnitInterval(0.0)); assert_eq!(UnitInterval::clamp(0.5), UnitInterval(0.5)); assert_eq!(UnitInterval::clamp(1.0), UnitInterval(1.0)); assert_eq!(UnitInterval::clamp(2.0), UnitInterval(1.0)); } #[test] fn parses_percentages() { assert_eq!(UnitInterval::parse_str("-100%").unwrap(), UnitInterval(0.0)); assert_eq!(UnitInterval::parse_str("0%").unwrap(), UnitInterval(0.0)); assert_eq!(UnitInterval::parse_str("50%").unwrap(), UnitInterval(0.5)); assert_eq!(UnitInterval::parse_str("100%").unwrap(), UnitInterval(1.0)); assert_eq!( UnitInterval::parse_str("100.1%").unwrap(), UnitInterval(1.0) ); assert_eq!(UnitInterval::parse_str("200%").unwrap(), UnitInterval(1.0)); } #[test] fn parses_number() { assert_eq!(UnitInterval::parse_str("0").unwrap(), UnitInterval(0.0)); assert_eq!(UnitInterval::parse_str("1").unwrap(), UnitInterval(1.0)); assert_eq!(UnitInterval::parse_str("0.5").unwrap(), UnitInterval(0.5)); } #[test] fn parses_out_of_range_number() { assert_eq!(UnitInterval::parse_str("-10").unwrap(), UnitInterval(0.0)); assert_eq!(UnitInterval::parse_str("10").unwrap(), UnitInterval(1.0)); } #[test] fn errors_on_invalid_input() { assert!(UnitInterval::parse_str("").is_err()); assert!(UnitInterval::parse_str("foo").is_err()); assert!(UnitInterval::parse_str("-x").is_err()); assert!(UnitInterval::parse_str("0.0foo").is_err()); assert!(UnitInterval::parse_str("0.0%%").is_err()); assert!(UnitInterval::parse_str("%").is_err()); } #[test] fn convert() { assert_eq!(u8::from(UnitInterval(0.0)), 0); assert_eq!(u8::from(UnitInterval(0.5)), 128); assert_eq!(u8::from(UnitInterval(1.0)), 255); } } librsvg-2.59.0/src/url_resolver.rs000064400000000000000000000275201046102023000152570ustar 00000000000000//! Determine which URLs are allowed for loading. use std::fmt; use std::ops::Deref; use url::Url; use crate::error::AllowedUrlError; /// Decides which URLs are allowed to be loaded. /// /// Currently only contains the base URL. /// /// The plan is to add: /// base_only: Only allow to load content from the same base URL. By default // this restriction is enabled and requires to provide base_url. /// include_xml: Allows to use xi:include with XML. Enabled by default. /// include_text: Allows to use xi:include with text. Enabled by default. /// local_only: Only allow to load content from the local filesystem. /// Enabled by default. #[derive(Clone)] pub struct UrlResolver { /// Base URL; all relative references will be resolved with respect to this. pub base_url: Option, } impl UrlResolver { /// Creates a `UrlResolver` with defaults, and sets the `base_url`. pub fn new(base_url: Option) -> Self { UrlResolver { base_url } } /// Decides which URLs are allowed to be loaded based on the presence of a base URL. /// /// This function implements the policy described in "Security and locations of /// referenced files" in the [crate /// documentation](index.html#security-and-locations-of-referenced-files). pub fn resolve_href(&self, href: &str) -> Result { let url = Url::options() .base_url(self.base_url.as_ref()) .parse(href) .map_err(AllowedUrlError::UrlParseError)?; // Allow loads of data: from any location if url.scheme() == "data" { return Ok(AllowedUrl(url)); } // Queries are not allowed. if url.query().is_some() { return Err(AllowedUrlError::NoQueriesAllowed); } // Fragment identifiers are not allowed. They should have been stripped // upstream, by NodeId. if url.fragment().is_some() { return Err(AllowedUrlError::NoFragmentIdentifierAllowed); } // All other sources require a base url if self.base_url.is_none() { return Err(AllowedUrlError::BaseRequired); } let base_url = self.base_url.as_ref().unwrap(); // Deny loads from differing URI schemes if url.scheme() != base_url.scheme() { return Err(AllowedUrlError::DifferentUriSchemes); } // resource: is allowed to load anything from other resources if url.scheme() == "resource" { return Ok(AllowedUrl(url)); } // Non-file: isn't allowed to load anything if url.scheme() != "file" { return Err(AllowedUrlError::DisallowedScheme); } // The rest of this function assumes file: URLs; guard against // incorrect refactoring. assert!(url.scheme() == "file"); // If we have a base_uri of "file:///foo/bar.svg", and resolve an href of ".", // Url.parse() will give us "file:///foo/". We don't want that, so check // if the last path segment is empty - it will not be empty for a normal file. if let Some(segments) = url.path_segments() { if segments .last() .expect("URL path segments always contain at last 1 element") .is_empty() { return Err(AllowedUrlError::NotSiblingOrChildOfBaseFile); } } else { unreachable!("the file: URL cannot have an empty path"); } // We have two file: URIs. Now canonicalize them (remove .. and symlinks, etc.) // and see if the directories match let url_path = url .to_file_path() .map_err(|_| AllowedUrlError::InvalidPath)?; let base_path = base_url .to_file_path() .map_err(|_| AllowedUrlError::InvalidPath)?; let base_parent = base_path.parent(); if base_parent.is_none() { return Err(AllowedUrlError::BaseIsRoot); } let base_parent = base_parent.unwrap(); let path_canon = url_path .canonicalize() .map_err(|_| AllowedUrlError::CanonicalizationError)?; let parent_canon = base_parent .canonicalize() .map_err(|_| AllowedUrlError::CanonicalizationError)?; if path_canon.starts_with(parent_canon) { // Finally, convert the canonicalized path back to a URL. let path_to_url = Url::from_file_path(path_canon).unwrap(); Ok(AllowedUrl(path_to_url)) } else { Err(AllowedUrlError::NotSiblingOrChildOfBaseFile) } } } /// Wrapper for URLs which are allowed to be loaded /// /// SVG files can reference other files (PNG/JPEG images, other SVGs, /// CSS files, etc.). This object is constructed by checking whether /// a specified `href` (a possibly-relative filename, for example) /// should be allowed to be loaded, given the base URL of the SVG /// being loaded. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct AllowedUrl(Url); impl Deref for AllowedUrl { type Target = Url; fn deref(&self) -> &Url { &self.0 } } impl fmt::Display for AllowedUrl { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } #[cfg(test)] mod tests { use super::*; use std::path::PathBuf; #[test] fn disallows_relative_file_with_no_base_file() { let url_resolver = UrlResolver::new(None); assert!(matches!( url_resolver.resolve_href("foo.svg"), Err(AllowedUrlError::UrlParseError( url::ParseError::RelativeUrlWithoutBase )) )); } #[test] fn disallows_different_schemes() { let url_resolver = UrlResolver::new(Some( Url::parse("http://example.com/malicious.svg").unwrap(), )); assert!(matches!( url_resolver.resolve_href("file:///etc/passwd"), Err(AllowedUrlError::DifferentUriSchemes) )); } fn make_file_uri(p: &str) -> String { if cfg!(windows) { format!("file:///c:{}", p) } else { format!("file://{}", p) } } #[test] fn disallows_base_is_root() { let url_resolver = UrlResolver::new(Some(Url::parse(&make_file_uri("/")).unwrap())); assert!(matches!( url_resolver.resolve_href("foo.svg"), Err(AllowedUrlError::BaseIsRoot) )); } #[test] fn disallows_non_file_scheme() { let url_resolver = UrlResolver::new(Some(Url::parse("http://foo.bar/baz.svg").unwrap())); assert!(matches!( url_resolver.resolve_href("foo.svg"), Err(AllowedUrlError::DisallowedScheme) )); } #[test] fn allows_data_url_with_no_base_file() { let url_resolver = UrlResolver::new(None); assert_eq!( url_resolver .resolve_href("data:image/jpeg;base64,xxyyzz") .unwrap() .as_ref(), "data:image/jpeg;base64,xxyyzz", ); } fn url_from_test_fixtures(filename_relative_to_librsvg_srcdir: &str) -> Url { let path = PathBuf::from(filename_relative_to_librsvg_srcdir); let absolute = path .canonicalize() .expect("files from test fixtures are supposed to canonicalize"); Url::from_file_path(absolute).unwrap() } #[test] fn allows_relative() { let base_url = url_from_test_fixtures("tests/fixtures/loading/bar.svg"); let url_resolver = UrlResolver::new(Some(base_url)); let resolved = url_resolver.resolve_href("foo.svg").unwrap(); let resolved_str = resolved.as_str(); assert!(resolved_str.ends_with("/loading/foo.svg")); } #[test] fn allows_sibling() { let url_resolver = UrlResolver::new(Some(url_from_test_fixtures( "tests/fixtures/loading/bar.svg", ))); let resolved = url_resolver .resolve_href(url_from_test_fixtures("tests/fixtures/loading/foo.svg").as_str()) .unwrap(); let resolved_str = resolved.as_str(); assert!(resolved_str.ends_with("/loading/foo.svg")); } #[test] fn allows_child_of_sibling() { let url_resolver = UrlResolver::new(Some(url_from_test_fixtures( "tests/fixtures/loading/bar.svg", ))); let resolved = url_resolver .resolve_href(url_from_test_fixtures("tests/fixtures/loading/subdir/baz.svg").as_str()) .unwrap(); let resolved_str = resolved.as_str(); assert!(resolved_str.ends_with("/loading/subdir/baz.svg")); } // Ignore on Windows since we test for /etc/passwd #[cfg(unix)] #[test] fn disallows_non_sibling() { let url_resolver = UrlResolver::new(Some(url_from_test_fixtures( "tests/fixtures/loading/bar.svg", ))); assert!(matches!( url_resolver.resolve_href(&make_file_uri("/etc/passwd")), Err(AllowedUrlError::NotSiblingOrChildOfBaseFile) )); } #[test] fn disallows_queries() { let url_resolver = UrlResolver::new(Some( Url::parse(&make_file_uri("/example/bar.svg")).unwrap(), )); assert!(matches!( url_resolver.resolve_href(".?../../../../../../../../../../etc/passwd"), Err(AllowedUrlError::NoQueriesAllowed) )); } #[test] fn disallows_weird_relative_uris() { let url_resolver = UrlResolver::new(Some( Url::parse(&make_file_uri("/example/bar.svg")).unwrap(), )); assert!(url_resolver .resolve_href(".@../../../../../../../../../../etc/passwd") .is_err()); assert!(url_resolver .resolve_href(".$../../../../../../../../../../etc/passwd") .is_err()); assert!(url_resolver .resolve_href(".%../../../../../../../../../../etc/passwd") .is_err()); assert!(url_resolver .resolve_href(".*../../../../../../../../../../etc/passwd") .is_err()); assert!(url_resolver .resolve_href("~/../../../../../../../../../../etc/passwd") .is_err()); } #[test] fn disallows_dot_sibling() { let url_resolver = UrlResolver::new(Some( Url::parse(&make_file_uri("/example/bar.svg")).unwrap(), )); assert!(matches!( url_resolver.resolve_href("."), Err(AllowedUrlError::NotSiblingOrChildOfBaseFile) )); assert!(matches!( url_resolver.resolve_href(".#../../../../../../../../../../etc/passwd"), Err(AllowedUrlError::NoFragmentIdentifierAllowed) )); } #[test] fn disallows_fragment() { // UrlResolver::resolve_href() explicitly disallows fragment identifiers. // This is because they should have been stripped before calling that function, // by NodeId or the Iri machinery. let url_resolver = UrlResolver::new(Some(Url::parse("https://example.com/foo.svg").unwrap())); assert!(matches!( url_resolver.resolve_href("bar.svg#fragment"), Err(AllowedUrlError::NoFragmentIdentifierAllowed) )); } #[cfg(windows)] #[test] fn invalid_url_from_test_suite() { // This is required for Url to panic. let resolver = UrlResolver::new(Some(Url::parse("file:///c:/foo.svg").expect("initial url"))); // With this, it doesn't panic: // let resolver = UrlResolver::new(None); // The following panics, when using a base URL // match resolver.resolve_href("file://invalid.css") { // so, use a less problematic case, hopefully match resolver.resolve_href("file://") { Ok(_) => println!("yay!"), Err(e) => println!("err: {}", e), } } } librsvg-2.59.0/src/util.rs000064400000000000000000000101311046102023000134770ustar 00000000000000//! Miscellaneous utilities. use std::borrow::Cow; use std::ffi::CStr; use std::str; /// Converts a `char *` which is known to be valid UTF-8 into a `&str` /// /// The usual `from_glib_none(s)` allocates an owned String. The /// purpose of `utf8_cstr()` is to get a temporary string slice into a /// C string which is already known to be valid UTF-8; for example, /// as for strings which come from `libxml2`. /// /// Safety: `s` must be a nul-terminated, valid UTF-8 string of bytes. pub unsafe fn utf8_cstr<'a>(s: *const libc::c_char) -> &'a str { assert!(!s.is_null()); str::from_utf8_unchecked(CStr::from_ptr(s).to_bytes()) } /// Converts a `char *` which is known to be valid UTF-8 into an `Option<&str>` /// /// NULL pointers get converted to None. /// /// Safety: `s` must be null, or a nul-terminated, valid UTF-8 string of bytes. pub unsafe fn opt_utf8_cstr<'a>(s: *const libc::c_char) -> Option<&'a str> { if s.is_null() { None } else { Some(utf8_cstr(s)) } } /// Gets a known-to-be valid UTF-8 string given start/end-exclusive pointers to its bounds. /// /// Safety: `start` must be a valid pointer, and `end` must be the same for a zero-length string, /// or greater than `start`. All the bytes between them must be valid UTF-8. pub unsafe fn utf8_cstr_bounds<'a>( start: *const libc::c_char, end: *const libc::c_char, ) -> &'a str { let len = end.offset_from(start); assert!(len >= 0); utf8_cstr_len(start, len as usize) } /// Gets a known-to-be valid UTF-8 string given a pointer to its start and a length. /// /// Safety: `start` must be a valid pointer, and `len` bytes starting from it must be /// valid UTF-8. pub unsafe fn utf8_cstr_len<'a>(start: *const libc::c_char, len: usize) -> &'a str { // Convert from libc::c_char to u8. Why transmute? Because libc::c_char // is of different signedness depending on the architecture (u8 on aarch64, // i8 on x86_64). If one just uses "start as *const u8", it triggers a // trivial_casts warning. #[allow(trivial_casts)] let start = start as *const u8; let value_slice = std::slice::from_raw_parts(start, len); str::from_utf8_unchecked(value_slice) } /// Error-tolerant C string import pub unsafe fn cstr<'a>(s: *const libc::c_char) -> Cow<'a, str> { if s.is_null() { return Cow::Borrowed("(null)"); } CStr::from_ptr(s).to_string_lossy() } pub fn clamp(val: T, low: T, high: T) -> T { if val < low { low } else if val > high { high } else { val } } #[cfg(test)] mod tests { use super::*; #[allow(trivial_casts)] #[test] fn utf8_cstr_works() { unsafe { let hello = b"hello\0".as_ptr() as *const libc::c_char; assert_eq!(utf8_cstr(hello), "hello"); } } #[allow(trivial_casts)] #[test] fn opt_utf8_cstr_works() { unsafe { let hello = b"hello\0".as_ptr() as *const libc::c_char; assert_eq!(opt_utf8_cstr(hello), Some("hello")); assert_eq!(opt_utf8_cstr(std::ptr::null()), None); } } #[allow(trivial_casts)] #[test] fn utf8_cstr_bounds_works() { unsafe { let hello: *const libc::c_char = b"hello\0" as *const _ as *const _; assert_eq!(utf8_cstr_bounds(hello, hello.offset(5)), "hello"); assert_eq!(utf8_cstr_bounds(hello, hello), ""); } } #[allow(trivial_casts)] #[test] fn utf8_cstr_len_works() { unsafe { let hello: *const libc::c_char = b"hello\0" as *const _ as *const _; assert_eq!(utf8_cstr_len(hello, 5), "hello"); } } #[allow(trivial_casts)] #[test] fn cstr_works() { unsafe { let hello: *const libc::c_char = b"hello\0" as *const _ as *const _; let invalid_utf8: *const libc::c_char = b"hello\xff\0" as *const _ as *const _; assert_eq!(cstr(hello).as_ref(), "hello"); assert_eq!(cstr(std::ptr::null()).as_ref(), "(null)"); assert_eq!(cstr(invalid_utf8).as_ref(), "hello\u{fffd}"); } } } librsvg-2.59.0/src/viewbox.rs000064400000000000000000000047171046102023000142220ustar 00000000000000//! Parser for the `viewBox` attribute. use cssparser::Parser; use std::ops::Deref; use crate::error::*; use crate::parsers::{NumberList, Parse}; use crate::rect::Rect; /// Newtype around a [`Rect`], used to represent the `viewBox` attribute. /// /// A `ViewBox` is a new user-space coordinate system mapped onto the rectangle defined by /// the current viewport. See /// /// `ViewBox` derefs to [`Rect`], so you can use [`Rect`]'s methods and fields directly like /// `vbox.x0` or `vbox.width()`. #[derive(Debug, Copy, Clone, PartialEq)] pub struct ViewBox(Rect); impl Deref for ViewBox { type Target = Rect; fn deref(&self) -> &Rect { &self.0 } } impl From for ViewBox { fn from(r: Rect) -> ViewBox { ViewBox(r) } } impl Parse for ViewBox { // Parse a viewBox attribute // https://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute // // viewBox: double [,] double [,] double [,] double [,] // // x, y, w, h // // Where w and h must be nonnegative. fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result> { let loc = parser.current_source_location(); let NumberList::<4, 4>(v) = NumberList::parse(parser)?; let (x, y, width, height) = (v[0], v[1], v[2], v[3]); if width >= 0.0 && height >= 0.0 { Ok(ViewBox(Rect::new(x, y, x + width, y + height))) } else { Err(loc.new_custom_error(ValueErrorKind::value_error( "width and height must not be negative", ))) } } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_valid_viewboxes() { assert_eq!( ViewBox::parse_str(" 1 2 3 4").unwrap(), ViewBox(Rect::new(1.0, 2.0, 4.0, 6.0)) ); assert_eq!( ViewBox::parse_str(" -1.5 -2.5e1,34,56e2 ").unwrap(), ViewBox(Rect::new(-1.5, -25.0, 32.5, 5575.0)) ); } #[test] fn parsing_invalid_viewboxes_yields_error() { assert!(ViewBox::parse_str("").is_err()); assert!(ViewBox::parse_str(" 1,2,-3,-4 ").is_err()); assert!(ViewBox::parse_str("qwerasdfzxcv").is_err()); assert!(ViewBox::parse_str(" 1 2 3 4 5").is_err()); assert!(ViewBox::parse_str(" 1 2 foo 3 4").is_err()); // https://gitlab.gnome.org/GNOME/librsvg/issues/344 assert!(ViewBox::parse_str("0 0 9E80.7").is_err()); } } librsvg-2.59.0/src/xml/attributes.rs000064400000000000000000000167461046102023000155320ustar 00000000000000//! Store XML element attributes and their values. use std::slice; use std::str; use markup5ever::{ expanded_name, local_name, namespace_url, ns, LocalName, Namespace, Prefix, QualName, }; use string_cache::DefaultAtom; use crate::error::{ImplementationLimit, LoadingError}; use crate::limits; use crate::util::{opt_utf8_cstr, utf8_cstr, utf8_cstr_bounds}; /// Type used to store attribute values. /// /// Attribute values are often repeated in an SVG file, so we intern them using the /// string_cache crate. pub type AttributeValue = DefaultAtom; /// Iterable wrapper for libxml2's representation of attribute/value. /// /// See the [`new_from_xml2_attributes`] function for information. /// /// [`new_from_xml2_attributes`]: #method.new_from_xml2_attributes #[derive(Clone)] pub struct Attributes { attrs: Box<[(QualName, AttributeValue)]>, id_idx: Option, class_idx: Option, } /// Iterator from `Attributes.iter`. pub struct AttributesIter<'a>(slice::Iter<'a, (QualName, AttributeValue)>); #[cfg(test)] impl Default for Attributes { fn default() -> Self { Self::new() } } impl Attributes { #[cfg(test)] pub fn new() -> Attributes { Attributes { attrs: [].into(), id_idx: None, class_idx: None, } } /// Creates an iterable `Attributes` from the C array of borrowed C strings. /// /// With libxml2's SAX parser, the caller's startElementNsSAX2Func /// callback gets passed a `xmlChar **` for attributes, which /// comes in groups of (localname/prefix/URI/value_start/value_end). /// In those, localname/prefix/URI are NUL-terminated strings; /// value_start and value_end point to the start-inclusive and /// end-exclusive bytes in the attribute's value. /// /// # Safety /// /// This function is unsafe because the caller must guarantee the following: /// /// * `attrs` is a valid pointer, with (n_attributes * 5) elements. /// /// * All strings are valid UTF-8. pub unsafe fn new_from_xml2_attributes( n_attributes: usize, attrs: *const *const libc::c_char, ) -> Result { let mut array = Vec::with_capacity(n_attributes); let mut id_idx = None; let mut class_idx = None; if n_attributes > limits::MAX_LOADED_ATTRIBUTES { return Err(LoadingError::LimitExceeded( ImplementationLimit::TooManyAttributes, )); } if n_attributes > 0 && !attrs.is_null() { for attr in slice::from_raw_parts(attrs, n_attributes * 5).chunks_exact(5) { let localname = attr[0]; let prefix = attr[1]; let uri = attr[2]; let value_start = attr[3]; let value_end = attr[4]; assert!(!localname.is_null()); let localname = utf8_cstr(localname); let prefix = opt_utf8_cstr(prefix); let uri = opt_utf8_cstr(uri); let qual_name = QualName::new( prefix.map(Prefix::from), uri.map(Namespace::from) .unwrap_or_else(|| namespace_url!("")), LocalName::from(localname), ); if !value_start.is_null() && !value_end.is_null() { assert!(value_end >= value_start); let value_str = utf8_cstr_bounds(value_start, value_end); let value_atom = DefaultAtom::from(value_str); let idx = array.len() as u16; match qual_name.expanded() { expanded_name!("", "id") => id_idx = Some(idx), expanded_name!("", "class") => class_idx = Some(idx), _ => (), } array.push((qual_name, value_atom)); } } } Ok(Attributes { attrs: array.into(), id_idx, class_idx, }) } /// Returns the number of attributes. pub fn len(&self) -> usize { self.attrs.len() } /// Creates an iterator that yields `(QualName, &'a str)` tuples. pub fn iter(&self) -> AttributesIter<'_> { AttributesIter(self.attrs.iter()) } pub fn get_id(&self) -> Option<&str> { self.id_idx.and_then(|idx| { self.attrs .get(usize::from(idx)) .map(|(_name, value)| &value[..]) }) } pub fn get_class(&self) -> Option<&str> { self.class_idx.and_then(|idx| { self.attrs .get(usize::from(idx)) .map(|(_name, value)| &value[..]) }) } pub fn clear_class(&mut self) { self.class_idx = None; } } impl<'a> Iterator for AttributesIter<'a> { type Item = (QualName, &'a str); fn next(&mut self) -> Option { self.0.next().map(|(a, v)| (a.clone(), v.as_ref())) } } #[cfg(test)] mod tests { use super::*; use markup5ever::{expanded_name, local_name, namespace_url, ns}; use std::ffi::CString; use std::ptr; #[test] fn empty_attributes() { let map = unsafe { Attributes::new_from_xml2_attributes(0, ptr::null()).unwrap() }; assert_eq!(map.len(), 0); } #[test] fn attributes_with_namespaces() { let attrs = [ ( CString::new("href").unwrap(), Some(CString::new("xlink").unwrap()), Some(CString::new("http://www.w3.org/1999/xlink").unwrap()), CString::new("1").unwrap(), ), ( CString::new("ry").unwrap(), None, None, CString::new("2").unwrap(), ), ( CString::new("d").unwrap(), None, None, CString::new("").unwrap(), ), ]; let mut v: Vec<*const libc::c_char> = Vec::new(); for (localname, prefix, uri, val) in &attrs { v.push(localname.as_ptr()); v.push( prefix .as_ref() .map(|p: &CString| p.as_ptr()) .unwrap_or_else(ptr::null), ); v.push( uri.as_ref() .map(|p: &CString| p.as_ptr()) .unwrap_or_else(ptr::null), ); let val_start = val.as_ptr(); let val_end = unsafe { val_start.add(val.as_bytes().len()) }; v.push(val_start); // value_start v.push(val_end); // value_end } let attrs = unsafe { Attributes::new_from_xml2_attributes(3, v.as_ptr()).unwrap() }; let mut had_href: bool = false; let mut had_ry: bool = false; let mut had_d: bool = false; for (a, v) in attrs.iter() { match a.expanded() { expanded_name!(xlink "href") => { assert!(v == "1"); had_href = true; } expanded_name!("", "ry") => { assert!(v == "2"); had_ry = true; } expanded_name!("", "d") => { assert!(v.is_empty()); had_d = true; } _ => unreachable!(), } } assert!(had_href); assert!(had_ry); assert!(had_d); } } librsvg-2.59.0/src/xml/mod.rs000064400000000000000000000637431046102023000141220ustar 00000000000000//! The main XML parser. use encoding_rs::Encoding; use gio::{ prelude::BufferedInputStreamExt, BufferedInputStream, Cancellable, ConverterInputStream, InputStream, ZlibCompressorFormat, ZlibDecompressor, }; use glib::object::Cast; use markup5ever::{ expanded_name, local_name, namespace_url, ns, ExpandedName, LocalName, Namespace, QualName, }; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use std::str; use std::string::ToString; use std::sync::Arc; use xml5ever::buffer_queue::BufferQueue; use xml5ever::tendril::format_tendril; use xml5ever::tokenizer::{TagKind, Token, TokenSink, XmlTokenizer, XmlTokenizerOpts}; use crate::borrow_element_as; use crate::css::{Origin, Stylesheet}; use crate::document::{Document, DocumentBuilder, LoadOptions}; use crate::error::{ImplementationLimit, LoadingError}; use crate::io::{self, IoError}; use crate::limits::{MAX_LOADED_ELEMENTS, MAX_XINCLUDE_DEPTH}; use crate::node::{Node, NodeBorrow}; use crate::rsvg_log; use crate::session::Session; use crate::style::StyleType; use crate::url_resolver::AllowedUrl; use xml2_load::Xml2Parser; mod attributes; mod xml2; mod xml2_load; use xml2::xmlEntityPtr; pub use attributes::Attributes; #[derive(Clone)] enum Context { // Starting state Start, // Creating nodes for elements under the current node ElementCreation, // Inside "##, br##" "##, ); test_compare_render_output!( image_auto_width_height, 30, 30, br##" "##, br##" "##, ); test_compare_render_output!( rect_auto_width_height, 30, 30, br##" "##, br##" "## ); test_compare_render_output!( svg_auto_width_height, 30, 30, br##" "##, br##" "##, ); test_compare_render_output!( use_context_stroke, 100, 20, br##" "##, br##" "##, ); test_svg_reference!( isolation, "tests/fixtures/reftests/svg2-reftests/isolation.svg", "tests/fixtures/reftests/svg2-reftests/isolation-ref.svg" ); test_svg_reference!( mask_and_opacity, "tests/fixtures/reftests/svg2-reftests/mask-and-opacity.svg", "tests/fixtures/reftests/svg2-reftests/mask-and-opacity-ref.svg" ); test_svg_reference!( gaussian_blur_nonpositive_913, "tests/fixtures/reftests/svg2-reftests/bug913-gaussian-blur-nonpositive.svg", "tests/fixtures/reftests/svg2-reftests/bug913-gaussian-blur-nonpositive-ref.svg" ); test_svg_reference!( bug_880_horizontal_vertical_stroked_lines, "tests/fixtures/reftests/bugs-reftests/bug880-stroke-wide-line.svg", "tests/fixtures/reftests/bugs-reftests/bug880-stroke-wide-line-ref.svg" ); test_svg_reference!( bug_92_symbol_clip, "tests/fixtures/reftests/bugs-reftests/bug92-symbol-clip.svg", "tests/fixtures/reftests/bugs-reftests/bug92-symbol-clip-ref.svg" ); test_svg_reference!( bug_875_svg_use_width_height, "tests/fixtures/reftests/bugs-reftests/bug875-svg-use-width-height.svg", "tests/fixtures/reftests/bugs-reftests/bug875-svg-use-width-height-ref.svg" ); test_svg_reference!( bug_885_vector_effect_non_scaling_stroke, "tests/fixtures/reftests/bugs-reftests/bug885-vector-effect-non-scaling-stroke.svg", "tests/fixtures/reftests/bugs-reftests/bug885-vector-effect-non-scaling-stroke-ref.svg" ); test_svg_reference!( bug_930_invalid_clip_path_transform, "tests/fixtures/reftests/bugs-reftests/bug930-invalid-clip-path-transform.svg", "tests/fixtures/reftests/bugs-reftests/bug930-invalid-clip-path-transform-ref.svg" ); test_svg_reference!( bug_985_image_rendering_property, "tests/fixtures/reftests/svg2-reftests/image-rendering-985.svg", "tests/fixtures/reftests/svg2-reftests/image-rendering-985-ref.svg" ); test_svg_reference!( bug_992_use_symbol_cascade, "tests/fixtures/reftests/bugs/use-symbol-cascade-992.svg", "tests/fixtures/reftests/bugs/use-symbol-cascade-992-ref.svg" ); test_svg_reference!( color_types, "tests/fixtures/reftests/color-types.svg", "tests/fixtures/reftests/color-types-ref.svg" ); // Note that this uses the same reference file as color-types.svg - the result ought to be the same. test_svg_reference!( color_property_color_types, "tests/fixtures/reftests/color-property-color-types.svg", "tests/fixtures/reftests/color-types-ref.svg" ); test_svg_reference!( color_types_unsupported, "tests/fixtures/reftests/color-types-unsupported.svg", "tests/fixtures/reftests/color-types-unsupported-ref.svg" ); test_svg_reference!( invalid_gradient_transform, "tests/fixtures/reftests/invalid-gradient-transform.svg", "tests/fixtures/reftests/invalid-gradient-transform-ref.svg" ); test_svg_reference!( xinclude_data_url, "tests/fixtures/reftests/xinclude-data-url.svg", "tests/fixtures/reftests/xinclude-data-url-ref.svg" ); test_svg_reference!( markers_arc_segments, "tests/fixtures/reftests/markers-arc-segments.svg", "tests/fixtures/reftests/markers-arc-segments-ref.svg" ); test_svg_reference!( bug_1121_feimage_embedded_svg, "tests/fixtures/reftests/bugs-reftests/bug1121-feimage-embedded-svg.svg", "tests/fixtures/reftests/bugs-reftests/bug1121-feimage-embedded-svg-ref.svg" ); librsvg-2.59.0/tests/render_crash.rs000064400000000000000000000113021046102023000155350ustar 00000000000000//! Tests for crashes in the rendering stage. //! //! Ensures that redering a particular SVG doesn't crash, but we don't care //! about the resulting image or even whether there were errors during rendering. use rsvg::{CairoRenderer, Loader}; use std::path::PathBuf; fn render_crash(filename: &str) { let mut full_filename = PathBuf::new(); full_filename.push("tests/fixtures/render-crash"); full_filename.push(filename); let handle = Loader::new() .read_path(&full_filename) .unwrap_or_else(|e| panic!("could not load: {}", e)); let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 100, 100).unwrap(); let cr = cairo::Context::new(&surface).expect("Failed to create a cairo context"); // We just test for crashes during rendering, and don't care about success/error. let _ = CairoRenderer::new(&handle) .render_document(&cr, &cairo::Rectangle::new(0.0, 0.0, 100.0, 100.0)); } macro_rules! t { ($test_name:ident, $filename:expr) => { #[test] fn $test_name() { render_crash($filename); } }; } #[rustfmt::skip] mod tests { use super::*; t!(bug187_set_gradient_on_empty_path_svg, "bug187-set-gradient-on-empty-path.svg"); t!(bug193_filters_conv_05_f_svg, "bug193-filters-conv-05-f.svg"); t!(bug227_negative_dasharray_value_svg, "bug227-negative-dasharray-value.svg"); t!(bug266_filters_with_error_attributes_svg, "bug266-filters-with-error-attributes.svg"); t!(bug277_filter_on_empty_group_svg, "bug277-filter-on-empty-group.svg"); t!(bug292_clip_empty_group_svg, "bug292-clip-empty-group.svg"); t!(bug293_mask_empty_group_svg, "bug293-mask-empty-group.svg"); t!(bug324_empty_svg_svg, "bug324-empty-svg.svg"); t!(bug337_font_ex_svg, "bug337-font-ex.svg"); t!(bug338_zero_sized_image_svg, "bug338-zero-sized-image.svg"); t!(bug340_marker_with_zero_sized_vbox_svg, "bug340-marker-with-zero-sized-vbox.svg"); t!(bug342_use_references_ancestor_svg, "bug342-use-references-ancestor.svg"); t!(bug343_fecomponenttransfer_child_in_error_svg, "bug343-feComponentTransfer-child-in-error.svg"); t!(bug344_too_large_viewbox_svg, "bug344-too-large-viewbox.svg"); t!(bug345_too_large_size_svg, "bug345-too-large-size.svg"); t!(bug395_femorphology_negative_scaling_svg, "bug395-feMorphology-negative-scaling.svg"); t!(bug497_path_with_all_invalid_commands_svg, "bug497-path-with-all-invalid-commands.svg"); t!(bug581491_zero_sized_text_svg, "bug581491-zero-sized-text.svg"); t!(bug588_big_viewbox_yields_invalid_transform_svg, "bug588-big-viewbox-yields-invalid-transform.svg"); t!(bug591_vbox_overflow_svg, "bug591-vbox-overflow.svg"); t!(bug593_mask_empty_bbox_svg, "bug593-mask-empty-bbox.svg"); t!(bug721_pattern_cycle_from_child_svg, "bug721-pattern-cycle-from-child.svg"); t!(bug721_pattern_cycle_from_other_child_svg, "bug721-pattern-cycle-from-other-child.svg"); t!(bug777155_zero_sized_pattern_svg, "bug777155-zero-sized-pattern.svg"); t!(bug928_empty_fetile_bounds_svg, "bug928-empty-feTile-bounds.svg"); t!(bug932_too_big_font_size, "bug932-too-big-font-size.svg"); t!(bug1059_feoffset_overflow, "bug1059-feoffset-overflow.svg"); t!(bug1060_zero_sized_image_from_data_uri, "bug1060-zero-sized-image-from-data-uri.svg"); t!(bug1062_feturbulence_limit_numoctaves, "bug1062-feTurbulence-limit-numOctaves.svg"); t!(bug1092_fuzz_recursive_use_stack_overflow, "bug1092-fuzz-recursive-use-stack-overflow.svg"); t!(bug1115_feturbulence_overflow, "bug1115-feTurbulence-overflow.svg"); t!(femerge_color_interpolation_srgb_svg, "feMerge-color-interpolation-srgb.svg"); t!(filters_non_invertible_paffine_svg, "filters-non-invertible-paffine.svg"); t!(gradient_with_empty_bbox_svg, "gradient-with-empty-bbox.svg"); t!(gradient_with_no_children_svg, "gradient-with-no-children.svg"); t!(pattern_with_empty_bbox_svg, "pattern-with-empty-bbox.svg"); t!(pattern_with_no_children_svg, "pattern-with-no-children.svg"); t!(pixelrectangle_duplicate_crash_svg, "PixelRectangle-duplicate-crash.svg"); t!(recursive_feimage_svg, "recursive-feimage.svg"); } librsvg-2.59.0/tests/shapes.rs000064400000000000000000000006531046102023000143700ustar 00000000000000use rsvg::test_svg_reference; test_svg_reference!( ellipse_auto_rx_ry, "tests/fixtures/reftests/svg2-reftests/ellipse-auto-rx-ry.svg", "tests/fixtures/reftests/svg2-reftests/ellipse-auto-rx-ry-ref.svg" ); test_svg_reference!( ellipse_single_auto_rx_ry, "tests/fixtures/reftests/svg2-reftests/ellipse-single-auto-rx-ry.svg", "tests/fixtures/reftests/svg2-reftests/ellipse-single-auto-rx-ry-ref.svg" ); librsvg-2.59.0/tests/text.rs000064400000000000000000000077351046102023000141010ustar 00000000000000use float_cmp::approx_eq; use rsvg::{CairoRenderer, Loader}; use rsvg::test_utils::setup_font_map; use rsvg::{test_compare_render_output, test_svg_reference}; // From https://www.w3.org/Style/CSS/Test/Fonts/Ahem/ // // > The Ahem font was developed by Todd Fahrner and Myles C. Maxfield to // > help test writers develop predictable tests. The units per em is 1000, // > the ascent is 800, and the descent is 200, thereby making the em // > square exactly square. The glyphs for most characters is simply a box // > which fills this square. The codepoints mapped to this full square // > with a full advance are the following ranges: // // So, ascent is 4/5 of the font-size, descent is 1/5. Mind the positions below. test_compare_render_output!( ahem_font, 500, 500, br##" abcde "##, br##" "##, ); test_svg_reference!( text_anchor_chunk_806, "tests/fixtures/text/bug806-text-anchor-chunk.svg", "tests/fixtures/text/bug806-text-anchor-chunk-ref.svg" ); test_svg_reference!( span_bounds_when_offset_by_dx, "tests/fixtures/text/span-bounds-when-offset-by-dx.svg", "tests/fixtures/text/span-bounds-when-offset-by-dx-ref.svg" ); // FIXME: Ignored because with the change to render all text as paths, the rendering // of these is different. // // test_svg_reference!( // tspan_direction_change_804, // "tests/fixtures/text/bug804-tspan-direction-change.svg", // "tests/fixtures/text/bug804-tspan-direction-change-ref.svg" // ); test_svg_reference!( unicode_bidi_override, "tests/fixtures/text/unicode-bidi-override.svg", "tests/fixtures/text/unicode-bidi-override-ref.svg" ); test_svg_reference!( display_none, "tests/fixtures/text/display-none.svg", "tests/fixtures/text/display-none-ref.svg" ); test_svg_reference!( visibility_hidden, "tests/fixtures/text/visibility-hidden.svg", "tests/fixtures/text/visibility-hidden-ref.svg" ); test_svg_reference!( visibility_hidden_x_attr, "tests/fixtures/text/visibility-hidden-x-attr.svg", "tests/fixtures/text/visibility-hidden-ref.svg" ); test_svg_reference!( bounds, "tests/fixtures/text/bounds.svg", "tests/fixtures/text/bounds-ref.svg" ); fn rect(x: f64, y: f64, width: f64, height: f64) -> cairo::Rectangle { cairo::Rectangle::new(x, y, width, height) } fn rectangle_approx_eq(a: &cairo::Rectangle, b: &cairo::Rectangle) -> bool { // FIXME: this is super fishy; shouldn't we be using 2x the epsilon against the width/height // instead of the raw coordinates? approx_eq!(f64, a.x(), b.x()) && approx_eq!(f64, a.y(), b.y()) && approx_eq!(f64, a.width(), b.width()) && approx_eq!(f64, a.height(), b.height()) } // Test that the computed geometry of text layers is as expected. #[test] fn test_text_layer_geometry() { setup_font_map(); let handle = Loader::new() .read_path("tests/fixtures/text/bounds.svg") .unwrap_or_else(|e| panic!("could not load: {}", e)); let renderer = CairoRenderer::new(&handle).test_mode(true); let viewport = rect(0.0, 0.0, 600.0, 600.0); // tuples of (element_id, ink_rect) let cases = vec![ ("#a", rect(50.0, 60.0, 100.0, 50.0)), ("#b", rect(200.0, 60.0, 50.0, 100.0)), ("#c", rect(300.0, 60.0, 50.0, 100.0)), ("#d", rect(400.0, 60.0, 100.0, 50.0)), ]; for (id, expected_ink_rect) in cases { let (ink_rect, _) = renderer.geometry_for_layer(Some(id), &viewport).unwrap(); assert!( rectangle_approx_eq(&ink_rect, &expected_ink_rect), "ink_rect: {:?}, expected: {:?}", ink_rect, expected_ink_rect ); } }