[VX] Paragraph Formatter

Started by modern algebra, March 25, 2008, 03:49:32 pm

Previous topic - Next topic

modern algebra

March 25, 2008, 03:49:32 pm Last Edit: February 21, 2009, 05:45:36 am by shdwlink1993
Paragraph Formatter (VX Edition)
Authors: modern algebra
Version: 1.1
Type: Message Add-on
Key Term: Message Add-on



Introduction

This is a scripting tool. It's purpose is to allow the user to input an unbroken string and have returned a paragraph which has the ends of each line in line with each other. It serves roughly the same function as does the justifying option in Microsoft Word. See the screenshots if you are still confused.


Version History

The Version History refers to the original script made for XP

  • Version 1.1 -  Jan 20, 2008: Better Error Catching; Better comments; Better facade
  • Version 1.0 - Nov 2, 2007: Original Script.


Current Algorithms:


  • Formatter (generic)
  • Artist (generic)
  • Formatter_2 (based off Zeriab's algorithm, written for fonts with unvarying character width)


Planned Future Versions


  • Probably add some better formatting and Artist classes as I get better at scripting. As well, any other scripter is welcome to add their own formatting methods to the script.



Features


  • A nice, easy way to display long strings in a paragraph format
  • Easy to add and modify at runtime, allowing for the switching between formatting classes for each situation
  • You can write your own formatter or artist classes and use the paragraphing tool to suit your situation.
  • Highly customizable.



Screenshots

Spoiler: ShowHide



Script

Spoiler: ShowHide

#========================================================================
#  Paragraph Formatter (VX)
#  Version: 1.1
#  Author: modern algebra (rmrk.net)
#  Date: March 4, 2008
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#  Apology:
#    The idea behind this script is to easily separate a long string into a paragraph that fits in to
#     the dimensions you specify.
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#  Instructions:
#    For ease of use of people who are not neccesarily interested in writing their own algorithm,
#    I have included a facade which you can use simply by this code:
#
#           bitmap.draw_paragraph (x, y, width, height, string)
#
#    where x & y are the x & y coordinates on the specified bitmap, and width and height are the
#    maximum dimensions of the paragraph and string is the text you want to display in
#    paragraph formatter
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#  How it works:
#    The paragrapher expects two objects when initialized, a Formatter and an Artist. The idea
#    behind the formatter is that it is expected to take the initial specifications and convert it to a
#    Formatted Text object. Then, the Artist class is expected to interpret the Formatted Text
#    object and draw the paragraph. For details on how each specific algorithm works,  visit the
#    comments above and inside them. It is not necessary to use the default Formatter, Artist, or
#    Formatted Text objects.
#========================================================================

#======================================================================
# ** Bitmap
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Facade for easy application of the Paragraph Formatter
#======================================================================

class Bitmap
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # * Get Formatter
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  def paragraph_formatter
    @p_formatter = $game_system.default_formatter.new if @p_formatter.nil?
    return @p_formatter
  end
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # * Set Formatter
  #     formatter : The uninitialized formatter class you would like to use
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  def paragraph_formatter= (formatter)
    @p_formatter = formatter.new
  end
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # * Get Artist
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  def paragraph_artist
    @p_artist = $game_system.default_artist.new if @p_artist.nil?
    return @p_artist
  end
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # * Set Artist
  #     artist : The uninitialized artist class you would like to use
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  def paragraph_artist= (artist)
    @p_artist = artist.new
  end
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # * The Facade, which uses default Formatter and Artist to draw the formatted text directly
  #  to a bitmap, such as self.contents
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  def draw_paragraph (x, y, max_width, max_height, string)
    bitmap = Bitmap.new (max_width, max_height)
    bitmap.font = self.font
    pg = Paragrapher.new(paragraph_formatter, paragraph_artist)
    bitmap = pg.paragraph (string, bitmap)
    blt (x, y, bitmap, bitmap.rect)
    # Dispose of the proxy bitmap
    bitmap.dispose
  end
end

#========================================================================
# ** Game_System
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#  Summary of changes:
#     new instance variables - default_formatter, default_artist
#     aliased methods - initialize
#========================================================================

class Game_System
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # * Public Instance Variables
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  attr_accessor :default_formatter
  attr_accessor :default_artist
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # * Object Initialization
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  alias ma_paragraph_formatter_init initialize
  def initialize
    # Run original method
    ma_paragraph_formatter_init
    # Initialize original default format and artist classes
    @default_formatter = Paragrapher::Formatter_2
    @default_artist = Paragrapher::Artist
  end
end

#======================================================================
# ** Paragrapher
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#  Module containing the objects for the Paragrapher
#======================================================================
module Paragrapher
  #===================================================================
  # Allows the 'Paragrapher.new' command outside of the module to be used
  # rather than having to use 'Paragrapher::Paragrapher.new'
  #===================================================================
  class << self
    def new(*args, &block)
      return Paragrapher.new(*args, &block)
    end
  end
 
  #===================================================================
  # * The Paragrapher class
  #===================================================================
  class Paragrapher
    def initialize(formatter, artist)
      @formatter = formatter
      @artist = artist
    end
   
    def paragraph(string, *specifications)
      f = @formatter.format(string, *specifications)
      return @artist.draw(f)
    end
  end
 
  #===================================================================
  # * The Formatter class
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  This class converts a string into a formatted text object, which is then
  #  passed on to the Artist class
  #===================================================================
  class Formatter
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # * Format
    #     string         : the string to be formatted
    #     specifications : the desired width of the paragraph, or the bitmap
    #-------------------------------------------------------------------------------------------------------------------
    #  This works on a very simple algorithm. Basically, it just formats the
    #  text by going through each word in the string. If a word
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def format(string, specifications)
      # Initializes Formatted_Text object
      f = Formatted_Text.new
      # Checks whether specifications is a bitmap or a number. It then sets
      # max_width and f.bitmap accordingly
      if specifications.class == Bitmap
        f.bitmap = specifications
        max_width = specifications.width
      elsif specifications.class == Fixnum || specifications.class == Float
        max_width = specifications
        f.bitmap = Bitmap.new (max_width, 32)
      else
        # Error Catching: Bad Specifications
        bitmap = Bitmap.new (200, 64)
        f = format ('Specifications Error', bitmap)
        p 'Specifications Error: Please Pass Fixnum, Float or Bitmap'
        return f
      end
      # Breaks the given string into an array of all it's characters
      temp_word_array = string.scan (/./)
      position = 0
      line_break = 0
      # Initializes f.lines
      f.lines = []
      f.blank_width = []
      for i in 0...temp_word_array.size
        character = temp_word_array[i]
        # Error catching
        if character == "\n"
          p 'This formatter does not recognize line breaks'
          character = " "
        end
        # If at a new word
        if character == " " || i == temp_word_array.size - 1
          # Take into account the last character of the string
          if i == temp_word_array.size - 1
            i += 1
          end
          # If this word fits on the current line
          if f.bitmap.text_size (string[line_break, i-line_break]).width <= max_width
            position = i
          else
            line = temp_word_array[line_break, position-line_break]
            # Adds the first lines to f.lines
            f.lines.push (line)
            # Calculates the blank space left to cover in the line
            line_blank = max_width - f.bitmap.text_size(string[line_break,position-line_break]).width
            # Calculates the necessary distance between letters to make up for
            # line_blank and adds that value to the f.blank_width array
            f.blank_width.push (line_blank.to_f / (line.size.to_f-1.0))
            # Keeps track of the position in the array of each line
            line_break = position + 1
            position = i
          end
        end
      end
      # Adds the last line to f.lines
      f.lines.push (temp_word_array[line_break, temp_word_array.size - line_break])
      # Since the last line is drawn normally, blank_width should be 0
      f.blank_width.push (0)
      if specifications.class == Fixnum
        # Sets up the bitmap if it was unspecified.
        f.bitmap = Bitmap.new(max_width, f.lines.size*32)
      end
      # Returns the Formatted_Text object
      return f
    end
  end
 
  #===================================================================
  # * Formatter 2 (Using Zeriab's Algorithm)
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  This algorithm was written by Zeriab for fonts which have characters of the same width.
  #  This is like Courier New, UMEGothic and fonts of that sort. This algorithm attaches a
  #  cost to each line based on the amount of white space at the end of that line. It will
  #  display the way of writing the text with the lowest total cost. In prcatice, this will mean
  #  that it will, as much as possible, reduce the spacing between letters in a line and make
  #  the spacing more consistent for each line of the paragraph
  #===================================================================
  class Formatter_2
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # * Format
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def format (string, specifications)
      f = Formatted_Text.new
      f.lines, f.blank_width, word_lengths, words = [], [], [], []
      tracker = 0
      for i in 0...string.size
        if string[i,1] == " " || i == string.size - 1
          if i == string.size - 1
            i += 1
          end
          word_lengths.push (i - tracker)
          words.push (string[tracker, i - tracker])
          tracker = i + 1
        end
      end
      if specifications.class == Bitmap
        max_width = specifications.width
        f.bitmap = specifications
      elsif specifications.class == Fixnum || specifications.class == Float
        max_width = specifications
        f.bitmap = Bitmap.new (1,1)
      else
        # Error Catching: Bad specification
        bitmap = Bitmap.new (200, 64)
        f = format ('Specifications Error', bitmap)
        p 'Specifications Error: Please Pass Fixnum, Float or Bitmap'
        return f
      end
      tw = f.bitmap.text_size('a').width
      max_width = [max_width / tw, 180].min
      # Error Catching: Word too long
      if word_lengths.max > max_width
        f = format ('Too long' , specifications)
        p 'One or more words is too long for specified width'
        return f
      end
      position = line_break (word_lengths, max_width)
      lines = give_lines (position, position.size - 1, words)
      max_width *= tw
      for i in 0...lines.size
        line = lines[i]
        f.lines.push (line.scan (/./))
        if i == lines.size - 1
          f.blank_width.push (0)
        else
          text_width = line.size * tw
          extra_space = max_width - text_width
          f.blank_width.push (extra_space.to_f / (line.size.to_f - 1.0))
        end
      end
      if f.bitmap != specifications
        f.bitmap = Bitmap.new (max_width, f.lines.size*32)
      end
      return f
    end
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # * Line Break (written by Zeriab)
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def line_break(word_lengths, max_length)
    return false  if max_length > 180
    word_lengths.unshift(nil)
    extra_spaces = Table.new(word_lengths.size,word_lengths.size)
    line_prices = Table.new(word_lengths.size,word_lengths.size)
    word_price = []
    position = []
    inf = max_length*max_length + 1
    for i in 1...word_lengths.size
      extra_spaces[i,i] = max_length - word_lengths[i]
      for j in (i+1)..[word_lengths.size-1, max_length/2+i+1].min
      extra_spaces[i,j] = extra_spaces[i,j-1] - word_lengths[j]-1
      end
    end
    for i in 1...word_lengths.size
      for j in i..[word_lengths.size-1, max_length/2+i+1].min
      if extra_spaces[i,j] < 0
        line_prices[i,j] = inf
      elsif j == word_lengths.size-1 and extra_spaces[i,j] >= 0
        line_prices[i,j] = 0
      else
        line_prices[i,j] = extra_spaces[i,j]*extra_spaces[i,j]
      end
        end
    end
    word_price[0] = 0
    for j in 1...word_lengths.size
      word_price[j] = inf
      for ik in 1..j
      i = j - ik + 1
      break  if line_prices[i,j] == inf
      if word_price[i-1] + line_prices[i,j] < word_price[j]
        word_price[j] = word_price[i-1] + line_prices[i,j]
        position[j] = i
      end
      end
    end
    return position
    end
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # * Give_Lines (written by Zeriab)
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def give_lines(position,last_index,words)
    first_index = position[last_index]
    word_array = []
    if first_index != 1
      word_array = give_lines(position, first_index - 1,words)
    end
    str = ""
    for x in first_index..last_index
      str += ' '  if x != first_index
      str += words[x-1]
    end
    word_array << str
    return word_array
    end
  end

  #===================================================================
  # * The Artist class
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Interprets a Formatted Text object and draws the paragraph encoded
  #===================================================================
  class Artist
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # * Draw
    #     f : Formatted Text Object
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def draw (f)
      # Calculates the necessary distance between lines
      line_distance = f.bitmap.height.to_f / f.lines.size.to_f
      line_distance = [f.bitmap.font.size  + 10, line_distance].min
      # For all lines in the lines array
      for i in 0...f.lines.size
        blank_space = f.blank_width[i]
        position = 0
        # For all indices of the line array
        for j in 0...f.lines[i].size
          word = f.lines[i][j]
          ws = f.bitmap.text_size (word)
          position += blank_space if j != 0
          # Adds blank_space and position, and draws the string located at each index
          f.bitmap.draw_text (position, line_distance*i,ws.width+1,ws.height+1,word)
          # Keeps track of the position we are in in pixels
          position += ws.width
        end
      end
      return f.bitmap
    end
  end
 
  #===================================================================
  # * The Formatted_Text class containing the results of the formatter
  #===================================================================
  class Formatted_Text
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # * Public Instance Variables
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    attr_accessor :lines       # An array of strings, each a line of the paragraph
    attr_accessor :blank_width # The amount of white space between each letter
    attr_accessor :bitmap      # The bitmap drawn to
  end
end



Instructions

As a scripter's tool, it can be quite heavy for non-scripters to use. That is why I wrote a facade for common use of the tool. Naturally, you will still need some scripting knowledge, but the facade allows for this code:


bitmap.draw_paragraph (x, y, max_width, max_height, string)


where bitmap is the bitmap you are drawing to. This can be self.contents in a window, or any instance of the bitmap class.

It can be used like this, if you want to draw the paragraph in a different way:
    
    formatter = <formatter class you want to use>
    artist = <artist class you want to use>
    specifications = <max width, or bitmap>
    pg = Paragrapher.new (formatter, artist)
    text_bitmap = pg.paragraph (string, specifications)
    bitmap.blt (x, y, text_bitmap, Rect.new (0,0,text_bitmap.width, text_bitmap.height))


Basically, you choose your formatter and artist class at runtime. This means that if you want to use Paragraph::Formatter_2, because you are using a font with set width for all characters, then you would choose that here. Currently, there is only one Artist class, Paragraph::Artist, but of course you can make your own if it does not suit you. You can either specify a bitmap or a fixnum. The fixnum would just be the max width, and the paragrapher would create a bitmap which was at font_size 22, default font name, and it would space each line 32 pixels. With a bitmap, you specify max_width, max_height, font and font size, and anything else that has an effect. Naturally, bitmap in the code is the bitmap you are drawing the paragraph on. If you have any questions, just ask.

Also, the text_size method of Bitmap does not, in fact, work properly. In a little while I will post a way to get around this problem as it can get in the way of drawing nice paragraphs.

Put the scripts above main and below the default scripts.

For more pertinent instructions, see the header of the script


Credits and Thanks


  • Zeriab, for the idea, the motivation, and the instruction, as well as one of the algorithms
  • modern algebra, for the sloppy code :P



Author's Notes

Support will be provided anywhere that I (modern algebra) posts the script. At forums where it is posted by anyone else, I make no guarantees. I am willing to write formatting or artist methods to deal with any instances for which current algorithms are inapplicable or inefficient, as well as dealing with any bugs in the current algorithms.

This script was inspired by Zeriab, and pretty much everything that is good about this script is due to Zeriab. Zeriab deserves more credit for this script then I do, rightly, but since the world isn't just... :P Anyway, he deserves all my thanks for being an excellent teacher.

Further, this script is pretty much directly brought over from the version I made for XP. Any changes I have made are minor. Thus, if anyone runs into bugs, they may be an oversight on my part as I didn't see anything in the script that was dependent on RGSS or that would cause problems when put in RGSS2. In any case, please notify me if you run into problems.