# 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