Pargraph Formatter
Version: 1.0
Author: modern algebra
Date: November 2, 2007
Version History
Version 1.0 - Original Script: Current Algorithms:
- Formatter (generic)
- Artist (generic)
- Formatter (Zeriab's, 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.
Screenshots
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:
CODE
bitmap.new_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.
For scripters, the way the paragraph is basically used is like this:
CODE
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.
Script
CODE
#==============================================================================
# ** 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
#============================================================================
class Formatter
# The format method takes two arguments, the unbroken string, and the bitmap
# or desired length, and formats the text to fit within those specifications
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 - 2
elsif specifications.class == Fixnum || specifications.class == Float
max_width = specifications - 2
f.bitmap = Bitmap.new (max_width, 32)
else
# Error Catching
p 'Specifications Error: Please Pass Fixnum, Float or Bitmap'
f.lines = [['Specifications Error'],['Please Pass Fixnum or Bitmap']]
f.blank_width = [0,0]
f.bitmap = Bitmap.new (200, 64)
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]
# 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
#============================================================================
# * Zeriab Formatter
#----------------------------------------------------------------------------
# The 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 not
# only makes all the lines the same width, but also writes them in such
# a way that each line is the same width as the longest line, rather then
# the max width. This makes the difference in white space much smaller
# Adapted to fit into Paragraph Formatter by modern algebra
#============================================================================
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)
end
tw = f.bitmap.text_size('a').width
max_width /= tw
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
#------------------------------------------------------------------------
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
#-----------------------------------------------------------------------
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
#============================================================================
class Artist
# The draw method takes a Formatted_Text object and draws the text to the
# bitmap of the object.
def draw(f)
# Calculates the 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 pixels
position += ws.width
end
end
return f.bitmap
end
end
#============================================================================
# * The Formatted_Text class containing the results of the formatter
#============================================================================
class Formatted_Text
attr_accessor :lines
attr_accessor :blank_width
attr_accessor :bitmap
end
end
The facade for easy use:
CODE
class Bitmap
#------------------------------------------------------------------------
# * The Facade, which uses default Formatter and Artist to draw the formatted
# text directly to a bitmap, such as self.contents
#------------------------------------------------------------------------
def new_paragraph (x, y, max_width, max_height, string)
bitmap = Bitmap.new (max_width, max_height)
bitmap.font = self.font
pg = Paragrapher.new(Paragrapher::Formatter.new, Paragrapher::Artist.new)
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 formatting 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.
This post has been edited by modern algebra: Nov 11 2007, 12:23 PM