class HexaPDF::Type::OutlineItem

See: PDF2.0 s12.3.3
it is not recommended to manually do this but to rely on the provided convenience methods.
Since many dictionary keys need to be kept up-to-date when manipulating the outline item tree,
respectively the first and last descendant items.
and /Prev keys. Each item may have descendant items. If so, the /First and /Last keys point to
Outline item dictionaries are connected together in the form of a linked list using the /Next
sense to do this when the item has children.
If no destination/action is set, the item just acts as kind of a header. It usually only makes
items.
Additionally, items may have child items which makes it possible to create a hierarchy of
the text should appear bold and/or italic).
(either a simple destination or an explicit action object), the text color, and flags (whether
An item has a title and some optional attributes: the action that is activated when clicking
Represents an outline item dictionary.

def action(value = nil)

action value, the destination is automatically deleted.
If an action is set, the destination has to be unset; and vice versa. So when setting an

the given value (needs to be a valid HexaPDF::Type::Action dictionary).
Returns the item's action if no argument is given. Otherwise sets the action to

item.action(value) -> action
item.action -> action
:call-seq:
def action(value = nil)
  if value
    delete(:Dest)
    self[:A] = value
  else
    self[:A]
  end
end

def add_item(title, destination: nil, action: nil, position: :last, open: true,

end
item.add("First subitem", position: :first, destination: doc.pages[0])
item.add("Second subitem", destination: doc.pages[1]) # links to page 2
doc.destinations.add("Title") do |item| # no action, just container

Examples:

item text, see #flags for detail. Default is to use no variant.
An array of font variants (possible values are :bold and :italic) to set for the outline

flags::

#text_color for details). If not set, the text appears in black.
The text color of the outline item text which needs to be a valid RGB color (see

text_color::

or closed. Default: +true+.
Specifies whether the outline item should be open (i.e. one or more children are shown)

open::

zero-based index.
Integer:: When non-negative inserts before, otherwise after, the item at the given
+:last+:: Insert as last item (default)
+:first+:: Insert as first item

The position where the new child item should be inserted. Can either be:

position::

:action argument takes precedence.
HexaPDF::Type::Action for details. If the argument :destination is also specified, the
Specifies the action that should be taken when clicking on the outline item. See

action::

takes precedence if it is also specified,
See HexaPDF::Document::Destinations#use_or_create for details. The argument :action
Specifies the destination that should be activated when clicking on the outline item.

destination::

If a block is specified, the newly created item is yielded.

container.
This is only meaningful if the new item will have children as it then acts just as a
If neither :destination nor :action is specified, the outline item has no associated action.

appropriately.
/Next, /First, /Last, /Parent and /Count are deleted from the given item and set
title. If so, the only other argument that is used is +position+. Existing fields /Prev,
Alternatively, it is possible to provide an already initialized outline item instead of the

provided action on clicking. Returns the newly added item.
Adds, as child to this item, a new outline item with the given title that performs the
def add_item(title, destination: nil, action: nil, position: :last, open: true,
             text_color: nil, flags: nil) # :yield: item
  if title.kind_of?(HexaPDF::Object) && title.type == :XXOutlineItem
    item = title
    item.delete(:Prev)
    item.delete(:Next)
    item.delete(:First)
    item.delete(:Last)
    if item[:Count] && item[:Count] >= 0
      item[:Count] = 0
    else
      item.delete(:Count)
    end
    item[:Parent] = self
  else
    item = document.add({Parent: self}, type: :XXOutlineItem)
    item.title(title)
    if action
      item.action(action)
    else
      item.destination(destination)
    end
    item.text_color(text_color) if text_color
    item.flag(*flags) if flags
    item[:Count] = 0 if open # Count=0 means open if items are later added
  end
  unless position == :last || position == :first || position.kind_of?(Integer)
    raise ArgumentError, "position must be :first, :last, or an integer"
  end
  if self[:First]
    case position
    when :last, -1
      item[:Prev] = self[:Last]
      self[:Last][:Next] = item
      self[:Last] = item
    when :first, 0
      item[:Next] = self[:First]
      self[:First][:Prev] = item
      self[:First] = item
    when Integer
      temp, direction = if position > 0
                          [self[:First], :Next]
                        else
                          position = -position - 2
                          [self[:Last], :Prev]
                        end
      position.times { temp &&= temp[direction] }
      raise ArgumentError, "position out of bounds" if temp.nil?
      item[:Prev] = temp[:Prev]
      item[:Next] = temp
      temp[:Prev] = item
      item[:Prev][:Next] = item
    end
  else
    self[:First] = self[:Last] = item
  end
  # Re-calculate /Count entries
  temp = self
  while temp
    if !temp.key?(:Count) || temp[:Count] < 0
      temp[:Count] = (temp[:Count] || 0) - 1
      break
    else
      temp[:Count] += 1
    end
    temp = temp[:Parent]
  end
  yield(item) if block_given?
  item
end

def destination(value = nil)

destination value, the action is automatically deleted.
If an action is set, the destination has to be unset; and vice versa. So when setting a

values).
the given value (see HexaPDF::Document::Destinations#use_or_create for the posssible
Returns the item's destination if no argument is given. Otherwise sets the destination to

item.destination(value) -> destination
item.destination -> destination
:call-seq:
def destination(value = nil)
  if value
    delete(:A)
    self[:Dest] = document.destinations.use_or_create(value)
  else
    self[:Dest]
  end
end

def destination_page

* Otherwise +nil+ is returned.
* If an action is set and it is a GoTo action, the associated page is returned.
* If a destination is set, the associated page is returned.

Returns the destination page if there is any.
def destination_page
  dest = self[:Dest]
  dest = action[:D] if !dest && (action = self[:A]) && action[:S] == :GoTo
  document.destinations.resolve(dest)&.page
end

def each_item(&block)

descendants.
The descendant items are yielded in-order, yielding first the item itself and then its

Iterates over all descendant items of this one.

item.each_item -> Enumerator
item.each_item {|descendant_item, level| block } -> item
:call-seq:
def each_item(&block)
  return to_enum(__method__) unless block_given?
  return self unless (item = self[:First])
  level = self.level + 1
  while item
    yield(item, level)
    item.each_item(&block)
    item = item[:Next]
  end
  self
end

def level

Outline item 2 1
|- Sub item 3 2
|- Sub sub item 1 3
|- Sub item 2 2
|- Sub item 1 2
Outline item 1 1
Outline dictionary 0

associated level:
Here is an illustrated example of items contained in a document outline with their

The level of the items in the main outline dictionary, the root level, is 1.

Returns the outline level this item is one.
def level
  count = 0
  temp = self
  count += 1 while (temp = temp[:Parent])
  count
end

def must_be_indirect?

Returns +true+ since outline items must always be indirect objects.
def must_be_indirect?
  true
end

def open?

+nil+:: If this item doesn't (yet) have any child items.
+false+:: If this item is closed, i.e. not showing its child items.
+true+:: If this item is open, i.e. showing its child items.

Returns the open state of the item.
def open?
  self[:First] && key?(:Count) && self[:Count] >= 0
end

def perform_validation # :nodoc:

:nodoc:
def perform_validation # :nodoc:
  super
  first = self[:First]
  last = self[:Last]
  if (first && !last) || (!first && last)
    yield('Outline item dictionary is missing an endpoint reference', true)
    node, dir = first ? [first, :Next] : [last, :Prev]
    node = node[dir] while node[dir]
    self[dir == :Next ? :Last : :First] = node
  elsif !first && !last && self[:Count] && self[:Count] != 0
    yield('Outline item dictionary key /Count set but no descendants exist', true)
    delete(:Count)
  end
  prev_item = self[:Prev]
  if prev_item && (prev_item_next = prev_item[:Next]) != self
    if prev_item_next
      yield('Outline item /Prev points to item whose /Next points somewhere else', false)
    else
      yield('Outline item /Prev points to item without /Next', true)
      prev_item[:Next] = self
    end
  end
  next_item = self[:Next]
  if next_item && (next_item_prev = next_item[:Prev]) != self
    if next_item_prev
      yield('Outline item /Next points to item whose /Prev points somewhere else', false)
    else
      yield('Outline item /Next points to item without /Prev', true)
      next_item[:Prev] = self
    end
  end
end

def text_color(color = nil)

Note: The color *has* to be an RGB color.

HexaPDF::Content::ColorSpace.device_color_from_specification for possible +color+ values.
argument is given. Otherwise sets the text color, see
Returns the item's text color as HexaPDF::Content::ColorSpace::DeviceRGB::Color object if no

item.text_color(color) -> color
item.text_color -> color
:call-seq:
def text_color(color = nil)
  if color
    color = HexaPDF::Content::ColorSpace.device_color_from_specification(color)
    unless color.color_space.family == :DeviceRGB
      raise ArgumentError, "The given argument is not a valid RGB color"
    end
    self[:C] = color.components
  else
    Content::ColorSpace.prenormalized_device_color(self[:C])
  end
end

def title(value = nil)

value.
Returns the item's title if no argument is given. Otherwise sets the title to the given

item.title(value) -> title
item.title -> title
:call-seq:
def title(value = nil)
  if value
    self[:Title] = value
  else
    self[:Title]
  end
end