svgtypes-0.15.2/.cargo_vcs_info.json0000644000000001360000000000100130050ustar { "git": { "sha1": "fe22aac50fe28b13c4f82bb06e40cd23367bc3cf" }, "path_in_vcs": "" }svgtypes-0.15.2/.github/workflows/main.yml000064400000000000000000000004551046102023000166450ustar 00000000000000name: svgtypes on: [push, pull_request] env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest strategy: matrix: rust: - 1.65.0 - stable steps: - name: Checkout uses: actions/checkout@v2 - name: Test run: cargo test svgtypes-0.15.2/.gitignore000064400000000000000000000000621046102023000135630ustar 00000000000000target **/*.rs.bk Cargo.lock /.idea /svgtypes.iml svgtypes-0.15.2/CHANGELOG.md000064400000000000000000000152361046102023000134150ustar 00000000000000# Change Log All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ## [0.15.2] - 2024-08-20 ### Fixed - Path parsing with `S` or `T` segments after `A`. Was broken since v0.12 ## [0.15.1] - 2024-05-07 ### Fixed - Allow double quotes in FuncIRI. ## [0.15.0] - 2024-04-03 ### Changed - Bump `kurbo` and `siphasher` ## [0.14.0] - 2024-02-05 ### Added - `font-family` parsing. [@LaurenzV](https://github.com/LaurenzV) - `font` shorthand parsing. [@LaurenzV](https://github.com/LaurenzV) ## [0.13.0] - 2023-12-03 ### Added - `Paint::ContextFill` and `Paint::ContextStroke`. [@LaurenzV](https://github.com/LaurenzV) - `transform-origin` parsing. [@LaurenzV](https://github.com/LaurenzV) ## [0.12.0] - 2023-10-01 ### Added - Allow parsing of float RGB values from CSS Color 4 draft like `rgb(3.14, 110, 201)`. The values itself would still be stored as `u8`. [@yisibl](https://github.com/yisibl) - Allow quotes in FuncIRI. [@romanzes](https://github.com/romanzes) ## [0.11.0] - 2023-03-25 ### Added - `SimplifyingPathParser` handles implicit MoveTo commands after ClosePath now. Previously, `M 10 20 L 30 40 Z L 50 60` would have been parsed as is, but now it will be parsed as `M 10 20 L 30 40 Z M 10 20 L 50 60`. ## [0.10.0] - 2023-02-04 ### Changed - Bump `kurbo` - Bump MSRV to 1.65 (because of `kurbo`) ## [0.9.0] - 2022-12-25 ### Added - `SimplifyingPathParser` that allows parsing an already simplified Path Data. ## [0.8.2] - 2022-10-23 ### Added - `paint-order` ## [0.8.1] - 2022-06-11 ### Added - Support #RRGGBBAA and #RGBA color notation as per CSS Color 4. [@demurgos](https://github.com/demurgos) ## [0.8.0] - 2021-09-12 ### Added - `EnableBackground` - `Number`. Previously accessible via `Steam::parse_number`. - `IRI`. Previously accessible via `Steam::parse_iri`. - `FuncIRI`. Previously accessible via `Steam::parse_func_iri`. ### Changed - `Stream` is now private. ## [0.7.0] - 2021-09-04 **Breaking**: Almost a complete rewrite. This crate is strictly a parser from now. ### Added - [``](https://www.w3.org/TR/filter-effects-1/#typedef-filter-value-list) parsing using `FilterValueListParser`. - `ViewBoxError` ### Removed - Writing support. - Container types. Only stack allocated types and pull-based parsers are available. - `FuzzyEq` and `FuzzyZero`. ## [0.6.0] - 2021-08-22 ### Added - CSS3 colors support (`rgba`, `hsl`, `hsla`, `transparent`). - `turn` angle unit. - `Angle::to_degrees`. ### Changed - Move to Rust 2018 edition. - Rename `Stream::skip_string` into `Stream::consume_string`. - Rename `Color::new` into `Color::new_rgb`. - `Color` struct gained an `alpha` field. - Rename `Angle::num` into `Angle::number`. - Rename `Length::num` into `Length::number`. ## [0.5.0] - 2019-08-12 ### Added - Implement `Default` for `Length`, `LengthList`, `NumberList`, `Points` and `Path`. ### Changed - The minimum Rust version is 1.31 ### Removed - `PathBuilder`. Use `Path::push_*` instead. - `Style` parser. Use an external CSS parser instead, like `simplecss`. - `ElementId` and `AttributeId`. - `phf` dependency. Only `siphasher` is used now. ## [0.4.4] - 2019-06-11 - Update `float-cmp`. ## [0.4.3] - 2019-06-10 ### Added - `Transform::prepend`. - Implement `FuzzyEq` and `FuzzyZero` for `f32`. - Parsing of `Color`, `Paint`, `ElementId` and `AttributeId` can be disabled now. ## [0.4.2] - 2019-03-15 ### Changed - The `XmlByteExt` trait is private now. ## [0.4.1] - 2019-01-06 ### Fixed - Style with comments parsing. ## [0.4.0] - 2019-01-02 ### Added - An [`angle`](https://www.w3.org/TR/SVG11/types.html#DataTypeAngle) value type. ### Changed - `Length::from_str` will return an error if an input string has trailing data. So length like `1mmx` was previously parsed without errors. ## [0.3.0] - 2018-12-13 ### Changed - `PathParser` will return `Result` instead of `PathSegment` from now. - `Error` was rewritten. ### Removed - `FromSpan` trait. Use `FromStr`. - `StrSpan`. All strings are `&str` now. - `TextPos`. All errors have position in characters now. - `xmlparser` dependency. - `log` dependency. ## [0.2.0] - 2018-09-12 ### Added - `black`, `white`, `gray`, `red`, `green` and `blue` constructors to the `Color` struct. ### Changed - `StyleParser` will return `(StrSpan, StrSpan)` and not `StyleToken` from now. - `StyleParser` requires entity references to be resolved before parsing from now. ### Removed - `failure` dependency. - `StyleToken`. - `Error::InvalidEntityRef`. ## [0.1.1] - 2018-05-23 ### Added - `encoding` and `standalone` to AttributeId. - `new_translate`, `new_scale`, `new_rotate`, `new_rotate_at`, `new_skew_x`, `new_skew_y` and `rotate_at` methods to the `Transform`. ### Changed - `StreamExt::parse_iri` and `StreamExt::parse_func_iri` will parse not only well-formed data now. ### Fixed - `Paint::from_span` poor performance. [Unreleased]: https://github.com/RazrFalcon/svgtypes/compare/v0.15.2...HEAD [0.15.2]: https://github.com/RazrFalcon/svgtypes/compare/v0.15.1...v0.15.2 [0.15.1]: https://github.com/RazrFalcon/svgtypes/compare/v0.15.0...v0.15.1 [0.15.0]: https://github.com/RazrFalcon/svgtypes/compare/v0.14.0...v0.15.0 [0.14.0]: https://github.com/RazrFalcon/svgtypes/compare/v0.13.0...v0.14.0 [0.13.0]: https://github.com/RazrFalcon/svgtypes/compare/v0.12.0...v0.13.0 [0.12.0]: https://github.com/RazrFalcon/svgtypes/compare/v0.11.0...v0.12.0 [0.11.0]: https://github.com/RazrFalcon/svgtypes/compare/v0.10.0...v0.11.0 [0.10.0]: https://github.com/RazrFalcon/svgtypes/compare/v0.9.0...v0.10.0 [0.9.0]: https://github.com/RazrFalcon/svgtypes/compare/v0.8.2...v0.9.0 [0.8.2]: https://github.com/RazrFalcon/svgtypes/compare/v0.8.1...v0.8.2 [0.8.1]: https://github.com/RazrFalcon/svgtypes/compare/v0.8.0...v0.8.1 [0.8.0]: https://github.com/RazrFalcon/svgtypes/compare/v0.7.0...v0.8.0 [0.7.0]: https://github.com/RazrFalcon/svgtypes/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/RazrFalcon/svgtypes/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/RazrFalcon/svgtypes/compare/v0.4.4...v0.5.0 [0.4.4]: https://github.com/RazrFalcon/svgtypes/compare/v0.4.3...v0.4.4 [0.4.3]: https://github.com/RazrFalcon/svgtypes/compare/v0.4.2...v0.4.3 [0.4.2]: https://github.com/RazrFalcon/svgtypes/compare/v0.4.1...v0.4.2 [0.4.1]: https://github.com/RazrFalcon/svgtypes/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/RazrFalcon/svgtypes/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/RazrFalcon/svgtypes/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/RazrFalcon/svgtypes/compare/v0.1.1...v0.2.0 [0.1.1]: https://github.com/RazrFalcon/svgtypes/compare/v0.1.0...v0.1.1 svgtypes-0.15.2/Cargo.toml0000644000000017520000000000100110100ustar # 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 = "2018" name = "svgtypes" version = "0.15.2" authors = ["Yevhenii Reizner "] exclude = [ "benches/", "codegen/", "fuzz/", ] description = "SVG types parser." documentation = "https://docs.rs/svgtypes/" readme = "README.md" keywords = [ "svg", "parser", "tokenizer", ] categories = ["parser-implementations"] license = "MIT/Apache-2.0" repository = "https://github.com/RazrFalcon/svgtypes" [dependencies.kurbo] version = "0.11" [dependencies.siphasher] version = "1.0" svgtypes-0.15.2/Cargo.toml.orig000064400000000000000000000011001046102023000144540ustar 00000000000000[package] name = "svgtypes" version = "0.15.2" authors = ["Yevhenii Reizner "] categories = ["parser-implementations"] description = "SVG types parser." documentation = "https://docs.rs/svgtypes/" keywords = ["svg", "parser", "tokenizer"] license = "MIT/Apache-2.0" edition = "2018" readme = "README.md" repository = "https://github.com/RazrFalcon/svgtypes" exclude = ["benches/", "codegen/", "fuzz/"] [workspace] members = ["benches"] [dependencies] siphasher = "1.0" # perfect hash implementation for color names kurbo = "0.11" # ArcTo to CurveTo(s) svgtypes-0.15.2/LICENSE-APACHE000064400000000000000000000251371046102023000135310ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. svgtypes-0.15.2/LICENSE-MIT000064400000000000000000000020451046102023000132320ustar 00000000000000Copyright (c) 2018 Yevhenii Reizner Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. svgtypes-0.15.2/README.md000064400000000000000000000066541046102023000130670ustar 00000000000000## svgtypes ![Build Status](https://github.com/RazrFalcon/svgtypes/workflows/svgtypes/badge.svg) [![Crates.io](https://img.shields.io/crates/v/svgtypes.svg)](https://crates.io/crates/svgtypes) [![Documentation](https://docs.rs/svgtypes/badge.svg)](https://docs.rs/svgtypes) [![Rust 1.65+](https://img.shields.io/badge/rust-1.65+-orange.svg)](https://www.rust-lang.org) ![](https://img.shields.io/badge/unsafe-forbidden-brightgreen.svg) *svgtypes* is a collection of parsers for [SVG](https://www.w3.org/TR/SVG2/) types. ### Supported SVG types - [``](https://www.w3.org/TR/css-color-3/) - [``](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumber) - [``](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGLength) - [``](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGAngle) - [``](https://www.w3.org/TR/SVG2/coords.html#ViewBoxAttribute) - [``](https://www.w3.org/TR/SVG2/paths.html#PathData) - [``](https://www.w3.org/TR/SVG11/types.html#DataTypeTransformList) - [`transform-origin`](https://drafts.csswg.org/css-transforms/#transform-origin-property) - [``](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumberList) - [``](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGLengthList) - [``](https://www.w3.org/TR/SVG11/shapes.html#PointsBNF) - [``](https://www.w3.org/TR/filter-effects-1/#typedef-filter-value-list) - [``](https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint) - [``](https://www.w3.org/TR/SVG11/coords.html#PreserveAspectRatioAttribute) - [``](https://www.w3.org/TR/SVG11/filters.html#EnableBackgroundProperty) - [``](https://www.w3.org/TR/SVG11/types.html#DataTypeIRI) - [``](https://www.w3.org/TR/SVG11/types.html#DataTypeFuncIRI) - [`paint-order`](https://www.w3.org/TR/SVG2/painting.html#PaintOrder) - [``](https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#font-family-prop) - [`font`](https://www.w3.org/TR/css-fonts-3/#font-prop) ### Features - Complete support of paths, so data like `M10-20A5.5.3-4 110-.1` will be parsed correctly. - Implicit path commands will be automatically converted into explicit one. - Some SVG2 data types support. - Pretty fast. ### Limitations - Accepts only [normalized](https://www.w3.org/TR/REC-xml/#AVNormalize) values, e.g. an input text should not contain ` ` or `&data;`. - All keywords must be lowercase. Case-insensitive parsing is supported only for colors (requires allocation for named colors). - The `` followed by the `` is not supported. As the `` itself. - [System colors](https://www.w3.org/TR/css3-color/#css2-system), like `fill="AppWorkspace"`, are not supported. They were deprecated anyway. ### Safety - The library should not panic. Any panic considered as a critical bug and should be reported. - The library forbids unsafe code. ### Alternatives None. ### License Licensed under either of - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. svgtypes-0.15.2/src/angle.rs000064400000000000000000000064511046102023000140260ustar 00000000000000use crate::{Error, Stream}; /// List of all SVG angle units. #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[allow(missing_docs)] pub enum AngleUnit { Degrees, Gradians, Radians, Turns, } /// Representation of the [``] type. /// /// [``]: https://www.w3.org/TR/css-values-3/#angles #[derive(Clone, Copy, PartialEq, Debug)] #[allow(missing_docs)] pub struct Angle { pub number: f64, pub unit: AngleUnit, } impl Angle { /// Constructs a new angle. #[inline] pub fn new(number: f64, unit: AngleUnit) -> Angle { Angle { number, unit } } /// Converts angle to degrees. #[inline] pub fn to_degrees(&self) -> f64 { match self.unit { AngleUnit::Degrees => self.number, AngleUnit::Gradians => self.number * 180.0 / 200.0, AngleUnit::Radians => self.number.to_degrees(), AngleUnit::Turns => self.number * 360.0, } } } impl std::str::FromStr for Angle { type Err = Error; #[inline] fn from_str(text: &str) -> Result { let mut s = Stream::from(text); let l = s.parse_angle()?; if !s.at_end() { return Err(Error::UnexpectedData(s.calc_char_pos())); } Ok(Angle::new(l.number, l.unit)) } } impl<'a> Stream<'a> { /// Parses angle from the stream. /// /// /// /// # Notes /// /// - Suffix must be lowercase, otherwise it will be an error. pub fn parse_angle(&mut self) -> Result { self.skip_spaces(); let n = self.parse_number()?; if self.at_end() { return Ok(Angle::new(n, AngleUnit::Degrees)); } let u = if self.starts_with(b"deg") { self.advance(3); AngleUnit::Degrees } else if self.starts_with(b"grad") { self.advance(4); AngleUnit::Gradians } else if self.starts_with(b"rad") { self.advance(3); AngleUnit::Radians } else if self.starts_with(b"turn") { self.advance(4); AngleUnit::Turns } else { AngleUnit::Degrees }; Ok(Angle::new(n, u)) } } #[rustfmt::skip] #[cfg(test)] mod tests { use super::*; use std::str::FromStr; macro_rules! test_p { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { assert_eq!(Angle::from_str($text).unwrap(), $result); } ) } test_p!(parse_1, "1", Angle::new(1.0, AngleUnit::Degrees)); test_p!(parse_2, "1deg", Angle::new(1.0, AngleUnit::Degrees)); test_p!(parse_3, "1grad", Angle::new(1.0, AngleUnit::Gradians)); test_p!(parse_4, "1rad", Angle::new(1.0, AngleUnit::Radians)); test_p!(parse_5, "1turn", Angle::new(1.0, AngleUnit::Turns)); #[test] fn err_1() { let mut s = Stream::from("1q"); assert_eq!(s.parse_angle().unwrap(), Angle::new(1.0, AngleUnit::Degrees)); assert_eq!(s.parse_angle().unwrap_err().to_string(), "invalid number at position 2"); } #[test] fn err_2() { assert_eq!(Angle::from_str("1degq").unwrap_err().to_string(), "unexpected data at position 5"); } } svgtypes-0.15.2/src/aspect_ratio.rs000064400000000000000000000072411046102023000154130ustar 00000000000000use crate::{Error, Stream}; /// Representation of the `align` value of the [`preserveAspectRatio`] attribute. /// /// [`preserveAspectRatio`]: https://www.w3.org/TR/SVG11/coords.html#PreserveAspectRatioAttribute #[allow(missing_docs)] #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Align { None, XMinYMin, XMidYMin, XMaxYMin, XMinYMid, XMidYMid, XMaxYMid, XMinYMax, XMidYMax, XMaxYMax, } /// Representation of the [`preserveAspectRatio`] attribute. /// /// SVG 2 removed the `defer` keyword, but we still support it. /// /// [`preserveAspectRatio`]: https://www.w3.org/TR/SVG11/coords.html#PreserveAspectRatioAttribute #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct AspectRatio { /// `` value. /// /// Set to `true` when `defer` value is present. pub defer: bool, /// `` value. pub align: Align, /// `` value. /// /// - Set to `true` when `slice` value is present. /// - Set to `false` when `meet` value is present or value is not set at all. pub slice: bool, } impl std::str::FromStr for AspectRatio { type Err = Error; fn from_str(text: &str) -> Result { let mut s = Stream::from(text); s.skip_spaces(); let defer = s.starts_with(b"defer"); if defer { s.advance(5); s.consume_byte(b' ')?; s.skip_spaces(); } let start = s.pos(); let align = s.consume_ascii_ident(); let align = match align { "none" => Align::None, "xMinYMin" => Align::XMinYMin, "xMidYMin" => Align::XMidYMin, "xMaxYMin" => Align::XMaxYMin, "xMinYMid" => Align::XMinYMid, "xMidYMid" => Align::XMidYMid, "xMaxYMid" => Align::XMaxYMid, "xMinYMax" => Align::XMinYMax, "xMidYMax" => Align::XMidYMax, "xMaxYMax" => Align::XMaxYMax, _ => return Err(Error::UnexpectedData(s.calc_char_pos_at(start))), }; s.skip_spaces(); let mut slice = false; if !s.at_end() { let start = s.pos(); let v = s.consume_ascii_ident(); match v { "meet" => {} "slice" => slice = true, "" => {} _ => return Err(Error::UnexpectedData(s.calc_char_pos_at(start))), }; } Ok(AspectRatio { defer, align, slice, }) } } impl Default for AspectRatio { #[inline] fn default() -> Self { AspectRatio { defer: false, align: Align::XMidYMid, slice: false, } } } #[rustfmt::skip] #[cfg(test)] mod tests { use super::*; use std::str::FromStr; macro_rules! test { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { let v = AspectRatio::from_str($text).unwrap(); assert_eq!(v, $result); } ) } test!(parse_1, "none", AspectRatio { defer: false, align: Align::None, slice: false, }); test!(parse_2, "defer none", AspectRatio { defer: true, align: Align::None, slice: false, }); test!(parse_3, "xMinYMid", AspectRatio { defer: false, align: Align::XMinYMid, slice: false, }); test!(parse_4, "xMinYMid slice", AspectRatio { defer: false, align: Align::XMinYMid, slice: true, }); test!(parse_5, "xMinYMid meet", AspectRatio { defer: false, align: Align::XMinYMid, slice: false, }); } svgtypes-0.15.2/src/color.rs000064400000000000000000000353231046102023000140560ustar 00000000000000use crate::{colors, ByteExt, Error, Stream}; /// Representation of the [``] type. /// /// [``]: https://www.w3.org/TR/css-color-3/ #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[allow(missing_docs)] pub struct Color { pub red: u8, pub green: u8, pub blue: u8, pub alpha: u8, } impl Color { /// Constructs a new `Color` from RGB values. #[inline] pub fn new_rgb(red: u8, green: u8, blue: u8) -> Color { Color { red, green, blue, alpha: 255, } } /// Constructs a new `Color` from RGBA values. #[inline] pub fn new_rgba(red: u8, green: u8, blue: u8, alpha: u8) -> Color { Color { red, green, blue, alpha, } } /// Constructs a new `Color` set to black. #[inline] pub fn black() -> Color { Color::new_rgb(0, 0, 0) } /// Constructs a new `Color` set to white. #[inline] pub fn white() -> Color { Color::new_rgb(255, 255, 255) } /// Constructs a new `Color` set to gray. #[inline] pub fn gray() -> Color { Color::new_rgb(128, 128, 128) } /// Constructs a new `Color` set to red. #[inline] pub fn red() -> Color { Color::new_rgb(255, 0, 0) } /// Constructs a new `Color` set to green. #[inline] pub fn green() -> Color { Color::new_rgb(0, 128, 0) } /// Constructs a new `Color` set to blue. #[inline] pub fn blue() -> Color { Color::new_rgb(0, 0, 255) } } impl std::str::FromStr for Color { type Err = Error; /// Parses [CSS3](https://www.w3.org/TR/css-color-3/) `Color` from a string. /// /// # Errors /// /// - Returns error if a color has an invalid format. /// - Returns error if `` is followed by ``. It's not supported. /// /// # Notes /// /// - Any non-`hexdigit` bytes will be treated as `0`. /// - The [SVG 1.1 spec] has an error. /// There should be a `number`, not an `integer` for percent values ([details]). /// - It also supports 4 digits and 8 digits hex notation from the /// [CSS Color Module Level 4][css-color-4-hex]. /// /// [SVG 1.1 spec]: https://www.w3.org/TR/SVG11/types.html#DataTypeColor /// [details]: https://lists.w3.org/Archives/Public/www-svg/2014Jan/0109.html /// [css-color-4-hex]: https://www.w3.org/TR/css-color-4/#hex-notation fn from_str(text: &str) -> Result { let mut s = Stream::from(text); let color = s.parse_color()?; // Check that we are at the end of the stream. Otherwise color can be followed by icccolor, // which is not supported. s.skip_spaces(); if !s.at_end() { return Err(Error::UnexpectedData(s.calc_char_pos())); } Ok(color) } } impl<'a> Stream<'a> { /// Tries to parse a color, but doesn't advance on error. pub fn try_parse_color(&mut self) -> Option { let mut s = *self; if let Ok(color) = s.parse_color() { *self = s; Some(color) } else { None } } /// Parses a color. pub fn parse_color(&mut self) -> Result { self.skip_spaces(); let mut color = Color::black(); if self.curr_byte()? == b'#' { // See https://www.w3.org/TR/css-color-4/#hex-notation self.advance(1); let color_str = self.consume_bytes(|_, c| c.is_hex_digit()).as_bytes(); // get color data len until first space or stream end match color_str.len() { 6 => { // #rrggbb color.red = hex_pair(color_str[0], color_str[1]); color.green = hex_pair(color_str[2], color_str[3]); color.blue = hex_pair(color_str[4], color_str[5]); } 8 => { // #rrggbbaa color.red = hex_pair(color_str[0], color_str[1]); color.green = hex_pair(color_str[2], color_str[3]); color.blue = hex_pair(color_str[4], color_str[5]); color.alpha = hex_pair(color_str[6], color_str[7]); } 3 => { // #rgb color.red = short_hex(color_str[0]); color.green = short_hex(color_str[1]); color.blue = short_hex(color_str[2]); } 4 => { // #rgba color.red = short_hex(color_str[0]); color.green = short_hex(color_str[1]); color.blue = short_hex(color_str[2]); color.alpha = short_hex(color_str[3]); } _ => { return Err(Error::InvalidValue); } } } else { // TODO: remove allocation let name = self.consume_ascii_ident().to_ascii_lowercase(); if name == "rgb" || name == "rgba" { self.consume_byte(b'(')?; let mut is_percent = false; let value = self.parse_number()?; if self.starts_with(b"%") { self.advance(1); is_percent = true; } self.skip_spaces(); self.parse_list_separator(); if is_percent { fn from_percent(v: f64) -> u8 { let n = (v * 255.0).round() as i32; bound(0, n, 255) as u8 } color.red = from_percent(value / 100.0); color.green = from_percent(self.parse_list_number_or_percent()?); color.blue = from_percent(self.parse_list_number_or_percent()?); } else { color.red = f64_bound(0.0, (value.round() as i32).into(), 255.0) as u8; color.green = f64_bound(0.0, self.parse_list_number()?.round(), 255.0) as u8; color.blue = f64_bound(0.0, self.parse_list_number()?.round(), 255.0) as u8; } self.skip_spaces(); if !self.starts_with(b")") { color.alpha = (f64_bound(0.0, self.parse_list_number()?, 1.0) * 255.0) as u8; } self.skip_spaces(); self.consume_byte(b')')?; } else if name == "hsl" || name == "hsla" { self.consume_byte(b'(')?; let mut hue = self.parse_list_integer()?; hue = ((hue % 360) + 360) % 360; let saturation = f64_bound(0.0, self.parse_list_number_or_percent()?, 1.0); let lightness = f64_bound(0.0, self.parse_list_number_or_percent()?, 1.0); color = hsl_to_rgb(hue as f32 / 60.0, saturation as f32, lightness as f32); self.skip_spaces(); if !self.starts_with(b")") { color.alpha = (f64_bound(0.0, self.parse_list_number()?, 1.0) * 255.0) as u8; } self.skip_spaces(); self.consume_byte(b')')?; } else { match colors::from_str(&name) { Some(c) => { color = c; } None => { return Err(Error::InvalidValue); } } } } Ok(color) } } #[inline] fn from_hex(c: u8) -> u8 { match c { b'0'..=b'9' => c - b'0', b'a'..=b'f' => c - b'a' + 10, b'A'..=b'F' => c - b'A' + 10, _ => b'0', } } #[inline] fn short_hex(c: u8) -> u8 { let h = from_hex(c); (h << 4) | h } #[inline] fn hex_pair(c1: u8, c2: u8) -> u8 { let h1 = from_hex(c1); let h2 = from_hex(c2); (h1 << 4) | h2 } // `hue` is in a 0..6 range, while `saturation` and `lightness` are in a 0..=1 range. // Based on https://www.w3.org/TR/css-color-3/#hsl-color fn hsl_to_rgb(hue: f32, saturation: f32, lightness: f32) -> Color { let t2 = if lightness <= 0.5 { lightness * (saturation + 1.0) } else { lightness + saturation - (lightness * saturation) }; let t1 = lightness * 2.0 - t2; let red = hue_to_rgb(t1, t2, hue + 2.0); let green = hue_to_rgb(t1, t2, hue); let blue = hue_to_rgb(t1, t2, hue - 2.0); Color::new_rgb( (red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8, ) } fn hue_to_rgb(t1: f32, t2: f32, mut hue: f32) -> f32 { if hue < 0.0 { hue += 6.0; } if hue >= 6.0 { hue -= 6.0; } if hue < 1.0 { (t2 - t1) * hue + t1 } else if hue < 3.0 { t2 } else if hue < 4.0 { (t2 - t1) * (4.0 - hue) + t1 } else { t1 } } #[inline] fn bound(min: T, val: T, max: T) -> T { std::cmp::max(min, std::cmp::min(max, val)) } #[inline] fn f64_bound(min: f64, val: f64, max: f64) -> f64 { debug_assert!(min.is_finite()); debug_assert!(val.is_finite()); debug_assert!(max.is_finite()); val.max(min).min(max) } #[rustfmt::skip] #[cfg(test)] mod tests { use std::str::FromStr; use crate::Color; macro_rules! test { ($name:ident, $text:expr, $color:expr) => { #[test] fn $name() { assert_eq!(Color::from_str($text).unwrap(), $color); } }; } test!( rrggbb, "#ff0000", Color::new_rgb(255, 0, 0) ); test!( rrggbb_upper, "#FF0000", Color::new_rgb(255, 0, 0) ); test!( rgb_hex, "#f00", Color::new_rgb(255, 0, 0) ); test!( rrggbbaa, "#ff0000ff", Color::new_rgba(255, 0, 0, 255) ); test!( rrggbbaa_upper, "#FF0000FF", Color::new_rgba(255, 0, 0, 255) ); test!( rgba_hex, "#f00f", Color::new_rgba(255, 0, 0, 255) ); test!( rrggbb_spaced, " #ff0000 ", Color::new_rgb(255, 0, 0) ); test!( rgb_numeric, "rgb(254, 203, 231)", Color::new_rgb(254, 203, 231) ); test!( rgb_numeric_spaced, " rgb( 77 , 77 , 77 ) ", Color::new_rgb(77, 77, 77) ); test!( rgb_percentage, "rgb(50%, 50%, 50%)", Color::new_rgb(128, 128, 128) ); test!( rgb_percentage_overflow, "rgb(140%, -10%, 130%)", Color::new_rgb(255, 0, 255) ); test!( rgb_percentage_float, "rgb(33.333%,46.666%,93.333%)", Color::new_rgb(85, 119, 238) ); test!( rgb_numeric_upper_case, "RGB(254, 203, 231)", Color::new_rgb(254, 203, 231) ); test!( rgb_numeric_mixed_case, "RgB(254, 203, 231)", Color::new_rgb(254, 203, 231) ); test!( rgb_numeric_red_float, "rgb(3.141592653, 110, 201)", Color::new_rgb(3, 110, 201) ); test!( rgb_numeric_green_float, "rgb(254, 150.829521289232389, 210)", Color::new_rgb(254, 151, 210) ); test!( rgb_numeric_blue_float, "rgb(96, 255, 0.2)", Color::new_rgb(96, 255, 0) ); test!( rgb_numeric_all_float, "rgb(0.0, 129.82, 231.092)", Color::new_rgb(0, 130, 231) ); test!( rgb_numeric_all_float_with_alpha, "rgb(0.0, 129.82, 231.092, 0.5)", Color::new_rgba(0, 130, 231, 127) ); test!( rgb_numeric_all_float_overflow, "rgb(290.2, 255.9, 300.0)", Color::new_rgb(255, 255, 255) ); test!( name_red, "red", Color::new_rgb(255, 0, 0) ); test!( name_red_spaced, " red ", Color::new_rgb(255, 0, 0) ); test!( name_red_upper_case, "RED", Color::new_rgb(255, 0, 0) ); test!( name_red_mixed_case, "ReD", Color::new_rgb(255, 0, 0) ); test!( name_cornflowerblue, "cornflowerblue", Color::new_rgb(100, 149, 237) ); test!( transparent, "transparent", Color::new_rgba(0, 0, 0, 0) ); test!( rgba_half, "rgba(10, 20, 30, 0.5)", Color::new_rgba(10, 20, 30, 127) ); test!( rgba_numeric_red_float, "rgba(3.141592653, 110, 201, 1.0)", Color::new_rgba(3, 110, 201, 255) ); test!( rgba_numeric_all_float, "rgba(0.0, 129.82, 231.092, 1.5)", Color::new_rgba(0, 130, 231, 255) ); test!( rgba_negative, "rgba(10, 20, 30, -2)", Color::new_rgba(10, 20, 30, 0) ); test!( rgba_large_alpha, "rgba(10, 20, 30, 2)", Color::new_rgba(10, 20, 30, 255) ); test!( rgb_with_alpha, "rgb(10, 20, 30, 0.5)", Color::new_rgba(10, 20, 30, 127) ); test!( hsl_green, "hsl(120, 100%, 75%)", Color::new_rgba(127, 255, 127, 255) ); test!( hsl_yellow, "hsl(60, 100%, 50%)", Color::new_rgba(255, 255, 0, 255) ); test!( hsl_hue_360, "hsl(360, 100%, 100%)", Color::new_rgba(255, 255, 255, 255) ); test!( hsl_out_of_bounds, "hsl(800, 150%, -50%)", Color::new_rgba(0, 0, 0, 255) ); test!( hsla_green, "hsla(120, 100%, 75%, 0.5)", Color::new_rgba(127, 255, 127, 127) ); test!( hsl_with_alpha, "hsl(120, 100%, 75%, 0.5)", Color::new_rgba(127, 255, 127, 127) ); macro_rules! test_err { ($name:ident, $text:expr, $err:expr) => { #[test] fn $name() { assert_eq!(Color::from_str($text).unwrap_err().to_string(), $err); } }; } test_err!( not_a_color_1, "text", "invalid value" ); test_err!( icc_color_not_supported_1, "#CD853F icc-color(acmecmyk, 0.11, 0.48, 0.83, 0.00)", "unexpected data at position 9" ); test_err!( icc_color_not_supported_2, "red icc-color(acmecmyk, 0.11, 0.48, 0.83, 0.00)", "unexpected data at position 5" ); test_err!( invalid_input_1, "rgb(-0\x0d", "unexpected end of stream" ); test_err!( invalid_input_2, "#9ߞpx! ;", "invalid value" ); test_err!( rgba_with_percent_alpha, "rgba(10, 20, 30, 5%)", "expected ')' not '%' at position 19" ); test_err!( rgb_mixed_units, "rgb(140%, -10mm, 130pt)", "invalid number at position 14" ); } svgtypes-0.15.2/src/colors.rs000064400000000000000000000321741046102023000142420ustar 00000000000000// This file is autogenerated. Do not edit it! use crate::Color; static COLORS: Map = Map { key: 3213172566270843353, disps: &[ (0, 37), (0, 74), (1, 0), (0, 22), (0, 92), (2, 125), (0, 73), (0, 8), (0, 2), (0, 4), (26, 16), (67, 121), (0, 3), (0, 4), (0, 60), (0, 39), (0, 130), (0, 15), (3, 38), (0, 136), (7, 75), (6, 141), (7, 67), (0, 14), (0, 128), (27, 111), (1, 31), (0, 12), (2, 49), (0, 2), ], entries: &[ ("lightgrey", Color { red: 211, green: 211, blue: 211, alpha: 255 }), ("lavenderblush", Color { red: 255, green: 240, blue: 245, alpha: 255 }), ("deeppink", Color { red: 255, green: 20, blue: 147, alpha: 255 }), ("seashell", Color { red: 255, green: 245, blue: 238, alpha: 255 }), ("lightsalmon", Color { red: 255, green: 160, blue: 122, alpha: 255 }), ("green", Color { red: 0, green: 128, blue: 0, alpha: 255 }), ("lightgreen", Color { red: 144, green: 238, blue: 144, alpha: 255 }), ("black", Color { red: 0, green: 0, blue: 0, alpha: 255 }), ("deepskyblue", Color { red: 0, green: 191, blue: 255, alpha: 255 }), ("mistyrose", Color { red: 255, green: 228, blue: 225, alpha: 255 }), ("silver", Color { red: 192, green: 192, blue: 192, alpha: 255 }), ("dimgray", Color { red: 105, green: 105, blue: 105, alpha: 255 }), ("navajowhite", Color { red: 255, green: 222, blue: 173, alpha: 255 }), ("royalblue", Color { red: 65, green: 105, blue: 225, alpha: 255 }), ("peru", Color { red: 205, green: 133, blue: 63, alpha: 255 }), ("darkgrey", Color { red: 169, green: 169, blue: 169, alpha: 255 }), ("steelblue", Color { red: 70, green: 130, blue: 180, alpha: 255 }), ("teal", Color { red: 0, green: 128, blue: 128, alpha: 255 }), ("orangered", Color { red: 255, green: 69, blue: 0, alpha: 255 }), ("mediumslateblue", Color { red: 123, green: 104, blue: 238, alpha: 255 }), ("blueviolet", Color { red: 138, green: 43, blue: 226, alpha: 255 }), ("cornflowerblue", Color { red: 100, green: 149, blue: 237, alpha: 255 }), ("cyan", Color { red: 0, green: 255, blue: 255, alpha: 255 }), ("beige", Color { red: 245, green: 245, blue: 220, alpha: 255 }), ("goldenrod", Color { red: 218, green: 165, blue: 32, alpha: 255 }), ("rosybrown", Color { red: 188, green: 143, blue: 143, alpha: 255 }), ("yellow", Color { red: 255, green: 255, blue: 0, alpha: 255 }), ("blue", Color { red: 0, green: 0, blue: 255, alpha: 255 }), ("darkblue", Color { red: 0, green: 0, blue: 139, alpha: 255 }), ("aliceblue", Color { red: 240, green: 248, blue: 255, alpha: 255 }), ("white", Color { red: 255, green: 255, blue: 255, alpha: 255 }), ("mediumblue", Color { red: 0, green: 0, blue: 205, alpha: 255 }), ("dodgerblue", Color { red: 30, green: 144, blue: 255, alpha: 255 }), ("limegreen", Color { red: 50, green: 205, blue: 50, alpha: 255 }), ("purple", Color { red: 128, green: 0, blue: 128, alpha: 255 }), ("lightsteelblue", Color { red: 176, green: 196, blue: 222, alpha: 255 }), ("lightslategray", Color { red: 119, green: 136, blue: 153, alpha: 255 }), ("seagreen", Color { red: 46, green: 139, blue: 87, alpha: 255 }), ("mediumvioletred", Color { red: 199, green: 21, blue: 133, alpha: 255 }), ("slategrey", Color { red: 112, green: 128, blue: 144, alpha: 255 }), ("darkslategrey", Color { red: 47, green: 79, blue: 79, alpha: 255 }), ("turquoise", Color { red: 64, green: 224, blue: 208, alpha: 255 }), ("paleturquoise", Color { red: 175, green: 238, blue: 238, alpha: 255 }), ("lightgoldenrodyellow", Color { red: 250, green: 250, blue: 210, alpha: 255 }), ("magenta", Color { red: 255, green: 0, blue: 255, alpha: 255 }), ("darkseagreen", Color { red: 143, green: 188, blue: 143, alpha: 255 }), ("lightcyan", Color { red: 224, green: 255, blue: 255, alpha: 255 }), ("lightcoral", Color { red: 240, green: 128, blue: 128, alpha: 255 }), ("mediumseagreen", Color { red: 60, green: 179, blue: 113, alpha: 255 }), ("palegoldenrod", Color { red: 238, green: 232, blue: 170, alpha: 255 }), ("palegreen", Color { red: 152, green: 251, blue: 152, alpha: 255 }), ("darkslateblue", Color { red: 72, green: 61, blue: 139, alpha: 255 }), ("moccasin", Color { red: 255, green: 228, blue: 181, alpha: 255 }), ("forestgreen", Color { red: 34, green: 139, blue: 34, alpha: 255 }), ("darkkhaki", Color { red: 189, green: 183, blue: 107, alpha: 255 }), ("chartreuse", Color { red: 127, green: 255, blue: 0, alpha: 255 }), ("floralwhite", Color { red: 255, green: 250, blue: 240, alpha: 255 }), ("snow", Color { red: 255, green: 250, blue: 250, alpha: 255 }), ("fuchsia", Color { red: 255, green: 0, blue: 255, alpha: 255 }), ("orchid", Color { red: 218, green: 112, blue: 214, alpha: 255 }), ("darkorchid", Color { red: 153, green: 50, blue: 204, alpha: 255 }), ("darkred", Color { red: 139, green: 0, blue: 0, alpha: 255 }), ("darksalmon", Color { red: 233, green: 150, blue: 122, alpha: 255 }), ("crimson", Color { red: 220, green: 20, blue: 60, alpha: 255 }), ("lime", Color { red: 0, green: 255, blue: 0, alpha: 255 }), ("palevioletred", Color { red: 219, green: 112, blue: 147, alpha: 255 }), ("lightseagreen", Color { red: 32, green: 178, blue: 170, alpha: 255 }), ("ivory", Color { red: 255, green: 255, blue: 240, alpha: 255 }), ("powderblue", Color { red: 176, green: 224, blue: 230, alpha: 255 }), ("aquamarine", Color { red: 127, green: 255, blue: 212, alpha: 255 }), ("darkturquoise", Color { red: 0, green: 206, blue: 209, alpha: 255 }), ("lavender", Color { red: 230, green: 230, blue: 250, alpha: 255 }), ("azure", Color { red: 240, green: 255, blue: 255, alpha: 255 }), ("mediumturquoise", Color { red: 72, green: 209, blue: 204, alpha: 255 }), ("lightgray", Color { red: 211, green: 211, blue: 211, alpha: 255 }), ("transparent", Color { red: 0, green: 0, blue: 0, alpha: 0 }), ("gainsboro", Color { red: 220, green: 220, blue: 220, alpha: 255 }), ("olivedrab", Color { red: 107, green: 142, blue: 35, alpha: 255 }), ("papayawhip", Color { red: 255, green: 239, blue: 213, alpha: 255 }), ("tomato", Color { red: 255, green: 99, blue: 71, alpha: 255 }), ("midnightblue", Color { red: 25, green: 25, blue: 112, alpha: 255 }), ("pink", Color { red: 255, green: 192, blue: 203, alpha: 255 }), ("yellowgreen", Color { red: 154, green: 205, blue: 50, alpha: 255 }), ("slategray", Color { red: 112, green: 128, blue: 144, alpha: 255 }), ("red", Color { red: 255, green: 0, blue: 0, alpha: 255 }), ("indigo", Color { red: 75, green: 0, blue: 130, alpha: 255 }), ("orange", Color { red: 255, green: 165, blue: 0, alpha: 255 }), ("grey", Color { red: 128, green: 128, blue: 128, alpha: 255 }), ("wheat", Color { red: 245, green: 222, blue: 179, alpha: 255 }), ("darkgoldenrod", Color { red: 184, green: 134, blue: 11, alpha: 255 }), ("lawngreen", Color { red: 124, green: 252, blue: 0, alpha: 255 }), ("lightslategrey", Color { red: 119, green: 136, blue: 153, alpha: 255 }), ("burlywood", Color { red: 222, green: 184, blue: 135, alpha: 255 }), ("aqua", Color { red: 0, green: 255, blue: 255, alpha: 255 }), ("saddlebrown", Color { red: 139, green: 69, blue: 19, alpha: 255 }), ("oldlace", Color { red: 253, green: 245, blue: 230, alpha: 255 }), ("lightskyblue", Color { red: 135, green: 206, blue: 250, alpha: 255 }), ("violet", Color { red: 238, green: 130, blue: 238, alpha: 255 }), ("dimgrey", Color { red: 105, green: 105, blue: 105, alpha: 255 }), ("darkorange", Color { red: 255, green: 140, blue: 0, alpha: 255 }), ("lightblue", Color { red: 173, green: 216, blue: 230, alpha: 255 }), ("khaki", Color { red: 240, green: 230, blue: 140, alpha: 255 }), ("coral", Color { red: 255, green: 127, blue: 80, alpha: 255 }), ("brown", Color { red: 165, green: 42, blue: 42, alpha: 255 }), ("mediumpurple", Color { red: 147, green: 112, blue: 219, alpha: 255 }), ("linen", Color { red: 250, green: 240, blue: 230, alpha: 255 }), ("mediumorchid", Color { red: 186, green: 85, blue: 211, alpha: 255 }), ("indianred", Color { red: 205, green: 92, blue: 92, alpha: 255 }), ("maroon", Color { red: 128, green: 0, blue: 0, alpha: 255 }), ("firebrick", Color { red: 178, green: 34, blue: 34, alpha: 255 }), ("skyblue", Color { red: 135, green: 206, blue: 235, alpha: 255 }), ("darkgray", Color { red: 169, green: 169, blue: 169, alpha: 255 }), ("hotpink", Color { red: 255, green: 105, blue: 180, alpha: 255 }), ("olive", Color { red: 128, green: 128, blue: 0, alpha: 255 }), ("sienna", Color { red: 160, green: 82, blue: 45, alpha: 255 }), ("cadetblue", Color { red: 95, green: 158, blue: 160, alpha: 255 }), ("darkslategray", Color { red: 47, green: 79, blue: 79, alpha: 255 }), ("slateblue", Color { red: 106, green: 90, blue: 205, alpha: 255 }), ("plum", Color { red: 221, green: 160, blue: 221, alpha: 255 }), ("mediumspringgreen", Color { red: 0, green: 250, blue: 154, alpha: 255 }), ("thistle", Color { red: 216, green: 191, blue: 216, alpha: 255 }), ("mintcream", Color { red: 245, green: 255, blue: 250, alpha: 255 }), ("darkmagenta", Color { red: 139, green: 0, blue: 139, alpha: 255 }), ("lemonchiffon", Color { red: 255, green: 250, blue: 205, alpha: 255 }), ("bisque", Color { red: 255, green: 228, blue: 196, alpha: 255 }), ("antiquewhite", Color { red: 250, green: 235, blue: 215, alpha: 255 }), ("darkgreen", Color { red: 0, green: 100, blue: 0, alpha: 255 }), ("whitesmoke", Color { red: 245, green: 245, blue: 245, alpha: 255 }), ("lightpink", Color { red: 255, green: 182, blue: 193, alpha: 255 }), ("darkcyan", Color { red: 0, green: 139, blue: 139, alpha: 255 }), ("tan", Color { red: 210, green: 180, blue: 140, alpha: 255 }), ("blanchedalmond", Color { red: 255, green: 235, blue: 205, alpha: 255 }), ("honeydew", Color { red: 240, green: 255, blue: 240, alpha: 255 }), ("salmon", Color { red: 250, green: 128, blue: 114, alpha: 255 }), ("lightyellow", Color { red: 255, green: 255, blue: 224, alpha: 255 }), ("springgreen", Color { red: 0, green: 255, blue: 127, alpha: 255 }), ("cornsilk", Color { red: 255, green: 248, blue: 220, alpha: 255 }), ("sandybrown", Color { red: 244, green: 164, blue: 96, alpha: 255 }), ("mediumaquamarine", Color { red: 102, green: 205, blue: 170, alpha: 255 }), ("darkviolet", Color { red: 148, green: 0, blue: 211, alpha: 255 }), ("darkolivegreen", Color { red: 85, green: 107, blue: 47, alpha: 255 }), ("gold", Color { red: 255, green: 215, blue: 0, alpha: 255 }), ("peachpuff", Color { red: 255, green: 218, blue: 185, alpha: 255 }), ("greenyellow", Color { red: 173, green: 255, blue: 47, alpha: 255 }), ("gray", Color { red: 128, green: 128, blue: 128, alpha: 255 }), ("navy", Color { red: 0, green: 0, blue: 128, alpha: 255 }), ("ghostwhite", Color { red: 248, green: 248, blue: 255, alpha: 255 }), ("chocolate", Color { red: 210, green: 105, blue: 30, alpha: 255 }), ], }; pub fn from_str(text: &str) -> Option { COLORS.get(text).cloned() } // A stripped down `phf` crate fork. // // https://github.com/sfackler/rust-phf use std::hash::Hasher; pub struct Map { pub key: u64, pub disps: &'static [(u32, u32)], pub entries: &'static[(&'static str, V)], } impl Map { pub fn get(&self, key: &str) -> Option<&V> { let hash = hash(key, self.key); let index = get_index(hash, self.disps, self.entries.len()); let entry = &self.entries[index as usize]; if entry.0 == key { Some(&entry.1) } else { None } } } #[inline] fn hash(x: &str, key: u64) -> u64 { let mut hasher = siphasher::sip::SipHasher13::new_with_keys(0, key); hasher.write(x.as_bytes()); hasher.finish() } #[inline] fn get_index(hash: u64, disps: &[(u32, u32)], len: usize) -> u32 { let (g, f1, f2) = split(hash); let (d1, d2) = disps[(g % (disps.len() as u32)) as usize]; displace(f1, f2, d1, d2) % (len as u32) } #[inline] fn split(hash: u64) -> (u32, u32, u32) { const BITS: u32 = 21; const MASK: u64 = (1 << BITS) - 1; ((hash & MASK) as u32, ((hash >> BITS) & MASK) as u32, ((hash >> (2 * BITS)) & MASK) as u32) } #[inline] fn displace(f1: u32, f2: u32, d1: u32, d2: u32) -> u32 { d2 + f1 * d1 + f2 } svgtypes-0.15.2/src/directional_position.rs000064400000000000000000000107561046102023000171640ustar 00000000000000use crate::{Error, Length, LengthUnit, Stream}; /// List of all SVG directional positions. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum DirectionalPosition { /// The `top` position. Top, /// The `center` position. Center, /// The `bottom` position. Bottom, /// The `right` position. Right, /// The `left` position. Left, } impl DirectionalPosition { /// Checks whether the value can be a horizontal position. #[inline] pub fn is_horizontal(&self) -> bool { match self { DirectionalPosition::Center | DirectionalPosition::Left | DirectionalPosition::Right => true, _ => false, } } /// Checks whether the value can be a vertical position. #[inline] pub fn is_vertical(&self) -> bool { match self { DirectionalPosition::Center | DirectionalPosition::Top | DirectionalPosition::Bottom => true, _ => false, } } } impl From for Length { fn from(value: DirectionalPosition) -> Self { match value { DirectionalPosition::Left | DirectionalPosition::Top => { Length::new(0.0, LengthUnit::Percent) } DirectionalPosition::Right | DirectionalPosition::Bottom => { Length::new(100.0, LengthUnit::Percent) } DirectionalPosition::Center => Length::new(50.0, LengthUnit::Percent), } } } impl std::str::FromStr for DirectionalPosition { type Err = Error; #[inline] fn from_str(text: &str) -> Result { let mut s = Stream::from(text); let dir_pos = s.parse_directional_position()?; if !s.at_end() { return Err(Error::UnexpectedData(s.calc_char_pos())); } Ok(dir_pos) } } impl<'a> Stream<'a> { /// Parses a directional position [`left`, `center`, `right`, `bottom`, `top`] from the stream. pub fn parse_directional_position(&mut self) -> Result { self.skip_spaces(); if self.starts_with(b"left") { self.advance(4); return Ok(DirectionalPosition::Left); } else if self.starts_with(b"right") { self.advance(5); return Ok(DirectionalPosition::Right); } else if self.starts_with(b"top") { self.advance(3); return Ok(DirectionalPosition::Top); } else if self.starts_with(b"bottom") { self.advance(6); return Ok(DirectionalPosition::Bottom); } else if self.starts_with(b"center") { self.advance(6); return Ok(DirectionalPosition::Center); } else { return Err(Error::InvalidString( vec![ self.slice_tail().to_string(), "left".to_string(), "right".to_string(), "top".to_string(), "bottom".to_string(), "center".to_string(), ], self.calc_char_pos(), )); } } } #[rustfmt::skip] #[cfg(test)] mod tests { use super::*; use std::str::FromStr; macro_rules! test_p { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { assert_eq!(DirectionalPosition::from_str($text).unwrap(), $result); } ) } test_p!(parse_1, "left", DirectionalPosition::Left); test_p!(parse_2, "right", DirectionalPosition::Right); test_p!(parse_3, "center", DirectionalPosition::Center); test_p!(parse_4, "top", DirectionalPosition::Top); test_p!(parse_5, "bottom", DirectionalPosition::Bottom); #[test] fn parse_6() { let mut s = Stream::from("left,"); assert_eq!(s.parse_directional_position().unwrap(), DirectionalPosition::Left); } #[test] fn parse_7() { let mut s = Stream::from("left ,"); assert_eq!(s.parse_directional_position().unwrap(), DirectionalPosition::Left); } #[test] fn parse_16() { let mut s = Stream::from("left center"); assert_eq!(s.parse_directional_position().unwrap(), DirectionalPosition::Left); } #[test] fn err_1() { let mut s = Stream::from("something"); assert_eq!(s.parse_directional_position().unwrap_err().to_string(), "expected 'left', 'right', 'top', 'bottom', 'center' not 'something' at position 1"); } } svgtypes-0.15.2/src/enable_background.rs000064400000000000000000000065561046102023000163730ustar 00000000000000use crate::{Error, Stream}; /// Representation of the [`enable-background`] attribute. /// /// [`enable-background`]: https://www.w3.org/TR/SVG11/filters.html#EnableBackgroundProperty #[derive(Clone, Copy, PartialEq, Debug)] #[allow(missing_docs)] pub enum EnableBackground { Accumulate, New, NewWithRegion { x: f64, y: f64, width: f64, height: f64, }, } impl std::str::FromStr for EnableBackground { type Err = Error; fn from_str(text: &str) -> Result { let mut s = Stream::from(text); s.skip_spaces(); if s.starts_with(b"accumulate") { s.advance(10); s.skip_spaces(); if !s.at_end() { return Err(Error::UnexpectedData(s.calc_char_pos())); } Ok(EnableBackground::Accumulate) } else if s.starts_with(b"new") { s.advance(3); s.skip_spaces(); if s.at_end() { return Ok(EnableBackground::New); } let x = s.parse_list_number()?; let y = s.parse_list_number()?; let width = s.parse_list_number()?; let height = s.parse_list_number()?; s.skip_spaces(); if !s.at_end() { return Err(Error::UnexpectedData(s.calc_char_pos())); } // Region size must be valid; if !(width > 0.0 && height > 0.0) { return Err(Error::InvalidValue); } Ok(EnableBackground::NewWithRegion { x, y, width, height, }) } else { Err(Error::InvalidValue) } } } #[rustfmt::skip] #[cfg(test)] mod tests { use super::*; use std::str::FromStr; #[test] fn parse_1() { assert_eq!(EnableBackground::from_str("accumulate").unwrap(), EnableBackground::Accumulate); } #[test] fn parse_2() { assert_eq!(EnableBackground::from_str(" accumulate ").unwrap(), EnableBackground::Accumulate); } #[test] fn parse_3() { assert_eq!(EnableBackground::from_str("new").unwrap(), EnableBackground::New); } #[test] fn parse_4() { assert_eq!(EnableBackground::from_str(" new ").unwrap(), EnableBackground::New); } #[test] fn parse_5() { assert_eq!(EnableBackground::from_str("new 1 2 3 4").unwrap(), EnableBackground::NewWithRegion { x: 1.0, y: 2.0, width: 3.0, height: 4.0 }); } #[test] fn err_1() { assert_eq!(EnableBackground::from_str(" accumulate b ").unwrap_err().to_string(), "unexpected data at position 13"); } #[test] fn err_2() { assert_eq!(EnableBackground::from_str(" new b ").unwrap_err().to_string(), "invalid number at position 6"); } #[test] fn err_3() { assert_eq!(EnableBackground::from_str("new 1 2 3").unwrap_err().to_string(), "unexpected end of stream"); } #[test] fn err_4() { assert_eq!(EnableBackground::from_str("new 1 2 3 4 5").unwrap_err().to_string(), "unexpected data at position 13"); } #[test] fn err_5() { assert_eq!(EnableBackground::from_str("new 0 0 0 0").unwrap_err().to_string(), "invalid value"); } } svgtypes-0.15.2/src/error.rs000064400000000000000000000060161046102023000140660ustar 00000000000000/// List of all errors. #[derive(Debug, PartialEq, Eq)] pub enum Error { /// An input data ended earlier than expected. /// /// Should only appear on invalid input data. /// Errors in a valid XML should be handled by errors below. UnexpectedEndOfStream, /// An input text contains unknown data. UnexpectedData(usize), /// A provided string doesn't have a valid data. /// /// For example, if we try to parse a color form `zzz` /// string - we will get this error. /// But if we try to parse a number list like `1.2 zzz`, /// then we will get `InvalidNumber`, because at least some data is valid. InvalidValue, /// An invalid ident. /// /// CSS idents have certain rules with regard to the characters they may contain. /// For example, they may not start with a number. If an invalid ident is encountered, /// this error will be returned. InvalidIdent, /// An invalid/unexpected character. /// /// The first byte is an actual one, others - expected. /// /// We are using a single value to reduce the struct size. InvalidChar(Vec, usize), /// An unexpected character instead of an XML space. /// /// The first string is an actual one, others - expected. /// /// We are using a single value to reduce the struct size. InvalidString(Vec, usize), /// An invalid number. InvalidNumber(usize), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { Error::UnexpectedEndOfStream => { write!(f, "unexpected end of stream") } Error::UnexpectedData(pos) => { write!(f, "unexpected data at position {}", pos) } Error::InvalidValue => { write!(f, "invalid value") } Error::InvalidIdent => { write!(f, "invalid ident") } Error::InvalidChar(ref chars, pos) => { // Vec -> Vec let list: Vec = chars .iter() .skip(1) .map(|c| String::from_utf8(vec![*c]).unwrap()) .collect(); write!( f, "expected '{}' not '{}' at position {}", list.join("', '"), chars[0] as char, pos ) } Error::InvalidString(ref strings, pos) => { write!( f, "expected '{}' not '{}' at position {}", strings[1..].join("', '"), strings[0], pos ) } Error::InvalidNumber(pos) => { write!(f, "invalid number at position {}", pos) } } } } impl std::error::Error for Error { fn description(&self) -> &str { "an SVG data parsing error" } } svgtypes-0.15.2/src/filter_functions.rs000064400000000000000000000454661046102023000163260ustar 00000000000000use crate::{Angle, AngleUnit, Color, Error, Length, LengthUnit, Stream}; /// Representation of the [``] | [``] type. /// /// Note that [`Length`] values in this enum do not contain % values. /// They are disallowed by the spec. /// /// [``]: https://www.w3.org/TR/filter-effects-1/#filter-functions /// [``]: https://www.w3.org/TR/filter-effects-1/#typedef-filter-url #[derive(Clone, Copy, PartialEq, Debug)] #[allow(missing_docs)] pub enum FilterValue<'a> { /// Cannot be negative and/or have a percentage units. Blur(Length), DropShadow { /// `currentColor` will be resolved as `None`, /// because it should be processed in the same way. color: Option, /// Cannot have a percentage units. dx: Length, /// Cannot have a percentage units. dy: Length, /// Cannot be negative and/or have a percentage units. std_dev: Length, }, /// Normalized value. Cannot be negative. Brightness(f64), /// Normalized value. Cannot be negative. Contrast(f64), /// Normalized value. Cannot be negative. Grayscale(f64), HueRotate(Angle), /// Normalized value. Cannot be negative. Invert(f64), /// Normalized value. Cannot be negative. Opacity(f64), /// Normalized value. Cannot be negative. Sepia(f64), /// Normalized value. Cannot be negative. Saturate(f64), /// Cannot be empty. Url(&'a str), } /// A list of possible [`FilterValueListParser`] errors. #[derive(Debug)] pub enum FilterValueListParserError { /// Lengths with percentage values are not allowed. PercentageValue(usize), /// Some values cannot be negative. NegativeValue(usize), /// An invalid angle value. InvalidAngle(usize), /// Drop shadow offset values must be set. MissingDropShadowOffset(usize), /// Usually indicates an empty url. InvalidUrl(usize), /// Other errors. StreamErrors(Error), } impl From for FilterValueListParserError { fn from(e: Error) -> Self { FilterValueListParserError::StreamErrors(e) } } impl std::fmt::Display for FilterValueListParserError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { FilterValueListParserError::PercentageValue(pos) => { write!(f, "a percentage value detected at position {}", pos) } FilterValueListParserError::NegativeValue(pos) => { write!(f, "a negative value detected at position {}", pos) } FilterValueListParserError::InvalidAngle(pos) => { write!(f, "an invalid angle at position {}", pos) } FilterValueListParserError::MissingDropShadowOffset(pos) => { write!( f, "drop-shadow offset values are expected at position {}", pos ) } FilterValueListParserError::InvalidUrl(pos) => { write!(f, "an invalid url at position {}", pos) } FilterValueListParserError::StreamErrors(ref e) => { write!(f, "{}", e) } } } } impl std::error::Error for FilterValueListParserError { fn description(&self) -> &str { "filter-value-list parsing error" } } /// A pull-based [``] parser. /// /// When value is set to `none`, the parser will return `None` immediately. /// Meaning that an empty string and `none` will produce the same results. /// /// [``]: https://www.w3.org/TR/filter-effects-1/#typedef-filter-value-list #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct FilterValueListParser<'a> { stream: Stream<'a>, } impl<'a> From<&'a str> for FilterValueListParser<'a> { fn from(text: &'a str) -> Self { FilterValueListParser { stream: Stream::from(text), } } } impl<'a> Iterator for FilterValueListParser<'a> { type Item = Result, FilterValueListParserError>; fn next(&mut self) -> Option { self.stream.skip_spaces(); if self.stream.at_end() { // an empty attribute is still a valid value return None; } if self.stream.starts_with(b"none") { self.stream.advance(4); self.stream.skip_spaces(); if self.stream.at_end() { return None; } else { self.stream.jump_to_end(); return Some(Err(Error::InvalidValue.into())); } } let res = self.parse_next(); if res.is_err() { self.stream.jump_to_end(); } Some(res) } } impl<'a> FilterValueListParser<'a> { fn parse_next(&mut self) -> Result, FilterValueListParserError> { let s = &mut self.stream; let start = s.pos(); let name = s.consume_ascii_ident(); s.skip_spaces(); s.consume_byte(b'(')?; s.skip_spaces(); let value = match name.as_bytes() { b"blur" => { if s.is_curr_byte_eq(b')') { FilterValue::Blur(Length::zero()) } else { let value = parse_filter_positive_length(s)?; FilterValue::Blur(value) } } b"drop-shadow" => parse_drop_shadow_func(s)?, b"hue-rotate" => { if s.is_curr_byte_eq(b')') { FilterValue::HueRotate(Angle::new(0.0, AngleUnit::Degrees)) } else { let value = parse_filter_angle(s)?; FilterValue::HueRotate(value) } } b"brightness" => FilterValue::Brightness(parse_generic_color_func(s)?), b"contrast" => FilterValue::Contrast(parse_generic_color_func(s)?), b"grayscale" => FilterValue::Grayscale(parse_generic_color_func(s)?), b"invert" => FilterValue::Invert(parse_generic_color_func(s)?), b"opacity" => FilterValue::Opacity(parse_generic_color_func(s)?), b"saturate" => FilterValue::Saturate(parse_generic_color_func(s)?), b"sepia" => FilterValue::Sepia(parse_generic_color_func(s)?), b"url" => { s.consume_byte(b'#')?; let link = s.consume_bytes(|_, c| c != b' ' && c != b')'); if !link.is_empty() { FilterValue::Url(link) } else { return Err(FilterValueListParserError::InvalidUrl( s.calc_char_pos_at(start), )); } } _ => { return Err(Error::UnexpectedData(s.calc_char_pos_at(start)).into()); } }; s.skip_spaces(); s.consume_byte(b')')?; s.skip_spaces(); Ok(value) } } #[inline(never)] fn parse_drop_shadow_func<'a>( s: &mut Stream<'a>, ) -> Result, FilterValueListParserError> { if s.is_curr_byte_eq(b')') { let pos = s.calc_char_pos(); return Err(FilterValueListParserError::MissingDropShadowOffset(pos)); } // Color can be set before and after lengths. let mut color = None; let mut is_current_color = false; if let Some(c) = s.try_parse_color() { color = Some(c); s.skip_spaces(); } else if s.starts_with(b"currentColor") { is_current_color = true; s.advance(12); s.skip_spaces(); } // Offset is the only mandatory value. let dx = parse_filter_length(s)?; s.skip_spaces(); let dy = parse_filter_length(s)?; s.skip_spaces(); // std_dev is optional let mut std_dev = Length::zero(); if let Ok(v) = parse_filter_positive_length(s) { std_dev = v; s.skip_spaces(); } // Try to parse a color after length, if it wasn't set before. if color.is_none() && !is_current_color { if let Some(c) = s.try_parse_color() { color = Some(c); s.skip_spaces(); } else if s.starts_with(b"currentColor") { s.advance(12); } } Ok(FilterValue::DropShadow { color, dx, dy, std_dev, }) } #[inline(never)] fn parse_generic_color_func(s: &mut Stream) -> Result { if s.is_curr_byte_eq(b')') { Ok(1.0) } else { let start = s.pos(); let value = s.parse_number_or_percent()?; if value.is_sign_negative() { let pos = s.calc_char_pos_at(start); return Err(FilterValueListParserError::NegativeValue(pos)); } Ok(value) } } fn parse_filter_length(s: &mut Stream) -> Result { let start = s.pos(); let value = s.parse_length()?; if value.unit == LengthUnit::Percent { let pos = s.calc_char_pos_at(start); return Err(FilterValueListParserError::PercentageValue(pos)); } Ok(value) } fn parse_filter_positive_length(s: &mut Stream) -> Result { let start = s.pos(); let value = s.parse_length()?; if value.number.is_sign_negative() { let pos = s.calc_char_pos_at(start); return Err(FilterValueListParserError::NegativeValue(pos)); } if value.unit == LengthUnit::Percent { let pos = s.calc_char_pos_at(start); return Err(FilterValueListParserError::PercentageValue(pos)); } Ok(value) } // Just like a normal angle, but units are mandatory. fn parse_filter_angle(s: &mut Stream) -> Result { s.skip_spaces(); let start = s.pos(); let n = s.parse_number()?; let u = if s.starts_with(b"deg") { s.advance(3); AngleUnit::Degrees } else if s.starts_with(b"grad") { s.advance(4); AngleUnit::Gradians } else if s.starts_with(b"rad") { s.advance(3); AngleUnit::Radians } else if s.starts_with(b"turn") { s.advance(4); AngleUnit::Turns } else { // Only zero value allowed to be unit-less. if n == 0.0 { AngleUnit::Degrees } else { let pos = s.calc_char_pos_at(start); return Err(FilterValueListParserError::InvalidAngle(pos)); } }; Ok(Angle::new(n, u)) } #[rustfmt::skip] #[cfg(test)] mod tests { use super::*; use crate::Color; #[test] fn empty() { let mut parser = FilterValueListParser::from(""); assert!(parser.next().is_none()); } #[test] fn none() { let mut parser = FilterValueListParser::from("none"); assert!(parser.next().is_none()); } #[test] fn blur_default() { let mut parser = FilterValueListParser::from("blur()"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::Blur(Length::zero())); assert!(parser.next().is_none()); } #[test] fn blur_2() { let mut parser = FilterValueListParser::from("blur(2)"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::Blur(Length::new(2.0, LengthUnit::None))); assert!(parser.next().is_none()); } #[test] fn blur_2mm() { let mut parser = FilterValueListParser::from("blur(2mm)"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::Blur(Length::new(2.0, LengthUnit::Mm))); assert!(parser.next().is_none()); } #[test] fn blur_2percent() { let mut parser = FilterValueListParser::from("blur(2%)"); assert_eq!(parser.next().unwrap().unwrap_err().to_string(), "a percentage value detected at position 6"); assert!(parser.next().is_none()); } #[test] fn blur_negative() { let mut parser = FilterValueListParser::from("blur(-1)"); assert_eq!(parser.next().unwrap().unwrap_err().to_string(), "a negative value detected at position 6"); assert!(parser.next().is_none()); } #[test] fn blur_two_values() { let mut parser = FilterValueListParser::from("blur(1 2)"); assert_eq!(parser.next().unwrap().unwrap_err().to_string(), "expected ')' not '2' at position 8"); assert!(parser.next().is_none()); } #[test] fn brightness_default() { let mut parser = FilterValueListParser::from("brightness()"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::Brightness(1.0)); assert!(parser.next().is_none()); } #[test] fn brightness_2() { let mut parser = FilterValueListParser::from("brightness(2)"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::Brightness(2.0)); assert!(parser.next().is_none()); } #[test] fn brightness_50percent() { let mut parser = FilterValueListParser::from("brightness(50%)"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::Brightness(0.5)); assert!(parser.next().is_none()); } #[test] fn brightness_negative() { let mut parser = FilterValueListParser::from("brightness(-1)"); assert_eq!(parser.next().unwrap().unwrap_err().to_string(), "a negative value detected at position 12"); assert!(parser.next().is_none()); } #[test] fn brightness_2mm() { let mut parser = FilterValueListParser::from("brightness(2mm)"); assert_eq!(parser.next().unwrap().unwrap_err().to_string(), "expected ')' not 'm' at position 13"); assert!(parser.next().is_none()); } #[test] fn drop_shadow_no_values() { let mut parser = FilterValueListParser::from("drop-shadow()"); assert_eq!(parser.next().unwrap().unwrap_err().to_string(), "drop-shadow offset values are expected at position 13"); assert!(parser.next().is_none()); } #[test] fn drop_shadow_default() { let mut parser = FilterValueListParser::from("drop-shadow(2 3)"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::DropShadow { color: None, dx: Length::new_number(2.0), dy: Length::new_number(3.0), std_dev: Length::zero(), }); assert!(parser.next().is_none()); } #[test] fn drop_shadow_color_first() { let mut parser = FilterValueListParser::from("drop-shadow(red 2 3)"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::DropShadow { color: Some(Color::new_rgb(255, 0, 0)), dx: Length::new_number(2.0), dy: Length::new_number(3.0), std_dev: Length::zero(), }); assert!(parser.next().is_none()); } #[test] fn drop_shadow_color_after() { let mut parser = FilterValueListParser::from("drop-shadow(2 3 red)"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::DropShadow { color: Some(Color::new_rgb(255, 0, 0)), dx: Length::new_number(2.0), dy: Length::new_number(3.0), std_dev: Length::zero(), }); assert!(parser.next().is_none()); } #[test] fn drop_shadow_curr_color_first() { let mut parser = FilterValueListParser::from("drop-shadow(currentColor 2 3)"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::DropShadow { color: None, dx: Length::new_number(2.0), dy: Length::new_number(3.0), std_dev: Length::zero(), }); assert!(parser.next().is_none()); } #[test] fn drop_shadow_curr_color_after() { let mut parser = FilterValueListParser::from("drop-shadow(2 3 currentColor)"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::DropShadow { color: None, dx: Length::new_number(2.0), dy: Length::new_number(3.0), std_dev: Length::zero(), }); assert!(parser.next().is_none()); } #[test] fn drop_shadow_with_dev() { let mut parser = FilterValueListParser::from("drop-shadow(red 2 3 4)"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::DropShadow { color: Some(Color::new_rgb(255, 0, 0)), dx: Length::new_number(2.0), dy: Length::new_number(3.0), std_dev: Length::new_number(4.0), }); assert!(parser.next().is_none()); } #[test] fn drop_shadow_color_twice() { let mut parser = FilterValueListParser::from("drop-shadow(red 2 3 4 red)"); assert_eq!(parser.next().unwrap().unwrap_err().to_string(), "expected ')' not 'r' at position 23"); assert!(parser.next().is_none()); } #[test] fn drop_shadow_curr_color_twice() { let mut parser = FilterValueListParser::from("drop-shadow(currentColor 2 3 4 currentColor)"); assert_eq!(parser.next().unwrap().unwrap_err().to_string(), "expected ')' not 'c' at position 32"); assert!(parser.next().is_none()); } #[test] fn drop_shadow_percent() { let mut parser = FilterValueListParser::from("drop-shadow(2% 3% 4%)"); assert_eq!(parser.next().unwrap().unwrap_err().to_string(), "a percentage value detected at position 13"); assert!(parser.next().is_none()); } #[test] fn drop_shadow_negative_offset() { let mut parser = FilterValueListParser::from("drop-shadow(-1 -2 3)"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::DropShadow { color: None, dx: Length::new_number(-1.0), dy: Length::new_number(-2.0), std_dev: Length::new_number(3.0), }); assert!(parser.next().is_none()); } #[test] fn hue_rotate_no_units() { let mut parser = FilterValueListParser::from("hue-rotate(45)"); assert_eq!(parser.next().unwrap().unwrap_err().to_string(), "an invalid angle at position 12"); assert!(parser.next().is_none()); } #[test] fn url() { let mut parser = FilterValueListParser::from("url(#qwe)"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::Url("qwe")); assert!(parser.next().is_none()); } #[test] fn multiple_1() { let mut parser = FilterValueListParser::from("blur() blur()"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::Blur(Length::zero())); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::Blur(Length::zero())); assert!(parser.next().is_none()); } #[test] fn multiple_2() { let mut parser = FilterValueListParser::from("blur() contrast(1)"); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::Blur(Length::zero())); assert_eq!(parser.next().unwrap().unwrap(), FilterValue::Contrast(1.0)); assert!(parser.next().is_none()); } } svgtypes-0.15.2/src/font.rs000064400000000000000000000313201046102023000136770ustar 00000000000000use crate::stream::{ByteExt, Stream}; use crate::Error; use std::fmt::Display; /// Parses a list of font families and generic families from a string. pub fn parse_font_families(text: &str) -> Result, Error> { let mut s = Stream::from(text); let font_families = s.parse_font_families()?; s.skip_spaces(); if !s.at_end() { return Err(Error::UnexpectedData(s.calc_char_pos())); } Ok(font_families) } /// A type of font family. #[derive(Clone, PartialEq, Eq, Debug, Hash)] pub enum FontFamily { /// A serif font. Serif, /// A sans-serif font. SansSerif, /// A cursive font. Cursive, /// A fantasy font. Fantasy, /// A monospace font. Monospace, /// A custom named font. Named(String), } impl Display for FontFamily { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let str = match self { FontFamily::Monospace => "monospace".to_string(), FontFamily::Serif => "serif".to_string(), FontFamily::SansSerif => "sans-serif".to_string(), FontFamily::Cursive => "cursive".to_string(), FontFamily::Fantasy => "fantasy".to_string(), FontFamily::Named(s) => format!("\"{}\"", s), }; write!(f, "{}", str) } } impl<'a> Stream<'a> { pub fn parse_font_families(&mut self) -> Result, Error> { let mut families = vec![]; while !self.at_end() { self.skip_spaces(); let family = { let ch = self.curr_byte()?; if ch == b'\'' || ch == b'\"' { let res = self.parse_quoted_string()?; FontFamily::Named(res.to_string()) } else { let mut idents = vec![]; while let Some(c) = self.chars().next() { if c != ',' { idents.push(self.parse_ident()?.to_string()); self.skip_spaces(); } else { break; } } let joined = idents.join(" "); // TODO: No CSS keyword must be matched as a family name... match joined.as_str() { "serif" => FontFamily::Serif, "sans-serif" => FontFamily::SansSerif, "cursive" => FontFamily::Cursive, "fantasy" => FontFamily::Fantasy, "monospace" => FontFamily::Monospace, _ => FontFamily::Named(joined), } } }; families.push(family); if let Ok(b) = self.curr_byte() { if b == b',' { self.advance(1); } else { break; } } } let families = families .into_iter() .filter(|f| match f { FontFamily::Named(s) => !s.is_empty(), _ => true, }) .collect(); Ok(families) } } /// The values of a [`font` shorthand](https://www.w3.org/TR/css-fonts-3/#font-prop). #[derive(Clone, PartialEq, Eq, Debug, Hash)] pub struct FontShorthand<'a> { /// The font style. pub font_style: Option<&'a str>, /// The font variant. pub font_variant: Option<&'a str>, /// The font weight. pub font_weight: Option<&'a str>, /// The font stretch. pub font_stretch: Option<&'a str>, /// The font size. pub font_size: &'a str, /// The font family. pub font_family: &'a str, } impl<'a> FontShorthand<'a> { /// Parses the `font` shorthand from a string. /// /// We can't use the `FromStr` trait because it requires /// an owned value as a return type. /// /// [font]: https://www.w3.org/TR/css-fonts-3/#font-prop pub fn from_str(text: &'a str) -> Result { let mut stream = Stream::from(text); stream.skip_spaces(); let mut prev_pos = stream.pos(); let mut font_style = None; let mut font_variant = None; let mut font_weight = None; let mut font_stretch = None; for _ in 0..4 { let ident = stream.consume_ascii_ident(); match ident { // TODO: Reuse actual parsers to prevent duplication. // We ignore normal because it's ambiguous to which it belongs and all // other attributes need to be resetted anyway. "normal" => {} "small-caps" => font_variant = Some(ident), "italic" | "oblique" => font_style = Some(ident), "bold" | "bolder" | "lighter" | "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900" => font_weight = Some(ident), "ultra-condensed" | "extra-condensed" | "condensed" | "semi-condensed" | "semi-expanded" | "expanded" | "extra-expanded" | "ultra-expanded" => { font_stretch = Some(ident) } _ => { // Not one of the 4 properties, so we backtrack and then start pasing font // size and family. stream = Stream::from(text); stream.advance(prev_pos); break; } } stream.skip_spaces(); prev_pos = stream.pos(); } prev_pos = stream.pos(); if stream.curr_byte()?.is_digit() { // A font size such as '15pt'. let _ = stream.parse_length()?; } else { // A font size like 'xx-large'. let size = stream.consume_ascii_ident(); if !matches!( size, "xx-small" | "x-small" | "small" | "medium" | "large" | "x-large" | "xx-large" | "larger" | "smaller" ) { return Err(Error::UnexpectedData(prev_pos)); } } let font_size = stream.slice_back(prev_pos); stream.skip_spaces(); if stream.curr_byte()? == b'/' { // We should ignore line height since it has no effect in SVG. stream.advance(1); stream.skip_spaces(); let _ = stream.parse_length()?; stream.skip_spaces(); } if stream.at_end() { return Err(Error::UnexpectedEndOfStream); } let font_family = stream.slice_tail(); Ok(Self { font_style, font_variant, font_weight, font_stretch, font_size, font_family, }) } } #[rustfmt::skip] #[cfg(test)] mod tests { use super::*; macro_rules! font_family { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { assert_eq!(parse_font_families($text).unwrap(), $result); } ) } macro_rules! named { ($text:expr) => ( FontFamily::Named($text.to_string()) ) } const SERIF: FontFamily = FontFamily::Serif; const SANS_SERIF: FontFamily = FontFamily::SansSerif; const FANTASY: FontFamily = FontFamily::Fantasy; const MONOSPACE: FontFamily = FontFamily::Monospace; const CURSIVE: FontFamily = FontFamily::Cursive; font_family!(font_family_1, "Times New Roman", vec![named!("Times New Roman")]); font_family!(font_family_2, "serif", vec![SERIF]); font_family!(font_family_3, "sans-serif", vec![SANS_SERIF]); font_family!(font_family_4, "cursive", vec![CURSIVE]); font_family!(font_family_5, "fantasy", vec![FANTASY]); font_family!(font_family_6, "monospace", vec![MONOSPACE]); font_family!(font_family_7, "'Times New Roman'", vec![named!("Times New Roman")]); font_family!(font_family_8, "'Times New Roman', sans-serif", vec![named!("Times New Roman"), SANS_SERIF]); font_family!(font_family_9, "'Times New Roman', sans-serif", vec![named!("Times New Roman"), SANS_SERIF]); font_family!(font_family_10, "Arial, sans-serif, 'fantasy'", vec![named!("Arial"), SANS_SERIF, named!("fantasy")]); font_family!(font_family_11, " Arial , monospace , 'fantasy'", vec![named!("Arial"), MONOSPACE, named!("fantasy")]); font_family!(font_family_12, "Times New Roman", vec![named!("Times New Roman")]); font_family!(font_family_13, "\"Times New Roman\", sans-serif, sans-serif, \"Arial\"", vec![named!("Times New Roman"), SANS_SERIF, SANS_SERIF, named!("Arial")] ); font_family!(font_family_14, "Times New Roman,,,Arial", vec![named!("Times New Roman"), named!("Arial")]); font_family!(font_family_15, "简体中文,sans-serif , ,\"日本語フォント\",Arial", vec![named!("简体中文"), SANS_SERIF, named!("日本語フォント"), named!("Arial")]); font_family!(font_family_16, "", vec![]); macro_rules! font_family_err { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { assert_eq!(parse_font_families($text).unwrap_err().to_string(), $result); } ) } font_family_err!(font_family_err_1, "Red/Black, sans-serif", "invalid ident"); font_family_err!(font_family_err_2, "\"Lucida\" Grande, sans-serif", "unexpected data at position 10"); font_family_err!(font_family_err_3, "Ahem!, sans-serif", "invalid ident"); font_family_err!(font_family_err_4, "test@foo, sans-serif", "invalid ident"); font_family_err!(font_family_err_5, "#POUND, sans-serif", "invalid ident"); font_family_err!(font_family_err_6, "Hawaii 5-0, sans-serif", "invalid ident"); impl<'a> FontShorthand<'a> { fn new(font_style: Option<&'a str>, font_variant: Option<&'a str>, font_weight: Option<&'a str>, font_stretch: Option<&'a str>, font_size: &'a str, font_family: &'a str) -> Self { Self { font_style, font_variant, font_weight, font_stretch, font_size, font_family } } } macro_rules! font_shorthand { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { assert_eq!(FontShorthand::from_str($text).unwrap(), $result); } ) } font_shorthand!(font_shorthand_1, "12pt/14pt sans-serif", FontShorthand::new(None, None, None, None, "12pt", "sans-serif")); font_shorthand!(font_shorthand_2, "80% sans-serif", FontShorthand::new(None, None, None, None, "80%", "sans-serif")); font_shorthand!(font_shorthand_3, "bold italic large Palatino, serif", FontShorthand::new(Some("italic"), None, Some("bold"), None, "large", "Palatino, serif")); font_shorthand!(font_shorthand_4, "x-large/110% \"new century schoolbook\", serif", FontShorthand::new(None, None, None, None, "x-large", "\"new century schoolbook\", serif")); font_shorthand!(font_shorthand_5, "normal small-caps 120%/120% fantasy", FontShorthand::new(None, Some("small-caps"), None, None, "120%", "fantasy")); font_shorthand!(font_shorthand_6, "condensed oblique 12pt \"Helvetica Neue\", serif", FontShorthand::new(Some("oblique"), None, None, Some("condensed"), "12pt", "\"Helvetica Neue\", serif")); font_shorthand!(font_shorthand_7, "italic 500 2em sans-serif, 'Noto Sans'", FontShorthand::new(Some("italic"), None, Some("500"), None, "2em", "sans-serif, 'Noto Sans'")); font_shorthand!(font_shorthand_8, "xx-large 'Noto Sans'", FontShorthand::new(None, None, None, None, "xx-large", "'Noto Sans'")); font_shorthand!(font_shorthand_9, "small-caps normal normal italic xx-small Times", FontShorthand::new(Some("italic"), Some("small-caps"), None, None, "xx-small", "Times")); macro_rules! font_shorthand_err { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { assert_eq!(FontShorthand::from_str($text).unwrap_err(), $result); } ) } font_shorthand_err!(font_shorthand_err_1, "", Error::UnexpectedEndOfStream); font_shorthand_err!(font_shorthand_err_2, "Noto Sans", Error::UnexpectedData(0)); font_shorthand_err!(font_shorthand_err_3, "12pt ", Error::UnexpectedEndOfStream); font_shorthand_err!(font_shorthand_err_4, "something 12pt 'Noto Sans'", Error::UnexpectedData(0)); font_shorthand_err!(font_shorthand_err_5, "'Noto Sans' 13pt", Error::UnexpectedData(0)); font_shorthand_err!(font_shorthand_err_6, "small-caps normal normal normal italic xx-large Times", Error::UnexpectedData(32)); } svgtypes-0.15.2/src/funciri.rs000064400000000000000000000136471046102023000144040ustar 00000000000000use crate::{Error, Stream}; /// Representation of the [``] type. /// /// [``]: https://www.w3.org/TR/SVG11/types.html#DataTypeIRI #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct IRI<'a>(pub &'a str); impl<'a> IRI<'a> { /// Parsers a `IRI` from a string. /// /// By the SVG spec, the ID must contain only [Name] characters, /// but since no one fallows this it will parse any characters. /// /// We can't use the `FromStr` trait because it requires /// an owned value as a return type. /// /// [Name]: https://www.w3.org/TR/xml/#NT-Name #[allow(clippy::should_implement_trait)] pub fn from_str(text: &'a str) -> Result { let mut s = Stream::from(text); let link = s.parse_iri()?; s.skip_spaces(); if !s.at_end() { return Err(Error::UnexpectedData(s.calc_char_pos())); } Ok(Self(link)) } } /// Representation of the [``] type. /// /// [``]: https://www.w3.org/TR/SVG11/types.html#DataTypeFuncIRI #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct FuncIRI<'a>(pub &'a str); impl<'a> FuncIRI<'a> { /// Parsers a `FuncIRI` from a string. /// /// By the SVG spec, the ID must contain only [Name] characters, /// but since no one fallows this it will parse any characters. /// /// We can't use the `FromStr` trait because it requires /// an owned value as a return type. /// /// [Name]: https://www.w3.org/TR/xml/#NT-Name #[allow(clippy::should_implement_trait)] pub fn from_str(text: &'a str) -> Result { let mut s = Stream::from(text); let link = s.parse_func_iri()?; s.skip_spaces(); if !s.at_end() { return Err(Error::UnexpectedData(s.calc_char_pos())); } Ok(Self(link)) } } impl<'a> Stream<'a> { pub fn parse_iri(&mut self) -> Result<&'a str, Error> { self.skip_spaces(); self.consume_byte(b'#')?; let link = self.consume_bytes(|_, c| c != b' '); if link.is_empty() { return Err(Error::InvalidValue); } Ok(link) } pub fn parse_func_iri(&mut self) -> Result<&'a str, Error> { self.skip_spaces(); self.consume_string(b"url(")?; self.skip_spaces(); let quote = match self.curr_byte() { Ok(b'\'') | Ok(b'"') => self.curr_byte().ok(), _ => None, }; if quote.is_some() { self.advance(1); self.skip_spaces(); } self.consume_byte(b'#')?; let link = if let Some(quote) = quote { self.consume_bytes(|_, c| c != quote).trim_end() } else { self.consume_bytes(|_, c| c != b' ' && c != b')') }; if link.is_empty() { return Err(Error::InvalidValue); } // Non-paired quotes is an error. if link.contains('\'') || link.contains('"') { return Err(Error::InvalidValue); } self.skip_spaces(); if let Some(quote) = quote { self.consume_byte(quote)?; self.skip_spaces(); } self.consume_byte(b')')?; Ok(link) } } #[rustfmt::skip] #[cfg(test)] mod tests { use super::*; #[test] fn parse_iri_1() { assert_eq!(IRI::from_str("#id").unwrap(), IRI("id")); } #[test] fn parse_iri_2() { assert_eq!(IRI::from_str(" #id ").unwrap(), IRI("id")); } #[test] fn parse_iri_3() { // Trailing data is ok for the Stream, by not for IRI. assert_eq!(Stream::from(" #id text").parse_iri().unwrap(), "id"); assert_eq!(IRI::from_str(" #id text").unwrap_err().to_string(), "unexpected data at position 10"); } #[test] fn parse_iri_4() { assert_eq!(IRI::from_str("#1").unwrap(), IRI("1")); } #[test] fn parse_err_iri_1() { assert_eq!(IRI::from_str("# id").unwrap_err().to_string(), "invalid value"); } #[test] fn parse_func_iri_1() { assert_eq!(FuncIRI::from_str("url(#id)").unwrap(), FuncIRI("id")); } #[test] fn parse_func_iri_2() { assert_eq!(FuncIRI::from_str("url(#1)").unwrap(), FuncIRI("1")); } #[test] fn parse_func_iri_3() { assert_eq!(FuncIRI::from_str(" url( #id ) ").unwrap(), FuncIRI("id")); } #[test] fn parse_func_iri_4() { // Trailing data is ok for the Stream, by not for FuncIRI. assert_eq!(Stream::from("url(#id) qwe").parse_func_iri().unwrap(), "id"); assert_eq!(FuncIRI::from_str("url(#id) qwe").unwrap_err().to_string(), "unexpected data at position 10"); } #[test] fn parse_func_iri_5() { assert_eq!(FuncIRI::from_str("url('#id')").unwrap(), FuncIRI("id")); assert_eq!(FuncIRI::from_str("url(' #id ')").unwrap(), FuncIRI("id")); } #[test] fn parse_func_iri_6() { assert_eq!(FuncIRI::from_str("url(\"#id\")").unwrap(), FuncIRI("id")); assert_eq!(FuncIRI::from_str("url(\" #id \")").unwrap(), FuncIRI("id")); } #[test] fn parse_err_func_iri_1() { assert_eq!(FuncIRI::from_str("url ( #1 )").unwrap_err().to_string(), "expected 'url(' not 'url ' at position 1"); } #[test] fn parse_err_func_iri_2() { assert_eq!(FuncIRI::from_str("url(#)").unwrap_err().to_string(), "invalid value"); } #[test] fn parse_err_func_iri_3() { assert_eq!(FuncIRI::from_str("url(# id)").unwrap_err().to_string(), "invalid value"); } #[test] fn parse_err_func_iri_4() { // If single quotes are present around the ID, they should be on both sides assert_eq!(FuncIRI::from_str("url('#id)").unwrap_err().to_string(), "unexpected end of stream"); assert_eq!(FuncIRI::from_str("url(#id')").unwrap_err().to_string(), "invalid value"); } } svgtypes-0.15.2/src/length.rs000064400000000000000000000145741046102023000142260ustar 00000000000000use crate::{Error, Stream}; /// List of all SVG length units. #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[allow(missing_docs)] pub enum LengthUnit { None, Em, Ex, Px, In, Cm, Mm, Pt, Pc, Percent, } /// Representation of the [``] type. /// /// [``]: https://www.w3.org/TR/SVG2/types.html#InterfaceSVGLength #[derive(Clone, Copy, PartialEq, Debug)] #[allow(missing_docs)] pub struct Length { pub number: f64, pub unit: LengthUnit, } impl Length { /// Constructs a new length. #[inline] pub fn new(number: f64, unit: LengthUnit) -> Length { Length { number, unit } } /// Constructs a new length with `LengthUnit::None`. #[inline] pub fn new_number(number: f64) -> Length { Length { number, unit: LengthUnit::None, } } /// Constructs a new length with a zero number. /// /// Shorthand for: `Length::new(0.0, Unit::None)`. #[inline] pub fn zero() -> Length { Length { number: 0.0, unit: LengthUnit::None, } } } impl Default for Length { #[inline] fn default() -> Self { Length::zero() } } impl std::str::FromStr for Length { type Err = Error; #[inline] fn from_str(text: &str) -> Result { let mut s = Stream::from(text); let l = s.parse_length()?; if !s.at_end() { return Err(Error::UnexpectedData(s.calc_char_pos())); } Ok(Length::new(l.number, l.unit)) } } impl<'a> Stream<'a> { /// Parses length from the stream. /// /// /// /// # Notes /// /// - Suffix must be lowercase, otherwise it will be an error. pub fn parse_length(&mut self) -> Result { self.skip_spaces(); let n = self.parse_number()?; if self.at_end() { return Ok(Length::new(n, LengthUnit::None)); } let u = if self.starts_with(b"%") { LengthUnit::Percent } else if self.starts_with(b"em") { LengthUnit::Em } else if self.starts_with(b"ex") { LengthUnit::Ex } else if self.starts_with(b"px") { LengthUnit::Px } else if self.starts_with(b"in") { LengthUnit::In } else if self.starts_with(b"cm") { LengthUnit::Cm } else if self.starts_with(b"mm") { LengthUnit::Mm } else if self.starts_with(b"pt") { LengthUnit::Pt } else if self.starts_with(b"pc") { LengthUnit::Pc } else { LengthUnit::None }; match u { LengthUnit::Percent => self.advance(1), LengthUnit::None => {} _ => self.advance(2), } Ok(Length::new(n, u)) } /// Parses length from a list of lengths. pub fn parse_list_length(&mut self) -> Result { if self.at_end() { return Err(Error::UnexpectedEndOfStream); } let l = self.parse_length()?; self.skip_spaces(); self.parse_list_separator(); Ok(l) } } /// A pull-based [``] parser. /// /// # Examples /// /// ``` /// use svgtypes::{Length, LengthUnit, LengthListParser}; /// /// let mut p = LengthListParser::from("10px 20% 50mm"); /// assert_eq!(p.next().unwrap().unwrap(), Length::new(10.0, LengthUnit::Px)); /// assert_eq!(p.next().unwrap().unwrap(), Length::new(20.0, LengthUnit::Percent)); /// assert_eq!(p.next().unwrap().unwrap(), Length::new(50.0, LengthUnit::Mm)); /// assert_eq!(p.next().is_none(), true); /// ``` /// /// [``]: https://www.w3.org/TR/SVG2/types.html#InterfaceSVGLengthList #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct LengthListParser<'a>(Stream<'a>); impl<'a> From<&'a str> for LengthListParser<'a> { #[inline] fn from(v: &'a str) -> Self { LengthListParser(Stream::from(v)) } } impl<'a> Iterator for LengthListParser<'a> { type Item = Result; fn next(&mut self) -> Option { if self.0.at_end() { None } else { let v = self.0.parse_list_length(); if v.is_err() { self.0.jump_to_end(); } Some(v) } } } #[rustfmt::skip] #[cfg(test)] mod tests { use super::*; use std::str::FromStr; macro_rules! test_p { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { assert_eq!(Length::from_str($text).unwrap(), $result); } ) } test_p!(parse_1, "1", Length::new(1.0, LengthUnit::None)); test_p!(parse_2, "1em", Length::new(1.0, LengthUnit::Em)); test_p!(parse_3, "1ex", Length::new(1.0, LengthUnit::Ex)); test_p!(parse_4, "1px", Length::new(1.0, LengthUnit::Px)); test_p!(parse_5, "1in", Length::new(1.0, LengthUnit::In)); test_p!(parse_6, "1cm", Length::new(1.0, LengthUnit::Cm)); test_p!(parse_7, "1mm", Length::new(1.0, LengthUnit::Mm)); test_p!(parse_8, "1pt", Length::new(1.0, LengthUnit::Pt)); test_p!(parse_9, "1pc", Length::new(1.0, LengthUnit::Pc)); test_p!(parse_10, "1%", Length::new(1.0, LengthUnit::Percent)); test_p!(parse_11, "1e0", Length::new(1.0, LengthUnit::None)); test_p!(parse_12, "1.0e0", Length::new(1.0, LengthUnit::None)); test_p!(parse_13, "1.0e0em", Length::new(1.0, LengthUnit::Em)); #[test] fn parse_14() { let mut s = Stream::from("1,"); assert_eq!(s.parse_length().unwrap(), Length::new(1.0, LengthUnit::None)); } #[test] fn parse_15() { let mut s = Stream::from("1 ,"); assert_eq!(s.parse_length().unwrap(), Length::new(1.0, LengthUnit::None)); } #[test] fn parse_16() { let mut s = Stream::from("1 1"); assert_eq!(s.parse_length().unwrap(), Length::new(1.0, LengthUnit::None)); } #[test] fn err_1() { let mut s = Stream::from("1q"); assert_eq!(s.parse_length().unwrap(), Length::new(1.0, LengthUnit::None)); assert_eq!(s.parse_length().unwrap_err().to_string(), "invalid number at position 2"); } #[test] fn err_2() { assert_eq!(Length::from_str("1mmx").unwrap_err().to_string(), "unexpected data at position 4"); } } svgtypes-0.15.2/src/lib.rs000064400000000000000000000066501046102023000135070ustar 00000000000000/*! *svgtypes* is a collection of parsers for [SVG](https://www.w3.org/TR/SVG2/) types. ## Supported SVG types - [``](https://www.w3.org/TR/css-color-3/) - [``](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumber) - [``](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGLength) - [``](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGAngle) - [``](https://www.w3.org/TR/SVG2/coords.html#ViewBoxAttribute) - [``](https://www.w3.org/TR/SVG2/paths.html#PathData) - [``](https://www.w3.org/TR/SVG11/types.html#DataTypeTransformList) - [``](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumberList) - [``](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGLengthList) - [``](https://www.w3.org/TR/SVG11/shapes.html#PointsBNF) - [``](https://www.w3.org/TR/filter-effects-1/#typedef-filter-value-list) - [``](https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint) - [``](https://www.w3.org/TR/SVG11/coords.html#PreserveAspectRatioAttribute) - [``](https://www.w3.org/TR/SVG11/filters.html#EnableBackgroundProperty) - [``](https://www.w3.org/TR/SVG11/types.html#DataTypeIRI) - [``](https://www.w3.org/TR/SVG11/types.html#DataTypeFuncIRI) - [`paint-order`](https://www.w3.org/TR/SVG2/painting.html#PaintOrder) ## Features - Complete support of paths, so data like `M10-20A5.5.3-4 110-.1` will be parsed correctly. - Implicit path commands will be automatically converted into explicit one. - Some SVG2 data types support. - Pretty fast. ## Limitations - Accepts only [normalized](https://www.w3.org/TR/REC-xml/#AVNormalize) values, e.g. an input text should not contain ` ` or `&data;`. - All keywords must be lowercase. Case-insensitive parsing is supported only for colors (requires allocation for named colors). - The `` followed by the `` is not supported. As the `` itself. - [System colors](https://www.w3.org/TR/css3-color/#css2-system), like `fill="AppWorkspace"`, are not supported. They were deprecated anyway. ## Safety - The library should not panic. Any panic considered as a critical bug and should be reported. - The library forbids unsafe code. ## Alternatives None. */ #![forbid(unsafe_code)] #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(missing_copy_implementations)] macro_rules! matches { ($expression:expr, $($pattern:tt)+) => { match $expression { $($pattern)+ => true, _ => false } } } mod angle; mod aspect_ratio; mod color; #[rustfmt::skip] mod colors; mod directional_position; mod enable_background; mod error; mod filter_functions; mod font; mod funciri; mod length; mod number; mod paint; mod paint_order; mod path; mod points; mod stream; mod transform; mod transform_origin; mod viewbox; use crate::stream::{ByteExt, Stream}; pub use crate::angle::*; pub use crate::aspect_ratio::*; pub use crate::color::*; pub use crate::directional_position::*; pub use crate::enable_background::*; pub use crate::error::*; pub use crate::filter_functions::*; pub use crate::font::*; pub use crate::funciri::*; pub use crate::length::*; pub use crate::number::*; pub use crate::paint::*; pub use crate::paint_order::*; pub use crate::path::*; pub use crate::points::*; pub use crate::transform::*; pub use crate::transform_origin::*; pub use crate::viewbox::*; svgtypes-0.15.2/src/number.rs000064400000000000000000000141331046102023000142240ustar 00000000000000use std::str::FromStr; use crate::{ByteExt, Error, Stream}; /// An [SVG number](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumber). #[derive(Clone, Copy, PartialEq, Debug)] pub struct Number(pub f64); impl std::str::FromStr for Number { type Err = Error; fn from_str(text: &str) -> Result { let mut s = Stream::from(text); let n = s.parse_number()?; s.skip_spaces(); if !s.at_end() { return Err(Error::UnexpectedData(s.calc_char_pos())); } Ok(Self(n)) } } impl<'a> Stream<'a> { /// Parses number from the stream. /// /// This method will detect a number length and then /// will pass a substring to the `f64::from_str` method. /// /// /// /// # Errors /// /// Returns only `InvalidNumber`. pub fn parse_number(&mut self) -> Result { // Strip off leading whitespaces. self.skip_spaces(); let start = self.pos(); if self.at_end() { return Err(Error::InvalidNumber(self.calc_char_pos_at(start))); } self.parse_number_impl() .map_err(|_| Error::InvalidNumber(self.calc_char_pos_at(start))) } fn parse_number_impl(&mut self) -> Result { let start = self.pos(); let mut c = self.curr_byte()?; // Consume sign. if c.is_sign() { self.advance(1); c = self.curr_byte()?; } // Consume integer. match c { b'0'..=b'9' => self.skip_digits(), b'.' => {} _ => return Err(Error::InvalidNumber(0)), } // Consume fraction. if let Ok(b'.') = self.curr_byte() { self.advance(1); self.skip_digits(); } if let Ok(c) = self.curr_byte() { if matches!(c, b'e' | b'E') { let c2 = self.next_byte()?; // Check for `em`/`ex`. if c2 != b'm' && c2 != b'x' { self.advance(1); match self.curr_byte()? { b'+' | b'-' => { self.advance(1); self.skip_digits(); } b'0'..=b'9' => self.skip_digits(), _ => { return Err(Error::InvalidNumber(0)); } } } } } let s = self.slice_back(start); // Use the default f64 parser now. if let Ok(n) = f64::from_str(s) { // inf, nan, etc. are an error. if n.is_finite() { return Ok(n); } } Err(Error::InvalidNumber(0)) } /// Parses number from a list of numbers. pub fn parse_list_number(&mut self) -> Result { if self.at_end() { return Err(Error::UnexpectedEndOfStream); } let n = self.parse_number()?; self.skip_spaces(); self.parse_list_separator(); Ok(n) } } /// A pull-based [``] parser. /// /// # Examples /// /// ``` /// use svgtypes::NumberListParser; /// /// let mut p = NumberListParser::from("10, 20 -50"); /// assert_eq!(p.next().unwrap().unwrap(), 10.0); /// assert_eq!(p.next().unwrap().unwrap(), 20.0); /// assert_eq!(p.next().unwrap().unwrap(), -50.0); /// assert_eq!(p.next().is_none(), true); /// ``` /// /// [``]: https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumberList #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct NumberListParser<'a>(Stream<'a>); impl<'a> From<&'a str> for NumberListParser<'a> { #[inline] fn from(v: &'a str) -> Self { NumberListParser(Stream::from(v)) } } impl<'a> Iterator for NumberListParser<'a> { type Item = Result; fn next(&mut self) -> Option { if self.0.at_end() { None } else { let v = self.0.parse_list_number(); if v.is_err() { self.0.jump_to_end(); } Some(v) } } } #[rustfmt::skip] #[cfg(test)] mod tests { use crate::Stream; macro_rules! test_p { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { let mut s = Stream::from($text); assert_eq!(s.parse_number().unwrap(), $result); } ) } test_p!(parse_1, "0", 0.0); test_p!(parse_2, "1", 1.0); test_p!(parse_3, "-1", -1.0); test_p!(parse_4, " -1 ", -1.0); test_p!(parse_5, " 1 ", 1.0); test_p!(parse_6, ".4", 0.4); test_p!(parse_7, "-.4", -0.4); test_p!(parse_8, "-.4text", -0.4); test_p!(parse_9, "-.01 text", -0.01); test_p!(parse_10, "-.01 4", -0.01); test_p!(parse_11, ".0000000000008", 0.0000000000008); test_p!(parse_12, "1000000000000", 1000000000000.0); test_p!(parse_13, "123456.123456", 123456.123456); test_p!(parse_14, "+10", 10.0); test_p!(parse_15, "1e2", 100.0); test_p!(parse_16, "1e+2", 100.0); test_p!(parse_17, "1E2", 100.0); test_p!(parse_18, "1e-2", 0.01); test_p!(parse_19, "1ex", 1.0); test_p!(parse_20, "1em", 1.0); test_p!(parse_21, "12345678901234567890", 12345678901234567000.0); test_p!(parse_22, "0.", 0.0); test_p!(parse_23, "1.3e-2", 0.013); // test_number!(parse_24, "1e", 1.0); // TODO: this macro_rules! test_p_err { ($name:ident, $text:expr) => ( #[test] fn $name() { let mut s = Stream::from($text); assert_eq!(s.parse_number().unwrap_err().to_string(), "invalid number at position 1"); } ) } test_p_err!(parse_err_1, "q"); test_p_err!(parse_err_2, ""); test_p_err!(parse_err_3, "-"); test_p_err!(parse_err_4, "+"); test_p_err!(parse_err_5, "-q"); test_p_err!(parse_err_6, "."); test_p_err!(parse_err_7, "99999999e99999999"); test_p_err!(parse_err_8, "-99999999e99999999"); } svgtypes-0.15.2/src/paint.rs000064400000000000000000000122131046102023000140440ustar 00000000000000use std::str::FromStr; use crate::{Color, Error, Stream}; /// Representation of the fallback part of the [``] type. /// /// Used by the [`Paint`](enum.Paint.html) type. /// /// [``]: https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum PaintFallback { /// The `none` value. None, /// The `currentColor` value. CurrentColor, /// [``] value. /// /// [``]: https://www.w3.org/TR/css-color-3/ Color(Color), } /// Representation of the [``] type. /// /// Doesn't own the data. Use only for parsing. /// /// `` isn't supported. /// /// [``]: https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint /// /// # Examples /// /// ``` /// use svgtypes::{Paint, PaintFallback, Color}; /// /// let paint = Paint::from_str("url(#gradient) red").unwrap(); /// assert_eq!(paint, Paint::FuncIRI("gradient", /// Some(PaintFallback::Color(Color::red())))); /// /// let paint = Paint::from_str("inherit").unwrap(); /// assert_eq!(paint, Paint::Inherit); /// ``` #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Paint<'a> { /// The `none` value. None, /// The `inherit` value. Inherit, /// The `currentColor` value. CurrentColor, /// [``] value. /// /// [``]: https://www.w3.org/TR/css-color-3/ Color(Color), /// [``] value with an optional fallback. /// /// [``]: https://www.w3.org/TR/SVG11/types.html#DataTypeFuncIRI FuncIRI(&'a str, Option), /// The `context-fill` value. ContextFill, /// The `context-stroke` value. ContextStroke, } impl<'a> Paint<'a> { /// Parses a `Paint` from a string. /// /// We can't use the `FromStr` trait because it requires /// an owned value as a return type. #[allow(clippy::should_implement_trait)] pub fn from_str(text: &'a str) -> Result { let text = text.trim(); match text { "none" => Ok(Paint::None), "inherit" => Ok(Paint::Inherit), "currentColor" => Ok(Paint::CurrentColor), "context-fill" => Ok(Paint::ContextFill), "context-stroke" => Ok(Paint::ContextStroke), _ => { let mut s = Stream::from(text); if s.starts_with(b"url(") { match s.parse_func_iri() { Ok(link) => { s.skip_spaces(); // get fallback if !s.at_end() { let fallback = s.slice_tail(); match fallback { "none" => Ok(Paint::FuncIRI(link, Some(PaintFallback::None))), "currentColor" => { Ok(Paint::FuncIRI(link, Some(PaintFallback::CurrentColor))) } _ => { let color = Color::from_str(fallback)?; Ok(Paint::FuncIRI(link, Some(PaintFallback::Color(color)))) } } } else { Ok(Paint::FuncIRI(link, None)) } } Err(_) => Err(Error::InvalidValue), } } else { match Color::from_str(text) { Ok(c) => Ok(Paint::Color(c)), Err(_) => Err(Error::InvalidValue), } } } } } } #[rustfmt::skip] #[cfg(test)] mod tests { use super::*; macro_rules! test { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { assert_eq!(Paint::from_str($text).unwrap(), $result); } ) } test!(parse_1, "none", Paint::None); test!(parse_2, " none ", Paint::None); test!(parse_3, " inherit ", Paint::Inherit); test!(parse_4, " currentColor ", Paint::CurrentColor); test!(parse_5, " red ", Paint::Color(Color::red())); test!(parse_6, " url(#qwe) ", Paint::FuncIRI("qwe", None)); test!(parse_7, " url(#qwe) none ", Paint::FuncIRI("qwe", Some(PaintFallback::None))); test!(parse_8, " url(#qwe) currentColor ", Paint::FuncIRI("qwe", Some(PaintFallback::CurrentColor))); test!(parse_9, " url(#qwe) red ", Paint::FuncIRI("qwe", Some(PaintFallback::Color(Color::red())))); macro_rules! test_err { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { assert_eq!(Paint::from_str($text).unwrap_err().to_string(), $result); } ) } test_err!(parse_err_1, "qwe", "invalid value"); test_err!(parse_err_2, "red icc-color(acmecmyk, 0.11, 0.48, 0.83, 0.00)", "invalid value"); // TODO: this // test_err!(parse_err_3, "url(#qwe) red icc-color(acmecmyk, 0.11, 0.48, 0.83, 0.00)", "invalid color at 1:15"); } svgtypes-0.15.2/src/paint_order.rs000064400000000000000000000113411046102023000152400ustar 00000000000000use crate::stream::Stream; /// [`paint-order`] property variants. /// /// [`paint-order`]: https://www.w3.org/TR/SVG2/painting.html#PaintOrder #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[allow(missing_docs)] pub enum PaintOrderKind { Fill, Stroke, Markers, } /// Representation of the [`paint-order`] property. /// /// [`paint-order`]: https://www.w3.org/TR/SVG2/painting.html#PaintOrder #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct PaintOrder { /// The order. /// /// Guarantee to not have duplicates. pub order: [PaintOrderKind; 3], } impl Default for PaintOrder { #[inline] fn default() -> Self { Self { order: [ PaintOrderKind::Fill, PaintOrderKind::Stroke, PaintOrderKind::Markers, ], } } } impl From<[PaintOrderKind; 3]> for PaintOrder { #[inline] fn from(order: [PaintOrderKind; 3]) -> Self { Self { order } } } impl std::str::FromStr for PaintOrder { type Err = (); /// Parses `PaintOrder` from a string. /// /// Never returns an error and fallbacks to the default value instead. fn from_str(text: &str) -> Result { let mut order = Vec::new(); let mut left = vec![ PaintOrderKind::Fill, PaintOrderKind::Stroke, PaintOrderKind::Markers, ]; let mut s = Stream::from(text); while !s.at_end() && order.len() < 3 { s.skip_spaces(); let name = s.consume_ascii_ident(); s.skip_spaces(); let name = match name { // `normal` is the special value that should short-circuit. "normal" => return Ok(PaintOrder::default()), "fill" => PaintOrderKind::Fill, "stroke" => PaintOrderKind::Stroke, "markers" => PaintOrderKind::Markers, _ => return Ok(PaintOrder::default()), }; if let Some(index) = left.iter().position(|v| *v == name) { left.remove(index); } order.push(name); } s.skip_spaces(); if !s.at_end() { // Any trailing data is an error. return Ok(PaintOrder::default()); } if order.is_empty() { return Ok(PaintOrder::default()); } // Any missing values should be added in the original order. while order.len() < 3 && !left.is_empty() { order.push(left.remove(0)); } // Any duplicates is an error. if order[0] == order[1] || order[0] == order[2] || order[1] == order[2] { // Any trailing data is an error. return Ok(PaintOrder::default()); } Ok(PaintOrder { order: [order[0], order[1], order[2]], }) } } #[rustfmt::skip] #[cfg(test)] mod tests { use super::*; use std::str::FromStr; #[test] fn parse_1() { assert_eq!(PaintOrder::from_str("normal").unwrap(), PaintOrder::default()); } #[test] fn parse_2() { assert_eq!(PaintOrder::from_str("qwe").unwrap(), PaintOrder::default()); } #[test] fn parse_3() { assert_eq!(PaintOrder::from_str("").unwrap(), PaintOrder::default()); } #[test] fn parse_4() { assert_eq!(PaintOrder::from_str("stroke qwe").unwrap(), PaintOrder::default()); } #[test] fn parse_5() { assert_eq!(PaintOrder::from_str("stroke stroke").unwrap(), PaintOrder::default()); } #[test] fn parse_6() { assert_eq!(PaintOrder::from_str("stroke").unwrap(), PaintOrder::from([ PaintOrderKind::Stroke, PaintOrderKind::Fill, PaintOrderKind::Markers ])); } #[test] fn parse_7() { assert_eq!(PaintOrder::from_str("stroke markers").unwrap(), PaintOrder::from([ PaintOrderKind::Stroke, PaintOrderKind::Markers, PaintOrderKind::Fill ])); } #[test] fn parse_8() { assert_eq!(PaintOrder::from_str("stroke markers fill").unwrap(), PaintOrder::from([ PaintOrderKind::Stroke, PaintOrderKind::Markers, PaintOrderKind::Fill ])); } #[test] fn parse_9() { assert_eq!(PaintOrder::from_str("markers").unwrap(), PaintOrder::from([ PaintOrderKind::Markers, PaintOrderKind::Fill, PaintOrderKind::Stroke ])); } #[test] fn parse_10() { assert_eq!(PaintOrder::from_str(" stroke\n").unwrap(), PaintOrder::from([ PaintOrderKind::Stroke, PaintOrderKind::Fill, PaintOrderKind::Markers ])); } #[test] fn parse_11() { assert_eq!(PaintOrder::from_str("stroke stroke stroke stroke").unwrap(), PaintOrder::default()); } } svgtypes-0.15.2/src/path.rs000064400000000000000000001065141046102023000136750ustar 00000000000000use crate::{Error, Stream}; /// Representation of a path segment. /// /// If you want to change the segment type (for example MoveTo to LineTo) /// you should create a new segment. /// But you still can change points or make segment relative or absolute. #[allow(missing_docs)] #[derive(Clone, Copy, PartialEq, Debug)] pub enum PathSegment { MoveTo { abs: bool, x: f64, y: f64, }, LineTo { abs: bool, x: f64, y: f64, }, HorizontalLineTo { abs: bool, x: f64, }, VerticalLineTo { abs: bool, y: f64, }, CurveTo { abs: bool, x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64, }, SmoothCurveTo { abs: bool, x2: f64, y2: f64, x: f64, y: f64, }, Quadratic { abs: bool, x1: f64, y1: f64, x: f64, y: f64, }, SmoothQuadratic { abs: bool, x: f64, y: f64, }, EllipticalArc { abs: bool, rx: f64, ry: f64, x_axis_rotation: f64, large_arc: bool, sweep: bool, x: f64, y: f64, }, ClosePath { abs: bool, }, } /// A pull-based [path data] parser. /// /// # Errors /// /// - Most of the `Error` types can occur. /// /// # Notes /// /// The library does not support implicit commands, so they will be converted to an explicit one. /// It mostly affects an implicit MoveTo, which will be converted, according to the spec, /// into explicit LineTo. /// /// Example: `M 10 20 30 40 50 60` -> `M 10 20 L 30 40 L 50 60` /// /// # Examples /// /// ``` /// use svgtypes::{PathParser, PathSegment}; /// /// let mut segments = Vec::new(); /// for segment in PathParser::from("M10-20l30.1.5.1-20z") { /// segments.push(segment.unwrap()); /// } /// /// assert_eq!(segments, &[ /// PathSegment::MoveTo { abs: true, x: 10.0, y: -20.0 }, /// PathSegment::LineTo { abs: false, x: 30.1, y: 0.5 }, /// PathSegment::LineTo { abs: false, x: 0.1, y: -20.0 }, /// PathSegment::ClosePath { abs: false }, /// ]); /// ``` /// /// [path data]: https://www.w3.org/TR/SVG2/paths.html#PathData #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct PathParser<'a> { stream: Stream<'a>, prev_cmd: Option, } impl<'a> From<&'a str> for PathParser<'a> { #[inline] fn from(v: &'a str) -> Self { PathParser { stream: Stream::from(v), prev_cmd: None, } } } impl<'a> Iterator for PathParser<'a> { type Item = Result; #[inline] fn next(&mut self) -> Option { let s = &mut self.stream; s.skip_spaces(); if s.at_end() { return None; } let res = next_impl(s, &mut self.prev_cmd); if res.is_err() { s.jump_to_end(); } Some(res) } } fn next_impl(s: &mut Stream, prev_cmd: &mut Option) -> Result { let start = s.pos(); let has_prev_cmd = prev_cmd.is_some(); let first_char = s.curr_byte_unchecked(); if !has_prev_cmd && !is_cmd(first_char) { return Err(Error::UnexpectedData(s.calc_char_pos_at(start))); } if !has_prev_cmd && !matches!(first_char, b'M' | b'm') { // The first segment must be a MoveTo. return Err(Error::UnexpectedData(s.calc_char_pos_at(start))); } // TODO: simplify let is_implicit_move_to; let cmd: u8; if is_cmd(first_char) { is_implicit_move_to = false; cmd = first_char; s.advance(1); } else if is_number_start(first_char) && has_prev_cmd { // unwrap is safe, because we checked 'has_prev_cmd' let p_cmd = prev_cmd.unwrap(); if p_cmd == b'Z' || p_cmd == b'z' { // ClosePath cannot be followed by a number. return Err(Error::UnexpectedData(s.calc_char_pos_at(start))); } if p_cmd == b'M' || p_cmd == b'm' { // 'If a moveto is followed by multiple pairs of coordinates, // the subsequent pairs are treated as implicit lineto commands.' // So we parse them as LineTo. is_implicit_move_to = true; cmd = if is_absolute(p_cmd) { b'L' } else { b'l' }; } else { is_implicit_move_to = false; cmd = p_cmd; } } else { return Err(Error::UnexpectedData(s.calc_char_pos_at(start))); } let cmdl = to_relative(cmd); let absolute = is_absolute(cmd); let token = match cmdl { b'm' => PathSegment::MoveTo { abs: absolute, x: s.parse_list_number()?, y: s.parse_list_number()?, }, b'l' => PathSegment::LineTo { abs: absolute, x: s.parse_list_number()?, y: s.parse_list_number()?, }, b'h' => PathSegment::HorizontalLineTo { abs: absolute, x: s.parse_list_number()?, }, b'v' => PathSegment::VerticalLineTo { abs: absolute, y: s.parse_list_number()?, }, b'c' => PathSegment::CurveTo { abs: absolute, x1: s.parse_list_number()?, y1: s.parse_list_number()?, x2: s.parse_list_number()?, y2: s.parse_list_number()?, x: s.parse_list_number()?, y: s.parse_list_number()?, }, b's' => PathSegment::SmoothCurveTo { abs: absolute, x2: s.parse_list_number()?, y2: s.parse_list_number()?, x: s.parse_list_number()?, y: s.parse_list_number()?, }, b'q' => PathSegment::Quadratic { abs: absolute, x1: s.parse_list_number()?, y1: s.parse_list_number()?, x: s.parse_list_number()?, y: s.parse_list_number()?, }, b't' => PathSegment::SmoothQuadratic { abs: absolute, x: s.parse_list_number()?, y: s.parse_list_number()?, }, b'a' => { // TODO: radius cannot be negative PathSegment::EllipticalArc { abs: absolute, rx: s.parse_list_number()?, ry: s.parse_list_number()?, x_axis_rotation: s.parse_list_number()?, large_arc: parse_flag(s)?, sweep: parse_flag(s)?, x: s.parse_list_number()?, y: s.parse_list_number()?, } } b'z' => PathSegment::ClosePath { abs: absolute }, _ => unreachable!(), }; *prev_cmd = Some(if is_implicit_move_to { if absolute { b'M' } else { b'm' } } else { cmd }); Ok(token) } /// Returns `true` if the selected char is the command. #[rustfmt::skip] #[inline] fn is_cmd(c: u8) -> bool { matches!(c, b'M' | b'm' | b'Z' | b'z' | b'L' | b'l' | b'H' | b'h' | b'V' | b'v' | b'C' | b'c' | b'S' | b's' | b'Q' | b'q' | b'T' | b't' | b'A' | b'a') } /// Returns `true` if the selected char is the absolute command. #[inline] fn is_absolute(c: u8) -> bool { debug_assert!(is_cmd(c)); matches!( c, b'M' | b'Z' | b'L' | b'H' | b'V' | b'C' | b'S' | b'Q' | b'T' | b'A' ) } /// Converts the selected command char into the relative command char. #[inline] fn to_relative(c: u8) -> u8 { debug_assert!(is_cmd(c)); match c { b'M' => b'm', b'Z' => b'z', b'L' => b'l', b'H' => b'h', b'V' => b'v', b'C' => b'c', b'S' => b's', b'Q' => b'q', b'T' => b't', b'A' => b'a', _ => c, } } #[inline] fn is_number_start(c: u8) -> bool { matches!(c, b'0'..=b'9' | b'.' | b'-' | b'+') } // By the SVG spec 'large-arc' and 'sweep' must contain only one char // and can be written without any separators, e.g.: 10 20 30 01 10 20. fn parse_flag(s: &mut Stream) -> Result { s.skip_spaces(); let c = s.curr_byte()?; match c { b'0' | b'1' => { s.advance(1); if s.is_curr_byte_eq(b',') { s.advance(1); } s.skip_spaces(); Ok(c == b'1') } _ => Err(Error::UnexpectedData(s.calc_char_pos_at(s.pos()))), } } #[rustfmt::skip] #[cfg(test)] mod tests { use super::*; macro_rules! test { ($name:ident, $text:expr, $( $seg:expr ),*) => ( #[test] fn $name() { let mut s = PathParser::from($text); $( assert_eq!(s.next().unwrap().unwrap(), $seg); )* if let Some(res) = s.next() { assert!(res.is_err()); } } ) } test!(null, "", ); test!(not_a_path, "q", ); test!(not_a_move_to, "L 20 30", ); test!(stop_on_err_1, "M 10 20 L 30 40 L 50", PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, PathSegment::LineTo { abs: true, x: 30.0, y: 40.0 } ); test!(move_to_1, "M 10 20", PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }); test!(move_to_2, "m 10 20", PathSegment::MoveTo { abs: false, x: 10.0, y: 20.0 }); test!(move_to_3, "M 10 20 30 40 50 60", PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, PathSegment::LineTo { abs: true, x: 30.0, y: 40.0 }, PathSegment::LineTo { abs: true, x: 50.0, y: 60.0 } ); test!(move_to_4, "M 10 20 30 40 50 60 M 70 80 90 100 110 120", PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, PathSegment::LineTo { abs: true, x: 30.0, y: 40.0 }, PathSegment::LineTo { abs: true, x: 50.0, y: 60.0 }, PathSegment::MoveTo { abs: true, x: 70.0, y: 80.0 }, PathSegment::LineTo { abs: true, x: 90.0, y: 100.0 }, PathSegment::LineTo { abs: true, x: 110.0, y: 120.0 } ); test!(arc_to_1, "M 10 20 A 5 5 30 1 1 20 20", PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, PathSegment::EllipticalArc { abs: true, rx: 5.0, ry: 5.0, x_axis_rotation: 30.0, large_arc: true, sweep: true, x: 20.0, y: 20.0 } ); test!(arc_to_2, "M 10 20 a 5 5 30 0 0 20 20", PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, PathSegment::EllipticalArc { abs: false, rx: 5.0, ry: 5.0, x_axis_rotation: 30.0, large_arc: false, sweep: false, x: 20.0, y: 20.0 } ); test!(arc_to_10, "M10-20A5.5.3-4 010-.1", PathSegment::MoveTo { abs: true, x: 10.0, y: -20.0 }, PathSegment::EllipticalArc { abs: true, rx: 5.5, ry: 0.3, x_axis_rotation: -4.0, large_arc: false, sweep: true, x: 0.0, y: -0.1 } ); test!(separator_1, "M 10 20 L 5 15 C 10 20 30 40 50 60", PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, PathSegment::LineTo { abs: true, x: 5.0, y: 15.0 }, PathSegment::CurveTo { abs: true, x1: 10.0, y1: 20.0, x2: 30.0, y2: 40.0, x: 50.0, y: 60.0, } ); test!(separator_2, "M 10, 20 L 5, 15 C 10, 20 30, 40 50, 60", PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, PathSegment::LineTo { abs: true, x: 5.0, y: 15.0 }, PathSegment::CurveTo { abs: true, x1: 10.0, y1: 20.0, x2: 30.0, y2: 40.0, x: 50.0, y: 60.0, } ); test!(separator_3, "M 10,20 L 5,15 C 10,20 30,40 50,60", PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, PathSegment::LineTo { abs: true, x: 5.0, y: 15.0 }, PathSegment::CurveTo { abs: true, x1: 10.0, y1: 20.0, x2: 30.0, y2: 40.0, x: 50.0, y: 60.0, } ); test!(separator_4, "M10, 20 L5, 15 C10, 20 30 40 50 60", PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, PathSegment::LineTo { abs: true, x: 5.0, y: 15.0 }, PathSegment::CurveTo { abs: true, x1: 10.0, y1: 20.0, x2: 30.0, y2: 40.0, x: 50.0, y: 60.0, } ); test!(separator_5, "M10 20V30H40V50H60Z", PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, PathSegment::VerticalLineTo { abs: true, y: 30.0 }, PathSegment::HorizontalLineTo { abs: true, x: 40.0 }, PathSegment::VerticalLineTo { abs: true, y: 50.0 }, PathSegment::HorizontalLineTo { abs: true, x: 60.0 }, PathSegment::ClosePath { abs: true } ); test!(all_segments_1, "M 10 20 L 30 40 H 50 V 60 C 70 80 90 100 110 120 S 130 140 150 160 Q 170 180 190 200 T 210 220 A 50 50 30 1 1 230 240 Z", PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, PathSegment::LineTo { abs: true, x: 30.0, y: 40.0 }, PathSegment::HorizontalLineTo { abs: true, x: 50.0 }, PathSegment::VerticalLineTo { abs: true, y: 60.0 }, PathSegment::CurveTo { abs: true, x1: 70.0, y1: 80.0, x2: 90.0, y2: 100.0, x: 110.0, y: 120.0, }, PathSegment::SmoothCurveTo { abs: true, x2: 130.0, y2: 140.0, x: 150.0, y: 160.0, }, PathSegment::Quadratic { abs: true, x1: 170.0, y1: 180.0, x: 190.0, y: 200.0, }, PathSegment::SmoothQuadratic { abs: true, x: 210.0, y: 220.0 }, PathSegment::EllipticalArc { abs: true, rx: 50.0, ry: 50.0, x_axis_rotation: 30.0, large_arc: true, sweep: true, x: 230.0, y: 240.0 }, PathSegment::ClosePath { abs: true } ); test!(all_segments_2, "m 10 20 l 30 40 h 50 v 60 c 70 80 90 100 110 120 s 130 140 150 160 q 170 180 190 200 t 210 220 a 50 50 30 1 1 230 240 z", PathSegment::MoveTo { abs: false, x: 10.0, y: 20.0 }, PathSegment::LineTo { abs: false, x: 30.0, y: 40.0 }, PathSegment::HorizontalLineTo { abs: false, x: 50.0 }, PathSegment::VerticalLineTo { abs: false, y: 60.0 }, PathSegment::CurveTo { abs: false, x1: 70.0, y1: 80.0, x2: 90.0, y2: 100.0, x: 110.0, y: 120.0, }, PathSegment::SmoothCurveTo { abs: false, x2: 130.0, y2: 140.0, x: 150.0, y: 160.0, }, PathSegment::Quadratic { abs: false, x1: 170.0, y1: 180.0, x: 190.0, y: 200.0, }, PathSegment::SmoothQuadratic { abs: false, x: 210.0, y: 220.0 }, PathSegment::EllipticalArc { abs: false, rx: 50.0, ry: 50.0, x_axis_rotation: 30.0, large_arc: true, sweep: true, x: 230.0, y: 240.0 }, PathSegment::ClosePath { abs: false } ); test!(close_path_1, "M10 20 L 30 40 ZM 100 200 L 300 400", PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, PathSegment::LineTo { abs: true, x: 30.0, y: 40.0 }, PathSegment::ClosePath { abs: true }, PathSegment::MoveTo { abs: true, x: 100.0, y: 200.0 }, PathSegment::LineTo { abs: true, x: 300.0, y: 400.0 } ); test!(close_path_2, "M10 20 L 30 40 zM 100 200 L 300 400", PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, PathSegment::LineTo { abs: true, x: 30.0, y: 40.0 }, PathSegment::ClosePath { abs: false }, PathSegment::MoveTo { abs: true, x: 100.0, y: 200.0 }, PathSegment::LineTo { abs: true, x: 300.0, y: 400.0 } ); test!(close_path_3, "M10 20 L 30 40 Z Z Z", PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, PathSegment::LineTo { abs: true, x: 30.0, y: 40.0 }, PathSegment::ClosePath { abs: true }, PathSegment::ClosePath { abs: true }, PathSegment::ClosePath { abs: true } ); // first token should be EndOfStream test!(invalid_1, "M\t.", ); // ClosePath can't be followed by a number test!(invalid_2, "M 0 0 Z 2", PathSegment::MoveTo { abs: true, x: 0.0, y: 0.0 }, PathSegment::ClosePath { abs: true } ); // ClosePath can be followed by any command test!(invalid_3, "M 0 0 Z H 10", PathSegment::MoveTo { abs: true, x: 0.0, y: 0.0 }, PathSegment::ClosePath { abs: true }, PathSegment::HorizontalLineTo { abs: true, x: 10.0 } ); } /// Representation of a simple path segment. #[allow(missing_docs)] #[derive(Clone, Copy, PartialEq, Debug)] pub enum SimplePathSegment { MoveTo { x: f64, y: f64, }, LineTo { x: f64, y: f64, }, CurveTo { x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64, }, Quadratic { x1: f64, y1: f64, x: f64, y: f64, }, ClosePath, } /// A simplifying Path Data parser. /// /// A more high-level Path Data parser on top of [`PathParser`] that provides: /// /// - Relative to absolute segment coordinates conversion /// - ArcTo to CurveTos conversion /// - SmoothCurveTo and SmoothQuadratic conversion /// - HorizontalLineTo and VerticalLineTo to LineTo conversion /// - Implicit MoveTo after ClosePath handling /// /// In the end, only absolute MoveTo, LineTo, CurveTo, Quadratic and ClosePath segments /// will be produced. #[derive(Clone, Debug)] pub struct SimplifyingPathParser<'a> { parser: PathParser<'a>, // Previous MoveTo coordinates. prev_mx: f64, prev_my: f64, // Previous SmoothQuadratic coordinates. prev_tx: f64, prev_ty: f64, // Previous coordinates. prev_x: f64, prev_y: f64, prev_seg: PathSegment, prev_simple_seg: Option, buffer: Vec, } impl<'a> From<&'a str> for SimplifyingPathParser<'a> { #[inline] fn from(v: &'a str) -> Self { SimplifyingPathParser { parser: PathParser::from(v), prev_mx: 0.0, prev_my: 0.0, prev_tx: 0.0, prev_ty: 0.0, prev_x: 0.0, prev_y: 0.0, prev_seg: PathSegment::MoveTo { abs: true, x: 0.0, y: 0.0, }, prev_simple_seg: None, buffer: Vec::new(), } } } impl<'a> Iterator for SimplifyingPathParser<'a> { type Item = Result; fn next(&mut self) -> Option { if !self.buffer.is_empty() { return Some(Ok(self.buffer.remove(0))); } let segment = match self.parser.next()? { Ok(v) => v, Err(e) => return Some(Err(e)), }; // If a ClosePath segment is followed by any command other than MoveTo or ClosePath // then MoveTo is implicit. if let Some(SimplePathSegment::ClosePath) = self.prev_simple_seg { match segment { PathSegment::MoveTo { .. } | PathSegment::ClosePath { .. } => {} _ => { let new_seg = SimplePathSegment::MoveTo { x: self.prev_mx, y: self.prev_my, }; self.buffer.push(new_seg); self.prev_simple_seg = Some(new_seg); } } } match segment { PathSegment::MoveTo { abs, mut x, mut y } => { if !abs { // When we get 'm'(relative) segment, which is not first segment - then it's // relative to a previous 'M'(absolute) segment, not to the first segment. if let Some(SimplePathSegment::ClosePath) = self.prev_simple_seg { x += self.prev_mx; y += self.prev_my; } else { x += self.prev_x; y += self.prev_y; } } self.buffer.push(SimplePathSegment::MoveTo { x, y }); self.prev_seg = segment; } PathSegment::LineTo { abs, mut x, mut y } => { if !abs { x += self.prev_x; y += self.prev_y; } self.buffer.push(SimplePathSegment::LineTo { x, y }); self.prev_seg = segment; } PathSegment::HorizontalLineTo { abs, mut x } => { if !abs { x += self.prev_x; } self.buffer .push(SimplePathSegment::LineTo { x, y: self.prev_y }); self.prev_seg = segment; } PathSegment::VerticalLineTo { abs, mut y } => { if !abs { y += self.prev_y; } self.buffer .push(SimplePathSegment::LineTo { x: self.prev_x, y }); self.prev_seg = segment; } PathSegment::CurveTo { abs, mut x1, mut y1, mut x2, mut y2, mut x, mut y, } => { if !abs { x1 += self.prev_x; y1 += self.prev_y; x2 += self.prev_x; y2 += self.prev_y; x += self.prev_x; y += self.prev_y; } self.buffer.push(SimplePathSegment::CurveTo { x1, y1, x2, y2, x, y, }); // Remember as absolute. self.prev_seg = PathSegment::CurveTo { abs: true, x1, y1, x2, y2, x, y, }; } PathSegment::SmoothCurveTo { abs, mut x2, mut y2, mut x, mut y, } => { // 'The first control point is assumed to be the reflection of the second control // point on the previous command relative to the current point. // (If there is no previous command or if the previous command // was not an C, c, S or s, assume the first control point is // coincident with the current point.)' let (x1, y1) = match self.prev_seg { PathSegment::CurveTo { x2, y2, x, y, .. } | PathSegment::SmoothCurveTo { x2, y2, x, y, .. } => { (x * 2.0 - x2, y * 2.0 - y2) } _ => (self.prev_x, self.prev_y), }; if !abs { x2 += self.prev_x; y2 += self.prev_y; x += self.prev_x; y += self.prev_y; } self.buffer.push(SimplePathSegment::CurveTo { x1, y1, x2, y2, x, y, }); // Remember as absolute. self.prev_seg = PathSegment::SmoothCurveTo { abs: true, x2, y2, x, y, }; } PathSegment::Quadratic { abs, mut x1, mut y1, mut x, mut y, } => { if !abs { x1 += self.prev_x; y1 += self.prev_y; x += self.prev_x; y += self.prev_y; } self.buffer .push(SimplePathSegment::Quadratic { x1, y1, x, y }); // Remember as absolute. self.prev_seg = PathSegment::Quadratic { abs: true, x1, y1, x, y, }; } PathSegment::SmoothQuadratic { abs, mut x, mut y } => { // 'The control point is assumed to be the reflection of // the control point on the previous command relative to // the current point. (If there is no previous command or // if the previous command was not a Q, q, T or t, assume // the control point is coincident with the current point.)' let (x1, y1) = match self.prev_seg { PathSegment::Quadratic { x1, y1, x, y, .. } => (x * 2.0 - x1, y * 2.0 - y1), PathSegment::SmoothQuadratic { x, y, .. } => { (x * 2.0 - self.prev_tx, y * 2.0 - self.prev_ty) } _ => (self.prev_x, self.prev_y), }; self.prev_tx = x1; self.prev_ty = y1; if !abs { x += self.prev_x; y += self.prev_y; } self.buffer .push(SimplePathSegment::Quadratic { x1, y1, x, y }); // Remember as absolute. self.prev_seg = PathSegment::SmoothQuadratic { abs: true, x, y }; } PathSegment::EllipticalArc { abs, rx, ry, x_axis_rotation, large_arc, sweep, mut x, mut y, } => { if !abs { x += self.prev_x; y += self.prev_y; } let svg_arc = kurbo::SvgArc { from: kurbo::Point::new(self.prev_x, self.prev_y), to: kurbo::Point::new(x, y), radii: kurbo::Vec2::new(rx, ry), x_rotation: x_axis_rotation.to_radians(), large_arc, sweep, }; match kurbo::Arc::from_svg_arc(&svg_arc) { Some(arc) => { arc.to_cubic_beziers(0.1, |p1, p2, p| { self.buffer.push(SimplePathSegment::CurveTo { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y, x: p.x, y: p.y, }); }); } None => { self.buffer.push(SimplePathSegment::LineTo { x, y }); } } self.prev_seg = segment; } PathSegment::ClosePath { .. } => { if let Some(SimplePathSegment::ClosePath) = self.prev_simple_seg { // Do not add sequential ClosePath segments. // Otherwise it will break markers rendering. } else { self.buffer.push(SimplePathSegment::ClosePath); } self.prev_seg = segment; } } // Remember last position. if let Some(new_segment) = self.buffer.last() { self.prev_simple_seg = Some(*new_segment); match *new_segment { SimplePathSegment::MoveTo { x, y } => { self.prev_x = x; self.prev_y = y; self.prev_mx = self.prev_x; self.prev_my = self.prev_y; } SimplePathSegment::LineTo { x, y } => { self.prev_x = x; self.prev_y = y; } SimplePathSegment::CurveTo { x, y, .. } => { self.prev_x = x; self.prev_y = y; } SimplePathSegment::Quadratic { x, y, .. } => { self.prev_x = x; self.prev_y = y; } SimplePathSegment::ClosePath => { // ClosePath moves us to the last MoveTo coordinate. self.prev_x = self.prev_mx; self.prev_y = self.prev_my; } } } if self.buffer.is_empty() { return self.next(); } Some(Ok(self.buffer.remove(0))) } } #[rustfmt::skip] #[cfg(test)] mod simple_tests { use super::*; macro_rules! test { ($name:ident, $text:expr, $( $seg:expr ),*) => ( #[test] fn $name() { let mut s = SimplifyingPathParser::from($text); $( assert_eq!(s.next().unwrap().unwrap(), $seg); )* if let Some(res) = s.next() { assert!(res.is_err()); } } ) } test!(ignore_duplicated_close_paths, "M 10 20 L 30 40 Z Z Z Z", SimplePathSegment::MoveTo { x: 10.0, y: 20.0 }, SimplePathSegment::LineTo { x: 30.0, y: 40.0 }, SimplePathSegment::ClosePath ); test!(relative_move_to, "m 30 40 110 120 -20 -130", SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }, SimplePathSegment::LineTo { x: 140.0, y: 160.0 }, SimplePathSegment::LineTo { x: 120.0, y: 30.0 } ); test!(smooth_curve_to_after_move_to, "M 30 40 S 171 45 180 155", SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }, SimplePathSegment::CurveTo { x1: 30.0, y1: 40.0, x2: 171.0, y2: 45.0, x: 180.0, y: 155.0 } ); test!(smooth_curve_to_after_curve_to, "M 30 40 C 16 137 171 45 100 90 S 171 45 180 155", SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }, SimplePathSegment::CurveTo { x1: 16.0, y1: 137.0, x2: 171.0, y2: 45.0, x: 100.0, y: 90.0 }, SimplePathSegment::CurveTo { x1: 29.0, y1: 135.0, x2: 171.0, y2: 45.0, x: 180.0, y: 155.0 } ); test!(relative_smooth_curve_to_after_arc_to, "M 1 5 A 5 5 0 0 1 3 1 s 3 2 8 2", SimplePathSegment::MoveTo { x: 1.0, y: 5.0 }, SimplePathSegment::CurveTo { x1: 1.0, y1: 3.4262134833347355, x2: 1.7409707866677877, y2: 1.9442719099991592, x: 2.9999999999999996, y: 1.0000000000000004 }, SimplePathSegment::CurveTo { x1: 2.9999999999999996, y1: 1.0000000000000004, x2: 6.0, y2: 3.0000000000000004, x: 11.0, y: 3.0000000000000004 } ); test!(smooth_quadratic_after_move_to, "M 30 40 T 180 155", SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }, SimplePathSegment::Quadratic { x1: 30.0, y1: 40.0, x: 180.0, y: 155.0 } ); test!(smooth_quadratic_after_quadratic, "M 30 40 Q 171 45 100 90 T 160 180", SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }, SimplePathSegment::Quadratic { x1: 171.0, y1: 45.0, x: 100.0, y: 90.0 }, SimplePathSegment::Quadratic { x1: 29.0, y1: 135.0, x: 160.0, y: 180.0 } ); test!(relative_smooth_quadratic_after_quadratic, "M 30 40 Q 171 45 100 90 t 60 80", SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }, SimplePathSegment::Quadratic { x1: 171.0, y1: 45.0, x: 100.0, y: 90.0 }, SimplePathSegment::Quadratic { x1: 29.0, y1: 135.0, x: 160.0, y: 170.0 } ); test!(relative_smooth_quadratic_after_relative_quadratic, "M 30 40 q 171 45 50 40 t 60 80", SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }, SimplePathSegment::Quadratic { x1: 201.0, y1: 85.0, x: 80.0, y: 80.0 }, SimplePathSegment::Quadratic { x1: -41.0, y1: 75.0, x: 140.0, y: 160.0 } ); test!(smooth_quadratic_after_smooth_quadratic, "M 30 30 T 40 140 T 170 30", SimplePathSegment::MoveTo { x: 30.0, y: 30.0 }, SimplePathSegment::Quadratic { x1: 30.0, y1: 30.0, x: 40.0, y: 140.0 }, SimplePathSegment::Quadratic { x1: 50.0, y1: 250.0, x: 170.0, y: 30.0 } ); test!(smooth_quadratic_after_relative_smooth_quadratic, "M 30 30 T 40 140 t 100 -30", SimplePathSegment::MoveTo { x: 30.0, y: 30.0 }, SimplePathSegment::Quadratic { x1: 30.0, y1: 30.0, x: 40.0, y: 140.0 }, SimplePathSegment::Quadratic { x1: 50.0, y1: 250.0, x: 140.0, y: 110.0 } ); test!(smooth_quadratic_after_relative_quadratic, "M 30 30 T 40 140 q 30 100 120 -30", SimplePathSegment::MoveTo { x: 30.0, y: 30.0 }, SimplePathSegment::Quadratic { x1: 30.0, y1: 30.0, x: 40.0, y: 140.0 }, SimplePathSegment::Quadratic { x1: 70.0, y1: 240.0, x: 160.0, y: 110.0 } ); test!(smooth_quadratic_after_relative_smooth_curve_to, "M 30 30 T 40 170 s 90 -20 90 -90", SimplePathSegment::MoveTo { x: 30.0, y: 30.0 }, SimplePathSegment::Quadratic { x1: 30.0, y1: 30.0, x: 40.0, y: 170.0 }, SimplePathSegment::CurveTo { x1: 40.0, y1: 170.0, x2: 130.0, y2: 150.0, x: 130.0, y: 80.0 } ); test!(quadratic_after_smooth_quadratic, "M 30 30 T 40 140 Q 80 180 170 30", SimplePathSegment::MoveTo { x: 30.0, y: 30.0 }, SimplePathSegment::Quadratic { x1: 30.0, y1: 30.0, x: 40.0, y: 140.0 }, SimplePathSegment::Quadratic { x1: 80.0, y1: 180.0, x: 170.0, y: 30.0 } ); test!(relative_smooth_quadratic_to_after_arc_to, "M 1 5 A 5 5 0 0 1 3 1 t 8 2", SimplePathSegment::MoveTo { x: 1.0, y: 5.0 }, SimplePathSegment::CurveTo { x1: 1.0, y1: 3.4262134833347355, x2: 1.7409707866677877, y2: 1.9442719099991592, x: 2.9999999999999996, y: 1.0000000000000004 }, SimplePathSegment::Quadratic { x1: 2.9999999999999996, y1: 1.0000000000000004, x: 11.0, y: 3.0000000000000004 } ); test!(implicit_move_to_after_close_path, "M 10 20 L 30 40 Z L 50 60", SimplePathSegment::MoveTo { x: 10.0, y: 20.0 }, SimplePathSegment::LineTo { x: 30.0, y: 40.0 }, SimplePathSegment::ClosePath, SimplePathSegment::MoveTo { x: 10.0, y: 20.0 }, SimplePathSegment::LineTo { x: 50.0, y: 60.0 } ); #[test] fn arc_to() { let mut s = SimplifyingPathParser::from("M 30 40 A 40 30 20 1 1 150 100"); assert_eq!(s.next().unwrap().unwrap(), SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }); let curve1 = s.next().unwrap().unwrap(); let curve2 = s.next().unwrap().unwrap(); if let Some(res) = s.next() { assert!(res.is_err()); } if let SimplePathSegment::CurveTo { x1, y1, x2, y2, x, y } = curve1 { assert_eq!(x1.round(), 45.0); assert_eq!(y1.round(), 16.0); assert_eq!(x2.round(), 84.0); assert_eq!(y2.round(), 10.0); assert_eq!(x.round(), 117.0); assert_eq!(y.round(), 27.0); } else { panic!("invalid type"); } if let SimplePathSegment::CurveTo { x1, y1, x2, y2, x, y } = curve2 { assert_eq!(x1.round(), 150.0); assert_eq!(y1.round(), 43.0); assert_eq!(x2.round(), 165.0); assert_eq!(y2.round(), 76.0); assert_eq!(x.round(), 150.0); assert_eq!(y.round(), 100.0); } else { panic!("invalid type"); } } } svgtypes-0.15.2/src/points.rs000064400000000000000000000041241046102023000142470ustar 00000000000000use crate::Stream; /// A pull-based [``] parser. /// /// Use it for the `points` attribute of the `polygon` and `polyline` elements. /// /// # Errors /// /// - Stops on a first invalid character. Follows the same rules as paths parser. /// /// # Notes /// /// - If data contains an odd number of coordinates - the last one will be ignored. /// As SVG spec states. /// - It doesn't validate that there are more than two coordinate pairs, /// which is required by the SVG spec. /// /// # Examples /// /// ``` /// use svgtypes::PointsParser; /// /// let mut p = PointsParser::from("10 20 30 40"); /// assert_eq!(p.next(), Some((10.0, 20.0))); /// assert_eq!(p.next(), Some((30.0, 40.0))); /// assert_eq!(p.next(), None); /// ``` /// /// [``]: https://www.w3.org/TR/SVG11/shapes.html#PointsBNF #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct PointsParser<'a>(Stream<'a>); impl<'a> From<&'a str> for PointsParser<'a> { #[inline] fn from(v: &'a str) -> Self { PointsParser(Stream::from(v)) } } impl<'a> Iterator for PointsParser<'a> { type Item = (f64, f64); fn next(&mut self) -> Option { if self.0.at_end() { None } else { let x = match self.0.parse_list_number() { Ok(x) => x, Err(_) => return None, }; let y = match self.0.parse_list_number() { Ok(y) => y, Err(_) => return None, }; Some((x, y)) } } } #[rustfmt::skip] #[cfg(test)] mod tests { use super::*; #[test] fn parse_1() { let mut parser = PointsParser::from("10 20 30 40"); assert_eq!(parser.next().unwrap(), (10.0, 20.0)); assert_eq!(parser.next().unwrap(), (30.0, 40.0)); assert!(parser.next().is_none()); } #[test] fn parse_2() { let mut parser = PointsParser::from("10 20 30 40 50"); assert_eq!(parser.next().unwrap(), (10.0, 20.0)); assert_eq!(parser.next().unwrap(), (30.0, 40.0)); assert!(parser.next().is_none()); } } svgtypes-0.15.2/src/stream.rs000064400000000000000000000312541046102023000142320ustar 00000000000000use std::str::FromStr; use crate::Error; /// Extension methods for XML-subset only operations. pub(crate) trait ByteExt { /// Checks if a byte is a numeric sign. fn is_sign(&self) -> bool; /// Checks if a byte is a digit. /// /// `[0-9]` fn is_digit(&self) -> bool; /// Checks if a byte is a hex digit. /// /// `[0-9A-Fa-f]` fn is_hex_digit(&self) -> bool; /// Checks if a byte is a space. /// /// `[ \r\n\t]` fn is_space(&self) -> bool; /// Checks if a byte is an ASCII ident char. fn is_ascii_ident(&self) -> bool; } impl ByteExt for u8 { #[inline] fn is_sign(&self) -> bool { matches!(*self, b'+' | b'-') } #[inline] fn is_digit(&self) -> bool { matches!(*self, b'0'..=b'9') } #[inline] fn is_hex_digit(&self) -> bool { matches!(*self, b'0'..=b'9' | b'A'..=b'F' | b'a'..=b'f') } #[inline] fn is_space(&self) -> bool { matches!(*self, b' ' | b'\t' | b'\n' | b'\r') } #[inline] fn is_ascii_ident(&self) -> bool { matches!(*self, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'-' | b'_') } } trait CharExt { fn is_name_start(&self) -> bool; fn is_name_char(&self) -> bool; fn is_non_ascii(&self) -> bool; fn is_escape(&self) -> bool; } impl CharExt for char { #[inline] fn is_name_start(&self) -> bool { match *self { '_' | 'a'..='z' | 'A'..='Z' => true, _ => self.is_non_ascii() || self.is_escape(), } } #[inline] fn is_name_char(&self) -> bool { match *self { '_' | 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' => true, _ => self.is_non_ascii() || self.is_escape(), } } #[inline] fn is_non_ascii(&self) -> bool { *self as u32 > 237 } #[inline] fn is_escape(&self) -> bool { // TODO: this false } } /// A streaming text parsing interface. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Stream<'a> { text: &'a str, pos: usize, } impl<'a> From<&'a str> for Stream<'a> { #[inline] fn from(text: &'a str) -> Self { Stream { text, pos: 0 } } } impl<'a> Stream<'a> { /// Returns the current position in bytes. #[inline] pub fn pos(&self) -> usize { self.pos } /// Calculates the current position in chars. pub fn calc_char_pos(&self) -> usize { self.calc_char_pos_at(self.pos) } /// Calculates the current position in chars. pub fn calc_char_pos_at(&self, byte_pos: usize) -> usize { let mut pos = 1; for (idx, _) in self.text.char_indices() { if idx >= byte_pos { break; } pos += 1; } pos } /// Sets current position equal to the end. /// /// Used to indicate end of parsing on error. #[inline] pub fn jump_to_end(&mut self) { self.pos = self.text.len(); } /// Checks if the stream is reached the end. /// /// Any [`pos()`] value larger than original text length indicates stream end. /// /// Accessing stream after reaching end via safe methods will produce /// an `UnexpectedEndOfStream` error. /// /// Accessing stream after reaching end via *_unchecked methods will produce /// a Rust's bound checking error. /// /// [`pos()`]: #method.pos #[inline] pub fn at_end(&self) -> bool { self.pos >= self.text.len() } /// Returns a byte from a current stream position. /// /// # Errors /// /// - `UnexpectedEndOfStream` #[inline] pub fn curr_byte(&self) -> Result { if self.at_end() { return Err(Error::UnexpectedEndOfStream); } Ok(self.curr_byte_unchecked()) } #[inline] pub fn chars(&self) -> std::str::Chars<'a> { self.text[self.pos..].chars() } /// Returns a byte from a current stream position. /// /// # Panics /// /// - if the current position is after the end of the data #[inline] pub fn curr_byte_unchecked(&self) -> u8 { self.text.as_bytes()[self.pos] } /// Checks that current byte is equal to provided. /// /// Returns `false` if no bytes left. #[inline] pub fn is_curr_byte_eq(&self, c: u8) -> bool { if !self.at_end() { self.curr_byte_unchecked() == c } else { false } } /// Returns a next byte from a current stream position. /// /// # Errors /// /// - `UnexpectedEndOfStream` #[inline] pub fn next_byte(&self) -> Result { if self.pos + 1 >= self.text.len() { return Err(Error::UnexpectedEndOfStream); } Ok(self.text.as_bytes()[self.pos + 1]) } /// Advances by `n` bytes. #[inline] pub fn advance(&mut self, n: usize) { debug_assert!(self.pos + n <= self.text.len()); self.pos += n; } /// Skips whitespaces. /// /// Accepted values: `' ' \n \r \t`. pub fn skip_spaces(&mut self) { while !self.at_end() && self.curr_byte_unchecked().is_space() { self.advance(1); } } /// Checks that the stream starts with a selected text. /// /// We are using `&[u8]` instead of `&str` for performance reasons. #[inline] pub fn starts_with(&self, text: &[u8]) -> bool { self.text.as_bytes()[self.pos..].starts_with(text) } /// Consumes current byte if it's equal to the provided byte. /// /// # Errors /// /// - `InvalidChar` /// - `UnexpectedEndOfStream` pub fn consume_byte(&mut self, c: u8) -> Result<(), Error> { if self.curr_byte()? != c { return Err(Error::InvalidChar( vec![self.curr_byte_unchecked(), c], self.calc_char_pos(), )); } self.advance(1); Ok(()) } /// Parses a single [ident](https://drafts.csswg.org/css-syntax-3/#typedef-ident-token). /// /// # Errors /// /// - `InvalidIdent` pub fn parse_ident(&mut self) -> Result<&'a str, Error> { let start = self.pos(); if self.curr_byte() == Ok(b'-') { self.advance(1); } let mut iter = self.chars(); if let Some(c) = iter.next() { if c.is_name_start() { self.advance(c.len_utf8()); } else { return Err(Error::InvalidIdent); } } for c in iter { if c.is_name_char() { self.advance(c.len_utf8()); } else { break; } } if start == self.pos() { return Err(Error::InvalidIdent); } let name = self.slice_back(start); Ok(name) } /// Consumes a single ident consisting of ASCII characters, if available. pub fn consume_ascii_ident(&mut self) -> &'a str { let start = self.pos; self.skip_bytes(|_, c| c.is_ascii_ident()); self.slice_back(start) } /// Parses a single [quoted string](https://drafts.csswg.org/css-syntax-3/#typedef-string-token) /// /// # Errors /// /// - `UnexpectedEndOfStream` /// - `InvalidValue` pub fn parse_quoted_string(&mut self) -> Result<&'a str, Error> { // Check for opening quote. let quote = self.curr_byte()?; if quote != b'\'' && quote != b'"' { return Err(Error::InvalidValue); } let mut prev = quote; self.advance(1); let start = self.pos(); while !self.at_end() { let curr = self.curr_byte_unchecked(); // Advance until the closing quote. if curr == quote { // Check for escaped quote. if prev != b'\\' { break; } } prev = curr; self.advance(1); } let value = self.slice_back(start); // Check for closing quote. self.consume_byte(quote)?; Ok(value) } /// Consumes selected string. /// /// # Errors /// /// - `InvalidChar` /// - `UnexpectedEndOfStream` pub fn consume_string(&mut self, text: &[u8]) -> Result<(), Error> { if self.at_end() { return Err(Error::UnexpectedEndOfStream); } if !self.starts_with(text) { let len = std::cmp::min(text.len(), self.text.len() - self.pos); // Collect chars and do not slice a string, // because the `len` can be on the char boundary. // Which lead to a panic. let actual = self.text[self.pos..].chars().take(len).collect(); // Assume that all input `text` are valid UTF-8 strings, so unwrap is safe. let expected = std::str::from_utf8(text).unwrap().to_owned(); return Err(Error::InvalidString( vec![actual, expected], self.calc_char_pos(), )); } self.advance(text.len()); Ok(()) } /// Consumes bytes by the predicate and returns them. /// /// The result can be empty. pub fn consume_bytes(&mut self, f: F) -> &'a str where F: Fn(&Stream, u8) -> bool, { let start = self.pos(); self.skip_bytes(f); self.slice_back(start) } /// Consumes bytes by the predicate. pub fn skip_bytes(&mut self, f: F) where F: Fn(&Stream, u8) -> bool, { while !self.at_end() { let c = self.curr_byte_unchecked(); if f(self, c) { self.advance(1); } else { break; } } } /// Slices data from `pos` to the current position. #[inline] pub fn slice_back(&self, pos: usize) -> &'a str { &self.text[pos..self.pos] } /// Slices data from the current position to the end. #[inline] pub fn slice_tail(&self) -> &'a str { &self.text[self.pos..] } /// Parses integer number from the stream. /// /// Same as [`parse_number()`], but only for integer. Does not refer to any SVG type. /// /// [`parse_number()`]: #method.parse_number pub fn parse_integer(&mut self) -> Result { self.skip_spaces(); if self.at_end() { return Err(Error::InvalidNumber(self.calc_char_pos())); } let start = self.pos(); // Consume sign. if self.curr_byte()?.is_sign() { self.advance(1); } // The current char must be a digit. if !self.curr_byte()?.is_digit() { return Err(Error::InvalidNumber(self.calc_char_pos_at(start))); } self.skip_digits(); // Use the default i32 parser now. let s = self.slice_back(start); match i32::from_str(s) { Ok(n) => Ok(n), Err(_) => Err(Error::InvalidNumber(self.calc_char_pos_at(start))), } } /// Parses integer from a list of numbers. pub fn parse_list_integer(&mut self) -> Result { if self.at_end() { return Err(Error::UnexpectedEndOfStream); } let n = self.parse_integer()?; self.skip_spaces(); self.parse_list_separator(); Ok(n) } /// Parses number or percent from the stream. /// /// Percent value will be normalized. pub fn parse_number_or_percent(&mut self) -> Result { self.skip_spaces(); let n = self.parse_number()?; if self.starts_with(b"%") { self.advance(1); Ok(n / 100.0) } else { Ok(n) } } /// Parses number or percent from a list of numbers and/or percents. pub fn parse_list_number_or_percent(&mut self) -> Result { if self.at_end() { return Err(Error::UnexpectedEndOfStream); } let l = self.parse_number_or_percent()?; self.skip_spaces(); self.parse_list_separator(); Ok(l) } /// Skips digits. pub fn skip_digits(&mut self) { self.skip_bytes(|_, c| c.is_digit()); } #[inline] pub(crate) fn parse_list_separator(&mut self) { if self.is_curr_byte_eq(b',') { self.advance(1); } } } #[rustfmt::skip] #[cfg(test)] mod tests { use super::*; #[test] fn parse_integer_1() { let mut s = Stream::from("10"); assert_eq!(s.parse_integer().unwrap(), 10); } #[test] fn parse_err_integer_1() { // error because of overflow let mut s = Stream::from("10000000000000"); assert_eq!(s.parse_integer().unwrap_err().to_string(), "invalid number at position 1"); } } svgtypes-0.15.2/src/transform.rs000064400000000000000000000246041046102023000147530ustar 00000000000000use std::f64; use crate::{Error, Stream}; /// Representation of the [``] type. /// /// [``]: https://www.w3.org/TR/SVG2/coords.html#InterfaceSVGTransform #[derive(Clone, Copy, PartialEq, Debug)] #[allow(missing_docs)] pub struct Transform { pub a: f64, pub b: f64, pub c: f64, pub d: f64, pub e: f64, pub f: f64, } impl Transform { /// Constructs a new transform. #[inline] pub fn new(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Self { Transform { a, b, c, d, e, f } } } impl Default for Transform { #[inline] fn default() -> Transform { Transform::new(1.0, 0.0, 0.0, 1.0, 0.0, 0.0) } } /// Transform list token. #[derive(Clone, Copy, PartialEq, Debug)] #[allow(missing_docs)] pub enum TransformListToken { Matrix { a: f64, b: f64, c: f64, d: f64, e: f64, f: f64, }, Translate { tx: f64, ty: f64, }, Scale { sx: f64, sy: f64, }, Rotate { angle: f64, }, SkewX { angle: f64, }, SkewY { angle: f64, }, } /// A pull-based [``] parser. /// /// # Errors /// /// - Most of the `Error` types can occur. /// /// # Notes /// /// - There are no separate `rotate( )` type. /// It will be automatically split into three `Transform` tokens: /// `translate( ) rotate() translate(- -)`. /// Just like the spec is stated. /// /// # Examples /// /// ``` /// use svgtypes::{TransformListParser, TransformListToken}; /// /// let mut p = TransformListParser::from("scale(2) translate(10, -20)"); /// assert_eq!(p.next().unwrap().unwrap(), TransformListToken::Scale { sx: 2.0, sy: 2.0 } ); /// assert_eq!(p.next().unwrap().unwrap(), TransformListToken::Translate { tx: 10.0, ty: -20.0 } ); /// assert_eq!(p.next().is_none(), true); /// ``` /// /// [``]: https://www.w3.org/TR/SVG11/shapes.html#PointsBNF #[derive(Clone, Copy, PartialEq, Debug)] pub struct TransformListParser<'a> { stream: Stream<'a>, rotate_ts: Option<(f64, f64)>, last_angle: Option, } impl<'a> From<&'a str> for TransformListParser<'a> { fn from(text: &'a str) -> Self { TransformListParser { stream: Stream::from(text), rotate_ts: None, last_angle: None, } } } impl<'a> Iterator for TransformListParser<'a> { type Item = Result; fn next(&mut self) -> Option { if let Some(a) = self.last_angle { self.last_angle = None; return Some(Ok(TransformListToken::Rotate { angle: a })); } if let Some((x, y)) = self.rotate_ts { self.rotate_ts = None; return Some(Ok(TransformListToken::Translate { tx: -x, ty: -y })); } self.stream.skip_spaces(); if self.stream.at_end() { // empty attribute is still a valid value return None; } let res = self.parse_next(); if res.is_err() { self.stream.jump_to_end(); } Some(res) } } impl<'a> TransformListParser<'a> { fn parse_next(&mut self) -> Result { let s = &mut self.stream; let start = s.pos(); let name = s.consume_ascii_ident(); s.skip_spaces(); s.consume_byte(b'(')?; let t = match name.as_bytes() { b"matrix" => TransformListToken::Matrix { a: s.parse_list_number()?, b: s.parse_list_number()?, c: s.parse_list_number()?, d: s.parse_list_number()?, e: s.parse_list_number()?, f: s.parse_list_number()?, }, b"translate" => { let x = s.parse_list_number()?; s.skip_spaces(); let y = if s.is_curr_byte_eq(b')') { // 'If is not provided, it is assumed to be zero.' 0.0 } else { s.parse_list_number()? }; TransformListToken::Translate { tx: x, ty: y } } b"scale" => { let x = s.parse_list_number()?; s.skip_spaces(); let y = if s.is_curr_byte_eq(b')') { // 'If is not provided, it is assumed to be equal to .' x } else { s.parse_list_number()? }; TransformListToken::Scale { sx: x, sy: y } } b"rotate" => { let a = s.parse_list_number()?; s.skip_spaces(); if !s.is_curr_byte_eq(b')') { // 'If optional parameters and are supplied, the rotate is about the // point (cx, cy). The operation represents the equivalent of the following // specification: // translate(, ) rotate() translate(-, -).' let cx = s.parse_list_number()?; let cy = s.parse_list_number()?; self.rotate_ts = Some((cx, cy)); self.last_angle = Some(a); TransformListToken::Translate { tx: cx, ty: cy } } else { TransformListToken::Rotate { angle: a } } } b"skewX" => TransformListToken::SkewX { angle: s.parse_list_number()?, }, b"skewY" => TransformListToken::SkewY { angle: s.parse_list_number()?, }, _ => { return Err(Error::UnexpectedData(s.calc_char_pos_at(start))); } }; s.skip_spaces(); s.consume_byte(b')')?; s.skip_spaces(); if s.is_curr_byte_eq(b',') { s.advance(1); } Ok(t) } } impl std::str::FromStr for Transform { type Err = Error; fn from_str(text: &str) -> Result { let tokens = TransformListParser::from(text); let mut ts = Transform::default(); for token in tokens { match token? { TransformListToken::Matrix { a, b, c, d, e, f } => { ts = multiply(&ts, &Transform::new(a, b, c, d, e, f)) } TransformListToken::Translate { tx, ty } => { ts = multiply(&ts, &Transform::new(1.0, 0.0, 0.0, 1.0, tx, ty)) } TransformListToken::Scale { sx, sy } => { ts = multiply(&ts, &Transform::new(sx, 0.0, 0.0, sy, 0.0, 0.0)) } TransformListToken::Rotate { angle } => { let v = angle.to_radians(); let a = v.cos(); let b = v.sin(); let c = -b; let d = a; ts = multiply(&ts, &Transform::new(a, b, c, d, 0.0, 0.0)) } TransformListToken::SkewX { angle } => { let c = angle.to_radians().tan(); ts = multiply(&ts, &Transform::new(1.0, 0.0, c, 1.0, 0.0, 0.0)) } TransformListToken::SkewY { angle } => { let b = angle.to_radians().tan(); ts = multiply(&ts, &Transform::new(1.0, b, 0.0, 1.0, 0.0, 0.0)) } } } Ok(ts) } } #[inline(never)] fn multiply(ts1: &Transform, ts2: &Transform) -> Transform { Transform { a: ts1.a * ts2.a + ts1.c * ts2.b, b: ts1.b * ts2.a + ts1.d * ts2.b, c: ts1.a * ts2.c + ts1.c * ts2.d, d: ts1.b * ts2.c + ts1.d * ts2.d, e: ts1.a * ts2.e + ts1.c * ts2.f + ts1.e, f: ts1.b * ts2.e + ts1.d * ts2.f + ts1.f, } } #[rustfmt::skip] #[cfg(test)] mod tests { use std::str::FromStr; use super::*; macro_rules! test { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { let ts = Transform::from_str($text).unwrap(); let s = format!("matrix({} {} {} {} {} {})", ts.a, ts.b, ts.c, ts.d, ts.e, ts.f); assert_eq!(s, $result); } ) } test!(parse_1, "matrix(1 0 0 1 10 20)", "matrix(1 0 0 1 10 20)" ); test!(parse_2, "translate(10 20)", "matrix(1 0 0 1 10 20)" ); test!(parse_3, "scale(2 3)", "matrix(2 0 0 3 0 0)" ); test!(parse_4, "rotate(30)", "matrix(0.8660254037844387 0.49999999999999994 -0.49999999999999994 0.8660254037844387 0 0)" ); test!(parse_5, "rotate(30 10 20)", "matrix(0.8660254037844387 0.49999999999999994 -0.49999999999999994 0.8660254037844387 11.339745962155611 -2.3205080756887746)" ); test!(parse_6, "translate(10 15) translate(0 5)", "matrix(1 0 0 1 10 20)" ); test!(parse_7, "translate(10) scale(2)", "matrix(2 0 0 2 10 0)" ); test!(parse_8, "translate(25 215) scale(2) skewX(45)", "matrix(2 0 1.9999999999999998 2 25 215)" ); test!(parse_9, "skewX(45)", "matrix(1 0 0.9999999999999999 1 0 0)" ); macro_rules! test_err { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { let ts = Transform::from_str($text); assert_eq!(ts.unwrap_err().to_string(), $result); } ) } test_err!(parse_err_1, "text", "unexpected end of stream"); #[test] fn parse_err_2() { let mut ts = TransformListParser::from("scale(2) text"); let _ = ts.next().unwrap(); assert_eq!(ts.next().unwrap().unwrap_err().to_string(), "unexpected end of stream"); } test_err!(parse_err_3, "???G", "expected '(' not '?' at position 1"); #[test] fn parse_err_4() { let mut ts = TransformListParser::from(" "); assert_eq!(ts.next().is_none(), true); } #[test] fn parse_err_5() { let mut ts = TransformListParser::from("\x01"); assert_eq!(ts.next().unwrap().is_err(), true); } test_err!(parse_err_6, "rect()", "unexpected data at position 1"); test_err!(parse_err_7, "scale(2) rect()", "unexpected data at position 10"); } svgtypes-0.15.2/src/transform_origin.rs000064400000000000000000000220271046102023000163170ustar 00000000000000use crate::directional_position::DirectionalPosition; use crate::stream::Stream; use crate::{Length, LengthUnit}; #[derive(Clone, Copy, PartialEq, Debug)] #[allow(missing_docs)] enum Position { Length(Length), DirectionalPosition(DirectionalPosition), } impl Position { fn is_vertical(&self) -> bool { match self { Position::Length(_) => true, Position::DirectionalPosition(dp) => dp.is_vertical(), } } fn is_horizontal(&self) -> bool { match self { Position::Length(_) => true, Position::DirectionalPosition(dp) => dp.is_horizontal(), } } } impl From for Length { fn from(value: Position) -> Self { match value { Position::Length(l) => l, Position::DirectionalPosition(dp) => dp.into(), } } } /// Representation of the [``] type. /// /// [``]: https://drafts.csswg.org/css-transforms/#transform-origin-property #[derive(Clone, Copy, PartialEq, Debug)] pub struct TransformOrigin { /// The x offset of the transform origin. pub x_offset: Length, /// The y offset of the transform origin. pub y_offset: Length, /// The z offset of the transform origin. pub z_offset: Length, } impl TransformOrigin { /// Constructs a new transform origin. #[inline] pub fn new(x_offset: Length, y_offset: Length, z_offset: Length) -> Self { TransformOrigin { x_offset, y_offset, z_offset, } } } /// List of possible [`TransformOrigin`] parsing errors. #[derive(Clone, Copy, Debug)] pub enum TransformOriginError { /// One of the numbers is invalid. MissingParameters, /// One of the parameters is invalid. InvalidParameters, /// z-index is not a percentage. ZIndexIsPercentage, } impl std::fmt::Display for TransformOriginError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { TransformOriginError::MissingParameters => { write!(f, "transform origin doesn't have enough parameters") } TransformOriginError::InvalidParameters => { write!(f, "transform origin has invalid parameters") } TransformOriginError::ZIndexIsPercentage => { write!(f, "z-index cannot be a percentage") } } } } impl std::error::Error for TransformOriginError { fn description(&self) -> &str { "a transform origin parsing error" } } impl std::str::FromStr for TransformOrigin { type Err = TransformOriginError; fn from_str(text: &str) -> Result { let mut stream = Stream::from(text); if stream.at_end() { return Err(TransformOriginError::MissingParameters); } let parse_part = |stream: &mut Stream| { if let Ok(dp) = stream.parse_directional_position() { Some(Position::DirectionalPosition(dp)) } else if let Ok(l) = stream.parse_length() { Some(Position::Length(l)) } else { None } }; let first_arg = parse_part(&mut stream); let mut second_arg = None; let mut third_arg = None; if !stream.at_end() { stream.skip_spaces(); stream.parse_list_separator(); second_arg = Some(parse_part(&mut stream).ok_or(TransformOriginError::InvalidParameters)?); } if !stream.at_end() { stream.skip_spaces(); stream.parse_list_separator(); third_arg = Some( stream .parse_length() .map_err(|_| TransformOriginError::InvalidParameters)?, ); } stream.skip_spaces(); if !stream.at_end() { return Err(TransformOriginError::InvalidParameters); } let result = match (first_arg, second_arg, third_arg) { (Some(p), None, None) => { let (x_offset, y_offset) = if p.is_horizontal() { (p.into(), DirectionalPosition::Center.into()) } else { (DirectionalPosition::Center.into(), p.into()) }; TransformOrigin::new(x_offset, y_offset, Length::new(0.0, LengthUnit::Px)) } (Some(p1), Some(p2), length) => { if let Some(length) = length { if length.unit == LengthUnit::Percent { return Err(TransformOriginError::ZIndexIsPercentage); } } let length = length.unwrap_or(Length::new(0.0, LengthUnit::Px)); let check = |pos| match pos { Position::Length(_) => true, Position::DirectionalPosition(dp) => dp == DirectionalPosition::Center, }; let only_keyword_is_center = check(p1) && check(p2); if only_keyword_is_center { TransformOrigin::new(p1.into(), p2.into(), length) } else { // There is at least one of `left`, `right`, `top`, or `bottom` if p1.is_horizontal() && p2.is_vertical() { TransformOrigin::new(p1.into(), p2.into(), length) } else if p1.is_vertical() && p2.is_horizontal() { TransformOrigin::new(p2.into(), p1.into(), length) } else { return Err(TransformOriginError::InvalidParameters); } } } _ => unreachable!(), }; Ok(result) } } #[rustfmt::skip] #[cfg(test)] mod tests { use super::*; use std::str::FromStr; macro_rules! test { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { let v = TransformOrigin::from_str($text).unwrap(); assert_eq!(v, $result); } ) } test!(parse_1, "center", TransformOrigin::new(Length::new(50.0, LengthUnit::Percent), Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px))); test!(parse_2, "left", TransformOrigin::new(Length::new(0.0, LengthUnit::Percent), Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px))); test!(parse_3, "right", TransformOrigin::new(Length::new(100.0, LengthUnit::Percent), Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px))); test!(parse_4, "top", TransformOrigin::new(Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px))); test!(parse_5, "bottom", TransformOrigin::new(Length::new(50.0, LengthUnit::Percent), Length::new(100.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px))); test!(parse_6, "30px", TransformOrigin::new(Length::new(30.0, LengthUnit::Px), Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px))); test!(parse_7, "center left", TransformOrigin::new(Length::new(0.0, LengthUnit::Percent), Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px))); test!(parse_8, "left center", TransformOrigin::new(Length::new(0.0, LengthUnit::Percent), Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px))); test!(parse_9, "center bottom", TransformOrigin::new(Length::new(50.0, LengthUnit::Percent), Length::new(100.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px))); test!(parse_10, "bottom center", TransformOrigin::new(Length::new(50.0, LengthUnit::Percent), Length::new(100.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px))); test!(parse_11, "30%, center", TransformOrigin::new(Length::new(30.0, LengthUnit::Percent), Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px))); test!(parse_12, " center, 30%", TransformOrigin::new(Length::new(50.0, LengthUnit::Percent), Length::new(30.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px))); test!(parse_13, "left top", TransformOrigin::new(Length::new(0.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px))); test!(parse_14, "center right 3px", TransformOrigin::new(Length::new(100.0, LengthUnit::Percent), Length::new(50.0, LengthUnit::Percent), Length::new(3.0, LengthUnit::Px))); macro_rules! test_err { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { assert_eq!(TransformOrigin::from_str($text).unwrap_err().to_string(), $result); } ) } test_err!(parse_err_1, "", "transform origin doesn't have enough parameters"); test_err!(parse_err_2, "some", "transform origin has invalid parameters"); test_err!(parse_err_3, "center some", "transform origin has invalid parameters"); test_err!(parse_err_4, "left right", "transform origin has invalid parameters"); test_err!(parse_err_5, "left top 3%", "z-index cannot be a percentage"); } svgtypes-0.15.2/src/viewbox.rs000064400000000000000000000061251046102023000144210ustar 00000000000000use crate::Stream; /// List of possible [`ViewBox`] parsing errors. #[derive(Clone, Copy, Debug)] pub enum ViewBoxError { /// One of the numbers is invalid. InvalidNumber, /// ViewBox has a negative or zero size. InvalidSize, } impl std::fmt::Display for ViewBoxError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { ViewBoxError::InvalidNumber => { write!(f, "viewBox contains an invalid number") } ViewBoxError::InvalidSize => { write!(f, "viewBox has a negative or zero size") } } } } impl std::error::Error for ViewBoxError { fn description(&self) -> &str { "a viewBox parsing error" } } /// Representation of the [``] type. /// /// [``]: https://www.w3.org/TR/SVG2/coords.html#ViewBoxAttribute #[allow(missing_docs)] #[derive(Clone, Copy, PartialEq, Debug)] pub struct ViewBox { pub x: f64, pub y: f64, pub w: f64, pub h: f64, } impl ViewBox { /// Creates a new `ViewBox`. pub fn new(x: f64, y: f64, w: f64, h: f64) -> Self { ViewBox { x, y, w, h } } } impl std::str::FromStr for ViewBox { type Err = ViewBoxError; fn from_str(text: &str) -> Result { let mut s = Stream::from(text); let x = s .parse_list_number() .map_err(|_| ViewBoxError::InvalidNumber)?; let y = s .parse_list_number() .map_err(|_| ViewBoxError::InvalidNumber)?; let w = s .parse_list_number() .map_err(|_| ViewBoxError::InvalidNumber)?; let h = s .parse_list_number() .map_err(|_| ViewBoxError::InvalidNumber)?; if w <= 0.0 || h <= 0.0 { return Err(ViewBoxError::InvalidSize); } Ok(ViewBox::new(x, y, w, h)) } } #[rustfmt::skip] #[cfg(test)] mod tests { use super::*; use std::str::FromStr; macro_rules! test { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { let v = ViewBox::from_str($text).unwrap(); assert_eq!(v, $result); } ) } test!(parse_1, "-20 30 100 500", ViewBox::new(-20.0, 30.0, 100.0, 500.0)); macro_rules! test_err { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { assert_eq!(ViewBox::from_str($text).unwrap_err().to_string(), $result); } ) } test_err!(parse_err_1, "qwe", "viewBox contains an invalid number"); test_err!(parse_err_2, "10 20 30 0", "viewBox has a negative or zero size"); test_err!(parse_err_3, "10 20 0 40", "viewBox has a negative or zero size"); test_err!(parse_err_4, "10 20 0 0", "viewBox has a negative or zero size"); test_err!(parse_err_5, "10 20 -30 0", "viewBox has a negative or zero size"); test_err!(parse_err_6, "10 20 30 -40", "viewBox has a negative or zero size"); test_err!(parse_err_7, "10 20 -30 -40", "viewBox has a negative or zero size"); }