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 | |
| 8 | use oxedyne_fe2o3_core::prelude::*; |
| 9 | use oxedyne_fe2o3_graphics::colour::Rgba; |
| 10 | use oxedyne_fe2o3_graphics::path::{ |
| 11 | Bounds, |
| 12 | PathBuilder, |
| 13 | Pt, |
| 14 | }; |
| 15 | use oxedyne_fe2o3_graphics::pixmap::Pixmap; |
| 16 | use oxedyne_fe2o3_graphics::stroke::Stroke; |
| 17 | use oxedyne_fe2o3_graphics::svg_doc::{ |
| 18 | self, |
| 19 | Anchor, |
| 20 | SvgOp, |
| 21 | }; |
| 22 | use oxedyne_fe2o3_graphics::transform::Transform; |
| 23 | |
| 24 | fn 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 | } |