Skip to content

Commit 3e0ba85

Browse files
committed
[rasterize] cleaner transform logic
1 parent cb129b4 commit 3e0ba85

File tree

5 files changed

+92
-85
lines changed

5 files changed

+92
-85
lines changed

Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rasterize"
3-
version = "0.6.3"
3+
version = "0.6.4"
44
authors = ["Pavel Aslanov <asl.pavel@gmail.com>"]
55
description = "Simple and small 2D rendering library"
66
edition = "2024"
@@ -50,7 +50,6 @@ name = "color_bench"
5050

5151
[[example]]
5252
name = "rasterize"
53-
required-features = ["png"]
5453

5554
[[example]]
5655
name = "scene"

examples/rasterize.rs

Lines changed: 64 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use rasterize::*;
55
use std::{
66
env,
77
fs::File,
8-
io::{BufWriter, Read, Write},
8+
io::{BufReader, BufWriter, Read, Write},
99
str::FromStr,
1010
sync::Arc,
1111
};
@@ -23,13 +23,15 @@ enum RasterizerType {
2323
enum OutputFormat {
2424
Bmp,
2525
Rgba,
26+
#[cfg(feature = "png")]
2627
Png,
2728
}
2829

2930
impl OutputFormat {
3031
fn write(self, image: &Layer<LinColor>, out: impl Write) -> Result<(), Error> {
3132
match self {
3233
OutputFormat::Bmp => image.write_bmp(out)?,
34+
#[cfg(feature = "png")]
3335
OutputFormat::Png => image.write_png(out)?,
3436
OutputFormat::Rgba => image.write_rgba(out)?,
3537
}
@@ -43,8 +45,11 @@ impl FromStr for OutputFormat {
4345
fn from_str(s: &str) -> Result<Self, Self::Err> {
4446
match s {
4547
"bmp" => Ok(OutputFormat::Bmp),
46-
"png" => Ok(OutputFormat::Png),
4748
"rgba" => Ok(OutputFormat::Rgba),
49+
#[cfg(feature = "png")]
50+
"png" => Ok(OutputFormat::Png),
51+
#[cfg(not(feature = "png"))]
52+
"png" => Err("png feature is disabled".into()),
4853
_ => Err(format!("Invalid output format: {s}").into()),
4954
}
5055
}
@@ -56,14 +61,14 @@ struct Args {
5661
output_file: String,
5762
output_format: OutputFormat,
5863
outline: bool,
59-
width: Option<usize>,
64+
size: Size,
6065
stroke: Option<Scalar>,
6166
flatness: Scalar,
6267
rasterizer: RasterizerType,
63-
tr: Transform,
68+
tr: Option<Transform>,
6469
fg: Option<LinColor>,
6570
bg: Option<LinColor>,
66-
bbox: Option<BBox>,
71+
view_box: Option<BBox>,
6772
}
6873

6974
impl Args {
@@ -81,45 +86,46 @@ impl Args {
8186
output_file: String::new(),
8287
output_format: OutputFormat::Bmp,
8388
outline: false,
84-
width: None,
89+
size: Size {
90+
height: 0,
91+
width: 0,
92+
},
8593
stroke: None,
8694
flatness: DEFAULT_FLATNESS,
8795
rasterizer: RasterizerType::SignedDifference,
88-
tr: Transform::identity(),
96+
tr: None,
8997
fg: None,
9098
bg: None,
91-
bbox: None,
99+
view_box: None,
92100
};
93101
let mut positional = 0;
94102
let mut args = env::args();
95-
let cmd = args.next().unwrap();
103+
let _cmd = args.next().unwrap();
96104
while let Some(arg) = args.next() {
97105
match arg.as_ref() {
98106
"-h" => {
99-
positional = 0;
100-
break;
107+
result.size.height = args.next().ok_or("-h requires argument")?.parse()?;
101108
}
102109
"-w" => {
103-
let width = args.next().ok_or("-w requires argument")?;
104-
result.width = Some(width.parse()?);
110+
result.size.width = args.next().ok_or("-w requires argument")?.parse()?;
105111
}
106112
"-b" => {
107-
let bbox = args.next().ok_or("-b requires argument")?;
108-
result.bbox = Some(bbox.parse()?);
113+
let view_box = args.next().ok_or("-b requires argument")?;
114+
result.view_box.replace(view_box.parse()?);
109115
}
110116
"-t" => {
111-
result.tr = args.next().ok_or("-t requires argument")?.parse()?;
117+
let tr = args.next().ok_or("-t requires argument")?.parse()?;
118+
result.tr.replace(tr);
112119
}
113120
"-s" => {
114121
let stroke = args.next().ok_or("-s requres argument")?;
115-
result.stroke = Some(stroke.parse()?);
122+
result.stroke.replace(stroke.parse()?);
116123
}
117124
"-o" => {
118125
result.outline = true;
119126
}
120127
"-of" => {
121-
let format = args.next().ok_or("-of requries argument")?.parse()?;
122-
result.output_format = format;
128+
result.output_format = args.next().ok_or("-of requries argument")?.parse()?;
123129
}
124130
"-a" => {
125131
result.rasterizer = RasterizerType::ActiveEdge;
@@ -161,12 +167,16 @@ impl Args {
161167
);
162168
eprintln!("\nUSAGE:");
163169
eprintln!(
164-
" {} [-w <width>] [-b <bbox>] [-s <stroke>] [-f <flatness>] [-o] [-of <format>] [-a] [-fg <color>] [-bg <color>] <file.path> <output_file>",
165-
cmd
170+
" rasterize [-h <height>] [-w <width>] [-b <bbox>] [-t <transform>] [-s <stroke>]",
166171
);
172+
eprintln!(
173+
" [-f <flatness>] [-o] [-of <format>] [-a] [-fg <color>] [-bg <color>]",
174+
);
175+
eprintln!(" <input_file> <output_file>");
167176
eprintln!("\nARGS:");
177+
eprintln!(" -h <height> height in pixels of the output image");
168178
eprintln!(" -w <width> width in pixels of the output image");
169-
eprintln!(" -b <bbox> custom bounding box");
179+
eprintln!(" -b <view_box> view box");
170180
eprintln!(" -t <transform> apply transform");
171181
eprintln!(" -s <stroke_width> stroke path before rendering");
172182
eprintln!(" -o show outline with control points instead of filling");
@@ -180,26 +190,14 @@ impl Args {
180190
" -f <flatness> flatness used by rasterizer (defualt: {})",
181191
DEFAULT_FLATNESS
182192
);
183-
eprintln!(" <file.path> file containing SVG path ('-' means stdin)");
184-
eprintln!(" <out.bmp> image rendered in the BMP format ('-' means stdout)");
193+
eprintln!(" <input_file> file containing SVG path ('-' means stdin)");
194+
eprintln!(" <output_file> image rendered in the BMP format ('-' means stdout)");
185195
std::process::exit(1);
186196
}
187197
Ok(result)
188198
}
189199
}
190200

191-
/// Load path for the file
192-
fn path_load(path: String) -> Result<Path, Error> {
193-
let mut contents = String::new();
194-
if path != "-" {
195-
let mut file = File::open(path)?;
196-
file.read_to_string(&mut contents)?;
197-
} else {
198-
std::io::stdin().read_to_string(&mut contents)?;
199-
}
200-
Ok(tracing::debug_span!("[parse]").in_scope(|| contents.parse())?)
201-
}
202-
203201
/// Convert path to the outline with control points.
204202
fn outline(path: &Path, tr: Transform) -> Scene {
205203
let mut path = path.clone();
@@ -273,35 +271,41 @@ fn main() -> Result<(), Error> {
273271
let args = Args::parse()?;
274272
let rasterizer = args.get_rasterizer();
275273

276-
let path = match args.stroke {
277-
None => path_load(args.input_file)?,
278-
Some(stroke_width) => {
279-
let path = path_load(args.input_file)?;
280-
let stroke_style = StrokeStyle {
281-
width: stroke_width,
282-
line_join: LineJoin::Round,
283-
line_cap: LineCap::Round,
284-
};
285-
tracing::debug_span!("[stroke]").in_scope(|| path.stroke(stroke_style))
286-
}
274+
// load path
275+
let mut path = {
276+
let file: &mut dyn Read = match args.input_file.as_str() {
277+
"-" => &mut std::io::stdin(),
278+
input_file => &mut File::open(input_file)?,
279+
};
280+
tracing::debug_span!("[parse]").in_scope(|| Path::read_svg_path(BufReader::new(file)))?
287281
};
282+
283+
// transform
284+
if let Some(tr) = args.tr {
285+
path.transform(tr);
286+
}
287+
288+
// stroke
289+
if let Some(stroke_width) = args.stroke {
290+
let stroke_style = StrokeStyle {
291+
width: stroke_width,
292+
line_join: LineJoin::Round,
293+
line_cap: LineCap::Round,
294+
};
295+
path = tracing::debug_span!("[stroke]").in_scope(|| path.stroke(stroke_style));
296+
}
297+
298+
// allocate path
288299
let path = Arc::new(path);
289300
tracing::debug!("[path:segments_count] {}", path.segments_count());
290301

291-
// transform if needed
292-
let tr = match args.width {
293-
Some(width) if width > 2 => {
294-
let src_bbox = match args.bbox {
295-
Some(bbox) => bbox.transform(args.tr),
296-
None => path.bbox(args.tr).ok_or("path is empty")?,
297-
};
298-
let width = width as Scalar;
299-
let height = src_bbox.height() * width / src_bbox.width();
300-
let dst_bbox = BBox::new(Point::new(1.0, 1.0), Point::new(width - 1.0, height - 1.0));
301-
Transform::fit_bbox(src_bbox, dst_bbox, Align::Mid) * args.tr
302-
}
303-
_ => args.tr,
302+
// render transform
303+
let Some(view_box) = args.view_box.or_else(|| path.bbox(Transform::identity())) else {
304+
return Err("nothing to render".into());
304305
};
306+
tracing::debug!(?view_box, "[view box]");
307+
let (size, tr) = Transform::fit_size(view_box, args.size, Align::Mid);
308+
let bbox = BBox::new((0.0, 0.0), (size.width as Scalar, size.height as Scalar));
305309

306310
// scene
307311
let mut group = Vec::new();
@@ -322,14 +326,6 @@ fn main() -> Result<(), Error> {
322326
let scene = Scene::group(group);
323327

324328
// add background or checkerboard
325-
let bbox = match args.bbox {
326-
Some(bbox) => bbox.transform(tr),
327-
None => scene
328-
.bbox(Transform::identity())
329-
.ok_or("nothing to render")?,
330-
};
331-
tracing::debug!(?bbox, "[bbox]");
332-
let bbox = BBox::new((bbox.x().round(), bbox.y().round()), bbox.max());
333329
let (scene, bg) = match args.bg {
334330
None => {
335331
let scene = Scene::group(vec![

rasterize.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
#!/usr/bin/env bash
22
CARGO=$(dirname "$(realpath "${BASH_SOURCE[0]}")")/Cargo.toml
33
# export RUST_LOG=debug
4-
exec cargo run --quiet --manifest-path="$CARGO" --release --example rasterize --features png -- "${@}"
4+
exec cargo run --quiet --manifest-path="$CARGO" --release --features=png --example=rasterize -- "${@}"

src/geometry.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -487,16 +487,32 @@ impl Transform {
487487
}
488488

489489
/// Create transformation needed to fit source bounding box to provided size image
490-
pub fn fit_size(src: BBox, size: Size, align: Align) -> Transform {
491-
let dst = if size.width < 3 || size.height < 3 {
492-
BBox::new((0.0, 0.0), (size.width as Scalar, size.height as Scalar))
493-
} else {
494-
BBox::new(
495-
(1.0, 1.0),
496-
((size.width - 1) as Scalar, (size.height - 1) as Scalar),
497-
)
490+
pub fn fit_size(src: BBox, size: Size, align: Align) -> (Size, Transform) {
491+
let src = {
492+
let min = Point::new(src.min().x().floor(), src.min().y().floor());
493+
let max = Point::new(src.max().x().ceil(), src.max().y().ceil());
494+
BBox::new(min, max)
495+
};
496+
let (height, width) = match (size.height, size.width) {
497+
(0, 0) => (src.height(), src.width()),
498+
(height, 0) => {
499+
let height = height as Scalar;
500+
(height, (src.width() * height / src.height()).ceil())
501+
}
502+
(0, width) => {
503+
let width = width as Scalar;
504+
((src.height() * width / src.width()).ceil(), width)
505+
}
506+
(height, width) => (height as Scalar, width as Scalar),
498507
};
499-
Transform::fit_bbox(src, dst, align)
508+
let dst = BBox::new((0.0, 0.0), (width, height));
509+
(
510+
Size {
511+
height: height as usize,
512+
width: width as usize,
513+
},
514+
Transform::fit_bbox(src, dst, align),
515+
)
500516
}
501517
}
502518

src/path.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,11 +1059,7 @@ impl FromStr for Path {
10591059
type Err = SvgParserError;
10601060

10611061
fn from_str(text: &str) -> Result<Path, Self::Err> {
1062-
let mut builder = PathBuilder::new();
1063-
for cmd in SvgPathParser::new(Cursor::new(text)) {
1064-
cmd?.apply(&mut builder);
1065-
}
1066-
Ok(builder.build())
1062+
Ok(Path::read_svg_path(Cursor::new(text))?)
10671063
}
10681064
}
10691065

0 commit comments

Comments
 (0)