///|
fn parse_xml_int_tag(
  xml : StringView,
  tag_name : StringView,
  msg : String,
) -> Int raise XlsxError {
  let body = match extract_tag_body_from(xml, tag_name) {
    Some(value) => value
    None => raise InvalidXml(msg~)
  }
  @string.parse_int(body, base=10) catch {
    _ => raise InvalidXml(msg~)
  }
}

///|
fn validate_drawing_axis_marker_for_read(
  axis : StringView,
  idx0 : Int,
  off_emu : Int,
  allow_end_boundary : Bool,
) -> Unit raise XlsxError {
  let limit = if axis == "col" {
    cell_ref_max_cols
  } else if axis == "row" {
    cell_ref_max_rows
  } else {
    raise InvalidXml(msg="drawing axis invalid")
  }
  let at_end_boundary = idx0 == limit && off_emu <= 0
  if idx0 < 0 ||
    idx0 > limit ||
    (idx0 == limit && (!allow_end_boundary || !at_end_boundary)) {
    raise InvalidXml(msg="drawing " + axis.to_owned() + " invalid")
  }
}

///|
fn drawing_reference_for_read(
  row_idx0 : Int,
  col_idx0 : Int,
  row_off_emu : Int,
  col_off_emu : Int,
) -> String raise XlsxError {
  validate_drawing_axis_marker_for_read("row", row_idx0, row_off_emu, false)
  validate_drawing_axis_marker_for_read("col", col_idx0, col_off_emu, false)
  cell_ref_from(row_idx0 + 1, col_idx0 + 1)
}

///|
fn validate_drawing_extent_for_read(
  value : Int,
  field : StringView,
) -> Int raise XlsxError {
  if value < 0 {
    raise InvalidXml(msg=field.to_owned() + " invalid")
  }
  value
}

///|
fn drawing_axis_position_emu_for_read(
  metrics : DrawingAxisMetrics,
  idx0 : Int,
  off_emu : Int,
) -> Int64 raise XlsxError {
  let pixels = metrics.pixel_span(0, idx0)
  let emu_per_pixel = Int64::from_int(picture_emu_per_pixel)
  let max_base = 0x7fff_ffff_ffff_ffffL - 0xffff_ffffL
  if pixels > max_base / emu_per_pixel {
    raise InvalidXml(msg="drawing span out of range")
  }
  pixels * emu_per_pixel + Int64::from_int(off_emu)
}

///|
fn drawing_axis_span_emu(
  metrics : DrawingAxisMetrics,
  from_idx0 : Int,
  from_off_emu : Int,
  to_idx0 : Int,
  to_off_emu : Int,
) -> Int raise XlsxError {
  validate_drawing_axis_marker_for_read(
    metrics.axis,
    from_idx0,
    from_off_emu,
    false,
  )
  validate_drawing_axis_marker_for_read(metrics.axis, to_idx0, to_off_emu, true)
  let from_position = drawing_axis_position_emu_for_read(
    metrics, from_idx0, from_off_emu,
  )
  let to_position = drawing_axis_position_emu_for_read(
    metrics, to_idx0, to_off_emu,
  )
  let span = if to_position >= from_position {
    to_position - from_position
  } else {
    from_position - to_position
  }
  if span > 0x7fff_ffffL {
    raise InvalidXml(msg="drawing span out of range")
  }
  span.to_int()
}

///|
fn drawing_anchor_has_unsupported_reader_object(xml : StringView) -> Bool {
  // Namespace-aware structure validation has already proved that these
  // canonical names can only designate the direct unsupported object (or a
  // descendant of its group subtree), so lexical filtering is safe here.
  xml.contains(" Int raise XlsxError {
  match attr_value(tag, name) {
    Some(value) =>
      @string.parse_int(value, base=10) catch {
        _ => raise InvalidXml(msg=invalid_message)
      }
    None => raise InvalidXml(msg=missing_message)
  }
}

///|
fn DrawingAnchor::origin_for_read(
  self : DrawingAnchor,
) -> DrawingAnchorOrigin raise XlsxError {
  if self.form == AbsoluteAnchor {
    let pos_tag = match tag_attributes_in(self.xml, "xdr:pos") {
      Some(value) => value
      None => raise InvalidXml(msg="drawing absolute position missing")
    }
    let x = parse_drawing_int_attr(
      pos_tag, "x", "drawing absolute x invalid", "drawing absolute x missing",
    )
    let y = parse_drawing_int_attr(
      pos_tag, "y", "drawing absolute y invalid", "drawing absolute y missing",
    )
    {
      reference: drawing_reference_for_read(0, 0, y, x),
      col_idx0: 0,
      row_idx0: 0,
      col_off_emu: x,
      row_off_emu: y,
    }
  } else {
    let from_body = match extract_tag_body_from(self.xml, "xdr:from") {
      Some(value) => value
      None => raise InvalidXml(msg="drawing anchor from missing")
    }
    let col_idx0 = parse_xml_int_tag(
      from_body, "xdr:col", "drawing col invalid",
    )
    let row_idx0 = parse_xml_int_tag(
      from_body, "xdr:row", "drawing row invalid",
    )
    let col_off_emu = parse_xml_int_tag(
      from_body, "xdr:colOff", "drawing colOff invalid",
    )
    let row_off_emu = parse_xml_int_tag(
      from_body, "xdr:rowOff", "drawing rowOff invalid",
    )
    {
      reference: drawing_reference_for_read(
        row_idx0, col_idx0, row_off_emu, col_off_emu,
      ),
      col_idx0,
      row_idx0,
      col_off_emu,
      row_off_emu,
    }
  }
}

///|
fn DrawingAnchor::uses_direct_extent(self : DrawingAnchor) -> Bool {
  match self.form {
    OneCellAnchor | AbsoluteAnchor => true
    TwoCellAnchor => false
  }
}

///|
fn DrawingAnchor::extent_for_read(
  self : DrawingAnchor,
  metrics : DrawingMetrics,
  origin : DrawingAnchorOrigin,
  object_name? : String = "",
) -> (Int, Int) raise XlsxError {
  let prefix = if object_name == "" {
    "drawing"
  } else {
    "drawing " + object_name
  }
  if self.uses_direct_extent() {
    let ext_tag = match tag_attributes_in(self.xml, "xdr:ext") {
      Some(value) => value
      None => raise InvalidXml(msg=prefix + " ext missing")
    }
    let cx = parse_drawing_int_attr(
      ext_tag,
      "cx",
      prefix + " cx invalid",
      prefix + " cx missing",
    )
    let cy = parse_drawing_int_attr(
      ext_tag,
      "cy",
      prefix + " cy invalid",
      prefix + " cy missing",
    )
    ignore(validate_drawing_extent_for_read(cx, prefix + " cx"))
    ignore(validate_drawing_extent_for_read(cy, prefix + " cy"))
    (cx, cy)
  } else {
    let to_body = match extract_tag_body_from(self.xml, "xdr:to") {
      Some(value) => value
      None => {
        let target = if object_name == "" { "drawing anchor" } else { prefix }
        raise InvalidXml(msg=target + " to missing")
      }
    }
    let to_col_idx0 = parse_xml_int_tag(
      to_body,
      "xdr:col",
      prefix + " to col invalid",
    )
    let to_row_idx0 = parse_xml_int_tag(
      to_body,
      "xdr:row",
      prefix + " to row invalid",
    )
    let to_col_off = parse_xml_int_tag(
      to_body,
      "xdr:colOff",
      prefix + " to colOff invalid",
    )
    let to_row_off = parse_xml_int_tag(
      to_body,
      "xdr:rowOff",
      prefix + " to rowOff invalid",
    )
    (
      drawing_axis_span_emu(
        metrics.cols,
        origin.col_idx0,
        origin.col_off_emu,
        to_col_idx0,
        to_col_off,
      ),
      drawing_axis_span_emu(
        metrics.rows,
        origin.row_idx0,
        origin.row_off_emu,
        to_row_idx0,
        to_row_off,
      ),
    )
  }
}

///|
fn parse_drawing_anchors(
  drawing_xml : StringView,
  budget? : ReadBudget,
) -> Array[DrawingAnchor] raise XlsxError {
  match budget {
    Some(value) => {
      value.checkpoint()
      value.charge_work(drawing_xml.length())
    }
    None => ()
  }
  let xml_str = drawing_xml.to_owned()
  let anchors : Array[DrawingAnchor] = []
  let anchor_names : ReadOnlyArray[String] = [
    "xdr:oneCellAnchor", "xdr:twoCellAnchor", "xdr:absoluteAnchor",
  ]
  let mut cursor = 0
  let mut scanned = 0
  for ;; {
    let mut next_start : Int? = None
    let mut next_name = ""
    for candidate in anchor_names {
      match find_xml_open_tag_start_from(xml_str, candidate, cursor) {
        Some(start) =>
          match next_start {
            Some(current) if current <= start => ()
            _ => {
              next_start = Some(start)
              next_name = candidate
            }
          }
        None => ()
      }
    }
    let start = match next_start {
      Some(value) => value
      None => break
    }
    if (scanned & 255) == 0 {
      match budget {
        Some(value) => value.checkpoint()
        None => ()
      }
    }
    let close_end = match find_xml_close_tag_from(xml_str, next_name, start) {
      Some((_, value)) => value
      None => {
        let local_name = next_name[4:].to_owned()
        raise InvalidXml(msg=local_name + " close missing")
      }
    }
    let full = xml_str[start:close_end + 1].to_owned()
    let form = if next_name == "xdr:absoluteAnchor" {
      AbsoluteAnchor
    } else if next_name == "xdr:oneCellAnchor" {
      OneCellAnchor
    } else {
      TwoCellAnchor
    }
    let positioning = match form {
      AbsoluteAnchor => Absolute
      OneCellAnchor => OneCell
      TwoCellAnchor =>
        match tag_attributes_in(full, "xdr:twoCellAnchor") {
          Some(tag) =>
            match attr_value(tag, "editAs") {
              Some("absolute") => Absolute
              Some("oneCell") => OneCell
              Some("twoCell") | None => TwoCell
              Some(_) => raise InvalidXml(msg="drawing editAs value is invalid")
            }
          None => TwoCell
        }
    }
    anchors.push({
      xml: full,
      positioning,
      form,
      order: scanned,
      unsupported: drawing_anchor_has_unsupported_reader_object(full),
    })
    scanned += 1
    cursor = close_end + 1
  }
  match budget {
    Some(value) => value.checkpoint()
    None => ()
  }
  anchors
}

///|
fn charge_drawing_work(
  budget : ReadBudget?,
  units : Int,
) -> Unit raise XlsxError {
  match budget {
    Some(value) => {
      value.checkpoint()
      value.charge_work(units)
    }
    None => ()
  }
}

///|
fn charge_drawing_anchor_pass(
  budget : ReadBudget?,
  anchor_xml : StringView,
) -> Unit raise XlsxError {
  charge_drawing_work(budget, anchor_xml.length())
}

///|
/// Returns the sole chart relationship selected by a chart-sheet drawing.
/// Drawing structure validation has already proved the canonical `c:chart`
/// path and relationship-attribute dialect; this pass enforces chart-sheet
/// cardinality and keeps relationship lookup tied to that exact payload.
fn single_drawing_chart_rel_id(
  anchors : ArrayView[DrawingAnchor],
  budget? : ReadBudget,
) -> String raise XlsxError {
  let mut selected : String? = None
  for anchor in anchors {
    if anchor.unsupported {
      continue
    }
    let anchor_xml = anchor.xml
    charge_drawing_anchor_pass(budget, anchor_xml)
    match tag_attributes_in(anchor_xml, "c:chart") {
      Some(tag) => {
        if selected is Some(_) {
          raise InvalidXml(msg="chartsheet drawing has multiple charts")
        }
        selected = match attr_value(tag, "r:id") {
          Some(value) if value.length() > 0 => Some(value.to_string())
          _ => raise InvalidXml(msg="chartsheet chart rel id missing")
        }
      }
      None => ()
    }
  }
  match selected {
    Some(value) => value
    None => raise InvalidXml(msg="chartsheet chart payload missing")
  }
}

///|
/// Returns the shape-level solid fill, never a line-level fill nested later in
/// `a:ln`. Drawing structure validation has already rejected every other
/// `a:solidFill` path before the canonical reader reaches this helper.
fn drawing_shape_solid_fill_body(sppr_body : StringView) -> String? {
  let fill_start = match find_xml_open_tag_start(sppr_body, "a:solidFill") {
    Some(value) => value
    None => return None
  }
  match find_xml_open_tag_start(sppr_body, "a:ln") {
    Some(line_start) if line_start < fill_start => return None
    _ => ()
  }
  extract_tag_body_from(sppr_body, "a:solidFill")
}

///|
fn drawing_text_bool_attribute(
  tag : StringView,
  name : StringView,
) -> Bool raise XlsxError {
  match attr_value(tag, name) {
    Some(value) => parse_bool_attr(value)
    None => false
  }
}

///|
fn parse_drawing_text_font(
  container_body : StringView,
) -> RichTextFont? raise XlsxError {
  let rpr_tag = match tag_attributes_in(container_body, "a:rPr") {
    Some(value) => value
    None => return None
  }
  let bold = drawing_text_bool_attribute(rpr_tag, "b")
  let italic = drawing_text_bool_attribute(rpr_tag, "i")
  let strike = match attr_value(rpr_tag, "strike") {
    Some("sngStrike") | Some("dblStrike") => true
    Some("noStrike") | None => false
    Some(_) => raise InvalidXml(msg="drawing text strike is invalid")
  }
  let underline = match attr_value(rpr_tag, "u") {
    Some("none") | None => None
    Some(value) => Some(unescape_xml_text(value))
  }
  let size = match attr_value(rpr_tag, "sz") {
    Some(value) => {
      let parsed = @string.parse_int(value, base=10) catch {
        _ => raise InvalidXml(msg="drawing text size is invalid")
      }
      if parsed < 0 {
        raise InvalidXml(msg="drawing text size is invalid")
      }
      Some(parsed.to_double() / 100.0)
    }
    None => None
  }
  let rpr_body = extract_tag_body_from(container_body, "a:rPr").unwrap_or("")
  let source = "" +
    rpr_body +
    ""
  let scanner = @ooxml.XmlStartTagScanner::new(source)
  let mut family : String? = None
  let mut color : String? = None
  let mut direct_solid_fill = false
  while workbook_scanner_next(scanner) {
    let depth = scanner.depth()
    if depth <= 2 {
      direct_solid_fill = false
    }
    if depth == 2 &&
      scanner.namespace_uri() == transitional_drawing_main_namespace &&
      scanner.parent_local_name() == Some("root") {
      if scanner.local_name() == "latin" && family is None {
        family = workbook_scanner_attribute(scanner, "", "typeface")
      } else if scanner.local_name() == "solidFill" {
        direct_solid_fill = true
      }
    } else if direct_solid_fill &&
      depth == 3 &&
      scanner.namespace_uri() == transitional_drawing_main_namespace &&
      scanner.local_name() == "srgbClr" &&
      scanner.parent_local_name() == Some("solidFill") &&
      scanner.parent_namespace_uri() ==
      Some(transitional_drawing_main_namespace) &&
      color is None {
      color = match workbook_scanner_attribute(scanner, "", "val") {
        Some(value) if value != "" => Some(value)
        _ => raise InvalidXml(msg="drawing text color is invalid")
      }
    }
  }
  if bold ||
    italic ||
    strike ||
    underline is Some(_) ||
    size is Some(_) ||
    family is Some(_) ||
    color is Some(_) {
    Some({
      bold,
      italic,
      strike,
      outline: false,
      shadow: false,
      condense: false,
      extended: false,
      underline,
      size,
      color,
      color_theme: None,
      color_indexed: None,
      color_tint: None,
      charset: None,
      family_number: None,
      scheme: None,
      vert_align: None,
      family,
    })
  } else {
    None
  }
}

///|
fn parse_drawing_shape_text_runs(
  tx_body : StringView,
) -> Array[RichTextRun] raise XlsxError {
  let runs : Array[RichTextRun] = []
  let source = tx_body.to_owned()
  let container_names : ReadOnlyArray[String] = ["a:r", "a:fld", "a:br"]
  let mut cursor = 0
  for ;; {
    let mut next_start : Int? = None
    let mut next_name = ""
    for candidate in container_names {
      match find_xml_open_tag_start_from(source, candidate, cursor) {
        Some(start) =>
          match next_start {
            Some(current) if current <= start => ()
            _ => {
              next_start = Some(start)
              next_name = candidate
            }
          }
        None => ()
      }
    }
    let start = match next_start {
      Some(value) => value
      None => break
    }
    let open_end = match xml_open_tag_end_from(source, start + 1) {
      Some(value) => value
      None => raise InvalidXml(msg="drawing text tag is not closed")
    }
    let self_closing = source[start + 1:open_end].trim().has_suffix("/")
    let (container_body, close_end) = if self_closing {
      ("", open_end)
    } else {
      let (close_start, end) = match
        find_xml_close_tag_from(source, next_name, open_end + 1) {
        Some(value) => value
        None => raise InvalidXml(msg="drawing text close is missing")
      }
      (source[open_end + 1:close_start].to_owned(), end)
    }
    if next_name == "a:br" {
      runs.push({ text: "\n", font: parse_drawing_text_font(container_body) })
    } else {
      let text = match extract_tag_body_from(container_body, "a:t") {
        Some(value) => unescape_xml_text(value)
        None =>
          if tag_attributes_in(container_body, "a:t") is Some(_) {
            ""
          } else {
            raise InvalidXml(msg="drawing text value is missing")
          }
      }
      runs.push({ text, font: parse_drawing_text_font(container_body) })
    }
    cursor = close_end + 1
  }
  runs
}

///|
fn parse_drawing_images(
  anchors : ArrayView[DrawingAnchor],
  drawing_rels_xml : StringView,
  drawing_part : StringView,
  part_names : Map[String, String],
  content_types : @ooxml.PackageContentTypes,
  metrics : DrawingMetrics,
  archive : @zip.Archive,
  media_cache? : Map[String, Bytes],
  budget? : ReadBudget,
  cancelled? : () -> Bool = () => false,
) -> Array[Image] raise XlsxError {
  let images : Array[Image] = []
  let retained_media = media_cache.unwrap_or(Map([]))
  let image_targets = if drawing_rels_xml is "" {
    Map([])
  } else {
    parse_internal_relationship_targets(
      drawing_rels_xml,
      rel_image,
      budget?,
      cancelled~,
    )
  }
  let hyperlink_targets = if drawing_rels_xml is "" {
    Map([])
  } else {
    parse_external_relationship_targets(
      drawing_rels_xml,
      rel_hyperlink,
      budget?,
      cancelled~,
    )
  }
  // location links (e.g. "Sheet1!B2") are written as internal-mode
  // relationships by this library's own writer; reading only external-mode
  // records would drop them on round-trip
  let location_hyperlink_targets = if drawing_rels_xml is "" {
    Map([])
  } else {
    parse_internal_relationship_targets(
      drawing_rels_xml,
      rel_hyperlink,
      budget?,
      cancelled~,
    )
  }
  for anchor in anchors {
    if anchor.unsupported {
      continue
    }
    let anchor_xml = anchor.xml
    let positioning = anchor.positioning
    charge_drawing_anchor_pass(budget, anchor_xml)
    let pic_body = match extract_tag_body_from(anchor_xml, "xdr:pic") {
      Some(value) => value
      None => continue
    }
    let origin = anchor.origin_for_read()
    let reference = origin.reference
    let offset_x = origin.col_off_emu / picture_emu_per_pixel
    let offset_y = origin.row_off_emu / picture_emu_per_pixel
    let (width_emu, height_emu) = anchor.extent_for_read(metrics, origin)
    let c_nv_pr_tag = match tag_attributes_in(pic_body, "xdr:cNvPr") {
      Some(value) => value
      None => raise InvalidXml(msg="drawing cNvPr missing")
    }
    let name = match attr_value(c_nv_pr_tag, "name") {
      Some(value) => unescape_xml_text(value)
      None => ""
    }
    let alt_text = match attr_value(c_nv_pr_tag, "descr") {
      Some(value) => unescape_xml_text(value)
      None => ""
    }
    let lock_aspect_ratio = match tag_attributes_in(pic_body, "a:picLocks") {
      Some(tag) =>
        match attr_value(tag, "noChangeAspect") {
          Some(value) => parse_bool_attr(value)
          None => false
        }
      None => false
    }
    let client_tag = match tag_attributes_in(anchor_xml, "xdr:clientData") {
      Some(value) => value
      None => ""
    }
    let locked = match attr_value(client_tag, "fLocksWithSheet") {
      Some(value) => parse_bool_attr(value)
      None => true
    }
    let print_object = match attr_value(client_tag, "fPrintsWithSheet") {
      Some(value) => parse_bool_attr(value)
      None => true
    }
    let blip_tag = match tag_attributes_in(pic_body, "a:blip") {
      Some(value) => value
      None => raise InvalidXml(msg="drawing blip missing")
    }
    let embed_id = match attr_value(blip_tag, "r:embed") {
      Some(value) => value
      None => raise InvalidXml(msg="drawing embed missing")
    }
    let target = match image_targets.get(embed_id.to_string()) {
      Some(value) => value
      None => raise InvalidXml(msg="drawing image target missing")
    }
    let media_path = actual_relationship_target_path(
      drawing_part,
      target,
      part_names,
      cancelled~,
    )
    let (source_data, identity) = load_image_part(
      archive,
      content_types,
      media_path,
      "drawing image",
      cancelled~,
    )
    let data = match retained_media.get(media_path) {
      Some(value) => value
      None => {
        let value = source_data.to_owned()
        retained_media[media_path] = value
        value
      }
    }
    let mut hyperlink = ""
    let mut hyperlink_type = HyperlinkType::Unset
    match tag_attributes_in(pic_body, "a:hlinkClick") {
      Some(tag) =>
        match attr_value(tag, "r:id") {
          Some(rel_id) =>
            match hyperlink_targets.get(rel_id.to_string()) {
              Some(value) => {
                hyperlink = value
                hyperlink_type = External
              }
              None =>
                match location_hyperlink_targets.get(rel_id.to_string()) {
                  Some(value) => {
                    hyperlink = value
                    hyperlink_type = Location
                  }
                  None => ()
                }
            }
          None => ()
        }
      None => ()
    }
    images.push({
      reference,
      data,
      extension: identity.extension,
      content_type: identity.content_type,
      offset_x,
      offset_y,
      scale_x: 1.0,
      scale_y: 1.0,
      width_emu,
      height_emu,
      print_object,
      locked,
      hyperlink,
      hyperlink_type,
      name,
      alt_text,
      lock_aspect_ratio,
      positioning,
      drawing_offset_x_emu: Some(origin.col_off_emu),
      drawing_offset_y_emu: Some(origin.row_off_emu),
      drawing_width_emu: Some(width_emu),
      drawing_height_emu: Some(height_emu),
      drawing_order: Some(anchor.order),
    })
  }
  images
}

///|
fn parse_drawing_charts(
  anchors : ArrayView[DrawingAnchor],
  drawing_rels_xml : StringView,
  drawing_part : StringView,
  part_names : Map[String, String],
  content_types : @ooxml.PackageContentTypes,
  metrics : DrawingMetrics,
  archive : @zip.Archive,
  decode : (BytesView) -> String raise XlsxError,
  budget? : ReadBudget,
  cancelled? : () -> Bool = () => false,
) -> Array[Chart] raise XlsxError {
  let charts : Array[Chart] = []
  let chart_targets = if drawing_rels_xml is "" {
    Map([])
  } else {
    parse_internal_relationship_targets(
      drawing_rels_xml,
      rel_chart,
      budget?,
      cancelled~,
    )
  }
  for anchor in anchors {
    if anchor.unsupported {
      continue
    }
    let anchor_xml = anchor.xml
    let positioning = anchor.positioning
    charge_drawing_anchor_pass(budget, anchor_xml)
    let chart_tag = match tag_attributes_in(anchor_xml, "c:chart") {
      Some(value) => value
      None => continue
    }
    let rel_id = match attr_value(chart_tag, "r:id") {
      Some(value) => value
      None => raise InvalidXml(msg="drawing chart rel id missing")
    }
    let target = match chart_targets.get(rel_id.to_string()) {
      Some(value) => value
      None => raise InvalidXml(msg="drawing chart target missing")
    }
    let chart_path = actual_relationship_target_path(
      drawing_part,
      target,
      part_names,
      cancelled~,
    )
    require_part_content_type(
      content_types,
      logical_archive_part_path(chart_path),
      [ct_chart],
      "chart",
      cancelled~,
    )
    let chart_bytes = match archive.get(chart_path) {
      Some(value) => value
      None => raise MissingPart(path=chart_path)
    }
    let xml = decode(chart_bytes)
    let origin = anchor.origin_for_read()
    let reference = origin.reference
    let offset_x = origin.col_off_emu / picture_emu_per_pixel
    let offset_y = origin.row_off_emu / picture_emu_per_pixel
    let (width_emu, height_emu) = anchor.extent_for_read(metrics, origin)
    let client_tag = match tag_attributes_in(anchor_xml, "xdr:clientData") {
      Some(value) => value
      None => ""
    }
    let locked = match attr_value(client_tag, "fLocksWithSheet") {
      Some(value) => parse_bool_attr(value)
      None => true
    }
    let print_object = match attr_value(client_tag, "fPrintsWithSheet") {
      Some(value) => parse_bool_attr(value)
      None => true
    }
    let drawing_frame_canonical = drawing_chart_frame_is_canonical(
      anchor_xml,
      width_emu,
      height_emu,
      locked,
      print_object,
      budget?,
      cancelled~,
    )
    charts.push({
      reference,
      xml,
      offset_x,
      offset_y,
      width_emu,
      height_emu,
      print_object,
      locked,
      positioning,
      drawing_offset_x_emu: Some(origin.col_off_emu),
      drawing_offset_y_emu: Some(origin.row_off_emu),
      drawing_width_emu: Some(width_emu),
      drawing_height_emu: Some(height_emu),
      drawing_order: Some(anchor.order),
      drawing_frame_canonical,
    })
  }
  charts
}

///|
/// Checks the complete graphic-frame subtree against the exact structural
/// subset the writer can regenerate from `Chart`. A single namespace-aware
/// scan rejects additional attributes, children, locks, names, alternative
/// text, or transforms instead of accepting the first familiar descendant.
fn drawing_chart_frame_is_canonical(
  anchor_xml : StringView,
  width_emu : Int,
  height_emu : Int,
  locked : Bool,
  print_object : Bool,
  budget? : ReadBudget,
  cancelled? : () -> Bool = () => false,
) -> Bool raise XlsxError {
  if cancelled() {
    raise ReadCancelled
  }
  let wrapper_prefix = ""
  let wrapper_suffix = ""
  let wrapped_length = wrapper_prefix.length() +
    anchor_xml.length() +
    wrapper_suffix.length()
  // Preflight the retained-fragment materialization. The two wrapped charges
  // cover the scanner's complete pass (including character-data
  // classification) and the exact-attribute pass. Attribute values are
  // extracted during that exact pass, so there are no hidden rescans.
  charge_drawing_work(budget, wrapped_length)
  let wrapper = StringBuilder::new(size_hint=wrapped_length)
  wrapper.write_string(wrapper_prefix)
  wrapper.write_view(anchor_xml)
  wrapper.write_string(wrapper_suffix)
  let wrapped = wrapper.to_string()
  charge_drawing_anchor_pass(budget, wrapped)
  charge_drawing_anchor_pass(budget, wrapped)
  try {
    let scanner = @ooxml.XmlStartTagScanner::new(wrapped, cancelled~)
    let drawing_namespace = transitional_spreadsheet_drawing_namespace
    let main_namespace = transitional_drawing_main_namespace
    let chart_namespace = transitional_drawing_chart_namespace
    let relationship_namespace = transitional_relationship_attribute_namespace
    let mut step = 0
    let mut frame_id = ""
    while workbook_scanner_next(scanner) {
      let depth = scanner.depth()
      let local_name = scanner.local_name()
      let namespace_uri = scanner.namespace_uri()
      if step == 0 {
        let direct_frame = depth == 3 &&
          namespace_uri == drawing_namespace &&
          local_name == "graphicFrame" &&
          scanner.parent_namespace_uri() == Some(drawing_namespace) &&
          scanner
          .parent_local_name()
          .map(name => xlsx_drawing_anchor_name(name))
          .unwrap_or(false)
        if direct_frame {
          guard workbook_scanner_has_exact_attributes(scanner, []) else {
            return false
          }
          step = 1
        }
        continue
      }
      if !scanner.intervening_character_data_is_whitespace() {
        return false
      }
      let matched = match step {
        1 =>
          depth == 4 &&
          namespace_uri == drawing_namespace &&
          local_name == "nvGraphicFramePr" &&
          scanner.parent_namespace_uri() == Some(drawing_namespace) &&
          scanner.parent_local_name() == Some("graphicFrame") &&
          workbook_scanner_has_exact_attributes(scanner, [])
        2 => {
          guard depth == 5 &&
            namespace_uri == drawing_namespace &&
            local_name == "cNvPr" &&
            scanner.parent_namespace_uri() == Some(drawing_namespace) &&
            scanner.parent_local_name() == Some("nvGraphicFramePr") else {
            return false
          }
          guard workbook_scanner_exact_attribute_values(scanner, [
              ("", "id"),
              ("", "name"),
            ])
            is Some(attributes) else {
            return false
          }
          frame_id = attributes[0]
          guard frame_id != "" else { return false }
          let parsed_id = @string.parse_int(frame_id, base=10) catch {
            _ => return false
          }
          parsed_id > 0 &&
          frame_id == "\{parsed_id}" &&
          attributes[1] == "Chart " + frame_id
        }
        3 =>
          depth == 5 &&
          namespace_uri == drawing_namespace &&
          local_name == "cNvGraphicFramePr" &&
          scanner.parent_namespace_uri() == Some(drawing_namespace) &&
          scanner.parent_local_name() == Some("nvGraphicFramePr") &&
          workbook_scanner_has_exact_attributes(scanner, [])
        4 =>
          depth == 4 &&
          namespace_uri == drawing_namespace &&
          local_name == "xfrm" &&
          scanner.parent_namespace_uri() == Some(drawing_namespace) &&
          scanner.parent_local_name() == Some("graphicFrame") &&
          workbook_scanner_has_exact_attributes(scanner, [])
        5 =>
          depth == 5 &&
          namespace_uri == main_namespace &&
          local_name == "off" &&
          scanner.parent_namespace_uri() == Some(drawing_namespace) &&
          scanner.parent_local_name() == Some("xfrm") &&
          workbook_scanner_has_exact_attributes(scanner, [
            ("", "x", "0"),
            ("", "y", "0"),
          ])
        6 =>
          depth == 5 &&
          namespace_uri == main_namespace &&
          local_name == "ext" &&
          scanner.parent_namespace_uri() == Some(drawing_namespace) &&
          scanner.parent_local_name() == Some("xfrm") &&
          workbook_scanner_has_exact_attributes(scanner, [
            ("", "cx", "\{width_emu}"),
            ("", "cy", "\{height_emu}"),
          ])
        7 =>
          depth == 4 &&
          namespace_uri == main_namespace &&
          local_name == "graphic" &&
          scanner.parent_namespace_uri() == Some(drawing_namespace) &&
          scanner.parent_local_name() == Some("graphicFrame") &&
          workbook_scanner_has_exact_attributes(scanner, [])
        8 =>
          depth == 5 &&
          namespace_uri == main_namespace &&
          local_name == "graphicData" &&
          scanner.parent_namespace_uri() == Some(main_namespace) &&
          scanner.parent_local_name() == Some("graphic") &&
          workbook_scanner_has_exact_attributes(scanner, [
            ("", "uri", chart_namespace),
          ])
        9 => {
          guard depth == 6 &&
            namespace_uri == chart_namespace &&
            local_name == "chart" &&
            scanner.parent_namespace_uri() == Some(main_namespace) &&
            scanner.parent_local_name() == Some("graphicData") else {
            return false
          }
          guard workbook_scanner_exact_attribute_values(scanner, [
              (relationship_namespace, "id"),
            ])
            is Some(attributes) else {
            return false
          }
          attributes[0] != ""
        }
        10 =>
          depth == 3 &&
          namespace_uri == drawing_namespace &&
          local_name == "clientData" &&
          scanner.parent_namespace_uri() == Some(drawing_namespace) &&
          scanner
          .parent_local_name()
          .map(name => xlsx_drawing_anchor_name(name))
          .unwrap_or(false) &&
          workbook_scanner_has_exact_attributes(scanner, [
            ("", "fLocksWithSheet", if locked { "1" } else { "0" }),
            ("", "fPrintsWithSheet", if print_object { "1" } else { "0" }),
          ])
        _ => false
      }
      if !matched {
        return false
      }
      step += 1
    }
    step == 11 &&
    frame_id != "" &&
    scanner.intervening_character_data_is_whitespace()
  } catch {
    InvalidXml(_) => false
    ReadCancelled => raise ReadCancelled
    error => raise error
  }
}

///|
fn parse_drawing_shapes(
  anchors : ArrayView[DrawingAnchor],
  metrics : DrawingMetrics,
  budget? : ReadBudget,
) -> Array[Shape] raise XlsxError {
  let shapes : Array[Shape] = []
  for anchor in anchors {
    if anchor.unsupported {
      continue
    }
    let anchor_xml = anchor.xml
    let positioning = anchor.positioning
    charge_drawing_anchor_pass(budget, anchor_xml)
    let sp_body = match extract_tag_body_from(anchor_xml, "xdr:sp") {
      Some(value) => value
      None => continue
    }
    if !sp_body.contains(" value
      None => raise InvalidXml(msg="drawing shape cNvPr missing")
    }
    let name = match attr_value(c_nv_pr_tag, "name") {
      Some(value) => unescape_xml_text(value)
      None => ""
    }
    let alt_text = match attr_value(c_nv_pr_tag, "descr") {
      Some(value) => unescape_xml_text(value)
      None => ""
    }
    let macro_name = match attr_value(c_nv_pr_tag, "macro") {
      Some(value) => unescape_xml_text(value)
      None => ""
    }
    let client_tag = match tag_attributes_in(anchor_xml, "xdr:clientData") {
      Some(value) => value
      None => ""
    }
    let locked = match attr_value(client_tag, "fLocksWithSheet") {
      Some(value) => parse_bool_attr(value)
      None => true
    }
    let print_object = match attr_value(client_tag, "fPrintsWithSheet") {
      Some(value) => parse_bool_attr(value)
      None => true
    }
    let sppr_body = match extract_tag_body_from(sp_body, "xdr:spPr") {
      Some(value) => value
      None => raise InvalidXml(msg="drawing shape spPr missing")
    }
    let width = (cx.to_double() / picture_emu_per_pixel.to_double())
      .round()
      .to_int()
    let height = (cy.to_double() / picture_emu_per_pixel.to_double())
      .round()
      .to_int()
    let geom_tag = match tag_attributes_in(sppr_body, "a:prstGeom") {
      Some(value) => value
      None => raise InvalidXml(msg="drawing shape prstGeom missing")
    }
    let shape_type = match attr_value(geom_tag, "prst") {
      Some(value) => unescape_xml_text(value)
      None => raise InvalidXml(msg="drawing shape prst missing")
    }
    let mut fill_color = ""
    let mut fill_transparency = 0
    match drawing_shape_solid_fill_body(sppr_body) {
      Some(fill_body) =>
        match tag_attributes_in(fill_body, "a:srgbClr") {
          Some(tag) =>
            match attr_value(tag, "val") {
              Some(value) => {
                fill_color = unescape_xml_text(value)
                match tag_attributes_in(fill_body, "a:alpha") {
                  Some(alpha_tag) =>
                    match attr_value(alpha_tag, "val") {
                      Some(alpha) =>
                        try @string.parse_int(alpha, base=10) catch {
                          _ => ()
                        } noraise {
                          value => {
                            let visible = value / 1000
                            let transparency = 100 - visible
                            if transparency >= 0 && transparency <= 100 {
                              fill_transparency = transparency
                            }
                          }
                        }
                      None => ()
                    }
                  None => ()
                }
              }
              None => ()
            }
          None => ()
        }
      None => ()
    }
    let mut line_color = ""
    let mut line_width = 1.0
    match tag_attributes_in(sppr_body, "a:ln") {
      Some(tag) => {
        match attr_value(tag, "w") {
          Some(value) =>
            try @string.parse_int(value, base=10) catch {
              _ => ()
            } noraise {
              value => line_width = value.to_double() / 12700.0
            }
          None => ()
        }
        match extract_tag_body_from(sppr_body, "a:ln") {
          Some(ln_body) =>
            match tag_attributes_in(ln_body, "a:srgbClr") {
              Some(color_tag) =>
                match attr_value(color_tag, "val") {
                  Some(value) => line_color = unescape_xml_text(value)
                  None => ()
                }
              None => ()
            }
          None => ()
        }
      }
      None => ()
    }
    let paragraph = match extract_tag_body_from(sp_body, "xdr:txBody") {
      Some(tx_body) => parse_drawing_shape_text_runs(tx_body)
      None => []
    }
    let text = ""
    let format = shape_graphic_options(
      1.0, 1.0, name, alt_text, print_object, locked, positioning,
    )
    let offset_x = origin.col_off_emu / picture_emu_per_pixel
    let offset_y = origin.row_off_emu / picture_emu_per_pixel
    if offset_x != 0 {
      format.offset_x = Some(offset_x)
    }
    if offset_y != 0 {
      format.offset_y = Some(offset_y)
    }
    let fill = shape_fill_from_values(fill_color, fill_transparency)
    let line = shape_line_from_values(line_color, line_width)
    shapes.push({
      reference,
      cell: reference,
      shape_type,
      macro_alias: macro_name,
      macro_name,
      format,
      text,
      paragraph,
      width,
      height,
      scale_x: 1.0,
      scale_y: 1.0,
      fill,
      fill_color,
      fill_transparency,
      line,
      line_color,
      line_width,
      name,
      alt_text,
      print_object,
      locked,
      positioning,
      drawing_offset_x_emu: Some(origin.col_off_emu),
      drawing_offset_y_emu: Some(origin.row_off_emu),
      drawing_width_emu: Some(cx),
      drawing_height_emu: Some(cy),
      drawing_order: Some(anchor.order),
    })
  }
  shapes
}

///|
priv struct SlicerAnchorInfo {
  cell : String
  offset_x : Int
  offset_y : Int
  width : Int
  height : Int
  positioning : PicturePositioning
  macro_name : String
  alt_text : String
  locked : Bool
  print_object : Bool
  offset_x_emu : Int
  offset_y_emu : Int
  width_emu : Int
  height_emu : Int
  order : Int
}

///|
fn parse_drawing_slicer_anchors(
  anchors : ArrayView[DrawingAnchor],
  metrics : DrawingMetrics,
  budget? : ReadBudget,
) -> Map[String, SlicerAnchorInfo] raise XlsxError {
  let out : Map[String, SlicerAnchorInfo] = Map([])
  for anchor in anchors {
    if anchor.unsupported {
      continue
    }
    let anchor_xml = anchor.xml
    let positioning = anchor.positioning
    charge_drawing_anchor_pass(budget, anchor_xml)
    let slicer_tag = match tag_attributes_in(anchor_xml, "sle:slicer") {
      Some(value) => value
      None => continue
    }
    let slicer_name = match attr_value(slicer_tag, "name") {
      Some(value) => unescape_xml_text(value)
      None => continue
    }
    let origin = anchor.origin_for_read()
    let cell = origin.reference
    let offset_x = origin.col_off_emu / picture_emu_per_pixel
    let offset_y = origin.row_off_emu / picture_emu_per_pixel
    let (width_emu, height_emu) = anchor.extent_for_read(
      metrics,
      origin,
      object_name="slicer",
    )
    let width = (width_emu.to_double() / picture_emu_per_pixel.to_double())
      .round()
      .to_int()
    let height = (height_emu.to_double() / picture_emu_per_pixel.to_double())
      .round()
      .to_int()
    let graphic_frame_tag = match
      tag_attributes_in(anchor_xml, "xdr:graphicFrame") {
      Some(value) => value
      None => ""
    }
    let macro_name = match attr_value(graphic_frame_tag, "macro") {
      Some(value) => unescape_xml_text(value)
      None => ""
    }
    let c_nv_pr_tag = match tag_attributes_in(anchor_xml, "xdr:cNvPr") {
      Some(value) => value
      None => ""
    }
    let alt_text = match attr_value(c_nv_pr_tag, "descr") {
      Some(value) => unescape_xml_text(value)
      None => ""
    }
    let client_tag = match tag_attributes_in(anchor_xml, "xdr:clientData") {
      Some(value) => value
      None => ""
    }
    let locked = match attr_value(client_tag, "fLocksWithSheet") {
      Some(value) => parse_bool_attr(value)
      None => true
    }
    let print_object = match attr_value(client_tag, "fPrintsWithSheet") {
      Some(value) => parse_bool_attr(value)
      None => true
    }
    out[slicer_name] = {
      cell,
      offset_x,
      offset_y,
      width,
      height,
      positioning,
      macro_name,
      alt_text,
      locked,
      print_object,
      offset_x_emu: origin.col_off_emu,
      offset_y_emu: origin.row_off_emu,
      width_emu,
      height_emu,
      order: anchor.order,
    }
  }
  out
}

///|
fn wb_from_xml() -> String {
  "0000"
}

///|
fn wb_pic_xml(embed_id : String) -> String {
  ""
}

///|
fn wb_one_cell_anchor(xml_from : String, xml_ext : String) -> String {
  let pic = wb_pic_xml("rId1")
  "\{xml_from}\{xml_ext}\{pic}"
}

///|
fn wb_two_cell_anchor(xml_from : String, xml_to : String) -> String {
  let pic = wb_pic_xml("rId1")
  "\{xml_from}\{xml_to}\{pic}"
}

///|
fn wb_rels_image_target(target : String) -> String {
  "" +
  "" +
  ""
}

///|
fn wb_canonical_chart_anchor() -> String {
  let anchor =
    #|
    #|  0000
    #|  
    #|  
    #|    
    #|      
    #|      
    #|    
    #|    
    #|      
    #|      
    #|    
    #|    
    #|      
    #|        
    #|      
    #|    
    #|  
    #|  
    #|
  anchor
}

///|
test "DrawingML MCE fallback feeds image chart shape and slicer readers" {
  let source =
    #|
    #|  0000
    #|  1000
    #|  2000
    #|  3000
    #|
  let drawing = canonicalize_xlsx_drawing_xml(
    source, default_max_xml_part_bytes,
  )
  assert_false(drawing.contains("bad-image"))
  assert_false(drawing.contains("chart-bad"))
  assert_false(drawing.contains("bad-shape"))
  assert_false(drawing.contains("slicer-bad"))
  assert_false(drawing.contains("
    #|  
    #|  
    #|
  let content_types = try! @ooxml.parse_package_content_types(
    (
      #|
      #|  
      #|  
      #|
    ),
  )
  let archive = @zip.Archive::new()
  archive.add("xl/media/good.png", @encoding/utf8.encode("good-image"))
  archive.add("xl/charts/good.xml", @encoding/utf8.encode("good-chart"))
  let metrics = drawing_metrics_for_sheet(Worksheet::new("Sheet1"))
  let images = parse_drawing_images(
    anchors,
    rels,
    "xl/drawings/drawing1.xml",
    Map([]),
    content_types,
    metrics,
    archive,
  )
  inspect(images.length(), content="1")
  inspect(images[0].name, content="good-image")
  let charts = parse_drawing_charts(
    anchors,
    rels,
    "xl/drawings/drawing1.xml",
    Map([]),
    content_types,
    metrics,
    archive,
    decode_utf8_or_invalid_xml,
  )
  inspect(charts.length(), content="1")
  inspect(charts[0].xml, content="good-chart")
  let shapes = parse_drawing_shapes(anchors, metrics)
  inspect(shapes.length(), content="1")
  inspect(shapes[0].name, content="good-shape")
  inspect(shapes[0].shape_type, content="rect")
  let slicers = parse_drawing_slicer_anchors(anchors, metrics)
  guard slicers.get("slicer-good") is Some(slicer) else {
    fail("expected projected slicer fallback")
  }
  inspect(slicers.length(), content="1")
  inspect(slicer.macro_name, content="good-macro")
  inspect(slicer.alt_text, content="good-slicer")
}

///|
test "DrawingML extension payload cannot spoof anchors or drawing features" {
  let source =
    #|
    #|  999900
    #|  0000
    #|  1000
    #|  2000
    #|  3000
    #|
  let drawing = canonicalize_xlsx_drawing_xml(
    source, default_max_xml_part_bytes,
  )
  assert_false(drawing.contains("bad-anchor"))
  assert_false(drawing.contains("bad-image"))
  assert_false(drawing.contains("chart-bad"))
  assert_false(drawing.contains("bad-shape"))
  assert_false(drawing.contains("slicer-bad"))
  assert_false(drawing.contains("extLst"))
  let anchors = parse_drawing_anchors(drawing)
  inspect(anchors.length(), content="4")
  let image_anchor = anchors[0].xml
  let chart_anchor = anchors[1].xml
  let shape_anchor = anchors[2].xml
  let slicer_anchor = anchors[3].xml
  assert_true(image_anchor.contains("image-good"))
  assert_true(chart_anchor.contains("chart-good"))
  assert_true(shape_anchor.contains("good-shape"))
  assert_true(slicer_anchor.contains("slicer-good"))
}

///|
test "DrawingML retains valid unsupported anchors without reading nested objects" {
  let source =
    #|
    #|  0000
    #|  1000
    #|
  let drawing = canonicalize_xlsx_drawing_xml(
    source, default_max_xml_part_bytes,
  )
  assert_true(drawing.contains(""))
  let anchors = parse_drawing_anchors(drawing)
  inspect(anchors.length(), content="2")
  assert_true(anchors[0].unsupported)
  assert_true(anchors[0].xml.contains("name=\"nested\""))
  assert_false(anchors[1].unsupported)
  let anchor_xml = anchors[1].xml
  assert_true(anchor_xml.contains("name=\"direct\""))
  assert_false(anchor_xml.contains("name=\"nested\""))
  let shapes = parse_drawing_shapes(
    anchors,
    drawing_metrics_for_sheet(Worksheet::new("Sheet1")),
  )
  assert_eq(shapes.length(), 1)
  assert_eq(shapes[0].name, "direct")
}

///|
test "DrawingML rejects extension entries outside extension lists" {
  let source =
    #|
  try canonicalize_xlsx_drawing_xml(source, default_max_xml_part_bytes) catch {
    InvalidXml(msg~) =>
      inspect(msg, content="drawing extension payload is misplaced")
    _ => fail("unexpected misplaced extension error")
  } noraise {
    _ => fail("expected a misplaced extension payload error")
  }
}

///|
test "DrawingML rejects supported-namespace reader decoys" {
  let cases : Array[(String, String)] = [
    (
      (
        #|
      ),
      "drawing anchor reader target is misplaced",
    ),
    (
      (
        #|
      ),
      "drawing reader object is misplaced",
    ),
    (
      (
        #|
      ),
      "drawing reader object is misplaced",
    ),
    (
      (
        #|
      ),
      "drawing chart graphicData URI is invalid",
    ),
    (
      (
        #|
      ),
      "drawing slicer graphicData URI is invalid",
    ),
    (
      (
        #|
      ),
      "drawing anchor has multiple reader objects",
    ),
    (
      (
        #|
      ),
      "drawing reader target dialect is mixed",
    ),
    (
      (
        #|
      ),
      "drawing blip target is misplaced",
    ),
    (
      (
        #|
      ),
      "drawing blip target is duplicated",
    ),
    (
      (
        #|
      ),
      "drawing reader target dialect is mixed",
    ),
    (
      (
        #|
      ),
      "drawing relationship attribute dialect is invalid",
    ),
    (
      (
        #|decoyreal
      ),
      "drawing text target is misplaced",
    ),
    (
      (
        #|decoyreal
      ),
      "drawing text target is misplaced",
    ),
  ]
  for case in cases {
    let (source, expected) = case
    try
      canonicalize_xlsx_drawing_xml(source, default_max_xml_part_bytes)
    catch {
      InvalidXml(msg~) => assert_eq(msg, expected)
      _ => fail("unexpected drawing structure error")
    } noraise {
      _ => fail("expected supported-namespace drawing decoy rejection")
    }
  }
}

///|
test "DrawingML rich text ignores unmodeled paragraph defaults" {
  let source =
    #|
    #|  
    #|    0000
    #|    
    #|    
    #|      
    #|      
    #|       real 
    #|    
    #|    
    #|  
    #|
  let canonical = canonicalize_xlsx_drawing_xml(
    source, default_max_xml_part_bytes,
  )
  let tx_body = extract_tag_body_from(canonical, "xdr:txBody").unwrap_or("")
  let runs = parse_drawing_shape_text_runs(tx_body)
  inspect(runs.length(), content="1")
  inspect(runs[0].text, content=" real ")
  let font = runs[0].font.unwrap()
  inspect(font.color.unwrap_or(""), content="A1B2C3")
  inspect(font.family.unwrap_or(""), content="")
}

///|
test "DrawingML shape fill selection excludes line-only fills" {
  let line_only =
    #|
  assert_true(drawing_shape_solid_fill_body(line_only) is None)
  let shape_and_line =
    #|
  assert_eq(
    drawing_shape_solid_fill_body(shape_and_line).unwrap_or(""),
    "",
  )
}

///|
test "chartsheet drawing selector requires exactly one chart payload" {
  let anchor =
    #|
  let one : ReadOnlyArray[DrawingAnchor] = [
    {
      xml: anchor,
      positioning: OneCell,
      form: OneCellAnchor,
      order: 0,
      unsupported: false,
    },
  ]
  assert_eq(single_drawing_chart_rel_id(one), "selected")
  let empty : ReadOnlyArray[DrawingAnchor] = []
  try single_drawing_chart_rel_id(empty) catch {
    InvalidXml(msg~) => assert_eq(msg, "chartsheet chart payload missing")
    _ => fail("unexpected empty chartsheet drawing error")
  } noraise {
    _ => fail("expected empty chartsheet drawing rejection")
  }
  let two : ReadOnlyArray[DrawingAnchor] = [
    {
      xml: anchor,
      positioning: OneCell,
      form: OneCellAnchor,
      order: 0,
      unsupported: false,
    },
    {
      xml: anchor,
      positioning: OneCell,
      form: OneCellAnchor,
      order: 1,
      unsupported: false,
    },
  ]
  try single_drawing_chart_rel_id(two) catch {
    InvalidXml(msg~) => assert_eq(msg, "chartsheet drawing has multiple charts")
    _ => fail("unexpected duplicate chartsheet chart error")
  } noraise {
    _ => fail("expected duplicate chartsheet chart rejection")
  }
}

///|
test "DrawingML validates Strict chart dialect and payload URI together" {
  let source =
    #|0000
  let canonical = canonicalize_xlsx_drawing_xml(
    source, default_max_xml_part_bytes,
  )
  assert_true(canonical.contains(""))
}

///|
test "DrawingML preprocessing charges every bounded pass" {
  let source =
    #|
  // Charging only the source and final output would fit this ceiling. The
  // independent root-validation, extension-removal, and MCE passes must not.
  let limit = source.length() * 2
  let budget = ReadBudget::new(
    ReadLimits::with_values(max_parser_work_units=limit),
  )
  try
    canonicalize_xlsx_drawing_xml(source, default_max_xml_part_bytes, budget~)
  catch {
    ResourceLimitExceeded(kind~, limit=actual_limit, actual~) => {
      assert_eq(kind, "parser_work_units")
      assert_eq(actual_limit, limit)
      assert_true(actual > actual_limit)
    }
    _ => fail("unexpected DrawingML preprocessing limit error")
  } noraise {
    _ => fail("expected DrawingML preprocessing work rejection")
  }
}

///|
test "chart frame fidelity scan rejects extra content and charges its work" {
  let anchor = wb_canonical_chart_anchor()
  let budget = ReadBudget::new(
    ReadLimits::with_values(max_parser_work_units=1024 * 1024),
  )
  assert_true(
    drawing_chart_frame_is_canonical(anchor, 9525, 9525, true, true, budget~),
  )
  let charged_work = budget.parser_work_units
  assert_true(charged_work > anchor.length() * 3)
  let exact_budget = ReadBudget::new(
    ReadLimits::with_values(max_parser_work_units=charged_work),
  )
  assert_true(
    drawing_chart_frame_is_canonical(
      anchor,
      9525,
      9525,
      true,
      true,
      budget=exact_budget,
    ),
  )
  let limited_budget = ReadBudget::new(
    ReadLimits::with_values(max_parser_work_units=charged_work - 1),
  )
  try
    drawing_chart_frame_is_canonical(
      anchor,
      9525,
      9525,
      true,
      true,
      budget=limited_budget,
    )
    |> ignore
  catch {
    ResourceLimitExceeded(kind~, ..) => assert_eq(kind, "parser_work_units")
    _ => fail("unexpected chart frame work-limit error")
  } noraise {
    _ => fail("expected chart frame work-limit rejection")
  }
  let extra_transform = anchor.replace(
    old="    \n",
    new="    \n    \n",
  )
  assert_false(
    drawing_chart_frame_is_canonical(extra_transform, 9525, 9525, true, true),
  )
  let extra_attribute = anchor.replace(
    old="\n",
    new="custom\n",
  )
  assert_false(
    drawing_chart_frame_is_canonical(extra_text, 9525, 9525, true, true),
  )
  let mut cancellation_checks = 0
  try
    drawing_chart_frame_is_canonical(anchor, 9525, 9525, true, true, cancelled=() => {
      cancellation_checks += 1
      cancellation_checks >= 2
    })
    |> ignore
  catch {
    ReadCancelled => assert_true(cancellation_checks >= 2)
    _ => fail("unexpected chart frame cancellation error")
  } noraise {
    _ => fail("expected chart frame cancellation")
  }
}

///|
test "read_drawing_xml wb: parse_xml_int_tag missing/invalid errors" {
  let missing_tag = "1"
  let missing_result : Result[Int, Error] = Ok(
    parse_xml_int_tag(missing_tag, "xdr:col", "drawing col invalid"),
  ) catch {
    e => Err(e)
  }
  match missing_result {
    Err(XlsxError::InvalidXml(msg~)) =>
      inspect(msg, content="drawing col invalid")
    _ => fail("expected InvalidXml for missing xdr:col")
  }

  let invalid_value = "abc"
  let invalid_result : Result[Int, Error] = Ok(
    parse_xml_int_tag(invalid_value, "xdr:col", "drawing col invalid"),
  ) catch {
    e => Err(e)
  }
  match invalid_result {
    Err(XlsxError::InvalidXml(msg~)) =>
      inspect(msg, content="drawing col invalid")
    _ => fail("expected InvalidXml for invalid xdr:col numeric value")
  }
}

///|
test "read_drawing_xml wb: drawing_axis_span_emu handles reverse and same-index spans" {
  let sheet = Worksheet::new("Sheet1")
  let metrics = drawing_metrics_for_sheet(sheet)
  inspect(drawing_axis_span_emu(metrics.cols, 1, 200, 1, 800), content="600")
  inspect(drawing_axis_span_emu(metrics.cols, 1, 800, 1, 200), content="600")
  inspect(
    drawing_axis_span_emu(metrics.cols, 0, -9525, 1, -9525),
    content="609600",
  )
}

///|
test "read_drawing_xml wb: drawing spans use sparse dimensions and grid boundary" {
  let sheet = Worksheet::new("Sheet1")
  sheet.set_col_width(501, 10.0)
  let metrics = drawing_metrics_for_sheet(sheet)
  inspect(
    drawing_axis_span_emu(metrics.cols, 0, 0, 1000, 0),
    content="609752400",
  )
  inspect(
    drawing_axis_span_emu(metrics.cols, 16383, 0, 16384, 0),
    content="609600",
  )
}

///|
test "read_drawing_xml wb: drawing spans reject invalid markers and overflow" {
  let sheet = Worksheet::new("Sheet1")
  let metrics = drawing_metrics_for_sheet(sheet)
  let invalid_marker : Result[Int, Error] = Ok(
    drawing_axis_span_emu(metrics.cols, 0, 0, 0x7fff_ffff, 0),
  ) catch {
    error => Err(error)
  }
  match invalid_marker {
    Err(XlsxError::InvalidXml(msg~)) =>
      inspect(msg, content="drawing col invalid")
    _ => fail("expected invalid drawing column")
  }
  let overflow : Result[Int, Error] = Ok(
    drawing_axis_span_emu(metrics.rows, 0, 0, cell_ref_max_rows, 0),
  ) catch {
    error => Err(error)
  }
  match overflow {
    Err(XlsxError::InvalidXml(msg~)) =>
      inspect(msg, content="drawing span out of range")
    _ => fail("expected drawing span overflow")
  }
}

///|
test "read_drawing_xml wb: parse_drawing_anchors validation and positioning" {
  let one_missing_close = ""
  let one_result : Result[Array[DrawingAnchor], Error] = Ok(
    parse_drawing_anchors(one_missing_close),
  ) catch {
    e => Err(e)
  }
  match one_result {
    Err(XlsxError::InvalidXml(msg~)) =>
      inspect(msg, content="oneCellAnchor close missing")
    _ => fail("expected InvalidXml for oneCellAnchor close missing")
  }

  let two_missing_close = ""
  let two_result : Result[Array[DrawingAnchor], Error] = Ok(
    parse_drawing_anchors(two_missing_close),
  ) catch {
    e => Err(e)
  }
  match two_result {
    Err(XlsxError::InvalidXml(msg~)) =>
      inspect(msg, content="twoCellAnchor close missing")
    _ => fail("expected InvalidXml for twoCellAnchor close missing")
  }

  let absolute_missing_close = ""
  let absolute_result : Result[Array[DrawingAnchor], Error] = Ok(
    parse_drawing_anchors(absolute_missing_close),
  ) catch {
    e => Err(e)
  }
  match absolute_result {
    Err(XlsxError::InvalidXml(msg~)) =>
      inspect(msg, content="absoluteAnchor close missing")
    _ => fail("expected InvalidXml for absoluteAnchor close missing")
  }

  let xml =
    #|
    #|  
    #|  
    #|  
    #|  
    #|  
    #|
  let anchors = parse_drawing_anchors(xml)
  inspect(anchors.length(), content="5")
  debug_inspect(anchors[0].positioning, content="Absolute")
  debug_inspect(anchors[1].positioning, content="OneCell")
  debug_inspect(anchors[2].positioning, content="Absolute")
  debug_inspect(anchors[3].positioning, content="OneCell")
  debug_inspect(anchors[4].positioning, content="TwoCell")
  debug_inspect(anchors[0].form, content="AbsoluteAnchor")
  debug_inspect(anchors[2].form, content="TwoCellAnchor")
  try
    parse_drawing_anchors(
      "",
    )
  catch {
    InvalidXml(msg~) => inspect(msg, content="drawing editAs value is invalid")
    _ => fail("unexpected drawing editAs error")
  } noraise {
    _ => fail("expected invalid drawing editAs rejection")
  }
}

///|
test "read_drawing_xml wb: anchor parsing checks limits before copying source" {
  let drawing_xml = ""
  let limited = ReadBudget::new(
    ReadLimits::with_values(max_parser_work_units=1),
  )
  let limit_result : Result[Array[DrawingAnchor], Error] = Ok(
    parse_drawing_anchors(drawing_xml, budget=limited),
  ) catch {
    error => Err(error)
  }
  match limit_result {
    Err(ResourceLimitExceeded(kind~, limit~, actual~)) => {
      inspect(kind, content="parser_work_units")
      inspect(limit, content="1")
      inspect(actual, content="2")
    }
    _ => fail("expected drawing work limit")
  }

  let mut checks = 0
  let cancelled = ReadBudget::new(ReadLimits::new(), cancelled=() => {
    checks += 1
    true
  })
  let cancel_result : Result[Array[DrawingAnchor], Error] = Ok(
    parse_drawing_anchors(drawing_xml, budget=cancelled),
  ) catch {
    error => Err(error)
  }
  assert_true(cancel_result is Err(ReadCancelled))
  inspect(checks, content="1")
}

///|
test "read_drawing_xml wb: parse_drawing_images empty rel map branch" {
  let drawing_xml = wb_one_cell_anchor(
    wb_from_xml(),
    "",
  )
  let archive = @zip.Archive::new()
  let metrics = drawing_metrics_for_sheet(Worksheet::new("Sheet1"))
  let result : Result[Array[Image], Error] = Ok(
    parse_drawing_images(
      parse_drawing_anchors(drawing_xml),
      "",
      "xl/drawings/drawing1.xml",
      Map([]),
      image_reader_test_content_types(),
      metrics,
      archive,
    ),
  ) catch {
    e => Err(e)
  }
  match result {
    Err(XlsxError::InvalidXml(msg~)) =>
      inspect(msg, content="drawing image target missing")
    _ => fail("expected InvalidXml for missing drawing image target")
  }
}

///|
test "read_drawing_xml wb: signed marker offsets are retained" {
  let archive = @zip.Archive::new()
  archive.add("xl/media/image1.png", @encoding/utf8.encode("img"))
  let rels_xml = wb_rels_image_target("../media/image1.png")
  let drawing_xml = wb_one_cell_anchor(
    "00-19050-9525",
    "",
  )
  let metrics = drawing_metrics_for_sheet(Worksheet::new("Sheet1"))
  let images = parse_drawing_images(
    parse_drawing_anchors(drawing_xml),
    rels_xml,
    "xl/drawings/drawing1.xml",
    Map([]),
    image_reader_test_content_types(),
    metrics,
    archive,
  )
  inspect(images.length(), content="1")
  inspect(images[0].reference, content="A1")
  inspect(images[0].offset_x, content="-2")
  inspect(images[0].offset_y, content="-1")
}

///|
test "read_drawing_xml wb: repeated anchors share one retained media payload" {
  let archive = @zip.Archive::new()
  archive.add("xl/media/image1.png", @encoding/utf8.encode("shared-image"))
  let rels_xml = wb_rels_image_target("../media/image1.png")
  let anchor = "\{wb_from_xml()}\{wb_pic_xml("rId1")}"
  let drawing_xml = "\{anchor}\{anchor}"
  let retained : Map[String, Bytes] = Map([])
  let images = parse_drawing_images(
    parse_drawing_anchors(drawing_xml),
    rels_xml,
    "xl/drawings/drawing1.xml",
    Map([]),
    image_reader_test_content_types(),
    drawing_metrics_for_sheet(Worksheet::new("Sheet1")),
    archive,
    media_cache=retained,
  )
  assert_eq(images.length(), 2)
  assert_eq(retained.length(), 1)
  assert_eq(images[0].data, images[1].data)
}

///|
test "read_drawing_xml wb: parse_drawing_images anchor/ext validation errors" {
  let archive = @zip.Archive::new()
  archive.add("xl/media/image1.png", @encoding/utf8.encode("img"))
  let rels_xml = wb_rels_image_target("../media/image1.png")
  let sheet = Worksheet::new("Sheet1")
  let metrics = drawing_metrics_for_sheet(sheet)

  let missing_from = wb_one_cell_anchor(
    "", "",
  )
  let missing_from_result : Result[Array[Image], Error] = Ok(
    parse_drawing_images(
      parse_drawing_anchors(missing_from),
      rels_xml,
      "xl/drawings/drawing1.xml",
      Map([]),
      image_reader_test_content_types(),
      metrics,
      archive,
    ),
  ) catch {
    e => Err(e)
  }
  match missing_from_result {
    Err(XlsxError::InvalidXml(msg~)) =>
      inspect(msg, content="drawing anchor from missing")
    _ => fail("expected InvalidXml drawing anchor from missing")
  }

  let missing_ext = wb_one_cell_anchor(wb_from_xml(), "")
  let missing_ext_result : Result[Array[Image], Error] = Ok(
    parse_drawing_images(
      parse_drawing_anchors(missing_ext),
      rels_xml,
      "xl/drawings/drawing1.xml",
      Map([]),
      image_reader_test_content_types(),
      metrics,
      archive,
    ),
  ) catch {
    e => Err(e)
  }
  match missing_ext_result {
    Err(XlsxError::InvalidXml(msg~)) =>
      inspect(msg, content="drawing ext missing")
    _ => fail("expected InvalidXml drawing ext missing")
  }

  let cx_invalid = wb_one_cell_anchor(
    wb_from_xml(),
    "",
  )
  let cx_invalid_result : Result[Array[Image], Error] = Ok(
    parse_drawing_images(
      parse_drawing_anchors(cx_invalid),
      rels_xml,
      "xl/drawings/drawing1.xml",
      Map([]),
      image_reader_test_content_types(),
      metrics,
      archive,
    ),
  ) catch {
    e => Err(e)
  }
  match cx_invalid_result {
    Err(XlsxError::InvalidXml(msg~)) =>
      inspect(msg, content="drawing cx invalid")
    _ => fail("expected InvalidXml drawing cx invalid")
  }

  let cy_missing = wb_one_cell_anchor(wb_from_xml(), "")
  let cy_missing_result : Result[Array[Image], Error] = Ok(
    parse_drawing_images(
      parse_drawing_anchors(cy_missing),
      rels_xml,
      "xl/drawings/drawing1.xml",
      Map([]),
      image_reader_test_content_types(),
      metrics,
      archive,
    ),
  ) catch {
    e => Err(e)
  }
  match cy_missing_result {
    Err(XlsxError::InvalidXml(msg~)) =>
      inspect(msg, content="drawing cy missing")
    _ => fail("expected InvalidXml drawing cy missing")
  }

  let invalid_to = wb_two_cell_anchor(
    wb_from_xml(),
    "2147483647000",
  )
  let invalid_to_result : Result[Array[Image], Error] = Ok(
    parse_drawing_images(
      parse_drawing_anchors(invalid_to),
      rels_xml,
      "xl/drawings/drawing1.xml",
      Map([]),
      image_reader_test_content_types(),
      metrics,
      archive,
    ),
  ) catch {
    error => Err(error)
  }
  match invalid_to_result {
    Err(XlsxError::InvalidXml(msg~)) =>
      inspect(msg, content="drawing col invalid")
    _ => fail("expected InvalidXml drawing to column invalid")
  }
}