flxzt/rnote · error

failed to parse `tool` attribute in XoppStroke with node id

Error message

failed to parse `tool` attribute in XoppStroke with node id {:?}, could not find attribute

What it means

Thrown by XoppStroke::load_from_xml when a `<stroke>` element in the imported .xopp file has no `tool` attribute at all. rnote requires the tool kind (pen, eraser, highlighter, etc.) to map the stroke to its internal stroke types, so a missing attribute aborts parsing with this anyhow error containing the XML node id.

Solutions

  1. Add the missing `tool` attribute to the offending `<stroke>` element (e.g. <stroke tool="pen" ...>) and retry the import.
  2. Regenerate the .xopp file with the tool that produced it, ensuring strokes include tool="pen"/"eraser"/"highlighter".
  3. Open the file in Xournal++ and re-save it to normalize/repair the XML, then import again.
  4. Check the file is not truncated/corrupted (compare file size or reopen in Xournal++).

Example fix

// before (file XML)
<stroke width="2.26" color="black">...</stroke>
// after (file XML)
<stroke tool="pen" width="2.26" color="black">...</stroke>
Defensive patterns

Strategy: validation

Validate before calling

fn stroke_elements_have_tool(xml: &str) -> Result<(), String> {
    for (i, chunk) in xml.split("<stroke").skip(1).enumerate() {
        let head = chunk.split('>').next().unwrap_or("");
        if !head.contains("tool=") {
            return Err(format!("stroke #{i} missing `tool` attribute"));
        }
    }
    Ok(())
}

Type guard

fn stroke_attrs_ok(attrs: &[(&str, &str)]) -> bool {
    attrs.iter().any(|(k, _)| *k == "tool")
}

Try / catch

match import_xopp(path) {
    Err(e) if e.to_string().contains("`tool` attribute in XoppStroke") => {
        eprintln!("File has a stroke without a tool attribute; repair or re-save in Xournal++: {e:#}");
    }
    Err(e) => eprintln!("import failed: {e:#}"),
    Ok(doc) => { /* proceed */ }
}

Prevention

When it happens

Trigger: Importing a .xopp file whose `<stroke>` element lacks the required `tool` attribute — e.g. a hand-edited, programmatically generated, truncated, or corrupted file, or an output of a tool that wrote strokes without the tool field.

Common situations: Programmatic xopp file generation (scripts writing raw XML) that omits `tool`; files damaged by partial download/sync; hand-editing XML and deleting the attribute by accident.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of flxzt/rnote@bbc5354502 (2026-09-08). Data as JSON: /api/errors/69cbc4d7686f026c. Report an issue: GitHub.

Appendix: source

Thrown at crates/rnote-engine/src/fileformats/xoppformat.rs:704

    ///
    /// The first element is the width of the entire stroke, and if existent,
    /// every following is a absolute width for the corresponding coordinate.
    /// If they don't exist, the stroke has the first width as constant width.
    pub width: Vec<f64>,
    /// The stroke coordinates.
    ///
    /// As points where the vector (1.0, 0.0) has length 1/72 inch.
    pub coords: Vec<Vector2>,
    /// Optional timestamp.
    pub timestamp: Option<u64>,
    /// Optional audio filename.
    pub audio_filename: Option<String>,
}

impl XmlLoadable for XoppStroke {
    fn load_from_xml(&mut self, node: Node) -> anyhow::Result<()> {
        match node.attribute("tool").ok_or_else(|| {
            anyhow::anyhow!(
                "failed to parse `tool` attribute in XoppStroke with node id {:?}, could not find attribute",
                node.id()
            )
        })? {
            "pen" => {
                self.tool = XoppTool::Pen;
            }
            "highlighter" => {
                self.tool = XoppTool::Highlighter;
            }
            "eraser" => {
                self.tool = XoppTool::Eraser;
            }
            _ => {}
        }

        self.color =
            XoppColor::from_strokecolor_attr_value(node.attribute("color").ok_or_else(|| {

View on GitHub (pinned to bbc5354502)