def textbox(text, properties = {})
      options = {
        x: page_size[0],
        y: page_size[1],
        width: 0,
        height: -1,
        text_align: :center,
        text_valign: :center,
        text_padding: 0.1,
        font: nil,
        font_size: :fit_text,
        max_font_size: nil,
        font_color: [0, 0, 0],
        stroke_color: nil,
        stroke_width: 0,
        box_color: nil,
        border_color: nil,
        border_width: 0,
        box_radius: 0,
        opacity: 1,
        ctm: nil # ~= [1,0,0,1,0,0]
      }
      options.update properties
      # reset the length and height to meaningful values, if negative
      options[:width] = mediabox[2] - options[:x] + options[:width] if options[:width] <= 0
      options[:height] = mediabox[3] - options[:y] + options[:height] if options[:height] <= 0

      # reset the padding value
      options[:text_padding] = 0 if options[:text_padding].to_f >= 1

      # create box stream
      box_stream = ''
      # set graphic state for box
      if options[:box_color] || (options[:border_width].to_i > 0 && options[:border_color])
        # compute x and y position for text
        x = options[:x]
        y = options[:y]

        # set graphic state for the box
        box_stream << "q\n"
        box_stream << "#{options[:ctm].join ' '} cm\n" if options[:ctm]
        box_graphic_state = { ca: options[:opacity], CA: options[:opacity], LW: options[:border_width], LC: 0, LJ: 0, LD: 0 }
        if options[:box_radius] != 0 # if the text box has rounded corners
          box_graphic_state[:LC] = 2
          box_graphic_state[:LJ] = 1
        end
        box_graphic_state = graphic_state box_graphic_state # adds the graphic state to Resources and gets the reference
        box_stream << "#{object_to_pdf box_graphic_state} gs\n"

        # the following line was removed for Acrobat Reader compatability
        # box_stream << "DeviceRGB CS\nDeviceRGB cs\n"

        box_stream << "#{options[:box_color].join(' ')} rg\n" if options[:box_color]
        if options[:border_width].to_i > 0 && options[:border_color]
          box_stream << "#{options[:border_color].join(' ')} RG\n"
        end
        # create the path
        radius = options[:box_radius]
        half_radius = (radius.to_f / 2).round 4
        ## set starting point
        box_stream << "#{options[:x] + radius} #{options[:y]} m\n"
        ## bottom and right corner - first line and first corner
        box_stream << "#{options[:x] + options[:width] - radius} #{options[:y]} l\n" # bottom
        if options[:box_radius] != 0 # make first corner, if not straight.
          box_stream << "#{options[:x] + options[:width] - half_radius} #{options[:y]} "
          box_stream << "#{options[:x] + options[:width]} #{options[:y] + half_radius} "
          box_stream << "#{options[:x] + options[:width]} #{options[:y] + radius} c\n"
        end
        ## right and top-right corner
        box_stream << "#{options[:x] + options[:width]} #{options[:y] + options[:height] - radius} l\n"
        if options[:box_radius] != 0
          box_stream << "#{options[:x] + options[:width]} #{options[:y] + options[:height] - half_radius} "
          box_stream << "#{options[:x] + options[:width] - half_radius} #{options[:y] + options[:height]} "
          box_stream << "#{options[:x] + options[:width] - radius} #{options[:y] + options[:height]} c\n"
        end
        ## top and top-left corner
        box_stream << "#{options[:x] + radius} #{options[:y] + options[:height]} l\n"
        if options[:box_radius] != 0
          box_stream << "#{options[:x] + half_radius} #{options[:y] + options[:height]} "
          box_stream << "#{options[:x]} #{options[:y] + options[:height] - half_radius} "
          box_stream << "#{options[:x]} #{options[:y] + options[:height] - radius} c\n"
        end
        ## left and bottom-left corner
        box_stream << "#{options[:x]} #{options[:y] + radius} l\n"
        if options[:box_radius] != 0
          box_stream << "#{options[:x]} #{options[:y] + half_radius} "
          box_stream << "#{options[:x] + half_radius} #{options[:y]} "
          box_stream << "#{options[:x] + radius} #{options[:y]} c\n"
        end
        # fill / stroke path
        box_stream << "h\n"
        if options[:box_color] && options[:border_width].to_i > 0 && options[:border_color]
          box_stream << "B\n"
        elsif options[:box_color] # fill if fill color is set
          box_stream << "f\n"
        elsif options[:border_width].to_i > 0 && options[:border_color] # stroke if border is set
          box_stream << "S\n"
        end

        # exit graphic state for the box
        box_stream << "Q\n"
      end
      contents << box_stream

      # reset x,y by text alignment - x,y are calculated from the bottom left
      # each unit (1) is 1/72 Inch
      # create text stream
      text_stream = ''
      if !text.to_s.empty? && options[:font_size] != 0 && (options[:font_color] || options[:stroke_color])
        # compute x and y position for text
        x = options[:x] + (options[:width] * options[:text_padding])
        y = options[:y] + (options[:height] * options[:text_padding])

        # set the fonts (fonts array, with :Helvetica as fallback).
        fonts = [*options[:font], :Helvetica]
        # fit text in box, if requested
        font_size = options[:font_size]
        if options[:font_size] == :fit_text
          font_size = fit_text text, fonts, (options[:width] * (1 - options[:text_padding])), (options[:height] * (1 - options[:text_padding]))
          font_size = options[:max_font_size] if options[:max_font_size] && font_size > options[:max_font_size]
        end

        text_size = dimensions_of text, fonts, font_size

        if options[:text_align] == :center
          x = ((options[:width] * (1 - (2 * options[:text_padding]))) - text_size[0]) / 2 + x
        elsif options[:text_align] == :right
          x = ((options[:width] * (1 - (1.5 * options[:text_padding]))) - text_size[0]) + x
        end
        if options[:text_valign] == :center
          y = ((options[:height] * (1 - (2 * options[:text_padding]))) - text_size[1]) / 2 + y
        elsif options[:text_valign] == :top
          y = (options[:height] * (1 - (1.5 * options[:text_padding]))) - text_size[1] + y
        end

        # set graphic state for text
        text_stream << "q\n"
        text_stream << "#{options[:ctm].join ' '} cm\n" if options[:ctm]
        text_graphic_state = graphic_state(ca: options[:opacity], CA: options[:opacity], LW: options[:stroke_width].to_f, LC: 2, LJ: 1, LD: 0)
        text_stream << "#{object_to_pdf text_graphic_state} gs\n"

        # the following line was removed for Acrobat Reader compatability
        # text_stream << "DeviceRGB CS\nDeviceRGB cs\n"

        # set text render mode
        if options[:font_color]
          text_stream << "#{options[:font_color].join(' ')} rg\n"
        end
        if options[:stroke_width].to_i > 0 && options[:stroke_color]
          text_stream << "#{options[:stroke_color].join(' ')} RG\n"
          if options[:font_color]
            text_stream << "2 Tr\n"
          else
            final_stream << "1 Tr\n"
          end
        elsif options[:font_color]
          text_stream << "0 Tr\n"
        else
          text_stream << "3 Tr\n"
        end
        # format text object(s)
        # text_stream << "#{options[:font_color].join(' ')} rg\n" # sets the color state
        encode_text(text, fonts).each do |encoded|
          text_stream << "BT\n" # the Begine Text marker
          text_stream << format_name_to_pdf(set_font(encoded[0])) # Set font name
          text_stream << " #{font_size.round 3} Tf\n" # set font size and add font operator
          text_stream << "#{x.round 4} #{y.round 4} Td\n" # set location for text object
          text_stream << (encoded[1]) # insert the encoded string to the stream
          text_stream << " Tj\n" # the Text object operator and the End Text marker
          text_stream << "ET\n" # the Text object operator and the End Text marker
          x += encoded[2] / 1000 * font_size # update text starting point
          y -= encoded[3] / 1000 * font_size # update text starting point
        end
        # exit graphic state for text
        text_stream << "Q\n"
      end
      contents << text_stream

      self
    end
def textbox(text, properties = {})
      options = {
        x: page_size[0],
        y: page_size[1],
        width: 0,
        height: -1,
        text_align: :center,
        text_valign: :center,
        text_padding: 0.1,
        font: nil,
        font_size: :fit_text,
        max_font_size: nil,
        font_color: [0, 0, 0],
        stroke_color: nil,
        stroke_width: 0,
        box_color: nil,
        border_color: nil,
        border_width: 0,
        box_radius: 0,
        opacity: 1,
        ctm: nil # ~= [1,0,0,1,0,0]
      }
      options.update properties
      # reset the length and height to meaningful values, if negative
      options[:width] = mediabox[2] - options[:x] + options[:width] if options[:width] <= 0
      options[:height] = mediabox[3] - options[:y] + options[:height] if options[:height] <= 0

      # reset the padding value
      options[:text_padding] = 0 if options[:text_padding].to_f >= 1

      # create box stream
      box_stream = ''
      # set graphic state for box
      if options[:box_color] || (options[:border_width].to_i > 0 && options[:border_color])
        # compute x and y position for text
        x = options[:x]
        y = options[:y]

        # set graphic state for the box
        box_stream << "q\n"
        box_stream << "#{options[:ctm].join ' '} cm\n" if options[:ctm]
        box_graphic_state = { ca: options[:opacity], CA: options[:opacity], LW: options[:border_width], LC: 0, LJ: 0, LD: 0 }
        if options[:box_radius] != 0 # if the text box has rounded corners
          box_graphic_state[:LC] = 2
          box_graphic_state[:LJ] = 1
        end
        box_graphic_state = graphic_state box_graphic_state # adds the graphic state to Resources and gets the reference
        box_stream << "#{object_to_pdf box_graphic_state} gs\n"

        # the following line was removed for Acrobat Reader compatability
        # box_stream << "DeviceRGB CS\nDeviceRGB cs\n"

        box_stream << "#{options[:box_color].join(' ')} rg\n" if options[:box_color]
        if options[:border_width].to_i > 0 && options[:border_color]
          box_stream << "#{options[:border_color].join(' ')} RG\n"
        end
        # create the path
        radius = options[:box_radius]
        half_radius = (radius.to_f / 2).round 4
        ## set starting point
        box_stream << "#{options[:x] + radius} #{options[:y]} m\n"
        ## bottom and right corner - first line and first corner
        box_stream << "#{options[:x] + options[:width] - radius} #{options[:y]} l\n" # bottom
        if options[:box_radius] != 0 # make first corner, if not straight.
          box_stream << "#{options[:x] + options[:width] - half_radius} #{options[:y]} "
          box_stream << "#{options[:x] + options[:width]} #{options[:y] + half_radius} "
          box_stream << "#{options[:x] + options[:width]} #{options[:y] + radius} c\n"
        end
        ## right and top-right corner
        box_stream << "#{options[:x] + options[:width]} #{options[:y] + options[:height] - radius} l\n"
        if options[:box_radius] != 0
          box_stream << "#{options[:x] + options[:width]} #{options[:y] + options[:height] - half_radius} "
          box_stream << "#{options[:x] + options[:width] - half_radius} #{options[:y] + options[:height]} "
          box_stream << "#{options[:x] + options[:width] - radius} #{options[:y] + options[:height]} c\n"
        end
        ## top and top-left corner
        box_stream << "#{options[:x] + radius} #{options[:y] + options[:height]} l\n"
        if options[:box_radius] != 0
          box_stream << "#{options[:x] + half_radius} #{options[:y] + options[:height]} "
          box_stream << "#{options[:x]} #{options[:y] + options[:height] - half_radius} "
          box_stream << "#{options[:x]} #{options[:y] + options[:height] - radius} c\n"
        end
        ## left and bottom-left corner
        box_stream << "#{options[:x]} #{options[:y] + radius} l\n"
        if options[:box_radius] != 0
          box_stream << "#{options[:x]} #{options[:y] + half_radius} "
          box_stream << "#{options[:x] + half_radius} #{options[:y]} "
          box_stream << "#{options[:x] + radius} #{options[:y]} c\n"
        end
        # fill / stroke path
        box_stream << "h\n"
        if options[:box_color] && options[:border_width].to_i > 0 && options[:border_color]
          box_stream << "B\n"
        elsif options[:box_color] # fill if fill color is set
          box_stream << "f\n"
        elsif options[:border_width].to_i > 0 && options[:border_color] # stroke if border is set
          box_stream << "S\n"
        end

        # exit graphic state for the box
        box_stream << "Q\n"
      end
      contents << box_stream

      # reset x,y by text alignment - x,y are calculated from the bottom left
      # each unit (1) is 1/72 Inch
      # create text stream
      text_stream = ''
      if !text.to_s.empty? && options[:font_size] != 0 && (options[:font_color] || options[:stroke_color])
        # compute x and y position for text
        x = options[:x] + (options[:width] * options[:text_padding])
        y = options[:y] + (options[:height] * options[:text_padding])

        # set the fonts (fonts array, with :Helvetica as fallback).
        fonts = [*options[:font], :Helvetica]
        # fit text in box, if requested
        font_size = options[:font_size]
        if options[:font_size] == :fit_text
          font_size = fit_text text, fonts, (options[:width] * (1 - options[:text_padding])), (options[:height] * (1 - options[:text_padding]))
          font_size = options[:max_font_size] if options[:max_font_size] && font_size > options[:max_font_size]
        end

        text_size = dimensions_of text, fonts, font_size

        if options[:text_align] == :center
          x = ((options[:width] * (1 - (2 * options[:text_padding]))) - text_size[0]) / 2 + x
        elsif options[:text_align] == :right
          x = ((options[:width] * (1 - (1.5 * options[:text_padding]))) - text_size[0]) + x
        end
        if options[:text_valign] == :center
          y = ((options[:height] * (1 - (2 * options[:text_padding]))) - text_size[1]) / 2 + y
        elsif options[:text_valign] == :top
          y = (options[:height] * (1 - (1.5 * options[:text_padding]))) - text_size[1] + y
        end

        # set graphic state for text
        text_stream << "q\n"
        text_stream << "#{options[:ctm].join ' '} cm\n" if options[:ctm]
        text_graphic_state = graphic_state(ca: options[:opacity], CA: options[:opacity], LW: options[:stroke_width].to_f, LC: 2, LJ: 1, LD: 0)
        text_stream << "#{object_to_pdf text_graphic_state} gs\n"

        # the following line was removed for Acrobat Reader compatability
        # text_stream << "DeviceRGB CS\nDeviceRGB cs\n"

        # set text render mode
        if options[:font_color]
          text_stream << "#{options[:font_color].join(' ')} rg\n"
        end
        if options[:stroke_width].to_i > 0 && options[:stroke_color]
          text_stream << "#{options[:stroke_color].join(' ')} RG\n"
          if options[:font_color]
            text_stream << "2 Tr\n"
          else
            final_stream << "1 Tr\n"
          end
        elsif options[:font_color]
          text_stream << "0 Tr\n"
        else
          text_stream << "3 Tr\n"
        end
        # format text object(s)
        # text_stream << "#{options[:font_color].join(' ')} rg\n" # sets the color state
        encode_text(text, fonts).each do |encoded|
          text_stream << "BT\n" # the Begine Text marker
          text_stream << format_name_to_pdf(set_font(encoded[0])) # Set font name
          text_stream << " #{font_size.round 3} Tf\n" # set font size and add font operator
          text_stream << "#{x.round 4} #{y.round 4} Td\n" # set location for text object
          text_stream << (encoded[1]) # insert the encoded string to the stream
          text_stream << " Tj\n" # the Text object operator and the End Text marker
          text_stream << "ET\n" # the Text object operator and the End Text marker
          x += encoded[2] / 1000 * font_size # update text starting point
          y -= encoded[3] / 1000 * font_size # update text starting point
        end
        # exit graphic state for text
        text_stream << "Q\n"
      end
      contents << text_stream

      self
    end
This method adds a simple text box to the Page represented by the PDFWriter class.
 This function takes two values:
 text:: the text to potin the box.
 properties:: a Hash of box properties.
 the symbols and values in the properties Hash could be any or all of the following:
 x:: the left position of the box.
 y:: the BOTTOM position of the box.
 width:: the width/length of the box. negative values will be computed from edge of page. defaults to 0 (end of page).
 height:: the height of the box. negative values will be computed from edge of page. defaults to 0 (end of page).
 text_align:: symbol for horizontal text alignment, can be ":center" (default), ":right", ":left"
 text_valign:: symbol for vertical text alignment, can be ":center" (default), ":top", ":bottom"
 text_padding:: a Float between 0 and 1, setting the padding for the text. defaults to 0.05 (5%).
 font:: a registered font name or an Array of names. defaults to ":Helvetica". The 14 standard fonts names are:
 - :"Times-Roman"
 - :"Times-Bold"
 - :"Times-Italic"
 - :"Times-BoldItalic"
 - :Helvetica
 - :"Helvetica-Bold"
 - :"Helvetica-BoldOblique"
 - :"Helvetica- Oblique"
 - :Courier
 - :"Courier-Bold"
 - :"Courier-Oblique"
 - :"Courier-BoldOblique"
 - :Symbol
 - :ZapfDingbats
 font_size:: an Integer for the font size, or :fit_text to fit the text in the box. defaults to ":fit_text"
 max_font_size:: if font_size is set to :fit_text, this will be the maximum font size. defaults to nil (no maximum)
 font_color:: text color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defaults to black.
 stroke_color:: text stroke color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defounlts to nil (no stroke).
 stroke_width:: text stroke width in PDF units. defaults to 0 (none).
 box_color:: box fill color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defaults to nil (none).
 border_color:: box border color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defaults to nil (none).
 border_width:: border width in PDF units. defaults to nil (none).
 box_radius:: border radius in PDF units. defaults to 0 (no corner rounding).
 opacity:: textbox opacity, a float between 0 (transparent) and 1 (opaque)
 ctm:: A PDF complient CTM data array that will manipulate the axis and allow transformations. i.e. `[1,0,0,1,0,0]`