# coding: utf-8
# typed: strict
# frozen_string_literal: true
module PDF
class Reader
# Page#walk will execute the content stream of a page, calling methods on a receiver class
# provided by the user. Each operator has a specific set of parameters it expects, and we
# wrap the users receiver class in this one to verify the PDF uses valid parameters.
#
# Without these checks, users can't be confident about the number of parameters they'll receive
# for an operator, or what the type of those parameters will be. Everyone ends up building their
# own type safety guard clauses and it's tedious.
#
# Not all operators have type safety implemented yet, but we can expand the number over time.
class ValidatingReceiver
def initialize(wrapped)
@wrapped = wrapped
end
def page=(page)
call_wrapped(:page=, page)
end
#####################################################
# Graphics State Operators
#####################################################
def save_graphics_state(*args)
call_wrapped(:save_graphics_state)
end
def restore_graphics_state(*args)
call_wrapped(:restore_graphics_state)
end
#####################################################
# Matrix Operators
#####################################################
def concatenate_matrix(*args)
a, b, c, d, e, f = *args
call_wrapped(
:concatenate_matrix,
TypeCheck.cast_to_numeric!(a),
TypeCheck.cast_to_numeric!(b),
TypeCheck.cast_to_numeric!(c),
TypeCheck.cast_to_numeric!(d),
TypeCheck.cast_to_numeric!(e),
TypeCheck.cast_to_numeric!(f),
)
end
#####################################################
# Text Object Operators
#####################################################
def begin_text_object(*args)
call_wrapped(:begin_text_object)
end
def end_text_object(*args)
call_wrapped(:end_text_object)
end
#####################################################
# Text State Operators
#####################################################
def set_character_spacing(*args)
char_spacing, _ = *args
call_wrapped(
:set_character_spacing,
TypeCheck.cast_to_numeric!(char_spacing)
)
end
def set_horizontal_text_scaling(*args)
h_scaling, _ = *args
call_wrapped(
:set_horizontal_text_scaling,
TypeCheck.cast_to_numeric!(h_scaling)
)
end
def set_text_font_and_size(*args)
label, size, _ = *args
call_wrapped(
:set_text_font_and_size,
TypeCheck.cast_to_symbol(label),
TypeCheck.cast_to_numeric!(size)
)
end
def set_text_leading(*args)
leading, _ = *args
call_wrapped(
:set_text_leading,
TypeCheck.cast_to_numeric!(leading)
)
end
def set_text_rendering_mode(*args)
mode, _ = *args
call_wrapped(
:set_text_rendering_mode,
TypeCheck.cast_to_numeric!(mode)
)
end
def set_text_rise(*args)
rise, _ = *args
call_wrapped(
:set_text_rise,
TypeCheck.cast_to_numeric!(rise)
)
end
def set_word_spacing(*args)
word_spacing, _ = *args
call_wrapped(
:set_word_spacing,
TypeCheck.cast_to_numeric!(word_spacing)
)
end
#####################################################
# Text Positioning Operators
#####################################################
def move_text_position(*args) # Td
x, y, _ = *args
call_wrapped(
:move_text_position,
TypeCheck.cast_to_numeric!(x),
TypeCheck.cast_to_numeric!(y)
)
end
def move_text_position_and_set_leading(*args) # TD
x, y, _ = *args
call_wrapped(
:move_text_position_and_set_leading,
TypeCheck.cast_to_numeric!(x),
TypeCheck.cast_to_numeric!(y)
)
end
def set_text_matrix_and_text_line_matrix(*args) # Tm
a, b, c, d, e, f = *args
call_wrapped(
:set_text_matrix_and_text_line_matrix,
TypeCheck.cast_to_numeric!(a),
TypeCheck.cast_to_numeric!(b),
TypeCheck.cast_to_numeric!(c),
TypeCheck.cast_to_numeric!(d),
TypeCheck.cast_to_numeric!(e),
TypeCheck.cast_to_numeric!(f),
)
end
def move_to_start_of_next_line(*args) # T*
call_wrapped(:move_to_start_of_next_line)
end
#####################################################
# Text Showing Operators
#####################################################
def show_text(*args) # Tj (AWAY)
string, _ = *args
call_wrapped(
:show_text,
TypeCheck.cast_to_string!(string)
)
end
def show_text_with_positioning(*args) # TJ [(A) 120 (WA) 20 (Y)]
params, _ = *args
unless params.is_a?(Array)
raise MalformedPDFError, "TJ operator expects a single Array argument"
end
call_wrapped(
:show_text_with_positioning,
params
)
end
def move_to_next_line_and_show_text(*args) # '
string, _ = *args
call_wrapped(
:move_to_next_line_and_show_text,
TypeCheck.cast_to_string!(string)
)
end
def set_spacing_next_line_show_text(*args) # "
aw, ac, string = *args
call_wrapped(
:set_spacing_next_line_show_text,
TypeCheck.cast_to_numeric!(aw),
TypeCheck.cast_to_numeric!(ac),
TypeCheck.cast_to_string!(string)
)
end
#####################################################
# Form XObject Operators
#####################################################
def invoke_xobject(*args)
label, _ = *args
call_wrapped(
:invoke_xobject,
TypeCheck.cast_to_symbol(label)
)
end
#####################################################
# Inline Image Operators
#####################################################
def begin_inline_image(*args)
call_wrapped(:begin_inline_image)
end
def begin_inline_image_data(*args)
# We can't use call_wrapped() here because sorbet won't allow splat args with a dynamic
# number of elements
@wrapped.begin_inline_image_data(*args) if @wrapped.respond_to?(:begin_inline_image_data)
end
def end_inline_image(*args)
data, _ = *args
call_wrapped(
:end_inline_image,
TypeCheck.cast_to_string!(data)
)
end
#####################################################
# Final safety net for any operators that don't have type checking enabled yet
#####################################################
def respond_to?(meth)
@wrapped.respond_to?(meth)
end
def method_missing(methodname, *args)
@wrapped.send(methodname, *args)
end
private
def call_wrapped(methodname, *args)
@wrapped.send(methodname, *args) if @wrapped.respond_to?(methodname)
end
end
end
end