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

///|
let pdf_image_image_mask_key_name : PdfName = pdf_image_name("/ImageMask")

///|
let pdf_image_im_key_name : PdfName = pdf_image_name("/IM")

///|
let pdf_image_colorspace_key_name : PdfName = pdf_image_name("/ColorSpace")

///|
let pdf_image_cs_key_name : PdfName = pdf_image_name("/CS")

///|
let pdf_image_bits_per_component_key_name : PdfName = pdf_image_name(
  "/BitsPerComponent",
)

///|
let pdf_image_bpc_key_name : PdfName = pdf_image_name("/BPC")

///|
let pdf_image_width_key_name : PdfName = pdf_image_name("/Width")

///|
let pdf_image_w_key_name : PdfName = pdf_image_name("/W")

///|
let pdf_image_height_key_name : PdfName = pdf_image_name("/Height")

///|
let pdf_image_h_key_name : PdfName = pdf_image_name("/H")

///|
let pdf_image_decode_key_name : PdfName = pdf_image_name("/Decode")

///|
let pdf_image_d_key_name : PdfName = pdf_image_name("/D")

///|
let pdf_image_decodeparms_key_name : PdfName = pdf_image_name("/DecodeParms")

///|
let pdf_image_dp_key_name : PdfName = pdf_image_name("/DP")

///|
let pdf_image_jbig2_globals_key_name : PdfName = pdf_image_name("/JBIG2Globals")

///|
let pdf_image_dctdecode_name_value : PdfName = pdf_image_name("/DCTDecode")

///|
let pdf_image_dct_name_value : PdfName = pdf_image_name("/DCT")

///|
let pdf_image_jbig2decode_name_value : PdfName = pdf_image_name("/JBIG2Decode")

///|
let pdf_image_jpxdecode_name_value : PdfName = pdf_image_name("/JPXDecode")

///|
fn pdf_image_image_mask_key() -> PdfName {
  pdf_image_image_mask_key_name
}

///|
fn pdf_image_im_key() -> PdfName {
  pdf_image_im_key_name
}

///|
fn pdf_image_colorspace_key() -> PdfName {
  pdf_image_colorspace_key_name
}

///|
fn pdf_image_cs_key() -> PdfName {
  pdf_image_cs_key_name
}

///|
fn pdf_image_bits_per_component_key() -> PdfName {
  pdf_image_bits_per_component_key_name
}

///|
fn pdf_image_bpc_key() -> PdfName {
  pdf_image_bpc_key_name
}

///|
fn pdf_image_width_key() -> PdfName {
  pdf_image_width_key_name
}

///|
fn pdf_image_w_key() -> PdfName {
  pdf_image_w_key_name
}

///|
fn pdf_image_height_key() -> PdfName {
  pdf_image_height_key_name
}

///|
fn pdf_image_h_key() -> PdfName {
  pdf_image_h_key_name
}

///|
fn pdf_image_decode_key() -> PdfName {
  pdf_image_decode_key_name
}

///|
fn pdf_image_d_key() -> PdfName {
  pdf_image_d_key_name
}

///|
fn pdf_image_decodeparms_key() -> PdfName {
  pdf_image_decodeparms_key_name
}

///|
fn pdf_image_dp_key() -> PdfName {
  pdf_image_dp_key_name
}

///|
fn pdf_image_jbig2_globals_key() -> PdfName {
  pdf_image_jbig2_globals_key_name
}

///|
fn pdf_image_dctdecode_name() -> PdfName {
  pdf_image_dctdecode_name_value
}

///|
fn pdf_image_dct_name() -> PdfName {
  pdf_image_dct_name_value
}

///|
fn pdf_image_jbig2decode_name() -> PdfName {
  pdf_image_jbig2decode_name_value
}

///|
fn pdf_image_jpxdecode_name() -> PdfName {
  pdf_image_jpxdecode_name_value
}

///|
/// Pixel layout for extracted raw image data.
pub(all) enum PdfPixelLayout {
  PdfBPP1
  PdfBPP8
  PdfBPP24
  PdfBPP48
} derive(Debug, Eq, ToJson)

///|
/// Extracted PDF image data.
///
/// JPEG, JPEG2000, and JBIG2 variants keep their encoded bytes and optional
/// decode array. `PdfJBIG2` also carries the optional `/JBIG2Globals` object
/// number. `PdfRawImage` stores width, height, layout, and decoded pixel bytes.
pub(all) enum PdfImage {
  PdfJPEG(PdfBytes, Array[Double]?)
  PdfJPEG2000(PdfBytes, Array[Double]?)
  PdfJBIG2(PdfBytes, Array[Double]?, Int?)
  PdfRawImage(Int, Int, PdfPixelLayout, PdfBytes)
} derive(Debug, Eq, ToJson)

///|
fn PdfDocument::pdf_image_is_mask(
  self : PdfDocument,
  image : PdfObject,
) -> Bool {
  match
    self.lookup_direct_or_else(
      pdf_image_image_mask_key(),
      pdf_image_im_key(),
      image,
    ) {
    Some(PdfBoolean(true)) => true
    _ => false
  }
}

///|
/// Read an image XObject colour space.
///
/// `/ImageMask true` images are treated as `PdfDeviceGray`. Otherwise this
/// reads `/ColorSpace` or `/CS`, resolving named spaces through `resources`.
pub fn PdfDocument::image_colour_space(
  self : PdfDocument,
  image : PdfObject,
  resources : PdfObject,
) -> PdfColourSpace raise PdfError {
  if self.pdf_image_is_mask(image) {
    PdfDeviceGray
  } else {
    match
      self.lookup_direct_or_else(
        pdf_image_colorspace_key(),
        pdf_image_cs_key(),
        image,
      ) {
      Some(colour_space) =>
        match self.read_colour_space(resources, colour_space) {
          Some(space) => space
          None =>
            raise PdfError::DictionaryKeyNotFound(pdf_image_colorspace_key())
        }
      None => raise PdfError::DictionaryKeyNotFound(pdf_image_colorspace_key())
    }
  }
}

///|
/// Compatibility alias for `PdfDocument::image_colour_space`.
pub fn PdfDocument::colspace(
  self : PdfDocument,
  image : PdfObject,
  resources : PdfObject,
) -> PdfColourSpace raise PdfError {
  self.image_colour_space(image, resources)
}

///|
/// Compatibility wrapper for `PdfDocument::colspace`.
pub fn pdf_image_colspace(
  document : PdfDocument,
  image : PdfObject,
  resources : PdfObject,
) -> PdfColourSpace raise PdfError {
  document.colspace(image, resources)
}

///|
/// Return an image's `/BitsPerComponent` or abbreviated `/BPC` entry.
///
/// Image masks default to `1` when no explicit value is present. The raw object
/// is returned to preserve the PDF representation.
pub fn PdfDocument::image_bits_per_component(
  self : PdfDocument,
  image : PdfObject,
) -> PdfObject? {
  match
    self.lookup_direct_or_else(
      pdf_image_bits_per_component_key(),
      pdf_image_bpc_key(),
      image,
    ) {
    Some(bits_per_component) => Some(bits_per_component)
    None =>
      if self.pdf_image_is_mask(image) {
        Some(PdfInteger(1))
      } else {
        None
      }
  }
}

///|
/// Compatibility alias for `PdfDocument::image_bits_per_component`.
pub fn PdfDocument::bpc(self : PdfDocument, image : PdfObject) -> PdfObject? {
  self.image_bits_per_component(image)
}

///|
/// Compatibility wrapper for `PdfDocument::bpc`.
pub fn pdf_image_bpc(document : PdfDocument, image : PdfObject) -> PdfObject? {
  document.bpc(image)
}

///|
fn PdfDocument::pdf_image_lookup_int_or_else(
  self : PdfDocument,
  first_key : PdfName,
  second_key : PdfName,
  image : PdfObject,
) -> Int raise PdfError {
  match self.lookup_direct_or_else(first_key, second_key, image) {
    Some(PdfInteger(value)) => value
    Some(_) => raise PdfError::NumberExpected
    None => raise PdfError::DictionaryKeyNotFound(first_key)
  }
}

///|
fn PdfDocument::pdf_image_bits_per_component_int(
  self : PdfDocument,
  image : PdfObject,
) -> Int? raise PdfError {
  match self.image_bits_per_component(image) {
    Some(PdfInteger(value)) => Some(value)
    Some(_) => raise PdfError::NumberExpected
    None => None
  }
}

///|
fn PdfDocument::pdf_image_decode_values(
  self : PdfDocument,
  image : PdfObject,
) -> Array[Double]? raise PdfError {
  match
    self.lookup_direct_or_else(pdf_image_decode_key(), pdf_image_d_key(), image) {
    Some(PdfArray(values)) => {
      let output : Array[Double] = []
      for value in values {
        output.push(self.get_number(value))
      }
      Some(output)
    }
    Some(_) => raise PdfError::NumberExpected
    None => None
  }
}

///|
fn pdf_image_decode_identity_components(components : Int) -> Array[Double] {
  let values : Array[Double] = []
  for _ in 0.. Array[Double] {
  let values : Array[Double] = [0.0, 1.0]
  for value in range {
    values.push(value)
  }
  values
}

///|
fn pdf_image_decode_defaults_for_space(
  colour_space : PdfColourSpace,
) -> Array[Double]? {
  match colour_space {
    PdfDeviceGray | PdfCalGray(_, _, _) | PdfSeparation(_, _, _) =>
      Some(pdf_image_decode_identity_components(1))
    PdfDeviceRGB | PdfCalRGB(_, _, _, _) =>
      Some(pdf_image_decode_identity_components(3))
    PdfDeviceCMYK => Some(pdf_image_decode_identity_components(4))
    PdfLab(_, _, range) => Some(pdf_image_decode_lab_defaults(range))
    PdfICCBased(_, _, range, _, _) => Some([ for value in range => value ])
    PdfIndexed(_, _) => Some(pdf_image_decode_identity_components(1))
    PdfDeviceN(names, _, _, _) =>
      Some(pdf_image_decode_identity_components(names.length()))
    PdfPattern | PdfPatternWithBaseColourSpace(_) => None
  }
}

///|
fn PdfDocument::pdf_image_decode_values_with_defaults(
  self : PdfDocument,
  resources : PdfObject,
  image : PdfObject,
) -> Array[Double]? raise PdfError {
  match self.pdf_image_decode_values(image) {
    Some(values) =>
      match
        self.lookup_direct_or_else(
          pdf_image_colorspace_key(),
          pdf_image_cs_key(),
          image,
        ) {
        None => Some(values)
        Some(_) =>
          match self.image_colour_space(image, resources) {
            PdfIndexed(_, _) => None
            _ => Some(values)
          }
      }
    None =>
      match
        self.lookup_direct_or_else(
          pdf_image_colorspace_key(),
          pdf_image_cs_key(),
          image,
        ) {
        None => None
        Some(_) =>
          pdf_image_decode_defaults_for_space(
            self.image_colour_space(image, resources),
          )
      }
  }
}

///|
fn PdfDocument::pdf_image_encoded_decode_values_with_defaults(
  self : PdfDocument,
  resources : PdfObject,
  image : PdfObject,
) -> Array[Double]? raise PdfError {
  match self.pdf_image_decode_values(image) {
    Some(values) => Some(values)
    None =>
      match
        self.lookup_direct_or_else(
          pdf_image_colorspace_key(),
          pdf_image_cs_key(),
          image,
        ) {
        None => None
        Some(_) =>
          pdf_image_decode_defaults_for_space(
            self.image_colour_space(image, resources),
          )
      }
  }
}

///|
priv enum PdfImageDecodedStream {
  PdfImageRawStream(PdfStream)
  PdfImageDctStream(PdfStream)
  PdfImageJpxStream(PdfStream)
  PdfImageJbig2Stream(PdfStream)
}

///|
fn PdfDocument::pdf_image_decode_stream_stages(
  self : PdfDocument,
  current : PdfStream,
  stages : Int,
) -> PdfImageDecodedStream raise PdfError {
  match self.pdf_stream_first_filter_direct(current.dictionary) {
    Some(filter) if filter == pdf_image_dctdecode_name() ||
      filter == pdf_image_dct_name() => PdfImageDctStream(current)
    Some(filter) if filter == pdf_image_jpxdecode_name() =>
      PdfImageJpxStream(current)
    Some(filter) if filter == pdf_image_jbig2decode_name() =>
      PdfImageJbig2Stream(current)
    Some(_) => {
      if stages >= 1024 {
        raise PdfError::FilterExpected
      }
      self.pdf_image_decode_stream_stages(
        self.pdf_decode_stream_onestage_value_direct(current),
        stages + 1,
      )
    }
    None => PdfImageRawStream(current)
  }
}

///|
fn PdfDocument::pdf_image_decode_to_image(
  self : PdfDocument,
  image : PdfObject,
) -> PdfImageDecodedStream raise PdfError {
  let current = match self.direct(image) {
    PdfStreamObject(stream) => stream
    _ => raise PdfError::ParseStreamExpected
  }
  self.pdf_image_decode_stream_stages(current, 0)
}

///|
fn PdfDocument::pdf_image_jbig2_globals(
  self : PdfDocument,
  dictionary : PdfObject,
) -> Int? {
  let first_decodeparms = match
    self.lookup_direct_or_else(
      pdf_image_decodeparms_key(),
      pdf_image_dp_key(),
      dictionary,
    ) {
    Some(PdfDictionary(_) as decodeparms) => Some(decodeparms)
    Some(PdfArray([first, ..])) =>
      match self.direct(first) {
        PdfDictionary(_) as decodeparms => Some(decodeparms)
        _ => None
      }
    _ => None
  }
  match first_decodeparms {
    Some(decodeparms) =>
      match decodeparms.lookup_immediate(pdf_image_jbig2_globals_key()) {
        Some(PdfIndirect(number)) => Some(number)
        _ => None
      }
    None => None
  }
}

///|
fn pdf_image_require_size(
  data : BytesView,
  required : Int,
) -> Unit raise PdfError {
  if data.length() < required {
    raise PdfError::EndOfInput
  }
}

///|
fn pdf_image_decode_is_identity(
  values : ArrayView[Double],
) -> Bool raise PdfError {
  if values.length() == 0 {
    true
  } else if values.length() % 2 != 0 {
    raise PdfError::FilterExpected
  } else {
    let mut identity = true
    for index in 0..<(values.length() / 2) {
      if values[index * 2] != 0.0 || values[index * 2 + 1] != 1.0 {
        identity = false
      }
    }
    identity
  }
}

///|
fn pdf_image_decode_interpolate(
  dmin : Double,
  dmax : Double,
  value : Int,
  bpc : Int,
) -> Int {
  let denominator = match bpc {
    1 => 1.0
    2 => 3.0
    4 => 15.0
    8 => 255.0
    16 => 65535.0
    _ => 1.0
  }
  (dmin + value.to_double() * (dmax - dmin) / denominator).to_int()
}

///|
fn pdf_image_apply_1bpp_decode(data : BytesView) -> PdfBytes {
  let output : Array[Byte] = []
  for value in data {
    output.push((value.to_int() ^ 0xFF).to_byte())
  }
  Bytes::from_array(output)
}

///|
fn pdf_image_apply_8bpp_decode(
  data : BytesView,
  values : ArrayView[Double],
) -> PdfBytes raise PdfError {
  let components = values.length() / 2
  let output : Array[Byte] = []
  for index in 0.. PdfBytes raise PdfError {
  match decode_values {
    None => data.to_owned()
    Some(values) =>
      if pdf_image_decode_is_identity(values) {
        data.to_owned()
      } else {
        match bpc {
          Some(1) => pdf_image_apply_1bpp_decode(data)
          Some(2) | Some(4) | Some(16) => data.to_owned()
          Some(8) => pdf_image_apply_8bpp_decode(data, values)
          _ => raise PdfError::FilterExpected
        }
      }
  }
}

///|
fn pdf_image_apply_decode(
  data : PdfBytes,
  bpc : Int?,
  decode_values : Array[Double]?,
) -> PdfBytes raise PdfError {
  match decode_values {
    None => data
    Some(values) =>
      if pdf_image_decode_is_identity(values) {
        data
      } else {
        match bpc {
          Some(1) => pdf_image_apply_decode_borrowed(data, bpc, Some(values))
          Some(2) | Some(4) | Some(16) => data
          Some(8) => pdf_image_apply_decode_borrowed(data, bpc, Some(values))
          _ => raise PdfError::FilterExpected
        }
      }
  }
}

///|
fn pdf_image_cmyk_component(channel : Int, black : Int) -> Int {
  let channel = channel.to_double()
  let black = black.to_double()
  (255.0 - double_min(255.0, channel / 255.0 * (255.0 - black) + black)).to_int()
}

///|
fn pdf_image_rgb_of_cmyk(c : Int, m : Int, y : Int, k : Int) -> (Int, Int, Int) {
  (
    pdf_image_cmyk_component(c, k),
    pdf_image_cmyk_component(m, k),
    pdf_image_cmyk_component(y, k),
  )
}

///|
fn pdf_image_gray_8bpp_as_rgb24(
  width : Int,
  height : Int,
  data : BytesView,
) -> PdfBytes raise PdfError {
  let pixels = width * height
  pdf_image_require_size(data, pixels)
  let output : Array[Byte] = []
  for index in 0.. PdfBytes raise PdfError {
  let pixels = width * height
  pdf_image_require_size(data, pixels * 4)
  let output : Array[Byte] = []
  for index in 0.. PdfBytes raise PdfError {
  let bytes_per_row = (width + 7) / 8
  pdf_image_require_size(data, bytes_per_row * height)
  let output : Array[Byte] = []
  for row in 0..> (7 - column % 8)) & 1
      let value : Byte = if bit == 1 { 255 } else { 0 }
      output.push(value)
      output.push(value)
      output.push(value)
    }
  }
  Bytes::from_array(output)
}

///|
fn pdf_image_packed_bytes_per_row(width : Int, bits_per_component : Int) -> Int {
  let row_bits = width * bits_per_component
  if row_bits % 8 == 0 {
    row_bits / 8
  } else {
    row_bits / 8 + 1
  }
}

///|
fn pdf_image_packed_sample(
  data : BytesView,
  row_offset : Int,
  column : Int,
  bits_per_component : Int,
) -> Int {
  let bit_offset = column * bits_per_component
  let byte = data[row_offset + bit_offset / 8].to_int()
  let shift = 8 - bits_per_component - bit_offset % 8
  let mask = (1 << bits_per_component) - 1
  (byte >> shift) & mask
}

///|
fn pdf_image_gray_packed_bpp_as_rgb24(
  width : Int,
  height : Int,
  bits_per_component : Int,
  data : BytesView,
) -> PdfBytes raise PdfError {
  let bytes_per_row = pdf_image_packed_bytes_per_row(width, bits_per_component)
  pdf_image_require_size(data, bytes_per_row * height)
  let max_sample = (1 << bits_per_component) - 1
  let output : Array[Byte] = []
  for row in 0.. PdfBytes raise PdfError {
  pdf_image_gray_packed_bpp_as_rgb24(width, height, 2, data)
}

///|
fn pdf_image_gray_4bpp_as_rgb24(
  width : Int,
  height : Int,
  data : BytesView,
) -> PdfBytes raise PdfError {
  pdf_image_gray_packed_bpp_as_rgb24(width, height, 4, data)
}

///|
fn pdf_image_rgb_packed_bpp_as_rgb24(
  width : Int,
  height : Int,
  bits_per_component : Int,
  data : BytesView,
) -> PdfBytes raise PdfError {
  let samples_per_row = width * 3
  let bytes_per_row = pdf_image_packed_bytes_per_row(
    samples_per_row, bits_per_component,
  )
  pdf_image_require_size(data, bytes_per_row * height)
  let max_sample = (1 << bits_per_component) - 1
  let output : Array[Byte] = []
  for row in 0.. PdfBytes raise PdfError {
  pdf_image_rgb_packed_bpp_as_rgb24(width, height, 1, data)
}

///|
fn pdf_image_rgb_2bpp_as_rgb24(
  width : Int,
  height : Int,
  data : BytesView,
) -> PdfBytes raise PdfError {
  pdf_image_rgb_packed_bpp_as_rgb24(width, height, 2, data)
}

///|
fn pdf_image_rgb_4bpp_as_rgb24(
  width : Int,
  height : Int,
  data : BytesView,
) -> PdfBytes raise PdfError {
  pdf_image_rgb_packed_bpp_as_rgb24(width, height, 4, data)
}

///|
fn pdf_image_indexed_lookup(
  table : ArrayView[(Int, Array[Int])],
  index : Int,
  components : Int,
) -> ArrayView[Int] raise PdfError {
  for entry in table {
    if entry.0 == index && entry.1.length() == components {
      break entry.1
    }
  } nobreak {
    raise PdfError::EndOfInput
  }
}

///|
fn pdf_image_rgb_indexed_value(
  output : Array[Byte],
  table : ArrayView[(Int, Array[Int])],
  index : Int,
) -> Unit raise PdfError {
  let values = pdf_image_indexed_lookup(table, index, 3)
  output.push(pdf_byte_of_int(values[0]))
  output.push(pdf_byte_of_int(values[1]))
  output.push(pdf_byte_of_int(values[2]))
}

///|
fn pdf_image_cmyk_indexed_value(
  output : Array[Byte],
  table : ArrayView[(Int, Array[Int])],
  index : Int,
) -> Unit raise PdfError {
  let values = pdf_image_indexed_lookup(table, index, 4)
  let (red, green, blue) = pdf_image_rgb_of_cmyk(
    values[0],
    values[1],
    values[2],
    values[3],
  )
  output.push(pdf_byte_of_int(red))
  output.push(pdf_byte_of_int(green))
  output.push(pdf_byte_of_int(blue))
}

///|
fn pdf_image_clamp_unit(value : Double) -> Double {
  if value > 1.0 {
    1.0
  } else if value < 0.0 {
    0.0
  } else {
    value
  }
}

///|
fn pdf_image_lab_g(value : Double) -> Double {
  if value >= 6.0 / 29.0 {
    value * value * value
  } else {
    108.0 / 841.0 * (value - 4.0 / 29.0)
  }
}

///|
fn pdf_image_xyz_of_lab(
  lightness : Double,
  a : Double,
  b : Double,
  white : PdfTristimulusPoint,
) -> (Double, Double, Double) {
  let l = (lightness + 16.0) / 116.0 + a / 500.0
  let m = (lightness + 16.0) / 116.0
  let n = (lightness + 16.0) / 116.0 - b / 200.0
  (
    pdf_image_clamp_unit(white.x * pdf_image_lab_g(l)),
    pdf_image_clamp_unit(white.y * pdf_image_lab_g(m)),
    pdf_image_clamp_unit(white.z * pdf_image_lab_g(n)),
  )
}

///|
fn pdf_image_rgb_of_xyz(
  x : Double,
  y : Double,
  z : Double,
) -> (Double, Double, Double) {
  (
    pdf_image_clamp_unit(2.5623 * x + -1.1661 * y + -0.3962 * z),
    pdf_image_clamp_unit(-1.0215 * x + 1.9778 * y + 0.0437 * z),
    pdf_image_clamp_unit(0.0752 * x + -0.2562 * y + 1.1810 * z),
  )
}

///|
fn pdf_image_lab_indexed_value(
  output : Array[Byte],
  table : ArrayView[(Int, Array[Int])],
  index : Int,
  white : PdfTristimulusPoint,
) -> Unit raise PdfError {
  let values = pdf_image_indexed_lookup(table, index, 3)
  let lightness = (values[0] - 127).to_double() / 255.0
  let a = (values[1] - 127).to_double() / 255.0
  let b = (values[2] - 127).to_double() / 255.0
  let (x, y, z) = pdf_image_xyz_of_lab(lightness, a, b, white)
  let (red, green, blue) = pdf_image_rgb_of_xyz(x, y, z)
  output.push(pdf_byte_of_int((red * 255.0).to_int()))
  output.push(pdf_byte_of_int((green * 255.0).to_int()))
  output.push(pdf_byte_of_int((blue * 255.0).to_int()))
}

///|
fn pdf_image_indexed_8bpp_as_rgb24(
  width : Int,
  height : Int,
  table : ArrayView[(Int, Array[Int])],
  data : BytesView,
) -> PdfBytes raise PdfError {
  let pixels = width * height
  pdf_image_require_size(data, pixels)
  let output : Array[Byte] = []
  for index in 0.. PdfBytes raise PdfError {
  let pixels = width * height
  pdf_image_require_size(data, pixels)
  let output : Array[Byte] = []
  for index in 0.. PdfBytes raise PdfError {
  let bytes_per_row = pdf_image_packed_bytes_per_row(width, bits_per_component)
  pdf_image_require_size(data, bytes_per_row * height)
  let output : Array[Byte] = []
  for row in 0.. PdfBytes raise PdfError {
  pdf_image_lab_indexed_packed_bpp_as_rgb24(
    width, height, 4, table, white, data,
  )
}

///|
fn pdf_image_lab_indexed_2bpp_as_rgb24(
  width : Int,
  height : Int,
  table : ArrayView[(Int, Array[Int])],
  white : PdfTristimulusPoint,
  data : BytesView,
) -> PdfBytes raise PdfError {
  pdf_image_lab_indexed_packed_bpp_as_rgb24(
    width, height, 2, table, white, data,
  )
}

///|
fn pdf_image_lab_indexed_1bpp_as_rgb24(
  width : Int,
  height : Int,
  table : ArrayView[(Int, Array[Int])],
  white : PdfTristimulusPoint,
  data : BytesView,
) -> PdfBytes raise PdfError {
  pdf_image_lab_indexed_packed_bpp_as_rgb24(
    width, height, 1, table, white, data,
  )
}

///|
fn pdf_image_cmyk_indexed_8bpp_as_rgb24(
  width : Int,
  height : Int,
  table : ArrayView[(Int, Array[Int])],
  data : BytesView,
) -> PdfBytes raise PdfError {
  let pixels = width * height
  pdf_image_require_size(data, pixels)
  let output : Array[Byte] = []
  for index in 0.. PdfBytes raise PdfError {
  let bytes_per_row = pdf_image_packed_bytes_per_row(width, bits_per_component)
  pdf_image_require_size(data, bytes_per_row * height)
  let output : Array[Byte] = []
  for row in 0.. PdfBytes raise PdfError {
  pdf_image_indexed_packed_bpp_as_rgb24(width, height, 4, table, data)
}

///|
fn pdf_image_indexed_2bpp_as_rgb24(
  width : Int,
  height : Int,
  table : ArrayView[(Int, Array[Int])],
  data : BytesView,
) -> PdfBytes raise PdfError {
  pdf_image_indexed_packed_bpp_as_rgb24(width, height, 2, table, data)
}

///|
fn pdf_image_indexed_1bpp_as_rgb24(
  width : Int,
  height : Int,
  table : ArrayView[(Int, Array[Int])],
  data : BytesView,
) -> PdfBytes raise PdfError {
  pdf_image_indexed_packed_bpp_as_rgb24(width, height, 1, table, data)
}

///|
fn pdf_image_cmyk_indexed_packed_bpp_as_rgb24(
  width : Int,
  height : Int,
  bits_per_component : Int,
  table : ArrayView[(Int, Array[Int])],
  data : BytesView,
) -> PdfBytes raise PdfError {
  let bytes_per_row = pdf_image_packed_bytes_per_row(width, bits_per_component)
  pdf_image_require_size(data, bytes_per_row * height)
  let output : Array[Byte] = []
  for row in 0.. PdfBytes raise PdfError {
  pdf_image_cmyk_indexed_packed_bpp_as_rgb24(width, height, 4, table, data)
}

///|
fn pdf_image_cmyk_indexed_2bpp_as_rgb24(
  width : Int,
  height : Int,
  table : ArrayView[(Int, Array[Int])],
  data : BytesView,
) -> PdfBytes raise PdfError {
  pdf_image_cmyk_indexed_packed_bpp_as_rgb24(width, height, 2, table, data)
}

///|
fn pdf_image_cmyk_indexed_1bpp_as_rgb24(
  width : Int,
  height : Int,
  table : ArrayView[(Int, Array[Int])],
  data : BytesView,
) -> PdfBytes raise PdfError {
  pdf_image_cmyk_indexed_packed_bpp_as_rgb24(width, height, 1, table, data)
}

///|
fn PdfDocument::pdf_image_separation_cmyk_8bpp_as_rgb24(
  self : PdfDocument,
  width : Int,
  height : Int,
  tint : PdfObject,
  data : BytesView,
) -> PdfBytes raise PdfError {
  let pixels = width * height
  pdf_image_require_size(data, pixels)
  let tint_function = self.parse_function(tint)
  let output : Array[Byte] = []
  for index in 0.. PdfBytes raise PdfError {
  match (colour_space, bpc) {
    (PdfDeviceRGB | PdfCalRGB(_, _, _, _), Some(8)) => {
      let required = width * height * 3
      pdf_image_require_size(data, required)
      data[:required].to_owned()
    }
    (PdfDeviceRGB | PdfCalRGB(_, _, _, _), Some(1)) =>
      pdf_image_rgb_1bpp_as_rgb24(width, height, data)
    (PdfDeviceRGB | PdfCalRGB(_, _, _, _), Some(2)) =>
      pdf_image_rgb_2bpp_as_rgb24(width, height, data)
    (PdfDeviceRGB | PdfCalRGB(_, _, _, _), Some(4)) =>
      pdf_image_rgb_4bpp_as_rgb24(width, height, data)
    (PdfDeviceGray | PdfCalGray(_, _, _), Some(8)) =>
      pdf_image_gray_8bpp_as_rgb24(width, height, data)
    (PdfDeviceCMYK, Some(8)) =>
      pdf_image_cmyk_8bpp_as_rgb24(width, height, data)
    (PdfDeviceGray | PdfCalGray(_, _, _), Some(2)) =>
      pdf_image_gray_2bpp_as_rgb24(width, height, data)
    (PdfDeviceGray | PdfCalGray(_, _, _), Some(4)) =>
      pdf_image_gray_4bpp_as_rgb24(width, height, data)
    (PdfIndexed(base, table), Some(1)) =>
      match pdf_space_basic_table_colour_space(base) {
        PdfDeviceRGB | PdfCalRGB(_, _, _, _) =>
          pdf_image_indexed_1bpp_as_rgb24(width, height, table, data)
        PdfDeviceCMYK =>
          pdf_image_cmyk_indexed_1bpp_as_rgb24(width, height, table, data)
        PdfLab(white, _, _) =>
          pdf_image_lab_indexed_1bpp_as_rgb24(width, height, table, white, data)
        _ => raise PdfError::FilterExpected
      }
    (PdfIndexed(base, table), Some(8)) =>
      match pdf_space_basic_table_colour_space(base) {
        PdfDeviceRGB | PdfCalRGB(_, _, _, _) =>
          pdf_image_indexed_8bpp_as_rgb24(width, height, table, data)
        PdfDeviceCMYK =>
          pdf_image_cmyk_indexed_8bpp_as_rgb24(width, height, table, data)
        PdfLab(white, _, _) =>
          pdf_image_lab_indexed_8bpp_as_rgb24(width, height, table, white, data)
        _ => raise PdfError::FilterExpected
      }
    (PdfIndexed(base, table), Some(2)) =>
      match pdf_space_basic_table_colour_space(base) {
        PdfDeviceRGB | PdfCalRGB(_, _, _, _) =>
          pdf_image_indexed_2bpp_as_rgb24(width, height, table, data)
        PdfDeviceCMYK =>
          pdf_image_cmyk_indexed_2bpp_as_rgb24(width, height, table, data)
        PdfLab(white, _, _) =>
          pdf_image_lab_indexed_2bpp_as_rgb24(width, height, table, white, data)
        _ => raise PdfError::FilterExpected
      }
    (PdfIndexed(base, table), Some(4)) =>
      match pdf_space_basic_table_colour_space(base) {
        PdfDeviceRGB | PdfCalRGB(_, _, _, _) =>
          pdf_image_indexed_4bpp_as_rgb24(width, height, table, data)
        PdfDeviceCMYK =>
          pdf_image_cmyk_indexed_4bpp_as_rgb24(width, height, table, data)
        PdfLab(white, _, _) =>
          pdf_image_lab_indexed_4bpp_as_rgb24(width, height, table, white, data)
        _ => raise PdfError::FilterExpected
      }
    (_, Some(1)) => pdf_image_1bpp_as_rgb24(width, height, data)
    (PdfSeparation(_, PdfDeviceCMYK, tint), Some(8)) =>
      self.pdf_image_separation_cmyk_8bpp_as_rgb24(width, height, tint, data)
    (PdfICCBased(_, alternate, _, _, _), _) =>
      self.pdf_image_raw_colour_space_as_rgb24(
        width, height, alternate, bpc, data,
      )
    _ => raise PdfError::FilterExpected
  }
}

///|
fn PdfDocument::pdf_image_raw_colour_space_as_rgb24_owned(
  self : PdfDocument,
  width : Int,
  height : Int,
  colour_space : PdfColourSpace,
  bpc : Int?,
  data : PdfBytes,
) -> PdfBytes raise PdfError {
  match (colour_space, bpc) {
    (PdfDeviceRGB | PdfCalRGB(_, _, _, _), Some(8)) => {
      let required = width * height * 3
      pdf_image_require_size(data, required)
      if data.length() == required {
        data
      } else {
        data[:required].to_owned()
      }
    }
    _ =>
      self.pdf_image_raw_colour_space_as_rgb24(
        width, height, colour_space, bpc, data,
      )
  }
}

///|
fn PdfDocument::pdf_image_raw_8bpp_as_rgb24_owned(
  self : PdfDocument,
  resources : PdfObject,
  width : Int,
  height : Int,
  dictionary : PdfObject,
  data : PdfBytes,
) -> PdfBytes raise PdfError {
  self.pdf_image_raw_colour_space_as_rgb24_owned(
    width,
    height,
    self.image_colour_space(dictionary, resources),
    self.pdf_image_bits_per_component_int(dictionary),
    data,
  )
}

///|
/// Extract an image as either encoded image bytes or 24bpp RGB data.
///
/// DCT/JPEG, JPX/JPEG2000, and JBIG2 streams are returned still encoded.
/// Other supported image streams are decoded, have supported `/Decode` arrays
/// applied, and are converted to `PdfRawImage(width, height, PdfBPP24, data)`
/// where `data` is RGBRGB byte order.
pub fn PdfDocument::get_image_24bpp(
  self : PdfDocument,
  resources : PdfObject,
  image : PdfObject,
) -> PdfImage raise PdfError {
  let decoded = self.pdf_image_decode_to_image(image)
  let decoded_stream = match decoded {
    PdfImageRawStream(stream)
    | PdfImageDctStream(stream)
    | PdfImageJpxStream(stream)
    | PdfImageJbig2Stream(stream) => stream
  }
  let width = self.pdf_image_lookup_int_or_else(
    pdf_image_width_key(),
    pdf_image_w_key(),
    decoded_stream.dictionary,
  )
  let height = self.pdf_image_lookup_int_or_else(
    pdf_image_height_key(),
    pdf_image_h_key(),
    decoded_stream.dictionary,
  )
  let data = pdf_stream_data_bytes(decoded_stream.data)
  match decoded {
    PdfImageDctStream(_) =>
      PdfJPEG(
        data,
        self.pdf_image_encoded_decode_values_with_defaults(
          resources,
          decoded_stream.dictionary,
        ),
      )
    PdfImageJpxStream(_) =>
      PdfJPEG2000(
        data,
        self.pdf_image_encoded_decode_values_with_defaults(
          resources,
          decoded_stream.dictionary,
        ),
      )
    PdfImageJbig2Stream(_) =>
      PdfJBIG2(
        data,
        self.pdf_image_encoded_decode_values_with_defaults(
          resources,
          decoded_stream.dictionary,
        ),
        self.pdf_image_jbig2_globals(decoded_stream.dictionary),
      )
    PdfImageRawStream(_) => {
      let decoded_data = pdf_image_apply_decode(
        data,
        self.pdf_image_bits_per_component_int(decoded_stream.dictionary),
        self.pdf_image_decode_values_with_defaults(
          resources,
          decoded_stream.dictionary,
        ),
      )
      PdfRawImage(
        width,
        height,
        PdfBPP24,
        self.pdf_image_raw_8bpp_as_rgb24_owned(
          resources,
          width,
          height,
          decoded_stream.dictionary,
          decoded_data,
        ),
      )
    }
  }
}

///|
/// Compatibility wrapper for `PdfDocument::get_image_24bpp`.
pub fn pdf_get_image_24bpp(
  document : PdfDocument,
  resources : PdfObject,
  image : PdfObject,
) -> PdfImage raise PdfError {
  document.get_image_24bpp(resources, image)
}