Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_graphics/examples/svg_raster.rs

4.5 KiB, 11 runs

created by r1870400018:37598, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Rasterises an SVG file through [`svg_doc`] to a PNG, for eyeballing render fidelity.
2//!
3//! Usage: `svg_raster <in.svg> <out.png> [width-px]`. The width defaults to 800. The picture is scaled
4//! to that width, drawn on a white ground with the same fill/stroke mapping the typesetter uses (a
5//! dashed or capped stroke baked to a filled outline), and written out. This is a throwaway verification
6//! aid, not part of the crate's public surface.
7
8use oxedyne_fe2o3_core::prelude::*;
9use oxedyne_fe2o3_graphics::colour::Rgba;
10use oxedyne_fe2o3_graphics::path::{
11 Bounds,
12 PathBuilder,
13 Pt,
14};
15use oxedyne_fe2o3_graphics::pixmap::Pixmap;
16use oxedyne_fe2o3_graphics::stroke::Stroke;
17use oxedyne_fe2o3_graphics::svg_doc::{
18 self,
19 Anchor,
20 SvgOp,
21};
22use oxedyne_fe2o3_graphics::transform::Transform;
23
24fn main() -> Outcome<()> {
25 let args: Vec<String> = std::env::args().collect();
26 if args.len() < 3 {
27 println!("usage: svg_raster <in.svg> <out.png> [width-px]");
28 return Ok(());
29 }
30 let src = res!(std::fs::read_to_string(&args[1]));
31 let out_w = args.get(3).and_then(|s| s.parse::<usize>().ok()).unwrap_or(800);
32
33 let pic = res!(svg_doc::read_document(&src));
34 let s = (out_w as f32) / pic.width.max(1.0);
35 let out_h = ((pic.height * s).ceil() as usize).max(1);
36 let t = Transform::scale(s, s);
37
38 let mut pm = res!(Pixmap::new(out_w, out_h));
39 res!(pm.fill_bounds(Bounds::new(0.0, 0.0, out_w as f32, out_h as f32), Rgba::WHITE, None));
40
41 // Text runs are handed on unshaped, since svg_doc has no font, and this raster has none either.
42 // A run is marked with a baseline rule rather than dropped, so the eyeball check sees where text
43 // sits without mistaking a shaped-glyph gap for lost content; the tally is reported at the end.
44 let mut texts = 0usize;
45
46 for op in pic.ops {
47 match op {
48 SvgOp::Fill { path, colour } => {
49 res!(pm.fill_path(&path, &t, colour, None));
50 },
51 SvgOp::Stroke { path, colour, stroke } => {
52 if stroke.dash.is_some() {
53 let outline = res!(path.stroke(&stroke));
54 res!(pm.fill_path(&outline, &t, colour, None));
55 } else {
56 let pen = res!(Stroke::new(stroke.width));
57 let pen = pen
58 .with_cap(stroke.cap)
59 .with_join(stroke.join);
60 res!(pm.stroke_path(&path, &t, colour, None, &pen));
61 }
62 },
63 SvgOp::Text { text, local, x, y, size, anchor, colour, .. } => {
64 texts += 1;
65 // A rough advance: half an em a character is a fair mean for an unshaped run, enough
66 // to place a baseline marker of about the right length. The run's frame is `local`;
67 // the endpoints are mapped into the picture frame and stroked through `t` like any
68 // other path, so the marker lands where the glyphs would.
69 let adv = (text.chars().count() as f32) * size * 0.5;
70 let (x0, x1) = match anchor {
71 Anchor::Start => (x, x + adv),
72 Anchor::Middle => (x - 0.5 * adv, x + 0.5 * adv),
73 Anchor::End => (x - adv, x),
74 };
75 let a = local.apply(Pt::new(x0, y));
76 let b = local.apply(Pt::new(x1, y));
77 let mut pb = PathBuilder::new();
78 pb.move_to(a);
79 pb.line_to(b);
80 let rule = res!(pb.finish());
81 let pen = res!(Stroke::new((size * 0.06).max(0.5)));
82 // Half coverage, so the marker reads as a placeholder rather than as rendered text.
83 res!(pm.stroke_path(&rule, &t, colour.with_coverage(0.5), None, &pen));
84 },
85 SvgOp::Image { rgba, iw, ih, x, y, w, h } => {
86 if iw == 0 || ih == 0 || w <= 0.0 || h <= 0.0 {
87 continue;
88 }
89 let img = res!(Pixmap::from_data(iw, ih, rgba));
90 // The placement rectangle, carried into device pixels by the same scale the paths use.
91 let dw = (w * s).max(1.0);
92 let dh = (h * s).max(1.0);
93 let ox = x * s;
94 let oy = y * s;
95 let px0 = ox.floor().max(0.0) as usize;
96 let py0 = oy.floor().max(0.0) as usize;
97 let px1 = ((ox + dw).ceil() as usize).min(out_w);
98 let py1 = ((oy + dh).ceil() as usize).min(out_h);
99 // Nearest-neighbour: enough to eyeball fidelity, and it pulls in no resampler.
100 for py in py0..py1 {
101 for px in px0..px1 {
102 let u = (((px as f32) + 0.5 - ox) / dw) * (iw as f32);
103 let v = (((py as f32) + 0.5 - oy) / dh) * (ih as f32);
104 if u < 0.0 || v < 0.0 {
105 continue;
106 }
107 let sx = (u as usize).min(iw - 1);
108 let sy = (v as usize).min(ih - 1);
109 if let Some(c) = img.pixel(sx, sy) {
110 pm.blend_pixel(px, py, c);
111 }
112 }
113 }
114 },
115 }
116 }
117
118 res!(pm.save_png(&args[2]));
119 println!("wrote {} ({}x{}) from {} ops-scaled", args[2], out_w, out_h, args[1]);
120 if texts > 0 {
121 println!("{} text run(s) marked at the baseline but not shaped (no font in this raster)", texts);
122 }
123 Ok(())
124}