///|
fn pdf_text_hex_digit_value(byte : Byte) -> Int? {
  let value = byte.to_int()
  if value >= 48 && value <= 57 {
    Some(value - 48)
  } else if value >= 65 && value <= 70 {
    Some(value - 55)
  } else if value >= 97 && value <= 102 {
    Some(value - 87)
  } else {
    None
  }
}

///|
fn pdf_text_parse_hex_bytes(
  line : BytesView,
  start : Int,
  stop : Int,
) -> PdfBytes? {
  guard stop >= start else { return None }
  let mut digit_count = 0
  let mut scan = start
  while scan < stop {
    let value = line[scan].to_int()
    if pdf_is_whitespace_byte(value) {
      ()
    } else {
      match pdf_text_hex_digit_value(line[scan]) {
        Some(_) => digit_count += 1
        None => return None
      }
    }
    scan += 1
  }
  let output = Array::make((digit_count + 1) / 2, b'\x00')
  let mut high : Int? = None
  let mut position = 0
  let mut index = start
  while index < stop {
    let value = line[index].to_int()
    if pdf_is_whitespace_byte(value) {
      ()
    } else {
      match pdf_text_hex_digit_value(line[index]) {
        Some(nibble) =>
          match high {
            Some(high_nibble) => {
              output[position] = (high_nibble * 16 + nibble).to_byte()
              position += 1
              high = None
            }
            None => high = Some(nibble)
          }
        None => ()
      }
    }
    index += 1
  }
  if high is Some(high_nibble) {
    output[position] = (high_nibble * 16).to_byte()
  }
  Some(Bytes::from_array(output))
}

///|
fn pdf_text_skip_cmap_whitespace(line : BytesView, start : Int) -> Int {
  let mut index = start
  while index < line.length() && pdf_is_whitespace_byte(line[index].to_int()) {
    index += 1
  }
  index
}

///|
fn pdf_text_line_contains_ascii(line : BytesView, word : String) -> Bool {
  pdf_text_index_of_ascii(line, word) != None
}

///|
fn pdf_text_index_of_ascii(line : BytesView, word : String) -> Int? {
  let needle = @ascii.encode(word)
  guard needle.length() > 0 && line.length() >= needle.length() else {
    return None
  }
  for index in 0..<=(line.length() - needle.length()) {
    let mut matched = true
    for offset in 0.. Int? {
  guard start < line.length() else { return None }
  match pdf_text_index_of_ascii(line[start:], word) {
    Some(index) => Some(start + index)
    None => None
  }
}

///|
fn pdf_text_after_ascii(line : BytesView, word : String) -> BytesView? {
  match pdf_text_index_of_ascii(line, word) {
    Some(index) => {
      let word_length = @ascii.encode(word).length()
      Some(line[index + word_length:])
    }
    None => None
  }
}

///|
fn pdf_text_view_equals_ascii(view : BytesView, text : String) -> Bool {
  let expected = @ascii.encode(text)
  guard view.length() == expected.length() else { return false }
  for index in 0.. (Int, Int)? {
  let markers = [
    "endbfchar", "endbfrange", "endcidchar", "endcidrange", "endnotdefchar", "endnotdefrange",
    "endcodespacerange",
  ]
  let mut best : (Int, Int)? = None
  for marker in markers {
    match pdf_text_index_of_ascii_from(line, marker, start) {
      Some(index) => {
        let length = @ascii.encode(marker).length()
        match best {
          Some((best_index, _)) if best_index <= index => ()
          _ => best = Some((index, length))
        }
      }
      None => ()
    }
  }
  best
}

///|
fn pdf_text_cmap_section_line_views(line : BytesView) -> Array[BytesView] {
  let views : Array[BytesView] = Array::new(capacity=line.length())
  let mut start = 0
  while start < line.length() {
    match pdf_text_cmap_end_marker(line, start) {
      Some((index, length)) => {
        let stop = index + length
        views.push(line[start:stop])
        start = stop
      }
      None => {
        views.push(line[start:])
        break
      }
    }
  }
  views
}

///|
fn pdf_text_cmap_comment_start(line : BytesView) -> Int {
  let mut in_literal = false
  let mut escaped = false
  let mut literal_depth = 0
  for index in 0.. PdfBytes {
  let stop = pdf_text_cmap_comment_start(line)
  let output = Array::make(stop, b'\x00')
  for index in 0.. Array[PdfBytes]? {
  let groups : Array[PdfBytes] = Array::new(capacity=line.length() / 2)
  let mut index = 0
  while index < line.length() {
    index = pdf_text_skip_cmap_whitespace(line, index)
    if index >= line.length() {
      break
    }
    if line[index].to_int() != 60 {
      break
    }
    let start = index + 1
    let mut stop = start
    while stop < line.length() && line[stop].to_int() != 62 {
      stop += 1
    }
    guard stop < line.length() else { return None }
    match pdf_text_parse_hex_bytes(line, start, stop) {
      Some(bytes) => groups.push(bytes)
      None => return None
    }
    index = stop + 1
  }
  if groups.length() == 0 {
    None
  } else {
    Some(groups)
  }
}

///|
fn pdf_text_parse_cmap_hex_group_at(
  line : BytesView,
  start : Int,
) -> (PdfBytes, Int)? {
  let index = pdf_text_skip_cmap_whitespace(line, start)
  guard index < line.length() && line[index].to_int() == 60 else { return None }
  let body_start = index + 1
  let mut stop = body_start
  while stop < line.length() && line[stop].to_int() != 62 {
    stop += 1
  }
  guard stop < line.length() else { return None }
  match pdf_text_parse_hex_bytes(line, body_start, stop) {
    Some(bytes) => Some((bytes, stop + 1))
    None => None
  }
}

///|
fn pdf_text_bytes_to_int(bytes : BytesView) -> Int? {
  guard bytes.length() > 0 else { return None }
  let mut value = 0
  for byte in bytes {
    value = (value << 8) | byte.to_int()
  }
  Some(value)
}

///|
fn pdf_text_parse_cmap_ascii_int_at(
  line : BytesView,
  start : Int,
) -> (Int, Int)? {
  let index = pdf_text_skip_cmap_whitespace(line, start)
  guard index < line.length() else { return None }
  let mut stop = index
  if line[stop].to_int() == 43 || line[stop].to_int() == 45 {
    stop += 1
  }
  let digit_start = stop
  while stop < line.length() {
    let value = line[stop].to_int()
    if value >= 48 && value <= 57 {
      stop += 1
    } else {
      break
    }
  }
  guard stop > digit_start else { return None }
  try pdf_parse_ascii_int_view(line[index:stop]) catch {
    _ => None
  } noraise {
    value => Some((value, stop))
  }
}

///|
fn pdf_text_index_of_byte(line : BytesView, target : Int, start : Int) -> Int? {
  let mut index = start
  while index < line.length() {
    if line[index].to_int() == target {
      break Some(index)
    }
    index += 1
  } nobreak {
    None
  }
}

///|
fn pdf_text_cmap_contains_whitespace(data : BytesView) -> Bool {
  for byte in data {
    if pdf_is_whitespace_byte(byte.to_int()) {
      break true
    }
  } nobreak {
    false
  }
}

///|
fn pdf_text_cmap_without_whitespace(data : BytesView) -> PdfBytes {
  let mut length = 0
  for byte in data {
    if !pdf_is_whitespace_byte(byte.to_int()) {
      length += 1
    }
  }
  let output = Array::make(length, b'\x00')
  let mut position = 0
  for byte in data {
    if !pdf_is_whitespace_byte(byte.to_int()) {
      output[position] = byte
      position += 1
    }
  }
  Bytes::from_array(output)
}

///|
let pdf_text_cmap_split_begin_marker : PdfBytes = b"begin"

///|
let pdf_text_cmap_split_end_marker : PdfBytes = b"end"

///|
let pdf_text_cmap_split_suffixes : ReadOnlyArray[PdfBytes] = [
  b"bfchar", b"bfrange", b"cidchar", b"cidrange", b"notdefchar", b"notdefrange",
  b"codespacerange",
]

///|
fn pdf_text_cmap_starts_with_at(
  data : BytesView,
  start : Int,
  expected : BytesView,
) -> Bool {
  if start < 0 || start + expected.length() > data.length() {
    false
  } else {
    for offset in 0.. Bool {
  guard pdf_text_cmap_starts_with_at(data, start, prefix) else { return false }
  let whitespace_start = start + prefix.length()
  let mut suffix_start = whitespace_start
  while suffix_start < data.length() &&
        pdf_is_whitespace_byte(data[suffix_start].to_int()) {
    suffix_start += 1
  }
  guard suffix_start > whitespace_start else { return false }
  for suffix in pdf_text_cmap_split_suffixes {
    if pdf_text_cmap_starts_with_at(data, suffix_start, suffix) {
      return true
    }
  }
  false
}

///|
fn pdf_text_cmap_has_split_marker(data : BytesView) -> Bool {
  for index in 0..