///|
let pdf_png_ihdr_chunk_type : UInt = 0x49484452U

///|
let pdf_png_idat_chunk_type : UInt = 0x49444154U

///|
let pdf_png_plte_chunk_type : UInt = 0x504C5445U

///|
let pdf_png_trns_chunk_type : UInt = 0x74524E53U

///|
let pdf_png_max_int64 : Int64 = 2147483647L

///|
/// Parsed PNG metadata and concatenated IDAT payload.
///
/// This mirrors the small `Cpdfpng` surface needed for image insertion,
/// with the compressed IDAT bytes preserved. `idat` is always a
/// *non-interlaced* datastream — an Adam7 source is rebuilt into the
/// equivalent sequential one when it is read, so consumers never see
/// interlacing. `palette` carries the PLTE payload (RGB triples) for
/// colour type 3 and is `None` for every other type.
pub(all) struct PdfPNG {
  width : Int
  height : Int
  bit_depth : Int
  color_type : Int
  idat : @core.PdfBytes
  palette : @core.PdfBytes?
} derive(Debug, Eq, ToJson)

///|
fn pdf_png_u32_to_int(
  value : UInt,
  context : String,
) -> Int raise @core.PdfError {
  let wide = value.to_int64()
  if wide > pdf_png_max_int64 {
    raise BadPNG(context)
  }
  wide.to_int()
}

///|
fn pdf_png_read_chunk(
  cursor : @core.ByteCursor,
) -> (UInt, @core.PdfBytes) raise @core.PdfError {
  let length = pdf_png_u32_to_int(
    byte_cursor_read_u32_be(cursor),
    "read_png: chunk too large",
  )
  let chunk_type = byte_cursor_read_u32_be(cursor)
  let chunk_data = cursor.read_bytes(length)
  ignore(byte_cursor_read_u32_be(cursor))
  (chunk_type, chunk_data)
}

///|
fn pdf_png_concat(chunks : ArrayView[@core.PdfBytes]) -> @core.PdfBytes {
  let mut length = 0
  for chunk in chunks {
    length += chunk.length()
  }
  let output = Array::make(length, b'\x00')
  let mut position = 0
  for chunk in chunks {
    for index in 0.. PdfPNG raise @core.PdfError {
  let cursor = @core.byte_cursor_of_view(data, source="png")
  try! cursor.seek(8)
  let (first_chunk_type, first_chunk_data) = pdf_png_read_chunk(cursor)
  if first_chunk_type != pdf_png_ihdr_chunk_type {
    raise BadPNG("read_png: first table not IHDR")
  }
  let header = @core.byte_cursor_of_view(first_chunk_data, source="png IHDR")
  let width = pdf_png_u32_to_int(
    byte_cursor_read_u32_be(header),
    "read_png: width too large",
  )
  let height = pdf_png_u32_to_int(
    byte_cursor_read_u32_be(header),
    "read_png: height too large",
  )
  let bit_depth = match header.read_byte() {
    Some(byte) => byte.to_int()
    None => raise EndOfInput
  }
  let color_type = match header.read_byte() {
    Some(byte) => byte.to_int()
    None => raise EndOfInput
  }
  if color_type != 0 &&
    color_type != 2 &&
    color_type != 3 &&
    color_type != 4 &&
    color_type != 6 {
    raise BadPNG("read_png: unknown colour type " + color_type.to_string())
  }
  // Palette depth is validated here even on the sequential path (which
  // historically checks no depths): indices cap at one byte, and a
  // 16-bit depth passed through would emit an /Indexed image PDF forbids
  // rather than fail.
  if color_type == 3 &&
    bit_depth != 1 &&
    bit_depth != 2 &&
    bit_depth != 4 &&
    bit_depth != 8 {
    raise BadPNG(
      "read_png: bit depth " +
      bit_depth.to_string() +
      " invalid for a palette image",
    )
  }
  match header.read_byte() {
    Some(_) => ()
    None => raise EndOfInput
  }
  match header.read_byte() {
    Some(_) => ()
    None => raise EndOfInput
  }
  let interlace_method = match header.read_byte() {
    Some(byte) => byte.to_int()
    None => raise EndOfInput
  }
  if interlace_method != 0 && interlace_method != 1 {
    raise BadPNG(
      "read_png: unknown interlace method " + interlace_method.to_string(),
    )
  }
  let idat_chunks : Array[@core.PdfBytes] = []
  let mut palette : @core.PdfBytes? = None
  let mut trns_entries : Int? = None
  let mut done = false
  while !done {
    try pdf_png_read_chunk(cursor) catch {
      _ => done = true
    } noraise {
      (chunk_type, chunk_data) =>
        if chunk_type == pdf_png_idat_chunk_type {
          idat_chunks.push(chunk_data)
        } else if chunk_type == pdf_png_plte_chunk_type {
          palette = Some(chunk_data)
        } else if chunk_type == pdf_png_trns_chunk_type && color_type == 3 {
          // A palette with real per-entry alpha would need the
          // transparency rebuilt as an /SMask, which forces a full decode
          // this passthrough deliberately avoids. Refusing keeps the loss
          // visible (the caller reports the image unreadable) instead of
          // drawing it opaque, which would be quietly wrong.
          //
          // A tRNS whose entries are all 255 declares no transparency at
          // all, though — every listed entry is fully opaque and PNG
          // defines the unlisted rest as opaque too — so that common
          // encoder artefact takes the normal path rather than losing the
          // picture. tRNS on colour types 0/2 (a transparent colour key)
          // stays ignored, as it always was.
          for index in 0.. {
      if plte.length() == 0 || plte.length() % 3 != 0 || plte.length() > 768 {
        raise BadPNG(
          "read_png: malformed palette of " +
          plte.length().to_string() +
          " bytes",
        )
      }
      // tRNS may not name more entries than the palette has. Checked
      // here rather than at the chunk, because the scan is
      // order-independent and the palette may not have been seen yet.
      match trns_entries {
        Some(entries) =>
          if entries > plte.length() / 3 {
            raise BadPNG("read_png: tRNS longer than the palette")
          }
        None => ()
      }
      Some(plte)
    }
    (None, 3) => raise BadPNG("read_png: palette image without a PLTE chunk")
    // PLTE on a truecolour image is a quantisation hint; on greyscale it
    // is invalid. Neither affects how the pixels decode, so it is dropped
    // rather than carried.
    (_, _) => None
  }
  let concatenated = pdf_png_concat(idat_chunks)
  let idat = if interlace_method == 1 {
    pdf_png_deinterlace(width, height, bit_depth, color_type, concatenated)
  } else {
    concatenated
  }
  { width, height, bit_depth, color_type, idat, palette, }
}

///|
/// Read a PNG from a byte view, de-interlacing an Adam7 image into a
/// sequential IDAT stream. Palette images carry their PLTE; real palette
/// transparency (a tRNS entry below 255) is refused rather than dropped.
pub fn pdf_read_png_view(data : BytesView) -> PdfPNG raise @core.PdfError {
  pdf_read_png_view_inner(data) catch {
    BadPNG(message) => raise BadPNG(message)
    EndOfInput => raise BadPNG("read_png: truncated input")
    InvalidReadLength(length) =>
      raise BadPNG("read_png: invalid chunk length " + length.to_string())
    error => raise error
  }
}

///|
/// Read a PNG from owned bytes, de-interlacing an Adam7 image into a
/// sequential IDAT stream. Palette images carry their PLTE; real palette
/// transparency (a tRNS entry below 255) is refused rather than dropped.
pub fn pdf_read_png(data : @core.PdfBytes) -> PdfPNG raise @core.PdfError {
  pdf_read_png_view(data)
}