# encoding: utf-8 # core/text/formatted/arranger.rb : Implements a data structure for 2-stage # processing of lines of formatted text # # Copyright February 2010, Daniel Nelson. All Rights Reserved. # # This is free software. Please see the LICENSE and COPYING files for details. module Prawn module Text module Formatted #:nodoc: # @private class Arranger #:nodoc: attr_reader :max_line_height attr_reader :max_descender attr_reader :max_ascender attr_reader :finalized attr_accessor :consumed # The following present only for testing purposes attr_reader :unconsumed attr_reader :fragments attr_reader :current_format_state def initialize(document, options = {}) @document = document @fragments = [] @unconsumed = [] @kerning = options[:kerning] end def space_count unless finalized fail "Lines must be finalized before calling #space_count" end @fragments.inject(0) do |sum, fragment| sum + fragment.space_count end end def line_width unless finalized fail "Lines must be finalized before calling #line_width" end @fragments.inject(0) do |sum, fragment| sum + fragment.width end end def line unless finalized fail "Lines must be finalized before calling #line" end @fragments.collect do |fragment| fragment.text.dup.force_encoding(::Encoding::UTF_8) end.join end def finalize_line @finalized = true omit_trailing_whitespace_from_line_width @fragments = [] @consumed.each do |hash| text = hash[:text] format_state = hash.dup format_state.delete(:text) fragment = Prawn::Text::Formatted::Fragment.new(text, format_state, @document) @fragments << fragment set_fragment_measurements(fragment) set_line_measurement_maximums(fragment) end end def format_array=(array) initialize_line @unconsumed = [] array.each do |hash| hash[:text].scan(/[^\n]+|\n/) do |line| @unconsumed << hash.merge(:text => line) end end end def initialize_line @finalized = false @max_line_height = 0 @max_descender = 0 @max_ascender = 0 @consumed = [] @fragments = [] end def finished? @unconsumed.length == 0 end def next_string if finalized fail "Lines must not be finalized when calling #next_string" end next_unconsumed_hash = @unconsumed.shift if next_unconsumed_hash @consumed << next_unconsumed_hash.dup @current_format_state = next_unconsumed_hash.dup @current_format_state.delete(:text) next_unconsumed_hash[:text] end end def preview_next_string next_unconsumed_hash = @unconsumed.first if next_unconsumed_hash next_unconsumed_hash[:text] end end def apply_color_and_font_settings(fragment, &block) if fragment.color original_fill_color = @document.fill_color original_stroke_color = @document.stroke_color @document.fill_color(*fragment.color) @document.stroke_color(*fragment.color) apply_font_settings(fragment, &block) @document.stroke_color = original_stroke_color @document.fill_color = original_fill_color else apply_font_settings(fragment, &block) end end def apply_font_settings(fragment = nil, &block) if fragment.nil? font = current_format_state[:font] size = current_format_state[:size] character_spacing = current_format_state[:character_spacing] || @document.character_spacing styles = current_format_state[:styles] font_style = font_style(styles) else font = fragment.font size = fragment.size character_spacing = fragment.character_spacing styles = fragment.styles font_style = font_style(styles) end @document.character_spacing(character_spacing) do if font || font_style != :normal fail "Bad font family" unless @document.font.family @document.font(font || @document.font.family, :style => font_style) do apply_font_size(size, styles, &block) end else apply_font_size(size, styles, &block) end end end def update_last_string(printed, unprinted, normalized_soft_hyphen = nil) return if printed.nil? if printed.empty? @consumed.pop else @consumed.last[:text] = printed if normalized_soft_hyphen @consumed.last[:normalized_soft_hyphen] = normalized_soft_hyphen end end unless unprinted.empty? @unconsumed.unshift(@current_format_state.merge(:text => unprinted)) end load_previous_format_state if printed.empty? end def retrieve_fragment unless finalized fail "Lines must be finalized before fragments can be retrieved" end @fragments.shift end def repack_unretrieved new_unconsumed = [] while fragment = retrieve_fragment fragment.include_trailing_white_space! new_unconsumed << fragment.format_state.merge(:text => fragment.text) end @unconsumed = new_unconsumed.concat(@unconsumed) end def font_style(styles) if styles.nil? :normal elsif styles.include?(:bold) && styles.include?(:italic) :bold_italic elsif styles.include?(:bold) :bold elsif styles.include?(:italic) :italic else :normal end end private def load_previous_format_state if @consumed.empty? @current_format_state = {} else hash = @consumed.last @current_format_state = hash.dup @current_format_state.delete(:text) end end def apply_font_size(size, styles) if subscript?(styles) || superscript?(styles) relative_size = 0.583 if size.nil? size = @document.font_size * relative_size else size = size * relative_size end end if size.nil? yield else @document.font_size(size) { yield } end end def subscript?(styles) if styles.nil? then false else styles.include?(:subscript) end end def superscript?(styles) if styles.nil? then false else styles.include?(:superscript) end end def omit_trailing_whitespace_from_line_width @consumed.reverse_each do |hash| if hash[:text] == "\n" break elsif hash[:text].strip.empty? && @consumed.length > 1 # this entire fragment is trailing white space hash[:exclude_trailing_white_space] = true else # this fragment contains the first non-white space we have # encountered since the end of the line hash[:exclude_trailing_white_space] = true break end end end def set_fragment_measurements(fragment) apply_font_settings(fragment) do fragment.width = @document.width_of(fragment.text, :kerning => @kerning) fragment.line_height = @document.font.height fragment.descender = @document.font.descender fragment.ascender = @document.font.ascender end end def set_line_measurement_maximums(fragment) @max_line_height = [defined?(@max_line_height) && @max_line_height, fragment.line_height].compact.max @max_descender = [defined?(@max_descender) && @max_descender, fragment.descender].compact.max @max_ascender = [defined?(@max_ascender) && @max_ascender, fragment.ascender].compact.max end end end end end