rrtf 0.1.2 → 1.0.0

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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/.byebug_history +6 -3
  3. data/CHANGELOG.md +24 -0
  4. data/README.md +194 -84
  5. data/documentation/RRTF.html +5 -5
  6. data/documentation/RRTF/AnonymousStyle.html +792 -0
  7. data/documentation/RRTF/BorderFormatting.html +821 -0
  8. data/documentation/RRTF/BorderStyle.html +493 -0
  9. data/documentation/RRTF/CharacterFormatting.html +293 -162
  10. data/documentation/RRTF/CharacterStyle.html +53 -109
  11. data/documentation/RRTF/Colour.html +61 -1
  12. data/documentation/RRTF/ColourTable.html +52 -52
  13. data/documentation/RRTF/CommandNode.html +367 -971
  14. data/documentation/RRTF/ContainerNode.html +44 -44
  15. data/documentation/RRTF/Converters.html +1 -1
  16. data/documentation/RRTF/Converters/HTML.html +1 -1
  17. data/documentation/RRTF/Converters/HTML/Helpers.html +1 -1
  18. data/documentation/RRTF/Converters/HTML/Node.html +1 -1
  19. data/documentation/RRTF/Converters/HTML/NodeSet.html +1 -1
  20. data/documentation/RRTF/Document.html +267 -255
  21. data/documentation/RRTF/DocumentFormatting.html +833 -0
  22. data/documentation/RRTF/DocumentProperties.html +444 -0
  23. data/documentation/RRTF/Font.html +1 -1
  24. data/documentation/RRTF/FontTable.html +1 -1
  25. data/documentation/RRTF/FooterNode.html +16 -16
  26. data/documentation/RRTF/GeometryNode.html +774 -0
  27. data/documentation/RRTF/GeometryProperties.html +1014 -0
  28. data/documentation/RRTF/HeaderNode.html +16 -16
  29. data/documentation/RRTF/ImageNode.html +705 -492
  30. data/documentation/RRTF/Information.html +1 -1
  31. data/documentation/RRTF/LinkNode.html +10 -10
  32. data/documentation/RRTF/ListLevel.html +1 -1
  33. data/documentation/RRTF/ListLevelNode.html +37 -37
  34. data/documentation/RRTF/ListMarker.html +1 -1
  35. data/documentation/RRTF/ListNode.html +19 -19
  36. data/documentation/RRTF/ListTable.html +1 -1
  37. data/documentation/RRTF/ListTemplate.html +1 -1
  38. data/documentation/RRTF/ListTextNode.html +14 -14
  39. data/documentation/RRTF/Node.html +26 -26
  40. data/documentation/RRTF/Page.html +129 -0
  41. data/documentation/RRTF/Page/Margin.html +1158 -0
  42. data/documentation/RRTF/Page/Size.html +946 -0
  43. data/documentation/RRTF/PageFormatting.html +954 -0
  44. data/documentation/RRTF/ParagraphFormatting.html +338 -56
  45. data/documentation/RRTF/ParagraphNode.html +10 -10
  46. data/documentation/RRTF/ParagraphStyle.html +72 -111
  47. data/documentation/RRTF/PositionFormatting.html +780 -0
  48. data/documentation/RRTF/PositionStyle.html +424 -0
  49. data/documentation/RRTF/Properties.html +243 -0
  50. data/documentation/RRTF/RTFError.html +21 -10
  51. data/documentation/RRTF/ShadingFormatting.html +712 -0
  52. data/documentation/RRTF/ShadingStyle.html +424 -0
  53. data/documentation/RRTF/Style.html +284 -697
  54. data/documentation/RRTF/Stylesheet.html +36 -3
  55. data/documentation/RRTF/TableCellNode.html +131 -131
  56. data/documentation/RRTF/TableNode.html +82 -82
  57. data/documentation/RRTF/TableRowNode.html +53 -53
  58. data/documentation/RRTF/TextNode.html +46 -46
  59. data/documentation/RRTF/Utilities.html +837 -17
  60. data/documentation/_index.html +139 -6
  61. data/documentation/class_list.html +1 -1
  62. data/documentation/file.README.html +218 -87
  63. data/documentation/index.html +218 -87
  64. data/documentation/method_list.html +631 -391
  65. data/documentation/top-level-namespace.html +1 -1
  66. data/examples/01.rtf +947 -20
  67. data/examples/01_everything.rb +176 -0
  68. data/examples/02.rtf +13 -0
  69. data/examples/02_basic_paragraph.rb +10 -0
  70. data/examples/03.rtf +20 -0
  71. data/examples/03_paragraph_inline_style.rb +14 -0
  72. data/examples/04.rtf +21 -0
  73. data/examples/04_paragraph_with_character_style.rb +18 -0
  74. data/examples/05.rtf +21 -0
  75. data/examples/05_hyperlinks.rb +21 -0
  76. data/examples/06.rtf +21 -0
  77. data/examples/06_basic_list.rb +21 -0
  78. data/examples/07.rtf +28 -0
  79. data/examples/07_nested_list.rb +27 -0
  80. data/examples/08.rtf +807 -0
  81. data/examples/08_images.rb +17 -0
  82. data/examples/09.rtf +84 -0
  83. data/examples/09_shapes.rb +56 -0
  84. data/examples/10.rtf +34 -0
  85. data/examples/10_stylesheet.rb +18 -0
  86. data/examples/resources/images/redshirt.png +0 -0
  87. data/examples/resources/images/redshirts.jpg +0 -0
  88. data/examples/resources/json/redshirt_styles.json +72 -8
  89. data/examples/~$01.rtf +0 -0
  90. data/lib/rrtf.rb +4 -16
  91. data/lib/rrtf/colour.rb +8 -0
  92. data/lib/rrtf/formatting.rb +988 -0
  93. data/lib/rrtf/node.rb +17 -1851
  94. data/lib/rrtf/node/command_node.rb +242 -0
  95. data/lib/rrtf/node/container_node.rb +75 -0
  96. data/lib/rrtf/node/document.rb +339 -0
  97. data/lib/rrtf/node/footer_node.rb +47 -0
  98. data/lib/rrtf/node/geometry_node.rb +65 -0
  99. data/lib/rrtf/node/header_node.rb +47 -0
  100. data/lib/rrtf/node/image_node.rb +175 -0
  101. data/lib/rrtf/node/link_node.rb +10 -0
  102. data/lib/rrtf/node/list_level_node.rb +44 -0
  103. data/lib/rrtf/node/list_node.rb +30 -0
  104. data/lib/rrtf/node/list_text_node.rb +22 -0
  105. data/lib/rrtf/node/node.rb +53 -0
  106. data/lib/rrtf/node/paragraph_node.rb +11 -0
  107. data/lib/rrtf/node/table_cell_node.rb +233 -0
  108. data/lib/rrtf/node/table_node.rb +136 -0
  109. data/lib/rrtf/node/table_row_node.rb +92 -0
  110. data/lib/rrtf/node/text_node.rb +76 -0
  111. data/lib/rrtf/page.rb +7 -0
  112. data/lib/rrtf/page/margin.rb +98 -0
  113. data/lib/rrtf/page/size.rb +98 -0
  114. data/lib/rrtf/properties.rb +3 -0
  115. data/lib/rrtf/properties/document_properties.rb +34 -0
  116. data/lib/rrtf/properties/geometry_properties.rb +380 -0
  117. data/lib/rrtf/properties/properties.rb +13 -0
  118. data/lib/rrtf/style.rb +4 -5
  119. data/lib/rrtf/style/anonymous_style.rb +73 -0
  120. data/lib/rrtf/style/border_style.rb +27 -0
  121. data/lib/rrtf/style/character_style.rb +1 -7
  122. data/lib/rrtf/style/paragraph_style.rb +0 -6
  123. data/lib/rrtf/style/position_style.rb +26 -0
  124. data/lib/rrtf/style/shading_style.rb +26 -0
  125. data/lib/rrtf/style/style.rb +60 -101
  126. data/lib/rrtf/utilities.rb +138 -0
  127. data/lib/rrtf/version.rb +1 -1
  128. data/rrtf.gemspec +1 -0
  129. metadata +85 -10
  130. data/examples/01_mac_libreoffice5_2_3_3.png +0 -0
  131. data/examples/01_mac_pages6_2.png +0 -0
  132. data/examples/01_mac_textedit1_12.png +0 -0
  133. data/examples/01_mac_word15_36.png +0 -0
  134. data/examples/01_styles_and_paragraphs.rb +0 -32
  135. data/lib/rrtf/paper.rb +0 -53
  136. data/lib/rrtf/style/document_style.rb +0 -116
  137. data/lib/rrtf/style/formatting.rb +0 -320
@@ -0,0 +1,76 @@
1
+ require 'stringio'
2
+
3
+ module RRTF
4
+ # This class represents a specialisation of the Node class to refer to a Node
5
+ # that simply contains text.
6
+ class TextNode < Node
7
+ # Actual text
8
+ attr_accessor :text
9
+
10
+ # This is the constructor for the TextNode class.
11
+ #
12
+ # ==== Parameters
13
+ # parent:: A reference to the Node that owns the TextNode. Must not be
14
+ # nil.
15
+ # text:: A String containing the node text. Defaults to nil.
16
+ #
17
+ # ==== Exceptions
18
+ # RTFError:: Generated whenever an nil parent object is specified to
19
+ # the method.
20
+ def initialize(parent, text=nil)
21
+ super(parent)
22
+ if parent.nil?
23
+ RTFError.fire("Nil parent specified for text node.")
24
+ end
25
+ @parent = parent
26
+ @text = text
27
+ end
28
+
29
+ # This method concatenates a String on to the end of the existing text
30
+ # within a TextNode object.
31
+ #
32
+ # ==== Parameters
33
+ # text:: The String to be added to the end of the text node.
34
+ def append(text)
35
+ @text = (@text.nil?) ? text.to_s : @text + text.to_s
36
+ end
37
+
38
+ # This method inserts a String into the existing text within a TextNode
39
+ # object. If the TextNode contains no text then it is simply set to the
40
+ # text passed in. If the offset specified is past the end of the nodes
41
+ # text then it is simply appended to the end.
42
+ #
43
+ # ==== Parameters
44
+ # text:: A String containing the text to be added.
45
+ # offset:: The numbers of characters from the first character to insert
46
+ # the new text at.
47
+ def insert(text, offset)
48
+ if !@text.nil?
49
+ @text = @text[0, offset] + text.to_s + @text[offset, @text.length]
50
+ else
51
+ @text = text.to_s
52
+ end
53
+ end
54
+
55
+ # This method generates the RTF equivalent for a TextNode object. This
56
+ # method escapes any special sequences that appear in the text.
57
+ def to_rtf
58
+ rtf=(@text.nil? ? '' : @text.gsub("{", "\\{").gsub("}", "\\}").gsub("\\", "\\\\"))
59
+ # This is from lfarcy / rtf-extensions
60
+ # I don't see the point of coding different 128<n<256 range
61
+
62
+ #f1=lambda { |n| n < 128 ? n.chr : n < 256 ? "\\'#{n.to_s(16)}" : "\\u#{n}\\'3f" }
63
+ # Encode as Unicode.
64
+
65
+ f=lambda { |n| n < 128 ? n.chr : "\\u#{n}\\'3f" }
66
+ # Ruby 1.9 is safe, cause detect original encoding
67
+ # and convert text to utf-16 first
68
+ if RUBY_VERSION>"1.9.0"
69
+ return rtf.encode("UTF-16LE", :undef=>:replace).each_codepoint.map(&f).join('')
70
+ else
71
+ # You SHOULD use UTF-8 as input, ok?
72
+ return rtf.unpack('U*').map(&f).join('')
73
+ end
74
+ end
75
+ end # End of the TextNode class.
76
+ end
@@ -0,0 +1,7 @@
1
+ require 'rrtf/page/margin'
2
+ require 'rrtf/page/size'
3
+
4
+ # Encapsulates classes that represent various aspects of a page in an RTF
5
+ # document.
6
+ module RRTF::Page
7
+ end
@@ -0,0 +1,98 @@
1
+ module RRTF::Page
2
+ # Represents the left, right, top, and bottom margin in a document page.
3
+ # @author Wesley Hileman
4
+ # @since 1.0.0
5
+ class Margin
6
+ attr_accessor :left, :right, :top, :bottom
7
+
8
+ # Extracts a margin object from a string.
9
+ # @see .parse_string
10
+ #
11
+ # @param (see {.parse_string})
12
+ # @return [Margin] the margin object created from the string.
13
+ # @example Parse margin from a String
14
+ # Margin.from_string("12.1pt, 2.2in, 5cm, 4in")
15
+ # # => #<RRTF::Margin:0x007fa1b3094500 @left=242, @right=3168, @top=2834, @bottom=5760>
16
+ def self.from_string(string)
17
+ self.new(parse_string(string))
18
+ end
19
+
20
+ # Extracts a margin hash from a string.
21
+ # @param [String] string the string from which to parse the margin, taking
22
+ # one of the following formats: "<marg:all>", "<marg:lr>,<marg:tb>", or
23
+ # "<marg:l>,<marg:r>,<marg:t>,<marg:b>" where each number may be suffixed
24
+ # by an optional unit (see {Utilities.value2twips}).
25
+ # @return [Hash<String, Integer>] the margin hash created from the string.
26
+ # @raise [RTFError] if the string cannot be converted into a margin hash.
27
+ # @example Parse margin from a String
28
+ # Margin.parse_string("12.1pt, 2.2in, 5cm, 4in")
29
+ # # => {"top"=>2834, "bottom"=>5760, "left"=>242, "right"=>3168} (twips)
30
+ def self.parse_string(string)
31
+ values = string.split(',').map(&:strip).collect{ |str| RRTF::Utilities.value2twips(str) }
32
+ case values.length
33
+ when 1
34
+ tblr = values.first
35
+ {"top" => tblr, "bottom" => tblr, "left" => tblr, "right" => tblr}
36
+ when 2
37
+ tb = values.last
38
+ lr = values.first
39
+ {"top" => tb, "bottom" => tb, "left" => lr, "right" => lr}
40
+ when 4
41
+ l = values[0]
42
+ r = values[1]
43
+ t = values[2]
44
+ b = values[3]
45
+ {"top" => t, "bottom" => b, "left" => l, "right" => r}
46
+ else
47
+ RRTF::RTFError.fire("Invalid margin '#{string}'.")
48
+ end # case
49
+ end
50
+
51
+ # Builds a new margin object from a string or hash.
52
+ # @note Margins are stored internally in twentieth points (twips).
53
+ # @see .from_string
54
+ # @see .parse_string
55
+ # @see RRTF::Utilities.value2twips
56
+ #
57
+ # @param [String, Hash] value the value from which to parse the margin.
58
+ # @option value [Integer] "left" the left margin in twips.
59
+ # @option value [Integer] "right" the right margin in twips.
60
+ # @option value [Integer] "top" the top margin in twips.
61
+ # @option value [Integer] "bottom" the bottom margin in twips.
62
+ def initialize(value = nil)
63
+ options = {
64
+ # default 1 inch margins
65
+ "left" => 1440,
66
+ "right" => 1440,
67
+ "top" => 1440,
68
+ "bottom" => 1440
69
+ }
70
+
71
+ case value
72
+ when String
73
+ options = options.merge(self.class.parse_string(value))
74
+ when Hash
75
+ options = options.merge(value)
76
+ when nil
77
+ else
78
+ RRTF::RTFError.fire("Cannot create margin from '#{value}'.")
79
+ end # case
80
+
81
+ @left = options.delete("left")
82
+ @right = options.delete("right")
83
+ @top = options.delete("top")
84
+ @bottom = options.delete("bottom")
85
+ end # initialize
86
+
87
+ def ==(obj)
88
+ obj.class == self.class && obj.state == state
89
+ end
90
+ alias_method :eql?, :==
91
+
92
+ protected
93
+
94
+ def state
95
+ [@left, @top, @right, @bottom]
96
+ end
97
+ end # class Margin
98
+ end # module Page
@@ -0,0 +1,98 @@
1
+ module RRTF::Page
2
+ # Represents the size of a page (width and height).
3
+ # @author Wesley Hileman
4
+ # @since 1.0.0
5
+ class Size
6
+ attr_accessor :width, :height
7
+
8
+ # Dictionary of standard paper sizes in twips.
9
+ DICTIONARY = {
10
+ "A0" => {"width" => 47685, "height" => 67416},
11
+ "A1" => {"width" => 33680, "height" => 47685},
12
+ "A2" => {"width" => 23814, "height" => 33680},
13
+ "A3" => {"width" => 16840, "height" => 23814},
14
+ "A4" => {"width" => 11907, "height" => 16840},
15
+ "A5" => {"width" => 8392, "height" => 11907},
16
+ "LETTER" => {"width" => 12247, "height" => 15819},
17
+ "LEGAL" => {"width" => 12247, "height" => 20185},
18
+ "EXECUTIVE" => {"width" => 10773, "height" => 14402},
19
+ "LEDGER_TABLOID" => {"width" => 15819, "height" => 24494}
20
+ }.freeze
21
+
22
+ # Converts a string representing a paper size into a paper object.
23
+ # @see .parse_string
24
+ #
25
+ # @param (see {.parse_string})
26
+ # @return [Size] the paper object representing the width & height of the paper.
27
+ def self.from_string(string)
28
+ self.new(self.parse_string(string))
29
+ end
30
+
31
+ # Converts a string representing a paper size into an ordered pair
32
+ # (a two-item array) representing the width and height of the paper.
33
+ # @see Utilities.value2twips
34
+ #
35
+ # @param [String] string the string to be parsed that can take on one of
36
+ # two formats: (1) "<SIZE>" where SIZE is an entry in {DICTIONARY}
37
+ # or (2) "WIDTH, HEIGHT" where WIDTH and HEIGHT are width and height
38
+ # strings with optional units suffix (see {Utilities.value2twips},
39
+ # default is twips)
40
+ # @return [Hash<String, Integer>] the ordered pair (hash with keys "width" and "height")
41
+ # representing the width and height of the paper.
42
+ def self.parse_string(string)
43
+ # first, try to lookup in size dictionary
44
+ size = DICTIONARY[string]
45
+ return size unless size.nil?
46
+
47
+ # if not found, try to extract width and height from string
48
+ parts = string.split(',').map(&:strip).map{ |str| RRTF::Utilities.value2twips(str) }
49
+ if parts.length == 2
50
+ return {"width" => parts [0], "height" => parts[1]}
51
+ end # if
52
+
53
+ # unable to parse string
54
+ RTFError.fire("Unable to parse size from string '#{string}'.")
55
+ end
56
+
57
+ # This is the constructor for the Paper class. All dimension parameters
58
+ # to this method are in twips.
59
+ # @note Paper size is stored internally in twentieth points (twips).
60
+ # @see .from_string
61
+ # @see .parse_string
62
+ # @see RRTF::Utilities.value2twips
63
+ #
64
+ # @param [String, Hash] value from which to parse the size.
65
+ # @option value [Integer] "width" the width of the paper in portrait mode (twips).
66
+ def initialize(value = nil)
67
+ # default options
68
+ options = {
69
+ "width" => 12247,
70
+ "height" => 15819
71
+ }
72
+
73
+ case value
74
+ when String
75
+ options = options.merge(self.class.parse_string(value))
76
+ when Hash
77
+ options = options.merge(value)
78
+ when nil
79
+ else
80
+ RRTF::RTFError.fire("Invalid page size format #{value}.")
81
+ end # case
82
+
83
+ @width = options.delete("width")
84
+ @height = options.delete("height")
85
+ end
86
+
87
+ def ==(obj)
88
+ obj.class == self.class && obj.state == state
89
+ end
90
+ alias_method :eql?, :==
91
+
92
+ protected
93
+
94
+ def state
95
+ [@width, @height]
96
+ end
97
+ end # class Size
98
+ end # module Page
@@ -0,0 +1,3 @@
1
+ require 'rrtf/properties/properties'
2
+ require 'rrtf/properties/document_properties'
3
+ require 'rrtf/properties/geometry_properties'
@@ -0,0 +1,34 @@
1
+ require 'stringio'
2
+
3
+ module RRTF
4
+ # This class represents properties that are to be applied at the
5
+ # document level.
6
+ # @author Wesley Hileman
7
+ # @since 1.0.0
8
+ class DocumentProperties < Properties
9
+ include DocumentFormatting
10
+ include PageFormatting
11
+
12
+ # This is a constructor for the DocumentProperties class.
13
+ #
14
+ # @param [Hash] options
15
+ # @option options (see {DocumentFormatting#initialize_document_formatting})
16
+ # @option options (see {PageFormatting#initialize_page_formatting})
17
+ def initialize(options = {})
18
+ initialize_document_formatting(options)
19
+ initialize_page_formatting(options)
20
+ end
21
+
22
+ # Converts a document properties object into an RTF sequence.
23
+ #
24
+ # @return [String] the RTF sequence corresponding to the properties object.
25
+ def to_rtf
26
+ rtf = StringIO.new
27
+
28
+ rtf << document_formatting_to_rtf
29
+ rtf << page_formatting_to_rtf
30
+
31
+ rtf.string
32
+ end
33
+ end # End of the DocumentProperties class
34
+ end # module RRTF
@@ -0,0 +1,380 @@
1
+ require 'stringio'
2
+
3
+ module RRTF
4
+ # This class represents properties that are to be applied to geometry
5
+ # objects.
6
+ # @author Wesley Hileman
7
+ # @since 1.0.0
8
+ class GeometryProperties < Properties
9
+
10
+ HORIZONTAL_REFERENCE_DICTIONARY = {
11
+ "MARGIN" => 0,
12
+ "PAGE" => 1,
13
+ "COLUMN" => 2,
14
+ "CHARACTER" => 3,
15
+ "LEFT_MARGIN" => 4,
16
+ "RIGHT_MARGIN" => 5,
17
+ "INSIDE_MARGIN" => 6,
18
+ "OUTSIDE_MARGIN" => 7
19
+ }.freeze
20
+
21
+ VERTICAL_REFERENCE_DICTIONARY = {
22
+ "MARGIN" => 0,
23
+ "PAGE" => 1,
24
+ "PARAGRAPH" => 2,
25
+ "LINE" => 3,
26
+ "TOP_MARGIN" => 4,
27
+ "BOTTOM_MARGIN" => 5,
28
+ "INSIDE_MARGIN" => 6,
29
+ "OUTSIDE_MARGIN" => 7
30
+ }.freeze
31
+
32
+ HORIZONTAL_ALIGNMENT_DICTIONARY = {
33
+ "ABSOLUTE" => 0,
34
+ "LEFT" => 1,
35
+ "CENTER" => 2,
36
+ "RIGHT" => 3,
37
+ "INSIDE" => 4,
38
+ "OUTSIDE" => 5
39
+ }.freeze
40
+
41
+ VERTICAL_ALIGNMENT_DICTIONARY = {
42
+ "ABSOLUTE" => 0,
43
+ "TOP" => 1,
44
+ "CENTER" => 2,
45
+ "BOTTOM" => 3,
46
+ "INSIDE" => 4,
47
+ "OUTSIDE" => 5
48
+ }.freeze
49
+
50
+ WIDTH_REFERENCE_DICTIONARY = {
51
+ "MARGIN" => 0,
52
+ "PAGE" => 1,
53
+ "LEFT_MARGIN" => 2,
54
+ "RIGHT_MARGIN" => 3,
55
+ "INSIDE_MARGIN" => 4,
56
+ "OUTSIDE_MARGIN" => 5
57
+ }.freeze
58
+
59
+ HEIGHT_REFERENCE_DICTIONARY = {
60
+ "MARGIN" => 0,
61
+ "PAGE" => 1,
62
+ "TOP_MARGIN" => 2,
63
+ "BOTTOM_MARGIN" => 3,
64
+ "INSIDE_MARGIN" => 4,
65
+ "OUTSIDE_MARGIN" => 5
66
+ }.freeze
67
+
68
+ TEXT_WRAP_DICTIONARY = {
69
+ "INLINE" => {"WRAP" => 1, "SIDE" => nil},
70
+ "AROUND_BOTH" => {"WRAP" => 2, "SIDE" => 0},
71
+ "AROUND_LEFT" => {"WRAP" => 2, "SIDE" => 1},
72
+ "AROUND_RIGHT" => {"WRAP" => 2, "SIDE" => 2},
73
+ "AROUND_LARGEST" => {"WRAP" => 2, "SIDE" => 3},
74
+ "NONE" => {"WRAP" => 3, "SIDE" => nil},
75
+ "TIGHT_AROUND_BOTH" => {"WRAP" => 4, "SIDE" => 0},
76
+ "TIGHT_AROUND_LEFT" => {"WRAP" => 4, "SIDE" => 1},
77
+ "TIGHT_AROUND_RIGHT" => {"WRAP" => 4, "SIDE" => 2},
78
+ "TIGHT_AROUND_LARGEST" => {"WRAP" => 4, "SIDE" => 3},
79
+ }.freeze
80
+
81
+ GEOMERTY_TYPE_DICTIONARY = {
82
+ "CUSTOM" => 0,
83
+ "RECTANGLE" => 1,
84
+ "ROUND_RECTANGLE" => 2,
85
+ "ELLIPSE" => 3,
86
+ "DIAMOND" => 4,
87
+ "ISOSCELES_TRIANGLE" => 5,
88
+ "RIGHT_TRIANGLE" => 6,
89
+ "PARALLELOGRAM" => 7,
90
+ "TRAPEZOID" => 8,
91
+ "HEXAGON" => 9,
92
+ "OCTAGON" => 10,
93
+ "PENTAGON" => 56,
94
+ "LINE" => 20,
95
+ "TEXT_BOX" => 202
96
+ }.freeze
97
+
98
+ TEXT_ANCHOR_DICTIONARY = {
99
+ "TOP" => 0,
100
+ "MIDDLE" => 1,
101
+ "BOTTOM" => 2,
102
+ "TOP_CENTERED" => 3,
103
+ "MIDDLE_CENTERED" => 4,
104
+ "BOTTOM_CENTERED" => 5,
105
+ "TOP_BASELINE" => 6,
106
+ "BOTTOM_BASELINE" => 7,
107
+ "TOP_CENTERED_BASELINE" => 8,
108
+ "BOTTOM_CENTERED_BASELINE" => 9
109
+ }.freeze
110
+
111
+ # @note The upper three bits store segment stype, lower 13 bits store the
112
+ # number of segments of that type to appear in series (always 1 -- except
113
+ # for control segments -- for the non-compressed encoding used here
114
+ # where the codes for segments of the same type that appear in series are
115
+ # repeated).
116
+ PATH_SEGMENT_DICTIONARY = {
117
+ # draw a line from the current point to a specified end point
118
+ # [requires one additional point]
119
+ "LINE_TO" => "0001".to_i(16),
120
+ # draw a cubic bezier curve using the current point, two control points,
121
+ # and an end point [requires three additional points]
122
+ "CUBIC_BEZIER_TO" => "2001".to_i(16),
123
+ # draw a line from the current point to the starting point and close
124
+ # the path [requires no additional points]
125
+ "CLOSE_PATH" => "6001".to_i(16),
126
+ # start a path (control segment) [requires one point]
127
+ "START_AT" => "4000".to_i(16),
128
+ # end a path (control segment) [requires no points]
129
+ "END" => "8000".to_i(16)
130
+ }.freeze
131
+
132
+ # This is a constructor for the GeometryProperties class.
133
+ #
134
+ # @param [Hash] options
135
+ def initialize(options = {})
136
+ @type = GEOMERTY_TYPE_DICTIONARY[options.delete("type")]
137
+ @rotation = Utilities.value2geomfrac(options.delete("rotate"))
138
+ @left = Utilities.value2twips(options.delete("left"))
139
+ @right = Utilities.value2twips(options.delete("right"))
140
+ @top = Utilities.value2twips(options.delete("top"))
141
+ @bottom = Utilities.value2twips(options.delete("bottom"))
142
+ @z_index = options.delete("z_index")
143
+ @horizontal_reference = HORIZONTAL_REFERENCE_DICTIONARY[options.delete("horizontal_reference")] || HORIZONTAL_REFERENCE_DICTIONARY["MARGIN"]
144
+ @vertical_reference = VERTICAL_REFERENCE_DICTIONARY[options.delete("vertical_reference")] || VERTICAL_REFERENCE_DICTIONARY["MARGIN"]
145
+ @text_wrap = TEXT_WRAP_DICTIONARY[options.delete("text_wrap")]
146
+ @below_text = Utilities.value2geombool(options.delete("below_text"))
147
+ @lock_anchor = options.delete("lock_anchor")
148
+ @horizontal_alignment = HORIZONTAL_ALIGNMENT_DICTIONARY[options.delete("horizontal_alignment")] || HORIZONTAL_ALIGNMENT_DICTIONARY["ABSOLUTE"]
149
+ @vertical_alignment = VERTICAL_ALIGNMENT_DICTIONARY[options.delete("vertical_alignment")] || VERTICAL_ALIGNMENT_DICTIONARY["ABSOLUTE"]
150
+ @allow_overlap = Utilities.value2geombool(options.delete("allow_overlap"))
151
+ @width_reference = WIDTH_REFERENCE_DICTIONARY[options.delete("width_reference")] || WIDTH_REFERENCE_DICTIONARY["MARGIN"]
152
+ @height_reference = HEIGHT_REFERENCE_DICTIONARY[options.delete("height_reference")] || HEIGHT_REFERENCE_DICTIONARY["MARGIN"]
153
+ @width, @width_units = Utilities.parse_string_with_units(options.delete("width"))
154
+ @height, @height_units = Utilities.parse_string_with_units(options.delete("height"))
155
+ @width = Utilities.value2twips("#{@width}#{@width_units}") unless @width.nil? || @width_units == '%'
156
+ @height = Utilities.value2twips("#{@height}#{@height_units}") unless @height.nil? || @height_units == '%'
157
+ @fill_color = options.delete("fill_color")
158
+ @has_fill = Utilities.value2geombool(options.delete("has_fill") || !@fill_color.nil?)
159
+ @line_color = options.delete("line_color")
160
+ @line_width = Utilities.value2emu(options.delete("line_width"))
161
+ @has_line = Utilities.value2geombool(options.delete("has_line") || !@line_color.nil? || !@line_width.nil?)
162
+ @text_margin = options.delete("text_margin")
163
+ @text_anchor = TEXT_ANCHOR_DICTIONARY[options.delete("text_anchor")]
164
+ @fit_to_text = Utilities.value2geombool(options.delete("fit_to_text"))
165
+ @fit_text_to_shape = Utilities.value2geombool(options.delete("fit_text_to_shape"))
166
+ @flip_horizontal = Utilities.value2geombool(options.delete("flip_horizontal"))
167
+ @flip_vertical = Utilities.value2geombool(options.delete("flip_vertical"))
168
+ @path = options.delete("path")
169
+ @path_coordinate_origin = options.delete("path_coordinate_origin") || [0, 0]
170
+ @path_coordinate_limits = options.delete("path_coordinate_limits") || [21600, 21600]
171
+
172
+ parse_dimensions!
173
+ parse_color! :fill_color, :line_color
174
+ parse_margin! :text_margin
175
+ parse_path!
176
+
177
+ unless options.empty?
178
+ RTFError.fire("Unreconized geometry options #{options}.")
179
+ end # unless
180
+ end # initialize()
181
+
182
+ # Converts a geometry properties object into an RTF sequence.
183
+ #
184
+ # @return [String] the RTF sequence corresponding to the properties object.
185
+ def to_rtf
186
+ rtf = StringIO.new
187
+
188
+ # keyword properties
189
+ rtf << "\\shpleft#{@left}" unless @left.nil?
190
+ rtf << "\\shpright#{@right}" unless @right.nil?
191
+ rtf << "\\shptop#{@top}" unless @top.nil?
192
+ rtf << "\\shpbottom#{@bottom}" unless @bottom.nil?
193
+ rtf << "\\shpz#{@z_index}" unless @z_index.nil?
194
+ rtf << "\\shpbxpage" if @horizontal_reference == HORIZONTAL_REFERENCE_DICTIONARY["PAGE"]
195
+ rtf << "\\shpbxmargin" if @horizontal_reference == HORIZONTAL_REFERENCE_DICTIONARY["MARGIN"]
196
+ rtf << "\\shpbxcolumn" if @horizontal_reference == HORIZONTAL_REFERENCE_DICTIONARY["COLUMN"]
197
+ rtf << "\\shpbxignore" unless @vertical_reference.nil?
198
+ rtf << "\\shpbypage" if @vertical_reference == VERTICAL_REFERENCE_DICTIONARY["PAGE"]
199
+ rtf << "\\shpbymargin" if @vertical_reference == VERTICAL_REFERENCE_DICTIONARY["MARGIN"]
200
+ rtf << "\\shpbypara" if @vertical_reference == VERTICAL_REFERENCE_DICTIONARY["PARAGRAPH"]
201
+ rtf << "\\shpbyignore" unless @vertical_reference.nil?
202
+ rtf << "\\shpwr#{@text_wrap["WRAP"]}" unless @text_wrap.nil? || @text_wrap["WRAP"].nil?
203
+ rtf << "\\shpwrk#{@text_wrap["SIDE"]}" unless @text_wrap.nil? || @text_wrap["SIDE"].nil?
204
+ rtf << "\\shpfblwtxt#{@below_text}" unless @below_text.nil?
205
+ rtf << "\\shplockanchor" if @lock_anchor
206
+
207
+ rtf << "\n"
208
+
209
+ # object properties
210
+ rtf << build_property("shapeType", @type) unless @type.nil?
211
+ rtf << build_property("rotation", @rotation) unless @rotation.nil?
212
+ rtf << build_property("posh", @horizontal_alignment) unless @horizontal_alignment.nil?
213
+ rtf << build_property("posrelh", @horizontal_reference) unless @horizontal_reference.nil?
214
+ rtf << build_property("posv", @vertical_alignment) unless @vertical_alignment.nil?
215
+ rtf << build_property("posrelv", @vertical_reference) unless @vertical_reference.nil?
216
+ rtf << build_property("fAllowOverlap", @allow_overlap) unless @allow_overlap.nil?
217
+ rtf << build_property("pctHoriz", @width) unless @width.nil? || @width_units != '%'
218
+ rtf << build_property("pctVert", @height) unless @height.nil? || @height_units != '%'
219
+ rtf << build_property("sizerelh", @width_reference) unless @width_reference.nil?
220
+ rtf << build_property("sizerelv", @height_reference) unless @height_reference.nil?
221
+ rtf << build_property("fFilled", @has_fill) unless @has_fill.nil?
222
+ rtf << build_property("fillColor", @fill_color) unless @fill_color.nil?
223
+ rtf << build_property("fLine", @has_line) unless @has_fill.nil?
224
+ rtf << build_property("lineColor", @line_color) unless @line_color.nil?
225
+ rtf << build_property("lineWidth", @line_width) unless @line_width.nil?
226
+ rtf << build_property("dxTextLeft", @text_margin_left) unless @text_margin_left.nil?
227
+ rtf << build_property("dxTextRight", @text_margin_right) unless @text_margin_right.nil?
228
+ rtf << build_property("dyTextTop", @text_margin_top) unless @text_margin_top.nil?
229
+ rtf << build_property("dyTextBottom", @text_margin_bottom) unless @text_margin_bottom.nil?
230
+ rtf << build_property("anchorText", @text_anchor) unless @text_anchor.nil?
231
+ rtf << build_property("fBehindDocument", @below_text) unless @below_text.nil?
232
+ rtf << build_property("fFitShapeToText", @fit_to_text) unless @fit_to_text.nil?
233
+ rtf << build_property("fFitTextToShape", @fit_text_to_shape) unless @fit_text_to_shape.nil?
234
+ rtf << build_property("fFlipH", @flip_horizontal) unless @flip_horizontal.nil?
235
+ rtf << build_property("fFlipV", @flip_vertical) unless @flip_vertical.nil?
236
+ rtf << build_property("geoLeft", @path_coordinate_origin[0]) unless @path.nil? || @path_coordinate_origin.nil?
237
+ rtf << build_property("geoTop", @path_coordinate_origin[1]) unless @path.nil? || @path_coordinate_origin.nil?
238
+ rtf << build_property("geoRight", @path_coordinate_limits[0]) unless @path.nil? || @path_coordinate_limits.nil?
239
+ rtf << build_property("geoBottom", @path_coordinate_limits[1]) unless @path.nil? || @path_coordinate_limits.nil?
240
+ rtf << build_property("pVerticies", @path_verticies) unless @path.nil? || @path_verticies.nil?
241
+ rtf << build_property("pSegmentInfo", @path_segment_info) unless @path.nil? || @path_segment_info.nil?
242
+ rtf << build_property("pConnectionSites", @path_connection_sites) unless @path.nil? || @path_connection_sites.nil?
243
+ rtf << build_property("fLineOK", 1)
244
+ rtf << build_property("fFillOK", 1)
245
+ rtf << build_property("f3DOK", 1)
246
+
247
+ rtf.string
248
+ end
249
+
250
+ private
251
+
252
+ def build_property(name, value)
253
+ "{\\sp{\\sn #{name}}{\\sv #{value}}}\n"
254
+ end
255
+
256
+ def build_array(array, bytes_per_element)
257
+ "#{bytes_per_element};#{array.length};#{array.join(';')}"
258
+ end
259
+
260
+ def array2emu(array)
261
+ array.collect{ |el| Utilities.value2emu(el) }
262
+ end
263
+
264
+ def parse_dimensions!
265
+ unless @width.nil?
266
+ if @width_units == '%'
267
+ @percent_width = @width
268
+ else
269
+ case [@left.nil?, @right.nil?]
270
+ when [true, true]
271
+ @left = 0
272
+ @right = @width
273
+ when [true, false]
274
+ @left = @right - @width
275
+ when [false, true]
276
+ @right = @left + @width
277
+ end # case
278
+ end # if
279
+ end # unless
280
+
281
+ unless @height.nil?
282
+ if @height_units == '%'
283
+ @percent_height = @height
284
+ else
285
+ case [@top.nil?, @bottom.nil?]
286
+ when [true, true]
287
+ @top = 0
288
+ @bottom = @height
289
+ when [true, false]
290
+ @top = @bottom - @height
291
+ when [false, true]
292
+ @bottom = @top + @height
293
+ end # case
294
+ end # if
295
+ end # unless
296
+ end # parse_dimensions()
297
+
298
+ def parse_color!(*color_attrs)
299
+ color_attrs.each do |color_attr|
300
+ color = instance_variable_get(:"@#{color_attr}")
301
+
302
+ unless color.nil?
303
+ case color
304
+ when String
305
+ color = Colour.from_string(color).to_decimal("reverse_bytes" => true)
306
+ when Colour
307
+ color = color.to_decimal
308
+ else
309
+ RTFError.fire("Unsupported color format #{color}.")
310
+ end # case
311
+ end # unless
312
+
313
+ instance_variable_set(:"@#{color_attr}", color)
314
+ end # each
315
+ end # parse_color()
316
+
317
+ def parse_margin!(*margin_attrs)
318
+ margin_attrs.each do |margin_attr|
319
+ margin = instance_variable_get("@#{margin_attr}")
320
+ next if margin.nil?
321
+
322
+ margin = Page::Margin.new(margin)
323
+ left = Utilities.value2emu(margin.left+'twip')
324
+ right = Utilities.value2emu(margin.right+'twip')
325
+ top = Utilities.value2emu(margin.top+'twip')
326
+ bottom = Utilities.value2emu(margin.bottom+'twip')
327
+
328
+ instance_variable_set("@#{margin_attr}", margin)
329
+ instance_variable_set("@#{margin_attr}_left", left)
330
+ instance_variable_set("@#{margin_attr}_right", right)
331
+ instance_variable_set("@#{margin_attr}_top", top)
332
+ instance_variable_set("@#{margin_attr}_bottom", bottom)
333
+ end
334
+ end # parse_margin()
335
+
336
+ def parse_path!
337
+ return if @path.nil?
338
+
339
+ verticies = []
340
+ connection_sites = []
341
+ seg_info = []
342
+
343
+ unless @path.is_a?(Array) && @path.collect{ |tup| tup.is_a?(Array) && (1..5).include?(tup.length) }.all?
344
+ RTFError.fire("Path segments must be an array of arrays with length 1 through 5.")
345
+ end # unless
346
+
347
+ sx = (@path_coordinate_limits[0] - @path_coordinate_origin[0]).to_f/(Utilities.value2emu("#{@width}twip")).to_f
348
+ sy = (@path_coordinate_limits[1] - @path_coordinate_origin[1]).to_f/(Utilities.value2emu("#{@height}twip")).to_f
349
+
350
+ @path.each do |seg|
351
+ # first item in segment array gives the segment type
352
+ type = seg[0]
353
+
354
+ if PATH_SEGMENT_DICTIONARY[type].nil?
355
+ RTFError.fire("Invalid segment type '#{type}'.")
356
+ end # case
357
+
358
+ if seg.length > 1
359
+ # remaining items give the points associated with the segment
360
+ # (last element is the end point; bezier curves also have a control
361
+ # point before last point; the starting point is given by the end
362
+ # point of the last segment)
363
+ points = seg[1..(seg.length - 1)].collect{ |p| array2emu(p) }.collect{ |p| [(p[0]*sx).round, (p[1]*sy).round] }
364
+ verticies += points
365
+ # the last point is the endpoint for the segment that forms a
366
+ # "connection site" with the next segment
367
+ connection_sites << points.last
368
+ end # if
369
+
370
+ # add appropriate code to the segment information array indicating the
371
+ # type of segment to create
372
+ seg_info << PATH_SEGMENT_DICTIONARY[type]
373
+ end # each
374
+
375
+ @path_verticies = build_array(verticies.collect{ |v| "(#{v[0]},#{v[1]})" }, 8)
376
+ @path_connection_sites = build_array(connection_sites.collect{ |s| "(#{s[0]},#{s[1]})" }, 8)
377
+ @path_segment_info = build_array(seg_info, 2)
378
+ end # parse_path()
379
+ end # End of the DocumentProperties class
380
+ end # module RRTF