# -*- encoding: utf-8; frozen_string_literal: true -*-
#
#--
# This file is part of HexaPDF.
#
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
# Copyright (C) 2014-2020 Thomas Leitner
#
# HexaPDF is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License version 3 as
# published by the Free Software Foundation with the addition of the
# following permission added to Section 15 as permitted in Section 7(a):
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
# INFRINGEMENT OF THIRD PARTY RIGHTS.
#
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with HexaPDF. If not, see .
#
# The interactive user interfaces in modified source and object code
# versions of HexaPDF must display Appropriate Legal Notices, as required
# under Section 5 of the GNU Affero General Public License version 3.
#
# In accordance with Section 7(b) of the GNU Affero General Public
# License, a covered work must retain the producer line in every PDF that
# is created or manipulated using HexaPDF.
#
# If the GNU Affero General Public License doesn't fit your need,
# commercial licenses are available at .
#++
module HexaPDF
module Utils
# Provides the convenience methods that are used for name trees and number trees.
#
# The provided methods require two methods defined in the including class so that they work
# correctly:
#
# leaf_node_container_name::
# Defines the dictionary entry name that contains the leaf node entries.
#
# For example, for name trees this would be :Names.
#
# key_type::
# Defines the class that is used for the keys in the tree.
#
# The class defined this way is used for making sure that only valid keys are used.
#
# For example, for name trees this would be String.
#
# Note: Like with HexaPDF::Dictionary, the keys are assumed to always be direct objects!
#
# See: HexaPDF::NameTreeNode, HexaPDF::NumberTreeNode
module SortedTreeNode
# Tree nodes must always be indirect.
#
# Note: There is no requirement that the root node of a tree must be indirect. However, making
# it indirect simplifies the implementation and is not against the spec.
def must_be_indirect?
true
end
# :call-seq:
# tree.add_entry(key, data, overwrite: true) -> true or false
#
# Adds a new tree entry (key-data pair) to the sorted tree and returns +true+ if it was
# successfully added.
#
# If the option +overwrite+ is +true+, an existing entry is overwritten. Otherwise an error is
# raised.
#
# This method has to be invoked on the root node of the tree!
def add_entry(key, data, overwrite: true)
if key?(:Limits)
raise HexaPDF::Error, "Adding a new tree entry is only allowed via the root node"
elsif !key.kind_of?(key_type)
raise ArgumentError, "A key must be a #{key_type} object, not a #{key.class}"
end
container_name = leaf_node_container_name
if (!key?(:Kids) && !key?(container_name)) ||
(value[:Kids] && self[:Kids].empty?)
value.delete(:Kids)
value[container_name] = []
end
if key?(container_name)
result = insert_pair(self[container_name], key, data, overwrite: overwrite)
split_if_needed(self, self)
else
stack = []
path_to_key(self, key, stack)
result = insert_pair(stack.last[container_name], key, data, overwrite: overwrite)
stack.last[:Limits] = stack.last[container_name].values_at(0, -2)
stack.reverse_each.inject do |nested_node, node|
nested_lower = nested_node[:Limits][0]
nested_upper = nested_node[:Limits][1]
if node[:Limits][0] > nested_lower
node[:Limits][0] = nested_lower
elsif node[:Limits][1] < nested_upper
node[:Limits][1] = nested_upper
end
node
end
split_if_needed(stack[-2] || self, stack[-1])
end
result
end
# Deletes the entry specified by the +key+ from the tree and returns the data. If the tree
# doesn't contain the key, +nil+ is returned.
#
# This method has to be invoked on the root node of the tree!
def delete_entry(key)
if key?(:Limits)
raise HexaPDF::Error, "Deleting a tree entry is only allowed via the root node"
end
stack = [self]
path_to_key(self, key, stack)
container_name = leaf_node_container_name
return unless stack.last[container_name]
index = find_in_leaf_node(stack.last[container_name], key)
return unless stack.last[container_name][index] == key
stack.last[container_name].delete_at(index) # deletes key
value = stack.last[container_name].delete_at(index)
stack.last[:Limits] = stack.last[container_name].values_at(0, -2) if stack.last[:Limits]
stack.reverse_each.inject do |nested_node, node|
if (!nested_node[container_name] || nested_node[container_name].empty?) &&
(!nested_node[:Kids] || nested_node[:Kids].empty?)
node[:Kids].delete_at(node[:Kids].index {|n| document.deref(n) == nested_node })
document.delete(nested_node)
end
if !node[:Kids].empty? && node[:Limits]
node[:Limits][0] = document.deref(node[:Kids][0])[:Limits][0]
node[:Limits][1] = document.deref(node[:Kids][-1])[:Limits][1]
end
node
end
value
end
# Finds and returns the associated entry for the key, or returns +nil+ if no such key is
# found.
def find_entry(key)
container_name = leaf_node_container_name
node = self
result = nil
while result.nil?
if node.key?(container_name)
index = find_in_leaf_node(node[container_name], key)
if node[container_name][index] == key
result = document.deref(node[container_name][index + 1])
end
elsif node.key?(:Kids)
index = find_in_intermediate_node(node[:Kids], key)
node = document.deref(node[:Kids][index])
break unless key >= node[:Limits][0] && key <= node[:Limits][1]
else
break
end
end
result
end
# :call-seq:
# node.each_entry {|key, data| block } -> node
# node.each_entry -> Enumerator
#
# Calls the given block once for each entry (key-data pair) of the sorted tree.
def each_entry
return to_enum(__method__) unless block_given?
container_name = leaf_node_container_name
stack = [self]
until stack.empty?
node = document.deref(stack.pop)
if node.key?(container_name)
data = node[container_name]
index = 0
while index < data.length
yield(data[index], document.deref(data[index + 1]))
index += 2
end
elsif node.key?(:Kids)
stack.concat(node[:Kids].reverse_each.to_a)
end
end
self
end
private
# Starting from node traverses the tree to the node where the key is located or, if not
# present, where it would be located and adds the nodes to the stack.
def path_to_key(node, key, stack)
return unless node.key?(:Kids)
index = find_in_intermediate_node(node[:Kids], key)
stack << document.deref(node[:Kids][index])
path_to_key(stack.last, key, stack)
end
# Returns the index into the /Kids array where the entry for +key+ is located or, if not
# present, where it would be located.
def find_in_intermediate_node(array, key)
left = 0
right = array.length - 1
while left < right
mid = (left + right) / 2
limits = document.deref(array[mid])[:Limits]
if limits[1] < key
left = mid + 1
elsif limits[0] > key
right = mid - 1
else
left = right = mid
end
end
left
end
# Inserts the key-data pair into array at the correct position and returns +true+ if the
# key-data pair was successfully inserted.
#
# An existing entry for the key is only overwritten if the option +overwrite+ is +true+.
def insert_pair(array, key, data, overwrite: true)
index = find_in_leaf_node(array, key)
return false if array[index] == key && !overwrite
if array[index] == key
array[index + 1] = data
else
array.insert(index, key, data)
end
true
end
# Returns the index into the array where the entry for +key+ is located or, if not present,
# where it would be located.
def find_in_leaf_node(array, key)
left = 0
right = array.length - 1
while left <= right
mid = ((left + right) / 2) & ~1 # mid must be even because of [key val key val...]
if array[mid] < key
left = mid + 2
elsif array[mid] > key
right = mid - 2
else
left = mid
right = left - 1
end
end
left
end
# Splits the leaf node if it contains the maximum number of entries.
def split_if_needed(parent, leaf_node)
container_name = leaf_node_container_name
max_size = config['sorted_tree.max_leaf_node_size'] * 2
return unless leaf_node[container_name].size >= max_size
split_point = (max_size / 2) & ~1
if parent == leaf_node
node1 = document.add(document.wrap({}, type: self.class))
node2 = document.add(document.wrap({}, type: self.class))
node1[container_name] = leaf_node[container_name][0, split_point]
node1[:Limits] = node1[container_name].values_at(0, -2)
node2[container_name] = leaf_node[container_name][split_point..-1]
node2[:Limits] = node2[container_name].values_at(0, -2)
parent.delete(container_name)
parent[:Kids] = [node1, node2]
else
node = document.add(document.wrap({}, type: self.class))
node[container_name] = leaf_node[container_name].slice!(split_point..-1)
node[:Limits] = node[container_name].values_at(0, -2)
leaf_node[:Limits][1] = leaf_node[container_name][-2]
index = 1 + parent[:Kids].index {|o| document.deref(o) == leaf_node }
parent[:Kids].insert(index, node)
end
end
# Validates the sorted tree node.
def perform_validation
super
container_name = leaf_node_container_name
# All kids entries must be indirect objects
if key?(:Kids)
self[:Kids].each do |kid|
unless (kid.kind_of?(HexaPDF::Object) && kid.indirect?) ||
kid.kind_of?(HexaPDF::Reference)
yield("Child entries of sorted tree nodes must be indirect objects", false)
end
end
end
# All keys of the container must be lexically ordered strings and the container must be
# correctly formatted
if key?(container_name)
container = self[container_name]
if container.length.odd?
yield("Sorted tree leaf node contains odd number of entries", false)
end
index = 0
old = nil
while index < container.length
key = document.unwrap(container[index])
if !key.kind_of?(key_type)
yield("A key must be a #{key_type} object, not a #{key.class}", false)
elsif old && old > key
yield("Sorted tree leaf node entries are not correctly sorted", false)
end
old = key
index += 2
end
end
end
end
end
end