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

///|
let pdf_ocg_root_key_name : PdfName = pdf_ocg_name("/Root")

///|
fn pdf_ocg_root_key() -> PdfName {
  pdf_ocg_root_key_name
}

///|
let pdf_ocg_ocproperties_key_name : PdfName = pdf_ocg_name("/OCProperties")

///|
fn pdf_ocg_ocproperties_key() -> PdfName {
  pdf_ocg_ocproperties_key_name
}

///|
let pdf_ocg_ocgs_key_name : PdfName = pdf_ocg_name("/OCGs")

///|
fn pdf_ocg_ocgs_key() -> PdfName {
  pdf_ocg_ocgs_key_name
}

///|
let pdf_ocg_d_key_name : PdfName = pdf_ocg_name("/D")

///|
fn pdf_ocg_d_key() -> PdfName {
  pdf_ocg_d_key_name
}

///|
let pdf_ocg_configs_key_name : PdfName = pdf_ocg_name("/Configs")

///|
fn pdf_ocg_configs_key() -> PdfName {
  pdf_ocg_configs_key_name
}

///|
let pdf_ocg_type_key_name : PdfName = pdf_ocg_name("/Type")

///|
fn pdf_ocg_type_key() -> PdfName {
  pdf_ocg_type_key_name
}

///|
let pdf_ocg_name_key_name : PdfName = pdf_ocg_name("/Name")

///|
fn pdf_ocg_name_key() -> PdfName {
  pdf_ocg_name_key_name
}

///|
let pdf_ocg_intent_key_name : PdfName = pdf_ocg_name("/Intent")

///|
fn pdf_ocg_intent_key() -> PdfName {
  pdf_ocg_intent_key_name
}

///|
let pdf_ocg_usage_key_name : PdfName = pdf_ocg_name("/Usage")

///|
fn pdf_ocg_usage_key() -> PdfName {
  pdf_ocg_usage_key_name
}

///|
let pdf_ocg_creatorinfo_key_name : PdfName = pdf_ocg_name("/CreatorInfo")

///|
fn pdf_ocg_creatorinfo_key() -> PdfName {
  pdf_ocg_creatorinfo_key_name
}

///|
let pdf_ocg_creator_key_name : PdfName = pdf_ocg_name("/Creator")

///|
fn pdf_ocg_creator_key() -> PdfName {
  pdf_ocg_creator_key_name
}

///|
let pdf_ocg_subtype_key_name : PdfName = pdf_ocg_name("/Subtype")

///|
fn pdf_ocg_subtype_key() -> PdfName {
  pdf_ocg_subtype_key_name
}

///|
let pdf_ocg_language_key_name : PdfName = pdf_ocg_name("/Language")

///|
fn pdf_ocg_language_key() -> PdfName {
  pdf_ocg_language_key_name
}

///|
let pdf_ocg_lang_key_name : PdfName = pdf_ocg_name("/Lang")

///|
fn pdf_ocg_lang_key() -> PdfName {
  pdf_ocg_lang_key_name
}

///|
let pdf_ocg_preferred_key_name : PdfName = pdf_ocg_name("/Preferred")

///|
fn pdf_ocg_preferred_key() -> PdfName {
  pdf_ocg_preferred_key_name
}

///|
let pdf_ocg_export_key_name : PdfName = pdf_ocg_name("/Export")

///|
fn pdf_ocg_export_key() -> PdfName {
  pdf_ocg_export_key_name
}

///|
let pdf_ocg_export_state_key_name : PdfName = pdf_ocg_name("/ExportState")

///|
fn pdf_ocg_export_state_key() -> PdfName {
  pdf_ocg_export_state_key_name
}

///|
let pdf_ocg_zoom_key_name : PdfName = pdf_ocg_name("/Zoom")

///|
fn pdf_ocg_zoom_key() -> PdfName {
  pdf_ocg_zoom_key_name
}

///|
let pdf_ocg_min_key_name : PdfName = pdf_ocg_name("/min")

///|
fn pdf_ocg_min_key() -> PdfName {
  pdf_ocg_min_key_name
}

///|
let pdf_ocg_max_key_name : PdfName = pdf_ocg_name("/max")

///|
fn pdf_ocg_max_key() -> PdfName {
  pdf_ocg_max_key_name
}

///|
let pdf_ocg_print_key_name : PdfName = pdf_ocg_name("/Print")

///|
fn pdf_ocg_print_key() -> PdfName {
  pdf_ocg_print_key_name
}

///|
let pdf_ocg_print_state_key_name : PdfName = pdf_ocg_name("/PrintState")

///|
fn pdf_ocg_print_state_key() -> PdfName {
  pdf_ocg_print_state_key_name
}

///|
let pdf_ocg_view_key_name : PdfName = pdf_ocg_name("/View")

///|
fn pdf_ocg_view_key() -> PdfName {
  pdf_ocg_view_key_name
}

///|
let pdf_ocg_view_state_key_name : PdfName = pdf_ocg_name("/ViewState")

///|
fn pdf_ocg_view_state_key() -> PdfName {
  pdf_ocg_view_state_key_name
}

///|
let pdf_ocg_user_key_name : PdfName = pdf_ocg_name("/User")

///|
fn pdf_ocg_user_key() -> PdfName {
  pdf_ocg_user_key_name
}

///|
let pdf_ocg_page_element_key_name : PdfName = pdf_ocg_name("/PageElement")

///|
fn pdf_ocg_page_element_key() -> PdfName {
  pdf_ocg_page_element_key_name
}

///|
let pdf_ocg_event_key_name : PdfName = pdf_ocg_name("/Event")

///|
fn pdf_ocg_event_key() -> PdfName {
  pdf_ocg_event_key_name
}

///|
let pdf_ocg_category_key_name : PdfName = pdf_ocg_name("/Category")

///|
fn pdf_ocg_category_key() -> PdfName {
  pdf_ocg_category_key_name
}

///|
let pdf_ocg_base_state_key_name : PdfName = pdf_ocg_name("/BaseState")

///|
fn pdf_ocg_base_state_key() -> PdfName {
  pdf_ocg_base_state_key_name
}

///|
let pdf_ocg_on_key_name : PdfName = pdf_ocg_name("/ON")

///|
fn pdf_ocg_on_key() -> PdfName {
  pdf_ocg_on_key_name
}

///|
let pdf_ocg_off_key_name : PdfName = pdf_ocg_name("/OFF")

///|
fn pdf_ocg_off_key() -> PdfName {
  pdf_ocg_off_key_name
}

///|
let pdf_ocg_as_key_name : PdfName = pdf_ocg_name("/AS")

///|
fn pdf_ocg_as_key() -> PdfName {
  pdf_ocg_as_key_name
}

///|
let pdf_ocg_order_key_name : PdfName = pdf_ocg_name("/Order")

///|
fn pdf_ocg_order_key() -> PdfName {
  pdf_ocg_order_key_name
}

///|
let pdf_ocg_list_mode_key_name : PdfName = pdf_ocg_name("/ListMode")

///|
fn pdf_ocg_list_mode_key() -> PdfName {
  pdf_ocg_list_mode_key_name
}

///|
let pdf_ocg_rb_groups_key_name : PdfName = pdf_ocg_name("/RBGroups")

///|
fn pdf_ocg_rb_groups_key() -> PdfName {
  pdf_ocg_rb_groups_key_name
}

///|
let pdf_ocg_locked_key_name : PdfName = pdf_ocg_name("/Locked")

///|
fn pdf_ocg_locked_key() -> PdfName {
  pdf_ocg_locked_key_name
}

///|
let pdf_ocg_ocg_name_value : PdfName = pdf_ocg_name("/OCG")

///|
fn pdf_ocg_ocg_name() -> PdfName {
  pdf_ocg_ocg_name_value
}

///|
fn pdf_ocg_view_name() -> PdfName {
  pdf_ocg_view_key_name
}

///|
fn pdf_ocg_print_name() -> PdfName {
  pdf_ocg_print_key_name
}

///|
fn pdf_ocg_export_name() -> PdfName {
  pdf_ocg_export_key_name
}

///|
fn pdf_ocg_off_name() -> PdfName {
  pdf_ocg_off_key_name
}

///|
let pdf_ocg_unchanged_name_value : PdfName = pdf_ocg_name("/Unchanged")

///|
fn pdf_ocg_unchanged_name() -> PdfName {
  pdf_ocg_unchanged_name_value
}

///|
let pdf_ocg_visible_pages_name_value : PdfName = pdf_ocg_name("/VisiblePages")

///|
fn pdf_ocg_visible_pages_name() -> PdfName {
  pdf_ocg_visible_pages_name_value
}

///|
/// Usage dictionary for a PDF optional content group.
///
/// Byte fields preserve PDF string bytes exactly. Name fields preserve the
/// corresponding PDF names for creator, language, export, zoom, print, view,
/// user, and page-element usage hints.
pub(all) struct PdfOcgUsage {
  creator_info : (PdfBytes, PdfName)?
  language : (PdfBytes, PdfName?)?
  export_state : PdfName?
  zoom_min : Double?
  zoom_max : Double?
  print_subtype : PdfName?
  print_state : PdfName?
  view_state : PdfName?
  user : (PdfName, Array[PdfBytes])?
  page_element_subtype : PdfName?
} derive(Debug, Eq, ToJson)

///|
/// One PDF optional content group dictionary.
///
/// The group's indirect object number is stored outside this record in
/// `PdfOptionalContent.ocgs`; `name` is the raw PDF string bytes.
pub(all) struct PdfOcg {
  name : PdfBytes
  intent : Array[PdfName]
  usage : PdfOcgUsage?
} derive(Debug, Eq, ToJson)

///|
/// Default visibility state for optional content groups in a configuration.
pub(all) enum PdfOcgState {
  OcgOn
  OcgOff
  OcgUnchanged
} derive(Debug, Eq, ToJson)

///|
/// Policy for whether the layer list is shown on all pages or only visible pages.
pub(all) enum PdfOcgListMode {
  OcgAllPages
  OcgVisiblePages
} derive(Debug, Eq, ToJson)

///|
/// Event context used by optional-content usage application dictionaries.
pub(all) enum PdfOcgEvent {
  OcgView
  OcgPrint
  OcgExport
} derive(Debug, Eq, ToJson)

///|
/// Optional-content usage application dictionary.
///
/// `ocgs` contains indirect object numbers of optional content groups, and
/// `category` stores the PDF usage-category names for the application.
pub(all) struct PdfOcgAppDict {
  event : PdfOcgEvent
  ocgs : Array[Int]
  category : Array[PdfName]
} derive(Debug, Eq, ToJson)

///|
/// Optional-content configuration dictionary.
///
/// Arrays such as `on`, `off`, `locked`, radio-button groups, and order entries
/// contain OCG indirect object numbers. `order` preserves the PDF layer-panel
/// grouping labels as raw string bytes.
pub(all) struct PdfOcgConfig {
  name : PdfBytes?
  creator : PdfBytes?
  base_state : PdfOcgState
  on : Array[Int]
  off : Array[Int]
  intent : Array[PdfName]
  usage_applications : Array[PdfOcgAppDict]
  order : Array[(PdfBytes?, Array[Int])]?
  list_mode : PdfOcgListMode
  radio_button_groups : Array[Array[Int]]?
  locked : Array[Int]
} derive(Debug, Eq, ToJson)

///|
/// Complete catalog optional-content properties.
///
/// `ocgs` pairs each group's existing indirect object number with the parsed
/// group dictionary. `default_config` maps `/D`; `configs` maps alternate
/// entries from `/Configs`.
pub(all) struct PdfOptionalContent {
  ocgs : Array[(Int, PdfOcg)]
  default_config : PdfOcgConfig
  configs : Array[PdfOcgConfig]
} derive(Debug, Eq, ToJson)

///|
/// Return the PDF default optional-content configuration used when none is set.
///
/// The default has base state `OcgOn`, `/Intent /View`, no explicit on/off
/// lists, no order, no radio-button groups, and no locked groups.
pub fn pdf_ocg_default_config() -> PdfOcgConfig {
  {
    name: None,
    creator: None,
    base_state: OcgOn,
    on: [],
    off: [],
    intent: [pdf_ocg_view_name()],
    usage_applications: [],
    order: None,
    list_mode: OcgAllPages,
    radio_button_groups: None,
    locked: [],
  }
}

///|
fn PdfDocument::pdf_ocg_root(self : PdfDocument) -> PdfObject raise PdfError {
  self.catalog()
}

///|
fn PdfDocument::pdf_ocg_name_array(
  self : PdfDocument,
  object : PdfObject,
) -> Array[PdfName] {
  match self.direct(object) {
    PdfNameObject(name) => [name]
    PdfArray(values) => {
      let names : Array[PdfName] = []
      for value in values {
        match self.direct(value) {
          PdfNameObject(name) => names.push(name)
          _ => ()
        }
      }
      names
    }
    _ => []
  }
}

///|
fn PdfDocument::pdf_ocg_indirect_numbers(
  self : PdfDocument,
  object : PdfObject,
) -> Array[Int] {
  match self.direct(object) {
    PdfArray(values) => {
      let numbers : Array[Int] = []
      for value in values {
        match value {
          PdfIndirect(number) => numbers.push(number)
          _ => ()
        }
      }
      numbers
    }
    _ => []
  }
}

///|
fn PdfDocument::pdf_ocg_string_array(
  self : PdfDocument,
  object : PdfObject,
) -> Array[PdfBytes] {
  match self.direct(object) {
    PdfString(text) => [text]
    PdfArray(values) => {
      let strings : Array[PdfBytes] = []
      for value in values {
        match self.direct(value) {
          PdfString(text) => strings.push(text)
          _ => ()
        }
      }
      strings
    }
    _ => []
  }
}

///|
fn PdfDocument::pdf_ocg_optional_number(
  self : PdfDocument,
  object : PdfObject,
) -> Double? {
  try self.get_number(object) catch {
    _ => None
  } noraise {
    value => Some(value)
  }
}

///|
fn PdfDocument::pdf_ocg_read_appdict(
  self : PdfDocument,
  appdict : PdfObject,
) -> PdfOcgAppDict {
  let event = match self.lookup_direct(pdf_ocg_event_key(), appdict) {
    Some(PdfNameObject(name)) if name == pdf_ocg_print_name() => OcgPrint
    Some(PdfNameObject(name)) if name == pdf_ocg_export_name() => OcgExport
    _ => OcgView
  }
  let ocgs = match self.lookup_direct(pdf_ocg_ocgs_key(), appdict) {
    Some(object) => self.pdf_ocg_indirect_numbers(object)
    _ => []
  }
  let category = match self.lookup_direct(pdf_ocg_category_key(), appdict) {
    Some(object) => self.pdf_ocg_name_array(object)
    _ => []
  }
  { event, ocgs, category }
}

///|
fn PdfDocument::pdf_ocg_read_order(
  self : PdfDocument,
  order : PdfObject,
) -> Array[(PdfBytes?, Array[Int])] {
  match self.direct(order) {
    PdfArray(items) => {
      let output : Array[(PdfBytes?, Array[Int])] = []
      for item in items {
        match item {
          PdfIndirect(number) => output.push((None, [number]))
          _ =>
            match self.direct(item) {
              PdfArray([PdfString(label), .. rest]) => {
                let numbers : Array[Int] = []
                for child in rest {
                  match child {
                    PdfIndirect(number) => numbers.push(number)
                    _ => ()
                  }
                }
                output.push((Some(label), numbers))
              }
              PdfArray(children) => {
                let numbers : Array[Int] = []
                for child in children {
                  match child {
                    PdfIndirect(number) => numbers.push(number)
                    _ => ()
                  }
                }
                output.push((None, numbers))
              }
              _ => output.push((None, []))
            }
        }
      }
      output
    }
    _ => []
  }
}

///|
fn PdfDocument::pdf_ocg_read_config(
  self : PdfDocument,
  config : PdfObject,
) -> PdfOcgConfig {
  let name = match self.lookup_direct(pdf_ocg_name_key(), config) {
    Some(PdfString(value)) => Some(value)
    _ => None
  }
  let creator = match self.lookup_direct(pdf_ocg_creator_key(), config) {
    Some(PdfString(value)) => Some(value)
    _ => None
  }
  let base_state = match self.lookup_direct(pdf_ocg_base_state_key(), config) {
    Some(PdfNameObject(name)) if name == pdf_ocg_off_name() => OcgOff
    Some(PdfNameObject(name)) if name == pdf_ocg_unchanged_name() =>
      OcgUnchanged
    _ => OcgOn
  }
  let on = match self.lookup_direct(pdf_ocg_on_key(), config) {
    Some(object) => self.pdf_ocg_indirect_numbers(object)
    _ => []
  }
  let off = match self.lookup_direct(pdf_ocg_off_key(), config) {
    Some(object) => self.pdf_ocg_indirect_numbers(object)
    _ => []
  }
  let intent = match self.lookup_direct(pdf_ocg_intent_key(), config) {
    Some(object) => self.pdf_ocg_name_array(object)
    _ => [pdf_ocg_view_name()]
  }
  let usage_applications = match self.lookup_direct(pdf_ocg_as_key(), config) {
    Some(PdfArray(appdicts)) =>
      [
        for appdict in appdicts => self.pdf_ocg_read_appdict(appdict)
      ]
    _ => []
  }
  let order = match self.lookup_direct(pdf_ocg_order_key(), config) {
    Some(object) => Some(self.pdf_ocg_read_order(object))
    None => None
  }
  let list_mode = match self.lookup_direct(pdf_ocg_list_mode_key(), config) {
    Some(PdfNameObject(name)) if name == pdf_ocg_visible_pages_name() =>
      OcgVisiblePages
    _ => OcgAllPages
  }
  let radio_button_groups = match
    self.lookup_direct(pdf_ocg_rb_groups_key(), config) {
    Some(PdfArray(groups)) =>
      Some([ for group in groups => self.pdf_ocg_indirect_numbers(group) ])
    _ => None
  }
  let locked = match self.lookup_direct(pdf_ocg_locked_key(), config) {
    Some(object) => self.pdf_ocg_indirect_numbers(object)
    _ => []
  }
  {
    name,
    creator,
    base_state,
    on,
    off,
    intent,
    usage_applications,
    order,
    list_mode,
    radio_button_groups,
    locked,
  }
}

///|
fn PdfDocument::pdf_ocg_read_usage(
  self : PdfDocument,
  usage : PdfObject,
) -> PdfOcgUsage {
  let creator = match
    self.lookup_chain(usage, [pdf_ocg_creatorinfo_key(), pdf_ocg_creator_key()]) {
    Some(PdfString(value)) => Some(value)
    _ => None
  }
  let creator_subtype = match
    self.lookup_chain(usage, [pdf_ocg_creatorinfo_key(), pdf_ocg_subtype_key()]) {
    Some(PdfNameObject(value)) => Some(value)
    _ => None
  }
  let creator_info = match (creator, creator_subtype) {
    (Some(creator), Some(subtype)) => Some((creator, subtype))
    (Some(creator), None) => Some((creator, pdf_name_of_bytes([])))
    (None, Some(subtype)) => Some(([], subtype))
    _ => None
  }
  let language_value = match
    self.lookup_chain(usage, [pdf_ocg_language_key(), pdf_ocg_lang_key()]) {
    Some(PdfString(value)) => Some(value)
    _ => None
  }
  let language_preferred = match
    self.lookup_chain(usage, [pdf_ocg_language_key(), pdf_ocg_preferred_key()]) {
    Some(PdfNameObject(value)) => Some(value)
    _ => None
  }
  let language = match language_value {
    Some(value) => Some((value, language_preferred))
    None => None
  }
  let export_state = match
    self.lookup_chain(usage, [pdf_ocg_export_key(), pdf_ocg_export_state_key()]) {
    Some(PdfNameObject(value)) => Some(value)
    _ => None
  }
  let zoom_min = match
    self.lookup_chain(usage, [pdf_ocg_zoom_key(), pdf_ocg_min_key()]) {
    Some(value) => self.pdf_ocg_optional_number(value)
    _ => None
  }
  let zoom_max = match
    self.lookup_chain(usage, [pdf_ocg_zoom_key(), pdf_ocg_max_key()]) {
    Some(value) => self.pdf_ocg_optional_number(value)
    _ => None
  }
  let print_subtype = match
    self.lookup_chain(usage, [pdf_ocg_print_key(), pdf_ocg_subtype_key()]) {
    Some(PdfNameObject(value)) => Some(value)
    _ => None
  }
  let print_state = match
    self.lookup_chain(usage, [pdf_ocg_print_key(), pdf_ocg_print_state_key()]) {
    Some(PdfNameObject(value)) => Some(value)
    _ => None
  }
  let view_state = match
    self.lookup_chain(usage, [pdf_ocg_view_key(), pdf_ocg_view_state_key()]) {
    Some(PdfNameObject(value)) => Some(value)
    _ => None
  }
  let user_type = match
    self.lookup_chain(usage, [pdf_ocg_user_key(), pdf_ocg_type_key()]) {
    Some(PdfNameObject(value)) => Some(value)
    _ => None
  }
  let user_names = match
    self.lookup_chain(usage, [pdf_ocg_user_key(), pdf_ocg_name_key()]) {
    Some(object) => {
      let names = self.pdf_ocg_string_array(object)
      if names.length() == 0 {
        None
      } else {
        Some(names)
      }
    }
    _ => None
  }
  let user = match (user_type, user_names) {
    (Some(user_type), Some(user_names)) => Some((user_type, user_names))
    _ => None
  }
  let page_element_subtype = match
    self.lookup_chain(usage, [pdf_ocg_page_element_key(), pdf_ocg_subtype_key()]) {
    Some(PdfNameObject(value)) => Some(value)
    _ => None
  }
  {
    creator_info,
    language,
    export_state,
    zoom_min,
    zoom_max,
    print_subtype,
    print_state,
    view_state,
    user,
    page_element_subtype,
  }
}

///|
fn PdfDocument::pdf_ocg_read_individual(
  self : PdfDocument,
  ocg : PdfObject,
) -> PdfOcg raise PdfError {
  let name = match self.lookup_direct(pdf_ocg_name_key(), ocg) {
    Some(PdfString(name)) => name
    _ => raise DictionaryKeyNotFound(pdf_ocg_name_key())
  }
  let intent = match self.lookup_direct(pdf_ocg_intent_key(), ocg) {
    Some(object) => self.pdf_ocg_name_array(object)
    _ => [pdf_ocg_view_name()]
  }
  let usage = match self.lookup_direct(pdf_ocg_usage_key(), ocg) {
    Some(usage) => Some(self.pdf_ocg_read_usage(usage))
    None => None
  }
  { name, intent, usage }
}

///|
/// Read catalog `/OCProperties` from the document.
///
/// Returns `None` when the catalog has no optional-content properties. When
/// properties are present, OCG entries are returned with their indirect object
/// numbers so callers can preserve or renumber references before writing.
pub fn PdfDocument::read_optional_content_groups(
  self : PdfDocument,
) -> PdfOptionalContent? raise PdfError {
  let root = self.pdf_ocg_root()
  match self.lookup_direct(pdf_ocg_ocproperties_key(), root) {
    None => None
    Some(ocproperties) => {
      let ocgs = match self.lookup_direct(pdf_ocg_ocgs_key(), ocproperties) {
        Some(PdfArray(indirects)) => {
          let output : Array[(Int, PdfOcg)] = []
          for indirect in indirects {
            match indirect {
              PdfIndirect(number) =>
                output.push((number, self.pdf_ocg_read_individual(indirect)))
              _ => ()
            }
          }
          output
        }
        _ => []
      }
      let default_config = match
        self.lookup_direct(pdf_ocg_d_key(), ocproperties) {
        Some(config) => self.pdf_ocg_read_config(config)
        None => raise DictionaryKeyNotFound(pdf_ocg_d_key())
      }
      let configs = match
        self.lookup_direct(pdf_ocg_configs_key(), ocproperties) {
        Some(PdfArray(configs)) =>
          [
            for config in configs => self.pdf_ocg_read_config(config)
          ]
        _ => []
      }
      Some({ ocgs, default_config, configs })
    }
  }
}

///|
/// Compatibility wrapper for `PdfDocument::read_optional_content_groups`.
pub fn pdf_read_ocg(
  document : PdfDocument,
) -> PdfOptionalContent? raise PdfError {
  document.read_optional_content_groups()
}

///|
/// Return the raw PDF string names of catalog optional-content groups.
///
/// This mirrors cpdf's `ocg_get_list`: missing optional-content metadata yields
/// an empty list, and malformed individual group entries are skipped.
pub fn PdfDocument::ocg_get_list(
  self : PdfDocument,
) -> Array[PdfBytes] raise PdfError {
  let root = self.pdf_ocg_root()
  match self.lookup_direct(pdf_ocg_ocproperties_key(), root) {
    None => []
    Some(ocproperties) =>
      match self.lookup_direct(pdf_ocg_ocgs_key(), ocproperties) {
        Some(PdfArray(indirects)) => {
          let names : Array[PdfBytes] = []
          for indirect in indirects {
            match indirect {
              PdfIndirect(_) =>
                match self.lookup_direct(pdf_ocg_name_key(), indirect) {
                  Some(PdfString(name)) => names.push(name)
                  _ => ()
                }
              _ => ()
            }
          }
          names
        }
        _ => []
      }
  }
}

///|
/// Compatibility wrapper for `PdfDocument::ocg_get_list`.
pub fn pdf_ocg_get_list(
  document : PdfDocument,
) -> Array[PdfBytes] raise PdfError {
  document.ocg_get_list()
}

///|
fn pdf_ocg_json_int(value : Int) -> Json {
  pdf_util_json_number(value)
}

///|
fn pdf_ocg_json_ints(values : ArrayView[Int]) -> Json {
  Json::array([ for value in values => pdf_ocg_json_int(value) ])
}

///|
fn pdf_ocg_json_string(bytes : BytesView) -> Json raise PdfError {
  let utf8 = pdf_utf8_of_pdfdocstring(bytes)
  Json::string(pdf_util_string_from_utf8(utf8))
}

///|
fn pdf_ocg_json_name(name : PdfName) -> Json raise PdfError {
  Json::string(pdf_util_string_from_name(name))
}

///|
fn pdf_ocg_json_optional_string(bytes : PdfBytes?) -> Json raise PdfError {
  match bytes {
    Some(bytes) => pdf_ocg_json_string(bytes)
    None => Json::null()
  }
}

///|
fn pdf_ocg_json_optional_name(name : PdfName?) -> Json raise PdfError {
  match name {
    Some(name) => pdf_ocg_json_name(name)
    None => Json::null()
  }
}

///|
fn pdf_ocg_json_optional_float(value : Double?) -> Json {
  match value {
    Some(value) => Json::number(value)
    None => Json::null()
  }
}

///|
fn pdf_ocg_json_names(names : ArrayView[PdfName]) -> Json raise PdfError {
  let output : Array[Json] = []
  for name in names {
    output.push(pdf_ocg_json_name(name))
  }
  Json::array(output)
}

///|
fn pdf_ocg_json_strings(strings : ArrayView[PdfBytes]) -> Json raise PdfError {
  let output : Array[Json] = []
  for text in strings {
    output.push(pdf_ocg_json_string(text))
  }
  Json::array(output)
}

///|
fn pdf_ocg_json_state(state : PdfOcgState) -> Json {
  Json::string(
    match state {
      OcgOn => "/ON"
      OcgOff => "/OFF"
      OcgUnchanged => "/Unchanged"
    },
  )
}

///|
fn pdf_ocg_json_list_mode(mode : PdfOcgListMode) -> Json {
  Json::string(
    match mode {
      OcgAllPages => "/AllPages"
      OcgVisiblePages => "/VisiblePages"
    },
  )
}

///|
fn pdf_ocg_json_event(event : PdfOcgEvent) -> Json {
  Json::string(
    match event {
      OcgView => "/View"
      OcgPrint => "/Print"
      OcgExport => "/Export"
    },
  )
}

///|
fn pdf_ocg_json_order(
  order : ArrayView[(PdfBytes?, Array[Int])],
) -> Json raise PdfError {
  let output : Array[Json] = []
  for item in order {
    let (name, ocgs) = item
    output.push(
      Json::object(
        Map::of([
          ("Name", pdf_ocg_json_optional_string(name)),
          ("OCGs", pdf_ocg_json_ints(ocgs)),
        ]),
      ),
    )
  }
  Json::array(output)
}

///|
fn pdf_ocg_json_optional_order(
  order : Array[(PdfBytes?, Array[Int])]?,
) -> Json raise PdfError {
  match order {
    Some(order) => pdf_ocg_json_order(order)
    None => Json::null()
  }
}

///|
fn pdf_ocg_json_radio_groups(groups : Array[Array[Int]]?) -> Json {
  match groups {
    Some(groups) =>
      Json::array([ for group in groups => pdf_ocg_json_ints(group) ])
    None => Json::null()
  }
}

///|
fn pdf_ocg_json_appdict(appdict : PdfOcgAppDict) -> Json raise PdfError {
  Json::object(
    Map::of([
      ("Event", pdf_ocg_json_event(appdict.event)),
      ("OCGs", pdf_ocg_json_ints(appdict.ocgs)),
      ("Category", pdf_ocg_json_names(appdict.category)),
    ]),
  )
}

///|
fn pdf_ocg_json_appdicts(
  appdicts : ArrayView[PdfOcgAppDict],
) -> Json raise PdfError {
  let output : Array[Json] = []
  for appdict in appdicts {
    output.push(pdf_ocg_json_appdict(appdict))
  }
  Json::array(output)
}

///|
fn pdf_ocg_json_config(config : PdfOcgConfig) -> Json raise PdfError {
  Json::object(
    Map::of([
      ("Name", pdf_ocg_json_optional_string(config.name)),
      ("Creator", pdf_ocg_json_optional_string(config.creator)),
      ("BaseState", pdf_ocg_json_state(config.base_state)),
      ("ON", pdf_ocg_json_ints(config.on)),
      ("OFF", pdf_ocg_json_ints(config.off)),
      ("Intent", pdf_ocg_json_names(config.intent)),
      ("AS", pdf_ocg_json_appdicts(config.usage_applications)),
      ("Order", pdf_ocg_json_optional_order(config.order)),
      ("ListMode", pdf_ocg_json_list_mode(config.list_mode)),
      ("RBGroups", pdf_ocg_json_radio_groups(config.radio_button_groups)),
      ("Locked", pdf_ocg_json_ints(config.locked)),
    ]),
  )
}

///|
fn pdf_ocg_json_usage(usage : PdfOcgUsage) -> Json raise PdfError {
  Json::object(
    Map::of([
      (
        "CreatorInfo-Creator",
        match usage.creator_info {
          Some((creator, _)) => pdf_ocg_json_string(creator)
          None => Json::null()
        },
      ),
      (
        "CreatorInfo-Subtype",
        match usage.creator_info {
          Some((_, subtype)) => pdf_ocg_json_name(subtype)
          None => Json::null()
        },
      ),
      (
        "Language-Language",
        match usage.language {
          Some((language, _)) => pdf_ocg_json_string(language)
          None => Json::null()
        },
      ),
      (
        "Language-Preferred",
        match usage.language {
          Some((_, preferred)) => pdf_ocg_json_optional_name(preferred)
          None => Json::null()
        },
      ),
      ("Export", pdf_ocg_json_optional_name(usage.export_state)),
      ("Zoom-Min", pdf_ocg_json_optional_float(usage.zoom_min)),
      ("Zoom-Max", pdf_ocg_json_optional_float(usage.zoom_max)),
      ("Print-Subtype", pdf_ocg_json_optional_name(usage.print_subtype)),
      ("Print-PrintState", pdf_ocg_json_optional_name(usage.print_state)),
      ("ViewState", pdf_ocg_json_optional_name(usage.view_state)),
      (
        "User-Type",
        match usage.user {
          Some((user_type, _)) => pdf_ocg_json_name(user_type)
          None => Json::null()
        },
      ),
      (
        "User-Name",
        match usage.user {
          Some((_, names)) => pdf_ocg_json_strings(names)
          None => Json::null()
        },
      ),
      (
        "PageElement-Subtype",
        pdf_ocg_json_optional_name(usage.page_element_subtype),
      ),
    ]),
  )
}

///|
fn pdf_ocg_json_optional_usage(usage : PdfOcgUsage?) -> Json raise PdfError {
  match usage {
    Some(usage) => pdf_ocg_json_usage(usage)
    None => Json::null()
  }
}

///|
fn pdf_ocg_json_ocg(ocg : PdfOcg) -> Json raise PdfError {
  Json::object(
    Map::of([
      ("Name", pdf_ocg_json_string(ocg.name)),
      ("Intent", pdf_ocg_json_names(ocg.intent)),
      ("Usage", pdf_ocg_json_optional_usage(ocg.usage)),
    ]),
  )
}

///|
fn pdf_ocg_json_ocgs(ocgs : ArrayView[(Int, PdfOcg)]) -> Json raise PdfError {
  let entries : Map[String, Json] = Map([])
  for item in ocgs {
    let (number, ocg) = item
    entries.set(number.to_string(), pdf_ocg_json_ocg(ocg))
  }
  Json::object(entries)
}

///|
fn pdf_ocg_json_configs(
  configs : ArrayView[PdfOcgConfig],
) -> Json raise PdfError {
  let output : Array[Json] = []
  for config in configs {
    output.push(pdf_ocg_json_config(config))
  }
  Json::array(output)
}

///|
/// Return cpdf's optional-content group JSON listing.
pub fn PdfDocument::ocg_list_json(self : PdfDocument) -> Json raise PdfError {
  match self.read_optional_content_groups() {
    None => Json::null()
    Some(content) =>
      Json::object(
        Map::of([
          ("OCGs", pdf_ocg_json_ocgs(content.ocgs)),
          ("Default", pdf_ocg_json_config(content.default_config)),
          ("Configs", pdf_ocg_json_configs(content.configs)),
        ]),
      )
  }
}

///|
/// Return cpdf's optional-content group JSON listing as UTF-8 JSON bytes.
pub fn PdfDocument::ocg_list_json_blob(
  self : PdfDocument,
) -> PdfBytes raise PdfError {
  @utf8.encode(self.ocg_list_json().stringify())
}

///|
/// Compatibility wrapper for `PdfDocument::ocg_list_json`.
pub fn pdf_ocg_list_json(document : PdfDocument) -> Json raise PdfError {
  document.ocg_list_json()
}

///|
/// Compatibility wrapper for `PdfDocument::ocg_list_json_blob`.
pub fn pdf_ocg_list_json_blob(
  document : PdfDocument,
) -> PdfBytes raise PdfError {
  document.ocg_list_json_blob()
}

///|
fn pdf_ocg_read_json_field(
  entries : Map[String, Json],
  key : String,
  message : String,
) -> Json raise PdfError {
  match entries.get(key) {
    Some(value) => value
    None => raise SoftError(message)
  }
}

///|
fn pdf_ocg_read_json_pdf_string(
  json : Json,
  message : String,
) -> PdfBytes raise PdfError {
  match json {
    String(text) => pdf_pdfdocstring_of_utf8(@utf8.encode(text))
    _ => raise SoftError(message)
  }
}

///|
fn pdf_ocg_read_json_optional_pdf_string(
  json : Json,
  message : String,
) -> PdfBytes? raise PdfError {
  match json {
    Null => None
    String(text) => Some(pdf_pdfdocstring_of_utf8(@utf8.encode(text)))
    _ => raise SoftError(message)
  }
}

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

///|
fn pdf_ocg_read_json_name(
  json : Json,
  message : String,
) -> PdfName raise PdfError {
  match json {
    String(text) => pdf_ocg_read_json_name_text(text)
    _ => raise SoftError(message)
  }
}

///|
fn pdf_ocg_read_json_optional_name(
  json : Json,
  message : String,
) -> PdfName? raise PdfError {
  match json {
    Null => None
    String(text) => Some(pdf_ocg_read_json_name_text(text))
    _ => raise SoftError(message)
  }
}

///|
fn pdf_ocg_read_json_optional_number(
  json : Json,
  message : String,
) -> Double? raise PdfError {
  match json {
    Null => None
    Number(value, repr=_) => Some(value)
    _ => raise SoftError(message)
  }
}

///|
fn pdf_ocg_read_json_int(json : Json, message : String) -> Int raise PdfError {
  match json {
    Number(value, repr=_) => pdf_util_json_truncated_number(value)
    _ => raise SoftError(message)
  }
}

///|
fn pdf_ocg_read_json_ints(
  json : Json,
  message : String,
) -> Array[Int] raise PdfError {
  match json {
    Array(values) => {
      let output : Array[Int] = []
      for value in values {
        output.push(pdf_ocg_read_json_int(value, message))
      }
      output
    }
    _ => raise SoftError(message)
  }
}

///|
fn pdf_ocg_read_json_names(
  json : Json,
  message : String,
) -> Array[PdfName] raise PdfError {
  match json {
    Array(values) => {
      let output : Array[PdfName] = []
      for value in values {
        output.push(pdf_ocg_read_json_name(value, message))
      }
      output
    }
    _ => raise SoftError(message)
  }
}

///|
fn pdf_ocg_read_json_pdf_strings(
  json : Json,
  message : String,
) -> Array[PdfBytes] raise PdfError {
  match json {
    Array(values) => {
      let output : Array[PdfBytes] = []
      for value in values {
        output.push(pdf_ocg_read_json_pdf_string(value, message))
      }
      output
    }
    _ => raise SoftError(message)
  }
}

///|
fn pdf_ocg_read_json_state(
  json : Json,
  message : String,
) -> PdfOcgState raise PdfError {
  match json {
    String("/ON") => OcgOn
    String("/OFF") => OcgOff
    String("/Unchanged") => OcgUnchanged
    _ => raise SoftError(message)
  }
}

///|
fn pdf_ocg_read_json_list_mode(
  json : Json,
  message : String,
) -> PdfOcgListMode raise PdfError {
  match json {
    String("/AllPages") => OcgAllPages
    String("/VisiblePages") => OcgVisiblePages
    _ => raise SoftError(message)
  }
}

///|
fn pdf_ocg_read_json_event(
  json : Json,
  message : String,
) -> PdfOcgEvent raise PdfError {
  match json {
    String("/View") => OcgView
    String("/Print") => OcgPrint
    String("/Export") => OcgExport
    _ => raise SoftError(message)
  }
}

///|
fn pdf_ocg_read_json_usage(json : Json) -> PdfOcgUsage? raise PdfError {
  match json {
    Null => None
    Object(entries) if entries.length() == 13 => {
      let cc = pdf_ocg_read_json_field(
        entries, "CreatorInfo-Creator", "ocg_read_usage: malformed",
      )
      let cs = pdf_ocg_read_json_field(
        entries, "CreatorInfo-Subtype", "ocg_read_usage: malformed",
      )
      let ll = pdf_ocg_read_json_field(
        entries, "Language-Language", "ocg_read_usage: malformed",
      )
      let lp = pdf_ocg_read_json_field(
        entries, "Language-Preferred", "ocg_read_usage: malformed",
      )
      let e = pdf_ocg_read_json_field(
        entries, "Export", "ocg_read_usage: malformed",
      )
      let zmin = pdf_ocg_read_json_field(
        entries, "Zoom-Min", "ocg_read_usage: malformed",
      )
      let zmax = pdf_ocg_read_json_field(
        entries, "Zoom-Max", "ocg_read_usage: malformed",
      )
      let ps = pdf_ocg_read_json_field(
        entries, "Print-Subtype", "ocg_read_usage: malformed",
      )
      let pp = pdf_ocg_read_json_field(
        entries, "Print-PrintState", "ocg_read_usage: malformed",
      )
      let vs = pdf_ocg_read_json_field(
        entries, "ViewState", "ocg_read_usage: malformed",
      )
      let ut = pdf_ocg_read_json_field(
        entries, "User-Type", "ocg_read_usage: malformed",
      )
      let un = pdf_ocg_read_json_field(
        entries, "User-Name", "ocg_read_usage: malformed",
      )
      let pes = pdf_ocg_read_json_field(
        entries, "PageElement-Subtype", "ocg_read_usage: malformed",
      )
      let creator_info = match (cc, cs) {
        (Null, Null) => None
        (String(creator), String(subtype)) =>
          Some(
            (
              pdf_pdfdocstring_of_utf8(@utf8.encode(creator)),
              pdf_ocg_read_json_name_text(subtype),
            ),
          )
        _ => raise SoftError("ocg_read_usage: malformed creator info")
      }
      let language = match (ll, lp) {
        (Null, Null) => None
        (String(language), String(preferred)) =>
          Some(
            (
              pdf_pdfdocstring_of_utf8(@utf8.encode(language)),
              Some(pdf_ocg_read_json_name_text(preferred)),
            ),
          )
        (String(language), Null) =>
          Some((pdf_pdfdocstring_of_utf8(@utf8.encode(language)), None))
        _ => raise SoftError("ocg_read_usage: malformed language")
      }
      let user = match (ut, un) {
        (Null, Null) => None
        (String(user_type), Array(_)) =>
          Some(
            (
              pdf_ocg_read_json_name_text(user_type),
              pdf_ocg_read_json_pdf_strings(
                un, "ocg_read_usage: malformed user",
              ),
            ),
          )
        _ => raise SoftError("ocg_read_usage: malformed user")
      }
      Some({
        creator_info,
        language,
        export_state: pdf_ocg_read_json_optional_name(
          e, "ocg_read_usage: malformed export",
        ),
        zoom_min: pdf_ocg_read_json_optional_number(
          zmin, "ocg_read_usage: malformed zoom min",
        ),
        zoom_max: pdf_ocg_read_json_optional_number(
          zmax, "ocg_read_usage: malformed zoom max",
        ),
        print_subtype: pdf_ocg_read_json_optional_name(
          ps, "ocg_read_usage: malformed print subtype",
        ),
        print_state: pdf_ocg_read_json_optional_name(
          pp, "ocg_read_usage: malformed print printstate",
        ),
        view_state: pdf_ocg_read_json_optional_name(
          vs, "ocg_read_usage: malformed print viewstate",
        ),
        user,
        page_element_subtype: pdf_ocg_read_json_optional_name(
          pes, "ocg_read_usage: malformed subtype",
        ),
      })
    }
    _ => raise SoftError("ocg_read_usage: malformed")
  }
}

///|
fn pdf_ocg_read_json_order_item(
  json : Json,
) -> (PdfBytes?, Array[Int]) raise PdfError {
  match json {
    Object(entries) if entries.length() == 2 => {
      let name = pdf_ocg_read_json_optional_pdf_string(
        pdf_ocg_read_json_field(
          entries, "Name", "ocg_read_config: malformed order pair",
        ),
        "ocg_read_config: malformed order name",
      )
      let ocgs = pdf_ocg_read_json_ints(
        pdf_ocg_read_json_field(
          entries, "OCGs", "ocg_read_config: malformed order pair",
        ),
        "ocg_read_config: malformed order ocg",
      )
      (name, ocgs)
    }
    _ => raise SoftError("ocg_read_config: malformed order pair")
  }
}

///|
fn pdf_ocg_read_json_order(
  json : Json,
) -> Array[(PdfBytes?, Array[Int])]? raise PdfError {
  match json {
    Null => None
    Array(values) => {
      let output : Array[(PdfBytes?, Array[Int])] = []
      for value in values {
        output.push(pdf_ocg_read_json_order_item(value))
      }
      Some(output)
    }
    _ => raise SoftError("ocg_read_config: malformed order")
  }
}

///|
fn pdf_ocg_read_json_radio_groups(
  json : Json,
) -> Array[Array[Int]]? raise PdfError {
  match json {
    Null => None
    Array(values) => {
      let output : Array[Array[Int]] = []
      for value in values {
        output.push(
          pdf_ocg_read_json_ints(value, "ocg_read_config: bad rbgroups"),
        )
      }
      Some(output)
    }
    _ => raise SoftError("ocg_read_config: bad rbgroups")
  }
}

///|
fn pdf_ocg_read_json_appdict(json : Json) -> PdfOcgAppDict? raise PdfError {
  match json {
    Null => None
    Object(entries) if entries.length() == 3 => {
      let event = pdf_ocg_read_json_event(
        pdf_ocg_read_json_field(entries, "Event", "ocg_read_appdict: malformed"),
        "ocg_read_appdict: bad event",
      )
      let ocgs = pdf_ocg_read_json_ints(
        pdf_ocg_read_json_field(entries, "OCGs", "ocg_read_appdict: malformed"),
        "ocg_read_appdict: bad ocg",
      )
      let category = pdf_ocg_read_json_names(
        pdf_ocg_read_json_field(
          entries, "Category", "ocg_read_appdict: malformed",
        ),
        "ocg_read_ocg: bad category",
      )
      Some({ event, ocgs, category })
    }
    _ => raise SoftError("ocg_read_appdict: malformed")
  }
}

///|
fn pdf_ocg_read_json_appdicts(
  json : Json,
) -> Array[PdfOcgAppDict] raise PdfError {
  match json {
    Array(values) => {
      let output : Array[PdfOcgAppDict] = []
      for value in values {
        match pdf_ocg_read_json_appdict(value) {
          Some(appdict) => output.push(appdict)
          None => ()
        }
      }
      output
    }
    _ =>
      raise SoftError(
        "ocg_read_config: malformed usage application dictionary list",
      )
  }
}

///|
fn pdf_ocg_read_json_config(json : Json) -> PdfOcgConfig raise PdfError {
  match json {
    Object(entries) if entries.length() == 11 => {
      let n = pdf_ocg_read_json_field(
        entries, "Name", "ocg_read_config: malformed",
      )
      let c = pdf_ocg_read_json_field(
        entries, "Creator", "ocg_read_config: malformed",
      )
      let bs = pdf_ocg_read_json_field(
        entries, "BaseState", "ocg_read_config: malformed",
      )
      let on = pdf_ocg_read_json_field(
        entries, "ON", "ocg_read_config: malformed",
      )
      let off = pdf_ocg_read_json_field(
        entries, "OFF", "ocg_read_config: malformed",
      )
      let i = pdf_ocg_read_json_field(
        entries, "Intent", "ocg_read_config: malformed",
      )
      let a = pdf_ocg_read_json_field(
        entries, "AS", "ocg_read_config: malformed",
      )
      let o = pdf_ocg_read_json_field(
        entries, "Order", "ocg_read_config: malformed",
      )
      let lm = pdf_ocg_read_json_field(
        entries, "ListMode", "ocg_read_config: malformed",
      )
      let rb = pdf_ocg_read_json_field(
        entries, "RBGroups", "ocg_read_config: malformed",
      )
      let l = pdf_ocg_read_json_field(
        entries, "Locked", "ocg_read_config: malformed",
      )
      {
        name: pdf_ocg_read_json_optional_pdf_string(
          n, "ocg_read_config: malformed name",
        ),
        creator: pdf_ocg_read_json_optional_pdf_string(
          c, "ocg_read_config: malformed creator",
        ),
        base_state: pdf_ocg_read_json_state(
          bs, "ocg_read_config: malformed base state",
        ),
        on: pdf_ocg_read_json_ints(on, "ocg_read_config: malformed on"),
        off: pdf_ocg_read_json_ints(off, "ocg_read_config: malformed off"),
        intent: pdf_ocg_read_json_names(i, "ocg_read_config: malformed intent"),
        usage_applications: pdf_ocg_read_json_appdicts(a),
        order: pdf_ocg_read_json_order(o),
        list_mode: pdf_ocg_read_json_list_mode(
          lm, "ocg_read_config: malformed list mode",
        ),
        radio_button_groups: pdf_ocg_read_json_radio_groups(rb),
        locked: pdf_ocg_read_json_ints(l, "ocg_read_config: malformed locked"),
      }
    }
    _ => raise SoftError("ocg_read_config: malformed")
  }
}

///|
fn pdf_ocg_read_json_ocg(json : Json) -> PdfOcg raise PdfError {
  match json {
    Object(entries) if entries.length() == 3 => {
      let intent = pdf_ocg_read_json_field(
        entries, "Intent", "ocg_read_ocg: malformed",
      )
      let name = pdf_ocg_read_json_field(
        entries, "Name", "ocg_read_ocg: malformed",
      )
      let usage = pdf_ocg_read_json_field(
        entries, "Usage", "ocg_read_ocg: malformed",
      )
      {
        name: pdf_ocg_read_json_pdf_string(name, "ocg_read_ocg: malformed"),
        intent: pdf_ocg_read_json_names(
          intent, "ocg_read_ocg: malformed intent",
        ),
        usage: pdf_ocg_read_json_usage(usage),
      }
    }
    _ => raise SoftError("ocg_read_ocg: malformed")
  }
}

///|
fn pdf_ocg_read_json_ocgs(json : Json) -> Array[(Int, PdfOcg)] raise PdfError {
  match json {
    Object(entries) => {
      let output : Array[(Int, PdfOcg)] = []
      for entry in entries {
        let number : Int = @strconv.from_str(entry.0) catch {
          _ =>
            raise SoftError(
              "ocg_read_json: malformed JSON top-level dictionary",
            )
        }
        output.push((number, pdf_ocg_read_json_ocg(entry.1)))
      }
      output
    }
    _ => raise SoftError("ocg_read_json: malformed JSON top-level dictionary")
  }
}

///|
fn pdf_ocg_read_json_configs(json : Json) -> Array[PdfOcgConfig] raise PdfError {
  match json {
    Array(values) => {
      let output : Array[PdfOcgConfig] = []
      for value in values {
        output.push(pdf_ocg_read_json_config(value))
      }
      output
    }
    _ => raise SoftError("ocg_read_json: malformed JSON top-level dictionary")
  }
}

///|
/// Parse cpdf's optional-content group JSON representation.
pub fn pdf_ocg_read_json(json : Json) -> PdfOptionalContent? raise PdfError {
  match json {
    Null => None
    Object(entries) if entries.length() == 3 => {
      let ocgs = pdf_ocg_read_json_field(
        entries, "OCGs", "ocg_read_json: malformed JSON top-level dictionary",
      )
      let default = pdf_ocg_read_json_field(
        entries, "Default", "ocg_read_json: malformed JSON top-level dictionary",
      )
      let configs = pdf_ocg_read_json_field(
        entries, "Configs", "ocg_read_json: malformed JSON top-level dictionary",
      )
      Some({
        ocgs: pdf_ocg_read_json_ocgs(ocgs),
        default_config: pdf_ocg_read_json_config(default),
        configs: pdf_ocg_read_json_configs(configs),
      })
    }
    Object(_) =>
      raise SoftError("ocg_read_json: malformed JSON top-level dictionary")
    _ => raise SoftError("ocg_read_json: malformed JSON top-level")
  }
}

///|
/// Return a copy with catalog `/OCProperties` replaced from cpdf OCG JSON.
pub fn PdfDocument::ocg_replace_json(
  self : PdfDocument,
  json : Json,
) -> PdfDocument raise PdfError {
  match pdf_ocg_read_json(json) {
    None => self.copy()
    Some(content) => self.write_optional_content_groups(content)
  }
}

///|
/// Compatibility wrapper for `PdfDocument::ocg_replace_json`.
pub fn pdf_ocg_replace_json(
  json : Json,
  document : PdfDocument,
) -> PdfDocument raise PdfError {
  document.ocg_replace_json(json)
}

///|
/// Return a copy of the document with matching optional-content group names
/// replaced.
pub fn PdfDocument::ocg_rename(
  self : PdfDocument,
  old_name : PdfBytes,
  new_name : PdfBytes,
) -> PdfDocument {
  let pdf = self.copy()
  pdf.map_objects_in_place(fn(object) {
    match object {
      PdfDictionary(entries) =>
        match
          (
            pdf.lookup_direct(pdf_ocg_type_key(), object),
            pdf.lookup_direct(pdf_ocg_name_key(), object),
          ) {
          (Some(PdfNameObject(kind)), Some(PdfString(name))) =>
            if kind == pdf_ocg_ocg_name() && name == old_name {
              PdfDictionary(
                dict_add_entries(
                  entries,
                  pdf_ocg_name_key(),
                  PdfString(new_name),
                ),
              )
            } else {
              object
            }
          _ => object
        }
      _ => object
    }
  })
  pdf
}

///|
/// Compatibility wrapper for `PdfDocument::ocg_rename`.
pub fn pdf_ocg_rename(
  old_name : PdfBytes,
  new_name : PdfBytes,
  document : PdfDocument,
) -> PdfDocument {
  document.ocg_rename(old_name, new_name)
}

///|
/// Return a copy of the document with every OCG listed in the default `/Order`.
///
/// Documents without optional-content properties, without `/OCGs`, or without a
/// default `/D` dictionary are returned unchanged.
pub fn PdfDocument::ocg_order_all(
  self : PdfDocument,
) -> PdfDocument raise PdfError {
  let root = self.pdf_ocg_root()
  match self.lookup_direct(pdf_ocg_ocproperties_key(), root) {
    None => self.copy()
    Some(ocproperties) =>
      match
        (
          self.lookup_direct(pdf_ocg_ocgs_key(), ocproperties),
          self.lookup_direct(pdf_ocg_d_key(), ocproperties),
        ) {
        (Some(PdfArray(ocgs)), Some(PdfDictionary(_))) => {
          let pdf = self.copy()
          let default_config = self
            .lookup_direct(pdf_ocg_d_key(), ocproperties)
            .unwrap()
            .add_dict_entry(pdf_ocg_order_key(), PdfArray(ocgs.copy()))
          let new_ocproperties = ocproperties.add_dict_entry(
            pdf_ocg_d_key(),
            default_config,
          )
          let ocproperties_number = pdf.add_object(new_ocproperties)
          let new_root = pdf.add_object(
            root.add_dict_entry(
              pdf_ocg_ocproperties_key(),
              PdfIndirect(ocproperties_number),
            ),
          )
          pdf.set_root(new_root)
          pdf.set_trailer_dict(
            pdf
            .trailer_dict()
            .add_dict_entry(pdf_ocg_root_key(), PdfIndirect(new_root)),
          )
          pdf
        }
        _ => self.copy()
      }
  }
}

///|
/// Compatibility wrapper for `PdfDocument::ocg_order_all`.
pub fn pdf_ocg_order_all(document : PdfDocument) -> PdfDocument raise PdfError {
  document.ocg_order_all()
}

///|
fn pdf_ocg_copy_usage(usage : PdfOcgUsage) -> PdfOcgUsage {
  {
    creator_info: usage.creator_info,
    language: usage.language,
    export_state: usage.export_state,
    zoom_min: usage.zoom_min,
    zoom_max: usage.zoom_max,
    print_subtype: usage.print_subtype,
    print_state: usage.print_state,
    view_state: usage.view_state,
    user: match usage.user {
      Some((user_type, names)) => Some((user_type, names.copy()))
      None => None
    },
    page_element_subtype: usage.page_element_subtype,
  }
}

///|
fn pdf_ocg_copy_ocg(ocg : PdfOcg) -> PdfOcg {
  {
    name: ocg.name,
    intent: ocg.intent.copy(),
    usage: match ocg.usage {
      Some(usage) => Some(pdf_ocg_copy_usage(usage))
      None => None
    },
  }
}

///|
fn pdf_ocg_copy_order(
  order : ArrayView[(PdfBytes?, Array[Int])],
) -> Array[(PdfBytes?, Array[Int])] {
  [
    for item in order => (item.0, item.1.copy())
  ]
}

///|
fn pdf_ocg_copy_groups(groups : ArrayView[Array[Int]]) -> Array[Array[Int]] {
  [
    for group in groups => group.copy()
  ]
}

///|
fn pdf_ocg_copy_appdict(appdict : PdfOcgAppDict) -> PdfOcgAppDict {
  {
    event: appdict.event,
    ocgs: appdict.ocgs.copy(),
    category: appdict.category.copy(),
  }
}

///|
fn pdf_ocg_copy_config(config : PdfOcgConfig) -> PdfOcgConfig {
  {
    name: config.name,
    creator: config.creator,
    base_state: config.base_state,
    on: config.on.copy(),
    off: config.off.copy(),
    intent: config.intent.copy(),
    usage_applications: [
      for appdict in config.usage_applications => pdf_ocg_copy_appdict(appdict)
    ],
    order: match config.order {
      Some(order) => Some(pdf_ocg_copy_order(order))
      None => None
    },
    list_mode: config.list_mode,
    radio_button_groups: match config.radio_button_groups {
      Some(groups) => Some(pdf_ocg_copy_groups(groups))
      None => None
    },
    locked: config.locked.copy(),
  }
}

///|
/// Merge optional-content metadata from already-renumbered documents.
///
/// Documents without optional content are skipped. Indirect object numbers are
/// copied as-is into the merged result, so callers must renumber source
/// documents before merging when object numbers may collide.
pub fn pdf_optional_content_merge(
  documents : ArrayView[PdfDocument],
) -> PdfOptionalContent? raise PdfError {
  let contents : Array[PdfOptionalContent] = []
  for document in documents {
    match document.read_optional_content_groups() {
      Some(content) => contents.push(content)
      None => ()
    }
  }
  if contents.length() == 0 {
    None
  } else {
    let ocgs : Array[(Int, PdfOcg)] = []
    let configs : Array[PdfOcgConfig] = []
    let on : Array[Int] = []
    let off : Array[Int] = []
    let order : Array[(PdfBytes?, Array[Int])] = []
    let radio_button_groups : Array[Array[Int]] = []
    let mut has_order = false
    let mut has_radio_button_groups = false
    let mut name : PdfBytes? = None
    let mut creator : PdfBytes? = None
    let mut base_state = OcgOn
    let mut intent : Array[PdfName] = []
    let mut usage_applications : Array[PdfOcgAppDict] = []
    let mut list_mode = OcgAllPages
    let mut locked : Array[Int] = []
    for content in contents {
      for entry in content.ocgs {
        ocgs.push((entry.0, pdf_ocg_copy_ocg(entry.1)))
      }
      for config in content.configs {
        configs.push(pdf_ocg_copy_config(config))
      }
      let default_config = content.default_config
      for number in default_config.on {
        on.push(number)
      }
      for number in default_config.off {
        off.push(number)
      }
      match default_config.order {
        Some(items) => {
          has_order = true
          for item in pdf_ocg_copy_order(items) {
            order.push(item)
          }
        }
        None => ()
      }
      match default_config.radio_button_groups {
        Some(groups) => {
          has_radio_button_groups = true
          for group in pdf_ocg_copy_groups(groups) {
            radio_button_groups.push(group)
          }
        }
        None => ()
      }
      if name is None {
        name = default_config.name
      }
      if creator is None {
        creator = default_config.creator
      }
      if base_state == OcgOn && default_config.base_state != OcgOn {
        base_state = default_config.base_state
      }
      if intent.length() == 0 && default_config.intent.length() > 0 {
        intent = default_config.intent.copy()
      }
      if usage_applications.length() == 0 &&
        default_config.usage_applications.length() > 0 {
        usage_applications = [
          for appdict in default_config.usage_applications => {
            pdf_ocg_copy_appdict(appdict)
          }
        ]
      }
      if list_mode == OcgAllPages && default_config.list_mode != OcgAllPages {
        list_mode = default_config.list_mode
      }
      if locked.length() == 0 && default_config.locked.length() > 0 {
        locked = default_config.locked.copy()
      }
    }
    Some({
      ocgs,
      default_config: {
        name,
        creator,
        base_state,
        on,
        off,
        intent,
        usage_applications,
        order: if has_order {
          Some(order)
        } else {
          None
        },
        list_mode,
        radio_button_groups: if has_radio_button_groups {
          Some(radio_button_groups)
        } else {
          None
        },
        locked,
      },
      configs,
    })
  }
}

///|
fn pdf_ocg_representative_number(
  representatives : ArrayView[(PdfBytes, Int)],
  name : PdfBytes,
) -> Int? {
  for representative in representatives {
    if representative.0 == name {
      break Some(representative.1)
    }
  } nobreak {
    None
  }
}

///|
fn pdf_ocg_changed_number(changes : ArrayView[(Int, Int)], number : Int) -> Int {
  for change in changes {
    if change.0 == number {
      break change.1
    }
  } nobreak {
    number
  }
}

///|
fn pdf_ocg_push_unique_number(output : Array[Int], number : Int) -> Unit {
  if !output.contains(number) {
    output.push(number)
  }
}

///|
fn pdf_ocg_coalesce_numbers(
  changes : ArrayView[(Int, Int)],
  numbers : ArrayView[Int],
) -> Array[Int] {
  let output : Array[Int] = []
  for number in numbers {
    pdf_ocg_push_unique_number(output, pdf_ocg_changed_number(changes, number))
  }
  output
}

///|
fn pdf_ocg_coalesce_order(
  changes : ArrayView[(Int, Int)],
  order : ArrayView[(PdfBytes?, Array[Int])],
) -> Array[(PdfBytes?, Array[Int])] {
  let output : Array[(PdfBytes?, Array[Int])] = []
  let seen : Array[Int] = []
  for item in order {
    let numbers : Array[Int] = []
    for number in item.1 {
      let changed = pdf_ocg_changed_number(changes, number)
      if !seen.contains(changed) {
        seen.push(changed)
        numbers.push(changed)
      }
    }
    if numbers.length() > 0 {
      output.push((item.0, numbers))
    }
  }
  output
}

///|
fn pdf_ocg_coalesce_appdict(
  changes : ArrayView[(Int, Int)],
  appdict : PdfOcgAppDict,
) -> PdfOcgAppDict {
  { ..appdict, ocgs: pdf_ocg_coalesce_numbers(changes, appdict.ocgs) }
}

///|
fn pdf_ocg_coalesce_config(
  changes : ArrayView[(Int, Int)],
  config : PdfOcgConfig,
) -> PdfOcgConfig {
  {
    ..config,
    on: pdf_ocg_coalesce_numbers(changes, config.on),
    off: pdf_ocg_coalesce_numbers(changes, config.off),
    usage_applications: [
      for appdict in config.usage_applications => {
        pdf_ocg_coalesce_appdict(changes, appdict)
      }
    ],
    order: match config.order {
      Some(order) => Some(pdf_ocg_coalesce_order(changes, order))
      None => None
    },
    radio_button_groups: match config.radio_button_groups {
      Some(groups) =>
        Some(
          [
            for group in groups => pdf_ocg_coalesce_numbers(changes, group)
          ],
        )
      None => None
    },
    locked: pdf_ocg_coalesce_numbers(changes, config.locked),
  }
}

///|
fn pdf_ocg_coalesce_content(
  content : PdfOptionalContent,
) -> (PdfOptionalContent, Array[(Int, Int)]) {
  let representatives : Array[(PdfBytes, Int)] = []
  let ocgs : Array[(Int, PdfOcg)] = []
  let changes : Array[(Int, Int)] = []
  for entry in content.ocgs {
    let (number, ocg) = entry
    match pdf_ocg_representative_number(representatives, ocg.name) {
      Some(representative) =>
        if representative != number {
          changes.push((number, representative))
        }
      None => {
        representatives.push((ocg.name, number))
        ocgs.push((number, pdf_ocg_copy_ocg(ocg)))
      }
    }
  }
  (
    {
      ocgs,
      default_config: pdf_ocg_coalesce_config(changes, content.default_config),
      configs: [
        for config in content.configs => {
          pdf_ocg_coalesce_config(changes, config)
        }
      ],
    },
    changes,
  )
}

///|
/// Return a copy with same-named optional-content groups coalesced.
///
/// The first group for each raw PDF name is kept. Duplicate group references in
/// optional-content metadata and parsed document objects are rewritten to the
/// kept object number, and duplicate OCG objects are removed.
pub fn PdfDocument::ocg_coalesce(
  self : PdfDocument,
) -> PdfDocument raise PdfError {
  match self.read_optional_content_groups() {
    None => self.copy()
    Some(content) => {
      let (coalesced, changes) = pdf_ocg_coalesce_content(content)
      if changes.length() == 0 {
        self.copy()
      } else {
        let pdf = self.write_optional_content_groups(coalesced)
        pdf.pdf_page_renumber_references(changes)
        for change in changes {
          pdf.remove_object(change.0)
        }
        pdf
      }
    }
  }
}

///|
/// Compatibility wrapper for `PdfDocument::ocg_coalesce`.
pub fn pdf_ocg_coalesce(document : PdfDocument) -> PdfDocument raise PdfError {
  document.ocg_coalesce()
}

///|
fn pdf_ocg_name_array_object(names : ArrayView[PdfName]) -> PdfObject {
  PdfArray([ for name in names => PdfNameObject(name) ])
}

///|
fn pdf_ocg_indirect_array_object(numbers : ArrayView[Int]) -> PdfObject {
  PdfArray([ for number in numbers => PdfIndirect(number) ])
}

///|
fn pdf_ocg_string_array_object(strings : ArrayView[PdfBytes]) -> PdfObject {
  PdfArray([ for string in strings => PdfString(string) ])
}

///|
fn pdf_ocg_write_appdict(appdict : PdfOcgAppDict) -> PdfObject {
  let event_name = match appdict.event {
    OcgView => pdf_ocg_view_name()
    OcgPrint => pdf_ocg_print_name()
    OcgExport => pdf_ocg_export_name()
  }
  PdfDictionary([
    (pdf_ocg_event_key(), PdfNameObject(event_name)),
    (pdf_ocg_ocgs_key(), pdf_ocg_indirect_array_object(appdict.ocgs)),
    (pdf_ocg_category_key(), pdf_ocg_name_array_object(appdict.category)),
  ])
}

///|
fn PdfDocument::pdf_ocg_write_usage(
  self : PdfDocument,
  usage : PdfOcgUsage,
) -> Int {
  let entries : Array[(PdfName, PdfObject)] = []
  match usage.creator_info {
    Some((creator, subtype)) =>
      entries.push(
        (
          pdf_ocg_creatorinfo_key(),
          PdfDictionary([
            (pdf_ocg_creator_key(), PdfString(creator)),
            (pdf_ocg_subtype_key(), PdfNameObject(subtype)),
          ]),
        ),
      )
    None => ()
  }
  match usage.language {
    Some((lang, None)) =>
      entries.push(
        (
          pdf_ocg_language_key(),
          PdfDictionary([(pdf_ocg_lang_key(), PdfString(lang))]),
        ),
      )
    Some((lang, Some(preferred))) =>
      entries.push(
        (
          pdf_ocg_language_key(),
          PdfDictionary([
            (pdf_ocg_lang_key(), PdfString(lang)),
            (pdf_ocg_preferred_key(), PdfNameObject(preferred)),
          ]),
        ),
      )
    None => ()
  }
  match usage.export_state {
    Some(state) =>
      entries.push(
        (
          pdf_ocg_export_key(),
          PdfDictionary([(pdf_ocg_export_state_key(), PdfNameObject(state))]),
        ),
      )
    None => ()
  }
  match (usage.zoom_min, usage.zoom_max) {
    (Some(min), Some(max)) =>
      entries.push(
        (
          pdf_ocg_zoom_key(),
          PdfDictionary([
            (pdf_ocg_min_key(), PdfReal(min)),
            (pdf_ocg_max_key(), PdfReal(max)),
          ]),
        ),
      )
    (Some(min), None) =>
      entries.push(
        (pdf_ocg_zoom_key(), PdfDictionary([(pdf_ocg_min_key(), PdfReal(min))])),
      )
    (None, Some(max)) =>
      entries.push(
        (pdf_ocg_zoom_key(), PdfDictionary([(pdf_ocg_max_key(), PdfReal(max))])),
      )
    _ => ()
  }
  match (usage.print_subtype, usage.print_state) {
    (Some(subtype), Some(state)) =>
      entries.push(
        (
          pdf_ocg_print_key(),
          PdfDictionary([
            (pdf_ocg_subtype_key(), PdfNameObject(subtype)),
            (pdf_ocg_print_state_key(), PdfNameObject(state)),
          ]),
        ),
      )
    (Some(subtype), None) =>
      entries.push(
        (
          pdf_ocg_print_key(),
          PdfDictionary([(pdf_ocg_subtype_key(), PdfNameObject(subtype))]),
        ),
      )
    (None, Some(state)) =>
      entries.push(
        (
          pdf_ocg_print_key(),
          PdfDictionary([(pdf_ocg_print_state_key(), PdfNameObject(state))]),
        ),
      )
    _ => ()
  }
  match usage.view_state {
    Some(state) =>
      entries.push(
        (
          pdf_ocg_view_key(),
          PdfDictionary([(pdf_ocg_view_state_key(), PdfNameObject(state))]),
        ),
      )
    None => ()
  }
  match usage.user {
    Some((user_type, names)) =>
      entries.push(
        (
          pdf_ocg_user_key(),
          PdfDictionary([
            (pdf_ocg_type_key(), PdfNameObject(user_type)),
            (pdf_ocg_name_key(), pdf_ocg_string_array_object(names)),
          ]),
        ),
      )
    None => ()
  }
  match usage.page_element_subtype {
    Some(subtype) =>
      entries.push(
        (
          pdf_ocg_page_element_key(),
          PdfDictionary([(pdf_ocg_subtype_key(), PdfNameObject(subtype))]),
        ),
      )
    None => ()
  }
  self.add_object(PdfDictionary(entries))
}

///|
fn PdfDocument::pdf_ocg_write_config(
  self : PdfDocument,
  config : PdfOcgConfig,
) -> Int {
  let entries : Array[(PdfName, PdfObject)] = []
  match config.name {
    Some(name) => entries.push((pdf_ocg_name_key(), PdfString(name)))
    None => ()
  }
  match config.creator {
    Some(creator) => entries.push((pdf_ocg_creator_key(), PdfString(creator)))
    None => ()
  }
  match config.base_state {
    OcgOn => ()
    OcgOff =>
      entries.push(
        (pdf_ocg_base_state_key(), PdfNameObject(pdf_ocg_off_name())),
      )
    OcgUnchanged =>
      entries.push(
        (pdf_ocg_base_state_key(), PdfNameObject(pdf_ocg_unchanged_name())),
      )
  }
  if config.on.length() > 0 {
    entries.push((pdf_ocg_on_key(), pdf_ocg_indirect_array_object(config.on)))
  }
  if config.off.length() > 0 {
    entries.push((pdf_ocg_off_key(), pdf_ocg_indirect_array_object(config.off)))
  }
  if config.intent.length() > 0 {
    entries.push(
      (pdf_ocg_intent_key(), pdf_ocg_name_array_object(config.intent)),
    )
  }
  if config.usage_applications.length() > 0 {
    entries.push(
      (
        pdf_ocg_as_key(),
        PdfArray(
          [
            for appdict in config.usage_applications => {
              pdf_ocg_write_appdict(appdict)
            }
          ],
        ),
      ),
    )
  }
  match config.order {
    Some(order) => {
      let order_objects : Array[PdfObject] = []
      for item in order {
        let (label, numbers) = item
        match label {
          None =>
            if numbers.length() == 1 {
              order_objects.push(PdfIndirect(numbers[0]))
            } else {
              order_objects.push(pdf_ocg_indirect_array_object(numbers))
            }
          Some(label) => {
            let children : Array[PdfObject] = [PdfString(label)]
            for number in numbers {
              children.push(PdfIndirect(number))
            }
            order_objects.push(PdfArray(children))
          }
        }
      }
      entries.push((pdf_ocg_order_key(), PdfArray(order_objects)))
    }
    None => ()
  }
  match config.list_mode {
    OcgAllPages => ()
    OcgVisiblePages =>
      entries.push(
        (pdf_ocg_list_mode_key(), PdfNameObject(pdf_ocg_visible_pages_name())),
      )
  }
  match config.radio_button_groups {
    Some(groups) =>
      entries.push(
        (
          pdf_ocg_rb_groups_key(),
          PdfArray(
            [
              for group in groups => pdf_ocg_indirect_array_object(group)
            ],
          ),
        ),
      )
    None => ()
  }
  if config.locked.length() > 0 {
    entries.push(
      (pdf_ocg_locked_key(), pdf_ocg_indirect_array_object(config.locked)),
    )
  }
  self.add_object(PdfDictionary(entries))
}

///|
fn PdfDocument::pdf_ocg_write_ocg(
  self : PdfDocument,
  ocg : PdfOcg,
) -> PdfObject {
  let entries : Array[(PdfName, PdfObject)] = [
    (pdf_ocg_type_key(), PdfNameObject(pdf_ocg_ocg_name())),
    (pdf_ocg_name_key(), PdfString(ocg.name)),
    (pdf_ocg_intent_key(), pdf_ocg_name_array_object(ocg.intent)),
  ]
  match ocg.usage {
    Some(usage) =>
      entries.push(
        (pdf_ocg_usage_key(), PdfIndirect(self.pdf_ocg_write_usage(usage))),
      )
    None => ()
  }
  PdfDictionary(entries)
}

///|
/// Return a copy of the document with catalog `/OCProperties` removed.
///
/// The original document is not mutated; the returned document receives a new
/// root object and trailer root reference.
pub fn PdfDocument::remove_optional_content_groups(
  self : PdfDocument,
) -> PdfDocument raise PdfError {
  let root = self.pdf_ocg_root()
  let pdf = self.copy()
  let root_without_ocproperties = root.remove_dict_entry(
    pdf_ocg_ocproperties_key(),
  )
  let new_root = pdf.add_object(root_without_ocproperties)
  pdf.set_root(new_root)
  pdf.set_trailer_dict(
    pdf.trailer_dict().add_dict_entry(pdf_ocg_root_key(), PdfIndirect(new_root)),
  )
  pdf
}

///|
/// Compatibility wrapper for `PdfDocument::remove_optional_content_groups`.
pub fn pdf_remove_ocg(document : PdfDocument) -> PdfDocument raise PdfError {
  document.remove_optional_content_groups()
}

///|
/// Return a copy of the document with catalog `/OCProperties` replaced.
///
/// OCG dictionaries are written using the object numbers stored in
/// `optional_content.ocgs`. Configuration dictionaries are added as new
/// objects, and the original document is left unchanged.
pub fn PdfDocument::write_optional_content_groups(
  self : PdfDocument,
  optional_content : PdfOptionalContent,
) -> PdfDocument raise PdfError {
  let root = self.pdf_ocg_root()
  let pdf = self.copy()
  let original_max_object_number = pdf.objects.max_object_number()
  let mut max_ocg_number = original_max_object_number
  for entry in optional_content.ocgs {
    if entry.0 > max_ocg_number {
      max_ocg_number = entry.0
    }
  }
  if max_ocg_number > original_max_object_number {
    pdf.add_object_given_number(max_ocg_number, PdfNull)
  }
  for entry in optional_content.ocgs {
    let (number, ocg) = entry
    pdf.add_object_given_number(number, pdf.pdf_ocg_write_ocg(ocg))
  }
  let ocg_array_number = pdf.add_object(
    PdfArray([ for entry in optional_content.ocgs => PdfIndirect(entry.0) ]),
  )
  let default_number = pdf.pdf_ocg_write_config(optional_content.default_config)
  let entries : Array[(PdfName, PdfObject)] = [
    (pdf_ocg_ocgs_key(), PdfIndirect(ocg_array_number)),
    (pdf_ocg_d_key(), PdfIndirect(default_number)),
  ]
  if optional_content.configs.length() > 0 {
    let configs_number = pdf.add_object(
      PdfArray(
        [
          for config in optional_content.configs => {
            PdfIndirect(pdf.pdf_ocg_write_config(config))
          }
        ],
      ),
    )
    entries.push((pdf_ocg_configs_key(), PdfIndirect(configs_number)))
  }
  let ocproperties_number = pdf.add_object(PdfDictionary(entries))
  let new_root = pdf.add_object(
    root.add_dict_entry(
      pdf_ocg_ocproperties_key(),
      PdfIndirect(ocproperties_number),
    ),
  )
  pdf.set_root(new_root)
  pdf.set_trailer_dict(
    pdf.trailer_dict().add_dict_entry(pdf_ocg_root_key(), PdfIndirect(new_root)),
  )
  pdf
}

///|
/// Compatibility wrapper for `PdfDocument::write_optional_content_groups`.
pub fn pdf_write_ocg(
  document : PdfDocument,
  optional_content : PdfOptionalContent,
) -> PdfDocument raise PdfError {
  document.write_optional_content_groups(optional_content)
}