a-b-street/abstreet · error

render_line( )

Error message

render_line({}): {}

What it means

render_line builds an SVG string wrapping the line's text, then parses it with usvg; if usvg::Tree::from_str fails it panics with the contents and the parser error. This usually means the generated <svg>/<text> markup is invalid, typically because text content broke the XML.

Solutions

  1. Sanitize/escape the text (XML entities) before passing to render_line; htmlescape::encode_minimal is used elsewhere in this codebase for curvey.
  2. Strip control characters and invalid XML chars from the string.
  3. Log the failing `contents` from the panic message and test the string in isolation.

Example fix

// before
canvas.draw_text(serde_json::to_string(&value).unwrap());
// after
canvas.draw_text(&htmlescape::encode_minimal(&serde_json::to_string(&value).unwrap()));
Defensive patterns

Strategy: validation

Validate before calling

fn is_safe_label(s: &str) -> bool {
    s.chars().all(|c| !c.is_control()) && xml::Writer::new(Vec::new()).write(&s).is_ok()
}

Try / catch

// Sanitize at the call site:
canvas.draw_text(&htmlescape::encode_minimal(&raw));

Prevention

When it happens

Trigger: Calling render_line (directly or via inner_render/inner_wrap_to_pixels) with text contents containing characters that break the generated SVG markup — unescaped XML, invalid control characters, or malformed fmt sequences; also usvg internal parse failures.

Common situations: Rendering user-supplied strings or logs containing raw '<'/'>'/'&' or invalid control bytes; extremely large strings; text with embedded NUL bytes from binary data.

Related errors


AI-assisted analysis of a-b-street/abstreet@0964f29315 (2026-09-13). Data as JSON: /api/errors/0df9c7ad7f7b6258. Report an issue: GitHub.

Appendix: source

Thrown at widgetry/src/text.rs:512

            if span.underlined {
                "text-decoration=\"underline\""
            } else {
                ""
            },
            if let Some(c) = span.outline_color {
                format!("stroke=\"{}\"", c.as_hex())
            } else {
                String::new()
            },
            htmlescape::encode_minimal(&span.text)
        )
        .unwrap();
    }
    write!(&mut svg, "{}</text></svg>", contents).unwrap();

    let mut svg_tree = match usvg::Tree::from_str(&svg, &usvg::Options::default()) {
        Ok(t) => t,
        Err(err) => panic!("render_line({}): {}", contents, err),
    };
    svg_tree.convert_text(&assets.fontdb.borrow());
    let mut batch = GeomBatch::new();
    match crate::svg::add_svg_inner(&mut batch, svg_tree, tolerance) {
        Ok(_) => batch,
        Err(err) => {
            error!("render_line({}): {}", contents, err);
            // We'll just wind up with a blank line
            batch
        }
    }
}

pub trait TextExt {
    fn text_widget(self, ctx: &EventCtx) -> Widget;
    fn batch_text(self, ctx: &EventCtx) -> Widget;
}

View on GitHub (pinned to 0964f29315)