rrtf 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/rrtf/node.rb ADDED
@@ -0,0 +1,1932 @@
1
+ require 'stringio'
2
+
3
+ module RRTF
4
+ # This class represents an element within an RTF document. The class provides
5
+ # a base class for more specific node types.
6
+ class Node
7
+ # Node parent.
8
+ attr_accessor :parent
9
+
10
+ # Constructor for the Node class.
11
+ #
12
+ # ==== Parameters
13
+ # parent:: A reference to the Node that owns the new Node. May be nil
14
+ # to indicate a base or root node.
15
+ def initialize(parent)
16
+ @parent = parent
17
+ end
18
+
19
+ # This method retrieves a Node objects previous peer node, returning nil
20
+ # if the Node has no previous peer.
21
+ def previous_node
22
+ peer = nil
23
+ if !parent.nil? and parent.respond_to?(:children)
24
+ index = parent.children.index(self)
25
+ peer = index > 0 ? parent.children[index - 1] : nil
26
+ end
27
+ peer
28
+ end
29
+
30
+ # This method retrieves a Node objects next peer node, returning nil
31
+ # if the Node has no previous peer.
32
+ def next_node
33
+ peer = nil
34
+ if !parent.nil? and parent.respond_to?(:children)
35
+ index = parent.children.index(self)
36
+ peer = parent.children[index + 1]
37
+ end
38
+ peer
39
+ end
40
+
41
+ # This method is used to determine whether a Node object represents a
42
+ # root or base element. The method returns true if the Nodes parent is
43
+ # nil, false otherwise.
44
+ def is_root?
45
+ @parent.nil?
46
+ end
47
+
48
+ # This method traverses a Node tree to locate the root element.
49
+ def root
50
+ node = self
51
+ node = node.parent while !node.parent.nil?
52
+ node
53
+ end
54
+ end # End of the Node class.
55
+
56
+
57
+ # This class represents a specialisation of the Node class to refer to a Node
58
+ # that simply contains text.
59
+ class TextNode < Node
60
+ # Actual text
61
+ attr_accessor :text
62
+
63
+ # This is the constructor for the TextNode class.
64
+ #
65
+ # ==== Parameters
66
+ # parent:: A reference to the Node that owns the TextNode. Must not be
67
+ # nil.
68
+ # text:: A String containing the node text. Defaults to nil.
69
+ #
70
+ # ==== Exceptions
71
+ # RTFError:: Generated whenever an nil parent object is specified to
72
+ # the method.
73
+ def initialize(parent, text=nil)
74
+ super(parent)
75
+ if parent.nil?
76
+ RTFError.fire("Nil parent specified for text node.")
77
+ end
78
+ @parent = parent
79
+ @text = text
80
+ end
81
+
82
+ # This method concatenates a String on to the end of the existing text
83
+ # within a TextNode object.
84
+ #
85
+ # ==== Parameters
86
+ # text:: The String to be added to the end of the text node.
87
+ def append(text)
88
+ @text = (@text.nil?) ? text.to_s : @text + text.to_s
89
+ end
90
+
91
+ # This method inserts a String into the existing text within a TextNode
92
+ # object. If the TextNode contains no text then it is simply set to the
93
+ # text passed in. If the offset specified is past the end of the nodes
94
+ # text then it is simply appended to the end.
95
+ #
96
+ # ==== Parameters
97
+ # text:: A String containing the text to be added.
98
+ # offset:: The numbers of characters from the first character to insert
99
+ # the new text at.
100
+ def insert(text, offset)
101
+ if !@text.nil?
102
+ @text = @text[0, offset] + text.to_s + @text[offset, @text.length]
103
+ else
104
+ @text = text.to_s
105
+ end
106
+ end
107
+
108
+ # This method generates the RTF equivalent for a TextNode object. This
109
+ # method escapes any special sequences that appear in the text.
110
+ def to_rtf
111
+ rtf=(@text.nil? ? '' : @text.gsub("{", "\\{").gsub("}", "\\}").gsub("\\", "\\\\"))
112
+ # This is from lfarcy / rtf-extensions
113
+ # I don't see the point of coding different 128<n<256 range
114
+
115
+ #f1=lambda { |n| n < 128 ? n.chr : n < 256 ? "\\'#{n.to_s(16)}" : "\\u#{n}\\'3f" }
116
+ # Encode as Unicode.
117
+
118
+ f=lambda { |n| n < 128 ? n.chr : "\\u#{n}\\'3f" }
119
+ # Ruby 1.9 is safe, cause detect original encoding
120
+ # and convert text to utf-16 first
121
+ if RUBY_VERSION>"1.9.0"
122
+ return rtf.encode("UTF-16LE", :undef=>:replace).each_codepoint.map(&f).join('')
123
+ else
124
+ # You SHOULD use UTF-8 as input, ok?
125
+ return rtf.unpack('U*').map(&f).join('')
126
+ end
127
+ end
128
+ end # End of the TextNode class.
129
+
130
+
131
+ # This class represents a Node that can contain other Node objects. Its a
132
+ # base class for more specific Node types.
133
+ class ContainerNode < Node
134
+ include Enumerable
135
+
136
+ # Children elements of the node
137
+ attr_accessor :children
138
+
139
+ # This is the constructor for the ContainerNode class.
140
+ #
141
+ # ==== Parameters
142
+ # parent:: A reference to the parent node that owners the new
143
+ # ContainerNode object.
144
+ def initialize(parent)
145
+ super(parent)
146
+ @children = []
147
+ @children.concat(yield) if block_given?
148
+ end
149
+
150
+ # This method adds a new node element to the end of the list of nodes
151
+ # maintained by a ContainerNode object. Nil objects are ignored.
152
+ #
153
+ # ==== Parameters
154
+ # node:: A reference to the Node object to be added.
155
+ def store(node)
156
+ if !node.nil?
157
+ @children.push(node) if !@children.include?(Node)
158
+ node.parent = self if node.parent != self
159
+ end
160
+ node
161
+ end
162
+
163
+ # This method fetches the first node child for a ContainerNode object. If
164
+ # a container contains no children this method returns nil.
165
+ def first
166
+ @children[0]
167
+ end
168
+
169
+ # This method fetches the last node child for a ContainerNode object. If
170
+ # a container contains no children this method returns nil.
171
+ def last
172
+ @children.last
173
+ end
174
+
175
+ # This method provides for iteration over the contents of a ContainerNode
176
+ # object.
177
+ def each
178
+ @children.each {|child| yield child}
179
+ end
180
+
181
+ # This method returns a count of the number of children a ContainerNode
182
+ # object contains.
183
+ def size
184
+ @children.size
185
+ end
186
+
187
+ # This method overloads the array dereference operator to allow for
188
+ # access to the child elements of a ContainerNode object.
189
+ #
190
+ # ==== Parameters
191
+ # index:: The offset from the first child of the child object to be
192
+ # returned. Negative index values work from the back of the
193
+ # list of children. An invalid index will cause a nil value
194
+ # to be returned.
195
+ def [](index)
196
+ @children[index]
197
+ end
198
+
199
+ # This method generates the RTF text for a ContainerNode object.
200
+ def to_rtf
201
+ RTFError.fire("#{self.class.name}.to_rtf method not yet implemented.")
202
+ end
203
+ end # End of the ContainerNode class.
204
+
205
+
206
+ # This class represents a RTF command element within a document. This class
207
+ # is concrete enough to be used on its own but will also be used as the
208
+ # base class for some specific command node types.
209
+ class CommandNode < ContainerNode
210
+ # String containing the prefix text for the command
211
+ attr_accessor :prefix
212
+ # String containing the suffix text for the command
213
+ attr_accessor :suffix
214
+ # A boolean to indicate whether the prefix and suffix should
215
+ # be written to separate lines whether the node is converted
216
+ # to RTF. Defaults to true
217
+ attr_accessor :split
218
+ # A boolean to indicate whether the prefix and suffix should
219
+ # be wrapped in curly braces. Defaults to true.
220
+ attr_accessor :wrap
221
+
222
+ # This is the constructor for the CommandNode class.
223
+ #
224
+ # ==== Parameters
225
+ # parent:: A reference to the node that owns the new node.
226
+ # prefix:: A String containing the prefix text for the command.
227
+ # suffix:: A String containing the suffix text for the command. Defaults
228
+ # to nil.
229
+ # split:: A boolean to indicate whether the prefix and suffix should
230
+ # be written to separate lines whether the node is converted
231
+ # to RTF. Defaults to true.
232
+ # wrap:: A boolean to indicate whether the prefix and suffix should
233
+ # be wrapped in curly braces. Defaults to true.
234
+ def initialize(parent, prefix, suffix=nil, split=true, wrap=true)
235
+ super(parent)
236
+ @prefix = prefix
237
+ @suffix = suffix
238
+ @split = split
239
+ @wrap = wrap
240
+ end
241
+
242
+ # This method adds text to a command node. If the last child node of the
243
+ # target node is a TextNode then the text is appended to that. Otherwise
244
+ # a new TextNode is created and append to the node.
245
+ #
246
+ # ==== Parameters
247
+ # text:: The String of text to be written to the node.
248
+ def <<(text)
249
+ if !last.nil? and last.respond_to?(:text=)
250
+ last.append(text)
251
+ else
252
+ self.store(TextNode.new(self, text))
253
+ end
254
+ end
255
+
256
+ # This method generates the RTF text for a CommandNode object.
257
+ def to_rtf
258
+ text = StringIO.new
259
+
260
+ text << '{' if wrap?
261
+ text << @prefix if @prefix
262
+
263
+ self.each do |entry|
264
+ text << "\n" if split?
265
+ text << entry.to_rtf
266
+ end
267
+
268
+ text << "\n" if split?
269
+ text << @suffix if @suffix
270
+ text << '}' if wrap?
271
+
272
+ text.string
273
+ end
274
+
275
+ # This method provides a short cut means of creating a paragraph command
276
+ # node. The method accepts a block that will be passed a single parameter
277
+ # which will be a reference to the paragraph node created. After the
278
+ # block is complete the paragraph node is appended to the end of the child
279
+ # nodes on the object that the method is called against.
280
+ #
281
+ # ==== Parameters
282
+ # style:: A reference to a ParagraphStyle object that defines the style
283
+ # for the new paragraph. Defaults to nil to indicate that the
284
+ # currently applied paragraph styling should be used.
285
+ def paragraph(style=nil)
286
+ node = ParagraphNode.new(self, style)
287
+ yield node if block_given?
288
+ self.store(node)
289
+ end
290
+
291
+ # This method provides a short cut means of creating a new ordered or
292
+ # unordered list. The method requires a block that will be passed a
293
+ # single parameter that'll be a reference to the first level of the
294
+ # list. See the +ListLevelNode+ doc for more information.
295
+ #
296
+ # Example usage:
297
+ #
298
+ # rtf.list do |level1|
299
+ # level1.item do |li|
300
+ # li << 'some text'
301
+ # li.apply(some_style) {|x| x << 'some styled text'}
302
+ # end
303
+ #
304
+ # level1.list(:decimal) do |level2|
305
+ # level2.item {|li| li << 'some other text in a decimal list'}
306
+ # level2.item {|li| li << 'and here we go'}
307
+ # end
308
+ # end
309
+ #
310
+ def list(kind=:bullets)
311
+ node = ListNode.new(self)
312
+ yield node.list(kind)
313
+ self.store(node)
314
+ end
315
+
316
+ def link(url, text=nil)
317
+ node = LinkNode.new(self, url)
318
+ node << text if text
319
+ yield node if block_given?
320
+ self.store(node)
321
+ end
322
+
323
+ # This method provides a short cut means of creating a line break command
324
+ # node. This command node does not take a block and may possess no other
325
+ # content.
326
+ def line_break
327
+ self.store(CommandNode.new(self, '\line', nil, false))
328
+ nil
329
+ end
330
+
331
+ # This method inserts a footnote at the current position in a node.
332
+ #
333
+ # ==== Parameters
334
+ # text:: A string containing the text for the footnote.
335
+ def footnote(text)
336
+ if !text.nil? and text != ''
337
+ mark = CommandNode.new(self, '\fs16\up6\chftn', nil, false)
338
+ note = CommandNode.new(self, '\footnote {\fs16\up6\chftn}', nil, false)
339
+ note.paragraph << text
340
+ self.store(mark)
341
+ self.store(note)
342
+ end
343
+ end
344
+
345
+ # This method inserts a new image at the current position in a node.
346
+ #
347
+ # ==== Parameters
348
+ # source:: Either a string containing the path and name of a file or a
349
+ # File object for the image file to be inserted.
350
+ #
351
+ # ==== Exceptions
352
+ # RTFError:: Generated whenever an invalid or inaccessible file is
353
+ # specified or the image file type is not supported.
354
+ def image(source)
355
+ self.store(ImageNode.new(self, source, root.get_id))
356
+ end
357
+
358
+ # This method provides a short cut means for applying multiple styles via
359
+ # single command node. The method accepts a block that will be passed a
360
+ # reference to the node created. Once the block is complete the new node
361
+ # will be append as the last child of the CommandNode the method is called
362
+ # on.
363
+ #
364
+ # ==== Parameters
365
+ # style:: A reference to a CharacterStyle object that contains the style
366
+ # settings to be applied.
367
+ #
368
+ # ==== Exceptions
369
+ # RTFError:: Generated whenever a non-character style is specified to
370
+ # the method.
371
+ def apply(style)
372
+ # Check the input style.
373
+ if !style.is_character_style?
374
+ RTFError.fire("Non-character style specified to the "\
375
+ "CommandNode#apply() method.")
376
+ end
377
+
378
+ # Store fonts and colours.
379
+ style.push_colours(root.colours)
380
+ style.push_fonts(root.fonts)
381
+
382
+ # Generate the command node.
383
+ node = CommandNode.new(self, style.prefix(root))
384
+ yield node if block_given?
385
+ self.store(node)
386
+ end
387
+
388
+ # This method provides a short cut means of creating a bold command node.
389
+ # The method accepts a block that will be passed a single parameter which
390
+ # will be a reference to the bold node created. After the block is
391
+ # complete the bold node is appended to the end of the child nodes on
392
+ # the object that the method is call against.
393
+ def bold
394
+ style = CharacterStyle.new
395
+ style.bold = true
396
+ if block_given?
397
+ apply(style) {|node| yield node}
398
+ else
399
+ apply(style)
400
+ end
401
+ end
402
+
403
+ # This method provides a short cut means of creating an italic command
404
+ # node. The method accepts a block that will be passed a single parameter
405
+ # which will be a reference to the italic node created. After the block is
406
+ # complete the italic node is appended to the end of the child nodes on
407
+ # the object that the method is call against.
408
+ def italic
409
+ style = CharacterStyle.new
410
+ style.italic = true
411
+ if block_given?
412
+ apply(style) {|node| yield node}
413
+ else
414
+ apply(style)
415
+ end
416
+ end
417
+
418
+ # This method provides a short cut means of creating an underline command
419
+ # node. The method accepts a block that will be passed a single parameter
420
+ # which will be a reference to the underline node created. After the block
421
+ # is complete the underline node is appended to the end of the child nodes
422
+ # on the object that the method is call against.
423
+ def underline
424
+ style = CharacterStyle.new
425
+ style.underline = true
426
+ if block_given?
427
+ apply(style) {|node| yield node}
428
+ else
429
+ apply(style)
430
+ end
431
+ end
432
+
433
+ # This method provides a short cut means of creating a subscript command
434
+ # node. The method accepts a block that will be passed a single parameter
435
+ # which will be a reference to the subscript node created. After the
436
+ # block is complete the subscript node is appended to the end of the
437
+ # child nodes on the object that the method is call against.
438
+ def subscript
439
+ style = CharacterStyle.new
440
+ style.subscript = true
441
+ if block_given?
442
+ apply(style) {|node| yield node}
443
+ else
444
+ apply(style)
445
+ end
446
+ end
447
+
448
+ # This method provides a short cut means of creating a superscript command
449
+ # node. The method accepts a block that will be passed a single parameter
450
+ # which will be a reference to the superscript node created. After the
451
+ # block is complete the superscript node is appended to the end of the
452
+ # child nodes on the object that the method is call against.
453
+ def superscript
454
+ style = CharacterStyle.new
455
+ style.superscript = true
456
+ if block_given?
457
+ apply(style) {|node| yield node}
458
+ else
459
+ apply(style)
460
+ end
461
+ end
462
+
463
+ # This method provides a short cut means of creating a strike command
464
+ # node. The method accepts a block that will be passed a single parameter
465
+ # which will be a reference to the strike node created. After the
466
+ # block is complete the strike node is appended to the end of the
467
+ # child nodes on the object that the method is call against.
468
+ def strike
469
+ style = CharacterStyle.new
470
+ style.strike = true
471
+ if block_given?
472
+ apply(style) {|node| yield node}
473
+ else
474
+ apply(style)
475
+ end
476
+ end
477
+
478
+ # This method provides a short cut means of creating a font command node.
479
+ # The method accepts a block that will be passed a single parameter which
480
+ # will be a reference to the font node created. After the block is
481
+ # complete the font node is appended to the end of the child nodes on the
482
+ # object that the method is called against.
483
+ #
484
+ # ==== Parameters
485
+ # font:: A reference to font object that represents the font to be used
486
+ # within the node.
487
+ # size:: An integer size setting for the font. Defaults to nil to
488
+ # indicate that the current font size should be used.
489
+ def font(font, size=nil)
490
+ style = CharacterStyle.new
491
+ style.font = font
492
+ style.font_size = size
493
+ root.fonts << font
494
+ if block_given?
495
+ apply(style) {|node| yield node}
496
+ else
497
+ apply(style)
498
+ end
499
+ end
500
+
501
+ # This method provides a short cut means of creating a foreground colour
502
+ # command node. The method accepts a block that will be passed a single
503
+ # parameter which will be a reference to the foreground colour node
504
+ # created. After the block is complete the foreground colour node is
505
+ # appended to the end of the child nodes on the object that the method
506
+ # is called against.
507
+ #
508
+ # ==== Parameters
509
+ # colour:: The foreground colour to be applied by the command.
510
+ def foreground(colour)
511
+ style = CharacterStyle.new
512
+ style.foreground = colour
513
+ root.colours << colour
514
+ if block_given?
515
+ apply(style) {|node| yield node}
516
+ else
517
+ apply(style)
518
+ end
519
+ end
520
+
521
+ # This method provides a short cut means of creating a background colour
522
+ # command node. The method accepts a block that will be passed a single
523
+ # parameter which will be a reference to the background colour node
524
+ # created. After the block is complete the background colour node is
525
+ # appended to the end of the child nodes on the object that the method
526
+ # is called against.
527
+ #
528
+ # ==== Parameters
529
+ # colour:: The background colour to be applied by the command.
530
+ def background(colour)
531
+ style = CharacterStyle.new
532
+ style.background = colour
533
+ root.colours << colour
534
+ if block_given?
535
+ apply(style) {|node| yield node}
536
+ else
537
+ apply(style)
538
+ end
539
+ end
540
+
541
+ # This method provides a short cut menas of creating a colour node that
542
+ # deals with foreground and background colours. The method accepts a
543
+ # block that will be passed a single parameter which will be a reference
544
+ # to the colour node created. After the block is complete the colour node
545
+ # is append to the end of the child nodes on the object that the method
546
+ # is called against.
547
+ #
548
+ # ==== Parameters
549
+ # fore:: The foreground colour to be applied by the command.
550
+ # back:: The background colour to be applied by the command.
551
+ def colour(fore, back)
552
+ style = CharacterStyle.new
553
+ style.foreground = fore
554
+ style.background = back
555
+ root.colours << fore
556
+ root.colours << back
557
+ if block_given?
558
+ apply(style) {|node| yield node}
559
+ else
560
+ apply(style)
561
+ end
562
+ end
563
+
564
+ # This method creates a new table node and returns it. The method accepts
565
+ # a block that will be passed the table as a parameter. The node is added
566
+ # to the node the method is called upon after the block is complete.
567
+ #
568
+ # ==== Parameters
569
+ # rows:: The number of rows that the table contains.
570
+ # columns:: The number of columns that the table contains.
571
+ # *widths:: One or more integers representing the widths for the table
572
+ # columns.
573
+ def table(rows, columns, *widths)
574
+ node = TableNode.new(self, rows, columns, *widths)
575
+ yield node if block_given?
576
+ store(node)
577
+ node
578
+ end
579
+
580
+ alias :write :<<
581
+ alias :color :colour
582
+ alias :split? :split
583
+ alias :wrap? :wrap
584
+ end # End of the CommandNode class.
585
+
586
+ # This class represents a paragraph within an RTF document.
587
+ class ParagraphNode < CommandNode
588
+ def initialize(parent, style=nil)
589
+ prefix = '\pard'
590
+ prefix << style.prefix(parent.root) if style
591
+
592
+ super(parent, prefix, '\par')
593
+ end
594
+ end
595
+
596
+ # This class represents an ordered/unordered list within an RTF document.
597
+ #
598
+ # Currently list nodes can contain any type of node, but this behaviour
599
+ # will change in future releases. The class overrides the +list+ method
600
+ # to return a +ListLevelNode+.
601
+ #
602
+ class ListNode < CommandNode
603
+ def initialize(parent)
604
+ prefix = "\\"
605
+
606
+ suffix = '\pard'
607
+ suffix << ListLevel::ResetTabs.map {|tw| "\\tx#{tw}"}.join
608
+ suffix << '\ql\qlnatural\pardirnatural\cf0 \\'
609
+
610
+ super(parent, prefix, suffix, true, false)
611
+
612
+ @template = root.lists.new_template
613
+ end
614
+
615
+ # This method creates a new +ListLevelNode+ of the given kind and
616
+ # stores it in the document tree.
617
+ #
618
+ # ==== Parameters
619
+ # kind:: The kind of this list level, may be either :bullets or :decimal
620
+ def list(kind)
621
+ self.store ListLevelNode.new(self, @template, kind)
622
+ end
623
+ end
624
+
625
+ # This class represents a list level, and carries out indenting information
626
+ # and the bullet or number that is prepended to each +ListTextNode+.
627
+ #
628
+ # The class overrides the +list+ method to implement nesting, and provides
629
+ # the +item+ method to add a new list item, the +ListTextNode+.
630
+ class ListLevelNode < CommandNode
631
+ def initialize(parent, template, kind, level=1)
632
+ @template = template
633
+ @kind = kind
634
+ @level = template.level_for(level, kind)
635
+
636
+ prefix = '\pard'
637
+ prefix << @level.tabs.map {|tw| "\\tx#{tw}"}.join
638
+ prefix << "\\li#{@level.indent}\\fi-#{@level.indent}"
639
+ prefix << "\\ql\\qlnatural\\pardirnatural\n"
640
+ prefix << "\\ls#{@template.id}\\ilvl#{@level.level-1}\\cf0"
641
+
642
+ super(parent, prefix, nil, true, false)
643
+ end
644
+
645
+ # Returns the kind of this level, either :bullets or :decimal
646
+ attr_reader :kind
647
+
648
+ # Returns the indenting level of this list, from 1 to 9
649
+ def level
650
+ @level.level
651
+ end
652
+
653
+ # Creates a new +ListTextNode+ and yields it to the calling block
654
+ def item
655
+ node = ListTextNode.new(self, @level)
656
+ yield node
657
+ self.store(node)
658
+ end
659
+
660
+ # Creates a new +ListLevelNode+ to implement nested lists
661
+ def list(kind=@kind)
662
+ node = ListLevelNode.new(self, @template, kind, @level.level+1)
663
+ yield node
664
+ self.store(node)
665
+ end
666
+ end
667
+
668
+ # This class represents a list item, that can contain text or
669
+ # other nodes. Currently any type of node is accepted, but after
670
+ # more extensive testing this behaviour may change.
671
+ class ListTextNode < CommandNode
672
+ def initialize(parent, level)
673
+ @level = level
674
+ @parent = parent
675
+
676
+ number = siblings_count + 1 if parent.kind == :decimal
677
+ prefix = "{\\listtext#{@level.marker.text_format(number)}}"
678
+ suffix = '\\'
679
+
680
+ super(parent, prefix, suffix, false, false)
681
+ end
682
+
683
+ private
684
+ def siblings_count
685
+ parent.children.select {|n| n.kind_of?(self.class)}.size
686
+ end
687
+ end
688
+
689
+ class LinkNode < CommandNode
690
+ def initialize(parent, url)
691
+ prefix = "\\field{\\*\\fldinst HYPERLINK \"#{url}\"}{\\fldrslt "
692
+ suffix = "}"
693
+
694
+ super(parent, prefix, suffix, false)
695
+ end
696
+ end
697
+
698
+ # This class represents a table node within an RTF document. Table nodes are
699
+ # specialised container nodes that contain only TableRowNodes and have their
700
+ # size specified when they are created an cannot be resized after that.
701
+ class TableNode < ContainerNode
702
+ # Cell margin. Default to 100
703
+ attr_accessor :cell_margin
704
+
705
+ # This is a constructor for the TableNode class.
706
+ #
707
+ # ==== Parameters
708
+ # parent:: A reference to the node that owns the table.
709
+ # rows:: The number of rows in the tabkle.
710
+ # columns:: The number of columns in the table.
711
+ # *widths:: One or more integers specifying the widths of the table
712
+ # columns.
713
+ def initialize(parent, *args, &block)
714
+ if args.size>=2
715
+ rows=args.shift
716
+ columns=args.shift
717
+ widths=args
718
+ super(parent) do
719
+ entries = []
720
+ rows.times {entries.push(TableRowNode.new(self, columns, *widths))}
721
+ entries
722
+ end
723
+
724
+ elsif block
725
+ block.arity<1 ? self.instance_eval(&block) : block.call(self)
726
+ else
727
+ raise "You should use 0 or >2 args"
728
+ end
729
+ @cell_margin = 100
730
+ end
731
+
732
+ # Attribute accessor.
733
+ def rows
734
+ entries.size
735
+ end
736
+
737
+ # Attribute accessor.
738
+ def columns
739
+ entries[0].length
740
+ end
741
+
742
+ # This method assigns a border width setting to all of the sides on all
743
+ # of the cells within a table.
744
+ #
745
+ # ==== Parameters
746
+ # width:: The border width setting to apply. Negative values are ignored
747
+ # and zero switches the border off.
748
+ def border_width=(width)
749
+ self.each {|row| row.border_width = width}
750
+ end
751
+
752
+ # This method assigns a shading colour to a specified row within a
753
+ # TableNode object.
754
+ #
755
+ # ==== Parameters
756
+ # index:: The offset from the first row of the row to have shading
757
+ # applied to it.
758
+ # colour:: A reference to a Colour object representing the shading colour
759
+ # to be used. Set to nil to clear shading.
760
+ def row_shading_colour(index, colour)
761
+ row = self[index]
762
+ row.shading_colour = colour if row != nil
763
+ end
764
+
765
+ # This method assigns a shading colour to a specified column within a
766
+ # TableNode object.
767
+ #
768
+ # ==== Parameters
769
+ # index:: The offset from the first column of the column to have shading
770
+ # applied to it.
771
+ # colour:: A reference to a Colour object representing the shading colour
772
+ # to be used. Set to nil to clear shading.
773
+ def column_shading_colour(index, colour)
774
+ self.each do |row|
775
+ cell = row[index]
776
+ cell.shading_colour = colour if cell != nil
777
+ end
778
+ end
779
+
780
+ # This method provides a means of assigning a shading colour to a
781
+ # selection of cells within a table. The method accepts a block that
782
+ # takes three parameters - a TableCellNode representing a cell within the
783
+ # table, an integer representing the x offset of the cell and an integer
784
+ # representing the y offset of the cell. If the block returns true then
785
+ # shading will be applied to the cell.
786
+ #
787
+ # ==== Parameters
788
+ # colour:: A reference to a Colour object representing the shading colour
789
+ # to be applied. Set to nil to remove shading.
790
+ def shading_colour(colour)
791
+ if block_given?
792
+ 0.upto(self.size - 1) do |x|
793
+ row = self[x]
794
+ 0.upto(row.size - 1) do |y|
795
+ apply = yield row[y], x, y
796
+ row[y].shading_colour = colour if apply
797
+ end
798
+ end
799
+ end
800
+ end
801
+
802
+ # This method overloads the store method inherited from the ContainerNode
803
+ # class to forbid addition of further nodes.
804
+ #
805
+ # ==== Parameters
806
+ # node:: A reference to the node to be added.
807
+ def store(node)
808
+ RTFError.fire("Table nodes cannot have nodes added to.")
809
+ end
810
+
811
+ # This method generates the RTF document text for a TableCellNode object.
812
+ def to_rtf
813
+ text = StringIO.new
814
+ size = 0
815
+
816
+ self.each do |row|
817
+ if size > 0
818
+ text << "\n"
819
+ else
820
+ size = 1
821
+ end
822
+ text << row.to_rtf
823
+ end
824
+
825
+ text.string.sub(/\\row(?!.*\\row)/m, "\\lastrow\n\\row")
826
+ end
827
+
828
+ alias :column_shading_color :column_shading_colour
829
+ alias :row_shading_color :row_shading_colour
830
+ alias :shading_color :shading_colour
831
+ end # End of the TableNode class.
832
+
833
+
834
+ # This class represents a row within an RTF table. The TableRowNode is a
835
+ # specialised container node that can hold only TableCellNodes and, once
836
+ # created, cannot be resized. Its also not possible to change the parent
837
+ # of a TableRowNode object.
838
+ class TableRowNode < ContainerNode
839
+ # This is the constructor for the TableRowNode class.
840
+ #
841
+ # ===== Parameters
842
+ # table:: A reference to table that owns the row.
843
+ # cells:: The number of cells that the row will contain.
844
+ # widths:: One or more integers specifying the widths for the table
845
+ # columns
846
+ def initialize(table, cells, *widths)
847
+ super(table) do
848
+ entries = []
849
+ cells.times do |index|
850
+ entries.push(TableCellNode.new(self, widths[index]))
851
+ end
852
+ entries
853
+ end
854
+ end
855
+
856
+ # Attribute accessors
857
+ def length
858
+ entries.size
859
+ end
860
+
861
+ # This method assigns a border width setting to all of the sides on all
862
+ # of the cells within a table row.
863
+ #
864
+ # ==== Parameters
865
+ # width:: The border width setting to apply. Negative values are ignored
866
+ # and zero switches the border off.
867
+ def border_width=(width)
868
+ self.each {|cell| cell.border_width = width}
869
+ end
870
+
871
+ # This method overloads the parent= method inherited from the Node class
872
+ # to forbid the alteration of the cells parent.
873
+ #
874
+ # ==== Parameters
875
+ # parent:: A reference to the new node parent.
876
+ def parent=(parent)
877
+ RTFError.fire("Table row nodes cannot have their parent changed.")
878
+ end
879
+
880
+ # This method sets the shading colour for a row.
881
+ #
882
+ # ==== Parameters
883
+ # colour:: A reference to the Colour object that represents the new
884
+ # shading colour. Set to nil to switch shading off.
885
+ def shading_colour=(colour)
886
+ self.each {|cell| cell.shading_colour = colour}
887
+ end
888
+
889
+ # This method overloads the store method inherited from the ContainerNode
890
+ # class to forbid addition of further nodes.
891
+ #
892
+ # ==== Parameters
893
+ # node:: A reference to the node to be added.
894
+ #def store(node)
895
+ # RTFError.fire("Table row nodes cannot have nodes added to.")
896
+ #end
897
+
898
+ # This method generates the RTF document text for a TableCellNode object.
899
+ def to_rtf
900
+ text = StringIO.new
901
+ temp = StringIO.new
902
+ offset = 0
903
+
904
+ text << "\\trowd\\tgraph#{parent.cell_margin}"
905
+ self.each do |entry|
906
+ widths = entry.border_widths
907
+ colour = entry.shading_colour
908
+
909
+ text << "\n"
910
+ text << "\\clbrdrt\\brdrw#{widths[0]}\\brdrs" if widths[0] != 0
911
+ text << "\\clbrdrl\\brdrw#{widths[3]}\\brdrs" if widths[3] != 0
912
+ text << "\\clbrdrb\\brdrw#{widths[2]}\\brdrs" if widths[2] != 0
913
+ text << "\\clbrdrr\\brdrw#{widths[1]}\\brdrs" if widths[1] != 0
914
+ text << "\\clcbpat#{root.colours.index(colour)}" if colour != nil
915
+ text << "\\cellx#{entry.width + offset}"
916
+ temp << "\n#{entry.to_rtf}"
917
+ offset += entry.width
918
+ end
919
+ text << "#{temp.string}\n\\row"
920
+
921
+ text.string
922
+ end
923
+ end # End of the TableRowNode class.
924
+
925
+
926
+ # This class represents a cell within an RTF table. The TableCellNode is a
927
+ # specialised command node that is forbidden from creating tables or having
928
+ # its parent changed.
929
+ class TableCellNode < CommandNode
930
+ # A definition for the default width for the cell.
931
+ DEFAULT_WIDTH = 300
932
+ # Top border
933
+ TOP = 0
934
+ # Right border
935
+ RIGHT = 1
936
+ # Bottom border
937
+ BOTTOM = 2
938
+ # Left border
939
+ LEFT = 3
940
+ # Width of cell
941
+ attr_accessor :width
942
+ # Attribute accessor.
943
+ attr_reader :shading_colour, :style
944
+
945
+ # This is the constructor for the TableCellNode class.
946
+ #
947
+ # ==== Parameters
948
+ # row:: The row that the cell belongs to.
949
+ # width:: The width to be assigned to the cell. This defaults to
950
+ # TableCellNode::DEFAULT_WIDTH.
951
+ # style:: The style that is applied to the cell. This must be a
952
+ # ParagraphStyle class. Defaults to nil.
953
+ # top:: The border width for the cells top border. Defaults to nil.
954
+ # right:: The border width for the cells right hand border. Defaults to
955
+ # nil.
956
+ # bottom:: The border width for the cells bottom border. Defaults to nil.
957
+ # left:: The border width for the cells left hand border. Defaults to
958
+ # nil.
959
+ #
960
+ # ==== Exceptions
961
+ # RTFError:: Generated whenever an invalid style setting is specified.
962
+ def initialize(row, width=DEFAULT_WIDTH, style=nil, top=nil, right=nil,
963
+ bottom=nil, left=nil)
964
+ super(row, nil)
965
+ if !style.nil? and !style.is_paragraph_style?
966
+ RTFError.fire("Non-paragraph style specified for TableCellNode "\
967
+ "constructor.")
968
+ end
969
+
970
+ @width = (width != nil && width > 0) ? width : DEFAULT_WIDTH
971
+ @borders = [(top != nil && top > 0) ? top : nil,
972
+ (right != nil && right > 0) ? right : nil,
973
+ (bottom != nil && bottom > 0) ? bottom : nil,
974
+ (left != nil && left > 0) ? left : nil]
975
+ @shading_colour = nil
976
+ @style = style
977
+ end
978
+
979
+ # Attribute mutator.
980
+ #
981
+ # ==== Parameters
982
+ # style:: A reference to the style object to be applied to the cell.
983
+ # Must be an instance of the ParagraphStyle class. Set to nil
984
+ # to clear style settings.
985
+ #
986
+ # ==== Exceptions
987
+ # RTFError:: Generated whenever an invalid style setting is specified.
988
+ def style=(style)
989
+ if !style.nil? and !style.is_paragraph_style?
990
+ RTFError.fire("Non-paragraph style specified for TableCellNode "\
991
+ "constructor.")
992
+ end
993
+ @style = style
994
+ end
995
+
996
+ # This method assigns a width, in twips, for the borders on all sides of
997
+ # the cell. Negative widths will be ignored and a width of zero will
998
+ # switch the border off.
999
+ #
1000
+ # ==== Parameters
1001
+ # width:: The setting for the width of the border.
1002
+ def border_width=(width)
1003
+ size = width.nil? ? 0 : width
1004
+ if size > 0
1005
+ @borders[TOP] = @borders[RIGHT] = @borders[BOTTOM] = @borders[LEFT] = size.to_i
1006
+ else
1007
+ @borders = [nil, nil, nil, nil]
1008
+ end
1009
+ end
1010
+
1011
+ # This method assigns a border width to the top side of a table cell.
1012
+ # Negative values are ignored and a value of 0 switches the border off.
1013
+ #
1014
+ # ==== Parameters
1015
+ # width:: The new border width setting.
1016
+ def top_border_width=(width)
1017
+ size = width.nil? ? 0 : width
1018
+ if size > 0
1019
+ @borders[TOP] = size.to_i
1020
+ else
1021
+ @borders[TOP] = nil
1022
+ end
1023
+ end
1024
+
1025
+ # This method assigns a border width to the right side of a table cell.
1026
+ # Negative values are ignored and a value of 0 switches the border off.
1027
+ #
1028
+ # ==== Parameters
1029
+ # width:: The new border width setting.
1030
+ def right_border_width=(width)
1031
+ size = width.nil? ? 0 : width
1032
+ if size > 0
1033
+ @borders[RIGHT] = size.to_i
1034
+ else
1035
+ @borders[RIGHT] = nil
1036
+ end
1037
+ end
1038
+
1039
+ # This method assigns a border width to the bottom side of a table cell.
1040
+ # Negative values are ignored and a value of 0 switches the border off.
1041
+ #
1042
+ # ==== Parameters
1043
+ # width:: The new border width setting.
1044
+ def bottom_border_width=(width)
1045
+ size = width.nil? ? 0 : width
1046
+ if size > 0
1047
+ @borders[BOTTOM] = size.to_i
1048
+ else
1049
+ @borders[BOTTOM] = nil
1050
+ end
1051
+ end
1052
+
1053
+ # This method assigns a border width to the left side of a table cell.
1054
+ # Negative values are ignored and a value of 0 switches the border off.
1055
+ #
1056
+ # ==== Parameters
1057
+ # width:: The new border width setting.
1058
+ def left_border_width=(width)
1059
+ size = width.nil? ? 0 : width
1060
+ if size > 0
1061
+ @borders[LEFT] = size.to_i
1062
+ else
1063
+ @borders[LEFT] = nil
1064
+ end
1065
+ end
1066
+
1067
+ # This method alters the shading colour associated with a TableCellNode
1068
+ # object.
1069
+ #
1070
+ # ==== Parameters
1071
+ # colour:: A reference to the Colour object to use in shading the cell.
1072
+ # Assign nil to clear cell shading.
1073
+ def shading_colour=(colour)
1074
+ root.colours << colour
1075
+ @shading_colour = colour
1076
+ end
1077
+
1078
+ # This method retrieves an array with the cell border width settings.
1079
+ # The values are inserted in top, right, bottom, left order.
1080
+ def border_widths
1081
+ widths = []
1082
+ @borders.each {|entry| widths.push(entry.nil? ? 0 : entry)}
1083
+ widths
1084
+ end
1085
+
1086
+ # This method fetches the width for top border of a cell.
1087
+ def top_border_width
1088
+ @borders[TOP].nil? ? 0 : @borders[TOP]
1089
+ end
1090
+
1091
+ # This method fetches the width for right border of a cell.
1092
+ def right_border_width
1093
+ @borders[RIGHT].nil? ? 0 : @borders[RIGHT]
1094
+ end
1095
+
1096
+ # This method fetches the width for bottom border of a cell.
1097
+ def bottom_border_width
1098
+ @borders[BOTTOM].nil? ? 0 : @borders[BOTTOM]
1099
+ end
1100
+
1101
+ # This method fetches the width for left border of a cell.
1102
+ def left_border_width
1103
+ @borders[LEFT].nil? ? 0 : @borders[LEFT]
1104
+ end
1105
+
1106
+ # This method overloads the paragraph method inherited from the
1107
+ # ComamndNode class to forbid the creation of paragraphs.
1108
+ #
1109
+ # ==== Parameters
1110
+ # style:: The paragraph style, ignored
1111
+ def paragraph(style=nil)
1112
+ RTFError.fire("TableCellNode#paragraph() called. Table cells cannot "\
1113
+ "contain paragraphs.")
1114
+ end
1115
+
1116
+ # This method overloads the parent= method inherited from the Node class
1117
+ # to forbid the alteration of the cells parent.
1118
+ #
1119
+ # ==== Parameters
1120
+ # parent:: A reference to the new node parent.
1121
+ def parent=(parent)
1122
+ RTFError.fire("Table cell nodes cannot have their parent changed.")
1123
+ end
1124
+
1125
+ # This method overrides the table method inherited from CommandNode to
1126
+ # forbid its use in table cells.
1127
+ #
1128
+ # ==== Parameters
1129
+ # rows:: The number of rows for the table.
1130
+ # columns:: The number of columns for the table.
1131
+ # *widths:: One or more integers representing the widths for the table
1132
+ # columns.
1133
+ def table(rows, columns, *widths)
1134
+ RTFError.fire("TableCellNode#table() called. Nested tables not allowed.")
1135
+ end
1136
+
1137
+ # This method generates the RTF document text for a TableCellNode object.
1138
+ def to_rtf
1139
+ text = StringIO.new
1140
+ separator = split? ? "\n" : " "
1141
+ line = (separator == " ")
1142
+
1143
+ text << "\\pard\\intbl"
1144
+ text << @style.prefix(root) if @style != nil
1145
+ text << separator
1146
+ self.each do |entry|
1147
+ text << "\n" if line
1148
+ line = true
1149
+ text << entry.to_rtf
1150
+ end
1151
+ text << (split? ? "\n" : " ")
1152
+ text << "\\cell"
1153
+
1154
+ text.string
1155
+ end
1156
+ end # End of the TableCellNode class.
1157
+
1158
+
1159
+ # This class represents a document header.
1160
+ class HeaderNode < CommandNode
1161
+ # A definition for a header type.
1162
+ UNIVERSAL = :header
1163
+
1164
+ # A definition for a header type.
1165
+ LEFT_PAGE = :headerl
1166
+
1167
+ # A definition for a header type.
1168
+ RIGHT_PAGE = :headerr
1169
+
1170
+ # A definition for a header type.
1171
+ FIRST_PAGE = :headerf
1172
+
1173
+ # Attribute accessor.
1174
+ attr_reader :type
1175
+
1176
+ # Attribute mutator.
1177
+ attr_writer :type
1178
+
1179
+
1180
+ # This is the constructor for the HeaderNode class.
1181
+ #
1182
+ # ==== Parameters
1183
+ # document:: A reference to the Document object that will own the new
1184
+ # header.
1185
+ # type:: The style type for the new header. Defaults to a value of
1186
+ # HeaderNode::UNIVERSAL.
1187
+ def initialize(document, type=UNIVERSAL)
1188
+ super(document, "\\#{type.id2name}", nil, false)
1189
+ @type = type
1190
+ end
1191
+
1192
+ # This method overloads the footnote method inherited from the CommandNode
1193
+ # class to prevent footnotes being added to headers.
1194
+ #
1195
+ # ==== Parameters
1196
+ # text:: Not used.
1197
+ #
1198
+ # ==== Exceptions
1199
+ # RTFError:: Always generated whenever this method is called.
1200
+ def footnote(text)
1201
+ RTFError.fire("Footnotes are not permitted in page headers.")
1202
+ end
1203
+ end # End of the HeaderNode class.
1204
+
1205
+
1206
+ # This class represents a document footer.
1207
+ class FooterNode < CommandNode
1208
+ # A definition for a header type.
1209
+ UNIVERSAL = :footer
1210
+
1211
+ # A definition for a header type.
1212
+ LEFT_PAGE = :footerl
1213
+
1214
+ # A definition for a header type.
1215
+ RIGHT_PAGE = :footerr
1216
+
1217
+ # A definition for a header type.
1218
+ FIRST_PAGE = :footerf
1219
+
1220
+ # Attribute accessor.
1221
+ attr_reader :type
1222
+
1223
+ # Attribute mutator.
1224
+ attr_writer :type
1225
+
1226
+
1227
+ # This is the constructor for the FooterNode class.
1228
+ #
1229
+ # ==== Parameters
1230
+ # document:: A reference to the Document object that will own the new
1231
+ # footer.
1232
+ # type:: The style type for the new footer. Defaults to a value of
1233
+ # FooterNode::UNIVERSAL.
1234
+ def initialize(document, type=UNIVERSAL)
1235
+ super(document, "\\#{type.id2name}", nil, false)
1236
+ @type = type
1237
+ end
1238
+
1239
+ # This method overloads the footnote method inherited from the CommandNode
1240
+ # class to prevent footnotes being added to footers.
1241
+ #
1242
+ # ==== Parameters
1243
+ # text:: Not used.
1244
+ #
1245
+ # ==== Exceptions
1246
+ # RTFError:: Always generated whenever this method is called.
1247
+ def footnote(text)
1248
+ RTFError.fire("Footnotes are not permitted in page footers.")
1249
+ end
1250
+ end # End of the FooterNode class.
1251
+
1252
+
1253
+ # This class represents an image within a RTF document. Currently only the
1254
+ # PNG, JPEG and Windows Bitmap formats are supported. Efforts are made to
1255
+ # identify the file type but these are not guaranteed to work.
1256
+ class ImageNode < Node
1257
+ # A definition for an image type constant.
1258
+ PNG = :pngblip
1259
+
1260
+ # A definition for an image type constant.
1261
+ JPEG = :jpegblip
1262
+
1263
+ # A definition for an image type constant.
1264
+ BITMAP = :dibitmap0
1265
+
1266
+ # A definition for an architecture endian constant.
1267
+ LITTLE_ENDIAN = :little
1268
+
1269
+ # A definition for an architecture endian constant.
1270
+ BIG_ENDIAN = :big
1271
+
1272
+ # Offsets for reading dimension data by filetype
1273
+ DIMENSIONS_OFFSET = {
1274
+ JPEG => 2,
1275
+ PNG => 8,
1276
+ BITMAP => 8,
1277
+ }.freeze
1278
+
1279
+ # Attribute accessor.
1280
+ attr_reader :x_scaling, :y_scaling, :top_crop, :right_crop, :bottom_crop,
1281
+ :left_crop, :width, :height, :displayed_width, :displayed_height
1282
+
1283
+ # Attribute mutator.
1284
+ attr_writer :x_scaling, :y_scaling, :top_crop, :right_crop, :bottom_crop,
1285
+ :left_crop, :displayed_width, :displayed_height
1286
+
1287
+
1288
+ # This is the constructor for the ImageNode class.
1289
+ #
1290
+ # ==== Parameters
1291
+ # parent:: A reference to the node that owns the new image node.
1292
+ # source:: A reference to the image source. This must be a String or a
1293
+ # File.
1294
+ # id:: The unique identifier for the image node.
1295
+ #
1296
+ # ==== Exceptions
1297
+ # RTFError:: Generated whenever the image specified is not recognised as
1298
+ # a supported image type, something other than a String or
1299
+ # File or IO is passed as the source parameter or if the
1300
+ # specified source does not exist or cannot be accessed.
1301
+ def initialize(parent, source, id)
1302
+ super(parent)
1303
+ @source = nil
1304
+ @id = id
1305
+ @type = nil
1306
+ @x_scaling = @y_scaling = nil
1307
+ @top_crop = @right_crop = @bottom_crop = @left_crop = nil
1308
+ @width = @height = nil
1309
+ @displayed_width = @displayed_height = nil
1310
+
1311
+ # store path to image
1312
+ @source = source if source.instance_of?(String) || source.instance_of?(Tempfile)
1313
+ @source = source.path if source.instance_of?(File)
1314
+
1315
+ # Check the file's existence and accessibility.
1316
+ if !File.exist?(@source)
1317
+ RTFError.fire("Unable to find the #{File.basename(@source)} file.")
1318
+ end
1319
+ if !File.readable?(@source)
1320
+ RTFError.fire("Access to the #{File.basename(@source)} file denied.")
1321
+ end
1322
+
1323
+ @type = get_file_type
1324
+ if @type == nil
1325
+ RTFError.fire("The #{File.basename(@source)} file contains an "\
1326
+ "unknown or unsupported image type.")
1327
+ end
1328
+
1329
+ @width, @height = get_dimensions
1330
+ end
1331
+
1332
+ def open_file(&block)
1333
+ if block
1334
+ File.open(@source, 'rb', &block)
1335
+ else
1336
+ File.open(@source, 'rb')
1337
+ end
1338
+ end
1339
+
1340
+ # This method attempts to determine the image type associated with a
1341
+ # file, returning nil if it fails to make the determination.
1342
+ def get_file_type
1343
+ type = nil
1344
+ read = []
1345
+ open_file do |file|
1346
+
1347
+ # Check if the file is a JPEG.
1348
+ read_source(file, read, 2)
1349
+ if read[0,2] == [255, 216]
1350
+ type = JPEG
1351
+ else
1352
+ # Check if it's a PNG.
1353
+ read_source(file, read, 6)
1354
+ if read[0,8] == [137, 80, 78, 71, 13, 10, 26, 10]
1355
+ type = PNG
1356
+ else
1357
+ # Check if its a bitmap.
1358
+ if read[0,2] == [66, 77]
1359
+ size = to_integer(read[2,4])
1360
+ type = BITMAP if size == File.size(@source)
1361
+ end
1362
+ end
1363
+ end
1364
+
1365
+ end
1366
+
1367
+ type
1368
+ end
1369
+
1370
+ # This method generates the RTF for an ImageNode object.
1371
+ def to_rtf
1372
+ text = StringIO.new
1373
+ count = 0
1374
+
1375
+ #text << '{\pard{\*\shppict{\pict'
1376
+ text << '{\*\shppict{\pict'
1377
+ text << "\\picscalex#{@x_scaling}" if @x_scaling != nil
1378
+ text << "\\picscaley#{@y_scaling}" if @y_scaling != nil
1379
+ text << "\\piccropl#{@left_crop}" if @left_crop != nil
1380
+ text << "\\piccropr#{@right_crop}" if @right_crop != nil
1381
+ text << "\\piccropt#{@top_crop}" if @top_crop != nil
1382
+ text << "\\piccropb#{@bottom_crop}" if @bottom_crop != nil
1383
+ text << "\\picwgoal#{@displayed_width}" if @displayed_width != nil
1384
+ text << "\\pichgoal#{@displayed_height}" if @displayed_height != nil
1385
+ text << "\\picw#{@width}\\pich#{@height}\\bliptag#{@id}"
1386
+ text << "\\#{@type.id2name}\n"
1387
+
1388
+ open_file do |file|
1389
+ file.each_byte do |byte|
1390
+ hex_str = byte.to_s(16)
1391
+ hex_str.insert(0,'0') if hex_str.length == 1
1392
+ text << hex_str
1393
+ count += 1
1394
+ if count == 40
1395
+ text << "\n"
1396
+ count = 0
1397
+ end
1398
+ end
1399
+ end
1400
+ #text << "\n}}\\par}"
1401
+ text << "\n}}"
1402
+
1403
+ text.string
1404
+ end
1405
+
1406
+ # This method is used to determine the underlying endianness of a
1407
+ # platform.
1408
+ def get_endian
1409
+ [0, 125].pack('c2').unpack('s') == [125] ? BIG_ENDIAN : LITTLE_ENDIAN
1410
+ end
1411
+
1412
+ # This method converts an array to an integer. The array must be either
1413
+ # two or four bytes in length.
1414
+ #
1415
+ # ==== Parameters
1416
+ # array:: A reference to the array containing the data to be converted.
1417
+ # signed:: A boolean to indicate whether the value is signed. Defaults
1418
+ # to false.
1419
+ def to_integer(array, signed=false)
1420
+ from = nil
1421
+ to = nil
1422
+ data = []
1423
+
1424
+ if array.size == 2
1425
+ data.concat(get_endian == BIG_ENDIAN ? array.reverse : array)
1426
+ from = 'C2'
1427
+ to = signed ? 's' : 'S'
1428
+ else
1429
+ data.concat(get_endian == BIG_ENDIAN ? array[0,4].reverse : array)
1430
+ from = 'C4'
1431
+ to = signed ? 'l' : 'L'
1432
+ end
1433
+ data.pack(from).unpack(to)[0]
1434
+ end
1435
+
1436
+ # This method loads the data for an image from its source. The method
1437
+ # accepts two call approaches. If called without a block then the method
1438
+ # considers the size parameter it is passed. If called with a block the
1439
+ # method executes until the block returns true.
1440
+ #
1441
+ # ==== Parameters
1442
+ # size:: The maximum number of bytes to be read from the file. Defaults
1443
+ # to nil to indicate that the remainder of the file should be read
1444
+ # in.
1445
+ def read_source(file, read, size=nil)
1446
+ if block_given?
1447
+ done = false
1448
+
1449
+ while !done and !file.eof?
1450
+ read << file.getbyte
1451
+ done = yield read[-1]
1452
+ end
1453
+ else
1454
+ if size != nil
1455
+ if size > 0
1456
+ total = 0
1457
+ while !file.eof? and total < size
1458
+ read << file.getbyte
1459
+ total += 1
1460
+ end
1461
+ end
1462
+ else
1463
+ file.each_byte {|byte| read << byte}
1464
+ end
1465
+ end
1466
+ end
1467
+
1468
+
1469
+ # This method fetches details of the dimensions associated with an image.
1470
+ def get_dimensions
1471
+ dimensions = nil
1472
+
1473
+ open_file do |file|
1474
+ file.pos = DIMENSIONS_OFFSET[@type]
1475
+ read = []
1476
+
1477
+ # Check the image type.
1478
+ if @type == JPEG
1479
+ # Read until we can't anymore or we've found what we're looking for.
1480
+ done = false
1481
+ while !file.eof? and !done
1482
+ # Read to the next marker.
1483
+ read_source(file,read) {|c| c == 0xff} # Read to the marker.
1484
+ read_source(file,read) {|c| c != 0xff} # Skip any padding.
1485
+
1486
+ if read[-1] >= 0xc0 && read[-1] <= 0xc3
1487
+ # Read in the width and height details.
1488
+ read_source(file, read, 7)
1489
+ dimensions = read[-4,4].pack('C4').unpack('nn').reverse
1490
+ done = true
1491
+ else
1492
+ # Skip the marker block.
1493
+ read_source(file, read, 2)
1494
+ read_source(file, read, read[-2,2].pack('C2').unpack('n')[0] - 2)
1495
+ end
1496
+ end
1497
+ elsif @type == PNG
1498
+ # Read in the data to contain the width and height.
1499
+ read_source(file, read, 16)
1500
+ dimensions = read[-8,8].pack('C8').unpack('N2')
1501
+ elsif @type == BITMAP
1502
+ # Read in the data to contain the width and height.
1503
+ read_source(file, read, 18)
1504
+ dimensions = [to_integer(read[-8,4]), to_integer(read[-4,4])]
1505
+ end
1506
+ end
1507
+
1508
+ dimensions
1509
+ end
1510
+
1511
+ private :get_file_type, :to_integer, :get_endian, :get_dimensions, :open_file
1512
+ end # End of the ImageNode class.
1513
+
1514
+
1515
+ # This class represents an RTF document. In actuality it is just a
1516
+ # specialised Node type that cannot be assigned a parent and that holds
1517
+ # document font, colour and information tables.
1518
+ class Document < CommandNode
1519
+ # A definition for a document character set setting.
1520
+ CS_ANSI = :ansi
1521
+
1522
+ # A definition for a document character set setting.
1523
+ CS_MAC = :mac
1524
+
1525
+ # A definition for a document character set setting.
1526
+ CS_PC = :pc
1527
+
1528
+ # A definition for a document character set setting.
1529
+ CS_PCA = :pca
1530
+
1531
+ # A definition for a document language setting.
1532
+ LC_AFRIKAANS = 1078
1533
+
1534
+ # A definition for a document language setting.
1535
+ LC_ARABIC = 1025
1536
+
1537
+ # A definition for a document language setting.
1538
+ LC_CATALAN = 1027
1539
+
1540
+ # A definition for a document language setting.
1541
+ LC_CHINESE_TRADITIONAL = 1028
1542
+
1543
+ # A definition for a document language setting.
1544
+ LC_CHINESE_SIMPLIFIED = 2052
1545
+
1546
+ # A definition for a document language setting.
1547
+ LC_CZECH = 1029
1548
+
1549
+ # A definition for a document language setting.
1550
+ LC_DANISH = 1030
1551
+
1552
+ # A definition for a document language setting.
1553
+ LC_DUTCH = 1043
1554
+
1555
+ # A definition for a document language setting.
1556
+ LC_DUTCH_BELGIAN = 2067
1557
+
1558
+ # A definition for a document language setting.
1559
+ LC_ENGLISH_UK = 2057
1560
+
1561
+ # A definition for a document language setting.
1562
+ LC_ENGLISH_US = 1033
1563
+
1564
+ # A definition for a document language setting.
1565
+ LC_FINNISH = 1035
1566
+
1567
+ # A definition for a document language setting.
1568
+ LC_FRENCH = 1036
1569
+
1570
+ # A definition for a document language setting.
1571
+ LC_FRENCH_BELGIAN = 2060
1572
+
1573
+ # A definition for a document language setting.
1574
+ LC_FRENCH_CANADIAN = 3084
1575
+
1576
+ # A definition for a document language setting.
1577
+ LC_FRENCH_SWISS = 4108
1578
+
1579
+ # A definition for a document language setting.
1580
+ LC_GERMAN = 1031
1581
+
1582
+ # A definition for a document language setting.
1583
+ LC_GERMAN_SWISS = 2055
1584
+
1585
+ # A definition for a document language setting.
1586
+ LC_GREEK = 1032
1587
+
1588
+ # A definition for a document language setting.
1589
+ LC_HEBREW = 1037
1590
+
1591
+ # A definition for a document language setting.
1592
+ LC_HUNGARIAN = 1038
1593
+
1594
+ # A definition for a document language setting.
1595
+ LC_ICELANDIC = 1039
1596
+
1597
+ # A definition for a document language setting.
1598
+ LC_INDONESIAN = 1057
1599
+
1600
+ # A definition for a document language setting.
1601
+ LC_ITALIAN = 1040
1602
+
1603
+ # A definition for a document language setting.
1604
+ LC_JAPANESE = 1041
1605
+
1606
+ # A definition for a document language setting.
1607
+ LC_KOREAN = 1042
1608
+
1609
+ # A definition for a document language setting.
1610
+ LC_NORWEGIAN_BOKMAL = 1044
1611
+
1612
+ # A definition for a document language setting.
1613
+ LC_NORWEGIAN_NYNORSK = 2068
1614
+
1615
+ # A definition for a document language setting.
1616
+ LC_POLISH = 1045
1617
+
1618
+ # A definition for a document language setting.
1619
+ LC_PORTUGUESE = 2070
1620
+
1621
+ # A definition for a document language setting.
1622
+ LC_POTUGUESE_BRAZILIAN = 1046
1623
+
1624
+ # A definition for a document language setting.
1625
+ LC_ROMANIAN = 1048
1626
+
1627
+ # A definition for a document language setting.
1628
+ LC_RUSSIAN = 1049
1629
+
1630
+ # A definition for a document language setting.
1631
+ LC_SERBO_CROATIAN_CYRILLIC = 2074
1632
+
1633
+ # A definition for a document language setting.
1634
+ LC_SERBO_CROATIAN_LATIN = 1050
1635
+
1636
+ # A definition for a document language setting.
1637
+ LC_SLOVAK = 1051
1638
+
1639
+ # A definition for a document language setting.
1640
+ LC_SPANISH_CASTILLIAN = 1034
1641
+
1642
+ # A definition for a document language setting.
1643
+ LC_SPANISH_MEXICAN = 2058
1644
+
1645
+ # A definition for a document language setting.
1646
+ LC_SWAHILI = 1089
1647
+
1648
+ # A definition for a document language setting.
1649
+ LC_SWEDISH = 1053
1650
+
1651
+ # A definition for a document language setting.
1652
+ LC_THAI = 1054
1653
+
1654
+ # A definition for a document language setting.
1655
+ LC_TURKISH = 1055
1656
+
1657
+ # A definition for a document language setting.
1658
+ LC_UNKNOWN = 1024
1659
+
1660
+ # A definition for a document language setting.
1661
+ LC_VIETNAMESE = 1066
1662
+
1663
+ # Attribute accessor.
1664
+ attr_reader :fonts, :lists, :colours, :information, :character_set,
1665
+ :language, :style, :stylesheet
1666
+
1667
+ # Attribute mutator.
1668
+ attr_writer :character_set, :language, :stylesheet
1669
+
1670
+
1671
+ # This is a constructor for the Document class.
1672
+ #
1673
+ # ==== Parameters
1674
+ # options:: A hashmap of options to use in initializing the document
1675
+ # default_font:: A font object OR string encapsulating the default
1676
+ # font to be used by the document.
1677
+ # STRING FORMAT "<FAMILY>:<Name>"
1678
+ # DEFAULT "SWISS:Helvetica".
1679
+ # document_style:: A DocumentStyle object OR options hash encapsulating
1680
+ # the style settings to be applied to the document.
1681
+ # DEFAULT DocumentStyle object with default settings.
1682
+ # character_set:: The character set to be applied to the document.
1683
+ # DEFAULT Document::CS_ANSI.
1684
+ # language:: The language setting to be applied to document.
1685
+ # DEFAULT Document::LC_ENGLISH_UK.
1686
+ # suppress_system_styles:: A Boolean indicating whether or not to add
1687
+ # the \noqfpromote control word, which indicates
1688
+ # "quick" or default styles should be suppressed.
1689
+ # DEFAULT false.
1690
+ # stylesheet:: A Stylesheet object OR hashmap array encapsulating
1691
+ # the styles to make available throughout the document.
1692
+ # DEFAULT nil.
1693
+ def initialize(options = {})
1694
+ # load default options
1695
+ options = {
1696
+ "default_font" => "SWISS:Helvetica",
1697
+ "document_style" => DocumentStyle.new,
1698
+ "character_set" => CS_ANSI,
1699
+ "language" => LC_ENGLISH_US,
1700
+ "suppress_system_styles" => false,
1701
+ "stylesheet" => nil
1702
+ }.merge(options)
1703
+
1704
+ super(nil, '\rtf1')
1705
+
1706
+ # parse font
1707
+ font = options.delete("default_font")
1708
+ case font
1709
+ when Font
1710
+ when String
1711
+ font = Font.from_string(font)
1712
+ else
1713
+ RTFError.fire("Unreconized font format #{font.class.to_s}")
1714
+ end # case
1715
+
1716
+ # parse document style
1717
+ style = options.delete("document_style")
1718
+ case style
1719
+ when DocumentStyle
1720
+ when Hash
1721
+ style = DocumentStyle.new(style)
1722
+ else
1723
+ RTFError.fire("Unreconized document style format #{font.class.to_s}")
1724
+ end # case
1725
+
1726
+ @fonts = FontTable.new(font)
1727
+ @lists = ListTable.new
1728
+ @default_font = 0
1729
+ @colours = ColourTable.new
1730
+ @information = Information.new
1731
+ @character_set = options.delete("character_set")
1732
+ @language = options.delete("language")
1733
+ @style = style
1734
+ @headers = [nil, nil, nil, nil]
1735
+ @footers = [nil, nil, nil, nil]
1736
+ @id = 0
1737
+
1738
+ # parse stylesheet (must be done after font and colour tables are
1739
+ # initialized since declared styles may push fonts/colours onto the
1740
+ # tables)
1741
+ stylesheet = options.delete("stylesheet")
1742
+ case stylesheet
1743
+ when Stylesheet
1744
+ stylesheet.document = self
1745
+ when Array
1746
+ stylesheet = Stylesheet.new(self, "styles" => stylesheet)
1747
+ when Hash
1748
+ stylesheet = Stylesheet.new(self, stylesheet)
1749
+ else
1750
+ RTFError.fire("Unreconized stylesheet format #{font.class.to_s}")
1751
+ end unless stylesheet.nil? # case
1752
+
1753
+ @stylesheet = stylesheet
1754
+ # additional options
1755
+ @options = options
1756
+ end
1757
+
1758
+ # This method provides a method that can be called to generate an
1759
+ # identifier that is unique within the document.
1760
+ def get_id
1761
+ @id += 1
1762
+ Time.now().strftime('%d%m%y') + @id.to_s
1763
+ end
1764
+
1765
+ # Attribute accessor.
1766
+ def default_font
1767
+ @fonts[@default_font]
1768
+ end
1769
+
1770
+ # This method assigns a new header to a document. A Document object can
1771
+ # have up to four header - a default header, a header for left pages, a
1772
+ # header for right pages and a header for the first page. The method
1773
+ # checks the header type and stores it appropriately.
1774
+ #
1775
+ # ==== Parameters
1776
+ # header:: A reference to the header object to be stored. Existing header
1777
+ # objects are overwritten.
1778
+ def header=(header)
1779
+ if header.type == HeaderNode::UNIVERSAL
1780
+ @headers[0] = header
1781
+ elsif header.type == HeaderNode::LEFT_PAGE
1782
+ @headers[1] = header
1783
+ elsif header.type == HeaderNode::RIGHT_PAGE
1784
+ @headers[2] = header
1785
+ elsif header.type == HeaderNode::FIRST_PAGE
1786
+ @headers[3] = header
1787
+ end
1788
+ end
1789
+
1790
+ # This method assigns a new footer to a document. A Document object can
1791
+ # have up to four footers - a default footer, a footer for left pages, a
1792
+ # footer for right pages and a footer for the first page. The method
1793
+ # checks the footer type and stores it appropriately.
1794
+ #
1795
+ # ==== Parameters
1796
+ # footer:: A reference to the footer object to be stored. Existing footer
1797
+ # objects are overwritten.
1798
+ def footer=(footer)
1799
+ if footer.type == FooterNode::UNIVERSAL
1800
+ @footers[0] = footer
1801
+ elsif footer.type == FooterNode::LEFT_PAGE
1802
+ @footers[1] = footer
1803
+ elsif footer.type == FooterNode::RIGHT_PAGE
1804
+ @footers[2] = footer
1805
+ elsif footer.type == FooterNode::FIRST_PAGE
1806
+ @footers[3] = footer
1807
+ end
1808
+ end
1809
+
1810
+ # This method fetches a header from a Document object.
1811
+ #
1812
+ # ==== Parameters
1813
+ # type:: One of the header types defined in the header class. Defaults to
1814
+ # HeaderNode::UNIVERSAL.
1815
+ def header(type=HeaderNode::UNIVERSAL)
1816
+ index = 0
1817
+ if type == HeaderNode::LEFT_PAGE
1818
+ index = 1
1819
+ elsif type == HeaderNode::RIGHT_PAGE
1820
+ index = 2
1821
+ elsif type == HeaderNode::FIRST_PAGE
1822
+ index = 3
1823
+ end
1824
+ @headers[index]
1825
+ end
1826
+
1827
+ # This method fetches a footer from a Document object.
1828
+ #
1829
+ # ==== Parameters
1830
+ # type:: One of the footer types defined in the footer class. Defaults to
1831
+ # FooterNode::UNIVERSAL.
1832
+ def footer(type=FooterNode::UNIVERSAL)
1833
+ index = 0
1834
+ if type == FooterNode::LEFT_PAGE
1835
+ index = 1
1836
+ elsif type == FooterNode::RIGHT_PAGE
1837
+ index = 2
1838
+ elsif type == FooterNode::FIRST_PAGE
1839
+ index = 3
1840
+ end
1841
+ @footers[index]
1842
+ end
1843
+
1844
+ # Loads a stylesheet for the document from an array of hashmaps
1845
+ # representing styles
1846
+ def load_stylesheet(hashmap_array)
1847
+ @stylesheet = Stylesheet.new(self, hashmap_array)
1848
+ end
1849
+
1850
+ # Attribute mutator.
1851
+ #
1852
+ # ==== Parameters
1853
+ # font:: The new default font for the Document object.
1854
+ def default_font=(font)
1855
+ @fonts << font
1856
+ @default_font = @fonts.index(font)
1857
+ end
1858
+
1859
+ # This method provides a short cut for obtaining the Paper object
1860
+ # associated with a Document object.
1861
+ def paper
1862
+ @style.paper
1863
+ end
1864
+
1865
+ # This method overrides the parent=() method inherited from the
1866
+ # CommandNode class to disallow setting a parent on a Document object.
1867
+ #
1868
+ # ==== Parameters
1869
+ # parent:: A reference to the new parent node for the Document object.
1870
+ #
1871
+ # ==== Exceptions
1872
+ # RTFError:: Generated whenever this method is called.
1873
+ def parent=(parent)
1874
+ RTFError.fire("Document objects may not have a parent.")
1875
+ end
1876
+
1877
+ # This method inserts a page break into a document.
1878
+ def page_break
1879
+ self.store(CommandNode.new(self, '\page', nil, false))
1880
+ nil
1881
+ end
1882
+
1883
+ # This method fetches the width of the available work area space for a
1884
+ # typical Document object page.
1885
+ def body_width
1886
+ @style.body_width
1887
+ end
1888
+
1889
+ # This method fetches the height of the available work area space for a
1890
+ # a typical Document object page.
1891
+ def body_height
1892
+ @style.body_height
1893
+ end
1894
+
1895
+ # This method generates the RTF text for a Document object.
1896
+ def to_rtf
1897
+ text = StringIO.new
1898
+
1899
+ text << "{#{prefix}\\#{@character_set.id2name}"
1900
+ text << "\\deff#{@default_font}"
1901
+ text << "\\deflang#{@language}" if !@language.nil?
1902
+ text << "\\plain\\fs24\\fet1"
1903
+ text << "\n#{@fonts.to_rtf}"
1904
+ text << "\n#{@colours.to_rtf}" if @colours.size > 0
1905
+ text << "\n\\noqfpromote" if @options["suppress_system_styles"]
1906
+ text << "\n#{@stylesheet.to_rtf}" if !@stylesheet.nil?
1907
+ text << "\n#{@information.to_rtf}"
1908
+ text << "\n#{@lists.to_rtf}"
1909
+ if @headers.compact != []
1910
+ text << "\n#{@headers[3].to_rtf}" if !@headers[3].nil?
1911
+ text << "\n#{@headers[2].to_rtf}" if !@headers[2].nil?
1912
+ text << "\n#{@headers[1].to_rtf}" if !@headers[1].nil?
1913
+ if @headers[1].nil? or @headers[2].nil?
1914
+ text << "\n#{@headers[0].to_rtf}"
1915
+ end
1916
+ end
1917
+ if @footers.compact != []
1918
+ text << "\n#{@footers[3].to_rtf}" if !@footers[3].nil?
1919
+ text << "\n#{@footers[2].to_rtf}" if !@footers[2].nil?
1920
+ text << "\n#{@footers[1].to_rtf}" if !@footers[1].nil?
1921
+ if @footers[1].nil? or @footers[2].nil?
1922
+ text << "\n#{@footers[0].to_rtf}"
1923
+ end
1924
+ end
1925
+ text << "\n#{@style.prefix(self)}" if !@style.nil?
1926
+ self.each {|entry| text << "\n#{entry.to_rtf}"}
1927
+ text << "\n}"
1928
+
1929
+ text.string
1930
+ end
1931
+ end # End of the Document class.
1932
+ end # End of the RTF module.