svgtypes-0.5.0/.gitignore010064400017500001750000000000621326471632700136220ustar0000000000000000target **/*.rs.bk Cargo.lock /.idea /svgtypes.iml svgtypes-0.5.0/.travis.yml010064400017500001750000000000531352150654500137360ustar0000000000000000language: rust rust: - 1.31.0 - stable svgtypes-0.5.0/CHANGELOG.md010064400017500001750000000060341352417550300134420ustar0000000000000000# 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.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.5.0...HEAD [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.5.0/Cargo.toml.orig010064400017500001750000000011631352417523500145200ustar0000000000000000[package] name = "svgtypes" # When updating version, also modify html_root_url in the lib.rs version = "0.5.0" authors = ["Evgeniy Reizner "] categories = ["parser-implementations"] description = "SVG types parser and writer." documentation = "https://docs.rs/svgtypes/" keywords = ["svg", "parser", "tokenizer"] license = "MIT/Apache-2.0" readme = "README.md" repository = "https://github.com/RazrFalcon/svgtypes" [workspace] members = ["benches"] [badges] travis-ci = { repository = "RazrFalcon/svgtypes" } [dependencies] float-cmp = { version = "0.5", default-features = false } siphasher = "0.2.3" svgtypes-0.5.0/Cargo.toml0000644000000020450000000000000107600ustar00# 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 believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] name = "svgtypes" version = "0.5.0" authors = ["Evgeniy Reizner "] description = "SVG types parser and writer." 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.float-cmp] version = "0.5" default-features = false [dependencies.siphasher] version = "0.2.3" [badges.travis-ci] repository = "RazrFalcon/svgtypes" svgtypes-0.5.0/LICENSE-APACHE010064400017500001750000000251371322753041200135540ustar0000000000000000 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.5.0/LICENSE-MIT010064400017500001750000000020441324355564700132730ustar0000000000000000Copyright (c) 2018 Evgeniy 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.5.0/README.md010064400017500001750000000125651352417540000131120ustar0000000000000000## svgtypes [![Build Status](https://travis-ci.org/RazrFalcon/svgtypes.svg?branch=master)](https://travis-ci.org/RazrFalcon/svgtypes) [![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) *svgtypes* is a collection of parsers, containers and writers for [SVG 1.1](https://www.w3.org/TR/SVG11/) types. Usage is simple as: ```rust use svgtypes::Path; let path: Path = "M10-20A5.5.3-4 110-.1".parse().unwrap(); assert_eq!(path.to_string(), "M 10 -20 A 5.5 0.3 -4 1 1 0 -0.1"); ``` You can also use a low-level, pull-based parser: ```rust use svgtypes::PathParser; let p = PathParser::from("M10-20A5.5.3-4 110-.1"); for token in p { println!("{:?}", token); } ``` You can also tweak an output format: ```rust use svgtypes::{Path, WriteBuffer, WriteOptions}; let path_str = "M10-20A5.5.3-4 110-.1"; let path: Path = path_str.parse().unwrap(); let opt = WriteOptions { remove_leading_zero: true, use_compact_path_notation: true, join_arc_to_flags: true, .. WriteOptions::default() }; assert_eq!(path.with_write_opt(&opt).to_string(), path_str); ``` ### Supported SVG types | SVG Type | Rust Type | Storage | Parser | | ------------------------- | ------------- | ------- | ------------------- | | [\] | Color | Stack | | | [\] | f64 | Stack | | | [\] | Length | Stack | | | [\] | Angle | Stack | | | [\] | ViewBox | Stack | | | [\] | Path | Heap | PathParser | | [\] | NumberList | Heap | NumberListParser | | [\] | LengthList | Heap | LengthListParser | | [\] | Transform | Stack | TransformListParser | | [\] | Points | Heap | PointsParser | | [\] | - | - | Paint | [\]: https://www.w3.org/TR/SVG11/types.html#DataTypeColor [\]: https://www.w3.org/TR/SVG11/types.html#DataTypeNumber [\]: https://www.w3.org/TR/SVG11/types.html#DataTypeLength [\]: https://www.w3.org/TR/SVG11/types.html#DataTypeAngle [\]: https://www.w3.org/TR/SVG11/coords.html#ViewBoxAttribute [\]: https://www.w3.org/TR/SVG11/paths.html#PathData [\]: https://www.w3.org/TR/SVG11/types.html#DataTypeList [\]: https://www.w3.org/TR/SVG11/types.html#DataTypeList [\]: https://www.w3.org/TR/SVG11/types.html#DataTypeTransformList [\]: https://www.w3.org/TR/SVG11/shapes.html#PointsBNF [\]: https://www.w3.org/TR/SVG11/painting.html#SpecifyingPaint - All types implement from string (`FromStr`) and to string traits (`Display`, `WriteBuffer`). - The library doesn't store transform list as is. It will premultiplied. - The `paint` type can only be parsed. ### Benefits - Complete support of paths, so data like `M10-20A5.5.3-4 110-.1` will be parsed correctly. - Access to pull-based parsers. - 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. - Implicit path commands are not supported. All commands will be parsed as explicit. - Implicit MoveTo commands will be automatically converted into explicit LineTo. ### Safety - The library should not panic. Any panic considered as a critical bug and should be reported. - The library forbids unsafe code. ### Alternatives None. ### Migration from svgparser This crate is a successor for the [`svgparser`](https://github.com/RazrFalcon/svgparser) crate, but it differs from it in many ways. - There is no XML parser or writer. You can use any you like. - Unlike the `svgparser` this crate not only parse values but can also store and write them. Currently, it has a minimal API for manipulating this values. - No [`AttributeValue`](https://docs.rs/svgparser/0.8.0/svgparser/enum.AttributeValue.html). This crate provides only value parsers. You should match attributes and values by yourself. - No [`ValueId`](https://docs.rs/svgparser/0.8.0/svgparser/enum.ValueId.html). It's up to you how to store those values. ### Dependency [Rust](https://www.rust-lang.org/) >= 1.31 ### 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.5.0/README.tpl010064400017500001750000000030611352417537300133110ustar0000000000000000## {{crate}} [![Build Status](https://travis-ci.org/RazrFalcon/{{crate}}.svg?branch=master)](https://travis-ci.org/RazrFalcon/{{crate}}) [![Crates.io](https://img.shields.io/crates/v/{{crate}}.svg)](https://crates.io/crates/{{crate}}) [![Documentation](https://docs.rs/{{crate}}/badge.svg)](https://docs.rs/{{crate}}) {{readme}} ### Migration from svgparser This crate is a successor for the [`svgparser`](https://github.com/RazrFalcon/svgparser) crate, but it differs from it in many ways. - There is no XML parser or writer. You can use any you like. - Unlike the `svgparser` this crate not only parse values but can also store and write them. Currently, it has a minimal API for manipulating this values. - No [`AttributeValue`](https://docs.rs/svgparser/0.8.0/svgparser/enum.AttributeValue.html). This crate provides only value parsers. You should match attributes and values by yourself. - No [`ValueId`](https://docs.rs/svgparser/0.8.0/svgparser/enum.ValueId.html). It's up to you how to store those values. ### Dependency [Rust](https://www.rust-lang.org/) >= 1.31 ### 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.5.0/src/angle.rs010064400017500001750000000057071342536116600140640ustar0000000000000000use std::str::FromStr; use { Error, FuzzyEq, Result, Stream, WriteBuffer, WriteOptions, }; /// List of all SVG angle units. #[derive(Clone, Copy, Debug, PartialEq)] #[allow(missing_docs)] pub enum AngleUnit { Degrees, Gradians, Radians, } /// Representation of the [``] type. /// /// [``]: https://www.w3.org/TR/SVG11/types.html#DataTypeAngle #[derive(Clone, Copy, Debug, PartialEq)] #[allow(missing_docs)] pub struct Angle { pub num: f64, pub unit: AngleUnit, } impl Angle { /// Constructs a new angle. #[inline] pub fn new(num: f64, unit: AngleUnit) -> Angle { Angle { num, unit } } } impl FromStr for Angle { type Err = Error; 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.num, l.unit)) } } impl WriteBuffer for Angle { fn write_buf_opt(&self, opt: &WriteOptions, buf: &mut Vec) { self.num.write_buf_opt(opt, buf); let t: &[u8] = match self.unit { AngleUnit::Degrees => b"deg", AngleUnit::Gradians => b"grad", AngleUnit::Radians => b"rad", }; buf.extend_from_slice(t); } } impl_display!(Angle); impl FuzzyEq for Angle { fn fuzzy_eq(&self, other: &Self) -> bool { if self.unit != other.unit { return false; } self.num.fuzzy_eq(&other.num) } } #[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] 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"); } macro_rules! test_w { ($name:ident, $len:expr, $unit:expr, $result:expr) => ( #[test] fn $name() { let l = Angle::new($len, $unit); assert_eq!(l.to_string(), $result); } ) } test_w!(write_1, 1.0, AngleUnit::Degrees, "1deg"); test_w!(write_2, 1.0, AngleUnit::Gradians, "1grad"); test_w!(write_3, 1.0, AngleUnit::Radians, "1rad"); } svgtypes-0.5.0/src/aspect_ratio.rs010064400017500001750000000117621342536117300154470ustar0000000000000000use std::str::FromStr; use { Error, Result, Stream, WriteBuffer, WriteOptions, }; /// 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, Debug)] pub enum Align { None, XMinYMin, XMidYMin, XMaxYMin, XMinYMid, XMidYMid, XMaxYMid, XMinYMax, XMidYMax, XMaxYMax, } /// Representation of the [`preserveAspectRatio`] attribute. /// /// # Examples /// /// ``` /// use std::str::FromStr; /// use svgtypes::AspectRatio; /// /// let ratio = AspectRatio::from_str("xMinYMax slice").unwrap(); /// assert_eq!(ratio.to_string(), "xMinYMax slice"); /// ``` /// /// [`preserveAspectRatio`]: https://www.w3.org/TR/SVG11/coords.html#PreserveAspectRatioAttribute #[derive(Clone, Copy, PartialEq, 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 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_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_ident(); match v { "meet" => {} "slice" => slice = true, "" => {} _ => { return Err(Error::UnexpectedData(s.calc_char_pos_at(start))) } }; } Ok(AspectRatio { defer, align, slice, }) } } impl WriteBuffer for AspectRatio { fn write_buf_opt(&self, _: &WriteOptions, buf: &mut Vec) { if self.defer { buf.extend_from_slice(b"defer "); } let align = match self.align { Align::None => "none", Align::XMinYMin => "xMinYMin", Align::XMidYMin => "xMidYMin", Align::XMaxYMin => "xMaxYMin", Align::XMinYMid => "xMinYMid", Align::XMidYMid => "xMidYMid", Align::XMaxYMid => "xMaxYMid", Align::XMinYMax => "xMinYMax", Align::XMidYMax => "xMidYMax", Align::XMaxYMax => "xMaxYMax", }; buf.extend_from_slice(align.as_bytes()); if self.slice { buf.extend_from_slice(b" slice"); } } } impl_display!(AspectRatio); impl Default for AspectRatio { fn default() -> Self { AspectRatio { defer: false, align: Align::XMidYMid, slice: false, } } } #[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, }); #[test] fn write_1() { assert_eq!(AspectRatio::default().to_string(), "xMidYMid"); } #[test] fn write_2() { assert_eq!(AspectRatio { defer: true, align: Align::None, slice: true, }.to_string(), "defer none slice"); } } svgtypes-0.5.0/src/color/colors.rs010064400017500001750000000265741352417506100154170ustar0000000000000000// This file is autogenerated. Do not edit it! use Color; static COLORS: Map = Map { key: 3213172566270843353, disps: &[ (3, 123), (2, 11), (1, 0), (1, 21), (0, 49), (2, 57), (2, 85), (1, 0), (0, 36), (0, 11), (7, 112), (24, 52), (0, 0), (0, 6), (1, 69), (0, 88), (1, 70), (0, 42), (9, 94), (0, 42), (0, 88), (5, 96), (2, 144), (2, 7), (3, 103), (1, 68), (0, 2), (0, 69), (0, 111), (0, 0), ], entries: &[ ("lawngreen", Color { red: 124, green: 252, blue: 0 }), ("black", Color { red: 0, green: 0, blue: 0 }), ("dimgray", Color { red: 105, green: 105, blue: 105 }), ("darkkhaki", Color { red: 189, green: 183, blue: 107 }), ("dodgerblue", Color { red: 30, green: 144, blue: 255 }), ("darkolivegreen", Color { red: 85, green: 107, blue: 47 }), ("coral", Color { red: 255, green: 127, blue: 80 }), ("slateblue", Color { red: 106, green: 90, blue: 205 }), ("powderblue", Color { red: 176, green: 224, blue: 230 }), ("orange", Color { red: 255, green: 165, blue: 0 }), ("red", Color { red: 255, green: 0, blue: 0 }), ("palegreen", Color { red: 152, green: 251, blue: 152 }), ("lightseagreen", Color { red: 32, green: 178, blue: 170 }), ("lightgray", Color { red: 211, green: 211, blue: 211 }), ("peru", Color { red: 205, green: 133, blue: 63 }), ("lavender", Color { red: 230, green: 230, blue: 250 }), ("indigo", Color { red: 75, green: 0, blue: 130 }), ("darkmagenta", Color { red: 139, green: 0, blue: 139 }), ("palegoldenrod", Color { red: 238, green: 232, blue: 170 }), ("darkseagreen", Color { red: 143, green: 188, blue: 143 }), ("mediumpurple", Color { red: 147, green: 112, blue: 219 }), ("yellowgreen", Color { red: 154, green: 205, blue: 50 }), ("orchid", Color { red: 218, green: 112, blue: 214 }), ("paleturquoise", Color { red: 175, green: 238, blue: 238 }), ("mediumblue", Color { red: 0, green: 0, blue: 205 }), ("firebrick", Color { red: 178, green: 34, blue: 34 }), ("bisque", Color { red: 255, green: 228, blue: 196 }), ("plum", Color { red: 221, green: 160, blue: 221 }), ("ivory", Color { red: 255, green: 255, blue: 240 }), ("salmon", Color { red: 250, green: 128, blue: 114 }), ("slategray", Color { red: 112, green: 128, blue: 144 }), ("lightgreen", Color { red: 144, green: 238, blue: 144 }), ("violet", Color { red: 238, green: 130, blue: 238 }), ("darksalmon", Color { red: 233, green: 150, blue: 122 }), ("aliceblue", Color { red: 240, green: 248, blue: 255 }), ("oldlace", Color { red: 253, green: 245, blue: 230 }), ("lightgoldenrodyellow", Color { red: 250, green: 250, blue: 210 }), ("cadetblue", Color { red: 95, green: 158, blue: 160 }), ("papayawhip", Color { red: 255, green: 239, blue: 213 }), ("purple", Color { red: 128, green: 0, blue: 128 }), ("lightskyblue", Color { red: 135, green: 206, blue: 250 }), ("wheat", Color { red: 245, green: 222, blue: 179 }), ("darkslategrey", Color { red: 47, green: 79, blue: 79 }), ("teal", Color { red: 0, green: 128, blue: 128 }), ("cornsilk", Color { red: 255, green: 248, blue: 220 }), ("lightblue", Color { red: 173, green: 216, blue: 230 }), ("lemonchiffon", Color { red: 255, green: 250, blue: 205 }), ("darkred", Color { red: 139, green: 0, blue: 0 }), ("royalblue", Color { red: 65, green: 105, blue: 225 }), ("mediumseagreen", Color { red: 60, green: 179, blue: 113 }), ("rosybrown", Color { red: 188, green: 143, blue: 143 }), ("linen", Color { red: 250, green: 240, blue: 230 }), ("cornflowerblue", Color { red: 100, green: 149, blue: 237 }), ("indianred", Color { red: 205, green: 92, blue: 92 }), ("palevioletred", Color { red: 219, green: 112, blue: 147 }), ("turquoise", Color { red: 64, green: 224, blue: 208 }), ("gold", Color { red: 255, green: 215, blue: 0 }), ("aquamarine", Color { red: 127, green: 255, blue: 212 }), ("seashell", Color { red: 255, green: 245, blue: 238 }), ("darkturquoise", Color { red: 0, green: 206, blue: 209 }), ("honeydew", Color { red: 240, green: 255, blue: 240 }), ("tan", Color { red: 210, green: 180, blue: 140 }), ("antiquewhite", Color { red: 250, green: 235, blue: 215 }), ("aqua", Color { red: 0, green: 255, blue: 255 }), ("tomato", Color { red: 255, green: 99, blue: 71 }), ("lightslategray", Color { red: 119, green: 136, blue: 153 }), ("green", Color { red: 0, green: 128, blue: 0 }), ("darkblue", Color { red: 0, green: 0, blue: 139 }), ("slategrey", Color { red: 112, green: 128, blue: 144 }), ("peachpuff", Color { red: 255, green: 218, blue: 185 }), ("darkslategray", Color { red: 47, green: 79, blue: 79 }), ("darkgoldenrod", Color { red: 184, green: 134, blue: 11 }), ("deeppink", Color { red: 255, green: 20, blue: 147 }), ("grey", Color { red: 128, green: 128, blue: 128 }), ("steelblue", Color { red: 70, green: 130, blue: 180 }), ("forestgreen", Color { red: 34, green: 139, blue: 34 }), ("darkgray", Color { red: 169, green: 169, blue: 169 }), ("lightcyan", Color { red: 224, green: 255, blue: 255 }), ("silver", Color { red: 192, green: 192, blue: 192 }), ("burlywood", Color { red: 222, green: 184, blue: 135 }), ("blue", Color { red: 0, green: 0, blue: 255 }), ("cyan", Color { red: 0, green: 255, blue: 255 }), ("skyblue", Color { red: 135, green: 206, blue: 235 }), ("mediumaquamarine", Color { red: 102, green: 205, blue: 170 }), ("lightsteelblue", Color { red: 176, green: 196, blue: 222 }), ("khaki", Color { red: 240, green: 230, blue: 140 }), ("navy", Color { red: 0, green: 0, blue: 128 }), ("pink", Color { red: 255, green: 192, blue: 203 }), ("blueviolet", Color { red: 138, green: 43, blue: 226 }), ("darkorchid", Color { red: 153, green: 50, blue: 204 }), ("mintcream", Color { red: 245, green: 255, blue: 250 }), ("chocolate", Color { red: 210, green: 105, blue: 30 }), ("chartreuse", Color { red: 127, green: 255, blue: 0 }), ("lime", Color { red: 0, green: 255, blue: 0 }), ("mediumorchid", Color { red: 186, green: 85, blue: 211 }), ("lavenderblush", Color { red: 255, green: 240, blue: 245 }), ("mediumslateblue", Color { red: 123, green: 104, blue: 238 }), ("darkorange", Color { red: 255, green: 140, blue: 0 }), ("ghostwhite", Color { red: 248, green: 248, blue: 255 }), ("fuchsia", Color { red: 255, green: 0, blue: 255 }), ("moccasin", Color { red: 255, green: 228, blue: 181 }), ("white", Color { red: 255, green: 255, blue: 255 }), ("darkgrey", Color { red: 169, green: 169, blue: 169 }), ("maroon", Color { red: 128, green: 0, blue: 0 }), ("midnightblue", Color { red: 25, green: 25, blue: 112 }), ("limegreen", Color { red: 50, green: 205, blue: 50 }), ("lightcoral", Color { red: 240, green: 128, blue: 128 }), ("hotpink", Color { red: 255, green: 105, blue: 180 }), ("mistyrose", Color { red: 255, green: 228, blue: 225 }), ("lightslategrey", Color { red: 119, green: 136, blue: 153 }), ("goldenrod", Color { red: 218, green: 165, blue: 32 }), ("mediumturquoise", Color { red: 72, green: 209, blue: 204 }), ("seagreen", Color { red: 46, green: 139, blue: 87 }), ("floralwhite", Color { red: 255, green: 250, blue: 240 }), ("blanchedalmond", Color { red: 255, green: 235, blue: 205 }), ("springgreen", Color { red: 0, green: 255, blue: 127 }), ("lightyellow", Color { red: 255, green: 255, blue: 224 }), ("navajowhite", Color { red: 255, green: 222, blue: 173 }), ("gainsboro", Color { red: 220, green: 220, blue: 220 }), ("greenyellow", Color { red: 173, green: 255, blue: 47 }), ("deepskyblue", Color { red: 0, green: 191, blue: 255 }), ("sandybrown", Color { red: 244, green: 164, blue: 96 }), ("azure", Color { red: 240, green: 255, blue: 255 }), ("brown", Color { red: 165, green: 42, blue: 42 }), ("magenta", Color { red: 255, green: 0, blue: 255 }), ("dimgrey", Color { red: 105, green: 105, blue: 105 }), ("mediumvioletred", Color { red: 199, green: 21, blue: 133 }), ("snow", Color { red: 255, green: 250, blue: 250 }), ("yellow", Color { red: 255, green: 255, blue: 0 }), ("gray", Color { red: 128, green: 128, blue: 128 }), ("orangered", Color { red: 255, green: 69, blue: 0 }), ("crimson", Color { red: 220, green: 20, blue: 60 }), ("darkgreen", Color { red: 0, green: 100, blue: 0 }), ("lightpink", Color { red: 255, green: 182, blue: 193 }), ("mediumspringgreen", Color { red: 0, green: 250, blue: 154 }), ("thistle", Color { red: 216, green: 191, blue: 216 }), ("sienna", Color { red: 160, green: 82, blue: 45 }), ("lightgrey", Color { red: 211, green: 211, blue: 211 }), ("darkslateblue", Color { red: 72, green: 61, blue: 139 }), ("lightsalmon", Color { red: 255, green: 160, blue: 122 }), ("darkviolet", Color { red: 148, green: 0, blue: 211 }), ("saddlebrown", Color { red: 139, green: 69, blue: 19 }), ("darkcyan", Color { red: 0, green: 139, blue: 139 }), ("olive", Color { red: 128, green: 128, blue: 0 }), ("whitesmoke", Color { red: 245, green: 245, blue: 245 }), ("beige", Color { red: 245, green: 245, blue: 220 }), ("olivedrab", Color { red: 107, green: 142, blue: 35 }), ], }; 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::borrow::Borrow; 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]; let b = entry.0.borrow(); if b == 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.5.0/src/color/mod.rs010064400017500001750000000022111342536064500146570ustar0000000000000000mod colors; mod parser; mod writer; /// Representation of the [``] type. /// /// [``]: https://www.w3.org/TR/SVG11/types.html#DataTypeColor #[derive(Clone, Copy, PartialEq, Debug)] #[allow(missing_docs)] pub struct Color { pub red: u8, pub green: u8, pub blue: u8, } impl Color { /// Constructs a new `Color` from `red`, `green` and `blue` values. pub fn new(red: u8, green: u8, blue: u8) -> Color { Color { red, green, blue } } /// Constructs a new `Color` set to black. pub fn black() -> Color { Color::new(0, 0, 0) } /// Constructs a new `Color` set to white. pub fn white() -> Color { Color::new(255, 255, 255) } /// Constructs a new `Color` set to gray. pub fn gray() -> Color { Color::new(128, 128, 128) } /// Constructs a new `Color` set to red. pub fn red() -> Color { Color::new(255, 0, 0) } /// Constructs a new `Color` set to green. pub fn green() -> Color { Color::new(0, 128, 0) } /// Constructs a new `Color` set to blue. pub fn blue() -> Color { Color::new(0, 0, 255) } } svgtypes-0.5.0/src/color/parser.rs010064400017500001750000000164161342536107300154040ustar0000000000000000use std::cmp; use std::str::FromStr; use super::colors; use { ByteExt, Color, Error, LengthUnit, Result, Stream, }; impl FromStr for Color { type Err = Error; /// Parses `Color` from `StrSpan`. /// /// Parsing is done according to [spec]: /// /// ```text /// color ::= "#" hexdigit hexdigit hexdigit (hexdigit hexdigit hexdigit)? /// | "rgb(" wsp* integer comma integer comma integer wsp* ")" /// | "rgb(" wsp* integer "%" comma integer "%" comma integer "%" wsp* ")" /// | color-keyword /// hexdigit ::= [0-9A-Fa-f] /// comma ::= wsp* "," wsp* /// ``` /// \* The SVG spec has an error. There should be `number`, /// not an `integer` for percent values ([details]). /// /// # 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`. /// - Allocates heap memory for case-insensitive named colors comparison. /// /// [spec]: http://www.w3.org/TR/SVG/types.html#DataTypeColor /// [details]: https://lists.w3.org/Archives/Public/www-svg/2014Jan/0109.html fn from_str(text: &str) -> Result { let mut s = Stream::from(text); s.skip_spaces(); let mut color = Color::black(); if s.curr_byte()? == b'#' { s.advance(1); let color_str = s.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]); } 3 => { // #rgb color.red = short_hex(color_str[0]); color.green = short_hex(color_str[1]); color.blue = short_hex(color_str[2]); } _ => { return Err(Error::InvalidValue); } } } else if is_rgb(&s) { s.advance(4); let l = s.parse_list_length()?; if l.unit == LengthUnit::Percent { fn from_percent(v: f64) -> u8 { let d = 255.0 / 100.0; let n = (v * d).round() as i32; bound(0, n, 255) as u8 } color.red = from_percent(l.num); color.green = from_percent(s.parse_list_length()?.num); color.blue = from_percent(s.parse_list_length()?.num); } else { color.red = bound(0, l.num as i32, 255) as u8; color.green = bound(0, s.parse_list_integer()?, 255) as u8; color.blue = bound(0, s.parse_list_integer()?, 255) as u8; } s.skip_spaces(); s.consume_byte(b')')?; } else { // TODO: use str::eq_ignore_ascii_case after 1.23 let name = s.consume_ident().to_lowercase(); match colors::from_str(&name) { Some(c) => { color = c; } None => { return Err(Error::InvalidValue); } } } // 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) } } #[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 } fn is_rgb(s: &Stream) -> bool { let mut s = s.clone(); let prefix = s.consume_bytes(|_, c| c != b'('); if s.consume_byte(b'(').is_err() { return false; } #[allow(unused_imports)] #[allow(deprecated)] use std::ascii::AsciiExt; prefix.eq_ignore_ascii_case("rgb") } #[inline] fn bound(min: T, val: T, max: T) -> T { cmp::max(min, cmp::min(max, val)) } #[cfg(test)] mod tests { use std::str::FromStr; use 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(255, 0, 0) ); test!( rrggbb_upper, "#FF0000", Color::new(255, 0, 0) ); test!( rgb_hex, "#f00", Color::new(255, 0, 0) ); test!( rrggbb_spaced, " #ff0000 ", Color::new(255, 0, 0) ); test!( rgb_numeric, "rgb(254, 203, 231)", Color::new(254, 203, 231) ); test!( rgb_numeric_spaced, " rgb( 77 , 77 , 77 ) ", Color::new(77, 77, 77) ); test!( rgb_percentage, "rgb(50%, 50%, 50%)", Color::new(127, 127, 127) ); test!( rgb_percentage_overflow, "rgb(140%, -10%, 130%)", Color::new(255, 0, 255) ); test!( rgb_percentage_float, "rgb(33.333%,46.666%,93.333%)", Color::new(85, 119, 238) ); test!( rgb_numeric_upper_case, "RGB(254, 203, 231)", Color::new(254, 203, 231) ); test!( rgb_numeric_mixed_case, "RgB(254, 203, 231)", Color::new(254, 203, 231) ); test!( name_red, "red", Color::new(255, 0, 0) ); test!( name_red_spaced, " red ", Color::new(255, 0, 0) ); test!( name_red_upper_case, "RED", Color::new(255, 0, 0) ); test!( name_red_mixed_case, "ReD", Color::new(255, 0, 0) ); test!( name_cornflowerblue, "cornflowerblue", Color::new(100, 149, 237) ); 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" ); } svgtypes-0.5.0/src/color/writer.rs010064400017500001750000000030131342536107600154140ustar0000000000000000use { Color, WriteBuffer, WriteOptions, }; static CHARS: &[u8] = b"0123456789abcdef"; #[inline] fn int2hex(n: u8) -> (u8, u8) { (CHARS[(n >> 4) as usize], CHARS[(n & 0xf) as usize]) } impl WriteBuffer for Color { fn write_buf_opt(&self, opt: &WriteOptions, buf: &mut Vec) { // TODO: rgb() support // TODO: color name support buf.push(b'#'); let (r1, r2) = int2hex(self.red); let (g1, g2) = int2hex(self.green); let (b1, b2) = int2hex(self.blue); if opt.trim_hex_colors && r1 == r2 && g1 == g2 && b1 == b2 { buf.push(r1); buf.push(g1); buf.push(b1); } else { buf.push(r1); buf.push(r2); buf.push(g1); buf.push(g2); buf.push(b1); buf.push(b2); } } } impl_display!(Color); #[cfg(test)] mod tests { use super::*; use {WriteOptions, WriteBuffer}; macro_rules! test { ($name:ident, $c:expr, $trim:expr, $result:expr) => ( #[test] fn $name() { let mut opt = WriteOptions::default(); opt.trim_hex_colors = $trim; assert_eq!($c.with_write_opt(&opt).to_string(), $result); } ) } test!(write_1, Color::new(255, 0, 0), false, "#ff0000"); test!(write_2, Color::new(255, 127, 5), false, "#ff7f05"); test!(write_3, Color::new(255, 0, 0), true, "#f00"); test!(write_4, Color::new(255, 127, 5), true, "#ff7f05"); } svgtypes-0.5.0/src/error.rs010064400017500001750000000053001342536120400141050ustar0000000000000000use std::fmt; use std::error; /// List of all errors. #[derive(Debug)] 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/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), /// A viewBox with a negative or zero size. InvalidViewbox, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> 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::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) } Error::InvalidViewbox => { write!(f, "viewBox should have a positive size") } } } } impl error::Error for Error { fn description(&self) -> &str { "an SVG data parsing error" } } /// An alias to `Result`. pub(crate) type Result = ::std::result::Result; svgtypes-0.5.0/src/length.rs010064400017500001750000000111571352166131600142500ustar0000000000000000use std::str::FromStr; use { Error, FuzzyEq, Result, Stream, WriteBuffer, WriteOptions, }; /// List of all SVG length units. #[derive(Clone, Copy, Debug, PartialEq)] #[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/SVG11/types.html#DataTypeLength #[derive(Clone, Copy, Debug, PartialEq)] #[allow(missing_docs)] pub struct Length { pub num: f64, pub unit: LengthUnit, } impl Length { /// Constructs a new length. #[inline] pub fn new(num: f64, unit: LengthUnit) -> Length { Length { num, unit } } /// Constructs a new length with `LengthUnit::None`. #[inline] pub fn new_number(num: f64) -> Length { Length { num, 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 { num: 0.0, unit: LengthUnit::None } } } impl Default for Length { fn default() -> Self { Length::zero() } } impl FromStr for Length { type Err = Error; 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.num, l.unit)) } } impl WriteBuffer for Length { fn write_buf_opt(&self, opt: &WriteOptions, buf: &mut Vec) { self.num.write_buf_opt(opt, buf); let t: &[u8] = match self.unit { LengthUnit::None => b"", LengthUnit::Em => b"em", LengthUnit::Ex => b"ex", LengthUnit::Px => b"px", LengthUnit::In => b"in", LengthUnit::Cm => b"cm", LengthUnit::Mm => b"mm", LengthUnit::Pt => b"pt", LengthUnit::Pc => b"pc", LengthUnit::Percent => b"%", }; buf.extend_from_slice(t); } } impl_display!(Length); impl FuzzyEq for Length { fn fuzzy_eq(&self, other: &Self) -> bool { if self.unit != other.unit { return false; } self.num.fuzzy_eq(&other.num) } } #[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 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"); } macro_rules! test_w { ($name:ident, $len:expr, $unit:expr, $result:expr) => ( #[test] fn $name() { let l = Length::new($len, $unit); assert_eq!(l.to_string(), $result); } ) } test_w!(write_1, 1.0, LengthUnit::None, "1"); test_w!(write_2, 1.0, LengthUnit::Em, "1em"); test_w!(write_3, 1.0, LengthUnit::Ex, "1ex"); test_w!(write_4, 1.0, LengthUnit::Px, "1px"); test_w!(write_5, 1.0, LengthUnit::In, "1in"); test_w!(write_6, 1.0, LengthUnit::Cm, "1cm"); test_w!(write_7, 1.0, LengthUnit::Mm, "1mm"); test_w!(write_8, 1.0, LengthUnit::Pt, "1pt"); test_w!(write_9, 1.0, LengthUnit::Pc, "1pc"); test_w!(write_10, 1.0, LengthUnit::Percent, "1%"); } svgtypes-0.5.0/src/length_list.rs010064400017500001750000000035431352173337200153050ustar0000000000000000use std::str::FromStr; use { Error, Length, Result, Stream, WriteBuffer, WriteOptions, }; /// Representation of the [``] type. /// /// [``]: https://www.w3.org/TR/SVG11/types.html#DataTypeList #[derive(Clone, PartialEq, Default)] pub struct LengthList(pub Vec); impl_from_vec!(LengthList, LengthList, Length); impl_vec_defer!(LengthList, Length); impl_display!(LengthList); impl_debug_from_display!(LengthList); /// A pull-based [``] parser. /// /// # Example /// /// ``` /// 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/SVG11/types.html#DataTypeList #[derive(Clone, Copy, PartialEq, Debug)] pub struct LengthListParser<'a>(Stream<'a>); impl<'a> From<&'a str> for LengthListParser<'a> { 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) } } } impl FromStr for LengthList { type Err = Error; fn from_str(text: &str) -> Result { let mut vec = Vec::new(); for number in LengthListParser::from(text) { vec.push(number?); } Ok(LengthList(vec)) } } svgtypes-0.5.0/src/lib.rs010064400017500001750000000114171352417533400135370ustar0000000000000000/*! *svgtypes* is a collection of parsers, containers and writers for [SVG 1.1](https://www.w3.org/TR/SVG11/) types. Usage is simple as: ```rust use svgtypes::Path; let path: Path = "M10-20A5.5.3-4 110-.1".parse().unwrap(); assert_eq!(path.to_string(), "M 10 -20 A 5.5 0.3 -4 1 1 0 -0.1"); ``` You can also use a low-level, pull-based parser: ```rust use svgtypes::PathParser; let p = PathParser::from("M10-20A5.5.3-4 110-.1"); for token in p { println!("{:?}", token); } ``` You can also tweak an output format: ```rust use svgtypes::{Path, WriteBuffer, WriteOptions}; let path_str = "M10-20A5.5.3-4 110-.1"; let path: Path = path_str.parse().unwrap(); let opt = WriteOptions { remove_leading_zero: true, use_compact_path_notation: true, join_arc_to_flags: true, .. WriteOptions::default() }; assert_eq!(path.with_write_opt(&opt).to_string(), path_str); ``` ## Supported SVG types | SVG Type | Rust Type | Storage | Parser | | ------------------------- | ------------- | ------- | ------------------- | | [\] | Color | Stack | | | [\] | f64 | Stack | | | [\] | Length | Stack | | | [\] | Angle | Stack | | | [\] | ViewBox | Stack | | | [\] | Path | Heap | PathParser | | [\] | NumberList | Heap | NumberListParser | | [\] | LengthList | Heap | LengthListParser | | [\] | Transform | Stack | TransformListParser | | [\] | Points | Heap | PointsParser | | [\] | - | - | Paint | [\]: https://www.w3.org/TR/SVG11/types.html#DataTypeColor [\]: https://www.w3.org/TR/SVG11/types.html#DataTypeNumber [\]: https://www.w3.org/TR/SVG11/types.html#DataTypeLength [\]: https://www.w3.org/TR/SVG11/types.html#DataTypeAngle [\]: https://www.w3.org/TR/SVG11/coords.html#ViewBoxAttribute [\]: https://www.w3.org/TR/SVG11/paths.html#PathData [\]: https://www.w3.org/TR/SVG11/types.html#DataTypeList [\]: https://www.w3.org/TR/SVG11/types.html#DataTypeList [\]: https://www.w3.org/TR/SVG11/types.html#DataTypeTransformList [\]: https://www.w3.org/TR/SVG11/shapes.html#PointsBNF [\]: https://www.w3.org/TR/SVG11/painting.html#SpecifyingPaint - All types implement from string (`FromStr`) and to string traits (`Display`, `WriteBuffer`). - The library doesn't store transform list as is. It will premultiplied. - The `paint` type can only be parsed. ## Benefits - Complete support of paths, so data like `M10-20A5.5.3-4 110-.1` will be parsed correctly. - Access to pull-based parsers. - 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. - Implicit path commands are not supported. All commands will be parsed as explicit. - Implicit MoveTo commands will be automatically converted into explicit LineTo. ## Safety - The library should not panic. Any panic considered as a critical bug and should be reported. - The library forbids unsafe code. ## Alternatives None. */ #![doc(html_root_url = "https://docs.rs/svgtypes/0.5.0")] #![forbid(unsafe_code)] #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(missing_copy_implementations)] extern crate float_cmp; extern crate siphasher; macro_rules! matches { ($expression:expr, $($pattern:tt)+) => { match $expression { $($pattern)+ => true, _ => false } } } #[macro_use] mod traits; mod angle; mod aspect_ratio; mod color; mod error; mod length; mod length_list; mod number; mod number_list; mod options; mod paint; mod path; mod points; mod stream; mod transform; mod viewbox; pub use angle::*; pub use aspect_ratio::*; pub use color::*; pub use error::*; pub use length::*; pub use length_list::*; pub use number::*; pub use number_list::*; pub use options::*; pub use paint::*; pub use path::*; pub use points::*; pub use stream::*; pub use traits::*; pub use transform::*; pub use viewbox::*; svgtypes-0.5.0/src/number.rs010064400017500001750000000117461351357177100142710ustar0000000000000000use std::io::Write; use float_cmp::ApproxEqUlps; use { FuzzyEq, FuzzyZero, WriteBuffer, WriteOptions, }; impl FuzzyEq for f32 { #[inline] fn fuzzy_eq(&self, other: &f32) -> bool { self.approx_eq_ulps(other, 4) } } impl FuzzyEq for f64 { #[inline] fn fuzzy_eq(&self, other: &f64) -> bool { self.approx_eq_ulps(other, 4) } } impl FuzzyZero for f32 { #[inline] fn is_fuzzy_zero(&self) -> bool { self.fuzzy_eq(&0.0) } } impl FuzzyZero for f64 { #[inline] fn is_fuzzy_zero(&self) -> bool { self.fuzzy_eq(&0.0) } } impl WriteBuffer for f64 { fn write_buf_opt(&self, opt: &WriteOptions, buf: &mut Vec) { write_num(self, opt.remove_leading_zero, buf); } } fn write_num(num: &f64, rm_leading_zero: bool, buf: &mut Vec) { // If number is an integer, it's faster to write it as i32. if num.fract().is_fuzzy_zero() { write!(buf, "{}", *num as i32).unwrap(); return; } // Round numbers up to 11 digits to prevent writing // ugly numbers like 29.999999999999996. // It's not 100% correct, but differences are insignificant. let v = (num * 100_000_000_000.0).round() / 100_000_000_000.0; let start_pos = buf.len(); write!(buf, "{}", v).unwrap(); if rm_leading_zero { let mut has_dot = false; let mut pos = 0; for c in buf.iter().skip(start_pos) { if *c == b'.' { has_dot = true; break; } pos += 1; } if has_dot && buf[start_pos + pos - 1] == b'0' { if pos == 2 && num.is_sign_negative() { // -0.1 -> -.1 buf.remove(start_pos + 1); } else if pos == 1 && num.is_sign_positive() { // 0.1 -> .1 buf.remove(start_pos); } } } } #[cfg(test)] mod tests { use super::*; use 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"); macro_rules! test_w { ($name:ident, $num:expr, $rm_zero:expr, $result:expr) => ( #[test] fn $name() { let mut v = Vec::new(); write_num(&$num, $rm_zero, &mut v); assert_eq!(String::from_utf8(v).unwrap(), $result); } ) } test_w!(write_1, 1.0, false, "1"); test_w!(write_2, 0.0, false, "0"); test_w!(write_3, -0.0, false, "0"); test_w!(write_4, -1.0, false, "-1"); test_w!(write_5, 12345678.12345678, false, "12345678.123456782"); test_w!(write_6, -0.1, true, "-.1"); test_w!(write_7, 0.1, true, ".1"); test_w!(write_8, 1.0, true, "1"); test_w!(write_9, -1.0, true, "-1"); test_w!(write_10, 1.5, false, "1.5"); test_w!(write_11, 0.14186, false, "0.14186"); test_w!(write_12, 29.999999999999996, false, "30"); test_w!(write_13, 0.49999999999999994, false, "0.5"); // With some algorithms may produce 4273.680000000001. test_w!(write_14, 4273.68, false, "4273.68"); } svgtypes-0.5.0/src/number_list.rs010064400017500001750000000050551352165353000153110ustar0000000000000000use std::str::FromStr; use { Error, Result, Stream, WriteBuffer, WriteOptions, }; /// Representation of the [``] type. /// /// [``]: https://www.w3.org/TR/SVG11/types.html#DataTypeList #[derive(Clone, PartialEq, Default)] pub struct NumberList(pub Vec); impl_from_vec!(NumberList, NumberList, f64); impl_vec_defer!(NumberList, f64); impl_display!(NumberList); impl_debug_from_display!(NumberList); /// A pull-based [``] parser. /// /// # Example /// /// ``` /// 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/SVG11/types.html#DataTypeList #[derive(Clone, Copy, PartialEq, Debug)] pub struct NumberListParser<'a>(Stream<'a>); impl<'a> From<&'a str> for NumberListParser<'a> { 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) } } } impl FromStr for NumberList { type Err = Error; fn from_str(text: &str) -> Result { let mut vec = Vec::new(); for number in NumberListParser::from(text) { vec.push(number?); } Ok(NumberList(vec)) } } #[cfg(test)] mod tests { use super::*; use ListSeparator; #[test] fn write_1() { let list = NumberList(vec![1.0, 2.0, 3.0]); let mut opt = WriteOptions::default(); opt.list_separator = ListSeparator::Space; assert_eq!(list.with_write_opt(&opt).to_string(), "1 2 3"); } #[test] fn write_2() { let list = NumberList(vec![1.0, 2.0, 3.0]); let mut opt = WriteOptions::default(); opt.list_separator = ListSeparator::Comma; assert_eq!(list.with_write_opt(&opt).to_string(), "1,2,3"); } #[test] fn write_3() { let list = NumberList(vec![1.0, 2.0, 3.0]); let mut opt = WriteOptions::default(); opt.list_separator = ListSeparator::CommaSpace; assert_eq!(list.with_write_opt(&opt).to_string(), "1, 2, 3"); } } svgtypes-0.5.0/src/options.rs010064400017500001750000000075771342536124400144750ustar0000000000000000/// A separator type for a list of values. /// /// #[derive(Clone, Copy, PartialEq, Debug)] pub enum ListSeparator { /// `10,20` Comma, /// `10 20` Space, /// `10, 20` CommaSpace, } /// Options for SVG types writing. #[derive(Clone, Copy, PartialEq, Debug)] pub struct WriteOptions { /// Use #RGB color notation when possible. /// /// By default all colors written using #RRGGBB notation. /// /// # Examples /// /// `#ff0000` -> `#f00`, `#000000` -> `#000`, `#00aa00` -> `#0a0` /// /// Default: disabled pub trim_hex_colors: bool, /// Remove leading zero from numbers. /// /// # Examples /// /// - `0.1` -> `.1` /// - `-0.1` -> `-.1` /// /// Default: disabled pub remove_leading_zero: bool, /// Use compact path notation. /// /// SVG allow us to remove some symbols from path notation without breaking parsing. /// /// # Examples /// /// `M 10 -20 A 5.5 0.3 -4 1 1 0 -0.1` -> `M10-20A5.5.3-4 1 1 0-.1` /// /// Default: disabled pub use_compact_path_notation: bool, /// Join ArcTo flags. /// /// Elliptical arc curve segment has flags parameters, which can have values of `0` or `1`. /// Since we have fixed-width values, we can skip spaces between them. /// /// **Note:** Sadly, but most of the viewers doesn't support such notation, /// even though it's valid according to the SVG spec. /// /// # Examples /// /// `A 5 5 30 1 1 10 10` -> `A 5 5 30 1110 10` /// /// Default: disabled pub join_arc_to_flags: bool, /// Remove duplicated commands. /// /// If a segment has the same type as a previous then we can skip command specifier. /// /// # Examples /// /// `M 10 10 L 20 20 L 30 30 L 40 40` -> `M 10 10 L 20 20 30 30 40 40` /// /// Default: disabled pub remove_duplicated_path_commands: bool, /// Use implicit LineTo commands. /// /// 'If a MoveTo is followed by multiple pairs of coordinates, /// the subsequent pairs are treated as implicit LineTo commands.' /// /// # Examples /// /// `M 10 10 L 20 20 L 30 30` -> `M 10 10 20 20 30 30` /// /// Default: disabled pub use_implicit_lineto_commands: bool, /// Simplify transform matrices into short equivalent when possible. /// /// If not set - all transform will be saved as 'matrix'. /// /// # Examples /// /// ```text /// matrix(1 0 0 1 10 20) -> translate(10 20) /// matrix(1 0 0 1 10 0) -> translate(10) /// matrix(2 0 0 3 0 0) -> scale(2 3) /// matrix(2 0 0 2 0 0) -> scale(2) /// matrix(0 1 -1 0 0 0) -> rotate(-90) /// ``` /// /// Default: disabled pub simplify_transform_matrices: bool, /// Set the separator type for list types. /// /// Affects `Points`, `LengthList`, `NumberList` and `Transform`. /// /// Default: `ListSeparator::Space` pub list_separator: ListSeparator, } impl Default for WriteOptions { fn default() -> WriteOptions { WriteOptions { trim_hex_colors: false, remove_leading_zero: false, use_compact_path_notation: false, join_arc_to_flags: false, remove_duplicated_path_commands: false, use_implicit_lineto_commands: false, simplify_transform_matrices: false, list_separator: ListSeparator::Space, } } } impl WriteOptions { /// Writes a selected separator to the output buffer. /// /// Uses `WriteOptions::list_separator` option. pub fn write_separator(&self, out: &mut Vec) { match self.list_separator { ListSeparator::Space => out.push(b' '), ListSeparator::Comma => out.push(b','), ListSeparator::CommaSpace => out.extend_from_slice(b", "), } } } svgtypes-0.5.0/src/paint.rs010064400017500001750000000121111342536124700140740ustar0000000000000000use std::str::FromStr; use { Color, Error, Result, Stream, }; /// Representation of the fallback part of the [``] type. /// /// Used by the [`Paint`](enum.Paint.html) type. /// /// [``]: https://www.w3.org/TR/SVG11/painting.html#SpecifyingPaint #[derive(Clone, Copy, PartialEq, Debug)] pub enum PaintFallback { /// The `none` value. None, /// The `currentColor` value. CurrentColor, /// [``] value. /// /// [``]: https://www.w3.org/TR/SVG11/types.html#DataTypeColor Color(Color), } /// Representation of the [``] type. /// /// Doesn't own the data. Use only for parsing. /// /// `` isn't supported. /// /// [``]: https://www.w3.org/TR/SVG11/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, Debug)] pub enum Paint<'a> { /// The `none` value. None, /// The `inherit` value. Inherit, /// The `currentColor` value. CurrentColor, /// [``] value. /// /// [``]: https://www.w3.org/TR/SVG11/types.html#DataTypeColor Color(Color), /// [``] value with an optional fallback. /// /// [``]: https://www.w3.org/TR/SVG11/types.html#DataTypeFuncIRI FuncIRI(&'a str, Option), } impl<'a> Paint<'a> { /// Parsers a `Paint` from a `StrSpan`. /// /// We can't use the `FromStr` trait because it requires /// an owned value as a return type. 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), _ => { let mut s = Stream::from(text); s.skip_spaces(); 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), } } } } } } #[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.5.0/src/path/mod.rs010064400017500001750000000345461352173354400145140ustar0000000000000000mod parser; mod segment; mod writer; pub use self::parser::*; pub use self::segment::*; /// Representation of the SVG [path data]. /// /// [path data]: https://www.w3.org/TR/SVG11/paths.html#PathData #[derive(Clone, PartialEq, Default)] pub struct Path(pub Vec); impl Path { /// Constructs a new path. pub fn new() -> Self { Path(Vec::new()) } /// Constructs a new path with a specified capacity. pub fn with_capacity(capacity: usize) -> Self { Path(Vec::with_capacity(capacity)) } /// Converts path's segments into absolute one in-place. /// /// Original segments can be mixed (relative, absolute). pub fn conv_to_absolute(&mut self) { // position of the previous segment let mut prev_x = 0.0; let mut prev_y = 0.0; // Position of the previous MoveTo segment. // When we get 'm'(relative) segment, which is not first segment - then it's // relative to previous 'M'(absolute) segment, not to first segment. let mut prev_mx = 0.0; let mut prev_my = 0.0; let mut prev_cmd = PathCommand::MoveTo; for seg in self.iter_mut() { if seg.cmd() == PathCommand::ClosePath { prev_x = prev_mx; prev_y = prev_my; seg.set_absolute(true); continue; } let offset_x; let offset_y; if seg.is_relative() { if seg.cmd() == PathCommand::MoveTo && prev_cmd == PathCommand::ClosePath { offset_x = prev_mx; offset_y = prev_my; } else { offset_x = prev_x; offset_y = prev_y; } } else { offset_x = 0.0; offset_y = 0.0; } if seg.is_relative() { shift_segment_data(seg, offset_x, offset_y); } if seg.cmd() == PathCommand::MoveTo { prev_mx = seg.x().unwrap(); prev_my = seg.y().unwrap(); } seg.set_absolute(true); if seg.cmd() == PathCommand::HorizontalLineTo { prev_x = seg.x().unwrap(); } else if seg.cmd() == PathCommand::VerticalLineTo { prev_y = seg.y().unwrap(); } else { prev_x = seg.x().unwrap(); prev_y = seg.y().unwrap(); } prev_cmd = seg.cmd(); } } /// Converts path's segments into relative one in-place. /// /// Original segments can be mixed (relative, absolute). pub fn conv_to_relative(&mut self) { // NOTE: this method may look like 'conv_to_absolute', but it's a bit different. // position of the previous segment let mut prev_x = 0.0; let mut prev_y = 0.0; // Position of the previous MoveTo segment. // When we get 'm'(relative) segment, which is not first segment - then it's // relative to previous 'M'(absolute) segment, not to first segment. let mut prev_mx = 0.0; let mut prev_my = 0.0; let mut prev_cmd = PathCommand::MoveTo; for seg in self.iter_mut() { if seg.cmd() == PathCommand::ClosePath { prev_x = prev_mx; prev_y = prev_my; seg.set_absolute(false); continue; } let offset_x; let offset_y; if seg.is_absolute() { if seg.cmd() == PathCommand::MoveTo && prev_cmd == PathCommand::ClosePath { offset_x = prev_mx; offset_y = prev_my; } else { offset_x = prev_x; offset_y = prev_y; } } else { offset_x = 0.0; offset_y = 0.0; } // unlike in 'to_absolute', we should take prev values before changing segment data if seg.is_absolute() { if seg.cmd() == PathCommand::HorizontalLineTo { prev_x = seg.x().unwrap(); } else if seg.cmd() == PathCommand::VerticalLineTo { prev_y = seg.y().unwrap(); } else { prev_x = seg.x().unwrap(); prev_y = seg.y().unwrap(); } } else { if seg.cmd() == PathCommand::HorizontalLineTo { prev_x += seg.x().unwrap(); } else if seg.cmd() == PathCommand::VerticalLineTo { prev_y += seg.y().unwrap(); } else { prev_x += seg.x().unwrap(); prev_y += seg.y().unwrap(); } } if seg.cmd() == PathCommand::MoveTo { if seg.is_absolute() { prev_mx = seg.x().unwrap(); prev_my = seg.y().unwrap(); } else { prev_mx += seg.x().unwrap(); prev_my += seg.y().unwrap(); } } if seg.is_absolute() { shift_segment_data(seg, -offset_x, -offset_y); } seg.set_absolute(false); prev_cmd = seg.cmd(); } } /// Appends an absolute MoveTo segment. pub fn push_move_to(&mut self, x: f64, y: f64) { self.push(PathSegment::MoveTo { abs: true, x, y }); } /// Appends a relative MoveTo segment. pub fn push_rel_move_to(&mut self, x: f64, y: f64) { self.push(PathSegment::MoveTo { abs: false, x, y }); } /// Appends an absolute ClosePath segment. pub fn push_close_path(&mut self) { self.push(PathSegment::ClosePath { abs: true }); } /// Appends a relative ClosePath segment. pub fn push_rel_close_path(&mut self) { self.push(PathSegment::ClosePath { abs: false }); } /// Appends an absolute LineTo segment. pub fn push_line_to(&mut self, x: f64, y: f64) { self.push(PathSegment::LineTo { abs: true, x, y }); } /// Appends a relative LineTo segment. pub fn push_rel_line_to(&mut self, x: f64, y: f64) { self.push(PathSegment::LineTo { abs: false, x, y }); } /// Appends an absolute HorizontalLineTo segment. pub fn push_hline_to(&mut self, x: f64) { self.push(PathSegment::HorizontalLineTo { abs: true, x }); } /// Appends a relative HorizontalLineTo segment. pub fn push_rel_hline_to(&mut self, x: f64) { self.push(PathSegment::HorizontalLineTo { abs: false, x }); } /// Appends an absolute VerticalLineTo segment. pub fn push_vline_to(&mut self, y: f64) { self.push(PathSegment::VerticalLineTo { abs: true, y }); } /// Appends a relative VerticalLineTo segment. pub fn push_rel_vline_to(&mut self, y: f64) { self.push(PathSegment::VerticalLineTo { abs: false, y }); } /// Appends an absolute CurveTo segment. pub fn push_curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64) { self.push(PathSegment::CurveTo { abs: true, x1, y1, x2, y2, x, y }); } /// Appends a relative CurveTo segment. pub fn push_rel_curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64) { self.push(PathSegment::CurveTo { abs: false, x1, y1, x2, y2, x, y }); } /// Appends an absolute SmoothCurveTo segment. pub fn push_smooth_curve_to(&mut self, x2: f64, y2: f64, x: f64, y: f64) { self.push(PathSegment::SmoothCurveTo { abs: true, x2, y2, x, y }); } /// Appends a relative SmoothCurveTo segment. pub fn push_rel_smooth_curve_to(&mut self, x2: f64, y2: f64, x: f64, y: f64) { self.push(PathSegment::SmoothCurveTo { abs: false, x2, y2, x, y }); } /// Appends an absolute QuadTo segment. pub fn push_quad_to(&mut self, x1: f64, y1: f64, x: f64, y: f64) { self.push(PathSegment::Quadratic { abs: true, x1, y1, x, y }); } /// Appends a relative QuadTo segment. pub fn push_rel_quad_to(&mut self, x1: f64, y1: f64, x: f64, y: f64) { self.push(PathSegment::Quadratic { abs: false, x1, y1, x, y }); } /// Appends an absolute SmoothQuadTo segment. pub fn push_smooth_quad_to(&mut self, x: f64, y: f64) { self.push(PathSegment::SmoothQuadratic { abs: true, x, y }); } /// Appends a relative SmoothQuadTo segment. pub fn push_rel_smooth_quad_to(&mut self, x: f64, y: f64) { self.push(PathSegment::SmoothQuadratic { abs: false, x, y }); } /// Appends an absolute ArcTo segment. pub fn push_arc_to( &mut self, rx: f64, ry: f64, x_axis_rotation: f64, large_arc: bool, sweep: bool, x: f64, y: f64, ) { self.push(PathSegment::EllipticalArc { abs: true, rx, ry, x_axis_rotation, large_arc, sweep, x, y }); } /// Appends a relative ArcTo segment. pub fn push_rel_arc_to( &mut self, rx: f64, ry: f64, x_axis_rotation: f64, large_arc: bool, sweep: bool, x: f64, y: f64, ) { self.push(PathSegment::EllipticalArc { abs: false, rx, ry, x_axis_rotation, large_arc, sweep, x, y }); } } fn shift_segment_data(d: &mut PathSegment, offset_x: f64, offset_y: f64) { match *d { PathSegment::MoveTo { ref mut x, ref mut y, .. } => { *x += offset_x; *y += offset_y; } PathSegment::LineTo { ref mut x, ref mut y, .. } => { *x += offset_x; *y += offset_y; } PathSegment::HorizontalLineTo { ref mut x, .. } => { *x += offset_x; } PathSegment::VerticalLineTo { ref mut y, .. } => { *y += offset_y; } PathSegment::CurveTo { ref mut x1, ref mut y1, ref mut x2, ref mut y2, ref mut x, ref mut y, .. } => { *x1 += offset_x; *y1 += offset_y; *x2 += offset_x; *y2 += offset_y; *x += offset_x; *y += offset_y; } PathSegment::SmoothCurveTo { ref mut x2, ref mut y2, ref mut x, ref mut y, .. } => { *x2 += offset_x; *y2 += offset_y; *x += offset_x; *y += offset_y; } PathSegment::Quadratic { ref mut x1, ref mut y1, ref mut x, ref mut y, .. } => { *x1 += offset_x; *y1 += offset_y; *x += offset_x; *y += offset_y; } PathSegment::SmoothQuadratic { ref mut x, ref mut y, .. } => { *x += offset_x; *y += offset_y; } PathSegment::EllipticalArc { ref mut x, ref mut y, .. } => { *x += offset_x; *y += offset_y; } PathSegment::ClosePath { .. } => {} } } impl_from_vec!(Path, Path, PathSegment); impl_vec_defer!(Path, PathSegment); #[cfg(test)] mod to_absolute { use std::str::FromStr; use super::*; macro_rules! test { ($name:ident, $in_text:expr, $out_text:expr) => ( #[test] fn $name() { let mut path = Path::from_str($in_text).unwrap(); path.conv_to_absolute(); assert_eq!(path.to_string(), $out_text); } ) } test!(line_to, "m 10 20 l 20 20", "M 10 20 L 30 40"); test!(close_path, "m 10 20 l 20 20 z", "M 10 20 L 30 40 Z"); // test to check that parses implicit MoveTo as LineTo test!(implicit_line_to, "m 10 20 20 20", "M 10 20 L 30 40"); test!(hline_vline, "m 10 20 v 10 h 10 l 10 10", "M 10 20 V 30 H 20 L 30 40"); test!(curve, "m 10 20 c 10 10 10 10 10 10", "M 10 20 C 20 30 20 30 20 30"); test!(move_to_1, "m 10 20 l 10 10 m 10 10 l 10 10", "M 10 20 L 20 30 M 30 40 L 40 50"); test!(move_to_2, "m 10 20 l 10 10 z m 10 10 l 10 10", "M 10 20 L 20 30 Z M 20 30 L 30 40"); test!(move_to_3, "m 10 20 l 10 10 Z m 10 10 l 10 10", "M 10 20 L 20 30 Z M 20 30 L 30 40"); // MoveTo after ClosePath can be skipped test!(move_to_4, "m 10 20 l 10 10 Z l 10 10", "M 10 20 L 20 30 Z L 20 30"); test!(smooth_curve, "m 10 20 s 10 10 10 10", "M 10 20 S 20 30 20 30"); test!(quad, "m 10 20 q 10 10 10 10", "M 10 20 Q 20 30 20 30"); test!(arc_mixed, "M 30 150 a 40 40 0 0 1 65 50 Z m 30 30 A 20 20 0 0 0 125 230 Z \ m 40 24 a 20 20 0 0 1 65 50 z", "M 30 150 A 40 40 0 0 1 95 200 Z M 60 180 A 20 20 0 0 0 125 230 Z \ M 100 204 A 20 20 0 0 1 165 254 Z"); } #[cfg(test)] mod to_relative { use std::str::FromStr; use super::*; macro_rules! test { ($name:ident, $in_text:expr, $out_text:expr) => ( #[test] fn $name() { let mut path = Path::from_str($in_text).unwrap(); path.conv_to_relative(); assert_eq!(path.to_string(), $out_text); } ) } test!(line_to, "M 10 20 L 30 40", "m 10 20 l 20 20"); test!(close_path, "M 10 20 L 30 40 Z", "m 10 20 l 20 20 z"); test!(implicit_line_to, "M 10 20 30 40", "m 10 20 l 20 20"); test!(hline_vline, "M 10 20 V 30 H 20 L 30 40", "m 10 20 v 10 h 10 l 10 10"); test!(curve, "M 10 20 C 20 30 20 30 20 30", "m 10 20 c 10 10 10 10 10 10"); test!(move_to_1, "M 10 20 L 20 30 M 30 40 L 40 50", "m 10 20 l 10 10 m 10 10 l 10 10"); test!(move_to_2, "M 10 20 L 20 30 Z M 20 30 L 30 40", "m 10 20 l 10 10 z m 10 10 l 10 10"); test!(move_to_3, "M 10 20 L 20 30 z M 20 30 L 30 40", "m 10 20 l 10 10 z m 10 10 l 10 10"); // MoveTo after ClosePath can be skipped test!(move_to_4, "M 10 20 L 20 30 Z L 20 30", "m 10 20 l 10 10 z l 10 10"); test!(smooth_curve, "M 10 20 S 20 30 20 30", "m 10 20 s 10 10 10 10"); test!(quad, "M 10 20 Q 20 30 20 30", "m 10 20 q 10 10 10 10"); test!(arc_mixed, "M 30 150 a 40 40 0 0 1 65 50 Z m 30 30 A 20 20 0 0 0 125 230 Z \ m 40 24 a 20 20 0 0 1 65 50 z", "m 30 150 a 40 40 0 0 1 65 50 z m 30 30 a 20 20 0 0 0 65 50 z \ m 40 24 a 20 20 0 0 1 65 50 z"); } svgtypes-0.5.0/src/path/parser.rs010064400017500001750000000404541343030315500152120ustar0000000000000000use std::str::FromStr; use { Error, Path, PathSegment, Result, Stream, }; impl FromStr for Path { type Err = Error; /// Parses a string as `Path`. /// /// # Errors /// /// Will always return `Ok`. If an error will occur during the parsing, /// will return current segments. Even if there are none of them. fn from_str(text: &str) -> Result { let mut data = Vec::new(); for token in PathParser::from(text) { match token { Ok(token) => data.push(token), Err(_) => break, } } Ok(Path(data)) } } /// 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` /// /// # Example /// /// ``` /// 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/SVG11/paths.html#PathData #[derive(Clone, Copy, PartialEq, Debug)] pub struct PathParser<'a> { stream: Stream<'a>, prev_cmd: Option, } impl<'a> From<&'a str> for PathParser<'a> { fn from(v: &'a str) -> Self { PathParser { stream: Stream::from(v), prev_cmd: None, } } } impl<'a> Iterator for PathParser<'a> { type Item = Result; 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 { if !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. fn is_cmd(c: u8) -> bool { 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' => true, _ => false, } } /// Returns `true` if the selected char is the absolute command. fn is_absolute(c: u8) -> bool { debug_assert!(is_cmd(c)); match c { b'M' | b'Z' | b'L' | b'H' | b'V' | b'C' | b'S' | b'Q' | b'T' | b'A' => true, _ => false, } } /// Converts the selected command char into the relative command char. 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()))) } } } #[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 } ); } svgtypes-0.5.0/src/path/segment.rs010064400017500001750000000234561342536111400153660ustar0000000000000000use { FuzzyEq, }; /// List of all path commands. #[derive(Clone, Copy, PartialEq, Debug)] #[allow(missing_docs)] pub enum PathCommand { MoveTo, LineTo, HorizontalLineTo, VerticalLineTo, CurveTo, SmoothCurveTo, Quadratic, SmoothQuadratic, EllipticalArc, ClosePath, } /// Representation of the 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, }, } impl PathSegment { /// Sets the segment absolute value. pub fn set_absolute(&mut self, new_abs: bool) { match *self { PathSegment::MoveTo { ref mut abs, .. } | PathSegment::LineTo { ref mut abs, .. } | PathSegment::HorizontalLineTo { ref mut abs, .. } | PathSegment::VerticalLineTo { ref mut abs, .. } | PathSegment::CurveTo { ref mut abs, .. } | PathSegment::SmoothCurveTo { ref mut abs, .. } | PathSegment::Quadratic { ref mut abs, .. } | PathSegment::SmoothQuadratic { ref mut abs, .. } | PathSegment::EllipticalArc { ref mut abs, .. } | PathSegment::ClosePath { ref mut abs, .. } => { *abs = new_abs; } } } /// Returns a segment type. pub fn cmd(&self) -> PathCommand { match *self { PathSegment::MoveTo { .. } => PathCommand::MoveTo, PathSegment::LineTo { .. } => PathCommand::LineTo, PathSegment::HorizontalLineTo { .. } => PathCommand::HorizontalLineTo, PathSegment::VerticalLineTo { .. } => PathCommand::VerticalLineTo, PathSegment::CurveTo { .. } => PathCommand::CurveTo, PathSegment::SmoothCurveTo { .. } => PathCommand::SmoothCurveTo, PathSegment::Quadratic { .. } => PathCommand::Quadratic, PathSegment::SmoothQuadratic { .. } => PathCommand::SmoothQuadratic, PathSegment::EllipticalArc { .. } => PathCommand::EllipticalArc, PathSegment::ClosePath { .. } => PathCommand::ClosePath, } } /// Returns `true` if the segment is absolute. #[inline] pub fn is_absolute(&self) -> bool { match *self { PathSegment::MoveTo { abs, .. } | PathSegment::LineTo { abs, .. } | PathSegment::HorizontalLineTo { abs, .. } | PathSegment::VerticalLineTo { abs, .. } | PathSegment::CurveTo { abs, .. } | PathSegment::SmoothCurveTo { abs, .. } | PathSegment::Quadratic { abs, .. } | PathSegment::SmoothQuadratic { abs, .. } | PathSegment::EllipticalArc { abs, .. } | PathSegment::ClosePath { abs, .. } => { abs } } } #[inline] /// Returns `true` if the segment is relative. pub fn is_relative(&self) -> bool { !self.is_absolute() } /// Returns the `x` coordinate of the segment if it has one. pub fn x(&self) -> Option { match *self { PathSegment::MoveTo { x, .. } | PathSegment::LineTo { x, .. } | PathSegment::HorizontalLineTo { x, .. } | PathSegment::CurveTo { x, .. } | PathSegment::SmoothCurveTo { x, .. } | PathSegment::Quadratic { x, .. } | PathSegment::SmoothQuadratic { x, .. } | PathSegment::EllipticalArc { x, .. } => Some(x), PathSegment::VerticalLineTo { .. } | PathSegment::ClosePath { .. } => None, } } /// Returns the `y` coordinate of the segment if it has one. pub fn y(&self) -> Option { match *self { PathSegment::MoveTo { y, .. } | PathSegment::LineTo { y, .. } | PathSegment::VerticalLineTo { y, .. } | PathSegment::CurveTo { y, .. } | PathSegment::SmoothCurveTo { y, .. } | PathSegment::Quadratic { y, .. } | PathSegment::SmoothQuadratic { y, .. } | PathSegment::EllipticalArc { y, .. } => Some(y), PathSegment::HorizontalLineTo { .. } | PathSegment::ClosePath { .. } => None, } } } impl FuzzyEq for PathSegment { fn fuzzy_eq(&self, other: &Self) -> bool { use self::PathSegment as Seg; // TODO: find a way to wrap it in macro match (*self, *other) { (Seg::MoveTo { abs, x, y }, Seg::MoveTo { abs: oabs, x: ox, y: oy }) | (Seg::LineTo { abs, x, y }, Seg::LineTo { abs: oabs, x: ox, y: oy }) | (Seg::SmoothQuadratic { abs, x, y }, Seg::SmoothQuadratic { abs: oabs, x: ox, y: oy }) => { abs == oabs && x.fuzzy_eq(&ox) && y.fuzzy_eq(&oy) } (Seg::HorizontalLineTo { abs, x }, Seg::HorizontalLineTo { abs: oabs, x: ox }) => { abs == oabs && x.fuzzy_eq(&ox) } (Seg::VerticalLineTo { abs, y }, Seg::VerticalLineTo { abs: oabs, y: oy }) => { abs == oabs && y.fuzzy_eq(&oy) } (Seg::CurveTo { abs, x1, y1, x2, y2, x, y }, Seg::CurveTo { abs: oabs, x1: ox1, y1: oy1, x2: ox2, y2: oy2, x: ox, y: oy }) => { abs == oabs && x.fuzzy_eq(&ox) && y.fuzzy_eq(&oy) && x1.fuzzy_eq(&ox1) && y1.fuzzy_eq(&oy1) && x2.fuzzy_eq(&ox2) && y2.fuzzy_eq(&oy2) } (Seg::SmoothCurveTo { abs, x2, y2, x, y }, Seg::SmoothCurveTo { abs: oabs, x2: ox2, y2: oy2, x: ox, y: oy }) => { abs == oabs && x.fuzzy_eq(&ox) && y.fuzzy_eq(&oy) && x2.fuzzy_eq(&ox2) && y2.fuzzy_eq(&oy2) } (Seg::Quadratic { abs, x1, y1, x, y }, Seg::Quadratic { abs: oabs, x1: ox1, y1: oy1, x: ox, y: oy }) => { abs == oabs && x.fuzzy_eq(&ox) && y.fuzzy_eq(&oy) && x1.fuzzy_eq(&ox1) && y1.fuzzy_eq(&oy1) } (Seg::EllipticalArc { abs, rx, ry, x_axis_rotation, large_arc, sweep, x, y }, Seg::EllipticalArc { abs: oabs, rx: orx, ry: ory, x_axis_rotation: ox_axis_rotation, large_arc: olarge_arc, sweep: osweep, x: ox, y: oy }) => { abs == oabs && x.fuzzy_eq(&ox) && y.fuzzy_eq(&oy) && rx.fuzzy_eq(&orx) && ry.fuzzy_eq(&ory) && x_axis_rotation.fuzzy_eq(&ox_axis_rotation) && large_arc == olarge_arc && sweep == osweep } (Seg::ClosePath { abs }, Seg::ClosePath { abs: oabs }) => { abs == oabs } _ => false, } } } #[cfg(test)] mod fuzzy_eq_tests { use super::*; macro_rules! test { ($name:ident, $seg1:expr, $seg2:expr) => ( #[test] fn $name() { assert!($seg1 != $seg2); assert!($seg1.fuzzy_eq(&$seg2)); }) } // TODO: find a better way test!(m, PathSegment::MoveTo { abs: true, x: 10.0, y: 10.1 + 10.2 }, PathSegment::MoveTo { abs: true, x: 10.0, y: 20.3 } ); test!(l, PathSegment::LineTo { abs: true, x: 10.0, y: 10.1 + 10.2 }, PathSegment::LineTo { abs: true, x: 10.0, y: 20.3 } ); test!(h, PathSegment::HorizontalLineTo { abs: true, x: 10.1 + 10.2 }, PathSegment::HorizontalLineTo { abs: true, x: 20.3 } ); test!(v, PathSegment::VerticalLineTo { abs: true, y: 10.1 + 10.2 }, PathSegment::VerticalLineTo { abs: true, y: 20.3 } ); test!(c, PathSegment::CurveTo { abs: true, x1: 10.0, y1: 10.1 + 10.2, x2: 10.0, y2: 10.0, x: 10.0, y: 10.0 }, PathSegment::CurveTo { abs: true, x1: 10.0, y1: 20.3, x2: 10.0, y2: 10.0, x: 10.0, y: 10.0 } ); test!(s, PathSegment::SmoothCurveTo { abs: true, x2: 10.0, y2: 10.1 + 10.2, x: 10.0, y: 10.0 }, PathSegment::SmoothCurveTo { abs: true, x2: 10.0, y2: 20.3, x: 10.0, y: 10.0 } ); test!(q, PathSegment::Quadratic { abs: true, x1: 10.0, y1: 10.1 + 10.2, x: 10.0, y: 10.0 }, PathSegment::Quadratic { abs: true, x1: 10.0, y1: 20.3, x: 10.0, y: 10.0 } ); test!(t, PathSegment::SmoothQuadratic { abs: true, x: 10.0, y: 10.1 + 10.2 }, PathSegment::SmoothQuadratic { abs: true, x: 10.0, y: 20.3 } ); test!(a, PathSegment::EllipticalArc { abs: true, rx: 100.0, ry: 100.0, x_axis_rotation: 0.0, large_arc: true, sweep: true, x: 10.1 + 10.2, y: 10.0, }, PathSegment::EllipticalArc { abs: true, rx: 100.0, ry: 100.0, x_axis_rotation: 0.0, large_arc: true, sweep: true, x: 20.3, y: 10.0, } ); } svgtypes-0.5.0/src/path/writer.rs010064400017500001750000000305471351357512700152500ustar0000000000000000use { Path, PathCommand, PathSegment, WriteBuffer, WriteOptions, }; struct PrevCmd { cmd: PathCommand, absolute: bool, implicit: bool, } impl WriteBuffer for Path { fn write_buf_opt(&self, opt: &WriteOptions, buf: &mut Vec) { if self.is_empty() { return; } let mut prev_cmd: Option = None; let mut prev_coord_has_dot = false; for seg in self.iter() { let is_written = write_cmd(seg, &mut prev_cmd, opt, buf); write_segment(seg, is_written, &mut prev_coord_has_dot, opt, buf); } if !opt.use_compact_path_notation { let len = buf.len(); buf.truncate(len - 1); } } } fn write_cmd( seg: &PathSegment, prev_cmd: &mut Option, opt: &WriteOptions, buf: &mut Vec ) -> bool { let mut print_cmd = true; if opt.remove_duplicated_path_commands { // check that previous command is the same as current if let Some(ref pcmd) = *prev_cmd { // MoveTo commands can't be skipped if pcmd.cmd != PathCommand::MoveTo { if seg.cmd() == pcmd.cmd && seg.is_absolute() == pcmd.absolute { print_cmd = false; } } } } let mut is_implicit = false; if opt.use_implicit_lineto_commands { let check_implicit = || { if let Some(ref pcmd) = *prev_cmd { if seg.is_absolute() != pcmd.absolute { return false; } if pcmd.implicit { if seg.cmd() == PathCommand::LineTo { return true; } } else if pcmd.cmd == PathCommand::MoveTo && seg.cmd() == PathCommand::LineTo { // if current segment is LineTo and previous was MoveTo return true; } } false }; if check_implicit() { is_implicit = true; print_cmd = false; } } *prev_cmd = Some(PrevCmd { cmd: seg.cmd(), absolute: seg.is_absolute(), implicit: is_implicit, }); if !print_cmd { // we do not update 'prev_cmd' if we do not wrote it return false; } write_cmd_char(seg, buf); if !(seg.cmd() == PathCommand::ClosePath || opt.use_compact_path_notation) { buf.push(b' '); } true } pub fn write_cmd_char(seg: &PathSegment, buf: &mut Vec) { let cmd: u8 = if seg.is_absolute() { match seg.cmd() { PathCommand::MoveTo => b'M', PathCommand::LineTo => b'L', PathCommand::HorizontalLineTo => b'H', PathCommand::VerticalLineTo => b'V', PathCommand::CurveTo => b'C', PathCommand::SmoothCurveTo => b'S', PathCommand::Quadratic => b'Q', PathCommand::SmoothQuadratic => b'T', PathCommand::EllipticalArc => b'A', PathCommand::ClosePath => b'Z', } } else { match seg.cmd() { PathCommand::MoveTo => b'm', PathCommand::LineTo => b'l', PathCommand::HorizontalLineTo => b'h', PathCommand::VerticalLineTo => b'v', PathCommand::CurveTo => b'c', PathCommand::SmoothCurveTo => b's', PathCommand::Quadratic => b'q', PathCommand::SmoothQuadratic => b't', PathCommand::EllipticalArc => b'a', PathCommand::ClosePath => b'z', } }; buf.push(cmd); } pub fn write_segment( data: &PathSegment, is_written: bool, prev_coord_has_dot: &mut bool, opt: &WriteOptions, buf: &mut Vec ) { match *data { PathSegment::MoveTo { x, y, .. } | PathSegment::LineTo { x, y, .. } | PathSegment::SmoothQuadratic { x, y, .. } => { write_coords(&[x, y], is_written, prev_coord_has_dot, opt, buf); } PathSegment::HorizontalLineTo { x, .. } => { write_coords(&[x], is_written, prev_coord_has_dot, opt, buf); } PathSegment::VerticalLineTo { y, .. } => { write_coords(&[y], is_written, prev_coord_has_dot, opt, buf); } PathSegment::CurveTo { x1, y1, x2, y2, x, y, .. } => { write_coords(&[x1, y1, x2, y2, x, y], is_written, prev_coord_has_dot, opt, buf); } PathSegment::SmoothCurveTo { x2, y2, x, y, .. } => { write_coords(&[x2, y2, x, y], is_written, prev_coord_has_dot, opt, buf); } PathSegment::Quadratic { x1, y1, x, y, .. } => { write_coords(&[x1, y1, x, y], is_written, prev_coord_has_dot, opt, buf); } PathSegment::EllipticalArc { rx, ry, x_axis_rotation, large_arc, sweep, x, y, .. } => { write_coords(&[rx, ry, x_axis_rotation], is_written, prev_coord_has_dot, opt, buf); if opt.use_compact_path_notation { // flags must always have a space before it buf.push(b' '); } write_flag(large_arc, buf); if !opt.join_arc_to_flags { buf.push(b' '); } write_flag(sweep, buf); if !opt.join_arc_to_flags { buf.push(b' '); } // reset, because flags can't have dots *prev_coord_has_dot = false; // 'is_explicit_cmd' is always 'true' // because it's relevant only for first coordinate of the segment write_coords(&[x, y], true, prev_coord_has_dot, opt, buf); } PathSegment::ClosePath { .. } => { if !opt.use_compact_path_notation { buf.push(b' '); } } } } fn write_coords( coords: &[f64], is_explicit_cmd: bool, prev_coord_has_dot: &mut bool, opt: &WriteOptions, buf: &mut Vec ) { if opt.use_compact_path_notation { for (i, num) in coords.iter().enumerate() { let start_pos = buf.len() - 1; num.write_buf_opt(opt, buf); let c = buf[start_pos + 1]; let write_space = if !*prev_coord_has_dot && c == b'.' { !(i == 0 && is_explicit_cmd) } else if i == 0 && is_explicit_cmd { false } else if (c as char).is_digit(10) { true } else { false }; if write_space { buf.insert(start_pos + 1, b' '); } *prev_coord_has_dot = false; for c in buf.iter().skip(start_pos) { if *c == b'.' { *prev_coord_has_dot = true; break; } } } } else { for num in coords.iter() { num.write_buf_opt(opt, buf); buf.push(b' '); } } } fn write_flag(flag: bool, buf: &mut Vec) { buf.push(if flag { b'1' } else { b'0' }); } impl_display!(Path); impl_debug_from_display!(Path); #[cfg(test)] mod tests { use std::str::FromStr; use super::*; use WriteOptions; #[test] fn write_1() { let mut path = Path::new(); path.push(PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }); path.push(PathSegment::LineTo { abs: true, x: 10.0, y: 20.0 }); assert_eq!(path.to_string(), "M 10 20 L 10 20"); } #[test] fn write_2() { let path = Path::from_str("M 10 20 l 10 20").unwrap(); assert_eq!(path.to_string(), "M 10 20 l 10 20"); } #[test] fn write_3() { let path = Path::from_str( "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").unwrap(); assert_eq!(path.to_string(), "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"); } #[test] fn write_4() { let path = Path::from_str( "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").unwrap(); assert_eq!(path.to_string(), "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"); } #[test] fn write_5() { let path = Path::from_str("").unwrap(); assert_eq!(path.to_string(), ""); } macro_rules! test_write_opt { ($name:ident, $in_text:expr, $out_text:expr, $flag:ident) => ( #[test] fn $name() { let path = Path::from_str($in_text).unwrap(); let mut opt = WriteOptions::default(); opt.$flag = true; assert_eq!(path.with_write_opt(&opt).to_string(), $out_text); } ) } test_write_opt!(write_6, "M 10 20 L 30 40 L 50 60 l 70 80", "M 10 20 L 30 40 50 60 l 70 80", remove_duplicated_path_commands); test_write_opt!(write_7, "M 10 20 30 40 50 60", "M 10 20 L 30 40 50 60", remove_duplicated_path_commands); test_write_opt!(write_8, "M 10 20 L 30 40", "M10 20L30 40", use_compact_path_notation); test_write_opt!(write_9, "M 10 20 V 30 H 40 V 50 H 60 Z", "M10 20V30H40V50H60Z", use_compact_path_notation); #[test] fn write_10() { let path = Path::from_str("M 10 -20 A 5.5 0.3 -4 1 1 0 -0.1").unwrap(); let mut opt = WriteOptions::default(); opt.use_compact_path_notation = true; opt.join_arc_to_flags = true; opt.remove_leading_zero = true; assert_eq!(path.with_write_opt(&opt).to_string(), "M10-20A5.5.3-4 110-.1"); } test_write_opt!(write_11, "M 10-10 a 1 1 0 1 1 -1 1", "M10-10a1 1 0 1 1 -1 1", use_compact_path_notation); test_write_opt!(write_12, "M 10-10 a 1 1 0 1 1 0.1 1", "M10-10a1 1 0 1 1 0.1 1", use_compact_path_notation); test_write_opt!(write_13, "M 10 20 L 30 40 L 50 60 H 10", "M 10 20 30 40 50 60 H 10", use_implicit_lineto_commands); // should be ignored, because of different 'absolute' values test_write_opt!(write_14, "M 10 20 l 30 40 L 50 60", "M 10 20 l 30 40 L 50 60", use_implicit_lineto_commands); test_write_opt!(write_15, "M 10 20 L 30 40 l 50 60 L 50 60", "M 10 20 30 40 l 50 60 L 50 60", use_implicit_lineto_commands); test_write_opt!(write_16, "M 10 20 L 30 40 l 50 60", "M 10 20 30 40 l 50 60", use_implicit_lineto_commands); test_write_opt!(write_17, "M 10 20 L 30 40 L 50 60 M 10 20 L 30 40 L 50 60", "M 10 20 30 40 50 60 M 10 20 30 40 50 60", use_implicit_lineto_commands); #[test] fn write_18() { let path = Path::from_str("M 10 20 L 30 40 L 50 60 M 10 20 L 30 40 L 50 60").unwrap(); let mut opt = WriteOptions::default(); opt.use_implicit_lineto_commands = true; opt.remove_duplicated_path_commands = true; assert_eq!(path.with_write_opt(&opt).to_string(), "M 10 20 30 40 50 60 M 10 20 30 40 50 60"); } #[test] fn write_19() { let path = Path::from_str("m10 20 A 10 10 0 1 0 0 0 A 2 2 0 1 0 2 0").unwrap(); let mut opt = WriteOptions::default(); opt.use_compact_path_notation = true; opt.remove_duplicated_path_commands = true; opt.remove_leading_zero = true; // may generate as 'm10 20A10 10 0 1 0 0 0 2 2 0 1 0 2 0' <- two spaces assert_eq!(path.with_write_opt(&opt).to_string(), "m10 20A10 10 0 1 0 0 0 2 2 0 1 0 2 0"); } #[test] fn write_20() { let path = Path::from_str("M 0.1 0.1 L 1 0.1 2 -0.1").unwrap(); let mut opt = WriteOptions::default(); opt.use_compact_path_notation = true; opt.remove_duplicated_path_commands = true; opt.remove_leading_zero = true; assert_eq!(path.with_write_opt(&opt).to_string(), "M.1.1L1 .1 2-.1"); } test_write_opt!(write_21, "M 10 20 M 30 40 M 50 60 L 30 40", "M 10 20 M 30 40 M 50 60 L 30 40", remove_duplicated_path_commands); } svgtypes-0.5.0/src/points/mod.rs010064400017500001750000000005271352173350000150540ustar0000000000000000mod parser; mod writer; pub use self::parser::*; /// Representation of the [``] type. /// /// [``]: https://www.w3.org/TR/SVG11/shapes.html#PointsBNF #[derive(Clone, PartialEq, Default)] pub struct Points(pub Vec<(f64, f64)>); impl_from_vec!(Points, Points, (f64, f64)); impl_vec_defer!(Points, (f64, f64)); svgtypes-0.5.0/src/points/parser.rs010064400017500001750000000052651342564432000156010ustar0000000000000000use std::str::FromStr; use { Error, Points, Result, 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. /// /// # Example /// /// ``` /// 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, Debug)] pub struct PointsParser<'a>(Stream<'a>); impl<'a> From<&'a str> for PointsParser<'a> { 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)) } } } impl FromStr for Points { type Err = Error; fn from_str(text: &str) -> Result { // TODO: should contain at least two coordinate pairs Ok(Points(PointsParser::from(text).collect())) } } #[cfg(test)] mod tests { use std::str::FromStr; use super::*; use {WriteBuffer, WriteOptions, ListSeparator}; #[test] fn parse_1() { let points = Points::from_str("10 20 30 40").unwrap(); assert_eq!(*points, vec![(10.0, 20.0), (30.0, 40.0)]); } #[test] fn parse_2() { let points = Points::from_str("10 20 30 40 50").unwrap(); assert_eq!(*points, vec![(10.0, 20.0), (30.0, 40.0)]); } #[test] fn parse_3() { let points = Points::from_str("10 20 30 40").unwrap(); assert_eq!(points.to_string(), "10 20 30 40"); } #[test] fn parse_4() { let points = Points::from_str("10 20 30 40").unwrap(); let opt = WriteOptions { list_separator: ListSeparator::Comma, .. WriteOptions::default() }; assert_eq!(points.with_write_opt(&opt).to_string(), "10,20,30,40"); } } svgtypes-0.5.0/src/points/writer.rs010064400017500001750000000025641342536113500156200ustar0000000000000000use { ListSeparator, Points, WriteBuffer, WriteOptions, }; impl WriteBuffer for (f64, f64) { fn write_buf_opt(&self, opt: &WriteOptions, buf: &mut Vec) { self.0.write_buf_opt(opt, buf); match opt.list_separator { ListSeparator::Space => buf.push(b' '), ListSeparator::Comma => buf.push(b','), ListSeparator::CommaSpace => buf.extend_from_slice(b", "), } self.1.write_buf_opt(opt, buf); } } impl_display!(Points); impl_debug_from_display!(Points); #[cfg(test)] mod tests { use super::*; #[test] fn write_1() { let list = Points(vec![(1.0, 2.0), (3.0, 4.0)]); let mut opt = WriteOptions::default(); opt.list_separator = ListSeparator::Space; assert_eq!(list.with_write_opt(&opt).to_string(), "1 2 3 4"); } #[test] fn write_2() { let list = Points(vec![(1.0, 2.0), (3.0, 4.0)]); let mut opt = WriteOptions::default(); opt.list_separator = ListSeparator::Comma; assert_eq!(list.with_write_opt(&opt).to_string(), "1,2,3,4"); } #[test] fn write_3() { let list = Points(vec![(1.0, 2.0), (3.0, 4.0)]); let mut opt = WriteOptions::default(); opt.list_separator = ListSeparator::CommaSpace; assert_eq!(list.with_write_opt(&opt).to_string(), "1, 2, 3, 4"); } } svgtypes-0.5.0/src/stream.rs010064400017500001750000000503021343030304000142370ustar0000000000000000use std::str::{self, FromStr}; use std::cmp; use { Angle, AngleUnit, Error, Length, LengthUnit, Result, }; /// 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 char. /// /// `[A-Za-z]` fn is_letter(&self) -> bool; /// Checks if a byte is an XML ident char. fn is_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_letter(&self) -> bool { matches!(*self, b'A'...b'Z' | b'a'...b'z') } #[inline] fn is_ident(&self) -> bool { matches!(*self, b'0'...b'9' | b'A'...b'Z' | b'a'...b'z' | b'-' | b'_') } } /// A streaming text parsing interface. #[derive(PartialEq, Clone, Copy, Debug)] pub struct Stream<'a> { text: &'a str, pos: usize, } impl<'a> From<&'a str> for Stream<'a> { fn from(text: &'a str) -> Self { Stream { text, pos: 0, } } } impl<'a> Stream<'a> { /// Returns the current position in bytes. 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. 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()) } /// 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 byte from a current stream position if there is one. #[inline] pub fn get_curr_byte(&self) -> Option { if !self.at_end() { Some(self.curr_byte_unchecked()) } else { None } } /// Returns a next byte from a current stream position. /// /// # Errors /// /// - `UnexpectedEndOfStream` 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. /// /// # Examples /// /// ```should_panic /// use svgtypes::Stream; /// /// let mut s = Stream::from("text"); /// s.advance(2); // ok /// s.advance(20); // will cause a panic via debug_assert!(). /// ``` #[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. /// /// # Examples /// /// ``` /// use svgtypes::Stream; /// /// let mut s = Stream::from("Some text."); /// s.advance(5); /// assert_eq!(s.starts_with(b"text"), true); /// assert_eq!(s.starts_with(b"long"), false); /// ``` #[inline] pub fn starts_with(&self, text: &[u8]) -> bool { self.text.as_bytes()[self.pos..].starts_with(text) } /// Checks if the stream is starts with a space. /// /// Uses [`skip_spaces()`](#method.curr_byte) internally. pub fn starts_with_space(&self) -> bool { if self.at_end() { return false; } let mut is_space = false; let c = self.curr_byte_unchecked(); if c.is_space() { is_space = true; } is_space } /// Consumes current byte if it's equal to the provided byte. /// /// # Errors /// /// - `InvalidChar` /// - `UnexpectedEndOfStream` /// /// # Examples /// /// ``` /// use svgtypes::Stream; /// /// let mut s = Stream::from("Some text."); /// s.consume_byte(b'S').unwrap(); /// s.consume_byte(b'o').unwrap(); /// s.consume_byte(b'm').unwrap(); /// // s.consume_byte(b'q').unwrap(); // will produce an error /// ``` pub fn consume_byte(&mut self, c: u8) -> Result<()> { if self.curr_byte()? != c { return Err( Error::InvalidChar( vec![self.curr_byte_unchecked(), c], self.calc_char_pos(), ) ); } self.advance(1); Ok(()) } /// Consumes selected string. /// /// # Errors /// /// - `InvalidChar` /// - `UnexpectedEndOfStream` pub fn skip_string(&mut self, text: &[u8]) -> Result<()> { if self.at_end() { return Err(Error::UnexpectedEndOfStream); } if !self.starts_with(text) { let len = 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 = 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; } } } /// Consumes bytes by the predicate and returns them. pub fn consume_ident(&mut self) -> &'a str { let start = self.pos; self.skip_bytes(|_, c| c.is_ident()); self.slice_back(start) } /// Slices data from `pos` to the current position. pub fn slice_back(&self, pos: usize) -> &'a str { &self.text[pos..self.pos] } /// Slices data from the current position to the end. pub fn slice_tail(&self) -> &'a str { &self.text[self.pos..] } /// 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`. /// /// # Examples /// /// ``` /// use svgtypes::Stream; /// /// let mut s = Stream::from("3.14"); /// assert_eq!(s.parse_number().unwrap(), 3.14); /// assert_eq!(s.at_end(), true); /// ``` 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 the list of numbers. /// /// # Examples /// /// ``` /// use svgtypes::Stream; /// /// let mut s = Stream::from("3.14, 12,5 , 20-4"); /// assert_eq!(s.parse_list_number().unwrap(), 3.14); /// assert_eq!(s.parse_list_number().unwrap(), 12.0); /// assert_eq!(s.parse_list_number().unwrap(), 5.0); /// assert_eq!(s.parse_list_number().unwrap(), 20.0); /// assert_eq!(s.parse_list_number().unwrap(), -4.0); /// ``` pub fn parse_list_number(&mut self) -> Result { if self.at_end() { return Err(Error::UnexpectedEndOfStream); } let n = self.parse_number()?; self.skip_spaces(); parse_list_separator(self); Ok(n) } /// 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 the 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(); parse_list_separator(self); Ok(n) } /// Parses length from the stream. /// /// /// /// # Examples /// /// ``` /// use svgtypes::{Stream, Length, LengthUnit}; /// /// let mut s = Stream::from("30%"); /// assert_eq!(s.parse_length().unwrap(), Length::new(30.0, LengthUnit::Percent)); /// ``` /// /// # 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 the 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(); parse_list_separator(self); Ok(l) } /// 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 { AngleUnit::Degrees }; Ok(Angle::new(n, u)) } /// Skips digits. pub fn skip_digits(&mut self) { self.skip_bytes(|_, c| c.is_digit()); } /// Parses a [IRI]. /// /// By the SVG spec, the ID must contain only [Name] characters, /// but since no one fallows this it will parse any characters. /// /// [IRI]: https://www.w3.org/TR/SVG11/types.html#DataTypeIRI /// [Name]: https://www.w3.org/TR/xml/#NT-Name pub fn parse_iri(&mut self) -> Result<&'a str> { let mut _impl = || -> Result<&'a str> { self.skip_spaces(); self.consume_byte(b'#')?; let link = self.consume_bytes(|_, c| c != b' '); if !link.is_empty() { Ok(link) } else { Err(Error::InvalidValue) } }; _impl().map_err(|_| Error::InvalidValue) } /// Parses a [FuncIRI]. /// /// By the SVG spec, the ID must contain only [Name] characters, /// but since no one fallows this it will parse any characters. /// /// [FuncIRI]: https://www.w3.org/TR/SVG11/types.html#DataTypeFuncIRI /// [Name]: https://www.w3.org/TR/xml/#NT-Name pub fn parse_func_iri(&mut self) -> Result<&'a str> { let mut _impl = || -> Result<&'a str> { self.skip_spaces(); self.skip_string(b"url(")?; self.skip_spaces(); self.consume_byte(b'#')?; let link = self.consume_bytes(|_, c| c != b' ' && c != b')'); self.skip_spaces(); self.consume_byte(b')')?; if !link.is_empty() { Ok(link) } else { Err(Error::InvalidValue) } }; _impl().map_err(|_| Error::InvalidValue) } } #[inline] fn parse_list_separator(s: &mut Stream) { if s.is_curr_byte_eq(b',') { s.advance(1); } } #[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"); } #[test] fn parse_length_1() { let mut s = Stream::from("1,"); assert_eq!(s.parse_length().unwrap(), Length::new(1.0, LengthUnit::None)); } #[test] fn parse_length_2() { let mut s = Stream::from("1 ,"); assert_eq!(s.parse_length().unwrap(), Length::new(1.0, LengthUnit::None)); } #[test] fn parse_length_3() { let mut s = Stream::from("1 1"); assert_eq!(s.parse_length().unwrap(), Length::new(1.0, LengthUnit::None)); } #[test] fn parse_iri_1() { assert_eq!(Stream::from("#id").parse_iri().unwrap(), "id"); } #[test] fn parse_iri_2() { assert_eq!(Stream::from(" #id ").parse_iri().unwrap(), "id"); } #[test] fn parse_iri_3() { assert_eq!(Stream::from(" #id text").parse_iri().unwrap(), "id"); } #[test] fn parse_iri_4() { assert_eq!(Stream::from("#1").parse_iri().unwrap(), "1"); } #[test] fn parse_err_iri_1() { assert_eq!(Stream::from("# id").parse_iri().unwrap_err().to_string(), "invalid value"); } #[test] fn parse_func_iri_1() { assert_eq!(Stream::from("url(#id)").parse_func_iri().unwrap(), "id"); } #[test] fn parse_func_iri_2() { assert_eq!(Stream::from("url(#1)").parse_func_iri().unwrap(), "1"); } #[test] fn parse_func_iri_3() { assert_eq!(Stream::from(" url( #id ) ").parse_func_iri().unwrap(), "id"); } #[test] fn parse_err_func_iri_1() { assert_eq!(Stream::from("url ( #1 )").parse_func_iri().unwrap_err().to_string(), "invalid value"); } #[test] fn parse_err_func_iri_2() { assert_eq!(Stream::from("url(#)").parse_func_iri().unwrap_err().to_string(), "invalid value"); } #[test] fn parse_err_func_iri_3() { assert_eq!(Stream::from("url(# id)").parse_func_iri().unwrap_err().to_string(), "invalid value"); } } svgtypes-0.5.0/src/traits.rs010064400017500001750000000102071351343601300142630ustar0000000000000000use std::fmt; use { WriteOptions, }; /// A trait for writing data to the buffer. pub trait WriteBuffer { /// Writes data to the `Vec` buffer using specified `WriteOptions`. fn write_buf_opt(&self, opt: &WriteOptions, buf: &mut Vec); /// Writes data to the `Vec` buffer using default `WriteOptions`. fn write_buf(&self, buf: &mut Vec) { self.write_buf_opt(&WriteOptions::default(), buf); } /// Returns an object that implements `fmt::Display` using provided write options. fn with_write_opt<'a>(&'a self, opt: &'a WriteOptions) -> DisplaySvg<'a, Self> where Self: Sized { DisplaySvg { value: self, opt } } } impl WriteBuffer for Vec { fn write_buf_opt(&self, opt: &WriteOptions, buf: &mut Vec) { for (n, l) in self.iter().enumerate() { l.write_buf_opt(opt, buf); if n < self.len() - 1 { opt.write_separator(buf); } } } } /// A wrapper to use `fmt::Display` with [`WriteOptions`]. /// /// Should be used via `WriteBuffer::with_write_opt`. /// /// # Example /// /// ``` /// use svgtypes::{Transform, WriteOptions, WriteBuffer, DisplaySvg}; /// /// let ts = Transform::new(1.0, 0.0, 0.0, 1.0, 10.0, 20.0); /// assert_eq!(ts.to_string(), "matrix(1 0 0 1 10 20)"); /// /// let opt = WriteOptions { /// simplify_transform_matrices: true, /// .. WriteOptions::default() /// }; /// assert_eq!(ts.with_write_opt(&opt).to_string(), "translate(10 20)"); /// ``` /// /// [`WriteOptions`]: struct.WriteOptions.html pub struct DisplaySvg<'a, T: 'a + WriteBuffer> { value: &'a T, opt: &'a WriteOptions, } impl<'a, T: WriteBuffer> fmt::Debug for DisplaySvg<'a, T> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // Use Display. write!(f, "{}", self) } } impl<'a, T: WriteBuffer> fmt::Display for DisplaySvg<'a, T> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use std::str; let mut out = Vec::with_capacity(32); self.value.write_buf_opt(self.opt, &mut out); write!(f, "{}", str::from_utf8(&out).unwrap()) } } macro_rules! impl_display { ($t:ty) => ( impl ::std::fmt::Display for $t { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { write!(f, "{}", self.with_write_opt(&WriteOptions::default())) } } ) } /// A trait for fuzzy/approximate equality comparisons of float numbers. pub trait FuzzyEq { /// Returns `true` if values are approximately equal. fn fuzzy_eq(&self, other: &Rhs) -> bool; /// Returns `true` if values are not approximately equal. #[inline] fn fuzzy_ne(&self, other: &Rhs) -> bool { !self.fuzzy_eq(other) } } impl FuzzyEq for Vec { fn fuzzy_eq(&self, other: &Self) -> bool { if self.len() != other.len() { return false; } for (a, b) in self.iter().zip(other.iter()) { if a.fuzzy_ne(b) { return false; } } true } } /// A trait for fuzzy/approximate comparisons of float numbers. pub trait FuzzyZero: FuzzyEq { /// Returns `true` if the number is approximately zero. fn is_fuzzy_zero(&self) -> bool; } macro_rules! impl_vec_defer { ($t:ty, $tt:ty) => ( impl ::std::ops::Deref for $t { type Target = Vec<$tt>; fn deref(&self) -> &Self::Target { &self.0 } } impl ::std::ops::DerefMut for $t { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } ) } macro_rules! impl_from_vec { ($t:ty, $te:expr, $s:ty) => ( impl From> for $t { fn from(v: Vec<$s>) -> Self { $te(v) } } ) } macro_rules! impl_debug_from_display { ($t:ty) => ( impl ::std::fmt::Debug for $t { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { // Overload Display. write!(f, "{}", &self) } } ) } svgtypes-0.5.0/src/transform/mod.rs010064400017500001750000000205371352064614200155620ustar0000000000000000use std::f64; mod parser; mod writer; pub use self::parser::*; use { FuzzyEq, }; /// Representation of the [``] type. /// /// [``]: https://www.w3.org/TR/SVG11/coords.html#TransformAttribute #[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. pub fn new(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Self { Transform { a, b, c, d, e, f, } } /// Constructs a new translate transform. pub fn new_translate(x: f64, y: f64) -> Self { Transform::new(1.0, 0.0, 0.0, 1.0, x, y) } /// Constructs a new scale transform. pub fn new_scale(sx: f64, sy: f64) -> Self { Transform::new(sx, 0.0, 0.0, sy, 0.0, 0.0) } /// Constructs a new rotate transform. pub fn new_rotate(angle: f64) -> Self { let v = (angle / 180.0) * f64::consts::PI; let a = v.cos(); let b = v.sin(); let c = -b; let d = a; Transform::new(a, b, c, d, 0.0, 0.0) } /// Constructs a new rotate transform at the specified position. pub fn new_rotate_at(angle: f64, x: f64, y: f64) -> Self { let mut ts = Self::default(); ts.translate(x, y); ts.rotate(angle); ts.translate(-x, -y); ts } /// Constructs a new skew transform along then X axis. pub fn new_skew_x(angle: f64) -> Self { let c = ((angle / 180.0) * f64::consts::PI).tan(); Transform::new(1.0, 0.0, c, 1.0, 0.0, 0.0) } /// Constructs a new skew transform along then Y axis. pub fn new_skew_y(angle: f64) -> Self { let b = ((angle / 180.0) * f64::consts::PI).tan(); Transform::new(1.0, b, 0.0, 1.0, 0.0, 0.0) } /// Translates the current transform. pub fn translate(&mut self, x: f64, y: f64) { self.append(&Transform::new_translate(x, y)); } /// Scales the current transform. pub fn scale(&mut self, sx: f64, sy: f64) { self.append(&Transform::new_scale(sx, sy)); } /// Rotates the current transform. pub fn rotate(&mut self, angle: f64) { self.append(&Transform::new_rotate(angle)); } /// Rotates the current transform at the specified position. pub fn rotate_at(&mut self, angle: f64, x: f64, y: f64) { self.translate(x, y); self.rotate(angle); self.translate(-x, -y); } /// Skews the current transform along the X axis. pub fn skew_x(&mut self, angle: f64) { self.append(&Transform::new_skew_x(angle)); } /// Skews the current transform along the Y axis. pub fn skew_y(&mut self, angle: f64) { self.append(&Transform::new_skew_y(angle)); } /// Appends transform to the current transform. pub fn append(&mut self, other: &Transform) { let ts = multiply(self, other); self.a = ts.a; self.b = ts.b; self.c = ts.c; self.d = ts.d; self.e = ts.e; self.f = ts.f; } /// Prepends transform to the current transform. pub fn prepend(&mut self, other: &Transform) { let ts = multiply(other, self); self.a = ts.a; self.b = ts.b; self.c = ts.c; self.d = ts.d; self.e = ts.e; self.f = ts.f; } /// Returns `true` if the transform is default, aka `(1 0 0 1 0 0)`. pub fn is_default(&self) -> bool { self.a.fuzzy_eq(&1.0) && self.b.fuzzy_eq(&0.0) && self.c.fuzzy_eq(&0.0) && self.d.fuzzy_eq(&1.0) && self.e.fuzzy_eq(&0.0) && self.f.fuzzy_eq(&0.0) } /// Returns `true` if the transform contains only translate part, aka `(1 0 0 1 x y)`. pub fn is_translate(&self) -> bool { self.a.fuzzy_eq(&1.0) && self.b.fuzzy_eq(&0.0) && self.c.fuzzy_eq(&0.0) && self.d.fuzzy_eq(&1.0) && (self.e.fuzzy_ne(&0.0) || self.f.fuzzy_ne(&0.0)) } /// Returns `true` if the transform contains only scale part, aka `(sx 0 0 sy 0 0)`. pub fn is_scale(&self) -> bool { (self.a.fuzzy_ne(&1.0) || self.d.fuzzy_ne(&1.0)) && self.b.fuzzy_eq(&0.0) && self.c.fuzzy_eq(&0.0) && self.e.fuzzy_eq(&0.0) && self.f.fuzzy_eq(&0.0) } /// Returns `true` if the transform contains translate part. pub fn has_translate(&self) -> bool { self.e.fuzzy_ne(&0.0) || self.f.fuzzy_ne(&0.0) } /// Returns `true` if the transform contains scale part. pub fn has_scale(&self) -> bool { let (sx, sy) = self.get_scale(); sx.fuzzy_ne(&1.0) || sy.fuzzy_ne(&1.0) } /// Returns `true` if the transform scale is proportional. /// /// The proportional scale is when `` equal to ``. pub fn has_proportional_scale(&self) -> bool { let (sx, sy) = self.get_scale(); sx.fuzzy_eq(&sy) } /// Returns `true` if the transform contains skew part. pub fn has_skew(&self) -> bool { let (skew_x, skew_y) = self.get_skew(); skew_x.fuzzy_ne(&0.0) || skew_y.fuzzy_ne(&0.0) } /// Returns `true` if the transform contains rotate part. pub fn has_rotate(&self) -> bool { self.get_rotate().fuzzy_ne(&0.0) } /// Returns transform's translate part. pub fn get_translate(&self) -> (f64, f64) { (self.e, self.f) } /// Returns transform's scale part. pub fn get_scale(&self) -> (f64, f64) { let x_scale = (self.a * self.a + self.c * self.c).sqrt(); let y_scale = (self.b * self.b + self.d * self.d).sqrt(); (x_scale, y_scale) } /// Returns transform's skew part. pub fn get_skew(&self) -> (f64, f64) { let rad = 180.0 / f64::consts::PI; let skew_x = rad * (self.d).atan2(self.c) - 90.0; let skew_y = rad * (self.b).atan2(self.a); (skew_x, skew_y) } /// Returns transform's rotate part. pub fn get_rotate(&self) -> f64 { let rad = 180.0 / f64::consts::PI; let mut angle = (-self.b/self.a).atan() * rad; if self.b < self.c || self.b > self.c { angle = -angle; } angle } /// Applies transform to selected coordinates. pub fn apply(&self, x: f64, y: f64) -> (f64, f64) { let new_x = self.a * x + self.c * y + self.e; let new_y = self.b * x + self.d * y + self.f; (new_x, new_y) } /// Applies transform to selected coordinates. pub fn apply_to(&self, x: &mut f64, y: &mut f64) { let tx = *x; let ty = *y; *x = self.a * tx + self.c * ty + self.e; *y = self.b * tx + self.d * ty + self.f; } } #[inline] 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, } } impl Default for Transform { fn default() -> Transform { Transform::new(1.0, 0.0, 0.0, 1.0, 0.0, 0.0) } } impl FuzzyEq for Transform { fn fuzzy_eq(&self, other: &Self) -> bool { self.a.fuzzy_eq(&other.a) && self.b.fuzzy_eq(&other.b) && self.c.fuzzy_eq(&other.c) && self.d.fuzzy_eq(&other.d) && self.e.fuzzy_eq(&other.e) && self.f.fuzzy_eq(&other.f) } } #[cfg(test)] mod tests { use std::str::FromStr; use super::*; #[test] fn api_1() { let mut ts = Transform::default(); ts.translate(10.0, 20.0); assert_eq!(Transform::from_str("translate(10 20)").unwrap(), ts); } #[test] fn api_2() { let mut ts = Transform::default(); ts.scale(2.0, 3.0); assert_eq!(Transform::from_str("scale(2 3)").unwrap(), ts); } #[test] fn api_3() { let mut ts = Transform::default(); ts.skew_x(20.0); assert_eq!(Transform::from_str("skewX(20)").unwrap(), ts); } #[test] fn api_4() { let mut ts = Transform::default(); ts.skew_y(20.0); assert_eq!(Transform::from_str("skewY(20)").unwrap(), ts); } #[test] fn api_5() { let mut ts = Transform::default(); ts.rotate(20.0); assert_eq!(Transform::from_str("rotate(20)").unwrap(), ts); } } svgtypes-0.5.0/src/transform/parser.rs010064400017500001750000000217471342564432000163030ustar0000000000000000use std::str::FromStr; use { Error, Result, Stream, Transform, }; /// 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. /// /// # Example /// /// ``` /// 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_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 FromStr for Transform { type Err = Error; fn from_str(text: &str) -> Result { let tokens = TransformListParser::from(text); let mut transform = Transform::default(); for token in tokens { match token? { TransformListToken::Matrix { a, b, c, d, e, f } => { transform.append(&Transform::new(a, b, c, d, e, f)); } TransformListToken::Translate { tx, ty } => { transform.translate(tx, ty); } TransformListToken::Scale { sx, sy } => { transform.scale(sx, sy); } TransformListToken::Rotate { angle } => { transform.rotate(angle); } TransformListToken::SkewX { angle } => { transform.skew_x(angle); } TransformListToken::SkewY { angle } => { transform.skew_y(angle); } } } // TODO: do nothing if the transform is default Ok(transform) } } #[cfg(test)] mod tests { use std::str::FromStr; use super::*; macro_rules! test { ($name:ident, $text:expr, $result:expr) => ( #[test] fn $name() { assert_eq!(Transform::from_str($text).unwrap().to_string(), $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.86602540378 0.5 -0.5 0.86602540378 0 0)" ); test!(parse_5, "rotate(30 10 20)", "matrix(0.86602540378 0.5 -0.5 0.86602540378 11.33974596216 -2.32050807569)" ); 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 2 2 25 215)" ); test!(parse_9, "skewX(45)", "matrix(1 0 1 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.5.0/src/transform/writer.rs010064400017500001750000000105211351357177100163160ustar0000000000000000use { FuzzyEq, Transform, WriteBuffer, WriteOptions, }; impl WriteBuffer for Transform { fn write_buf_opt(&self, opt: &WriteOptions, buf: &mut Vec) { if opt.simplify_transform_matrices { write_simplified_transform(self, opt, buf); } else { write_matrix_transform(self, opt, buf); } } } fn write_matrix_transform(ts: &Transform, opt: &WriteOptions, out: &mut Vec) { out.extend_from_slice(b"matrix("); ts.a.write_buf_opt(opt, out); opt.write_separator(out); ts.b.write_buf_opt(opt, out); opt.write_separator(out); ts.c.write_buf_opt(opt, out); opt.write_separator(out); ts.d.write_buf_opt(opt, out); opt.write_separator(out); ts.e.write_buf_opt(opt, out); opt.write_separator(out); ts.f.write_buf_opt(opt, out); out.push(b')'); } fn write_simplified_transform(ts: &Transform, opt: &WriteOptions, out: &mut Vec) { if ts.is_translate() { out.extend_from_slice(b"translate("); ts.e.write_buf_opt(opt, out); if ts.f.fuzzy_ne(&0.0) { out.push(b' '); ts.f.write_buf_opt(opt, out); } out.push(b')'); } else if ts.is_scale() { out.extend_from_slice(b"scale("); ts.a.write_buf_opt(opt, out); if ts.a.fuzzy_ne(&ts.d) { out.push(b' '); ts.d.write_buf_opt(opt, out); } out.push(b')'); } else if !ts.has_translate() { let a = ts.get_rotate(); let (sx, sy) = ts.get_scale(); let (skx, sky) = ts.get_skew(); if a.fuzzy_eq(&skx) && a.fuzzy_eq(&sky) && sx.fuzzy_eq(&1.0) && sy.fuzzy_eq(&1.0) { out.extend_from_slice(b"rotate("); a.write_buf_opt(opt, out); out.push(b')'); } else { write_matrix_transform(ts, opt, out); } } else { write_matrix_transform(ts, opt, out); } } impl_display!(Transform); #[cfg(test)] mod tests { use std::str::FromStr; use super::*; use { WriteOptions, WriteBuffer, ListSeparator, }; macro_rules! test { ($name:ident, $ts:expr, $simplify:expr, $result:expr) => ( #[test] fn $name() { let mut opt = WriteOptions::default(); opt.simplify_transform_matrices = $simplify; assert_eq!($ts.with_write_opt(&opt).to_string(), $result); } ) } test!(write_1, Transform::default(), false, "matrix(1 0 0 1 0 0)" ); test!(write_2, Transform::new(2.0, 0.0, 0.0, 3.0, 20.0, 30.0), false, "matrix(2 0 0 3 20 30)" ); test!(write_3, Transform::new(1.0, 0.0, 0.0, 1.0, 20.0, 30.0), true, "translate(20 30)" ); test!(write_4, Transform::new(1.0, 0.0, 0.0, 1.0, 20.0, 0.0), true, "translate(20)" ); test!(write_5, Transform::new(2.0, 0.0, 0.0, 3.0, 0.0, 0.0), true, "scale(2 3)" ); test!(write_6, Transform::new(2.0, 0.0, 0.0, 2.0, 0.0, 0.0), true, "scale(2)" ); test!(write_7, Transform::from_str("rotate(30)").unwrap(), true, "rotate(30)" ); test!(write_8, Transform::from_str("rotate(-45)").unwrap(), true, "rotate(-45)" ); test!(write_9, Transform::from_str("rotate(33)").unwrap(), true, "rotate(33)" ); test!(write_10, Transform::from_str("scale(-1)").unwrap(), true, "scale(-1)" ); test!(write_11, Transform::from_str("scale(-1 1)").unwrap(), true, "scale(-1 1)" ); test!(write_12, Transform::from_str("scale(1 -1)").unwrap(), true, "scale(1 -1)" ); test!(write_13, Transform::new(1.0, 0.0, 0.0, 1.0, 20.0, 30.0), false, "matrix(1 0 0 1 20 30)" ); #[test] fn write_14() { let mut opt = WriteOptions::default(); opt.list_separator = ListSeparator::Comma; assert_eq!(Transform::default().with_write_opt(&opt).to_string(), "matrix(1,0,0,1,0,0)"); } #[test] fn write_15() { let mut opt = WriteOptions::default(); opt.list_separator = ListSeparator::CommaSpace; assert_eq!(Transform::default().with_write_opt(&opt).to_string(), "matrix(1, 0, 0, 1, 0, 0)"); } } svgtypes-0.5.0/src/viewbox.rs010064400017500001750000000053451342536126100144530ustar0000000000000000use std::str::FromStr; use { Error, FuzzyEq, Result, Stream, WriteBuffer, WriteOptions, }; /// Representation of the [``] type. /// /// [``]: https://www.w3.org/TR/SVG11/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 FromStr for ViewBox { type Err = Error; fn from_str(text: &str) -> Result { let mut s = Stream::from(text); let x = s.parse_list_number()?; let y = s.parse_list_number()?; let w = s.parse_list_number()?; let h = s.parse_list_number()?; if w <= 0.0 || h <= 0.0 { return Err(Error::InvalidViewbox); } Ok(ViewBox::new(x, y, w, h)) } } impl WriteBuffer for ViewBox { fn write_buf_opt(&self, opt: &WriteOptions, buf: &mut Vec) { self.x.write_buf_opt(opt, buf); buf.push(b' '); self.y.write_buf_opt(opt, buf); buf.push(b' '); self.w.write_buf_opt(opt, buf); buf.push(b' '); self.h.write_buf_opt(opt, buf); } } impl_display!(ViewBox); impl FuzzyEq for ViewBox { fn fuzzy_eq(&self, other: &Self) -> bool { self.x.fuzzy_eq(&other.x) && self.y.fuzzy_eq(&other.y) && self.w.fuzzy_eq(&other.w) && self.h.fuzzy_eq(&other.h) } } #[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!(v.fuzzy_eq(&$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", "invalid number at position 1"); test_err!(parse_err_2, "10 20 30 0", "viewBox should have a positive size"); test_err!(parse_err_3, "10 20 0 40", "viewBox should have a positive size"); test_err!(parse_err_4, "10 20 0 0", "viewBox should have a positive size"); test_err!(parse_err_5, "10 20 -30 0", "viewBox should have a positive size"); test_err!(parse_err_6, "10 20 30 -40", "viewBox should have a positive size"); test_err!(parse_err_7, "10 20 -30 -40", "viewBox should have a positive size"); } svgtypes-0.5.0/.cargo_vcs_info.json0000644000000001120000000000000127530ustar00{ "git": { "sha1": "0fb5c47129535565ced8e02a1ec783b416fce6b9" } }