# encoding: utf-8 # text.rb : Implements PDF text primitives # # Copyright May 2008, Gregory Brown. All Rights Reserved. # # This is free software. Please see the LICENSE and COPYING files for details. require "prawn/core/text" require "prawn/text/formatted" require "prawn/text/box" require "zlib" module Prawn module Text include Prawn::Core::Text include Prawn::Text::Formatted # No-Break Space Prawn::Text::NBSP = " " # Zero Width Space (indicate word boundaries without a space) Prawn::Text::ZWSP = [8203].pack("U") # Soft Hyphen (invisible, except when causing a line break) Prawn::Text::SHY = "­" # If you want text to flow onto a new page or between columns, this is the # method to use. If, instead, if you want to place bounded text outside of # the flow of a document (for captions, labels, charts, etc.), use Text::Box # or its convenience method text_box. # # Draws text on the page. Prawn attempts to wrap the text to fit within your # current bounding box (or margin_box if no bounding box is being used). # Text will flow onto the next page when it reaches the bottom of the # bounding box. Text wrap in Prawn does not re-flow linebreaks, so if you # want fully automated text wrapping, be sure to remove newlines before # attempting to draw your string. # # == Examples # # pdf.text "Will be wrapped when it hits the edge of your bounding box" # pdf.text "This will be centered", :align => :center # pdf.text "This will be right aligned", :align => :right # pdf.text "This includes inline " + # "formatting", :inline_format => true # # If your font contains kerning pair data that Prawn can parse, the # text will be kerned by default. You can disable kerning by including # a false :kerning option. If you want to disable kerning on an # entire document, set default_kerning = false for that document # # === Text Positioning Details # # The text is positioned at font.ascender below the baseline, # making it easy to use this method within bounding boxes and spans. # # == Encoding # # Note that strings passed to this function should be encoded as UTF-8. # If you get unexpected characters appearing in your rendered document, # check this. # # If the current font is a built-in one, although the string must be # encoded as UTF-8, only characters that are available in WinAnsi # are allowed. # # If an empty box is rendered to your PDF instead of the character you # wanted it usually means the current font doesn't include that character. # # == Options (default values marked in []) # # :inline_format:: # boolean. If true, then the string parameter is interpreted # as a HTML-esque string that recognizes the following tags: # \:: bold # \:: italic # \:: underline # \:: strikethrough # \:: subscript # \:: superscript # \:: # with the following attributes (using double or single quotes) # size="24":: # attribute for setting size # character_spacing="2.5":: # attribute for setting character spacing # name="Helvetica":: # attribute for setting the font. The font name must be an # AFM font with the desired faces or must be a font that is # already registered using Prawn::Document#font_families # \:: # with the following attributes # rgb="ffffff" or rgb="#ffffff":: # c="100" m="100" y="100" k="100":: # \:: # with the following attributes # href="http://example.com":: an external link # anchor="ToC":: # where the value of the anchor attribute is the name of a # destination that has already been or will be registered # using Prawn::Core::Destinations#add_dest. A clickable link # will be created to that destination. # Note that you must explicitly underline and color using the # appropriate tags if you which to draw attention to the link # # :kerning:: boolean. Whether or not to use kerning (if it # is available with the current font) # [value of document.default_kerning?] # :size:: number. The font size to use. [current font # size] # :color:: an RGB color ("ff0000") or CMYK array [10, 20, 30, 40]. # :character_spacing:: number. The amount of space to add # to or remove from the default character # spacing. [0] # :style:: The style to use. The requested style must be part of # the current font familly. [current style] # :indent_paragraphs:: number. The amount to indent the # first line of each paragraph. Omit this # option if you do not want indenting # :direction:: # :ltr, :rtl, Direction of the text (left-to-right # or right-to-left) [value of document.text_direction] # :fallback_fonts:: # An array of font names. Each name must be the name of an AFM font or # the name that was used to register a family of TTF fonts (see # Prawn::Document#font_families). If present, then each glyph will be # rendered using the first font that includes the glyph, starting with # the current font and then moving through :fallback_fonts from # left to right. # :align:: # :left, :center, :right, or # :justify Alignment within the bounding box # [:left if direction is :ltr, :right if direction is :rtl] # :valign:: :top, :center, or :bottom. # Vertical alignment within the bounding box [:top] # :leading:: # number. Additional space between lines [value of # document.default_leading] # :final_gap:: boolean. If true, then the space between # each line is included below the last line; # otherwise, document.y is placed just below the # descender of the last line printed [true] # :mode:: The text rendering mode to use. Use this to specify if the # text should render with the fill color, stroke color or # both. See the comments to text_rendering_mode() to see # a list of valid options. [0] # # == Exceptions # # Raises ArgumentError if :at option included # # Raises Prawn::Errrors::CannotFit if not wide enough to print # any text # def text(string, options={}) # we modify the options. don't change the user's hash options = options.dup if options[:inline_format] options.delete(:inline_format) array = Text::Formatted::Parser.to_array(string) else array = [{ :text => string }] end formatted_text(array, options) end # Draws formatted text to the page. # Formatted text is comprised of an array of hashes, where each hash defines # text and format information. See Text::Formatted#formatted_text_box for # more information on the structure of this array # # == Example # # text([{ :text => "hello" }, # { :text => "world", # :size => 24, # :style => [:bold, :italic] }]) # # == Options # # Accepts the same options as #text # # == Exceptions # # Same as for #text # def formatted_text(array, options={}) options = inspect_options_for_text(options.dup) if color = options.delete(:color) array = array.map do |fragment| fragment[:color] ? fragment : fragment.merge(:color => color) end end if @indent_paragraphs Text::Formatted::Parser.array_paragraphs(array).each do |paragraph| options[:skip_encoding] = false remaining_text = draw_indented_formatted_line(paragraph, options) options[:skip_encoding] = true if @no_text_printed # unless this paragraph was an empty line unless @all_text_printed @bounding_box.move_past_bottom options[:skip_encoding] = false remaining_text = draw_indented_formatted_line(paragraph, options) options[:skip_encoding] = true end end remaining_text = fill_formatted_text_box(remaining_text, options) draw_remaining_formatted_text_on_new_pages(remaining_text, options) end else remaining_text = fill_formatted_text_box(array, options) options[:skip_encoding] = true draw_remaining_formatted_text_on_new_pages(remaining_text, options) end end # Draws text on the page, beginning at the point specified by the :at option # the string is assumed to be pre-formatted to properly fit the page. # # pdf.draw_text "Hello World", :at => [100,100] # pdf.draw_text "Goodbye World", :at => [50,50], :size => 16 # # If your font contains kerning pair data that Prawn can parse, the # text will be kerned by default. You can disable kerning by including # a false :kerning option. If you want to disable kerning on an # entire document, set default_kerning = false for that document # # === Text Positioning Details: # # Prawn will position your text by the left-most edge of its baseline, and # flow along a single line. (This means that :align will not work) # # == Rotation # # Text can be rotated before it is placed on the canvas by specifying the # :rotate option with a given angle. Rotation occurs counter-clockwise. # # == Encoding # # Note that strings passed to this function should be encoded as UTF-8. # If you get unexpected characters appearing in your rendered document, # check this. # # If the current font is a built-in one, although the string must be # encoded as UTF-8, only characters that are available in WinAnsi # are allowed. # # If an empty box is rendered to your PDF instead of the character you # wanted it usually means the current font doesn't include that character. # # == Options (default values marked in []) # # :at:: [x, y](required). The position at which to start the text # :kerning:: boolean. Whether or not to use kerning (if it # is available with the current font) # [value of default_kerning?] # :size:: number. The font size to use. [current font # size] # :style:: The style to use. The requested style must be part of # the current font familly. [current style] # # :rotate:: number. The angle to which to rotate text # # == Exceptions # # Raises ArgumentError if :at option omitted # # Raises ArgumentError if :align option included # def draw_text(text, options) options = inspect_options_for_draw_text(options.dup) # dup because normalize_encoding changes the string text = text.to_s.dup save_font do process_text_options(options) font.normalize_encoding!(text) unless @skip_encoding font_size(options[:size]) { draw_text!(text, options) } end end # Gets height of text in PDF points. # Same options as #text, except as noted. # Not compatible with :indent_paragraphs option # # ==Example # # height_of("hello\nworld") # # == Exceptions # # Raises NotImplementedError if :indent_paragraphs # option included # # Raises Prawn::Errrors::CannotFit if not wide enough to print # any text # def height_of(string, options={}) height_of_formatted([{ :text => string }], options) end # Gets height of formatted text in PDF points. # See documentation for #height_of. # # ==Example # # height_of_formatted([{ :text => "hello" }, # { :text => "world", # :size => 24, # :style => [:bold, :italic] }]) # def height_of_formatted(array, options={}) if options[:indent_paragraphs] raise NotImplementedError, ":indent_paragraphs option not available" + "with height_of" end process_final_gap_option(options) box = Text::Formatted::Box.new(array, options.merge(:height => 100000000, :document => self)) printed = box.render(:dry_run => true) height = box.height height += box.line_gap + box.leading if @final_gap height end private def draw_remaining_formatted_text_on_new_pages(remaining_text, options) while remaining_text.length > 0 @bounding_box.move_past_bottom previous_remaining_text = remaining_text remaining_text = fill_formatted_text_box(remaining_text, options) break if remaining_text == previous_remaining_text end end def draw_indented_formatted_line(string, options) indent(@indent_paragraphs) do fill_formatted_text_box(string, options.dup.merge(:single_line => true)) end end def fill_formatted_text_box(text, options) merge_text_box_positioning_options(options) box = Text::Formatted::Box.new(text, options) remaining_text = box.render @no_text_printed = box.nothing_printed? @all_text_printed = box.everything_printed? self.y -= box.height self.y -= box.line_gap + box.leading if @final_gap remaining_text end def merge_text_box_positioning_options(options) bottom = @bounding_box.stretchy? ? @margin_box.absolute_bottom : @bounding_box.absolute_bottom options[:height] = y - bottom options[:width] = bounds.width options[:at] = [@bounding_box.left_side - @bounding_box.absolute_left, y - @bounding_box.absolute_bottom] end def inspect_options_for_draw_text(options) if options[:at].nil? raise ArgumentError, "The :at option is required for draw_text" elsif options[:align] raise ArgumentError, "The :align option does not work with draw_text" end if options[:kerning].nil? then options[:kerning] = default_kerning? end valid_options = Prawn::Core::Text::VALID_OPTIONS + [:at, :rotate] Prawn.verify_options(valid_options, options) options end def inspect_options_for_text(options) if options[:at] raise ArgumentError, ":at is no longer a valid option with text." + "use draw_text or text_box instead" end process_final_gap_option(options) process_indent_paragraphs_option(options) options[:document] = self options end def process_final_gap_option(options) @final_gap = options[:final_gap].nil? || options[:final_gap] options.delete(:final_gap) end def process_indent_paragraphs_option(options) @indent_paragraphs = options[:indent_paragraphs] options.delete(:indent_paragraphs) end def move_text_position(dy) bottom = @bounding_box.stretchy? ? @margin_box.absolute_bottom : @bounding_box.absolute_bottom @bounding_box.move_past_bottom if (y - dy) < bottom self.y -= dy end end end