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