Pargraph Formatter
Version: 1.1
Author: modern algebra
Date: January 20, 2008
Version History
- 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.
Description
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.
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.
ScreenshotsInstructions 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
Script
#==============================================================================
# Paragraph Formatter
# Version: 1.1
# Author: modern algebra
# Date: January 20, 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.
#
# To change the default formatter and artist, use this code:
#
# $game_system.default_formatter = Paragrapher::<new formatter>*
# $game_system.default_artist = Paragrapher::<new artist>*
#
# To change the formatter for a specific bitmap, use the code:
#
# bitmap.paragraph_formatter = Paragrapher::<new formatter>*
# bitmap.paragraph_artist = Paragrapher::<new artist>*
#
# * Note: It is unnecessary to initialize the class by typing .new
#------------------------------------------------------------------------------
# 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 class works,
# visit the comments above and inside them. It is not necessary to use the
# default Formatter, Artist, or Formatted Text objects.
#==============================================================================
#==============================================================================
# ** 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
@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 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. Basically, 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
The facade for easy use:
#==========================================================================
# ** Bitmap
#--------------------------------------------------------------------------
# Facade for easy application of the Paragraph Formatter
#==========================================================================
class Bitmap
#------------------------------------------------------------------------
# * Get Formatter
#------------------------------------------------------------------------
def paragraph_formatter
if @p_formatter.nil?
@p_formatter = $game_system.default_formatter.new
end
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
if @p_artist.nil?
@p_artist = $game_system.default_artist.new
end
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.dup
pg = Paragrapher.new(paragraph_formatter, paragraph_artist)
new_bitmap = pg.paragraph (string, bitmap)
blt (x, y, new_bitmap, Rect.new (0,0,new_bitmap.width,new_bitmap.height))
end
end
Credit
- Zeriab, for the idea, the motivation, and the instruction, as well as one of the algorithms
- modern algebra, for the sloppy code
Support
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.
Author's Notes
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...
Anyway, he deserves all my thanks for being an excellent teacher.