lib/prawn/text/formatted/box.rb in prawn-0.11.1.pre vs lib/prawn/text/formatted/box.rb in prawn-0.11.1
- old
+ new
@@ -64,13 +64,10 @@
#
# == Options
#
# Accepts the same options as Text::Box with the below exceptions
#
- # <tt>:overflow</tt>::
- # does not accept :ellipses
- #
# == Returns
#
# Returns a formatted text array representing any text that did not print
# under the current settings.
#
@@ -79,51 +76,227 @@
# Raises "Bad font family" if no font family is defined for the current font
#
# Raises <tt>Prawn::Errrors::CannotFit</tt> if not wide enough to print
# any text
#
- # Raises <tt>NotImplementedError</tt> if <tt>:ellipses</tt> <tt>overflow</tt>
- # option included
- #
- def formatted_text_box(array, options)
+ def formatted_text_box(array, options={})
Text::Formatted::Box.new(array, options.merge(:document => self)).render
end
# Generally, one would use the Prawn::Text::Formatted#formatted_text_box
# convenience method. However, using Text::Formatted::Box.new in
# conjunction with #render(:dry_run => true) enables one to do look-ahead
# calculations prior to placing text on the page, or to determine how much
# vertical space was consumed by the printed text
#
- class Box < Prawn::Text::Box
+ class Box
include Prawn::Core::Text::Formatted::Wrap
- def initialize(array, options={})
- super(array, options)
- if @overflow == :ellipses
- raise NotImplementedError, "ellipses overflow unavailable with" +
- "formatted box"
+ def valid_options
+ Prawn::Core::Text::VALID_OPTIONS + [:at, :height, :width,
+ :align, :valign,
+ :rotate, :rotate_around,
+ :overflow, :min_font_size,
+ :leading, :character_spacing,
+ :mode, :single_line,
+ :skip_encoding,
+ :document,
+ :direction,
+ :fallback_fonts]
+ end
+
+ # The text that was successfully printed (or, if <tt>dry_run</tt> was
+ # used, the text that would have been successfully printed)
+ attr_reader :text
+
+ # True iff nothing printed (or, if <tt>dry_run</tt> was
+ # used, nothing would have been successfully printed)
+ def nothing_printed?
+ @nothing_printed
+ end
+
+ # True iff everything printed (or, if <tt>dry_run</tt> was
+ # used, everything would have been successfully printed)
+ def everything_printed?
+ @everything_printed
+ end
+
+ # The upper left corner of the text box
+ attr_reader :at
+ # The line height of the last line printed
+ attr_reader :line_height
+ # The height of the ascender of the last line printed
+ attr_reader :ascender
+ # The height of the descender of the last line printed
+ attr_reader :descender
+ # The leading used during printing
+ attr_reader :leading
+
+ def line_gap
+ line_height - (ascender + descender)
+ end
+
+ #
+ # Example (see Prawn::Text::Core::Formatted::Wrap for what is required
+ # of the wrap method if you want to override the default wrapping
+ # algorithm):
+ #
+ #
+ # module MyWrap
+ #
+ # def wrap(array)
+ # initialize_wrap([{ :text => 'all your base are belong to us' }])
+ # @line_wrap.wrap_line(:document => @document,
+ # :kerning => @kerning,
+ # :width => 10000,
+ # :arranger => @arranger)
+ # fragment = @arranger.retrieve_fragment
+ # format_and_draw_fragment(fragment, 0, @line_wrap.width, 0)
+ # []
+ # end
+ #
+ # end
+ #
+ # Prawn::Text::Formatted::Box.extensions << MyWrap
+ #
+ # box = Prawn::Text::Formatted::Box.new('hello world')
+ # box.render('why can't I print anything other than' +
+ # '"all your base are belong to us"?')
+ #
+ #
+ def self.extensions
+ @extensions ||= []
+ end
+
+ def self.inherited(base) #:nodoc:
+ extensions.each { |e| base.extensions << e }
+ end
+
+ # See Prawn::Text#text_box for valid options
+ #
+ def initialize(formatted_text, options={})
+ @inked = false
+ Prawn.verify_options(valid_options, options)
+ options = options.dup
+
+ self.class.extensions.reverse_each { |e| extend e }
+
+ @overflow = options[:overflow] || :truncate
+
+ self.original_text = formatted_text
+ @text = nil
+
+ @document = options[:document]
+ @direction = options[:direction] || @document.text_direction
+ @fallback_fonts = options[:fallback_fonts] ||
+ @document.fallback_fonts
+ @at = (options[:at] ||
+ [@document.bounds.left, @document.bounds.top]).dup
+ @width = options[:width] ||
+ @document.bounds.right - @at[0]
+ @height = options[:height] || default_height
+ @align = options[:align] ||
+ (@direction == :rtl ? :right : :left)
+ @vertical_align = options[:valign] || :top
+ @leading = options[:leading] || @document.default_leading
+ @character_spacing = options[:character_spacing] ||
+ @document.character_spacing
+ @mode = options[:mode] || @document.text_rendering_mode
+ @rotate = options[:rotate] || 0
+ @rotate_around = options[:rotate_around] || :upper_left
+ @single_line = options[:single_line]
+ @skip_encoding = options[:skip_encoding] || @document.skip_encoding
+
+ if @overflow == :expand
+ # if set to expand, then we simply set the bottom
+ # as the bottom of the document bounds, since that
+ # is the maximum we should expand to
+ @height = default_height
+ @overflow = :truncate
end
+ @min_font_size = options[:min_font_size] || 5
+ if options[:kerning].nil? then
+ options[:kerning] = @document.default_kerning?
+ end
+ @options = { :kerning => options[:kerning],
+ :size => options[:size],
+ :style => options[:style] }
+
+ super(formatted_text, options)
end
+ # Render text to the document based on the settings defined in initialize.
+ #
+ # In order to facilitate look-ahead calculations, <tt>render</tt> accepts
+ # a <tt>:dry_run => true</tt> option. If provided, then everything is
+ # executed as if rendering, with the exception that nothing is drawn on
+ # the page. Useful for look-ahead computations of height, unprinted text,
+ # etc.
+ #
+ # Returns any text that did not print under the current settings
+ #
+ def render(flags={})
+ unprinted_text = []
+
+ @document.save_font do
+ @document.character_spacing(@character_spacing) do
+ @document.text_rendering_mode(@mode) do
+ process_options
+
+ if @skip_encoding
+ text = original_text
+ else
+ text = normalize_encoding
+ end
+
+ @document.font_size(@font_size) do
+ shrink_to_fit(text) if @overflow == :shrink_to_fit
+ process_vertical_alignment(text)
+ @inked = true unless flags[:dry_run]
+ if @rotate != 0 && @inked
+ unprinted_text = render_rotated(text)
+ else
+ unprinted_text = wrap(text)
+ end
+ @inked = false
+ end
+ end
+ end
+ end
+
+ unprinted_text
+ end
+
+ # The width available at this point in the box
+ #
+ def available_width
+ @width
+ end
+
# The height actually used during the previous <tt>render</tt>
#
def height
return 0 if @baseline_y.nil? || @descender.nil?
- @baseline_y.abs + @line_height - @ascender
+ (@baseline_y - @descender).abs
end
# <tt>fragment</tt> is a Prawn::Text::Formatted::Fragment object
#
def draw_fragment(fragment, accumulated_width=0, line_width=0, word_spacing=0) #:nodoc:
case(@align)
- when :left, :justify
+ when :left
x = @at[0]
when :center
x = @at[0] + @width * 0.5 - line_width * 0.5
when :right
x = @at[0] + @width - line_width
+ when :justify
+ if @direction == :ltr
+ x = @at[0]
+ else
+ x = @at[0] + @width - line_width
+ end
end
x += accumulated_width
y = @at[1] + @baseline_y
@@ -131,47 +304,199 @@
y += fragment.y_offset
fragment.left = x
fragment.baseline = y
- draw_fragment_underlays(fragment)
-
if @inked
+ draw_fragment_underlays(fragment)
+
@document.word_spacing(word_spacing) {
@document.draw_text!(fragment.text, :at => [x, y],
:kerning => @kerning)
}
+
draw_fragment_overlays(fragment)
end
end
private
def original_text
@original_array.collect { |hash| hash.dup }
end
- def original_text=(array)
- @original_array = array
+ def original_text=(formatted_text)
+ @original_array = formatted_text
end
def normalize_encoding
- array = original_text
- array.each do |hash|
- hash[:text] = @document.font.normalize_encoding(hash[:text])
+ formatted_text = original_text
+
+ unless @fallback_fonts.empty?
+ formatted_text = process_fallback_fonts(formatted_text)
end
- array
+
+ formatted_text.each do |hash|
+ if hash[:font]
+ @document.font(hash[:font]) do
+ hash[:text] = @document.font.normalize_encoding(hash[:text])
+ end
+ else
+ hash[:text] = @document.font.normalize_encoding(hash[:text])
+ end
+ end
+
+ formatted_text
end
+ def process_fallback_fonts(formatted_text)
+ modified_formatted_text = []
+
+ formatted_text.each do |hash|
+ fragments = analyze_glyphs_for_fallback_font_support(hash)
+ modified_formatted_text.concat(fragments)
+ end
+
+ modified_formatted_text
+ end
+
+ def analyze_glyphs_for_fallback_font_support(hash)
+ font_glyph_pairs = []
+
+ original_font = @document.font.family
+ fragment_font = hash[:font] || original_font
+ @document.font(fragment_font)
+
+ fallback_fonts = @fallback_fonts.dup
+ # always default back to the current font if the glyph is missing from
+ # all fonts
+ fallback_fonts << fragment_font
+
+ hash[:text].unpack("U*").each do |char_int|
+ char = [char_int].pack("U")
+ @document.font(fragment_font)
+ font_glyph_pairs << [find_font_for_this_glyph(char,
+ @document.font.family,
+ fallback_fonts.dup),
+ char]
+ end
+
+ @document.font(original_font)
+
+ form_fragments_from_like_font_glyph_pairs(font_glyph_pairs, hash)
+ end
+
+ def find_font_for_this_glyph(char, current_font, fallback_fonts)
+ if fallback_fonts.length == 0 || @document.font.glyph_present?(char)
+ current_font
+ else
+ current_font = fallback_fonts.shift
+ @document.font(current_font)
+ find_font_for_this_glyph(char, @document.font.family, fallback_fonts)
+ end
+ end
+
+ def form_fragments_from_like_font_glyph_pairs(font_glyph_pairs, hash)
+ fragments = []
+ fragment = nil
+ current_font = nil
+
+ font_glyph_pairs.each do |font, char|
+ if font != current_font
+ current_font = font
+ fragment = hash.dup
+ fragment[:text] = char
+ fragment[:font] = font
+ fragments << fragment
+ else
+ fragment[:text] += char
+ end
+ end
+
+ fragments
+ end
+
def move_baseline_down
if @baseline_y == 0
@baseline_y = -@ascender
else
@baseline_y -= (@line_height + @leading)
end
end
+ # Returns the default height to be used if none is provided or if the
+ # overflow option is set to :expand. If we are in a stretchy bounding
+ # box, assume we can stretch to the bottom of the innermost non-stretchy
+ # box.
+ #
+ def default_height
+ # Find the "frame", the innermost non-stretchy bbox.
+ frame = @document.bounds
+ frame = frame.parent while frame.stretchy? && frame.parent
+
+ @at[1] + @document.bounds.absolute_bottom - frame.absolute_bottom
+ end
+
+ def process_vertical_alignment(text)
+ return if @vertical_align == :top
+ wrap(text)
+
+ case @vertical_align
+ when :center
+ @at[1] = @at[1] - (@height - height) * 0.5
+ when :bottom
+ @at[1] = @at[1] - (@height - height)
+ end
+ @height = height
+ end
+
+ # Decrease the font size until the text fits or the min font
+ # size is reached
+ def shrink_to_fit(text)
+ wrap(text)
+ until @everything_printed || @font_size <= @min_font_size
+ @font_size = [@font_size - 0.5, @min_font_size].max
+ @document.font_size = @font_size
+ wrap(text)
+ end
+ end
+
+ def process_options
+ # must be performed within a save_font bock because
+ # document.process_text_options sets the font
+ @document.process_text_options(@options)
+ @font_size = @options[:size]
+ @kerning = @options[:kerning]
+ end
+
+ def render_rotated(text)
+ unprinted_text = ''
+
+ case @rotate_around
+ when :center
+ x = @at[0] + @width * 0.5
+ y = @at[1] - @height * 0.5
+ when :upper_right
+ x = @at[0] + @width
+ y = @at[1]
+ when :lower_right
+ x = @at[0] + @width
+ y = @at[1] - @height
+ when :lower_left
+ x = @at[0]
+ y = @at[1] - @height
+ else
+ x = @at[0]
+ y = @at[1]
+ end
+
+ @document.rotate(@rotate, :origin => [x, y]) do
+ unprinted_text = wrap(text)
+ end
+ unprinted_text
+ end
+
def draw_fragment_underlays(fragment)
fragment.callback_objects.each do |obj|
obj.render_behind(fragment) if obj.respond_to?(:render_behind)
end
end
@@ -190,10 +515,10 @@
box = fragment.absolute_bounding_box
@document.link_annotation(box,
:Border => [0, 0, 0],
:A => { :Type => :Action,
:S => :URI,
- :URI => Prawn::Core::LiteralString.new(fragment.link) })
+ :URI => Prawn::Core::LiteralString.new(fragment.link) })
end
def draw_fragment_overlay_anchor(fragment)
return unless fragment.anchor
box = fragment.absolute_bounding_box