oxvg_path/convert/
mod.rs

1//! A collection of utility function to filter-map SVG paths.
2//!
3//! Use the `run` function for a high level way of running all the available conversions to produce
4//! the best path optimisation available.
5//!
6//! From a low-level perspective, the process of optimising a path is as follows:
7//! 1. Convert all commands to one type. In our case, we've arbitrarily selected relative commands
8//! 2. Filter-map commands by converting, merging, or removing commands when possible
9//! 3. Convert commands back to a mix of absolute and relative commands, depending which is more
10//!    compressed
11//! 4. Cleanup, doing a bit of post-processing to make sure any mistakes made prior are fixed
12
13mod cleanup;
14pub mod filter;
15mod mixed;
16mod relative;
17
18pub use crate::convert::cleanup::{cleanup, cleanup_unpositioned};
19pub use crate::convert::filter::filter;
20pub use crate::convert::mixed::{mixed, to_absolute};
21pub use crate::convert::relative::relative;
22use crate::geometry::MakeArcs;
23use crate::math::to_fixed;
24use crate::{command, Path};
25
26bitflags! {
27    /// External style information that may be relevant when optimising a path
28    ///
29    /// If you aren't able to get such information, try using the
30    /// `StyleInfo::conservative` constructor
31    #[derive(Debug)]
32    pub struct StyleInfo: usize {
33        /// Whether a `marker-mid` CSS style is assigned to the element
34        const has_marker_mid = 0b0_0001;
35        /// Whether a `stroke` style or attribute with an svg-paint is applied to the element
36        const maybe_has_stroke = 0b0010;
37        /// Whether a `stroke-linecap` style or attribute with `"round"` or `"square` is
38        /// applied to the element
39        const maybe_has_linecap = 0b100;
40        /// Whether a `stroke-linecap` and `stroke-linejoin` style of attribute with `"round"` is
41        /// applied to the element
42        const is_safe_to_use_z = 0b1000;
43        /// Whether a `marker-start` or `marker-end` attribute is applied to the element
44        const has_marker = 0b_0001_0000;
45    }
46}
47
48bitflags! {
49    /// Control flags for certain behaviours while optimising a path
50    #[derive(Debug)]
51    pub struct Flags: usize {
52        /// Whether to remove redundant paths that don't draw anything
53        const remove_useless_flag= 0b0000_0000_0000_0001;
54        /// Whether to round arc radius more accurately
55        const smart_arc_rounding_flag= 0b_0000_0000_0010;
56        /// Whether to convert commands which are straight into lines
57        const straight_curves_flag = 0b00_0000_0000_0100;
58        /// Whether to convert cubic beziers to quadratic beziers when they essentially are
59        const convert_to_q_flag = 0b_0000_0000_0000_1000;
60        /// Whether to convert lines to vertical/horizontal when they move in one direction
61        const line_shorthands_flag = 0b00_0000_0001_0000;
62        /// Whether to collapse repeated commands which can be expressed as one
63        const collapse_repeated_flag = 0b_0000_0010_0000;
64        /// Whether to convert smooth curves where possible
65        const curve_smooth_shorthands_flag = 0b0100_0000;
66        /// Whether to convert returning lines to z
67        const convert_to_z_flag = 0b_0000_0000_1000_0000;
68        /// Whether to strongly force absolute commands, even when suboptimal
69        const force_absolute_path_flag = 0b001_0000_0000;
70        /// Whether to weakly force absolute commands, when slightly suboptimal
71        const negative_extra_space_flag = 0b10_0000_0000;
72        /// Whether to not strongly force relative commands, even when suboptimal
73        const utilize_absolute_flag = 0b0_0100_0000_0000;
74    }
75}
76
77#[cfg_attr(feature = "napi", napi)]
78#[derive(Debug, Copy, Clone, Default)]
79/// How many decimal points to round path command arguments
80pub enum Precision {
81    /// Use default precision
82    #[default]
83    None,
84    /// Avoid rounding where possible
85    ///
86    /// Error tolerance will be 1e-2 where necessary
87    Disabled,
88    /// Precision to a specific decimal place
89    Enabled(i32),
90}
91
92#[derive(Debug, Default)]
93/// The main options for controlling how the path optimisations are completed.
94pub struct Options {
95    /// See [`Flags`]
96    pub flags: Flags,
97    /// See [`MakeArcs`]
98    pub make_arcs: MakeArcs,
99    /// See [`Precision`]
100    pub precision: Precision,
101}
102
103/// Returns an optimised version of the input path
104///
105/// Note that depending on the options and style-info given, the optimisation may be lossy.
106///
107/// # Examples
108///
109/// If you don't have any access to attributes or styles for a specific SVG element the path
110/// belongs to, try running this with the conservative approach
111///
112/// ```
113/// use oxvg_path::Path;
114/// use oxvg_path::convert::{Options, StyleInfo, run};
115/// use oxvg_path::parser::Parse as _;
116///
117/// let mut path = Path::parse_string("M 10,50 L 10,50").unwrap();
118/// let options = Options::default();
119/// let style_info = StyleInfo::conservative();
120///
121/// run(&mut path, &options, &style_info);
122/// assert_eq!(&path.to_string(), "M10 50h0");
123/// ```
124pub fn run(path: &mut Path, options: &Options, style_info: &StyleInfo) {
125    let includes_vertices = path
126        .0
127        .iter()
128        .any(|c| !matches!(c, command::Data::MoveBy(_) | command::Data::MoveTo(_)));
129    // The general optimisation process: original -> naively relative -> filter redundant ->
130    // optimal mixed
131    log::debug!("convert::run: converting path: {path}");
132    let mut positioned_path = relative(std::mem::take(path));
133    let mut state = filter::State::new(&positioned_path, options, style_info);
134    positioned_path = filter(positioned_path, options, &mut state, style_info);
135    if options.flags.utilize_absolute() {
136        positioned_path = mixed(positioned_path, options);
137    }
138    positioned_path = cleanup(positioned_path);
139    for command in &mut positioned_path.0 {
140        if command.command.is_by() {
141            options.round_data(command.command.args_mut(), options.error());
142        } else {
143            options.round_absolute_command_data(
144                command.command.args_mut(),
145                options.error(),
146                &command.start.0,
147            );
148        }
149    }
150
151    *path = positioned_path.take();
152    let has_marker = style_info.contains(StyleInfo::has_marker);
153    let is_markers_only_path = has_marker
154        && includes_vertices
155        && path
156            .0
157            .iter()
158            .all(|c| matches!(c, command::Data::MoveBy(_) | command::Data::MoveTo(_)));
159    if is_markers_only_path {
160        path.0.push(command::Data::ClosePath);
161    }
162    log::debug!("convert::run: done: {path}");
163}
164
165impl StyleInfo {
166    /// Returns a safe set of style-info
167    ///
168    /// Use this if no contextual details are available
169    pub fn conservative() -> Self {
170        let mut result = Self::all();
171        result.set(Self::is_safe_to_use_z, false);
172        result
173    }
174}
175
176impl Default for StyleInfo {
177    fn default() -> Self {
178        Self::empty()
179    }
180}
181
182impl Flags {
183    fn remove_useless(&self) -> bool {
184        self.contains(Self::remove_useless_flag)
185    }
186
187    fn smart_arc_rounding(&self) -> bool {
188        self.contains(Self::smart_arc_rounding_flag)
189    }
190
191    fn straight_curves(&self) -> bool {
192        self.contains(Self::straight_curves_flag)
193    }
194
195    fn convert_to_q(&self) -> bool {
196        self.contains(Self::convert_to_q_flag)
197    }
198
199    fn line_shorthands(&self) -> bool {
200        self.contains(Self::line_shorthands_flag)
201    }
202
203    fn collapse_repeated(&self) -> bool {
204        self.contains(Self::collapse_repeated_flag)
205    }
206
207    fn curve_smooth_shorthands(&self) -> bool {
208        self.contains(Self::curve_smooth_shorthands_flag)
209    }
210
211    fn convert_to_z(&self) -> bool {
212        self.contains(Self::convert_to_z_flag)
213    }
214
215    fn force_absolute_path(&self) -> bool {
216        self.contains(Self::force_absolute_path_flag)
217    }
218
219    fn negative_extra_space(&self) -> bool {
220        self.contains(Self::negative_extra_space_flag)
221    }
222
223    fn utilize_absolute(&self) -> bool {
224        self.contains(Self::utilize_absolute_flag)
225    }
226}
227
228impl Default for Flags {
229    fn default() -> Self {
230        let mut flags = Self::all();
231        flags.set(Self::force_absolute_path_flag, false);
232        flags
233    }
234}
235
236impl Options {
237    /// Converts the precision into a tolerance that can be compared against
238    pub fn error(&self) -> f64 {
239        match self.precision.inner() {
240            Some(precision) => {
241                let trunc_by = f64::powi(10.0, precision);
242                f64::trunc(f64::powi(0.1, precision) * trunc_by) / trunc_by
243            }
244            None => 1e-2,
245        }
246    }
247
248    /// Rounds a number to a decimal place based on the given error
249    pub fn round(&self, data: f64, error: f64) -> f64 {
250        let precision = self.precision.unwrap_or(0);
251        if precision > 0 && precision < 20 {
252            let fixed = to_fixed(data, precision);
253            if (fixed - data).abs() == 0.0 {
254                return data;
255            }
256            let rounded = to_fixed(data, precision - 1);
257            if to_fixed((rounded - data).abs(), precision + 1) >= error {
258                fixed
259            } else {
260                rounded
261            }
262        } else {
263            data.round()
264        }
265    }
266
267    /// Rounds a set of numbers to a decimal place
268    pub fn round_data(&self, data: &mut [f64], error: f64) {
269        data.iter_mut().enumerate().for_each(|(i, d)| {
270            let result = self.round(*d, error);
271            if i > 4 && result == 0.0 {
272                // Don't accidentally null arcs
273                return;
274            }
275            *d = result;
276        });
277    }
278
279    /// Rounds a set of numbers to a decimal place
280    pub fn round_absolute_command_data(&self, data: &mut [f64], error: f64, start: &[f64; 2]) {
281        data.iter_mut().enumerate().for_each(|(i, d)| {
282            let result = self.round(*d, error);
283            if (i == 5 && result == start[0]) || (i == 6 && result == start[1]) {
284                // Don't accidentally null arcs
285                return;
286            }
287            *d = result;
288        });
289    }
290
291    /// Rounds a path's data to a decimal place
292    pub fn round_path(&self, path: &mut Path, error: f64) {
293        path.0
294            .iter_mut()
295            .for_each(|c| self.round_data(c.args_mut(), error));
296    }
297
298    /// Produces the safest options for path optimisation
299    pub fn conservative() -> Self {
300        Self {
301            flags: Flags::default(),
302            make_arcs: MakeArcs::default(),
303            precision: Precision::conservative(),
304        }
305    }
306}
307
308impl Precision {
309    fn is_disabled(self) -> bool {
310        matches!(self, Self::Disabled)
311    }
312
313    fn unwrap_or(self, default: i32) -> i32 {
314        match self.inner() {
315            Some(x) => x,
316            None => default,
317        }
318    }
319
320    fn inner(self) -> Option<i32> {
321        match self {
322            Self::Enabled(x) => Some(x),
323            Self::None => Some(3),
324            Self::Disabled => None,
325        }
326    }
327
328    /// Returns the maximum possible precision
329    pub fn conservative() -> Self {
330        Self::Enabled(19)
331    }
332}
333
334#[test]
335fn test_convert() {
336    use crate::Path;
337    use oxvg_parse::Parse as _;
338
339    let mut path = Path::parse_string("m 1208.23,1821.01 c 74.07,14.24 196.57,17.09 293.43,-14.24 122.5,-42.74 22.79,-199.42 48.43,-207.97 25.64,-8.55 59.83,108.25 287.73,96.86 230.75,-11.39 256.39,-113.95 287.73,-96.86 31.34,17.09 -31.34,284.88 313.37,222.21 0,0 -361.8,96.86 -344.71,-165.23 0,0 -207.96,159.53 -498.54,17.09 2.85,0 76.92,245 -387.44,148.14").unwrap();
340    run(&mut path, &Options::default(), &StyleInfo::default());
341    assert_eq!(
342        String::from(path),
343        String::from("M1208.23 1821.01c74.07 14.24 196.57 17.09 293.43-14.24 122.5-42.74 22.79-199.42 48.43-207.97s59.83 108.25 287.73 96.86c230.75-11.39 256.39-113.95 287.73-96.86s-31.34 284.88 313.37 222.21c0 0-361.8 96.86-344.71-165.23 0 0-207.96 159.53-498.54 17.09 2.85 0 76.92 245-387.44 148.14")
344    );
345}