///|
/// Colour specification used by cpdf draw operations.
///
/// The `CYMK` spelling follows the OCaml source variant name in `cpdfdraw.ml`.
pub(all) enum PdfDrawColour {
  PdfDrawNoCol
  PdfDrawRGB(Double, Double, Double)
  PdfDrawGrey(Double)
  PdfDrawCYMK(Double, Double, Double, Double)
} derive(Debug, Eq, ToJson)

///|
/// Paragraph justification used by cpdf draw operations.
pub(all) enum PdfDrawJustification {
  PdfDrawLeft
  PdfDrawRight
  PdfDrawCentre
} derive(Debug, Eq, ToJson)

///|
pub(all) struct PdfDrawExtGStateResource {
  kind : PdfName
  value : Double
  name : PdfName
} derive(Debug, Eq, ToJson)

///|
pub(all) struct PdfDrawXObjectResource {
  source_name : String
  resource_name : PdfName
  object_number : Int
} derive(Debug, Eq, ToJson)

///|
pub(all) struct PdfDrawFontResource {
  identifier : String
  font_index : Int
  resource_name : PdfName
  object_number : Int
} derive(Debug, Eq, ToJson)

///|
pub(all) struct PdfDrawFontPackState {
  identifier : String
  fontpack : PdfFontPack
} derive(Debug, Eq, ToJson)

///|
pub(all) enum PdfDrawStructureData {
  PdfDrawStructureMCID(String, Int, Int)
  PdfDrawStructureBeginTree(String)
  PdfDrawStructureEndTree
  PdfDrawStructureNamespace(String)
  PdfDrawStructureElementInfo(String, PdfObject)
  PdfDrawStructureEndElementInfo(String)
} derive(Debug, Eq, ToJson)

///|
/// Page-local context used to render `PdfDrawSpecialText`.
pub(all) struct PdfDrawSpecialTextContext {
  time : PdfStrftimeTime
  replacement_values : PdfAddTextReplacementValues
} derive(Debug, Eq, ToJson)

///|
/// Resource allocation state for rendering cpdf draw operations.
///
/// This ports the resource-numbering and ExtGState cache used by
/// `cpdfdraw.ml`'s `res`/`fresh_name`/`extgstate` helpers. Later rendering
/// slices will extend this state with tagged-content resources.
pub(all) struct PdfDrawRenderState {
  mut resource_number : Int
  mut marked_content_id : Int
  extgstates : Array[PdfDrawExtGStateResource]
  xobjects : Array[PdfDrawXObjectResource]
  form_xobjects : Array[PdfDrawXObjectResource]
  fonts : Array[PdfDrawFontResource]
  fontpacks : Array[PdfDrawFontPackState]
  mut current_fontpack_identifier : String
  mut current_fontpack : PdfFontPack
  mut font_size : Double
  mut structure_tree_enabled : Bool
  mut auto_tag : Bool
  mut structure_page_number : Int
  structure_data : Array[PdfDrawStructureData]
  page_names : Array[PdfName]
} derive(Debug, Eq, ToJson)

///|
fn pdf_draw_name(text : String) -> PdfName {
  pdf_name_of_bytes(@ascii.encode(text))
}

///|
let pdf_draw_extgstate_key_name : PdfName = pdf_draw_name("/ExtGState")

///|
let pdf_draw_xobject_key_name : PdfName = pdf_draw_name("/XObject")

///|
let pdf_draw_font_key_name : PdfName = pdf_draw_name("/Font")

///|
let pdf_draw_length_key_name : PdfName = pdf_draw_name("/Length")

///|
let pdf_draw_subtype_key_name : PdfName = pdf_draw_name("/Subtype")

///|
let pdf_draw_form_key_name : PdfName = pdf_draw_name("/Form")

///|
let pdf_draw_resources_key_name : PdfName = pdf_draw_name("/Resources")

///|
let pdf_draw_bbox_key_name : PdfName = pdf_draw_name("/BBox")

///|
let pdf_draw_mcid_key_name : PdfName = pdf_draw_name("/MCID")

///|
let pdf_draw_type_key_name : PdfName = pdf_draw_name("/Type")

///|
let pdf_draw_struct_tree_root_key_name : PdfName = pdf_draw_name(
  "/StructTreeRoot",
)

///|
let pdf_draw_parent_tree_key_name : PdfName = pdf_draw_name("/ParentTree")

///|
let pdf_draw_s_key_name : PdfName = pdf_draw_name("/S")

///|
let pdf_draw_p_key_name : PdfName = pdf_draw_name("/P")

///|
let pdf_draw_k_key_name : PdfName = pdf_draw_name("/K")

///|
let pdf_draw_pg_key_name : PdfName = pdf_draw_name("/Pg")

///|
let pdf_draw_ns_key_name : PdfName = pdf_draw_name("/NS")

///|
let pdf_draw_namespaces_key_name : PdfName = pdf_draw_name("/Namespaces")

///|
let pdf_draw_role_map_key_name : PdfName = pdf_draw_name("/RoleMap")

///|
let pdf_draw_struct_parents_key_name : PdfName = pdf_draw_name("/StructParents")

///|
let pdf_draw_begin_artifact_name : PdfName = pdf_draw_name("/BeginArtifact")

///|
let pdf_draw_end_artifact_name : PdfName = pdf_draw_name("/EndArtifact")

///|
let pdf_draw_fill_alpha_key_name : PdfName = pdf_draw_name("/ca")

///|
let pdf_draw_stroke_alpha_key_name : PdfName = pdf_draw_name("/CA")

///|
fn pdf_draw_extgstate_key() -> PdfName {
  pdf_draw_extgstate_key_name
}

///|
fn pdf_draw_xobject_key() -> PdfName {
  pdf_draw_xobject_key_name
}

///|
fn pdf_draw_font_key() -> PdfName {
  pdf_draw_font_key_name
}

///|
fn pdf_draw_length_key() -> PdfName {
  pdf_draw_length_key_name
}

///|
fn pdf_draw_subtype_key() -> PdfName {
  pdf_draw_subtype_key_name
}

///|
fn pdf_draw_form_key() -> PdfName {
  pdf_draw_form_key_name
}

///|
fn pdf_draw_resources_key() -> PdfName {
  pdf_draw_resources_key_name
}

///|
fn pdf_draw_bbox_key() -> PdfName {
  pdf_draw_bbox_key_name
}

///|
fn pdf_draw_mcid_key() -> PdfName {
  pdf_draw_mcid_key_name
}

///|
fn pdf_draw_type_key() -> PdfName {
  pdf_draw_type_key_name
}

///|
fn pdf_draw_struct_tree_root_key() -> PdfName {
  pdf_draw_struct_tree_root_key_name
}

///|
fn pdf_draw_parent_tree_key() -> PdfName {
  pdf_draw_parent_tree_key_name
}

///|
fn pdf_draw_s_key() -> PdfName {
  pdf_draw_s_key_name
}

///|
fn pdf_draw_p_key() -> PdfName {
  pdf_draw_p_key_name
}

///|
fn pdf_draw_k_key() -> PdfName {
  pdf_draw_k_key_name
}

///|
fn pdf_draw_pg_key() -> PdfName {
  pdf_draw_pg_key_name
}

///|
fn pdf_draw_ns_key() -> PdfName {
  pdf_draw_ns_key_name
}

///|
fn pdf_draw_namespaces_key() -> PdfName {
  pdf_draw_namespaces_key_name
}

///|
fn pdf_draw_role_map_key() -> PdfName {
  pdf_draw_role_map_key_name
}

///|
fn pdf_draw_struct_parents_key() -> PdfName {
  pdf_draw_struct_parents_key_name
}

///|
fn pdf_draw_begin_artifact() -> PdfName {
  pdf_draw_begin_artifact_name
}

///|
fn pdf_draw_end_artifact() -> PdfName {
  pdf_draw_end_artifact_name
}

///|
fn pdf_draw_fill_alpha_key() -> PdfName {
  pdf_draw_fill_alpha_key_name
}

///|
fn pdf_draw_stroke_alpha_key() -> PdfName {
  pdf_draw_stroke_alpha_key_name
}

///|
pub fn pdf_draw_render_state_new() -> PdfDrawRenderState {
  {
    resource_number: 0,
    marked_content_id: -1,
    extgstates: [],
    xobjects: [],
    form_xobjects: [],
    fonts: [],
    fontpacks: [],
    current_fontpack_identifier: "",
    current_fontpack: { fonts: [], entries: [] },
    font_size: 12.0,
    structure_tree_enabled: false,
    auto_tag: true,
    structure_page_number: 0,
    structure_data: [],
    page_names: [],
  }
}

///|
/// Build a special-text context from explicit replacement values.
pub fn pdf_draw_special_text_context(
  time : PdfStrftimeTime,
  replacement_values : PdfAddTextReplacementValues,
) -> PdfDrawSpecialTextContext {
  { time, replacement_values }
}

///|
/// Build cpdf draw special-text context for one page of a document.
pub fn PdfDocument::draw_special_text_context(
  self : PdfDocument,
  page_number : Int,
  filename? : String = "",
  bates? : Int = 1,
  bates_pad? : Int? = None,
  time? : PdfStrftimeTime? = None,
) -> PdfDrawSpecialTextContext raise PdfError {
  let resolved_time = match time {
    Some(value) => value
    None => pdf_strftime_dummy_time()
  }
  let bookmarks = self.read_bookmarks(preserve_actions=false)
  let values = self.addtext_replacement_values(
    bookmarks,
    page_number,
    self.endpage(),
    filename,
    bates,
    bates_pad,
  )
  { time: resolved_time, replacement_values: values }
}

///|
/// Compatibility wrapper for `PdfDocument::draw_special_text_context`.
pub fn pdf_draw_special_text_context_for_document(
  document : PdfDocument,
  page_number : Int,
  filename? : String = "",
  bates? : Int = 1,
  bates_pad? : Int? = None,
  time? : PdfStrftimeTime? = None,
) -> PdfDrawSpecialTextContext raise PdfError {
  document.draw_special_text_context(
    page_number,
    filename~,
    bates~,
    bates_pad~,
    time~,
  )
}

///|
fn PdfDrawRenderState::fresh_name(
  self : PdfDrawRenderState,
  prefix : String,
) -> PdfName {
  self.resource_number += 1
  pdf_draw_name(prefix + self.resource_number.to_string())
}

///|
fn PdfDrawRenderState::fresh_mcid(self : PdfDrawRenderState) -> Int {
  self.marked_content_id += 1
  self.marked_content_id
}

///|
fn PdfDrawRenderState::set_minimum_resource_number(
  self : PdfDrawRenderState,
  minimum : Int,
) -> Unit {
  if self.resource_number < minimum {
    self.resource_number = minimum
  }
}

///|
fn PdfDrawRenderState::reset_for_page_chunk(self : PdfDrawRenderState) -> Unit {
  self.extgstates.clear()
  self.marked_content_id = -1
}

///|
fn PdfDrawRenderState::set_structure_tree_enabled(
  self : PdfDrawRenderState,
  enabled : Bool,
) -> Unit {
  self.structure_tree_enabled = enabled
}

///|
fn PdfDrawRenderState::set_structure_page_number(
  self : PdfDrawRenderState,
  page_number : Int,
) -> Unit {
  self.structure_page_number = page_number
}

///|
fn PdfDrawRenderState::extgstate(
  self : PdfDrawRenderState,
  kind : PdfName,
  value : Double,
) -> PdfName {
  for entry in self.extgstates {
    if entry.kind == kind && entry.value == value {
      return entry.name
    }
  }
  let name = self.fresh_name("/G")
  self.extgstates.push({ kind, value, name })
  name
}

///|
fn PdfDrawRenderState::register_xobject(
  self : PdfDrawRenderState,
  source_name : String,
  prefix : String,
  object_number : Int,
) -> PdfName {
  let resource_name = self.fresh_name(prefix)
  for index in 0.. PdfName {
  let resource_name = self.fresh_name("/X")
  for index in 0.. PdfFontPack? {
  for entry in self.fontpacks {
    if entry.identifier == identifier {
      return Some(entry.fontpack)
    }
  }
  None
}

///|
fn PdfDrawRenderState::ensure_fontpack(
  self : PdfDrawRenderState,
  identifier : String,
  fontpack : PdfFontPack,
) -> PdfFontPack {
  match self.stored_fontpack(identifier) {
    Some(stored) => stored
    None => {
      self.fontpacks.push({ identifier, fontpack })
      fontpack
    }
  }
}

///|
fn PdfDrawRenderState::stored_font_resource(
  self : PdfDrawRenderState,
  identifier : String,
  font_index : Int,
) -> PdfDrawFontResource? {
  for resource in self.fonts {
    if resource.identifier == identifier && resource.font_index == font_index {
      return Some(resource)
    }
  }
  None
}

///|
fn PdfDrawRenderState::register_font_resource(
  self : PdfDrawRenderState,
  identifier : String,
  font_index : Int,
  object_number : Int,
) -> PdfName {
  let resource_name = self.fresh_name("/F")
  self.fonts.push({ identifier, font_index, resource_name, object_number })
  resource_name
}

///|
fn PdfDrawRenderState::register_fontpack_resources(
  self : PdfDrawRenderState,
  identifier : String,
  fontpack : PdfFontPack,
  document : PdfDocument,
) -> Unit raise PdfError {
  let stored = self.ensure_fontpack(identifier, fontpack)
  for index in 0.. resource
      None => {
        let object_number = document.write_font(stored.fonts[index])
        let resource_name = self.register_font_resource(
          identifier, index, object_number,
        )
        { identifier, font_index: index, resource_name, object_number }
      }
    }
    self.mark_page_resource(resource.resource_name)
  }
}

///|
fn PdfDrawRenderState::mark_page_resource(
  self : PdfDrawRenderState,
  resource_name : PdfName,
) -> Unit {
  for name in self.page_names {
    if name == resource_name {
      return
    }
  }
  self.page_names.push(resource_name)
}

///|
fn PdfDrawRenderState::image_resource_name(
  self : PdfDrawRenderState,
  source_name : String,
) -> PdfName raise PdfError {
  for entry in self.xobjects {
    if entry.source_name == source_name {
      self.mark_page_resource(entry.resource_name)
      return entry.resource_name
    }
  }
  raise SoftError("Image not found: " + source_name)
}

///|
fn PdfDrawRenderState::form_resource_name(
  self : PdfDrawRenderState,
  source_name : String,
) -> PdfName raise PdfError {
  for entry in self.form_xobjects {
    if entry.source_name == source_name {
      self.mark_page_resource(entry.resource_name)
      return entry.resource_name
    }
  }
  raise SoftError("Form XObject not found: " + source_name)
}

///|
fn PdfDrawRenderState::font_resource_name(
  self : PdfDrawRenderState,
  identifier : String,
  font_index : Int,
) -> PdfName raise PdfError {
  match self.stored_font_resource(identifier, font_index) {
    Some(resource) => {
      self.mark_page_resource(resource.resource_name)
      resource.resource_name
    }
    None => raise SoftError("Font resource not found: " + identifier)
  }
}

///|
fn PdfDrawRenderState::set_current_font(
  self : PdfDrawRenderState,
  identifier : String,
  size : Double,
) -> Unit raise PdfError {
  match self.stored_fontpack(identifier) {
    Some(fontpack) => {
      self.current_fontpack_identifier = identifier
      self.current_fontpack = fontpack
      self.font_size = size
    }
    None => raise SoftError("FontPack not found: " + identifier)
  }
}

///|
fn PdfDrawRenderState::text_ops(
  self : PdfDrawRenderState,
  text : String,
) -> Array[PdfContentOp] raise PdfError {
  let width_cache = pdf_draw_font_width_cache_new(self.font_size)
  let (ops, _) = self.text_ops_and_width_with_cache(text, width_cache)
  ops
}

///|
priv struct PdfDrawFontWidthCache {
  font_size : Double
  entries : Array[(Int, Array[Double])]
}

///|
fn pdf_draw_font_width_cache_new(font_size : Double) -> PdfDrawFontWidthCache {
  { font_size, entries: [] }
}

///|
fn PdfDrawFontWidthCache::widths(
  self : PdfDrawFontWidthCache,
  font_index : Int,
  font : PdfFont,
) -> Array[Double] raise PdfError {
  for entry in self.entries {
    if entry.0 == font_index {
      return entry.1
    }
  }
  let widths = pdf_type_font_widths(font, self.font_size)
  self.entries.push((font_index, widths))
  widths
}

///|
fn PdfDrawRenderState::text_ops_and_width_with_cache(
  self : PdfDrawRenderState,
  text : String,
  width_cache : PdfDrawFontWidthCache,
) -> (Array[PdfContentOp], Double) raise PdfError {
  if self.current_fontpack_identifier == "" {
    raise SoftError("FontPack not selected")
  }
  let codepoints = pdf_codepoints_of_utf8(@utf8.encode(text))
  let triples : Array[(Int, Int, PdfFont)] = Array::new(
    capacity=codepoints.length(),
  )
  let mut width = 0.0
  for codepoint in codepoints {
    match self.current_fontpack.get_char(codepoint) {
      Some((charcode, font_index, font)) => {
        let widths = width_cache.widths(font_index, font)
        if charcode < 0 || charcode >= widths.length() {
          raise FontWidthExpected
        }
        width += widths[charcode]
        triples.push((charcode, font_index, font))
      }
      None => ()
    }
  }
  let output : Array[PdfContentOp] = Array::new(capacity=triples.length() * 2)
  for run in pdf_fontpack_collate_runs(triples) {
    guard run.length() > 0 else { continue }
    let font_index = run[0].1
    let resource_name = self.font_resource_name(
      self.current_fontpack_identifier,
      font_index,
    )
    let bytes = Array::make(run.length(), b'\x00')
    for index, triple in run {
      bytes[index] = pdf_byte_of_int(triple.0)
    }
    output.push(Op_Tf(resource_name, self.font_size))
    output.push(Op_Tj(Bytes::from_array(bytes)))
  }
  (output, width)
}

///|
fn pdf_draw_decode_lossy_view(bytes : BytesView) -> String {
  let output = Array::make(bytes.length(), b'\x00')
  for index, byte in bytes {
    output[index] = byte
  }
  @utf8.decode_lossy(Bytes::from_array(output))
}

///|
fn pdf_draw_split_on_space(text : String) -> Array[String] {
  let bytes = @utf8.encode(text)
  let parts : Array[String] = Array::new(capacity=bytes.length())
  let mut part_start = 0
  let mut index = 0
  while index < bytes.length() {
    if bytes[index] == b' ' {
      parts.push(pdf_draw_decode_lossy_view(bytes[part_start:index]))
      index += 1
      part_start = index
    } else {
      index += 1
    }
  }
  parts.push(pdf_draw_decode_lossy_view(bytes[part_start:]))
  parts
}

///|
fn pdf_draw_remove_duplicate_tfs(
  ops : ArrayView[PdfContentOp],
) -> Array[PdfContentOp] {
  let output : Array[PdfContentOp] = Array::new(capacity=ops.length())
  let mut previous : PdfName? = None
  for op in ops {
    match op {
      Op_Tf(font, _) =>
        match previous {
          Some(seen) if seen == font => ()
          _ => {
            output.push(op)
            previous = Some(font)
          }
        }
      _ => output.push(op)
    }
  }
  output
}

///|
fn pdf_draw_merge_adjacent_tjs(
  ops : ArrayView[PdfContentOp],
) -> Array[PdfContentOp] {
  let output : Array[PdfContentOp] = Array::new(capacity=ops.length())
  let mut index = 0
  while index < ops.length() {
    match ops[index] {
      Op_Tj(_) => {
        let run_start = index
        let mut total_length = 0
        while index < ops.length() {
          match ops[index] {
            Op_Tj(text) => {
              total_length += text.length()
              index += 1
            }
            _ => break
          }
        }
        let bytes = Array::make(total_length, b'\x00')
        let mut position = 0
        for op_index in run_start..
              for byte in text {
                bytes[position] = byte
                position += 1
              }
            _ => ()
          }
        }
        output.push(Op_Tj(Bytes::from_array(bytes)))
      }
      _ => {
        output.push(ops[index])
        index += 1
      }
    }
  }
  output
}

///|
fn pdf_draw_clean_up_text_ops(
  ops : ArrayView[PdfContentOp],
) -> Array[PdfContentOp] {
  pdf_draw_merge_adjacent_tjs(pdf_draw_remove_duplicate_tfs(ops))
}

///|
fn PdfDrawRenderState::justify_paragraph_line(
  self : PdfDrawRenderState,
  ops : ArrayView[PdfContentOp],
  first_line : Bool,
  indent : Double,
  justification : PdfDrawJustification,
  remaining : Double,
) -> Array[PdfContentOp] {
  ignore(self)
  let output : Array[PdfContentOp] = Array::new(capacity=ops.length() + 2)
  match justification {
    PdfDrawLeft => {
      if first_line {
        output.push(Op_Td(-indent, 0.0))
      }
      output.append(ops)
      if first_line {
        output.push(Op_Td(indent, 0.0))
      }
    }
    PdfDrawRight => {
      output.push(Op_Td(-remaining, 0.0))
      output.append(ops)
      output.push(Op_Td(remaining, 0.0))
    }
    PdfDrawCentre => {
      output.push(Op_Td(-remaining / 2.0, 0.0))
      output.append(ops)
      output.push(Op_Td(remaining / 2.0, 0.0))
    }
  }
  output
}

///|
fn PdfDrawRenderState::format_paragraph(
  self : PdfDrawRenderState,
  indent : Double,
  justification : PdfDrawJustification,
  width : Double,
  text : String,
) -> Array[PdfContentOp] raise PdfError {
  let width_cache = pdf_draw_font_width_cache_new(self.font_size)
  let split_words = pdf_draw_split_on_space(text)
  let words : Array[(Array[PdfContentOp], Double)] = Array::new(
    capacity=split_words.length(),
  )
  for word in split_words {
    words.push(self.text_ops_and_width_with_cache(word, width_cache))
  }
  let (space_ops, space_width) = self.text_ops_and_width_with_cache(
    " ", width_cache,
  )
  let output : Array[PdfContentOp] = Array::new(capacity=words.length() * 2)
  let mut line_ops : Array[PdfContentOp] = Array::new(
    capacity=words.length() * 2,
  )
  let mut remaining = width
  let mut first_line = true
  let mut first_loop = true
  let mut index = 0
  while index < words.length() {
    if first_loop {
      remaining -= indent
      first_loop = false
    }
    let (word_ops, word_width) = words[index]
    if remaining == width {
      line_ops.append(word_ops)
      remaining -= word_width
      index += 1
    } else if word_width + space_width <= remaining {
      line_ops.append(space_ops)
      line_ops.append(word_ops)
      remaining -= word_width + space_width
      index += 1
    } else {
      output.push(Op_Tstar)
      output.append(
        self.justify_paragraph_line(
          line_ops, first_line, indent, justification, remaining,
        ),
      )
      first_line = false
      line_ops = []
      remaining = width
    }
  }
  output.push(Op_Tstar)
  output.append(
    self.justify_paragraph_line(
      line_ops, first_line, indent, justification, remaining,
    ),
  )
  pdf_draw_clean_up_text_ops(output)
}

///|
fn PdfDrawRenderState::paragraph_ops(
  self : PdfDrawRenderState,
  indent : Double?,
  justification : PdfDrawJustification,
  width : Double,
  paragraphs : ArrayView[String],
) -> Array[PdfContentOp] raise PdfError {
  let output : Array[PdfContentOp] = Array::new(capacity=paragraphs.length())
  let mut first = true
  for paragraph in paragraphs {
    if !first {
      match indent {
        None => output.push(Op_Tstar)
        Some(_) => ()
      }
    }
    let paragraph_indent = match indent {
      Some(value) if !first => value
      _ => 0.0
    }
    let paragraph_ops = self.format_paragraph(
      paragraph_indent, justification, width, paragraph,
    )
    if self.structure_tree_enabled && self.auto_tag {
      output.append(self.structure_tagged_ops("/P", paragraph_ops))
    } else {
      output.append(paragraph_ops)
    }
    first = false
  }
  output
}

///|
fn pdf_draw_copy_xobject_resources(
  resources : ArrayView[PdfDrawXObjectResource],
) -> Array[PdfDrawXObjectResource] {
  let output : Array[PdfDrawXObjectResource] = Array::new(
    capacity=resources.length(),
  )
  for entry in resources {
    output.push(entry)
  }
  output
}

///|
fn pdf_draw_copy_font_resources(
  resources : ArrayView[PdfDrawFontResource],
) -> Array[PdfDrawFontResource] {
  let output : Array[PdfDrawFontResource] = Array::new(
    capacity=resources.length(),
  )
  for entry in resources {
    output.push(entry)
  }
  output
}

///|
fn pdf_draw_copy_fontpacks(
  fontpacks : ArrayView[PdfDrawFontPackState],
) -> Array[PdfDrawFontPackState] {
  let output : Array[PdfDrawFontPackState] = Array::new(
    capacity=fontpacks.length(),
  )
  for entry in fontpacks {
    output.push(entry)
  }
  output
}

///|
fn pdf_draw_copy_page_names(names : ArrayView[PdfName]) -> Array[PdfName] {
  let output : Array[PdfName] = Array::new(capacity=names.length())
  for name in names {
    output.push(name)
  }
  output
}

///|
fn PdfDrawRenderState::nested_form_state(
  self : PdfDrawRenderState,
) -> PdfDrawRenderState {
  {
    resource_number: self.resource_number,
    marked_content_id: self.marked_content_id,
    extgstates: [],
    xobjects: pdf_draw_copy_xobject_resources(self.xobjects),
    form_xobjects: pdf_draw_copy_xobject_resources(self.form_xobjects),
    fonts: pdf_draw_copy_font_resources(self.fonts),
    fontpacks: pdf_draw_copy_fontpacks(self.fontpacks),
    current_fontpack_identifier: self.current_fontpack_identifier,
    current_fontpack: self.current_fontpack,
    font_size: self.font_size,
    structure_tree_enabled: self.structure_tree_enabled,
    auto_tag: self.auto_tag,
    structure_page_number: self.structure_page_number,
    structure_data: self.structure_data,
    page_names: pdf_draw_copy_page_names(self.page_names),
  }
}

///|
fn PdfDrawRenderState::adopt_resource_number(
  self : PdfDrawRenderState,
  child : PdfDrawRenderState,
) -> Unit {
  if child.resource_number > self.resource_number {
    self.resource_number = child.resource_number
  }
  if child.marked_content_id > self.marked_content_id {
    self.marked_content_id = child.marked_content_id
  }
}

///|
fn pdf_draw_resource_is_used(
  page_names : ArrayView[PdfName],
  name : PdfName,
) -> Bool {
  for page_name in page_names {
    if page_name == name {
      return true
    }
  }
  false
}

///|
/// Return the resource dictionary needed by the operations rendered so far.
pub fn PdfDrawRenderState::resources(self : PdfDrawRenderState) -> PdfObject {
  let states : Array[(PdfName, PdfObject)] = Array::new(
    capacity=self.extgstates.length(),
  )
  for entry in self.extgstates {
    states.push(
      (entry.name, PdfDictionary([(entry.kind, PdfReal(entry.value))])),
    )
  }
  let xobjects : Array[(PdfName, PdfObject)] = Array::new(
    capacity=self.xobjects.length() + self.form_xobjects.length(),
  )
  for entry in self.xobjects {
    if pdf_draw_resource_is_used(self.page_names, entry.resource_name) {
      xobjects.push((entry.resource_name, PdfIndirect(entry.object_number)))
    }
  }
  for entry in self.form_xobjects {
    if pdf_draw_resource_is_used(self.page_names, entry.resource_name) {
      xobjects.push((entry.resource_name, PdfIndirect(entry.object_number)))
    }
  }
  let fonts : Array[(PdfName, PdfObject)] = Array::new(
    capacity=self.fonts.length(),
  )
  for entry in self.fonts {
    if pdf_draw_resource_is_used(self.page_names, entry.resource_name) {
      fonts.push((entry.resource_name, PdfIndirect(entry.object_number)))
    }
  }
  let resources : Array[(PdfName, PdfObject)] = Array::new(capacity=3)
  if xobjects.length() > 0 {
    resources.push((pdf_draw_xobject_key(), PdfDictionary(xobjects)))
  }
  if states.length() > 0 {
    resources.push((pdf_draw_extgstate_key(), PdfDictionary(states)))
  }
  if fonts.length() > 0 {
    resources.push((pdf_draw_font_key(), PdfDictionary(fonts)))
  }
  PdfDictionary(resources)
}

///|
/// Source-level cpdf draw operation.
///
/// This mirrors the operation shape from `cpdfdraw.ml`. The first rendering
/// slice converts path, graphics-state, colour, matrix, and paint operations to
/// `PdfContentOp`; structure and page-level operations are represented here
/// and will be rendered by later cpdfdraw slices.
pub(all) enum PdfDrawOp {
  PdfDrawRect(Double, Double, Double, Double)
  PdfDrawBezier(Double, Double, Double, Double, Double, Double)
  PdfDrawBezier23(Double, Double, Double, Double)
  PdfDrawBezier13(Double, Double, Double, Double)
  PdfDrawTo(Double, Double)
  PdfDrawLine(Double, Double)
  PdfDrawClosePath
  PdfDrawSetFill(PdfDrawColour)
  PdfDrawSetStroke(PdfDrawColour)
  PdfDrawSetLineThickness(Double)
  PdfDrawSetLineCap(Int)
  PdfDrawSetLineJoin(Int)
  PdfDrawSetMiterLimit(Double)
  PdfDrawSetDashPattern(Array[Double], Double)
  PdfDrawMatrix(TransformMatrix)
  PdfDrawQq(Array[PdfDrawOp])
  PdfDrawFill
  PdfDrawFillEvenOdd
  PdfDrawStroke
  PdfDrawFillStroke
  PdfDrawFillStrokeEvenOdd
  PdfDrawClip
  PdfDrawClipEvenOdd
  PdfDrawFormXObject(Double, Double, Double, Double, String, Array[PdfDrawOp])
  PdfDrawUse(String)
  PdfDrawImageXObject(String, PdfObject)
  PdfDrawImage(String)
  PdfDrawNewPage
  PdfDrawOpacity(Double)
  PdfDrawSOpacity(Double)
  PdfDrawFontPack(String, PdfFontPack, Array[Int])
  PdfDrawFont(String, Double)
  PdfDrawTextSection(Array[PdfDrawOp])
  PdfDrawText(String)
  PdfDrawSpecialText(String)
  PdfDrawPara(Double?, PdfDrawJustification, Double, Array[String])
  PdfDrawNewline
  PdfDrawLeading(Double)
  PdfDrawCharSpace(Double)
  PdfDrawWordSpace(Double)
  PdfDrawTextScale(Double)
  PdfDrawRenderMode(Int)
  PdfDrawRise(Double)
  PdfDrawTag(String)
  PdfDrawEndTag
  PdfDrawSTag(String)
  PdfDrawEndSTag
  PdfDrawBeginArtifact
  PdfDrawEndArtifact
  PdfDrawNamespace(String)
  PdfDrawEltInfo(String, PdfObject)
  PdfDrawEndEltInfo(String)
  PdfDrawAutoTag(Bool)
} derive(Debug, Eq, ToJson)

///|
fn pdf_draw_colour_fill_ops(colour : PdfDrawColour) -> Array[PdfContentOp] {
  match colour {
    PdfDrawNoCol => []
    PdfDrawRGB(r, g, b) => [Op_rg(r, g, b)]
    PdfDrawGrey(g) => [Op_g(g)]
    PdfDrawCYMK(c, m, y, k) => [Op_k(c, m, y, k)]
  }
}

///|
fn pdf_draw_colour_stroke_ops(colour : PdfDrawColour) -> Array[PdfContentOp] {
  match colour {
    PdfDrawNoCol => []
    PdfDrawRGB(r, g, b) => [Op_RG(r, g, b)]
    PdfDrawGrey(g) => [Op_G(g)]
    PdfDrawCYMK(c, m, y, k) => [Op_K(c, m, y, k)]
  }
}

///|
/// Convert a cpdf content colour into the draw colour variant.
pub fn PdfContentColour::to_draw_colour(
  self : PdfContentColour,
) -> PdfDrawColour {
  match self {
    PdfContentGrey(grey) => PdfDrawGrey(grey)
    PdfContentRGB(red, green, blue) => PdfDrawRGB(red, green, blue)
    PdfContentCMYK(cyan, magenta, yellow, black) =>
      PdfDrawCYMK(cyan, magenta, yellow, black)
  }
}

///|
/// Parse a cpdf draw-control colour argument.
///
/// This ports `cpdfdrawcontrol.ml`'s `parse_colour`/`col_of_string` path while
/// preserving the draw-layer `NoCol` sentinel separately from content colours.
pub fn pdf_parse_draw_colour(text : String) -> PdfDrawColour raise PdfError {
  pdf_parse_content_colour(text).to_draw_colour()
}

///|
/// Parse a cpdf draw colour, returning `PdfDrawNoCol` on malformed input.
pub fn pdf_draw_colour_of_string(text : String) -> PdfDrawColour {
  pdf_parse_draw_colour(text) catch {
    _ => PdfDrawNoCol
  }
}

///|
fn pdf_draw_content_unsupported(
  op : PdfDrawOp,
) -> Array[PdfContentOp] raise PdfError {
  raise SoftError("Cpdfdraw: unsupported draw operation " + op.debug_name())
}

///|
fn pdf_draw_form_xobject_stream(
  bbox : (Double, Double, Double, Double),
  resources : PdfObject,
  ops : ArrayView[PdfContentOp],
) -> PdfObject raise PdfError {
  let data = pdf_content_bytes_of_ops(ops)
  let (a, b, c, d) = bbox
  pdf_stream(
    PdfDictionary([
      (pdf_draw_length_key(), PdfInteger(data.length())),
      (pdf_draw_subtype_key(), PdfNameObject(pdf_draw_form_key())),
      (pdf_draw_resources_key(), resources),
      (
        pdf_draw_bbox_key(),
        PdfArray([PdfReal(a), PdfReal(b), PdfReal(c), PdfReal(d)]),
      ),
    ]),
    StreamGot(data),
  )
}

///|
fn PdfDrawRenderState::tag_ops(
  self : PdfDrawRenderState,
  tag : String,
) -> Array[PdfContentOp] {
  let mcid = self.fresh_mcid()
  if self.structure_tree_enabled {
    self.structure_data.push(
      PdfDrawStructureMCID("/" + tag, self.structure_page_number, mcid),
    )
  }
  [
    Op_BDC(
      pdf_draw_name("/" + tag),
      PdfDictionary([(pdf_draw_mcid_key(), PdfInteger(mcid))]),
    ),
  ]
}

///|
fn PdfDrawRenderState::structure_tagged_ops(
  self : PdfDrawRenderState,
  tag : String,
  ops : ArrayView[PdfContentOp],
) -> Array[PdfContentOp] {
  let mcid = self.fresh_mcid()
  self.structure_data.push(
    PdfDrawStructureMCID(tag, self.structure_page_number, mcid),
  )
  let output : Array[PdfContentOp] = [
    Op_BDC(
      pdf_draw_name(tag),
      PdfDictionary([(pdf_draw_mcid_key(), PdfInteger(mcid))]),
    ),
  ]
  for op in ops {
    output.push(op)
  }
  output.push(Op_EMC)
  output
}

///|
fn PdfDrawSpecialTextContext::process(
  self : PdfDrawSpecialTextContext,
  text : String,
) -> String {
  pdf_addtext_process_text_with_time(
    self.time,
    text,
    pdf_addtext_replacement_pairs(self.replacement_values),
  )
}

///|
fn PdfDrawOp::to_content_ops_internal(
  self : PdfDrawOp,
  state : PdfDrawRenderState?,
  document : PdfDocument?,
  special_text_context : PdfDrawSpecialTextContext?,
) -> Array[PdfContentOp] raise PdfError {
  match self {
    PdfDrawQq(ops) => {
      let output : Array[PdfContentOp] = [Op_q]
      for op in ops {
        for
          content_op in op.to_content_ops_internal(
            state, document, special_text_context,
          ) {
          output.push(content_op)
        }
      }
      output.push(Op_Q)
      output
    }
    PdfDrawTextSection(ops) => {
      let output : Array[PdfContentOp] = [Op_BT]
      for op in ops {
        for
          content_op in op.to_content_ops_internal(
            state, document, special_text_context,
          ) {
          output.push(content_op)
        }
      }
      output.push(Op_ET)
      output
    }
    PdfDrawSetFill(colour) => pdf_draw_colour_fill_ops(colour)
    PdfDrawSetStroke(colour) => pdf_draw_colour_stroke_ops(colour)
    PdfDrawSetLineThickness(width) => [Op_w(width)]
    PdfDrawSetLineCap(cap) => [Op_J(cap)]
    PdfDrawSetLineJoin(join) => [Op_j(join)]
    PdfDrawSetMiterLimit(limit) => [Op_M(limit)]
    PdfDrawSetDashPattern(dashes, phase) => [Op_d(dashes, phase)]
    PdfDrawMatrix(matrix) => [Op_cm(matrix)]
    PdfDrawRect(x, y, w, h) => [Op_re(x, y, w, h)]
    PdfDrawBezier(x1, y1, x2, y2, x3, y3) => [Op_c(x1, y1, x2, y2, x3, y3)]
    PdfDrawBezier23(x2, y2, x3, y3) => [Op_v(x2, y2, x3, y3)]
    PdfDrawBezier13(x1, y1, x3, y3) => [Op_y(x1, y1, x3, y3)]
    PdfDrawTo(x, y) => [Op_m(x, y)]
    PdfDrawLine(x, y) => [Op_l(x, y)]
    PdfDrawClosePath => [Op_h]
    PdfDrawFill => [Op_f]
    PdfDrawFillEvenOdd => [Op_fstar]
    PdfDrawStroke => [Op_S]
    PdfDrawFillStroke => [Op_B]
    PdfDrawFillStrokeEvenOdd => [Op_Bstar]
    PdfDrawClip => [Op_W, Op_n]
    PdfDrawClipEvenOdd => [Op_Wstar, Op_n]
    PdfDrawFormXObject(a, b, c, d, source_name, ops) =>
      match (state, document) {
        (Some(state), Some(document)) => {
          let form_state = state.nested_form_state()
          let content_ops = pdf_drawops_to_content_ops_internal(
            Some(form_state),
            Some(document),
            special_text_context,
            ops,
          )
          let form_object = pdf_draw_form_xobject_stream(
            (a, b, c, d),
            form_state.resources(),
            content_ops,
          )
          state.adopt_resource_number(form_state)
          let object_number = document.add_object(form_object)
          let _ = state.register_form_xobject(source_name, object_number)
          []
        }
        _ => pdf_draw_content_unsupported(self)
      }
    PdfDrawUse(source_name) =>
      match state {
        Some(state) => [Op_Do(state.form_resource_name(source_name))]
        None => pdf_draw_content_unsupported(self)
      }
    PdfDrawImage(source_name) =>
      match state {
        Some(state) => {
          let ops = [Op_Do(state.image_resource_name(source_name))]
          if state.structure_tree_enabled && state.auto_tag {
            state.structure_tagged_ops("/Figure", ops)
          } else {
            ops
          }
        }
        None => pdf_draw_content_unsupported(self)
      }
    PdfDrawImageXObject(source_name, object) =>
      match (state, document) {
        (Some(state), Some(document)) => {
          let object_number = document.add_object(object)
          let _ = state.register_xobject(source_name, "/I", object_number)
          []
        }
        _ => pdf_draw_content_unsupported(self)
      }
    PdfDrawFontPack(identifier, fontpack, codepoints) =>
      match (state, document) {
        (Some(state), Some(document)) => {
          ignore(codepoints)
          state.register_fontpack_resources(identifier, fontpack, document)
          []
        }
        _ => pdf_draw_content_unsupported(self)
      }
    PdfDrawFont(identifier, size) =>
      match state {
        Some(state) => {
          state.set_current_font(identifier, size)
          []
        }
        None => pdf_draw_content_unsupported(self)
      }
    PdfDrawText(text) =>
      match state {
        Some(state) => {
          let ops = state.text_ops(text)
          if state.structure_tree_enabled && state.auto_tag {
            state.structure_tagged_ops("/P", ops)
          } else {
            ops
          }
        }
        None => pdf_draw_content_unsupported(self)
      }
    PdfDrawSpecialText(text) =>
      match (state, special_text_context) {
        (Some(state), Some(context)) => {
          let ops = state.text_ops(context.process(text))
          if state.structure_tree_enabled && state.auto_tag {
            state.structure_tagged_ops("/P", ops)
          } else {
            ops
          }
        }
        _ => pdf_draw_content_unsupported(self)
      }
    PdfDrawPara(indent, justification, width, paragraphs) =>
      match state {
        Some(state) =>
          state.paragraph_ops(indent, justification, width, paragraphs)
        None => pdf_draw_content_unsupported(self)
      }
    PdfDrawOpacity(value) =>
      match state {
        Some(state) =>
          [Op_gs(state.extgstate(pdf_draw_fill_alpha_key(), value))]
        None => pdf_draw_content_unsupported(self)
      }
    PdfDrawSOpacity(value) =>
      match state {
        Some(state) =>
          [Op_gs(state.extgstate(pdf_draw_stroke_alpha_key(), value))]
        None => pdf_draw_content_unsupported(self)
      }
    PdfDrawLeading(leading) => [Op_TL(leading)]
    PdfDrawCharSpace(spacing) => [Op_Tc(spacing)]
    PdfDrawWordSpace(spacing) => [Op_Tw(spacing)]
    PdfDrawTextScale(scale) => [Op_Tz(scale)]
    PdfDrawRenderMode(mode) => [Op_Tr(mode)]
    PdfDrawRise(rise) => [Op_Ts(rise)]
    PdfDrawNewline => [Op_Tstar]
    PdfDrawTag(tag) =>
      match state {
        Some(state) => state.tag_ops(tag)
        None => pdf_draw_content_unsupported(self)
      }
    PdfDrawEndTag => [Op_EMC]
    PdfDrawBeginArtifact => [Op_BMC(pdf_draw_begin_artifact())]
    PdfDrawEndArtifact => [Op_BMC(pdf_draw_end_artifact())]
    PdfDrawSTag(name) => {
      match state {
        Some(state) if state.structure_tree_enabled =>
          state.structure_data.push(PdfDrawStructureBeginTree("/" + name))
        _ => ()
      }
      []
    }
    PdfDrawEndSTag => {
      match state {
        Some(state) if state.structure_tree_enabled =>
          state.structure_data.push(PdfDrawStructureEndTree)
        _ => ()
      }
      []
    }
    PdfDrawNamespace(namespace_uri) => {
      match state {
        Some(state) if state.structure_tree_enabled =>
          state.structure_data.push(PdfDrawStructureNamespace(namespace_uri))
        _ => ()
      }
      []
    }
    PdfDrawEltInfo(key, value) => {
      match state {
        Some(state) if state.structure_tree_enabled =>
          state.structure_data.push(PdfDrawStructureElementInfo(key, value))
        _ => ()
      }
      []
    }
    PdfDrawEndEltInfo(key) => {
      match state {
        Some(state) if state.structure_tree_enabled =>
          state.structure_data.push(PdfDrawStructureEndElementInfo(key))
        _ => ()
      }
      []
    }
    PdfDrawAutoTag(value) => {
      match state {
        Some(state) => state.auto_tag = value
        None => ()
      }
      []
    }
    _ => pdf_draw_content_unsupported(self)
  }
}

///|
/// Return the cpdf diagnostic spelling for one draw operation.
pub fn PdfDrawOp::debug_name(self : PdfDrawOp) -> String {
  match self {
    PdfDrawQq(ops) => "Qq (" + pdf_string_of_drawops(ops) + ")"
    PdfDrawFormXObject(_, _, _, _, _, ops) =>
      "FormXObject (" + pdf_string_of_drawops(ops) + ")"
    PdfDrawTextSection(ops) =>
      "TextSection (" + pdf_string_of_drawops(ops) + ")"
    PdfDrawRect(_, _, _, _) => "Rect"
    PdfDrawBezier(_, _, _, _, _, _) => "Bezier"
    PdfDrawBezier23(_, _, _, _) => "Bezier23"
    PdfDrawBezier13(_, _, _, _) => "Bezier13"
    PdfDrawTo(_, _) => "To"
    PdfDrawLine(_, _) => "Line"
    PdfDrawClosePath => "ClosePath"
    PdfDrawSetFill(_) => "SetFill"
    PdfDrawSetStroke(_) => "SetStroke"
    PdfDrawSetLineThickness(_) => "SetLineThickness"
    PdfDrawSetLineCap(_) => "SetLineCap"
    PdfDrawSetLineJoin(_) => "SetLineJoin"
    PdfDrawSetMiterLimit(_) => "SetMiterLimit"
    PdfDrawSetDashPattern(_, _) => "SetDashPattern"
    PdfDrawMatrix(_) => "SetMatrix"
    PdfDrawFill => "Fill"
    PdfDrawFillEvenOdd => "FillEvenOdd"
    PdfDrawStroke => "Stroke"
    PdfDrawFillStroke => "FillStroke"
    PdfDrawFillStrokeEvenOdd => "FillStrokeEvenOdd"
    PdfDrawClip => "Clip"
    PdfDrawClipEvenOdd => "ClipEvenOdd"
    PdfDrawUse(_) => "Use"
    PdfDrawImageXObject(_, _) => "ImageXObject"
    PdfDrawImage(_) => "Image"
    PdfDrawNewPage => "NewPage"
    PdfDrawOpacity(_) => "Opacity"
    PdfDrawSOpacity(_) => "SOpacity"
    PdfDrawFontPack(name, _, _) => "FontPack " + name + " "
    PdfDrawFont(font, _) => "Font " + font + " "
    PdfDrawText(_) => "Text"
    PdfDrawSpecialText(_) => "SpecialText"
    PdfDrawPara(_, _, _, _) => "Para"
    PdfDrawNewline => "Newline"
    PdfDrawLeading(_) => "Leading"
    PdfDrawCharSpace(_) => "CharSpace"
    PdfDrawWordSpace(_) => "WordSpace"
    PdfDrawTextScale(_) => "TextScale"
    PdfDrawRenderMode(_) => "RenderMode"
    PdfDrawRise(_) => "Rise"
    PdfDrawEndTag => "EndTag"
    PdfDrawTag(name) => "Tag " + name
    PdfDrawEndSTag => "EndSTag"
    PdfDrawSTag(name) => "Tag " + name
    PdfDrawBeginArtifact => "BeginArtifact"
    PdfDrawEndArtifact => "EndArtifact"
    PdfDrawNamespace(name) => "Namespace " + name
    PdfDrawEltInfo(_, _) => "EltInfo"
    PdfDrawEndEltInfo(_) => "EndEltInfo"
    PdfDrawAutoTag(_) => "AutoTag"
  }
}

///|
/// Return cpdf's space-prefixed diagnostic string for a list of draw ops.
pub fn pdf_string_of_drawops(ops : ArrayView[PdfDrawOp]) -> String {
  let mut output = ""
  for op in ops {
    output = output + " " + op.debug_name()
  }
  output
}

///|
/// Compatibility wrapper for `PdfDrawOp::debug_name`.
pub fn pdf_string_of_drawop(op : PdfDrawOp) -> String {
  op.debug_name()
}

///|
/// Convert one cpdf draw operation to flat PDF content operators.
///
/// The currently ported rendering subset covers the direct operator mappings in
/// the front of `cpdfdraw.ml`'s `ops_of_drawop`: graphics state, matrix, path,
/// colour, paint, clip, `q`/`Q` grouping, resource-backed XObjects, fontpack
/// text, and paragraph layout. Later slices will fill in tagged-content and
/// page-level rendering.
pub fn PdfDrawOp::to_content_ops(
  self : PdfDrawOp,
) -> Array[PdfContentOp] raise PdfError {
  self.to_content_ops_internal(None, None, None)
}

///|
/// Convert one cpdf draw operation using resource allocation state.
pub fn PdfDrawOp::to_content_ops_with_state(
  self : PdfDrawOp,
  state : PdfDrawRenderState,
) -> Array[PdfContentOp] raise PdfError {
  self.to_content_ops_internal(Some(state), None, None)
}

///|
/// Convert one cpdf draw operation using resource state and special-text data.
pub fn PdfDrawOp::to_content_ops_with_context(
  self : PdfDrawOp,
  state : PdfDrawRenderState,
  context : PdfDrawSpecialTextContext,
) -> Array[PdfContentOp] raise PdfError {
  self.to_content_ops_internal(Some(state), None, Some(context))
}

///|
/// Convert one cpdf draw operation using resource state and a document for
/// indirect object allocation.
pub fn PdfDrawOp::to_content_ops_with_document(
  self : PdfDrawOp,
  state : PdfDrawRenderState,
  document : PdfDocument,
) -> Array[PdfContentOp] raise PdfError {
  self.to_content_ops_internal(Some(state), Some(document), None)
}

///|
/// Convert one cpdf draw operation using document resources and special-text data.
pub fn PdfDrawOp::to_content_ops_with_document_context(
  self : PdfDrawOp,
  state : PdfDrawRenderState,
  document : PdfDocument,
  context : PdfDrawSpecialTextContext,
) -> Array[PdfContentOp] raise PdfError {
  self.to_content_ops_internal(Some(state), Some(document), Some(context))
}

///|
fn pdf_drawops_to_content_ops_internal(
  state : PdfDrawRenderState?,
  document : PdfDocument?,
  context : PdfDrawSpecialTextContext?,
  ops : ArrayView[PdfDrawOp],
) -> Array[PdfContentOp] raise PdfError {
  let output : Array[PdfContentOp] = Array::new(capacity=ops.length())
  for op in ops {
    for content_op in op.to_content_ops_internal(state, document, context) {
      output.push(content_op)
    }
  }
  output
}

///|
/// Convert a sequence of cpdf draw operations to flat PDF content operators.
pub fn pdf_drawops_to_content_ops(
  ops : ArrayView[PdfDrawOp],
) -> Array[PdfContentOp] raise PdfError {
  pdf_drawops_to_content_ops_internal(None, None, None, ops)
}

///|
/// Compatibility wrapper for `PdfDrawOp::to_content_ops`.
pub fn pdf_drawop_to_content_ops(
  op : PdfDrawOp,
) -> Array[PdfContentOp] raise PdfError {
  op.to_content_ops()
}

///|
/// Convert a sequence of cpdf draw operations using resource allocation state.
pub fn pdf_drawops_to_content_ops_with_state(
  state : PdfDrawRenderState,
  ops : ArrayView[PdfDrawOp],
) -> Array[PdfContentOp] raise PdfError {
  pdf_drawops_to_content_ops_internal(Some(state), None, None, ops)
}

///|
/// Convert a sequence of cpdf draw operations using state and special-text data.
pub fn pdf_drawops_to_content_ops_with_context(
  state : PdfDrawRenderState,
  context : PdfDrawSpecialTextContext,
  ops : ArrayView[PdfDrawOp],
) -> Array[PdfContentOp] raise PdfError {
  pdf_drawops_to_content_ops_internal(Some(state), None, Some(context), ops)
}

///|
/// Compatibility wrapper for `PdfDrawOp::to_content_ops_with_state`.
pub fn pdf_drawop_to_content_ops_with_state(
  state : PdfDrawRenderState,
  op : PdfDrawOp,
) -> Array[PdfContentOp] raise PdfError {
  op.to_content_ops_with_state(state)
}

///|
/// Compatibility wrapper for `PdfDrawOp::to_content_ops_with_context`.
pub fn pdf_drawop_to_content_ops_with_context(
  state : PdfDrawRenderState,
  context : PdfDrawSpecialTextContext,
  op : PdfDrawOp,
) -> Array[PdfContentOp] raise PdfError {
  op.to_content_ops_with_context(state, context)
}

///|
/// Convert a sequence of cpdf draw operations using resource state and a
/// document for indirect object allocation.
pub fn pdf_drawops_to_content_ops_with_document(
  state : PdfDrawRenderState,
  document : PdfDocument,
  ops : ArrayView[PdfDrawOp],
) -> Array[PdfContentOp] raise PdfError {
  pdf_drawops_to_content_ops_internal(Some(state), Some(document), None, ops)
}

///|
/// Convert a sequence of cpdf draw operations using document resources and
/// special-text data.
pub fn pdf_drawops_to_content_ops_with_document_context(
  state : PdfDrawRenderState,
  document : PdfDocument,
  context : PdfDrawSpecialTextContext,
  ops : ArrayView[PdfDrawOp],
) -> Array[PdfContentOp] raise PdfError {
  pdf_drawops_to_content_ops_internal(
    Some(state),
    Some(document),
    Some(context),
    ops,
  )
}

///|
/// Compatibility wrapper for `PdfDrawOp::to_content_ops_with_document`.
pub fn pdf_drawop_to_content_ops_with_document(
  state : PdfDrawRenderState,
  document : PdfDocument,
  op : PdfDrawOp,
) -> Array[PdfContentOp] raise PdfError {
  op.to_content_ops_with_document(state, document)
}

///|
/// Compatibility wrapper for `PdfDrawOp::to_content_ops_with_document_context`.
pub fn pdf_drawop_to_content_ops_with_document_context(
  state : PdfDrawRenderState,
  document : PdfDocument,
  context : PdfDrawSpecialTextContext,
  op : PdfDrawOp,
) -> Array[PdfContentOp] raise PdfError {
  op.to_content_ops_with_document_context(state, document, context)
}

///|
fn pdf_draw_resource_name_number(name : PdfName) -> Int? {
  let bytes = name.bytes()
  let mut start = bytes.length()
  while start > 0 {
    let byte = bytes[start - 1].to_int()
    if byte >= 48 && byte <= 57 {
      start -= 1
    } else {
      break
    }
  }
  if start == bytes.length() {
    None
  } else {
    let mut value = 0
    for index in start.. Array[Int] {
  let output : Array[Int] = Array::new(capacity=3)
  for
    key in [
      pdf_draw_xobject_key(),
      pdf_draw_extgstate_key(),
      pdf_draw_font_key(),
    ] {
    match self.lookup_direct(key, resources) {
      Some(PdfDictionary(entries)) =>
        for entry in entries {
          match pdf_draw_resource_name_number(entry.0) {
            Some(number) => output.push(number)
            None => ()
          }
        }
      _ => ()
    }
  }
  output
}

///|
fn PdfDocument::pdf_draw_minimum_resource_number(
  self : PdfDocument,
  selected_pages : @number_set.PdfNumberSet,
) -> Int raise PdfError {
  let pages = self.pages_of_pagetree()
  let mut maximum = -1
  for index in 0.. maximum {
          maximum = number
        }
      }
    }
  }
  if maximum < 0 {
    0
  } else {
    maximum + 1
  }
}

///|
fn pdf_draw_contains_special_text(op : PdfDrawOp) -> Bool {
  match op {
    PdfDrawSpecialText(_) => true
    PdfDrawQq(ops)
    | PdfDrawTextSection(ops)
    | PdfDrawFormXObject(_, _, _, _, _, ops) =>
      pdf_drawops_contain_special_text(ops)
    _ => false
  }
}

///|
fn pdf_drawops_contain_special_text(ops : ArrayView[PdfDrawOp]) -> Bool {
  for op in ops {
    if pdf_draw_contains_special_text(op) {
      return true
    }
  }
  false
}

///|
fn pdf_draw_fixup_manual_artifacts(
  ops : ArrayView[PdfContentOp],
) -> Array[PdfContentOp] {
  let output : Array[PdfContentOp] = Array::new(capacity=ops.length())
  for op in ops {
    match op {
      Op_BMC(name) if name == pdf_draw_begin_artifact() =>
        output.push(pdf_content_begin_artifact())
      Op_BMC(name) if name == pdf_draw_end_artifact() =>
        output.push(pdf_content_end_artifact())
      _ => output.push(op)
    }
  }
  output
}

///|
fn pdf_draw_artifact_ops(
  ops : ArrayView[PdfContentOp],
  add_artifacts : Bool,
) -> Array[PdfContentOp] {
  if add_artifacts {
    pdf_content_add_artifacts(ops)
  } else {
    pdf_draw_fixup_manual_artifacts(ops)
  }
}

///|
fn pdf_draw_double_trailing_new_page(
  drawops : ArrayView[PdfDrawOp],
) -> Array[PdfDrawOp] {
  let output = [ for op in drawops => op ]
  if output.length() > 0 && output[output.length() - 1] == PdfDrawNewPage {
    output.push(PdfDrawNewPage)
  }
  output
}

///|
fn pdf_draw_split_on_new_page(
  drawops : ArrayView[PdfDrawOp],
) -> Array[Array[PdfDrawOp]] {
  let chunks : Array[Array[PdfDrawOp]] = [[]]
  for op in drawops {
    if op == PdfDrawNewPage {
      chunks.push([])
    } else {
      chunks[chunks.length() - 1].push(op)
    }
  }
  if chunks.length() > 1 && chunks[chunks.length() - 1].length() == 0 {
    ignore(chunks.pop())
  }
  chunks
}

///|
fn PdfDocument::pdf_draw_has_fresh_structure_tree(self : PdfDocument) -> Bool {
  match
    self.lookup_chain(self.trailer_dict(), [
      pdf_structure_root_key(),
      pdf_draw_struct_tree_root_key(),
    ]) {
    Some(PdfDictionary([(key, PdfNameObject(value))])) =>
      key == pdf_draw_type_key() && value == pdf_draw_struct_tree_root_key()
    _ => false
  }
}

///|
fn pdf_draw_single_page_ops(
  document : PdfDocument,
  state : PdfDrawRenderState,
  drawops : ArrayView[PdfDrawOp],
  page_number : Int,
  filename : String,
  bates : Int,
  bates_pad : Int?,
  time : PdfStrftimeTime?,
) -> Array[PdfContentOp] raise PdfError {
  let context = document.draw_special_text_context(
    page_number,
    filename~,
    bates~,
    bates_pad~,
    time~,
  )
  pdf_drawops_to_content_ops_with_document_context(
    state, document, context, drawops,
  )
}

///|
fn PdfDocument::pdf_draw_render_page_ops(
  self : PdfDocument,
  state : PdfDrawRenderState,
  selected_pages : @number_set.PdfNumberSet,
  drawops : ArrayView[PdfDrawOp],
  filename : String,
  bates : Int,
  bates_pad : Int?,
  time : PdfStrftimeTime?,
) -> Array[Array[PdfContentOp]] raise PdfError {
  let pages = self.pages_of_pagetree()
  let per_page : Array[Array[PdfContentOp]] = [ for _ in pages => [] ]
  if pdf_drawops_contain_special_text(drawops) {
    for index in 0.. PdfDocument raise PdfError {
  state.set_structure_tree_enabled(struct_tree)
  let selected_pages = @number_set.pdf_number_set(page_numbers)
  state.set_minimum_resource_number(
    self.pdf_draw_minimum_resource_number(selected_pages),
  )
  let per_page = self.pdf_draw_render_page_ops(
    state, selected_pages, drawops, filename, bates, bates_pad, time,
  )
  let draw_resources = state.resources()
  let pages = self.pages_of_pagetree()
  let rewritten : Array[PdfPage] = Array::new(capacity=pages.length())
  for index in 0.. (PdfDocument, Array[Int]) raise PdfError {
  let end_page = document.endpage()
  match page_numbers {
    [page_number] if end_page > page_number => (document, [page_number + 1])
    _ => (document.pad_after([end_page]), [end_page + 1])
  }
}

///|
let pdf_draw_standard_namespace : String = "http://iso.org/pdf/ssn"

///|
priv enum PdfDrawStructureNode {
  PdfDrawStructureNodeMCID(Int)
  PdfDrawStructureNodeItem(PdfDrawStructureItem)
}

///|
priv struct PdfDrawStructureItem {
  kind : String
  namespace_uri : String
  page_object_number : Int?
  alt : Array[(String, PdfObject)]
  children : Array[PdfDrawStructureNode]
}

///|
priv struct PdfDrawStructureBuildContext {
  mut namespace_uri : String
  element_info : Array[(String, PdfObject)]
}

///|
fn pdf_draw_structure_context_new() -> PdfDrawStructureBuildContext {
  { namespace_uri: pdf_draw_standard_namespace, element_info: [] }
}

///|
fn PdfDrawStructureBuildContext::alt_copy(
  self : PdfDrawStructureBuildContext,
) -> Array[(String, PdfObject)] {
  [
    for entry in self.element_info => entry
  ]
}

///|
fn PdfDrawStructureBuildContext::set_element_info(
  self : PdfDrawStructureBuildContext,
  key : String,
  value : PdfObject,
) -> Unit {
  for index in 0.. Unit {
  let output : Array[(String, PdfObject)] = Array::new(
    capacity=self.element_info.length(),
  )
  for entry in self.element_info {
    if entry.0 != key {
      output.push(entry)
    }
  }
  self.element_info.clear()
  for entry in output {
    self.element_info.push(entry)
  }
}

///|
fn pdf_draw_structure_page_object_number(
  page_object_numbers : ArrayView[Int],
  page_number : Int,
) -> Int? {
  if page_number >= 1 && page_number <= page_object_numbers.length() {
    Some(page_object_numbers[page_number - 1])
  } else {
    None
  }
}

///|
fn pdf_draw_make_structure_tree_items(
  data : ArrayView[PdfDrawStructureData],
  index : Ref[Int],
  context : PdfDrawStructureBuildContext,
  page_object_numbers : ArrayView[Int],
  expect_end_tree : Bool,
) -> Array[PdfDrawStructureNode] raise PdfError {
  let output : Array[PdfDrawStructureNode] = Array::new(
    capacity=data.length() - index.val,
  )
  while index.val < data.length() {
    match data[index.val] {
      PdfDrawStructureMCID(kind, page_number, mcid) => {
        output.push(
          PdfDrawStructureNodeItem({
            kind,
            namespace_uri: context.namespace_uri,
            page_object_number: pdf_draw_structure_page_object_number(
              page_object_numbers, page_number,
            ),
            alt: context.alt_copy(),
            children: [PdfDrawStructureNodeMCID(mcid)],
          }),
        )
        index.val += 1
      }
      PdfDrawStructureBeginTree(kind) => {
        let namespace_uri = context.namespace_uri
        let alt = context.alt_copy()
        index.val += 1
        let children = pdf_draw_make_structure_tree_items(
          data, index, context, page_object_numbers, true,
        )
        output.push(
          PdfDrawStructureNodeItem({
            kind,
            namespace_uri,
            page_object_number: None,
            alt,
            children,
          }),
        )
      }
      PdfDrawStructureEndTree =>
        if expect_end_tree {
          index.val += 1
          return output
        } else {
          raise SoftError("Too many -end-tags")
        }
      PdfDrawStructureNamespace(namespace_uri) => {
        context.namespace_uri = namespace_uri
        index.val += 1
      }
      PdfDrawStructureElementInfo(key, value) => {
        context.set_element_info(key, value)
        index.val += 1
      }
      PdfDrawStructureEndElementInfo(key) => {
        context.remove_element_info(key)
        index.val += 1
      }
    }
  }
  if expect_end_tree {
    raise SoftError("not enough -end-stag")
  } else {
    output
  }
}

///|
fn PdfDocument::pdf_draw_make_structure_tree(
  self : PdfDocument,
  data : ArrayView[PdfDrawStructureData],
) -> Array[PdfDrawStructureNode] raise PdfError {
  let index : Ref[Int] = { val: 0 }
  pdf_draw_make_structure_tree_items(
    data,
    index,
    pdf_draw_structure_context_new(),
    self.page_reference_numbers(),
    false,
  )
}

///|
priv struct PdfDrawNamespaceMap {
  entries : Array[(String, Int)]
}

///|
fn pdf_draw_namespace_map_new() -> PdfDrawNamespaceMap {
  { entries: [] }
}

///|
fn PdfDrawNamespaceMap::object_number(
  self : PdfDrawNamespaceMap,
  document : PdfDocument,
  namespace_uri : String,
) -> Int? {
  if namespace_uri == pdf_draw_standard_namespace {
    None
  } else {
    for entry in self.entries {
      if entry.0 == namespace_uri {
        return Some(entry.1)
      }
    }
    let number = document.add_object(
      PdfDictionary([
        (pdf_draw_ns_key(), PdfString(@ascii.encode(namespace_uri))),
      ]),
    )
    self.entries.push((namespace_uri, number))
    Some(number)
  }
}

///|
priv struct PdfDrawParentMap {
  entries : Array[(Int, Array[Int])]
}

///|
fn pdf_draw_parent_map_new() -> PdfDrawParentMap {
  { entries: [] }
}

///|
fn PdfDrawParentMap::add(
  self : PdfDrawParentMap,
  page_object_number : Int,
  structure_object_number : Int,
) -> Unit {
  for index in 0.. Array[(Int, PdfObject)] {
  [
    for entry in self.entries => {
      (
        entry.0,
        PdfArray([ for object_number in entry.1 => PdfIndirect(object_number) ]),
      )
    }
  ]
}

///|
fn PdfDocument::pdf_draw_write_structure_node(
  self : PdfDocument,
  parent_object_number : Int,
  namespaces : PdfDrawNamespaceMap,
  parent_map : PdfDrawParentMap,
  node : PdfDrawStructureNode,
) -> PdfObject raise PdfError {
  match node {
    PdfDrawStructureNodeMCID(mcid) => PdfInteger(mcid)
    PdfDrawStructureNodeItem(item) => {
      let object_number = self.add_object(PdfNull)
      match item.page_object_number {
        Some(page_object_number) =>
          parent_map.add(page_object_number, object_number)
        None => ()
      }
      let entries : Array[(PdfName, PdfObject)] = Array::new(
        capacity=item.alt.length() + 4,
      )
      for entry in item.alt {
        entries.push((pdf_draw_name("/" + entry.0), entry.1))
      }
      match item.page_object_number {
        Some(page_object_number) =>
          entries.push((pdf_draw_pg_key(), PdfIndirect(page_object_number)))
        None => ()
      }
      match namespaces.object_number(self, item.namespace_uri) {
        Some(namespace_object_number) =>
          entries.push(
            (pdf_draw_ns_key(), PdfIndirect(namespace_object_number)),
          )
        None => ()
      }
      entries.push((pdf_draw_s_key(), PdfNameObject(pdf_draw_name(item.kind))))
      entries.push((pdf_draw_p_key(), PdfIndirect(parent_object_number)))
      let children : Array[PdfObject] = Array::new(
        capacity=item.children.length(),
      )
      for child in item.children {
        children.push(
          self.pdf_draw_write_structure_node(
            object_number, namespaces, parent_map, child,
          ),
        )
      }
      entries.push((pdf_draw_k_key(), PdfArray(children)))
      self.add_object_given_number(object_number, PdfDictionary(entries))
      PdfIndirect(object_number)
    }
  }
}

///|
fn PdfDocument::pdf_draw_write_structure_tree(
  self : PdfDocument,
  data : ArrayView[PdfDrawStructureData],
  role_map : String,
) -> Unit raise PdfError {
  let tree = self.pdf_draw_make_structure_tree(data)
  let namespaces = pdf_draw_namespace_map_new()
  let parent_map = pdf_draw_parent_map_new()
  let root_object_number = self.add_object(PdfNull)
  let children : Array[PdfObject] = Array::new(capacity=tree.length())
  for node in tree {
    children.push(
      self.pdf_draw_write_structure_node(
        root_object_number, namespaces, parent_map, node,
      ),
    )
  }
  for entry in parent_map.entries {
    let page_object = self.lookup_object_or_null(entry.0)
    self.add_object_given_number(
      entry.0,
      page_object.add_dict_entry(
        pdf_draw_struct_parents_key(),
        PdfInteger(entry.0),
      ),
    )
  }
  let parent_tree_number = self.add_object(
    self.build_number_tree(parent_map.number_tree_entries()),
  )
  let root_entries : Array[(PdfName, PdfObject)] = [
    (pdf_draw_type_key(), PdfNameObject(pdf_draw_struct_tree_root_key())),
    (pdf_draw_parent_tree_key(), PdfIndirect(parent_tree_number)),
    (pdf_draw_k_key(), PdfArray(children)),
  ]
  if role_map != "" {
    root_entries.push(
      (
        pdf_draw_role_map_key(),
        pdf_parse_object_from_bytes(@ascii.encode("<<" + role_map + ">>")),
      ),
    )
  }
  if namespaces.entries.length() > 0 {
    root_entries.push(
      (
        pdf_draw_namespaces_key(),
        PdfArray([ for entry in namespaces.entries => PdfIndirect(entry.1) ]),
      ),
    )
  }
  self.add_object_given_number(root_object_number, PdfDictionary(root_entries))
  self.replace_chain(
    [pdf_structure_root_key(), pdf_draw_struct_tree_root_key()],
    PdfIndirect(root_object_number),
  )
}

///|
/// Draw cpdf draw operations on selected pages.
///
/// This ports the page orchestration from `cpdfdraw.ml`: draw operations are
/// split around `PdfDrawNewPage`, selected page ranges are advanced or padded
/// between chunks, resources are merged into page resources, manual artifact
/// back-channel markers are fixed before writing content, and `struct_tree`
/// writes a fresh structure tree from rendered MCIDs only when the document has
/// cpdf's fresh `/StructTreeRoot` placeholder. If a real structure tree is
/// already present, or no fresh placeholder exists, requested structure mode
/// artifacts the drawn content instead of replacing the tree. When `role_map`
/// is not empty, it is parsed as the body of a PDF role-map dictionary and
/// attached to the generated `/StructTreeRoot`.
pub fn PdfDocument::draw(
  self : PdfDocument,
  page_numbers : ArrayView[Int],
  drawops : ArrayView[PdfDrawOp],
  struct_tree? : Bool = false,
  fast? : Bool = false,
  underneath? : Bool = false,
  filename? : String = "",
  bates? : Int = 1,
  bates_pad? : Int? = None,
  time? : PdfStrftimeTime? = None,
  add_artifacts? : Bool = false,
  role_map? : String = "",
) -> PdfDocument raise PdfError {
  pdf_page_box_validate_page_numbers(page_numbers, self.endpage(), "draw")
  let write_structure_tree = struct_tree &&
    self.pdf_draw_has_fresh_structure_tree()
  let effective_add_artifacts = add_artifacts ||
    (struct_tree && !write_structure_tree)
  let state = pdf_draw_render_state_new()
  state.set_structure_tree_enabled(write_structure_tree)
  let chunks = pdf_draw_split_on_new_page(
    pdf_draw_double_trailing_new_page(drawops),
  )
  let mut document = self
  let mut current_pages = [ for page_number in page_numbers => page_number ]
  for index in 0.. 0 {
      if current_pages.length() > 0 {
        state.set_structure_page_number(current_pages[0])
      } else {
        state.set_structure_page_number(0)
      }
      document = document.pdf_draw_apply_chunk(
        state,
        current_pages,
        chunks[index],
        write_structure_tree,
        fast,
        underneath,
        filename,
        bates,
        bates_pad,
        time,
        effective_add_artifacts,
      )
    }
    if index + 1 < chunks.length() {
      let next = pdf_draw_next_range(document, current_pages)
      document = next.0
      current_pages = next.1
    }
  }
  if write_structure_tree {
    document.pdf_draw_write_structure_tree(state.structure_data, role_map)
  }
  document
}

///|
/// Compatibility wrapper for `PdfDocument::draw`.
pub fn pdf_draw(
  document : PdfDocument,
  page_numbers : ArrayView[Int],
  drawops : ArrayView[PdfDrawOp],
  struct_tree? : Bool = false,
  fast? : Bool = false,
  underneath? : Bool = false,
  filename? : String = "",
  bates? : Int = 1,
  bates_pad? : Int? = None,
  time? : PdfStrftimeTime? = None,
  add_artifacts? : Bool = false,
  role_map? : String = "",
) -> PdfDocument raise PdfError {
  document.draw(
    page_numbers,
    drawops,
    struct_tree~,
    fast~,
    underneath~,
    filename~,
    bates~,
    bates_pad~,
    time~,
    add_artifacts~,
    role_map~,
  )
}