Repository URL to install this package:
|
Version:
0.5.0 ▾
|
#!/usr/bin/env ruby
require 'stringio'
module RTF
# This class represents an element within an RTF document. The class provides
# a base class for more specific node types.
class Node
# Node parent.
attr_accessor :parent
# Constructor for the Node class.
#
# ==== Parameters
# parent:: A reference to the Node that owns the new Node. May be nil
# to indicate a base or root node.
def initialize(parent)
@parent = parent
end
# This method retrieves a Node objects previous peer node, returning nil
# if the Node has no previous peer.
def previous_node
peer = nil
if !parent.nil? and parent.respond_to?(:children)
index = parent.children.index(self)
peer = index > 0 ? parent.children[index - 1] : nil
end
peer
end
# This method retrieves a Node objects next peer node, returning nil
# if the Node has no previous peer.
def next_node
peer = nil
if !parent.nil? and parent.respond_to?(:children)
index = parent.children.index(self)
peer = parent.children[index + 1]
end
peer
end
# This method is used to determine whether a Node object represents a
# root or base element. The method returns true if the Nodes parent is
# nil, false otherwise.
def is_root?
@parent.nil?
end
# This method traverses a Node tree to locate the root element.
def root
node = self
node = node.parent while !node.parent.nil?
node
end
end # End of the Node class.
# This class represents a specialisation of the Node class to refer to a Node
# that simply contains text.
class TextNode < Node
# Actual text
attr_accessor :text
# This is the constructor for the TextNode class.
#
# ==== Parameters
# parent:: A reference to the Node that owns the TextNode. Must not be
# nil.
# text:: A String containing the node text. Defaults to nil.
#
# ==== Exceptions
# RTFError:: Generated whenever an nil parent object is specified to
# the method.
def initialize(parent, text=nil)
super(parent)
if parent.nil?
RTFError.fire("Nil parent specified for text node.")
end
@parent = parent
@text = text
end
# This method concatenates a String on to the end of the existing text
# within a TextNode object.
#
# ==== Parameters
# text:: The String to be added to the end of the text node.
def append(text)
@text = (@text.nil?) ? text.to_s : @text + text.to_s
end
# This method inserts a String into the existing text within a TextNode
# object. If the TextNode contains no text then it is simply set to the
# text passed in. If the offset specified is past the end of the nodes
# text then it is simply appended to the end.
#
# ==== Parameters
# text:: A String containing the text to be added.
# offset:: The numbers of characters from the first character to insert
# the new text at.
def insert(text, offset)
if !@text.nil?
@text = @text[0, offset] + text.to_s + @text[offset, @text.length]
else
@text = text.to_s
end
end
# This method generates the RTF equivalent for a TextNode object. This
# method escapes any special sequences that appear in the text.
def to_rtf
rtf=(@text.nil? ? '' : @text.gsub("{", "\\{").gsub("}", "\\}").gsub("\\", "\\\\"))
# This is from lfarcy / rtf-extensions
# I don't see the point of coding different 128<n<256 range
#f1=lambda { |n| n < 128 ? n.chr : n < 256 ? "\\'#{n.to_s(16)}" : "\\u#{n}\\'3f" }
# Encode as Unicode.
f=lambda { |n| n < 128 ? n.chr : "\\u#{n}\\'3f" }
# Ruby 1.9 is safe, cause detect original encoding
# and convert text to utf-16 first
if RUBY_VERSION>"1.9.0"
return rtf.encode("UTF-16LE", :undef=>:replace).each_codepoint.map(&f).join('')
else
# You SHOULD use UTF-8 as input, ok?
return rtf.unpack('U*').map(&f).join('')
end
end
end # End of the TextNode class.
# This class represents a Node that can contain other Node objects. Its a
# base class for more specific Node types.
class ContainerNode < Node
include Enumerable
# Children elements of the node
attr_accessor :children
# This is the constructor for the ContainerNode class.
#
# ==== Parameters
# parent:: A reference to the parent node that owners the new
# ContainerNode object.
def initialize(parent)
super(parent)
@children = []
@children.concat(yield) if block_given?
end
# This method adds a new node element to the end of the list of nodes
# maintained by a ContainerNode object. Nil objects are ignored.
#
# ==== Parameters
# node:: A reference to the Node object to be added.
def store(node)
if !node.nil?
@children.push(node) if !@children.include?(Node)
node.parent = self if node.parent != self
end
node
end
# This method fetches the first node child for a ContainerNode object. If
# a container contains no children this method returns nil.
def first
@children[0]
end
# This method fetches the last node child for a ContainerNode object. If
# a container contains no children this method returns nil.
def last
@children.last
end
# This method provides for iteration over the contents of a ContainerNode
# object.
def each
@children.each {|child| yield child}
end
# This method returns a count of the number of children a ContainerNode
# object contains.
def size
@children.size
end
# This method overloads the array dereference operator to allow for
# access to the child elements of a ContainerNode object.
#
# ==== Parameters
# index:: The offset from the first child of the child object to be
# returned. Negative index values work from the back of the
# list of children. An invalid index will cause a nil value
# to be returned.
def [](index)
@children[index]
end
# This method generates the RTF text for a ContainerNode object.
def to_rtf
RTFError.fire("#{self.class.name}.to_rtf method not yet implemented.")
end
end # End of the ContainerNode class.
# This class represents a RTF command element within a document. This class
# is concrete enough to be used on its own but will also be used as the
# base class for some specific command node types.
class CommandNode < ContainerNode
# String containing the prefix text for the command
attr_accessor :prefix
# String containing the suffix text for the command
attr_accessor :suffix
# A boolean to indicate whether the prefix and suffix should
# be written to separate lines whether the node is converted
# to RTF. Defaults to true
attr_accessor :split
# A boolean to indicate whether the prefix and suffix should
# be wrapped in curly braces. Defaults to true.
attr_accessor :wrap
# This is the constructor for the CommandNode class.
#
# ==== Parameters
# parent:: A reference to the node that owns the new node.
# prefix:: A String containing the prefix text for the command.
# suffix:: A String containing the suffix text for the command. Defaults
# to nil.
# split:: A boolean to indicate whether the prefix and suffix should
# be written to separate lines whether the node is converted
# to RTF. Defaults to true.
# wrap:: A boolean to indicate whether the prefix and suffix should
# be wrapped in curly braces. Defaults to true.
def initialize(parent, prefix, suffix=nil, split=true, wrap=true)
super(parent)
@prefix = prefix
@suffix = suffix
@split = split
@wrap = wrap
end
# This method adds text to a command node. If the last child node of the
# target node is a TextNode then the text is appended to that. Otherwise
# a new TextNode is created and append to the node.
#
# ==== Parameters
# text:: The String of text to be written to the node.
def <<(text)
if !last.nil? and last.respond_to?(:text=)
last.append(text)
else
self.store(TextNode.new(self, text))
end
end
# This method generates the RTF text for a CommandNode object.
def to_rtf
text = StringIO.new
text << '{' if wrap?
text << @prefix if @prefix
self.each do |entry|
text << "\n" if split?
text << entry.to_rtf
end
text << "\n" if split?
text << @suffix if @suffix
text << '}' if wrap?
text.string
end
# This method provides a short cut means of creating a paragraph command
# node. The method accepts a block that will be passed a single parameter
# which will be a reference to the paragraph node created. After the
# block is complete the paragraph node is appended to the end of the child
# nodes on the object that the method is called against.
#
# ==== Parameters
# style:: A reference to a ParagraphStyle object that defines the style
# for the new paragraph. Defaults to nil to indicate that the
# currently applied paragraph styling should be used.
def paragraph(style=nil)
node = ParagraphNode.new(self, style)
yield node if block_given?
self.store(node)
end
# This method provides a short cut means of creating a new ordered or
# unordered list. The method requires a block that will be passed a
# single parameter that'll be a reference to the first level of the
# list. See the +ListLevelNode+ doc for more information.
#
# Example usage:
#
# rtf.list do |level1|
# level1.item do |li|
# li << 'some text'
# li.apply(some_style) {|x| x << 'some styled text'}
# end
#
# level1.list(:decimal) do |level2|
# level2.item {|li| li << 'some other text in a decimal list'}
# level2.item {|li| li << 'and here we go'}
# end
# end
#
def list(kind=:bullets)
node = ListNode.new(self)
yield node.list(kind)
self.store(node)
end
def link(url, text=nil)
node = LinkNode.new(self, url)
node << text if text
yield node if block_given?
self.store(node)
end
# This method provides a short cut means of creating a line break command
# node. This command node does not take a block and may possess no other
# content.
def line_break
self.store(CommandNode.new(self, '\line', nil, false))
nil
end
# This method inserts a footnote at the current position in a node.
#
# ==== Parameters
# text:: A string containing the text for the footnote.
def footnote(text)
if !text.nil? and text != ''
mark = CommandNode.new(self, '\fs16\up6\chftn', nil, false)
note = CommandNode.new(self, '\footnote {\fs16\up6\chftn}', nil, false)
note.paragraph << text
self.store(mark)
self.store(note)
end
end
# This method inserts a new image at the current position in a node.
#
# ==== Parameters
# source:: Either a string containing the path and name of a file or a
# File object for the image file to be inserted.
#
# ==== Exceptions
# RTFError:: Generated whenever an invalid or inaccessible file is
# specified or the image file type is not supported.
def image(source)
self.store(ImageNode.new(self, source, root.get_id))
end
# This method provides a short cut means for applying multiple styles via
# single command node. The method accepts a block that will be passed a
# reference to the node created. Once the block is complete the new node
# will be append as the last child of the CommandNode the method is called
# on.
#
# ==== Parameters
# style:: A reference to a CharacterStyle object that contains the style
# settings to be applied.
#
# ==== Exceptions
# RTFError:: Generated whenever a non-character style is specified to
# the method.
def apply(style)
# Check the input style.
if !style.is_character_style?
RTFError.fire("Non-character style specified to the "\
"CommandNode#apply() method.")
end
# Store fonts and colours.
root.colours << style.foreground unless style.foreground.nil?
root.colours << style.background unless style.background.nil?
root.fonts << style.font unless style.font.nil?
# Generate the command node.
node = CommandNode.new(self, style.prefix(root.fonts, root.colours))
yield node if block_given?
self.store(node)
end
# This method provides a short cut means of creating a bold command node.
# The method accepts a block that will be passed a single parameter which
# will be a reference to the bold node created. After the block is
# complete the bold node is appended to the end of the child nodes on
# the object that the method is call against.
def bold
style = CharacterStyle.new
style.bold = true
if block_given?
apply(style) {|node| yield node}
else
apply(style)
end
end
# This method provides a short cut means of creating an italic command
# node. The method accepts a block that will be passed a single parameter
# which will be a reference to the italic node created. After the block is
# complete the italic node is appended to the end of the child nodes on
# the object that the method is call against.
def italic
style = CharacterStyle.new
style.italic = true
if block_given?
apply(style) {|node| yield node}
else
apply(style)
end
end
# This method provides a short cut means of creating an underline command
# node. The method accepts a block that will be passed a single parameter
# which will be a reference to the underline node created. After the block
# is complete the underline node is appended to the end of the child nodes
# on the object that the method is call against.
def underline
style = CharacterStyle.new
style.underline = true
if block_given?
apply(style) {|node| yield node}
else
apply(style)
end
end
# This method provides a short cut means of creating a subscript command
# node. The method accepts a block that will be passed a single parameter
# which will be a reference to the subscript node created. After the
# block is complete the subscript node is appended to the end of the
# child nodes on the object that the method is call against.
def subscript
style = CharacterStyle.new
style.subscript = true
if block_given?
apply(style) {|node| yield node}
else
apply(style)
end
end
# This method provides a short cut means of creating a superscript command
# node. The method accepts a block that will be passed a single parameter
# which will be a reference to the superscript node created. After the
# block is complete the superscript node is appended to the end of the
# child nodes on the object that the method is call against.
def superscript
style = CharacterStyle.new
style.superscript = true
if block_given?
apply(style) {|node| yield node}
else
apply(style)
end
end
# This method provides a short cut means of creating a strike command
# node. The method accepts a block that will be passed a single parameter
# which will be a reference to the strike node created. After the
# block is complete the strike node is appended to the end of the
# child nodes on the object that the method is call against.
def strike
style = CharacterStyle.new
style.strike = true
if block_given?
apply(style) {|node| yield node}
else
apply(style)
end
end
# This method provides a short cut means of creating a font command node.
# The method accepts a block that will be passed a single parameter which
# will be a reference to the font node created. After the block is
# complete the font node is appended to the end of the child nodes on the
# object that the method is called against.
#
# ==== Parameters
# font:: A reference to font object that represents the font to be used
# within the node.
# size:: An integer size setting for the font. Defaults to nil to
# indicate that the current font size should be used.
def font(font, size=nil)
style = CharacterStyle.new
style.font = font
style.font_size = size
root.fonts << font
if block_given?
apply(style) {|node| yield node}
else
apply(style)
end
end
# This method provides a short cut means of creating a foreground colour
# command node. The method accepts a block that will be passed a single
# parameter which will be a reference to the foreground colour node
# created. After the block is complete the foreground colour node is
# appended to the end of the child nodes on the object that the method
# is called against.
#
# ==== Parameters
# colour:: The foreground colour to be applied by the command.
def foreground(colour)
style = CharacterStyle.new
style.foreground = colour
root.colours << colour
if block_given?
apply(style) {|node| yield node}
else
apply(style)
end
end
# This method provides a short cut means of creating a background colour
# command node. The method accepts a block that will be passed a single
# parameter which will be a reference to the background colour node
# created. After the block is complete the background colour node is
# appended to the end of the child nodes on the object that the method
# is called against.
#
# ==== Parameters
# colour:: The background colour to be applied by the command.
def background(colour)
style = CharacterStyle.new
style.background = colour
root.colours << colour
if block_given?
apply(style) {|node| yield node}
else
apply(style)
end
end
# This method provides a short cut menas of creating a colour node that
# deals with foreground and background colours. The method accepts a
# block that will be passed a single parameter which will be a reference
# to the colour node created. After the block is complete the colour node
# is append to the end of the child nodes on the object that the method
# is called against.
#
# ==== Parameters
# fore:: The foreground colour to be applied by the command.
# back:: The background colour to be applied by the command.
def colour(fore, back)
style = CharacterStyle.new
style.foreground = fore
style.background = back
root.colours << fore
root.colours << back
if block_given?
apply(style) {|node| yield node}
else
apply(style)
end
end
# This method creates a new table node and returns it. The method accepts
# a block that will be passed the table as a parameter. The node is added
# to the node the method is called upon after the block is complete.
#
# ==== Parameters
# rows:: The number of rows that the table contains.
# columns:: The number of columns that the table contains.
# *widths:: One or more integers representing the widths for the table
# columns.
def table(rows, columns, *widths)
node = TableNode.new(self, rows, columns, *widths)
yield node if block_given?
store(node)
node
end
alias :write :<<
alias :color :colour
alias :split? :split
alias :wrap? :wrap
end # End of the CommandNode class.
# This class represents a paragraph within an RTF document.
class ParagraphNode < CommandNode
def initialize(parent, style=nil)
prefix = '\pard'
prefix << style.prefix(nil, nil) if style
super(parent, prefix, '\par')
end
end
# This class represents an ordered/unordered list within an RTF document.
#
# Currently list nodes can contain any type of node, but this behaviour
# will change in future releases. The class overrides the +list+ method
# to return a +ListLevelNode+.
#
class ListNode < CommandNode
def initialize(parent)
prefix = "\\"
suffix = '\pard'
suffix << ListLevel::ResetTabs.map {|tw| "\\tx#{tw}"}.join
suffix << '\ql\qlnatural\pardirnatural\cf0 \\'
super(parent, prefix, suffix, true, false)
@template = root.lists.new_template
end
# This method creates a new +ListLevelNode+ of the given kind and
# stores it in the document tree.
#
# ==== Parameters
# kind:: The kind of this list level, may be either :bullets or :decimal
def list(kind)
self.store ListLevelNode.new(self, @template, kind)
end
end
# This class represents a list level, and carries out indenting information
# and the bullet or number that is prepended to each +ListTextNode+.
#
# The class overrides the +list+ method to implement nesting, and provides
# the +item+ method to add a new list item, the +ListTextNode+.
class ListLevelNode < CommandNode
def initialize(parent, template, kind, level=1)
@template = template
@kind = kind
@level = template.level_for(level, kind)
prefix = '\pard'
prefix << @level.tabs.map {|tw| "\\tx#{tw}"}.join
prefix << "\\li#{@level.indent}\\fi-#{@level.indent}"
prefix << "\\ql\\qlnatural\\pardirnatural\n"
prefix << "\\ls#{@template.id}\\ilvl#{@level.level-1}\\cf0"
super(parent, prefix, nil, true, false)
end
# Returns the kind of this level, either :bullets or :decimal
attr_reader :kind
# Returns the indenting level of this list, from 1 to 9
def level
@level.level
end
# Creates a new +ListTextNode+ and yields it to the calling block
def item
node = ListTextNode.new(self, @level)
yield node
self.store(node)
end
# Creates a new +ListLevelNode+ to implement nested lists
def list(kind=@kind)
node = ListLevelNode.new(self, @template, kind, @level.level+1)
yield node
self.store(node)
end
end
# This class represents a list item, that can contain text or
# other nodes. Currently any type of node is accepted, but after
# more extensive testing this behaviour may change.
class ListTextNode < CommandNode
def initialize(parent, level)
@level = level
@parent = parent
number = siblings_count + 1 if parent.kind == :decimal
prefix = "{\\listtext#{@level.marker.text_format(number)}}"
suffix = '\\'
super(parent, prefix, suffix, false, false)
end
private
def siblings_count
parent.children.select {|n| n.kind_of?(self.class)}.size
end
end
class LinkNode < CommandNode
def initialize(parent, url)
prefix = "\\field{\\*\\fldinst HYPERLINK \"#{url}\"}{\\fldrslt "
suffix = "}"
super(parent, prefix, suffix, false)
end
end
# This class represents a table node within an RTF document. Table nodes are
# specialised container nodes that contain only TableRowNodes and have their
# size specified when they are created an cannot be resized after that.
class TableNode < ContainerNode
# Cell margin. Default to 100
attr_accessor :cell_margin
# This is a constructor for the TableNode class.
#
# ==== Parameters
# parent:: A reference to the node that owns the table.
# rows:: The number of rows in the tabkle.
# columns:: The number of columns in the table.
# *widths:: One or more integers specifying the widths of the table
# columns.
def initialize(parent, *args, &block)
if args.size>=2
rows=args.shift
columns=args.shift
widths=args
super(parent) do
entries = []
rows.times {entries.push(TableRowNode.new(self, columns, *widths))}
entries
end
elsif block
block.arity<1 ? self.instance_eval(&block) : block.call(self)
else
raise "You should use 0 or >2 args"
end
@cell_margin = 100
end
# Attribute accessor.
def rows
entries.size
end
# Attribute accessor.
def columns
entries[0].length
end
# This method assigns a border width setting to all of the sides on all
# of the cells within a table.
#
# ==== Parameters
# width:: The border width setting to apply. Negative values are ignored
# and zero switches the border off.
def border_width=(width)
self.each {|row| row.border_width = width}
end
# This method assigns a shading colour to a specified row within a
# TableNode object.
#
# ==== Parameters
# index:: The offset from the first row of the row to have shading
# applied to it.
# colour:: A reference to a Colour object representing the shading colour
# to be used. Set to nil to clear shading.
def row_shading_colour(index, colour)
row = self[index]
row.shading_colour = colour if row != nil
end
# This method assigns a shading colour to a specified column within a
# TableNode object.
#
# ==== Parameters
# index:: The offset from the first column of the column to have shading
# applied to it.
# colour:: A reference to a Colour object representing the shading colour
# to be used. Set to nil to clear shading.
def column_shading_colour(index, colour)
self.each do |row|
cell = row[index]
cell.shading_colour = colour if cell != nil
end
end
# This method provides a means of assigning a shading colour to a
# selection of cells within a table. The method accepts a block that
# takes three parameters - a TableCellNode representing a cell within the
# table, an integer representing the x offset of the cell and an integer
# representing the y offset of the cell. If the block returns true then
# shading will be applied to the cell.
#
# ==== Parameters
# colour:: A reference to a Colour object representing the shading colour
# to be applied. Set to nil to remove shading.
def shading_colour(colour)
if block_given?
0.upto(self.size - 1) do |x|
row = self[x]
0.upto(row.size - 1) do |y|
apply = yield row[y], x, y
row[y].shading_colour = colour if apply
end
end
end
end
# This method overloads the store method inherited from the ContainerNode
# class to forbid addition of further nodes.
#
# ==== Parameters
# node:: A reference to the node to be added.
def store(node)
RTFError.fire("Table nodes cannot have nodes added to.")
end
# This method generates the RTF document text for a TableCellNode object.
def to_rtf
text = StringIO.new
size = 0
self.each do |row|
if size > 0
text << "\n"
else
size = 1
end
text << row.to_rtf
end
text.string.sub(/\\row(?!.*\\row)/m, "\\lastrow\n\\row")
end
alias :column_shading_color :column_shading_colour
alias :row_shading_color :row_shading_colour
alias :shading_color :shading_colour
end # End of the TableNode class.
# This class represents a row within an RTF table. The TableRowNode is a
# specialised container node that can hold only TableCellNodes and, once
# created, cannot be resized. Its also not possible to change the parent
# of a TableRowNode object.
class TableRowNode < ContainerNode
# This is the constructor for the TableRowNode class.
#
# ===== Parameters
# table:: A reference to table that owns the row.
# cells:: The number of cells that the row will contain.
# widths:: One or more integers specifying the widths for the table
# columns
def initialize(table, cells, *widths)
super(table) do
entries = []
cells.times do |index|
entries.push(TableCellNode.new(self, widths[index]))
end
entries
end
end
# Attribute accessors
def length
entries.size
end
# This method assigns a border width setting to all of the sides on all
# of the cells within a table row.
#
# ==== Parameters
# width:: The border width setting to apply. Negative values are ignored
# and zero switches the border off.
def border_width=(width)
self.each {|cell| cell.border_width = width}
end
# This method overloads the parent= method inherited from the Node class
# to forbid the alteration of the cells parent.
#
# ==== Parameters
# parent:: A reference to the new node parent.
def parent=(parent)
RTFError.fire("Table row nodes cannot have their parent changed.")
end
# This method sets the shading colour for a row.
#
# ==== Parameters
# colour:: A reference to the Colour object that represents the new
# shading colour. Set to nil to switch shading off.
def shading_colour=(colour)
self.each {|cell| cell.shading_colour = colour}
end
# This method overloads the store method inherited from the ContainerNode
# class to forbid addition of further nodes.
#
# ==== Parameters
# node:: A reference to the node to be added.
#def store(node)
# RTFError.fire("Table row nodes cannot have nodes added to.")
#end
# This method generates the RTF document text for a TableCellNode object.
def to_rtf
text = StringIO.new
temp = StringIO.new
offset = 0
text << "\\trowd\\tgraph#{parent.cell_margin}"
self.each do |entry|
widths = entry.border_widths
colour = entry.shading_colour
text << "\n"
text << "\\clbrdrt\\brdrw#{widths[0]}\\brdrs" if widths[0] != 0
text << "\\clbrdrl\\brdrw#{widths[3]}\\brdrs" if widths[3] != 0
text << "\\clbrdrb\\brdrw#{widths[2]}\\brdrs" if widths[2] != 0
text << "\\clbrdrr\\brdrw#{widths[1]}\\brdrs" if widths[1] != 0
text << "\\clcbpat#{root.colours.index(colour)}" if colour != nil
text << "\\cellx#{entry.width + offset}"
temp << "\n#{entry.to_rtf}"
offset += entry.width
end
text << "#{temp.string}\n\\row"
text.string
end
end # End of the TableRowNode class.
# This class represents a cell within an RTF table. The TableCellNode is a
# specialised command node that is forbidden from creating tables or having
# its parent changed.
class TableCellNode < CommandNode
# A definition for the default width for the cell.
DEFAULT_WIDTH = 300
# Top border
TOP = 0
# Right border
RIGHT = 1
# Bottom border
BOTTOM = 2
# Left border
LEFT = 3
# Width of cell
attr_accessor :width
# Attribute accessor.
attr_reader :shading_colour, :style
# This is the constructor for the TableCellNode class.
#
# ==== Parameters
# row:: The row that the cell belongs to.
# width:: The width to be assigned to the cell. This defaults to
# TableCellNode::DEFAULT_WIDTH.
# style:: The style that is applied to the cell. This must be a
# ParagraphStyle class. Defaults to nil.
# top:: The border width for the cells top border. Defaults to nil.
# right:: The border width for the cells right hand border. Defaults to
# nil.
# bottom:: The border width for the cells bottom border. Defaults to nil.
# left:: The border width for the cells left hand border. Defaults to
# nil.
#
# ==== Exceptions
# RTFError:: Generated whenever an invalid style setting is specified.
def initialize(row, width=DEFAULT_WIDTH, style=nil, top=nil, right=nil,
bottom=nil, left=nil)
super(row, nil)
if !style.nil? and !style.is_paragraph_style?
RTFError.fire("Non-paragraph style specified for TableCellNode "\
"constructor.")
end
@width = (width != nil && width > 0) ? width : DEFAULT_WIDTH
@borders = [(top != nil && top > 0) ? top : nil,
(right != nil && right > 0) ? right : nil,
(bottom != nil && bottom > 0) ? bottom : nil,
(left != nil && left > 0) ? left : nil]
@shading_colour = nil
@style = style
end
# Attribute mutator.
#
# ==== Parameters
# style:: A reference to the style object to be applied to the cell.
# Must be an instance of the ParagraphStyle class. Set to nil
# to clear style settings.
#
# ==== Exceptions
# RTFError:: Generated whenever an invalid style setting is specified.
def style=(style)
if !style.nil? and !style.is_paragraph_style?
RTFError.fire("Non-paragraph style specified for TableCellNode "\
"constructor.")
end
@style = style
end
# This method assigns a width, in twips, for the borders on all sides of
# the cell. Negative widths will be ignored and a width of zero will
# switch the border off.
#
# ==== Parameters
# width:: The setting for the width of the border.
def border_width=(width)
size = width.nil? ? 0 : width
if size > 0
@borders[TOP] = @borders[RIGHT] = @borders[BOTTOM] = @borders[LEFT] = size.to_i
else
@borders = [nil, nil, nil, nil]
end
end
# This method assigns a border width to the top side of a table cell.
# Negative values are ignored and a value of 0 switches the border off.
#
# ==== Parameters
# width:: The new border width setting.
def top_border_width=(width)
size = width.nil? ? 0 : width
if size > 0
@borders[TOP] = size.to_i
else
@borders[TOP] = nil
end
end
# This method assigns a border width to the right side of a table cell.
# Negative values are ignored and a value of 0 switches the border off.
#
# ==== Parameters
# width:: The new border width setting.
def right_border_width=(width)
size = width.nil? ? 0 : width
if size > 0
@borders[RIGHT] = size.to_i
else
@borders[RIGHT] = nil
end
end
# This method assigns a border width to the bottom side of a table cell.
# Negative values are ignored and a value of 0 switches the border off.
#
# ==== Parameters
# width:: The new border width setting.
def bottom_border_width=(width)
size = width.nil? ? 0 : width
if size > 0
@borders[BOTTOM] = size.to_i
else
@borders[BOTTOM] = nil
end
end
# This method assigns a border width to the left side of a table cell.
# Negative values are ignored and a value of 0 switches the border off.
#
# ==== Parameters
# width:: The new border width setting.
def left_border_width=(width)
size = width.nil? ? 0 : width
if size > 0
@borders[LEFT] = size.to_i
else
@borders[LEFT] = nil
end
end
# This method alters the shading colour associated with a TableCellNode
# object.
#
# ==== Parameters
# colour:: A reference to the Colour object to use in shading the cell.
# Assign nil to clear cell shading.
def shading_colour=(colour)
root.colours << colour
@shading_colour = colour
end
# This method retrieves an array with the cell border width settings.
# The values are inserted in top, right, bottom, left order.
def border_widths
widths = []
@borders.each {|entry| widths.push(entry.nil? ? 0 : entry)}
widths
end
# This method fetches the width for top border of a cell.
def top_border_width
@borders[TOP].nil? ? 0 : @borders[TOP]
end
# This method fetches the width for right border of a cell.
def right_border_width
@borders[RIGHT].nil? ? 0 : @borders[RIGHT]
end
# This method fetches the width for bottom border of a cell.
def bottom_border_width
@borders[BOTTOM].nil? ? 0 : @borders[BOTTOM]
end
# This method fetches the width for left border of a cell.
def left_border_width
@borders[LEFT].nil? ? 0 : @borders[LEFT]
end
# This method overloads the paragraph method inherited from the
# ComamndNode class to forbid the creation of paragraphs.
#
# ==== Parameters
# style:: The paragraph style, ignored
def paragraph(style=nil)
RTFError.fire("TableCellNode#paragraph() called. Table cells cannot "\
"contain paragraphs.")
end
# This method overloads the parent= method inherited from the Node class
# to forbid the alteration of the cells parent.
#
# ==== Parameters
# parent:: A reference to the new node parent.
def parent=(parent)
RTFError.fire("Table cell nodes cannot have their parent changed.")
end
# This method overrides the table method inherited from CommandNode to
# forbid its use in table cells.
#
# ==== Parameters
# rows:: The number of rows for the table.
# columns:: The number of columns for the table.
# *widths:: One or more integers representing the widths for the table
# columns.
def table(rows, columns, *widths)
RTFError.fire("TableCellNode#table() called. Nested tables not allowed.")
end
# This method generates the RTF document text for a TableCellNode object.
def to_rtf
text = StringIO.new
separator = split? ? "\n" : " "
line = (separator == " ")
text << "\\pard\\intbl"
text << @style.prefix(nil, nil) if @style != nil
text << separator
self.each do |entry|
text << "\n" if line
line = true
text << entry.to_rtf
end
text << (split? ? "\n" : " ")
text << "\\cell"
text.string
end
end # End of the TableCellNode class.
# This class represents a document header.
class HeaderNode < CommandNode
# A definition for a header type.
UNIVERSAL = :header
# A definition for a header type.
LEFT_PAGE = :headerl
# A definition for a header type.
RIGHT_PAGE = :headerr
# A definition for a header type.
FIRST_PAGE = :headerf
# Attribute accessor.
attr_reader :type
# Attribute mutator.
attr_writer :type
# This is the constructor for the HeaderNode class.
#
# ==== Parameters
# document:: A reference to the Document object that will own the new
# header.
# type:: The style type for the new header. Defaults to a value of
# HeaderNode::UNIVERSAL.
def initialize(document, type=UNIVERSAL)
super(document, "\\#{type.id2name}", nil, false)
@type = type
end
# This method overloads the footnote method inherited from the CommandNode
# class to prevent footnotes being added to headers.
#
# ==== Parameters
# text:: Not used.
#
# ==== Exceptions
# RTFError:: Always generated whenever this method is called.
def footnote(text)
RTFError.fire("Footnotes are not permitted in page headers.")
end
end # End of the HeaderNode class.
# This class represents a document footer.
class FooterNode < CommandNode
# A definition for a header type.
UNIVERSAL = :footer
# A definition for a header type.
LEFT_PAGE = :footerl
# A definition for a header type.
RIGHT_PAGE = :footerr
# A definition for a header type.
FIRST_PAGE = :footerf
# Attribute accessor.
attr_reader :type
# Attribute mutator.
attr_writer :type
# This is the constructor for the FooterNode class.
#
# ==== Parameters
# document:: A reference to the Document object that will own the new
# footer.
# type:: The style type for the new footer. Defaults to a value of
# FooterNode::UNIVERSAL.
def initialize(document, type=UNIVERSAL)
super(document, "\\#{type.id2name}", nil, false)
@type = type
end
# This method overloads the footnote method inherited from the CommandNode
# class to prevent footnotes being added to footers.
#
# ==== Parameters
# text:: Not used.
#
# ==== Exceptions
# RTFError:: Always generated whenever this method is called.
def footnote(text)
RTFError.fire("Footnotes are not permitted in page footers.")
end
end # End of the FooterNode class.
# This class represents an image within a RTF document. Currently only the
# PNG, JPEG and Windows Bitmap formats are supported. Efforts are made to
# identify the file type but these are not guaranteed to work.
class ImageNode < Node
# A definition for an image type constant.
PNG = :pngblip
# A definition for an image type constant.
JPEG = :jpegblip
# A definition for an image type constant.
BITMAP = :dibitmap0
# A definition for an architecture endian constant.
LITTLE_ENDIAN = :little
# A definition for an architecture endian constant.
BIG_ENDIAN = :big
# Offsets for reading dimension data by filetype
DIMENSIONS_OFFSET = {
JPEG => 2,
PNG => 8,
BITMAP => 8,
}.freeze
# Attribute accessor.
attr_reader :x_scaling, :y_scaling, :top_crop, :right_crop, :bottom_crop,
:left_crop, :width, :height, :displayed_width, :displayed_height
# Attribute mutator.
attr_writer :x_scaling, :y_scaling, :top_crop, :right_crop, :bottom_crop,
:left_crop, :displayed_width, :displayed_height
# This is the constructor for the ImageNode class.
#
# ==== Parameters
# parent:: A reference to the node that owns the new image node.
# source:: A reference to the image source. This must be a String or a
# File.
# id:: The unique identifier for the image node.
#
# ==== Exceptions
# RTFError:: Generated whenever the image specified is not recognised as
# a supported image type, something other than a String or
# File or IO is passed as the source parameter or if the
# specified source does not exist or cannot be accessed.
def initialize(parent, source, id)
super(parent)
@source = nil
@id = id
@type = nil
@x_scaling = @y_scaling = nil
@top_crop = @right_crop = @bottom_crop = @left_crop = nil
@width = @height = nil
@displayed_width = @displayed_height = nil
# store path to image
@source = source if source.instance_of?(String) || source.instance_of?(Tempfile)
@source = source.path if source.instance_of?(File)
# Check the file's existence and accessibility.
if !File.exist?(@source)
RTFError.fire("Unable to find the #{File.basename(@source)} file.")
end
if !File.readable?(@source)
RTFError.fire("Access to the #{File.basename(@source)} file denied.")
end
@type = get_file_type
if @type == nil
RTFError.fire("The #{File.basename(@source)} file contains an "\
"unknown or unsupported image type.")
end
@width, @height = get_dimensions
end
def open_file(&block)
if block
File.open(@source, 'rb', &block)
else
File.open(@source, 'rb')
end
end
# This method attempts to determine the image type associated with a
# file, returning nil if it fails to make the determination.
def get_file_type
type = nil
read = []
open_file do |file|
# Check if the file is a JPEG.
read_source(file, read, 2)
if read[0,2] == [255, 216]
type = JPEG
else
# Check if it's a PNG.
read_source(file, read, 6)
if read[0,8] == [137, 80, 78, 71, 13, 10, 26, 10]
type = PNG
else
# Check if its a bitmap.
if read[0,2] == [66, 77]
size = to_integer(read[2,4])
type = BITMAP if size == File.size(@source)
end
end
end
end
type
end
# This method generates the RTF for an ImageNode object.
def to_rtf
text = StringIO.new
count = 0
#text << '{\pard{\*\shppict{\pict'
text << '{\*\shppict{\pict'
text << "\\picscalex#{@x_scaling}" if @x_scaling != nil
text << "\\picscaley#{@y_scaling}" if @y_scaling != nil
text << "\\piccropl#{@left_crop}" if @left_crop != nil
text << "\\piccropr#{@right_crop}" if @right_crop != nil
text << "\\piccropt#{@top_crop}" if @top_crop != nil
text << "\\piccropb#{@bottom_crop}" if @bottom_crop != nil
text << "\\picwgoal#{@displayed_width}" if @displayed_width != nil
text << "\\pichgoal#{@displayed_height}" if @displayed_height != nil
text << "\\picw#{@width}\\pich#{@height}\\bliptag#{@id}"
text << "\\#{@type.id2name}\n"
open_file do |file|
file.each_byte do |byte|
hex_str = byte.to_s(16)
hex_str.insert(0,'0') if hex_str.length == 1
text << hex_str
count += 1
if count == 40
text << "\n"
count = 0
end
end
end
#text << "\n}}\\par}"
text << "\n}}"
text.string
end
# This method is used to determine the underlying endianness of a
# platform.
def get_endian
[0, 125].pack('c2').unpack('s') == [125] ? BIG_ENDIAN : LITTLE_ENDIAN
end
# This method converts an array to an integer. The array must be either
# two or four bytes in length.
#
# ==== Parameters
# array:: A reference to the array containing the data to be converted.
# signed:: A boolean to indicate whether the value is signed. Defaults
# to false.
def to_integer(array, signed=false)
from = nil
to = nil
data = []
if array.size == 2
data.concat(get_endian == BIG_ENDIAN ? array.reverse : array)
from = 'C2'
to = signed ? 's' : 'S'
else
data.concat(get_endian == BIG_ENDIAN ? array[0,4].reverse : array)
from = 'C4'
to = signed ? 'l' : 'L'
end
data.pack(from).unpack(to)[0]
end
# This method loads the data for an image from its source. The method
# accepts two call approaches. If called without a block then the method
# considers the size parameter it is passed. If called with a block the
# method executes until the block returns true.
#
# ==== Parameters
# size:: The maximum number of bytes to be read from the file. Defaults
# to nil to indicate that the remainder of the file should be read
# in.
def read_source(file, read, size=nil)
if block_given?
done = false
while !done and !file.eof?
read << file.getbyte
done = yield read[-1]
end
else
if size != nil
if size > 0
total = 0
while !file.eof? and total < size
read << file.getbyte
total += 1
end
end
else
file.each_byte {|byte| read << byte}
end
end
end
# This method fetches details of the dimensions associated with an image.
def get_dimensions
dimensions = nil
open_file do |file|
file.pos = DIMENSIONS_OFFSET[@type]
read = []
# Check the image type.
if @type == JPEG
# Read until we can't anymore or we've found what we're looking for.
done = false
while !file.eof? and !done
# Read to the next marker.
read_source(file,read) {|c| c == 0xff} # Read to the marker.
read_source(file,read) {|c| c != 0xff} # Skip any padding.
if read[-1] >= 0xc0 && read[-1] <= 0xc3
# Read in the width and height details.
read_source(file, read, 7)
dimensions = read[-4,4].pack('C4').unpack('nn').reverse
done = true
else
# Skip the marker block.
read_source(file, read, 2)
read_source(file, read, read[-2,2].pack('C2').unpack('n')[0] - 2)
end
end
elsif @type == PNG
# Read in the data to contain the width and height.
read_source(file, read, 16)
dimensions = read[-8,8].pack('C8').unpack('N2')
elsif @type == BITMAP
# Read in the data to contain the width and height.
read_source(file, read, 18)
dimensions = [to_integer(read[-8,4]), to_integer(read[-4,4])]
end
end
dimensions
end
private :get_file_type, :to_integer, :get_endian, :get_dimensions, :open_file
end # End of the ImageNode class.
# This class represents an RTF document. In actuality it is just a
# specialised Node type that cannot be assigned a parent and that holds
# document font, colour and information tables.
class Document < CommandNode
# A definition for a document character set setting.
CS_ANSI = :ansi
# A definition for a document character set setting.
CS_MAC = :mac
# A definition for a document character set setting.
CS_PC = :pc
# A definition for a document character set setting.
CS_PCA = :pca
# A definition for a document language setting.
LC_AFRIKAANS = 1078
# A definition for a document language setting.
LC_ARABIC = 1025
# A definition for a document language setting.
LC_CATALAN = 1027
# A definition for a document language setting.
LC_CHINESE_TRADITIONAL = 1028
# A definition for a document language setting.
LC_CHINESE_SIMPLIFIED = 2052
# A definition for a document language setting.
LC_CZECH = 1029
# A definition for a document language setting.
LC_DANISH = 1030
# A definition for a document language setting.
LC_DUTCH = 1043
# A definition for a document language setting.
LC_DUTCH_BELGIAN = 2067
# A definition for a document language setting.
LC_ENGLISH_UK = 2057
# A definition for a document language setting.
LC_ENGLISH_US = 1033
# A definition for a document language setting.
LC_FINNISH = 1035
# A definition for a document language setting.
LC_FRENCH = 1036
# A definition for a document language setting.
LC_FRENCH_BELGIAN = 2060
# A definition for a document language setting.
LC_FRENCH_CANADIAN = 3084
# A definition for a document language setting.
LC_FRENCH_SWISS = 4108
# A definition for a document language setting.
LC_GERMAN = 1031
# A definition for a document language setting.
LC_GERMAN_SWISS = 2055
# A definition for a document language setting.
LC_GREEK = 1032
# A definition for a document language setting.
LC_HEBREW = 1037
# A definition for a document language setting.
LC_HUNGARIAN = 1038
# A definition for a document language setting.
LC_ICELANDIC = 1039
# A definition for a document language setting.
LC_INDONESIAN = 1057
# A definition for a document language setting.
LC_ITALIAN = 1040
# A definition for a document language setting.
LC_JAPANESE = 1041
# A definition for a document language setting.
LC_KOREAN = 1042
# A definition for a document language setting.
LC_NORWEGIAN_BOKMAL = 1044
# A definition for a document language setting.
LC_NORWEGIAN_NYNORSK = 2068
# A definition for a document language setting.
LC_POLISH = 1045
# A definition for a document language setting.
LC_PORTUGUESE = 2070
# A definition for a document language setting.
LC_POTUGUESE_BRAZILIAN = 1046
# A definition for a document language setting.
LC_ROMANIAN = 1048
# A definition for a document language setting.
LC_RUSSIAN = 1049
# A definition for a document language setting.
LC_SERBO_CROATIAN_CYRILLIC = 2074
# A definition for a document language setting.
LC_SERBO_CROATIAN_LATIN = 1050
# A definition for a document language setting.
LC_SLOVAK = 1051
# A definition for a document language setting.
LC_SPANISH_CASTILLIAN = 1034
# A definition for a document language setting.
LC_SPANISH_MEXICAN = 2058
# A definition for a document language setting.
LC_SWAHILI = 1089
# A definition for a document language setting.
LC_SWEDISH = 1053
# A definition for a document language setting.
LC_THAI = 1054
# A definition for a document language setting.
LC_TURKISH = 1055
# A definition for a document language setting.
LC_UNKNOWN = 1024
# A definition for a document language setting.
LC_VIETNAMESE = 1066
# Attribute accessor.
attr_reader :fonts, :lists, :colours, :information, :character_set,
:language, :style
# Attribute mutator.
attr_writer :character_set, :language
# This is a constructor for the Document class.
#
# ==== Parameters
# font:: The default font to be used by the document.
# style:: The style settings to be applied to the document. This
# defaults to nil.
# character:: The character set to be applied to the document. This
# defaults to Document::CS_ANSI.
# language:: The language setting to be applied to document. This
# defaults to Document::LC_ENGLISH_UK.
def initialize(font, style=nil, character=CS_ANSI, language=LC_ENGLISH_UK)
super(nil, '\rtf1')
@fonts = FontTable.new(font)
@lists = ListTable.new
@default_font = 0
@colours = ColourTable.new
@information = Information.new
@character_set = character
@language = language
@style = style == nil ? DocumentStyle.new : style
@headers = [nil, nil, nil, nil]
@footers = [nil, nil, nil, nil]
@id = 0
end
# This method provides a method that can be called to generate an
# identifier that is unique within the document.
def get_id
@id += 1
Time.now().strftime('%d%m%y') + @id.to_s
end
# Attribute accessor.
def default_font
@fonts[@default_font]
end
# This method assigns a new header to a document. A Document object can
# have up to four header - a default header, a header for left pages, a
# header for right pages and a header for the first page. The method
# checks the header type and stores it appropriately.
#
# ==== Parameters
# header:: A reference to the header object to be stored. Existing header
# objects are overwritten.
def header=(header)
if header.type == HeaderNode::UNIVERSAL
@headers[0] = header
elsif header.type == HeaderNode::LEFT_PAGE
@headers[1] = header
elsif header.type == HeaderNode::RIGHT_PAGE
@headers[2] = header
elsif header.type == HeaderNode::FIRST_PAGE
@headers[3] = header
end
end
# This method assigns a new footer to a document. A Document object can
# have up to four footers - a default footer, a footer for left pages, a
# footer for right pages and a footer for the first page. The method
# checks the footer type and stores it appropriately.
#
# ==== Parameters
# footer:: A reference to the footer object to be stored. Existing footer
# objects are overwritten.
def footer=(footer)
if footer.type == FooterNode::UNIVERSAL
@footers[0] = footer
elsif footer.type == FooterNode::LEFT_PAGE
@footers[1] = footer
elsif footer.type == FooterNode::RIGHT_PAGE
@footers[2] = footer
elsif footer.type == FooterNode::FIRST_PAGE
@footers[3] = footer
end
end
# This method fetches a header from a Document object.
#
# ==== Parameters
# type:: One of the header types defined in the header class. Defaults to
# HeaderNode::UNIVERSAL.
def header(type=HeaderNode::UNIVERSAL)
index = 0
if type == HeaderNode::LEFT_PAGE
index = 1
elsif type == HeaderNode::RIGHT_PAGE
index = 2
elsif type == HeaderNode::FIRST_PAGE
index = 3
end
@headers[index]
end
# This method fetches a footer from a Document object.
#
# ==== Parameters
# type:: One of the footer types defined in the footer class. Defaults to
# FooterNode::UNIVERSAL.
def footer(type=FooterNode::UNIVERSAL)
index = 0
if type == FooterNode::LEFT_PAGE
index = 1
elsif type == FooterNode::RIGHT_PAGE
index = 2
elsif type == FooterNode::FIRST_PAGE
index = 3
end
@footers[index]
end
# Attribute mutator.
#
# ==== Parameters
# font:: The new default font for the Document object.
def default_font=(font)
@fonts << font
@default_font = @fonts.index(font)
end
# This method provides a short cut for obtaining the Paper object
# associated with a Document object.
def paper
@style.paper
end
# This method overrides the parent=() method inherited from the
# CommandNode class to disallow setting a parent on a Document object.
#
# ==== Parameters
# parent:: A reference to the new parent node for the Document object.
#
# ==== Exceptions
# RTFError:: Generated whenever this method is called.
def parent=(parent)
RTFError.fire("Document objects may not have a parent.")
end
# This method inserts a page break into a document.
def page_break
self.store(CommandNode.new(self, '\page', nil, false))
nil
end
# This method fetches the width of the available work area space for a
# typical Document object page.
def body_width
@style.body_width
end
# This method fetches the height of the available work area space for a
# a typical Document object page.
def body_height
@style.body_height
end
# This method generates the RTF text for a Document object.
def to_rtf
text = StringIO.new
text << "{#{prefix}\\#{@character_set.id2name}"
text << "\\deff#{@default_font}"
text << "\\deflang#{@language}" if !@language.nil?
text << "\\plain\\fs24\\fet1"
text << "\n#{@fonts.to_rtf}"
text << "\n#{@colours.to_rtf}" if @colours.size > 0
text << "\n#{@information.to_rtf}"
text << "\n#{@lists.to_rtf}"
if @headers.compact != []
text << "\n#{@headers[3].to_rtf}" if !@headers[3].nil?
text << "\n#{@headers[2].to_rtf}" if !@headers[2].nil?
text << "\n#{@headers[1].to_rtf}" if !@headers[1].nil?
if @headers[1].nil? or @headers[2].nil?
text << "\n#{@headers[0].to_rtf}"
end
end
if @footers.compact != []
text << "\n#{@footers[3].to_rtf}" if !@footers[3].nil?
text << "\n#{@footers[2].to_rtf}" if !@footers[2].nil?
text << "\n#{@footers[1].to_rtf}" if !@footers[1].nil?
if @footers[1].nil? or @footers[2].nil?
text << "\n#{@footers[0].to_rtf}"
end
end
text << "\n#{@style.prefix(self)}" if !@style.nil?
self.each {|entry| text << "\n#{entry.to_rtf}"}
text << "\n}"
text.string
end
end # End of the Document class.
end # End of the RTF module.