lib/rtf/node.rb in rtf-0.1.0 vs lib/rtf/node.rb in rtf-0.3.0
- old
+ new
@@ -61,16 +61,13 @@
# This class represents a specialisation of the Node class to refer to a Node
# that simply contains text.
class TextNode < Node
- # Attribute accessor.
- attr_reader :text
+ # Actual text
+ attr_accessor :text
- # Attribute mutator.
- attr_writer :text
-
# This is the constructor for the TextNode class.
#
# ==== Parameters
# parent:: A reference to the Node that owns the TextNode. Must not be
# nil.
@@ -129,16 +126,13 @@
# 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
- # Attribute accessor.
- attr_reader :children
+ # Children elements of the node
+ attr_accessor :children
- # Attribute mutator.
- attr_writer :children
-
# This is the constructor for the ContainerNode class.
#
# ==== Parameters
# parent:: A reference to the parent node that owners the new
# ContainerNode object.
@@ -206,16 +200,19 @@
# 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
- # Attribute accessor.
- attr_reader :prefix, :suffix, :split
+ # 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
- # Attribute mutator.
- attr_writer :prefix, :suffix, :split
-
# 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.
@@ -304,10 +301,23 @@
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.
@@ -507,30 +517,37 @@
# 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
- # Attribute accessor.
- attr_reader :cell_margin
-
- # Attribute mutator.
- attr_writer :cell_margin
-
+ # 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, rows, columns, *widths)
+ 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
@@ -654,11 +671,11 @@
end
entries
end
end
- # Attrobute accessors
+ # Attribute accessors
def length
entries.size
end
# This method assigns a border width setting to all of the sides on all
@@ -730,17 +747,15 @@
# 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
-
+ # Width of cell
+ attr_accessor :width
# Attribute accessor.
- attr_reader :width, :shading_colour, :style
-
- # Attribute mutator.
- attr_writer :width, :style
-
+ 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
@@ -1056,10 +1071,252 @@
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
+
+ # Attribute accessor.
+ attr_reader :x_scaling, :y_scaling, :top_crop, :right_crop, :bottom_crop,
+ :left_crop, :width, :height
+
+ # Attribute mutator.
+ attr_writer :x_scaling, :y_scaling, :top_crop, :right_crop, :bottom_crop,
+ :left_crop
+
+
+ # 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
+ @read = []
+ @type = nil
+ @x_scaling = @y_scaling = nil
+ @top_crop = @right_crop = @bottom_crop = @left_crop = nil
+ @width = @height = nil
+
+ # Check what we were given.
+ src = source
+ src.binmode if src.instance_of?(File)
+ src = File.new(source, 'rb') if source.instance_of?(String)
+ if src.instance_of?(File)
+ # Check the files existence and accessibility.
+ if !File.exist?(src.path)
+ RTFError.fire("Unable to find the #{File.basename(source)} file.")
+ end
+ if !File.readable?(src.path)
+ RTFError.fire("Access to the #{File.basename(source)} file denied.")
+ end
+ @source = src
+ else
+ RTFError.fire("Unrecognised source specified for ImageNode.")
+ end
+
+ @type = get_file_type(src)
+ if @type == nil
+ RTFError.fire("The #{File.basename(source)} file contains an "\
+ "unknown or unsupported image type.")
+ end
+
+ @width, @height = get_dimensions
+ end
+
+ # This method attempts to determine the image type associated with a
+ # file, returning nil if it fails to make the determination.
+ #
+ # ==== Parameters
+ # file:: A reference to the file to check for image type.
+ def get_file_type(file)
+ type = nil
+
+ # Check if the file is a JPEG.
+ read_source(2)
+
+ if @read[0,2] == [255, 216]
+ type = JPEG
+ else
+ # Check if it's a PNG.
+ read_source(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(file.path)
+ 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 << "\\picw#{@width}\\pich#{@height}\\bliptag#{@id}"
+ text << "\\#{@type.id2name}\n"
+ @source.each_byte {|byte| @read << byte} if @source.eof? == false
+ @read.each do |byte|
+ text << ("%02x" % byte)
+ count += 1
+ if count == 40
+ text << "\n"
+ count = 0
+ 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(size=nil)
+ if block_given?
+ done = false
+
+ while done == false && @source.eof? == false
+ @read << @source.getbyte
+ done = yield @read[-1]
+ end
+ else
+ if size != nil
+ if size > 0
+ total = 0
+ while @source.eof? == false && total < size
+
+ @read << @source.getbyte
+ total += 1
+ end
+ end
+ else
+ @source.each_byte {|byte| @read << byte}
+ end
+ end
+ end
+
+ # This method fetches details of the dimensions associated with an image.
+ def get_dimensions
+ dimensions = nil
+
+ # 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 @source.eof? == false && done == false
+ # Read to the next marker.
+ read_source {|c| c == 0xff} # Read to the marker.
+ read_source {|c| c != 0xff} # Skip any padding.
+
+ if @read[-1] >= 0xc0 && @read[-1] <= 0xc3
+ # Read in the width and height details.
+ read_source(7)
+ dimensions = @read[-4,4].pack('C4').unpack('nn').reverse
+ done = true
+ else
+ # Skip the marker block.
+ read_source(2)
+ read_source(@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(16)
+ dimensions = @read[-8,8].pack('C8').unpack('N2')
+ elsif @type == BITMAP
+ # Read in the data to contain the width and height.
+ read_source(18)
+ dimensions = [to_integer(@read[-8,4]), to_integer(@read[-4,4])]
+ end
+
+ dimensions
+ end
+
+ private :read_source, :get_file_type, :to_integer, :get_endian,
+ :get_dimensions
+ 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.
@@ -1233,9 +1490,17 @@
@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]
\ No newline at end of file