require 'stringio' module RRTF # 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"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. style.push_colours(root.colours) style.push_fonts(root.fonts) # Generate the command node. node = CommandNode.new(self, style.prefix(root)) 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(parent.root) 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(root) 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. # @author Peter Wood class Document < CommandNode # A hash mapping character set string constants to their RTF counterparts. # @return [Hash] the RTF character set dictionary. CS_DICTIONARY = { "ANSI" => :ansi, "MAC" => :mac, "PC" => :pc, "PCA" => :pca }.freeze # A hash mapping langauge set string constants to their RTF counterparts. # @return [Hash] the RTF langauge setting dictionary. LS_DICTIONARY = { "AFRIKAANS" => 1078, "ARABIC" => 1025, "CATALAN" => 1027, "CHINESE_TRADITIONAL" => 1028, "CHINESE_SIMPLIFIED" => 2052, "CZECH" => 1029, "DANISH" => 1030, "DUTCH" => 1043, "DUTCH_BELGIAN" => 2067, "ENGLISH_UK" => 2057, "ENGLISH_US" => 1033, "FINNISH" => 1035, "FRENCH" => 1036, "FRENCH_BELGIAN" => 2060, "FRENCH_CANADIAN" => 3084, "FRENCH_SWISS" => 4108, "GERMAN" => 1031, "GERMAN_SWISS" => 2055, "GREEK" => 1032, "HEBREW" => 1037, "HUNGARIAN" => 1038, "ICELANDIC" => 1039, "INDONESIAN" => 1057, "ITALIAN" => 1040, "JAPANESE" => 1041, "KOREAN" => 1042, "NORWEGIAN_BOKMAL" => 1044, "NORWEGIAN_NYNORSK" => 2068, "POLISH" => 1045, "PORTUGUESE" => 2070, "POTUGUESE_BRAZILIAN" => 1046, "ROMANIAN" => 1048, "RUSSIAN" => 1049, "SERBO_CROATIAN_CYRILLIC" => 2074, "SERBO_CROATIAN_LATIN" => 1050, "SLOVAK" => 1051, "SPANISH_CASTILLIAN" => 1034, "SPANISH_MEXICAN" => 2058, "SWAHILI" => 1089, "SWEDISH" => 1053, "THAI" => 1054, "TURKISH" => 1055, "UNKNOWN" => 1024, "VIETNAMESE" => 1066 }.freeze # Attribute accessor. attr_reader :fonts, :lists, :colours, :information, :character_set, :language, :style, :stylesheet # Attribute mutator. attr_writer :character_set, :language, :stylesheet # Represents an entire RTF document. # @note The "suppress_system_styles" option is ignored by most RTF platforms including Word and LibreOffice. # @see DocumentStyle#initialize DocumentStyle#initialize for available document style options. # @see Stylesheet#add_style Stylesheet#initialize for available stylesheet options. # # @param [Hash] options the options to use in creating the document. # @option options [String, Font] "default_font" ("SWISS:Helvetica") a font object OR string encapsulating the default font to be used by the document (see {Font.from_string} for string format). # @option options [String] "character_set" ("ANSI") the character set to be applied to the document (see {CS_DICTIONARY} for valid values). # @option options [String] "language" ("ENGLISH_US") the language setting to be applied to the document (see {LS_DICTIONARY} for valid values). # @option options [Boolean] "suppress_system_styles" (false) whether or not to suppress styles provided in the host platform (adds the \noqfpromote control word before stylesheet definition). # @option options [DocumentStyle] "document_style" (DocumentStyle.new) a DocumentStyle object OR options hash encapsulating the style settings to be applied to the document. # @option options [Array, Hash, Stylesheet] "stylesheet" (nil) a Stylesheet object OR array of style hashes OR hash of stylesheet options with which to use as or construct the stylesheet for the document. def initialize(options = {}) # load default options options = { "default_font" => "SWISS:Helvetica", "document_style" => DocumentStyle.new, "character_set" => "ANSI", "language" => "ENGLISH_US", "suppress_system_styles" => false, "stylesheet" => nil }.merge(options) super(nil, '\rtf1') # parse font font = options.delete("default_font") case font when Font when String font = Font.from_string(font) else RTFError.fire("Unreconized font format #{font.class.to_s}") end # case # parse document style style = options.delete("document_style") case style when DocumentStyle when Hash style = DocumentStyle.new(style) else RTFError.fire("Unreconized document style format #{font.class.to_s}") end # case # parse character set cs_string = options.delete("character_set") cs_val = CS_DICTIONARY[cs_string] if cs_val.nil? RTFError.fire("Unreconized character set '#{cs_string}'.") end # if # parse language setting ls_string = options.delete("language") ls_val = LS_DICTIONARY[ls_string] if ls_val.nil? RTFError.fire("Unreconized language '#{ls_string}'.") end # if @fonts = FontTable.new(font) @lists = ListTable.new @default_font = 0 @colours = ColourTable.new @information = Information.new @character_set = cs_val @language = ls_val @style = style @headers = [nil, nil, nil, nil] @footers = [nil, nil, nil, nil] @id = 0 # parse stylesheet (must be done after font and colour tables are # initialized since declared styles may push fonts/colours onto the # tables) stylesheet = options.delete("stylesheet") case stylesheet when Stylesheet stylesheet.document = self when Array stylesheet = Stylesheet.new(self, "styles" => stylesheet) when Hash stylesheet = Stylesheet.new(self, stylesheet) else RTFError.fire("Unreconized stylesheet format #{font.class.to_s}") end unless stylesheet.nil? # case @stylesheet = stylesheet # additional options @options = options 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 # Loads a stylesheet for the document from an array of hashmaps # representing styles def load_stylesheet(hashmap_array) @stylesheet = Stylesheet.new(self, hashmap_array) 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\\noqfpromote" if @options["suppress_system_styles"] text << "\n#{@stylesheet.to_rtf}" if !@stylesheet.nil? 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.