ruby_pptx 0.1.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 (101) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +83 -0
  3. data/LICENSE +21 -0
  4. data/NOTICE +48 -0
  5. data/README.md +272 -0
  6. data/lib/ruby_pptx/action.rb +120 -0
  7. data/lib/ruby_pptx/autoshape_spec.rb +337 -0
  8. data/lib/ruby_pptx/builder.rb +228 -0
  9. data/lib/ruby_pptx/chart/categories.rb +208 -0
  10. data/lib/ruby_pptx/chart/chart.rb +443 -0
  11. data/lib/ruby_pptx/chart/combo.rb +230 -0
  12. data/lib/ruby_pptx/chart/data.rb +147 -0
  13. data/lib/ruby_pptx/chart/format.rb +647 -0
  14. data/lib/ruby_pptx/chart/workbook_writer.rb +212 -0
  15. data/lib/ruby_pptx/chart/xml_writer.rb +953 -0
  16. data/lib/ruby_pptx/chart/xy_data.rb +182 -0
  17. data/lib/ruby_pptx/core_ext.rb +12 -0
  18. data/lib/ruby_pptx/dml/color.rb +155 -0
  19. data/lib/ruby_pptx/dml/effect.rb +33 -0
  20. data/lib/ruby_pptx/dml/fill.rb +234 -0
  21. data/lib/ruby_pptx/element_proxy.rb +53 -0
  22. data/lib/ruby_pptx/enum/action.rb +46 -0
  23. data/lib/ruby_pptx/enum/base.rb +139 -0
  24. data/lib/ruby_pptx/enum/chart.rb +278 -0
  25. data/lib/ruby_pptx/enum/dml.rb +237 -0
  26. data/lib/ruby_pptx/enum/lang.rb +441 -0
  27. data/lib/ruby_pptx/enum/prog_id.rb +38 -0
  28. data/lib/ruby_pptx/enum/shapes.rb +514 -0
  29. data/lib/ruby_pptx/enum/text.rb +108 -0
  30. data/lib/ruby_pptx/errors.rb +15 -0
  31. data/lib/ruby_pptx/geometry.rb +89 -0
  32. data/lib/ruby_pptx/image.rb +347 -0
  33. data/lib/ruby_pptx/length.rb +120 -0
  34. data/lib/ruby_pptx/media.rb +64 -0
  35. data/lib/ruby_pptx/numeric_lengths.rb +30 -0
  36. data/lib/ruby_pptx/opc/constants.rb +205 -0
  37. data/lib/ruby_pptx/opc/oxml.rb +99 -0
  38. data/lib/ruby_pptx/opc/pack_uri.rb +146 -0
  39. data/lib/ruby_pptx/opc/package.rb +482 -0
  40. data/lib/ruby_pptx/opc/serialized.rb +229 -0
  41. data/lib/ruby_pptx/opc/spec.rb +37 -0
  42. data/lib/ruby_pptx/oxml/action.rb +43 -0
  43. data/lib/ruby_pptx/oxml/chart.rb +785 -0
  44. data/lib/ruby_pptx/oxml/content_model.rb +186 -0
  45. data/lib/ruby_pptx/oxml/core_properties.rb +145 -0
  46. data/lib/ruby_pptx/oxml/dml/color.rb +115 -0
  47. data/lib/ruby_pptx/oxml/dml/fill.rb +137 -0
  48. data/lib/ruby_pptx/oxml/element.rb +329 -0
  49. data/lib/ruby_pptx/oxml/ns.rb +115 -0
  50. data/lib/ruby_pptx/oxml/presentation.rb +110 -0
  51. data/lib/ruby_pptx/oxml/section.rb +65 -0
  52. data/lib/ruby_pptx/oxml/shapes/autoshape.rb +310 -0
  53. data/lib/ruby_pptx/oxml/shapes/groupshape.rb +217 -0
  54. data/lib/ruby_pptx/oxml/shapes/other.rb +405 -0
  55. data/lib/ruby_pptx/oxml/shapes/shared.rb +292 -0
  56. data/lib/ruby_pptx/oxml/simple_types.rb +563 -0
  57. data/lib/ruby_pptx/oxml/slide.rb +290 -0
  58. data/lib/ruby_pptx/oxml/table.rb +253 -0
  59. data/lib/ruby_pptx/oxml/text.rb +324 -0
  60. data/lib/ruby_pptx/oxml/theme.rb +93 -0
  61. data/lib/ruby_pptx/package.rb +139 -0
  62. data/lib/ruby_pptx/parts/chart.rb +75 -0
  63. data/lib/ruby_pptx/parts/core_properties.rb +40 -0
  64. data/lib/ruby_pptx/parts/embedded_package.rb +43 -0
  65. data/lib/ruby_pptx/parts/image.rb +55 -0
  66. data/lib/ruby_pptx/parts/media.rb +17 -0
  67. data/lib/ruby_pptx/parts/presentation.rb +79 -0
  68. data/lib/ruby_pptx/parts/slide.rb +221 -0
  69. data/lib/ruby_pptx/parts/theme.rb +26 -0
  70. data/lib/ruby_pptx/pattern_matching.rb +50 -0
  71. data/lib/ruby_pptx/presentation.rb +128 -0
  72. data/lib/ruby_pptx/refinements.rb +21 -0
  73. data/lib/ruby_pptx/section.rb +130 -0
  74. data/lib/ruby_pptx/shapes/adjustments.rb +83 -0
  75. data/lib/ruby_pptx/shapes/authoring.rb +110 -0
  76. data/lib/ruby_pptx/shapes/base.rb +138 -0
  77. data/lib/ruby_pptx/shapes/freeform.rb +141 -0
  78. data/lib/ruby_pptx/shapes/placeholder.rb +184 -0
  79. data/lib/ruby_pptx/shapes/shape.rb +337 -0
  80. data/lib/ruby_pptx/shapes/shape_tree.rb +634 -0
  81. data/lib/ruby_pptx/sliceable.rb +34 -0
  82. data/lib/ruby_pptx/slide.rb +430 -0
  83. data/lib/ruby_pptx/table.rb +316 -0
  84. data/lib/ruby_pptx/table_paging.rb +91 -0
  85. data/lib/ruby_pptx/templates/default.pptx +0 -0
  86. data/lib/ruby_pptx/templates/docx-icon.emf +0 -0
  87. data/lib/ruby_pptx/templates/generic-icon.emf +0 -0
  88. data/lib/ruby_pptx/templates/media-speaker.png +0 -0
  89. data/lib/ruby_pptx/templates/notes.xml +23 -0
  90. data/lib/ruby_pptx/templates/notesMaster.xml +352 -0
  91. data/lib/ruby_pptx/templates/pptx-icon.emf +0 -0
  92. data/lib/ruby_pptx/templates/slideMaster.xml +277 -0
  93. data/lib/ruby_pptx/templates/theme.xml +321 -0
  94. data/lib/ruby_pptx/templates/xlsx-icon.emf +0 -0
  95. data/lib/ruby_pptx/text/fitter.rb +72 -0
  96. data/lib/ruby_pptx/text/font_metrics.rb +221 -0
  97. data/lib/ruby_pptx/text/text.rb +393 -0
  98. data/lib/ruby_pptx/theme.rb +98 -0
  99. data/lib/ruby_pptx/version.rb +5 -0
  100. data/lib/ruby_pptx.rb +90 -0
  101. metadata +174 -0
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/oxml/element"
4
+
5
+ module Pptx
6
+ module Oxml
7
+ # The class macros that declare an element's content model.
8
+ #
9
+ # This replaces python-pptx's `MetaOxmlElement` metaclass. Where Python
10
+ # collected descriptor objects out of the class dict and post-processed
11
+ # them, Ruby can just generate the methods as each macro runs, which reads
12
+ # far more directly.
13
+ #
14
+ # Generated method names follow the declared property name +name+:
15
+ #
16
+ # | macro | generates |
17
+ # |---------------------|------------------------------------------------------|
18
+ # | `optional_attr` | `name`, `name=` |
19
+ # | `required_attr` | `name`, `name=` |
20
+ # | `one_and_only_one` | `name` (raises if absent) |
21
+ # | `zero_or_one` | `name`, `get_or_add_name`, `add_name`, `new_name`,
22
+ # `insert_name`, `remove_name` |
23
+ # | `zero_or_more` | `name_list`, `add_name`, `new_name`, `insert_name` |
24
+ # | `one_or_more` | as `zero_or_more` |
25
+ # | `zero_or_one_choice`| `name`, `remove_name`, plus per-choice accessors and `get_or_change_to_x` |
26
+ #
27
+ # python-pptx generates a private `_add_x` and, for repeating elements, a
28
+ # separate public `add_x`. Here they are one `add_x` taking keyword
29
+ # arguments, which is simpler but means a hand-written convenience wrapper
30
+ # cannot reuse the name -- call it something like `add_x_for` instead.
31
+ module ContentModel
32
+ # A member of an `EG_*` element group, for {#zero_or_one_choice}.
33
+ Choice = Data.define(:nsptag) do
34
+ def prop_name = Ns.split_tag(nsptag).last
35
+ end
36
+
37
+ # Declare one member of a choice group.
38
+ def choice(nsptag) = Choice.new(nsptag)
39
+
40
+ # An attribute that may be absent. Reading returns +default+ when it is;
41
+ # assigning +default+ removes it, so a document never carries an
42
+ # attribute that only restates the schema default.
43
+ def optional_attr(xml_name, type:, default: nil, as: nil)
44
+ name = as || attr_prop_name(xml_name)
45
+
46
+ define_method(name) do
47
+ raw = get(xml_name)
48
+ raw.nil? ? default : type.from_xml(raw)
49
+ end
50
+
51
+ define_method("#{name}=") do |value|
52
+ if value == default || value.nil?
53
+ delete_attribute(xml_name)
54
+ else
55
+ set(xml_name, type.to_xml(value))
56
+ end
57
+ value
58
+ end
59
+ end
60
+
61
+ # An attribute the schema requires. Reading raises when it is missing,
62
+ # because that means the document is invalid rather than merely sparse.
63
+ def required_attr(xml_name, type:, as: nil)
64
+ name = as || attr_prop_name(xml_name)
65
+
66
+ define_method(name) do
67
+ raw = get(xml_name)
68
+ if raw.nil?
69
+ raise InvalidXmlError,
70
+ "required #{xml_name.inspect} attribute not present on <#{nsptag}>"
71
+ end
72
+ type.from_xml(raw)
73
+ end
74
+
75
+ define_method("#{name}=") { |value| set(xml_name, type.to_xml(value)) }
76
+ end
77
+
78
+ # A required child element.
79
+ def one_and_only_one(nsptag, as: nil)
80
+ name = as || Ns.split_tag(nsptag).last
81
+
82
+ define_method(name) do
83
+ find(nsptag) ||
84
+ raise(InvalidXmlError, "required <#{nsptag}> child element not present")
85
+ end
86
+ end
87
+
88
+ # An optional child element.
89
+ def zero_or_one(nsptag, successors: [], as: nil)
90
+ name = as || Ns.split_tag(nsptag).last
91
+
92
+ define_method(name) { find(nsptag) }
93
+ define_creator(name, nsptag)
94
+ define_inserter(name, successors)
95
+ define_adder(name)
96
+ define_once("get_or_add_#{name}") { find(nsptag) || public_send("add_#{name}") }
97
+ define_once("remove_#{name}") { remove_all(nsptag) }
98
+ end
99
+
100
+ # A child element that may repeat. The singular accessor is deliberately
101
+ # not generated -- with a repeating element there is no "the" one.
102
+ def zero_or_more(nsptag, successors: [], as: nil)
103
+ name = as || Ns.split_tag(nsptag).last
104
+
105
+ define_method("#{name}_list") { find_all(nsptag) }
106
+ define_creator(name, nsptag)
107
+ define_inserter(name, successors)
108
+ define_adder(name)
109
+ end
110
+
111
+ # A child element that must appear at least once. Structurally identical
112
+ # to {#zero_or_more}; the distinction is documentation of the schema.
113
+ def one_or_more(nsptag, successors: [], as: nil)
114
+ zero_or_more(nsptag, successors: successors, as: as)
115
+ end
116
+
117
+ # An `EG_*` group, at most one member of which may be present.
118
+ def zero_or_one_choice(choices, as:, successors: [])
119
+ member_tags = choices.map(&:nsptag)
120
+
121
+ define_method(as) { first_child_found_in(*member_tags) }
122
+ define_once("remove_#{as}") { remove_all(*member_tags) }
123
+
124
+ choices.each { |c| define_choice(c, as, successors) }
125
+ end
126
+
127
+ private
128
+
129
+ def define_choice(choice, group_name, successors)
130
+ name = choice.prop_name
131
+ nsptag = choice.nsptag
132
+
133
+ define_method(name) { find(nsptag) }
134
+ define_creator(name, nsptag)
135
+ define_inserter(name, successors)
136
+ define_adder(name)
137
+
138
+ # Swapping in a different group member means clearing whichever one is
139
+ # there now; two members present at once would be invalid.
140
+ define_once("get_or_change_to_#{name}") do
141
+ find(nsptag) || begin
142
+ public_send("remove_#{group_name}")
143
+ public_send("add_#{name}")
144
+ end
145
+ end
146
+ end
147
+
148
+ def define_creator(name, nsptag)
149
+ define_once("new_#{name}") { build(nsptag) }
150
+ end
151
+
152
+ def define_inserter(name, successors)
153
+ define_once("insert_#{name}") do |child|
154
+ insert_element_before(child, *successors)
155
+ end
156
+ end
157
+
158
+ def define_adder(name)
159
+ define_once("add_#{name}") do |**attrs|
160
+ child = public_send("new_#{name}")
161
+ attrs.each { |attr, value| child.public_send("#{attr}=", value) }
162
+ public_send("insert_#{name}", child)
163
+ child
164
+ end
165
+ end
166
+
167
+ # Define +name+ unless the class (or an ancestor) already has it, so a
168
+ # hand-written override placed above the macro wins. A definition placed
169
+ # below the macro wins by simply replacing it.
170
+ def define_once(name, &)
171
+ return if method_defined?(name) || private_method_defined?(name)
172
+
173
+ define_method(name, &)
174
+ end
175
+
176
+ # "r:embed" -> "embed", "macro" -> "macro"
177
+ def attr_prop_name(xml_name)
178
+ xml_name.to_s.include?(":") ? Ns.split_tag(xml_name.to_s).last : xml_name.to_s
179
+ end
180
+ end
181
+
182
+ class Element
183
+ extend ContentModel
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "date"
5
+ require "ruby_pptx/oxml/element"
6
+ require "ruby_pptx/oxml/content_model"
7
+ require "ruby_pptx/oxml/ns"
8
+
9
+ module Pptx
10
+ module Oxml
11
+ # `cp:coreProperties`, the root of `/docProps/core.xml`.
12
+ #
13
+ # Implements the Dublin Core metadata elements. Text properties read as an
14
+ # empty string when absent and are capped at 255 characters, as the schema
15
+ # requires.
16
+ class CT_CoreProperties < Element
17
+ tag "cp:coreProperties"
18
+
19
+ TEXT_PROPERTIES = {
20
+ category: "cp:category",
21
+ content_status: "cp:contentStatus",
22
+ author: "dc:creator",
23
+ comments: "dc:description",
24
+ identifier: "dc:identifier",
25
+ keywords: "cp:keywords",
26
+ language: "dc:language",
27
+ last_modified_by: "cp:lastModifiedBy",
28
+ subject: "dc:subject",
29
+ title: "dc:title",
30
+ version: "cp:version"
31
+ }.freeze
32
+
33
+ DATETIME_PROPERTIES = {
34
+ created: "dcterms:created",
35
+ last_printed: "cp:lastPrinted",
36
+ modified: "dcterms:modified"
37
+ }.freeze
38
+
39
+ # `dcterms:created` and `dcterms:modified` must carry an explicit
40
+ # xsi:type; the others must not.
41
+ W3CDTF_TYPED = %i[created modified].freeze
42
+
43
+ MAX_TEXT_LENGTH = 255
44
+
45
+ (TEXT_PROPERTIES.values + DATETIME_PROPERTIES.values + ["cp:revision"]).each do |nsptag|
46
+ zero_or_one nsptag, successors: []
47
+ end
48
+
49
+ def self.new_element
50
+ Element.parse(%(<cp:coreProperties #{Ns.nsdecls("cp", "dc", "dcterms")}/>))
51
+ end
52
+
53
+ TEXT_PROPERTIES.each do |name, nsptag|
54
+ define_method(name) { find(nsptag)&.text.to_s }
55
+
56
+ define_method("#{name}=") do |value|
57
+ value = value.to_s
58
+ if value.length > MAX_TEXT_LENGTH
59
+ raise ArgumentError,
60
+ "#{name} exceeds the #{MAX_TEXT_LENGTH}-character limit (got #{value.length})"
61
+ end
62
+
63
+ public_send("get_or_add_#{Ns.split_tag(nsptag).last}").text = value
64
+ end
65
+ end
66
+
67
+ DATETIME_PROPERTIES.each do |name, nsptag|
68
+ define_method(name) do
69
+ raw = find(nsptag)&.text
70
+ raw.nil? || raw.empty? ? nil : CT_CoreProperties.parse_w3cdtf(raw)
71
+ end
72
+
73
+ define_method("#{name}=") do |value|
74
+ raise TypeError, "#{name} requires a Time, got #{value.class}" unless value.is_a?(Time)
75
+
76
+ element = public_send("get_or_add_#{Ns.split_tag(nsptag).last}")
77
+ element.text = value.getutc.strftime("%Y-%m-%dT%H:%M:%SZ")
78
+ if W3CDTF_TYPED.include?(name)
79
+ # PowerPoint declares xsi on the root, not on each child that uses
80
+ # it, so declare it here first and let the child find it in scope.
81
+ declare_namespace("xsi")
82
+ element.set("xsi:type", "dcterms:W3CDTF")
83
+ end
84
+ value
85
+ end
86
+ end
87
+
88
+ # A non-integer, negative or absent revision all read as 0.
89
+ def revision
90
+ raw = find("cp:revision")&.text
91
+ return 0 if raw.nil?
92
+
93
+ value = Integer(raw, 10, exception: false) || 0
94
+ value.negative? ? 0 : value
95
+ end
96
+
97
+ def revision=(value)
98
+ unless value.is_a?(Integer) && value >= 1
99
+ raise ArgumentError, "revision requires a positive Integer, got #{value.inspect}"
100
+ end
101
+
102
+ get_or_add_revision.text = value.to_s
103
+ end
104
+
105
+ # Parse a W3CDTF timestamp, which may be a year, a year-month, a date,
106
+ # or a full timestamp with a `Z` or numeric offset. Returns UTC, or nil
107
+ # when the value does not parse.
108
+ #
109
+ # Each format must consume the *whole* string. Ruby's `strptime` is happy
110
+ # to parse a prefix and discard the rest, so without that check "%Y"
111
+ # would match "2003-12-31T10:14:55" and silently yield 2003-01-01.
112
+ FORMATS = ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%d", "%Y-%m", "%Y"].freeze
113
+
114
+ def self.parse_w3cdtf(value)
115
+ parseable = value[0, 19]
116
+ offset = value[19..].to_s
117
+
118
+ parts = FORMATS.lazy.filter_map { |format| full_match(parseable, format) }.first
119
+ return nil if parts.nil?
120
+
121
+ time = Time.utc(parts[:year], parts[:mon] || 1, parts[:mday] || 1,
122
+ parts[:hour] || 0, parts[:min] || 0, parts[:sec] || 0)
123
+ apply_offset(time, offset)
124
+ end
125
+
126
+ # Parsed fields for +value+, but only if +format+ consumed all of it.
127
+ def self.full_match(value, format)
128
+ parts = Date._strptime(value, format)
129
+ parts if parts && parts[:leftover].nil? && parts[:year]
130
+ end
131
+ private_class_method :full_match
132
+
133
+ # A trailing "+02:00" means the stamp is local to that offset; shift it
134
+ # back to UTC. A "Z" or an absent offset is already UTC.
135
+ def self.apply_offset(time, offset)
136
+ match = /\A([+-])(\d\d):(\d\d)\z/.match(offset)
137
+ return time unless match
138
+
139
+ sign = match[1] == "+" ? -1 : 1
140
+ time + (sign * ((Integer(match[2], 10) * 3600) + (Integer(match[3], 10) * 60)))
141
+ end
142
+ private_class_method :apply_offset
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/oxml/element"
4
+ require "ruby_pptx/oxml/content_model"
5
+ require "ruby_pptx/oxml/simple_types"
6
+ require "ruby_pptx/enum/dml"
7
+
8
+ module Pptx
9
+ module Oxml
10
+ # The colour elements share a luminance-adjustment pair: `a:lumMod` scales
11
+ # the colour (a shade) and `a:lumOff` lightens it (a tint).
12
+ module BaseColorElement
13
+ def self.included(base)
14
+ base.class_eval do
15
+ zero_or_one "a:lumMod", successors: []
16
+ zero_or_one "a:lumOff", successors: []
17
+ end
18
+ end
19
+
20
+ def add_lumMod(value) = add_lumMod_element(value)
21
+
22
+ def add_lumOff(value) = add_lumOff_element(value)
23
+
24
+ # Remove any luminance adjustment, returning self.
25
+ def clear_lum
26
+ remove_lumMod
27
+ remove_lumOff
28
+ self
29
+ end
30
+
31
+ private
32
+
33
+ def add_lumMod_element(value)
34
+ el = get_or_add_lumMod
35
+ el.val = value
36
+ el
37
+ end
38
+
39
+ def add_lumOff_element(value)
40
+ el = get_or_add_lumOff
41
+ el.val = value
42
+ el
43
+ end
44
+ end
45
+
46
+ # `a:lumMod` and `a:lumOff`.
47
+ class CT_Percentage < Element
48
+ tag "a:lumMod", "a:lumOff"
49
+ required_attr "val", type: SimpleTypes::ST_Percentage
50
+ end
51
+
52
+ # `a:srgbClr`, a literal RGB colour.
53
+ class CT_SRgbColor < Element
54
+ include BaseColorElement
55
+
56
+ tag "a:srgbClr"
57
+ required_attr "val", type: SimpleTypes::ST_HexColorRGB
58
+ end
59
+
60
+ # `a:schemeClr`, a reference to a theme colour.
61
+ class CT_SchemeColor < Element
62
+ include BaseColorElement
63
+
64
+ tag "a:schemeClr"
65
+ required_attr "val", type: Enum::MSO_THEME_COLOR
66
+ end
67
+
68
+ # `a:hslClr`, a hue/saturation/luminance colour.
69
+ class CT_HslColor < Element
70
+ include BaseColorElement
71
+
72
+ tag "a:hslClr"
73
+ end
74
+
75
+ # `a:sysClr`, a system colour such as "windowText".
76
+ class CT_SystemColor < Element
77
+ include BaseColorElement
78
+
79
+ tag "a:sysClr"
80
+ end
81
+
82
+ # `a:prstClr`, a named preset colour.
83
+ class CT_PresetColor < Element
84
+ include BaseColorElement
85
+
86
+ tag "a:prstClr"
87
+ end
88
+
89
+ # `a:scrgbClr`, a percentage-based RGB colour.
90
+ class CT_ScRgbColor < Element
91
+ include BaseColorElement
92
+
93
+ tag "a:scrgbClr"
94
+ end
95
+
96
+ # The six-way colour choice shared by everything that carries a colour.
97
+ module ColorChoice
98
+ COLOR_CHOICES = %w[a:scrgbClr a:srgbClr a:hslClr a:sysClr a:schemeClr a:prstClr].freeze
99
+
100
+ def self.included(base)
101
+ base.class_eval do
102
+ zero_or_one_choice ColorChoice::COLOR_CHOICES.map { |t| choice(t) },
103
+ successors: [], as: :eg_colorChoice
104
+ end
105
+ end
106
+ end
107
+
108
+ # `a:fgClr` and `a:bgClr`, which are colours and nothing else.
109
+ class CT_Color < Element
110
+ include ColorChoice
111
+
112
+ tag "a:fgClr", "a:bgClr"
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/oxml/dml/color"
4
+
5
+ module Pptx
6
+ module Oxml
7
+ # `a:solidFill`, a single flat colour.
8
+ class CT_SolidColorFillProperties < Element
9
+ include ColorChoice
10
+
11
+ tag "a:solidFill"
12
+ end
13
+
14
+ # `a:noFill`, explicitly transparent.
15
+ class CT_NoFillProperties < Element
16
+ tag "a:noFill"
17
+ end
18
+
19
+ # `a:grpFill`, inheriting the group's fill.
20
+ class CT_GroupFillProperties < Element
21
+ tag "a:grpFill"
22
+ end
23
+
24
+ # `a:gs`, one stop in a gradient.
25
+ class CT_GradientStop < Element
26
+ include ColorChoice
27
+
28
+ tag "a:gs"
29
+ required_attr "pos", type: SimpleTypes::ST_PositiveFixedPercentage
30
+ end
31
+
32
+ # `a:gsLst`, the stops of a gradient.
33
+ class CT_GradientStopList < Element
34
+ tag "a:gsLst"
35
+ one_or_more "a:gs", as: :gs
36
+ end
37
+
38
+ # `a:lin`, the direction of a linear gradient.
39
+ class CT_LinearShadeProperties < Element
40
+ tag "a:lin"
41
+ optional_attr "ang", type: SimpleTypes::ST_PositiveFixedAngle
42
+ optional_attr "scaled", type: SimpleTypes::XsdBoolean
43
+ end
44
+
45
+ # `a:gradFill`, a gradient fill.
46
+ class CT_GradientFillProperties < Element
47
+ tag "a:gradFill"
48
+ zero_or_one "a:gsLst", successors: %w[a:lin a:path a:tileRect]
49
+ zero_or_one "a:lin", successors: %w[a:path a:tileRect]
50
+ zero_or_one "a:path", successors: %w[a:tileRect]
51
+
52
+ # python-pptx's default, verbatim: PowerPoint's "White" template
53
+ # gradient, two accent-1 stops on a linear path. The `a:lin` carries no
54
+ # angle, so the direction is inherited until one is set.
55
+ DEFAULT_XML = <<~XML.freeze
56
+ <a:gradFill #{Ns.nsdecls("a")} rotWithShape="1">
57
+ <a:gsLst>
58
+ <a:gs pos="0">
59
+ <a:schemeClr val="accent1">
60
+ <a:tint val="100000"/>
61
+ <a:shade val="100000"/>
62
+ <a:satMod val="130000"/>
63
+ </a:schemeClr>
64
+ </a:gs>
65
+ <a:gs pos="100000">
66
+ <a:schemeClr val="accent1">
67
+ <a:tint val="50000"/>
68
+ <a:shade val="100000"/>
69
+ <a:satMod val="350000"/>
70
+ </a:schemeClr>
71
+ </a:gs>
72
+ </a:gsLst>
73
+ <a:lin scaled="0"/>
74
+ </a:gradFill>
75
+ XML
76
+
77
+ def self.new_grad_fill(context) = context.build_from_xml(DEFAULT_XML)
78
+ end
79
+
80
+ # `a:blip`, the image reference inside a picture fill.
81
+ class CT_Blip < Element
82
+ tag "a:blip"
83
+ optional_attr "r:embed", type: SimpleTypes::ST_RelationshipId, as: :embed
84
+ zero_or_one "a:extLst", successors: []
85
+
86
+ # The Office 2016 extension that carries an SVG beside the raster the
87
+ # blip itself points at. Consumers that understand it draw the vector;
88
+ # those that do not draw the fallback.
89
+ SVG_EXT_URI = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}"
90
+
91
+ # Point this blip's SVG extension at the part related by +r_id+.
92
+ def add_svg_blip(r_id)
93
+ ext_list = get_or_add_extLst
94
+ ext_list.append(ext_list.build_from_xml(<<~XML))
95
+ <a:ext #{Ns.nsdecls("a")} uri="#{SVG_EXT_URI}">
96
+ <asvg:svgBlip #{Ns.nsdecls("asvg", "r")} r:embed="#{r_id}"/>
97
+ </a:ext>
98
+ XML
99
+ self
100
+ end
101
+
102
+ # The relationship id of the SVG, or nil when this is a plain raster.
103
+ def svg_rId = xpath("./a:extLst/a:ext/asvg:svgBlip/@r:embed").first&.value
104
+ end
105
+
106
+ # `a:extLst` as it appears on a blip.
107
+ class CT_BlipExtensionList < Element
108
+ tag "a:extLst"
109
+ zero_or_more "a:ext", as: :ext
110
+ end
111
+
112
+ # `a:srcRect`: how much of an image is cropped from each side, as a
113
+ # fraction of its size. Negative values extend past the image edge.
114
+ class CT_RelativeRect < Element
115
+ tag "a:srcRect"
116
+ optional_attr "l", type: SimpleTypes::ST_Percentage, default: 0.0
117
+ optional_attr "t", type: SimpleTypes::ST_Percentage, default: 0.0
118
+ optional_attr "r", type: SimpleTypes::ST_Percentage, default: 0.0
119
+ optional_attr "b", type: SimpleTypes::ST_Percentage, default: 0.0
120
+ end
121
+
122
+ # `a:blipFill`, a picture fill; `p:blipFill` on a picture is the same type.
123
+ class CT_BlipFillProperties < Element
124
+ tag "a:blipFill", "p:blipFill"
125
+ zero_or_one "a:blip", successors: %w[a:srcRect a:tile a:stretch]
126
+ zero_or_one "a:srcRect", successors: %w[a:tile a:stretch]
127
+ end
128
+
129
+ # `a:pattFill`, a two-colour pattern fill.
130
+ class CT_PatternFillProperties < Element
131
+ tag "a:pattFill"
132
+ zero_or_one "a:fgClr", successors: %w[a:bgClr]
133
+ zero_or_one "a:bgClr", successors: []
134
+ optional_attr "prst", type: Enum::MSO_PATTERN_TYPE
135
+ end
136
+ end
137
+ end