# 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