Skip to content

Commit e060e17

Browse files
committed
[geometry] use lexical for scalar formatting
1 parent 2dda798 commit e060e17

File tree

5 files changed

+134
-93
lines changed

5 files changed

+134
-93
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rasterize"
3-
version = "0.5.2"
3+
version = "0.6.0"
44
authors = ["Pavel Aslanov <asl.pavel@gmail.com>"]
55
description = "Simple and small 2D rendering library"
66
edition = "2021"
@@ -30,6 +30,7 @@ serde = { version = "1.0", features = ["rc", "derive"], optional = true }
3030
serde_json = { version = "1.0", features = ["preserve_order"], optional = true }
3131
png = { version = "^0.17", optional = true }
3232
bytemuck = { version = "1.21", features = ["derive"] }
33+
lexical-core = { version = "^1.0", default-features = false, features = [ "write-floats", "format" ] }
3334

3435
[dev-dependencies]
3536
criterion = { version = "^0.5", features = ["html_reports"] }

src/geometry.rs

Lines changed: 94 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,80 @@ pub const EPSILON_SQRT: f64 = 1.490_116_119_384_765_6e-8;
1717
/// Mathematical pi constant
1818
pub const PI: f64 = std::f64::consts::PI;
1919

20-
pub struct ScalarFmt(pub Scalar);
20+
const SCALAR_PRECISION: usize = 4;
21+
const SCALAR_FORMAT: u128 = lexical_core::NumberFormatBuilder::new()
22+
.required_integer_digits(false)
23+
.build();
24+
const SCALAR_FORMAT_OPTIONS: lexical_core::WriteFloatOptions =
25+
lexical_core::WriteFloatOptions::builder()
26+
.max_significant_digits(std::num::NonZero::new(SCALAR_PRECISION))
27+
.trim_floats(true)
28+
.build_unchecked();
29+
30+
pub struct ScalarFormatter {
31+
precision: usize,
32+
round: bool, // whether to preround (correctly removes)
33+
options: lexical_core::WriteFloatOptions,
34+
buffer: [u8; lexical_core::BUFFER_SIZE],
35+
}
36+
impl ScalarFormatter {
37+
pub fn new(precision: Option<usize>, round: bool) -> Self {
38+
let options = precision
39+
.and_then(|precision| {
40+
lexical_core::WriteFloatOptionsBuilder::new()
41+
.max_significant_digits(std::num::NonZero::new(precision))
42+
.trim_floats(true)
43+
.build()
44+
.ok()
45+
})
46+
.unwrap_or(SCALAR_FORMAT_OPTIONS);
47+
Self {
48+
precision: precision.unwrap_or(SCALAR_PRECISION),
49+
options,
50+
round,
51+
buffer: [0u8; lexical_core::BUFFER_SIZE],
52+
}
53+
}
2154

22-
impl fmt::Debug for ScalarFmt {
23-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24-
let value = self.0;
25-
let value_abs = value.abs();
26-
if value_abs.fract() < EPSILON {
27-
write!(f, "{}", value.trunc() as i64)
28-
} else if value_abs > 9999.0 || value_abs <= 0.0001 {
29-
write!(f, "{:.3e}", value)
30-
} else {
31-
let ten: Scalar = 10.0;
32-
let round = ten.powi(6 - (value_abs.trunc() + 1.0).log10().ceil() as i32);
33-
write!(f, "{}", (value * round).round() / round)
55+
pub fn new_fmt(fmt: &fmt::Formatter<'_>) -> Self {
56+
Self::new(fmt.precision(), fmt.alternate())
57+
}
58+
59+
pub fn format(&mut self, mut value: Scalar) -> &[u8] {
60+
if self.round {
61+
value = Self::round_significant(value, self.precision);
62+
}
63+
lexical_core::write_with_options::<_, SCALAR_FORMAT>(value, &mut self.buffer, &self.options)
64+
}
65+
66+
pub fn format_str(&mut self, value: Scalar) -> &str {
67+
unsafe {
68+
// SAFETY: trust lexical to produce valid utf-8 string
69+
std::str::from_utf8_unchecked(self.format(value))
3470
}
3571
}
72+
73+
pub fn round_significant(value: f64, precision: usize) -> f64 {
74+
let shift = precision as i32 - value.abs().log10().ceil() as i32;
75+
let shift_factor = 10_f64.powi(shift);
76+
(value * shift_factor).round() / shift_factor
77+
}
3678
}
3779

38-
impl fmt::Display for ScalarFmt {
39-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40-
write!(f, "{:?}", self)
80+
pub struct ScalarFormat(pub Scalar);
81+
82+
impl fmt::Debug for ScalarFormat {
83+
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
84+
let mut formatter = ScalarFormatter::new_fmt(fmt);
85+
fmt.write_str(formatter.format_str(self.0))
4186
}
4287
}
4388

44-
/// Format floats in a compact way suitable for SVG path
45-
pub fn scalar_fmt(f: &mut fmt::Formatter<'_>, value: Scalar) -> fmt::Result {
46-
use std::fmt::Debug;
47-
ScalarFmt(value).fmt(f)
89+
impl fmt::Display for ScalarFormat {
90+
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
91+
let mut formatter = ScalarFormatter::new_fmt(fmt);
92+
fmt.write_str(formatter.format_str(self.0))
93+
}
4894
}
4995

5096
/// Value representing a 2D point or vector.
@@ -62,11 +108,12 @@ impl std::hash::Hash for Point {
62108
}
63109

64110
impl fmt::Debug for Point {
65-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111+
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
66112
let Point([x, y]) = self;
67-
scalar_fmt(f, *x)?;
68-
write!(f, ",")?;
69-
scalar_fmt(f, *y)?;
113+
let mut formatter = ScalarFormatter::new_fmt(fmt);
114+
fmt.write_str(formatter.format_str(*x))?;
115+
fmt.write_str(",")?;
116+
fmt.write_str(formatter.format_str(*y))?;
70117
Ok(())
71118
}
72119
}
@@ -278,15 +325,16 @@ impl fmt::Debug for Transform {
278325
}
279326

280327
impl fmt::Display for Transform {
281-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328+
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
329+
let mut formatter = ScalarFormatter::new(fmt.precision().or(Some(6)), fmt.alternate());
282330
let Self([m00, m01, m02, m10, m11, m12]) = self;
283-
write!(f, "matrix(")?;
331+
fmt.write_str("matrix(")?;
284332
for val in [m00, m10, m01, m11, m02] {
285-
scalar_fmt(f, *val)?;
286-
write!(f, " ")?;
333+
fmt.write_str(formatter.format_str(*val))?;
334+
fmt.write_str(" ")?;
287335
}
288-
scalar_fmt(f, *m12)?;
289-
write!(f, ")")?;
336+
fmt.write_str(formatter.format_str(*m12))?;
337+
fmt.write_str(")")?;
290338
Ok(())
291339
}
292340
}
@@ -633,25 +681,26 @@ impl FromStr for BBox {
633681
}
634682

635683
impl fmt::Display for BBox {
636-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
637-
write!(
638-
f,
639-
"{} {} {} {}",
640-
ScalarFmt(self.x()),
641-
ScalarFmt(self.y()),
642-
ScalarFmt(self.width()),
643-
ScalarFmt(self.height())
644-
)
684+
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
685+
let mut formatter = ScalarFormatter::new_fmt(fmt);
686+
fmt.write_str(formatter.format_str(self.x()))?;
687+
fmt.write_str(" ")?;
688+
fmt.write_str(formatter.format_str(self.y()))?;
689+
fmt.write_str(" ")?;
690+
fmt.write_str(formatter.format_str(self.width()))?;
691+
fmt.write_str(" ")?;
692+
fmt.write_str(formatter.format_str(self.height()))
645693
}
646694
}
647695

648696
impl fmt::Debug for BBox {
649-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
650-
f.debug_struct("BBox")
651-
.field("x", &ScalarFmt(self.x()))
652-
.field("y", &ScalarFmt(self.y()))
653-
.field("w", &ScalarFmt(self.width()))
654-
.field("h", &ScalarFmt(self.height()))
697+
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
698+
let mut formatter = ScalarFormatter::new_fmt(fmt);
699+
fmt.debug_struct("BBox")
700+
.field("x", &formatter.format_str(self.x()))
701+
.field("y", &formatter.format_str(self.y()))
702+
.field("w", &formatter.format_str(self.width()))
703+
.field("h", &formatter.format_str(self.height()))
655704
.finish()
656705
}
657706
}

src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ pub use curve::{
4747
Cubic, Curve, CurveExtremities, CurveFlattenIter, CurveRoots, Line, Quad, Segment,
4848
};
4949
pub use ellipse::EllipArc;
50-
pub use geometry::{scalar_fmt, Align, BBox, Point, Scalar, Transform, EPSILON, EPSILON_SQRT, PI};
50+
pub use geometry::{
51+
Align, BBox, Point, Scalar, ScalarFormat, ScalarFormatter, Transform, EPSILON, EPSILON_SQRT, PI,
52+
};
5153
pub use grad::{GradLinear, GradRadial, GradSpread, GradStop, GradStops};
5254
pub use image::{
5355
Image, ImageIter, ImageMut, ImageMutIter, ImageMutRef, ImageOwned, ImageRef, Shape,

src/path.rs

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
22
curve::line_offset, rasterize::Rasterizer, utils::clamp, BBox, Cubic, Curve, EllipArc,
3-
ImageMut, LinColor, Line, Paint, Point, Quad, Scalar, Segment, Size, SvgParserError,
4-
SvgPathParser, Transform, EPSILON,
3+
ImageMut, LinColor, Line, Paint, Point, Quad, Scalar, ScalarFormatter, Segment, Size,
4+
SvgParserError, SvgPathParser, Transform, EPSILON,
55
};
66
#[cfg(feature = "serde")]
77
use serde::{Deserialize, Serialize};
@@ -232,12 +232,6 @@ pub struct Path {
232232
closed: Vec<bool>,
233233
}
234234

235-
impl fmt::Debug for Path {
236-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237-
write!(f, "{}", self)
238-
}
239-
}
240-
241235
impl Path {
242236
pub fn new(segments: Vec<Segment>, subpaths: Vec<usize>, closed: Vec<bool>) -> Self {
243237
Self {
@@ -553,9 +547,16 @@ impl Path {
553547
}
554548
}
555549

550+
impl fmt::Debug for Path {
551+
fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
552+
use std::fmt::Display;
553+
self.display(false, Transform::identity()).fmt(out)
554+
}
555+
}
556+
556557
impl fmt::Display for Path {
557558
fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
558-
write!(out, "{}", self.display(false, Transform::identity()))
559+
self.display(false, Transform::identity()).fmt(out)
559560
}
560561
}
561562

@@ -583,63 +584,53 @@ pub struct PathSvgDisplay<'a> {
583584
}
584585

585586
impl PathSvgDisplay<'_> {
586-
// round floating point to only contain specified amount of digits
587-
fn round_significant(x: f64, digits: usize) -> f64 {
588-
if x == 0. || digits == 0 {
589-
0.
590-
} else {
591-
let shift = digits as i32 - x.abs().log10().ceil() as i32;
592-
let shift_factor = 10_f64.powi(shift);
593-
594-
(x * shift_factor).round() / shift_factor
595-
}
596-
}
597-
598587
fn fmt_point(
599588
&self,
600589
out: &mut fmt::Formatter<'_>,
590+
formatter: &mut ScalarFormatter,
601591
point: Point,
602592
previous: Option<Point>,
603-
precision: usize,
604593
sep: bool,
605594
) -> Result<Point, fmt::Error> {
606595
let point_tr = self.tr.apply(point);
607596
let point = previous.map_or_else(|| point_tr, |point_prev| point_tr - point_prev);
608597

609-
let x = Self::round_significant(point.x(), precision);
610-
let y = Self::round_significant(point.y(), precision);
611-
let eps = 10_f64.powi(-(precision as i32));
598+
let x = point.x();
599+
let y = point.y();
600+
let eps = 10_f64.powi(-(out.precision().unwrap_or(4) as i32));
601+
612602
if sep && x >= 0.0 {
613-
write!(out, " ")?;
603+
out.write_str(" ")?;
614604
}
615605
if x.abs() < eps {
616-
write!(out, "0")?;
606+
out.write_str("0")?;
617607
} else {
618-
write!(out, "{x}")?;
608+
out.write_str(formatter.format_str(x))?;
619609
}
620610
if y >= 0.0 {
621-
write!(out, ",")?;
611+
out.write_str(",")?;
622612
}
623613
if y.abs() < eps {
624-
write!(out, "0")?;
614+
out.write_str("0")?;
625615
} else {
626-
write!(out, "{y}")?;
616+
out.write_str(formatter.format_str(y))?;
627617
}
628618
Ok(point_tr)
629619
}
630620
}
631621

632622
impl fmt::Display for PathSvgDisplay<'_> {
633623
fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
634-
let precision = out.precision().unwrap_or(4);
624+
let mut formatter = ScalarFormatter::new_fmt(out);
635625
let mut previous: Option<Point> = None;
636626
for subpath in self.path.subpaths() {
637627
out.write_str(if self.relative && previous.is_some() {
638628
"m"
639629
} else {
640630
"M"
641631
})?;
642-
let point_last = self.fmt_point(out, subpath.start(), previous, precision, false)?;
632+
let point_last =
633+
self.fmt_point(out, &mut formatter, subpath.start(), previous, false)?;
643634
if self.relative {
644635
previous = Some(point_last);
645636
}
@@ -648,20 +639,20 @@ impl fmt::Display for PathSvgDisplay<'_> {
648639
let point_last = match segment {
649640
Segment::Line(line) => {
650641
out.write_str(if self.relative { "l" } else { "L" })?;
651-
self.fmt_point(out, line.end(), previous, precision, false)?
642+
self.fmt_point(out, &mut formatter, line.end(), previous, false)?
652643
}
653644
Segment::Quad(quad) => {
654645
out.write_str(if self.relative { "q" } else { "Q" })?;
655646
let [_, p1, p2] = quad.points();
656-
self.fmt_point(out, p1, previous, precision, false)?;
657-
self.fmt_point(out, p2, previous, precision, true)?
647+
self.fmt_point(out, &mut formatter, p1, previous, false)?;
648+
self.fmt_point(out, &mut formatter, p2, previous, true)?
658649
}
659650
Segment::Cubic(cubic) => {
660651
out.write_str(if self.relative { "c" } else { "C" })?;
661652
let [_, p1, p2, p3] = cubic.points();
662-
self.fmt_point(out, p1, previous, precision, false)?;
663-
self.fmt_point(out, p2, previous, precision, true)?;
664-
self.fmt_point(out, p3, previous, precision, true)?
653+
self.fmt_point(out, &mut formatter, p1, previous, false)?;
654+
self.fmt_point(out, &mut formatter, p2, previous, true)?;
655+
self.fmt_point(out, &mut formatter, p3, previous, true)?
665656
}
666657
};
667658
if self.relative {
@@ -1115,7 +1106,7 @@ mod tests {
11151106
use crate::{assert_approx_eq, PI};
11161107

11171108
fn assert_path_eq(p0: &Path, p1: &Path) {
1118-
assert_eq!(format!("{:?}", p0), format!("{:?}", p1));
1109+
assert_eq!(format!("{:#}", p0), format!("{:#}", p1));
11191110
}
11201111

11211112
#[test]
@@ -1264,7 +1255,6 @@ mod tests {
12641255
width: 1.0,
12651256
line_cap: LineCap::Round,
12661257
line_join: LineJoin::Round,
1267-
..Default::default()
12681258
});
12691259
let path_reference: Path = r#"
12701260
M2,1.5 L8,1.5 C9.80902,1.5 10.75,3.38197 10.75,5 10.75,6.61803 9.80902,8.5 8,8.5 7.84274,8.5 7.69436,8.42581
@@ -1281,7 +1271,6 @@ mod tests {
12811271
width: 1.0,
12821272
line_cap: LineCap::Round,
12831273
line_join: LineJoin::Round,
1284-
..Default::default()
12851274
});
12861275
let path_reference: Path = r#"
12871276
M2,1.5 L8,1.5 C9.80902,1.5 10.75,3.38197 10.75,5 10.75,6.61803 9.80902,8.5 8,8.5 7.84274,8.5 7.69436,8.42581
@@ -1311,12 +1300,12 @@ mod tests {
13111300
fn test_display() -> Result<(), SvgParserError> {
13121301
let path: Path = SQUIRREL.parse()?;
13131302

1314-
let path_display = format!("{}", path);
1303+
let path_display = format!("{:#}", path);
13151304
assert_path_eq(&path, &path_display.parse()?);
13161305
let path_reference = "M12,1C9.79,1 8,2.31 8,3.92C8,5.86 8.5,6.95 8,10C8,5.5 5.23,3.66 4,3.66C4.05,3.16 3.52,3 3.52,3C3.52,3 3.3,3.11 3.22,3.34C2.95,3.03 2.66,3.07 2.66,3.07L2.53,3.65C2.53,3.65 0.7,4.29 0.68,6.87C0.88,7.2 2.21,7.47 3.15,7.3C4.04,7.35 3.82,8.09 3.62,8.29C2.78,9.13 2,8 1,8C0,8 0,9 1,9C2,9 2,10 4,10C0.91,11.2 4,14 4,14L3,14C2,14 2,15 2,15L8,15C11,15 13,14 13,11.53C13,10.68 12.57,9.74 12,9C10.89,7.54 12.23,6.32 13,7C13.77,7.68 16,8 16,5C16,2.79 14.21,1 12,1ZM2.5,6C2.22,6 2,5.78 2,5.5C2,5.22 2.22,5 2.5,5C2.78,5 3,5.22 3,5.5C3,5.78 2.78,6 2.5,6Z";
13171306
assert_eq!(path_reference, path_display.as_str());
13181307

1319-
let path_relative = format!("{}", path.display(true, Transform::identity()));
1308+
let path_relative = format!("{:#}", path.display(true, Transform::identity()));
13201309
assert_path_eq(&path, &path_relative.parse()?);
13211310
let path_reference = "M12,1c-2.21,0-4,1.31-4,2.92c0,1.94 0.5,3.03 0,6.08c0-4.5-2.77-6.34-4-6.34c0.05-0.5-0.48-0.66-0.48-0.66c0,0-0.22,0.11-0.3,0.34c-0.27-0.31-0.56-0.27-0.56-0.27l-0.13,0.58c0,0-1.83,0.64-1.85,3.22c0.2,0.33 1.53,0.6 2.47,0.43c0.89,0.05 0.67,0.79 0.47,0.99c-0.84,0.84-1.62-0.29-2.62-0.29c-1,0-1,1 0,1c1,0 1,1 3,1c-3.09,1.2 0,4 0,4l-1,0c-1,0-1,1-1,1l6,0c3,0 5-1 5-3.47c0-0.85-0.43-1.79-1-2.53c-1.11-1.46 0.23-2.68 1-2c0.77,0.68 3,1 3-2c0-2.21-1.79-4-4-4Zm-9.5,5c-0.28,0-0.5-0.22-0.5-0.5c0-0.28 0.22-0.5 0.5-0.5c0.28,0 0.5,0.22 0.5,0.5c0,0.28-0.22,0.5-0.5,0.5Z";
13221311
assert_eq!(path_reference, path_relative.as_str());

0 commit comments

Comments
 (0)