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,324 @@
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/oxml/dml/fill"
7
+ require "ruby_pptx/enum/text"
8
+ require "ruby_pptx/enum/lang"
9
+
10
+ module Pptx
11
+ module Oxml
12
+ # The fill choice and its successor list, shared by run properties and
13
+ # shape properties.
14
+ FILL_CHOICES = %w[a:noFill a:solidFill a:gradFill a:blipFill a:pattFill a:grpFill].freeze
15
+
16
+ # `a:t`, the literal text of a run.
17
+ class CT_TextCharacterText < Element
18
+ tag "a:t"
19
+ end
20
+
21
+ # `a:r`, a run: a span of text sharing one set of character properties.
22
+ class CT_RegularTextRun < Element
23
+ tag "a:r"
24
+ zero_or_one "a:rPr", successors: %w[a:t]
25
+ one_and_only_one "a:t"
26
+
27
+ def text = t.text
28
+
29
+ def text=(value)
30
+ t.text = CT_RegularTextRun.escape_control_chars(value.to_s)
31
+ end
32
+
33
+ # XML 1.0 cannot carry most control characters, so PowerPoint writes
34
+ # them as a literal escape like "_x0007_". Tab and line-feed are legal
35
+ # and pass through unchanged.
36
+ def self.escape_control_chars(string)
37
+ string.gsub(/([\x00-\x08\x0B-\x1F])/) { format("_x%04X_", ::Regexp.last_match(1).ord) }
38
+ end
39
+ end
40
+
41
+ # `a:br`, a soft line break within a paragraph.
42
+ class CT_TextLineBreak < Element
43
+ tag "a:br"
44
+ zero_or_one "a:rPr", successors: []
45
+
46
+ # A break carries no text of its own; it *is* the line feed, which we
47
+ # report as a vertical tab to match how PowerPoint puts it on the
48
+ # clipboard.
49
+ def text = "\v"
50
+ end
51
+
52
+ # `a:fld`, a slide-number or date field.
53
+ class CT_TextField < Element
54
+ tag "a:fld"
55
+ zero_or_one "a:rPr", successors: %w[a:pPr a:t]
56
+ zero_or_one "a:t", successors: []
57
+
58
+ def text = t&.text.to_s
59
+ end
60
+
61
+ # `a:latin`, `a:ea`, `a:cs` and `a:sym`: a typeface reference.
62
+ class CT_TextFont < Element
63
+ tag "a:latin", "a:ea", "a:cs", "a:sym"
64
+ required_attr "typeface", type: SimpleTypes::ST_TextTypeface
65
+ end
66
+
67
+ # `a:rPr`, `a:defRPr` and `a:endParaRPr`: character properties.
68
+ class CT_TextCharacterProperties < Element
69
+ tag "a:rPr", "a:defRPr", "a:endParaRPr"
70
+
71
+ FILL_SUCCESSORS = %w[a:effectLst a:effectDag a:highlight a:uLnTx a:uLn
72
+ a:uFillTx a:uFill a:latin a:ea a:cs a:sym
73
+ a:hlinkClick a:hlinkMouseOver a:rtl a:extLst].freeze
74
+
75
+ zero_or_one_choice FILL_CHOICES.map { |t| choice(t) },
76
+ successors: FILL_SUCCESSORS, as: :eg_fillProperties
77
+ zero_or_one "a:latin",
78
+ successors: %w[a:ea a:cs a:sym a:hlinkClick a:hlinkMouseOver a:rtl a:extLst]
79
+ zero_or_one "a:hlinkClick", successors: %w[a:hlinkMouseOver a:rtl a:extLst]
80
+
81
+ optional_attr "lang", type: Enum::MSO_LANGUAGE_ID
82
+ optional_attr "sz", type: SimpleTypes::ST_TextFontSize
83
+ optional_attr "b", type: SimpleTypes::XsdBoolean
84
+ optional_attr "i", type: SimpleTypes::XsdBoolean
85
+ optional_attr "u", type: Enum::MSO_TEXT_UNDERLINE_TYPE
86
+
87
+ def new_gradFill = CT_GradientFillProperties.new_grad_fill(self)
88
+ end
89
+
90
+ # `a:bodyPr`, the properties of a text body: margins, anchoring, autofit.
91
+ class CT_TextBodyProperties < Element
92
+ tag "a:bodyPr"
93
+
94
+ zero_or_one_choice [choice("a:noAutofit"), choice("a:normAutofit"), choice("a:spAutoFit")],
95
+ successors: %w[a:scene3d a:sp3d a:flatTx a:extLst], as: :eg_textAutoFit
96
+
97
+ optional_attr "lIns", type: SimpleTypes::ST_Coordinate32, default: Pptx::Length.emu(91_440)
98
+ optional_attr "tIns", type: SimpleTypes::ST_Coordinate32, default: Pptx::Length.emu(45_720)
99
+ optional_attr "rIns", type: SimpleTypes::ST_Coordinate32, default: Pptx::Length.emu(91_440)
100
+ optional_attr "bIns", type: SimpleTypes::ST_Coordinate32, default: Pptx::Length.emu(45_720)
101
+ optional_attr "anchor", type: Enum::MSO_VERTICAL_ANCHOR
102
+ optional_attr "wrap", type: SimpleTypes::ST_TextWrappingType
103
+
104
+ # @return [Pptx::Enum::MSO_AUTO_SIZE, nil] nil when inherited
105
+ def autofit
106
+ return Enum::MSO_AUTO_SIZE::NONE unless find("a:noAutofit").nil?
107
+ return Enum::MSO_AUTO_SIZE::TEXT_TO_FIT_SHAPE unless find("a:normAutofit").nil?
108
+ return Enum::MSO_AUTO_SIZE::SHAPE_TO_FIT_TEXT unless find("a:spAutoFit").nil?
109
+
110
+ nil
111
+ end
112
+
113
+ def autofit=(value)
114
+ remove_eg_textAutoFit
115
+ return if value.nil?
116
+
117
+ member = Enum::MSO_AUTO_SIZE.fetch(value)
118
+ case member.name
119
+ when :NONE then add_noAutofit
120
+ when :TEXT_TO_FIT_SHAPE then add_normAutofit
121
+ when :SHAPE_TO_FIT_TEXT then add_spAutoFit
122
+ else
123
+ raise ArgumentError, "#{member} cannot be assigned as an autofit setting"
124
+ end
125
+ end
126
+ end
127
+
128
+ # `a:normAutofit`, shrink-text-on-overflow with its font scale.
129
+ class CT_TextNormalAutofit < Element
130
+ tag "a:normAutofit"
131
+ optional_attr "fontScale", type: SimpleTypes::ST_TextFontScalePercentOrPercentString,
132
+ default: 100.0
133
+ end
134
+
135
+ # `a:spcPct`, spacing as a multiple of line height.
136
+ class CT_TextSpacingPercent < Element
137
+ tag "a:spcPct"
138
+ required_attr "val", type: SimpleTypes::ST_TextSpacingPercentOrPercentString
139
+ end
140
+
141
+ # `a:spcPts`, spacing as a fixed distance.
142
+ class CT_TextSpacingPoint < Element
143
+ tag "a:spcPts"
144
+ required_attr "val", type: SimpleTypes::ST_TextSpacingPoint
145
+ end
146
+
147
+ # `a:lnSpc`, `a:spcBef` and `a:spcAft`: a spacing value, expressed either
148
+ # as a multiple of line height or as a fixed distance.
149
+ class CT_TextSpacing < Element
150
+ tag "a:lnSpc", "a:spcBef", "a:spcAft"
151
+ # Strictly a one-and-only-one choice; modelling it as two optional
152
+ # children is simpler and the setters keep only one present.
153
+ zero_or_one "a:spcPct", successors: []
154
+ zero_or_one "a:spcPts", successors: []
155
+
156
+ # Spacing as a multiple of line height, e.g. 1.75 lines.
157
+ def set_spc_pct(value)
158
+ remove_spcPts
159
+ get_or_add_spcPct.val = value
160
+ end
161
+
162
+ # Spacing as a fixed distance.
163
+ def set_spc_pts(value)
164
+ remove_spcPct
165
+ get_or_add_spcPts.val = value
166
+ end
167
+ end
168
+
169
+ # `a:pPr`, paragraph properties.
170
+ class CT_TextParagraphProperties < Element
171
+ tag "a:pPr"
172
+ TAG_SEQ = %w[a:lnSpc a:spcBef a:spcAft a:buClrTx a:buClr a:buSzTx a:buSzPct
173
+ a:buSzPts a:buFontTx a:buFont a:buNone a:buAutoNum a:buChar
174
+ a:buBlip a:tabLst a:defRPr a:extLst].freeze
175
+
176
+ zero_or_one "a:lnSpc", successors: TAG_SEQ[1..]
177
+ zero_or_one "a:spcBef", successors: TAG_SEQ[2..]
178
+ zero_or_one "a:spcAft", successors: TAG_SEQ[3..]
179
+ zero_or_one "a:defRPr", successors: TAG_SEQ[16..]
180
+ optional_attr "lvl", type: SimpleTypes::ST_TextIndentLevelType, default: 0
181
+ optional_attr "algn", type: Enum::PP_PARAGRAPH_ALIGNMENT
182
+
183
+ # A Float means a number of lines; a {Pptx::Length} means a fixed
184
+ # distance. nil when no spacing is set.
185
+ def line_spacing
186
+ spacing = lnSpc
187
+ return nil if spacing.nil?
188
+
189
+ spacing.spcPts ? spacing.spcPts.val : spacing.spcPct&.val
190
+ end
191
+
192
+ def line_spacing=(value)
193
+ remove_lnSpc
194
+ return if value.nil?
195
+
196
+ spacing = get_or_add_lnSpc
197
+ value.is_a?(Pptx::Length) ? spacing.set_spc_pts(value) : spacing.set_spc_pct(value)
198
+ end
199
+
200
+ def space_before = spcBef&.spcPts&.val
201
+
202
+ def space_before=(value)
203
+ remove_spcBef
204
+ get_or_add_spcBef.set_spc_pts(value) unless value.nil?
205
+ end
206
+
207
+ def space_after = spcAft&.spcPts&.val
208
+
209
+ def space_after=(value)
210
+ remove_spcAft
211
+ get_or_add_spcAft.set_spc_pts(value) unless value.nil?
212
+ end
213
+ end
214
+
215
+ # `a:p`, a paragraph.
216
+ class CT_TextParagraph < Element
217
+ tag "a:p"
218
+ zero_or_one "a:pPr", successors: %w[a:r a:br a:fld a:endParaRPr]
219
+ zero_or_more "a:r", successors: %w[a:endParaRPr], as: :r
220
+ zero_or_more "a:br", successors: %w[a:endParaRPr], as: :br
221
+ zero_or_one "a:endParaRPr", successors: []
222
+
223
+ CONTENT_TAGS = %w[a:r a:br a:fld].freeze
224
+
225
+ # A run is created with its required `a:t` already in place.
226
+ def new_r = build_from_xml(%(<a:r #{Ns.nsdecls("a")}><a:t/></a:r>))
227
+
228
+ def add_run(text = nil)
229
+ run = add_r
230
+ run.text = text if text && !text.empty?
231
+ run
232
+ end
233
+
234
+ def add_line_break = add_br
235
+
236
+ # Append +text+ as runs, turning each "\n" or "\v" into a line break.
237
+ #
238
+ # Breaks go *between* items, so leading text never produces one, and an
239
+ # empty segment contributes no run.
240
+ def append_text(text)
241
+ text.to_s.split(/[\n\v]/, -1).each_with_index do |segment, index|
242
+ add_line_break if index.positive?
243
+ add_run(segment) unless segment.empty?
244
+ end
245
+ self
246
+ end
247
+
248
+ # The `a:r`, `a:br` and `a:fld` children, in document order.
249
+ def content_children
250
+ tags = CONTENT_TAGS.map { |t| Ns.qn(t) }
251
+ @node.element_children
252
+ .select { |c| tags.include?(Ns.clark_name_of(c)) }
253
+ .map { |c| Element.wrap(c) }
254
+ end
255
+
256
+ def text = content_children.map(&:text).join
257
+ end
258
+
259
+ # `p:txBody`, `a:txBody` and `c:txPr`: a body of text.
260
+ class CT_TextBody < Element
261
+ # `c:rich` is a text body under a different name: it is what a chart or
262
+ # axis title holds.
263
+ tag "p:txBody", "a:txBody", "c:txPr", "c:rich"
264
+ one_and_only_one "a:bodyPr"
265
+ one_or_more "a:p", successors: [], as: :p
266
+
267
+ # A chart's `c:txPr` holds a paragraph for its formatting alone; the
268
+ # default run properties of that first paragraph are where a chart,
269
+ # axis, legend or data-label font lives.
270
+ def defRPr = p_list.first.get_or_add_pPr.get_or_add_defRPr
271
+
272
+ # The `c:txPr` a chart element gains when its font is first set.
273
+ TXPR_XML = <<~XML.freeze
274
+ <c:txPr #{Ns.nsdecls("c", "a")}>
275
+ <a:bodyPr/>
276
+ <a:lstStyle/>
277
+ <a:p>
278
+ <a:pPr>
279
+ <a:defRPr/>
280
+ </a:pPr>
281
+ </a:p>
282
+ </c:txPr>
283
+ XML
284
+
285
+ class << self
286
+ # A `p:txBody` with one empty paragraph, built in +context+'s document.
287
+ def new_element(context) = context.build_from_xml(txbody_xml)
288
+
289
+ # A standalone `p:txBody`, for callers with no element to build from.
290
+ def parse_new = Element.parse(txbody_xml)
291
+
292
+ def txbody_xml
293
+ <<~XML
294
+ <p:txBody #{Ns.nsdecls("a", "p")}>
295
+ <a:bodyPr/>
296
+ <a:lstStyle/>
297
+ <a:p/>
298
+ </p:txBody>
299
+ XML
300
+ end
301
+ end
302
+
303
+ # Remove every paragraph, leaving any other children alone.
304
+ def clear_content
305
+ p_list.each { |paragraph| remove(paragraph) }
306
+ self
307
+ end
308
+
309
+ # Restore the minimum the schema requires: at least one paragraph.
310
+ def unclear_content
311
+ add_p if p_list.empty?
312
+ self
313
+ end
314
+
315
+ # True when the body holds a single empty paragraph.
316
+ def empty?
317
+ paragraphs = p_list
318
+ raise InvalidXmlError, "a text body must have at least one a:p" if paragraphs.empty?
319
+
320
+ paragraphs.size == 1 && paragraphs.first.text.empty?
321
+ end
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,93 @@
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/oxml/dml/color"
7
+ require "ruby_pptx/oxml/text"
8
+
9
+ module Pptx
10
+ module Oxml
11
+ # `a:theme`, the root of a theme part.
12
+ #
13
+ # Only the parts a caller can reasonably edit are modelled: the colour
14
+ # scheme and the font scheme. The format scheme -- fills, lines, effects
15
+ # and background fills, keyed by the `idx` values that shapes and slide
16
+ # backgrounds refer to -- is carried through from the base theme unchanged.
17
+ # See PORTING.md for why.
18
+ class CT_OfficeStyleSheet < Element
19
+ tag "a:theme"
20
+ one_and_only_one "a:themeElements"
21
+ required_attr "name", type: SimpleTypes::XsdString
22
+
23
+ def self.new_default = Oxml.parse_from_template("theme")
24
+
25
+ def clrScheme = themeElements.clrScheme
26
+ def fontScheme = themeElements.fontScheme
27
+ end
28
+
29
+ # `a:themeElements`, the four schemes that make up a theme.
30
+ class CT_BaseStyles < Element
31
+ tag "a:themeElements"
32
+ one_and_only_one "a:clrScheme"
33
+ one_and_only_one "a:fontScheme"
34
+ one_and_only_one "a:fmtScheme"
35
+ end
36
+
37
+ # `a:clrScheme`, the twelve theme colours.
38
+ class CT_ColorScheme < Element
39
+ tag "a:clrScheme"
40
+ required_attr "name", type: SimpleTypes::XsdString
41
+
42
+ # In schema order, which is also the order PowerPoint shows them in.
43
+ SLOTS = %w[dk1 lt1 dk2 lt2 accent1 accent2 accent3 accent4 accent5
44
+ accent6 hlink folHlink].freeze
45
+
46
+ SLOTS.each do |slot|
47
+ one_and_only_one "a:#{slot}"
48
+ end
49
+
50
+ def slot(name)
51
+ raise NotFoundError, "no theme colour named #{name}" unless SLOTS.include?(name.to_s)
52
+
53
+ send(name.to_s)
54
+ end
55
+ end
56
+
57
+ # One slot of a colour scheme, such as `a:accent1`.
58
+ #
59
+ # Every slot holds the same six-way colour choice as any other colour in
60
+ # DrawingML, so {Pptx::ColorFormat} drives them unchanged.
61
+ class CT_ThemeColor < Element
62
+ include ColorChoice
63
+
64
+ tag(*CT_ColorScheme::SLOTS.map { |slot| "a:#{slot}" })
65
+ end
66
+
67
+ # `a:fontScheme`, the major (heading) and minor (body) font collections.
68
+ class CT_FontScheme < Element
69
+ tag "a:fontScheme"
70
+ required_attr "name", type: SimpleTypes::XsdString
71
+ one_and_only_one "a:majorFont"
72
+ one_and_only_one "a:minorFont"
73
+ end
74
+
75
+ # `a:majorFont` and `a:minorFont`.
76
+ #
77
+ # The Latin typeface is the one that matters here; the East Asian and
78
+ # complex-script slots are required by the schema and are left as the base
79
+ # theme has them unless set.
80
+ class CT_FontCollection < Element
81
+ tag "a:majorFont", "a:minorFont"
82
+ one_and_only_one "a:latin"
83
+ one_and_only_one "a:ea"
84
+ one_and_only_one "a:cs"
85
+
86
+ def typeface = latin.typeface
87
+
88
+ def typeface=(value)
89
+ latin.typeface = value
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/opc/package"
4
+ require "ruby_pptx/opc/constants"
5
+ require "ruby_pptx/parts/core_properties"
6
+ require "ruby_pptx/parts/media"
7
+ require "ruby_pptx/parts/presentation"
8
+ require "ruby_pptx/parts/slide"
9
+ require "ruby_pptx/parts/theme"
10
+
11
+ module Pptx
12
+ # A .pptx package: an OPC package that knows about PowerPoint's parts.
13
+ class Package < Opc::OpcPackage
14
+ # The core-properties part, created if the package has none.
15
+ def core_properties
16
+ @core_properties ||= begin
17
+ part_related_by(Opc::RELATIONSHIP_TYPE::CORE_PROPERTIES)
18
+ rescue NotFoundError
19
+ Parts::CorePropertiesPart.default(self).tap do |part|
20
+ relate_to(part, Opc::RELATIONSHIP_TYPE::CORE_PROPERTIES)
21
+ end
22
+ end
23
+ end
24
+
25
+ def presentation_part = main_document_part
26
+
27
+ # The media part holding +video+, created if the package has no part with
28
+ # the same content. Matched by SHA-1, as images are.
29
+ def get_or_add_media_part(video)
30
+ find_media_part(video.sha1) || Parts::MediaPart.new_media(self, video)
31
+ end
32
+
33
+ # The image part holding +image_file+, created if the package has no part
34
+ # with the same content.
35
+ #
36
+ # Images are matched by SHA-1 of their bytes, so the same picture used on
37
+ # several slides is stored once.
38
+ def get_or_add_image_part(image_file)
39
+ image = Image.from_file(image_file)
40
+ find_image_part(image.sha1) || Parts::ImagePart.new_image(self, image)
41
+ end
42
+
43
+ # The next free `/ppt/media/imageN.<ext>` partname, reusing gaps.
44
+ def next_image_partname(ext) = next_media_like_partname("image", ext)
45
+
46
+ # The next free `/ppt/media/mediaN.<ext>` partname, reusing gaps.
47
+ def next_media_partname(ext) = next_media_like_partname("media", ext)
48
+
49
+ private
50
+
51
+ def find_media_part(sha1)
52
+ media_parts.find { |part| part.respond_to?(:sha1) && part.sha1 == sha1 }
53
+ end
54
+
55
+ # Parts reached by a media relationship, each once.
56
+ def media_parts
57
+ seen = {}.compare_by_identity
58
+ each_rel.filter_map do |rel|
59
+ next if rel.external? || rel.reltype != Opc::RELATIONSHIP_TYPE::MEDIA
60
+
61
+ part = rel.target_part
62
+ next if seen.key?(part)
63
+
64
+ seen[part] = true
65
+ part
66
+ end
67
+ end
68
+
69
+ def find_image_part(sha1)
70
+ image_parts.find { |part| part.respond_to?(:sha1) && part.sha1 == sha1 }
71
+ end
72
+
73
+ # Parts reached by an image relationship, each once.
74
+ #
75
+ # Scoped by relationship rather than by class on purpose: the package
76
+ # thumbnail is also an image part, but it is related as a thumbnail, and
77
+ # matching a new picture against it would relate the picture by the wrong
78
+ # reltype.
79
+ def image_parts
80
+ seen = {}.compare_by_identity
81
+ each_rel.filter_map do |rel|
82
+ next if rel.external? || rel.reltype != Opc::RELATIONSHIP_TYPE::IMAGE
83
+
84
+ part = rel.target_part
85
+ next if seen.key?(part)
86
+
87
+ seen[part] = true
88
+ part
89
+ end
90
+ end
91
+
92
+ def next_media_like_partname(stem, ext)
93
+ prefix = "/ppt/media/#{stem}"
94
+ indexes = parts.filter_map do |part|
95
+ name = part.partname.to_s
96
+ part.partname.idx if name.start_with?(prefix)
97
+ end.sort
98
+
99
+ idx = indexes.each_with_index.find { |used, i| (i + 1) < used }&.last&.succ ||
100
+ (indexes.size + 1)
101
+ Opc::PackURI.new("#{prefix}#{idx}.#{ext}")
102
+ end
103
+ end
104
+ end
105
+
106
+ # Register the part classes for the content types this library models. Anything
107
+ # not listed here loads as a plain Opc::Part, which is enough to carry it
108
+ # through a round-trip untouched.
109
+ {
110
+ Pptx::Opc::CONTENT_TYPE::PML_PRESENTATION_MAIN => Pptx::Parts::PresentationPart,
111
+ Pptx::Opc::CONTENT_TYPE::PML_PRES_MACRO_MAIN => Pptx::Parts::PresentationPart,
112
+ Pptx::Opc::CONTENT_TYPE::PML_SLIDE => Pptx::Parts::SlidePart,
113
+ Pptx::Opc::CONTENT_TYPE::PML_SLIDE_LAYOUT => Pptx::Parts::SlideLayoutPart,
114
+ Pptx::Opc::CONTENT_TYPE::PML_SLIDE_MASTER => Pptx::Parts::SlideMasterPart,
115
+ Pptx::Opc::CONTENT_TYPE::PML_NOTES_MASTER => Pptx::Parts::NotesMasterPart,
116
+ Pptx::Opc::CONTENT_TYPE::OFC_THEME => Pptx::Parts::ThemePart,
117
+ Pptx::Opc::CONTENT_TYPE::PML_NOTES_SLIDE => Pptx::Parts::NotesSlidePart,
118
+ Pptx::Opc::CONTENT_TYPE::OPC_CORE_PROPERTIES => Pptx::Parts::CorePropertiesPart,
119
+ Pptx::Opc::CONTENT_TYPE::PNG => Pptx::Parts::ImagePart,
120
+ Pptx::Opc::CONTENT_TYPE::JPEG => Pptx::Parts::ImagePart,
121
+ Pptx::Opc::CONTENT_TYPE::GIF => Pptx::Parts::ImagePart,
122
+ Pptx::Opc::CONTENT_TYPE::BMP => Pptx::Parts::ImagePart,
123
+ Pptx::Opc::CONTENT_TYPE::TIFF => Pptx::Parts::ImagePart,
124
+ Pptx::Opc::CONTENT_TYPE::MS_PHOTO => Pptx::Parts::ImagePart,
125
+ Pptx::Opc::CONTENT_TYPE::X_EMF => Pptx::Parts::ImagePart,
126
+ Pptx::Opc::CONTENT_TYPE::X_WMF => Pptx::Parts::ImagePart,
127
+ Pptx::Opc::CONTENT_TYPE::DML_CHART => Pptx::Parts::ChartPart,
128
+ Pptx::Opc::CONTENT_TYPE::SML_SHEET => Pptx::Parts::EmbeddedXlsxPart,
129
+ Pptx::Opc::CONTENT_TYPE::ASF => Pptx::Parts::MediaPart,
130
+ Pptx::Opc::CONTENT_TYPE::AVI => Pptx::Parts::MediaPart,
131
+ Pptx::Opc::CONTENT_TYPE::MOV => Pptx::Parts::MediaPart,
132
+ Pptx::Opc::CONTENT_TYPE::MP4 => Pptx::Parts::MediaPart,
133
+ Pptx::Opc::CONTENT_TYPE::MPG => Pptx::Parts::MediaPart,
134
+ Pptx::Opc::CONTENT_TYPE::MS_VIDEO => Pptx::Parts::MediaPart,
135
+ Pptx::Opc::CONTENT_TYPE::SWF => Pptx::Parts::MediaPart,
136
+ Pptx::Opc::CONTENT_TYPE::VIDEO => Pptx::Parts::MediaPart,
137
+ Pptx::Opc::CONTENT_TYPE::WMV => Pptx::Parts::MediaPart,
138
+ Pptx::Opc::CONTENT_TYPE::X_MS_VIDEO => Pptx::Parts::MediaPart
139
+ }.each { |content_type, part_class| Pptx::Opc::PartFactory.register(content_type, part_class) }
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/opc/package"
4
+ require "ruby_pptx/opc/constants"
5
+ require "ruby_pptx/oxml/chart"
6
+ require "ruby_pptx/chart/chart"
7
+
8
+ module Pptx
9
+ module Parts
10
+ # Another OPC package embedded inside this one -- here, the Excel workbook
11
+ # that backs a chart.
12
+ class EmbeddedXlsxPart < Opc::Part
13
+ PARTNAME_TEMPLATE = "/ppt/embeddings/Microsoft_Excel_Sheet%d.xlsx"
14
+
15
+ def self.new_xlsx(blob, package)
16
+ new(package.next_partname(PARTNAME_TEMPLATE),
17
+ Opc::CONTENT_TYPE::SML_SHEET, package, blob)
18
+ end
19
+ end
20
+
21
+ # A chart part, `/ppt/charts/chartN.xml`.
22
+ class ChartPart < Opc::XmlPart
23
+ PARTNAME_TEMPLATE = "/ppt/charts/chart%d.xml"
24
+
25
+ # A new chart part depicting +chart_data+, together with the embedded
26
+ # workbook that backs it.
27
+ def self.new_chart(chart_type, chart_data, package)
28
+ part = load(package.next_partname(PARTNAME_TEMPLATE), Opc::CONTENT_TYPE::DML_CHART,
29
+ package, ChartXmlWriter.write(chart_type, chart_data))
30
+ part.workbook.replace_with(chart_data.xlsx_blob)
31
+ part
32
+ end
33
+
34
+ # A chart part drawing several plots over the same categories.
35
+ def self.new_combo_chart(builder, package)
36
+ part = load(package.next_partname(PARTNAME_TEMPLATE), Opc::CONTENT_TYPE::DML_CHART,
37
+ package, ComboChartWriter.new(builder.validate!).xml)
38
+ part.workbook.replace_with(builder.chart_data.xlsx_blob)
39
+ part
40
+ end
41
+
42
+ def chart = @chart ||= Chart.new(element, self)
43
+
44
+ def workbook = @workbook ||= ChartWorkbook.new(element, self)
45
+ end
46
+
47
+ # The embedded workbook behind a chart.
48
+ class ChartWorkbook
49
+ def initialize(chart_space, chart_part)
50
+ @chart_space = chart_space
51
+ @chart_part = chart_part
52
+ end
53
+
54
+ # @return [EmbeddedXlsxPart, nil]
55
+ def xlsx_part
56
+ r_id = @chart_space.xlsx_part_rId
57
+ r_id && @chart_part.related_part(r_id)
58
+ end
59
+
60
+ # Put +xlsx_blob+ behind this chart, adding the part and the
61
+ # `c:externalData` link if the chart does not have one yet.
62
+ def replace_with(xlsx_blob)
63
+ existing = xlsx_part
64
+ return existing.blob = xlsx_blob if existing
65
+
66
+ part = EmbeddedXlsxPart.new_xlsx(xlsx_blob, @chart_part.package)
67
+ r_id = @chart_part.relate_to(part, Opc::RELATIONSHIP_TYPE::PACKAGE)
68
+ external_data = @chart_space.get_or_add_externalData
69
+ external_data.rId = r_id
70
+ external_data.ensure_auto_update
71
+ part
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/opc/package"
4
+ require "ruby_pptx/opc/constants"
5
+ require "ruby_pptx/oxml/core_properties"
6
+
7
+ module Pptx
8
+ module Parts
9
+ # `/docProps/core.xml`, the Dublin Core metadata for the package.
10
+ #
11
+ # Reads and writes are delegated straight to the root element, so this
12
+ # class is mostly a named home for them.
13
+ class CorePropertiesPart < Opc::XmlPart
14
+ DELEGATED = (Oxml::CT_CoreProperties::TEXT_PROPERTIES.keys +
15
+ Oxml::CT_CoreProperties::DATETIME_PROPERTIES.keys +
16
+ [:revision]).freeze
17
+
18
+ DELEGATED.each do |name|
19
+ define_method(name) { element.public_send(name) }
20
+ define_method("#{name}=") { |value| element.public_send("#{name}=", value) }
21
+ end
22
+
23
+ # A core-properties part for a package that has none, matching what
24
+ # PowerPoint would write for a new presentation.
25
+ def self.default(package)
26
+ part = new(
27
+ Opc::PackURI.new("/docProps/core.xml"),
28
+ Opc::CONTENT_TYPE::OPC_CORE_PROPERTIES,
29
+ package,
30
+ Oxml::CT_CoreProperties.new_element
31
+ )
32
+ part.title = "PowerPoint Presentation"
33
+ part.last_modified_by = "ruby_pptx"
34
+ part.revision = 1
35
+ part.modified = Time.now.utc
36
+ part
37
+ end
38
+ end
39
+ end
40
+ end