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,430 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/pattern_matching"
4
+
5
+ require "ruby_pptx/element_proxy"
6
+ require "ruby_pptx/shapes/shape_tree"
7
+ require "ruby_pptx/dml/fill"
8
+
9
+ module Pptx
10
+ # Behaviour common to slides, layouts, masters and notes slides.
11
+ class BaseSlide < PartElementProxy
12
+ # The internal name of this slide; an empty string when unnamed.
13
+ def name = @element.cSld.name
14
+
15
+ def name=(value)
16
+ @element.cSld.name = value.to_s
17
+ end
18
+
19
+ def shape_tree = @element.spTree
20
+
21
+ # The slide background.
22
+ #
23
+ # Note that merely reading this is destructive: a background given by a
24
+ # style reference, or inherited, is replaced with an explicit no-fill so
25
+ # there is something to interrogate. python-pptx behaves the same way.
26
+ def background = @background ||= Background.new(@element.cSld)
27
+ end
28
+
29
+ # The background of a slide, layout or master.
30
+ class Background < ElementProxy
31
+ def fill = @fill ||= FillFormat.from_fill_parent(@element.get_or_add_bgPr)
32
+ end
33
+
34
+ # Common to slide masters and the notes master.
35
+ class BaseMaster < BaseSlide
36
+ # The shapes on this master.
37
+ def shapes = @shapes ||= MasterShapes.new(@element.spTree, self)
38
+
39
+ # The placeholders on this master, in `idx` order.
40
+ def placeholders = @placeholders ||= MasterPlaceholders.new(@element.spTree, self)
41
+ end
42
+
43
+ # The master every notes page in a presentation inherits from.
44
+ #
45
+ # There is at most one per presentation, and it is created on first use --
46
+ # the first time a slide is given speaker notes, or {Presentation#notes_master}
47
+ # is asked for.
48
+ class NotesMaster < BaseMaster
49
+ def inspect = "#<Pptx::NotesMaster #{part.partname}>"
50
+ end
51
+
52
+ # The speaker notes page belonging to one slide.
53
+ #
54
+ # slide.notes_slide.notes_text_frame.text = "Mention the Q3 numbers"
55
+ # slide.notes = "Mention the Q3 numbers" # the same, shorter
56
+ class NotesSlide < BaseSlide
57
+ # Placeholder types copied from the notes master onto a new notes slide.
58
+ # The header, date and footer stay on the master, as they do in PowerPoint.
59
+ CLONEABLE = %i[SLIDE_IMAGE BODY SLIDE_NUMBER].freeze
60
+
61
+ def shapes = @shapes ||= NotesSlideShapes.new(@element.spTree, self)
62
+
63
+ def placeholders = @placeholders ||= NotesSlidePlaceholders.new(@element.spTree, self)
64
+
65
+ # The body placeholder that holds the notes text, or nil if it was removed.
66
+ def notes_placeholder = placeholders.find { |ph| ph.placeholder_format.type.name == :BODY }
67
+
68
+ # The text frame of the notes placeholder, or nil when there is none.
69
+ #
70
+ # @return [TextFrame, nil]
71
+ def notes_text_frame = notes_placeholder&.text_frame
72
+
73
+ # Copy the cloneable placeholders from +notes_master+, keeping their order.
74
+ # Called once, when the notes slide is created.
75
+ def clone_master_placeholders(notes_master)
76
+ notes_master.shapes.each do |shape|
77
+ next unless shape.placeholder?
78
+ next unless CLONEABLE.include?(shape.element.ph_type.name)
79
+
80
+ shapes.clone_placeholder(shape)
81
+ end
82
+ self
83
+ end
84
+
85
+ def inspect = "#<Pptx::NotesSlide #{part.partname}>"
86
+ end
87
+
88
+ # One slide in a presentation.
89
+ class Slide < BaseSlide
90
+ include PatternMatching
91
+
92
+ pattern_keys :slide_id, :name, :layout, :shapes, :placeholders
93
+
94
+ # The id that identifies this slide within the presentation, stable across
95
+ # reordering.
96
+ def slide_id = part.slide_id
97
+
98
+ # The layout this slide takes its appearance from.
99
+ def layout = part.slide_layout
100
+
101
+ # True when this slide inherits the master's background.
102
+ def follows_master_background? = @element.bg.nil?
103
+
104
+ def notes_slide? = part.notes_slide?
105
+
106
+ # This slide's speaker notes page, created on first use.
107
+ #
108
+ # Use {#notes_slide?} to ask whether there is one without creating it, or
109
+ # {#notes} and {#notes=} for the common case of just the text.
110
+ #
111
+ # @return [NotesSlide]
112
+ def notes_slide = part.notes_slide
113
+
114
+ # The speaker notes as plain text, or nil when the slide has none.
115
+ #
116
+ # Reading never creates a notes slide; that would add parts to a file
117
+ # merely by looking at it.
118
+ def notes = notes_slide? ? notes_slide.notes_text_frame&.text : nil
119
+
120
+ # Replace the speaker notes, creating the notes slide if need be.
121
+ def notes=(text)
122
+ frame = notes_slide.notes_text_frame
123
+ raise Error, "this slide's notes page has no notes placeholder" if frame.nil?
124
+
125
+ frame.text = text
126
+ end
127
+
128
+ # The shapes on this slide, in z-order.
129
+ def shapes = @shapes ||= SlideShapes.new(@element.spTree, self)
130
+
131
+ # The placeholders on this slide, keyed by `idx`.
132
+ def placeholders = @placeholders ||= SlidePlaceholders.new(@element.spTree, self)
133
+
134
+ def inspect = "#<Pptx::Slide id=#{slide_id} #{part.partname}>"
135
+ end
136
+
137
+ # The slides of a presentation, in order.
138
+ #
139
+ # Enumerable, and indexable by position.
140
+ class Slides < ParentedElementProxy
141
+ include DeconstructToArray
142
+
143
+ include Enumerable
144
+
145
+ def initialize(sld_id_list, presentation)
146
+ super
147
+ @sld_id_list = sld_id_list
148
+ end
149
+
150
+ def each
151
+ return enum_for(:each) { size } unless block_given?
152
+
153
+ @sld_id_list.sldId_list.each { |sld_id| yield part.related_slide(sld_id.rId) }
154
+ self
155
+ end
156
+
157
+ def size = @sld_id_list.size
158
+ alias length size
159
+
160
+ def empty? = size.zero?
161
+
162
+ # Indexed access, supporting a negative index as Ruby arrays do.
163
+ #
164
+ # @return [Slide, nil] nil when +index+ is out of range
165
+ def [](index, length = nil)
166
+ slice_members(@sld_id_list.sldId_list, index, length) { |entry| part.related_slide(entry.rId) }
167
+ end
168
+
169
+ # As {#[]}, but raises rather than returning nil.
170
+ def fetch(index)
171
+ self[index] || raise(IndexError, "slide index #{index} out of range")
172
+ end
173
+
174
+ # Enumerable gives us #first but not #last.
175
+ def last = self[-1]
176
+
177
+ # The slide with the given slide id.
178
+ #
179
+ # @return [Slide, nil]
180
+ def by_id(slide_id) = part.slide_by_id(slide_id)
181
+
182
+ # Add a slide inheriting from +slide_layout+, and return it.
183
+ #
184
+ # The layout's placeholders are copied onto the new slide, preserving
185
+ # z-order; latent ones are left to the layout.
186
+ def add(slide_layout)
187
+ r_id, slide = part.add_slide(slide_layout)
188
+ slide.shapes.clone_layout_placeholders(slide_layout)
189
+ @sld_id_list.add_slide_id(r_id)
190
+ slide
191
+ end
192
+
193
+ # The zero-based position of +slide+.
194
+ #
195
+ # @return [Integer, nil] nil when the slide is not in this collection
196
+ def index(slide) = each_with_index.find { |s, _| s == slide }&.last
197
+
198
+ def inspect = "#<Pptx::Slides size=#{size}>"
199
+ end
200
+
201
+ # A slide layout: the arrangement a slide inherits from.
202
+ class SlideLayout < BaseSlide
203
+ include PatternMatching
204
+
205
+ pattern_keys :name, :type, :slide_master, :shapes, :placeholders
206
+
207
+ # Placeholders PowerPoint renders from the layout rather than copying onto
208
+ # each slide, so they are not cloned when a slide is created.
209
+ LATENT_PLACEHOLDER_TYPES = [
210
+ Enum::PP_PLACEHOLDER::DATE,
211
+ Enum::PP_PLACEHOLDER::FOOTER,
212
+ Enum::PP_PLACEHOLDER::SLIDE_NUMBER
213
+ ].freeze
214
+
215
+ # The shapes on this layout.
216
+ def shapes = @shapes ||= LayoutShapes.new(@element.spTree, self)
217
+
218
+ # The placeholders on this layout, in `idx` order.
219
+ def placeholders = @placeholders ||= LayoutPlaceholders.new(@element.spTree, self)
220
+
221
+ # The placeholders a new slide based on this layout should receive.
222
+ def cloneable_placeholders
223
+ placeholders.reject { |ph| LATENT_PLACEHOLDER_TYPES.include?(ph.element.ph_type) }
224
+ end
225
+
226
+ # The master this layout inherits from.
227
+ def slide_master = part.slide_master
228
+
229
+ # The kind of layout this is, e.g. "title" or "obj". PowerPoint uses it to
230
+ # decide which layout to offer for a given command.
231
+ def type = @element.type
232
+
233
+ def type=(value)
234
+ @element.type = value&.to_s
235
+ end
236
+
237
+ # The slides based on this layout.
238
+ def used_by_slides
239
+ part.package.presentation_part.presentation.slides.select { |s| s.layout == self }
240
+ end
241
+
242
+ def inspect = "#<Pptx::SlideLayout #{name.inspect}>"
243
+ end
244
+
245
+ # The layouts belonging to one slide master.
246
+ class SlideLayouts < ParentedElementProxy
247
+ include DeconstructToArray
248
+
249
+ include Enumerable
250
+
251
+ def initialize(sld_layout_id_list, slide_master)
252
+ super
253
+ @sld_layout_id_list = sld_layout_id_list
254
+ end
255
+
256
+ def each
257
+ return enum_for(:each) { size } unless block_given?
258
+
259
+ @sld_layout_id_list.sldLayoutId_list.each do |entry|
260
+ yield part.related_slide_layout(entry.rId)
261
+ end
262
+ self
263
+ end
264
+
265
+ def size = @sld_layout_id_list.size
266
+ alias length size
267
+
268
+ # Indexed by position, or looked up by layout name.
269
+ #
270
+ # layouts[1]
271
+ # layouts["Title and Content"]
272
+ def [](key, length = nil)
273
+ return by_name(key) if key.is_a?(String)
274
+
275
+ slice_members(@sld_layout_id_list.sldLayoutId_list, key, length) do |entry|
276
+ part.related_slide_layout(entry.rId)
277
+ end
278
+ end
279
+
280
+ def fetch(key)
281
+ self[key] || raise(IndexError, "no slide layout #{key.inspect}")
282
+ end
283
+
284
+ # @return [SlideLayout, nil]
285
+ def by_name(name) = find { |layout| layout.name == name }
286
+
287
+ # @return [Integer, nil]
288
+ def index(slide_layout) = each_with_index.find { |l, _| l == slide_layout }&.last
289
+
290
+ # Remove +slide_layout+ from this collection and from the package.
291
+ #
292
+ # @raise [Error] when one or more slides still use the layout
293
+ def delete(slide_layout)
294
+ unless slide_layout.used_by_slides.empty?
295
+ raise Error, "cannot remove a slide layout in use by one or more slides"
296
+ end
297
+
298
+ target_index = index(slide_layout) or
299
+ raise NotFoundError, "layout is not in this collection"
300
+
301
+ entry = @sld_layout_id_list.sldLayoutId_list[target_index]
302
+ r_id = entry.rId
303
+ # Removing the id stops the layout appearing; dropping the relationship
304
+ # is what actually removes it from the package, along with anything only
305
+ # it referred to.
306
+ @sld_layout_id_list.remove(entry)
307
+ slide_layout.slide_master.part.drop_rel(r_id)
308
+ slide_layout
309
+ end
310
+
311
+ # Add a layout to this master.
312
+ #
313
+ # layout = master.slide_layouts.add("Section Header", type: "secHead")
314
+ # layout.placeholders.add(:title, at: [x, y], size: [cx, cy])
315
+ #
316
+ # The layout starts empty; placeholders are added to it explicitly, which
317
+ # is what decides what a slide using it inherits.
318
+ #
319
+ # @param name [String] the name PowerPoint shows in the layout gallery
320
+ # @param type [String, Symbol, nil] one of the 36 ST_SlideLayoutType values
321
+ # @return [SlideLayout]
322
+ def add(name, type: "obj")
323
+ master_part = parent.part
324
+ layout_part = Parts::SlideLayoutPart.new_layout(master_part.package, master_part, name,
325
+ type&.to_s)
326
+ master_part.add_slide_layout(layout_part)
327
+ layout = layout_part.slide_layout
328
+ yield layout if block_given?
329
+ layout
330
+ end
331
+
332
+ def inspect = "#<Pptx::SlideLayouts size=#{size}>"
333
+ end
334
+
335
+ # A slide master.
336
+ class SlideMaster < BaseMaster
337
+ # The layouts that inherit from this master.
338
+ def slide_layouts
339
+ @slide_layouts ||= SlideLayouts.new(@element.get_or_add_sldLayoutIdLst, self)
340
+ end
341
+
342
+ # The colours and fonts this master draws from.
343
+ #
344
+ # @return [Pptx::Theme]
345
+ def theme = part.theme_part.theme
346
+
347
+ # The placeholders every layout and slide under this master inherits from.
348
+ #
349
+ # @return [MasterPlaceholders]
350
+ def placeholders = @placeholders ||= MasterPlaceholders.new(@element.spTree, self)
351
+
352
+ def inspect = "#<Pptx::SlideMaster #{part.partname}>"
353
+ end
354
+
355
+ # The slide masters of a presentation.
356
+ class SlideMasters < ParentedElementProxy
357
+ include DeconstructToArray
358
+
359
+ include Enumerable
360
+
361
+ def initialize(sld_master_id_list, presentation)
362
+ super
363
+ @sld_master_id_list = sld_master_id_list
364
+ end
365
+
366
+ def each
367
+ return enum_for(:each) { size } unless block_given?
368
+
369
+ @sld_master_id_list.sldMasterId_list.each do |entry|
370
+ yield part.related_slide_master(entry.rId)
371
+ end
372
+ self
373
+ end
374
+
375
+ def size = @sld_master_id_list.size
376
+ alias length size
377
+
378
+ def [](index, length = nil)
379
+ slice_members(@sld_master_id_list.sldMasterId_list, index, length) do |entry|
380
+ part.related_slide_master(entry.rId)
381
+ end
382
+ end
383
+
384
+ # Add a slide master to the presentation, with its own theme.
385
+ #
386
+ # master = deck.slide_masters.add(name: "Corporate")
387
+ # master.theme.colors.update(accent1: "1F497D")
388
+ # master.slide_layouts.add("Title Slide", type: "title")
389
+ #
390
+ # The master starts with the five placeholders PowerPoint expects of one --
391
+ # title, body, date, footer and slide number -- positioned proportionally
392
+ # to this presentation's slide size. Pass <tt>placeholders: :none</tt> to
393
+ # get a bare master and place them yourself.
394
+ #
395
+ # @param name [String] names both the master's theme and its schemes
396
+ # @param placeholders [Symbol] :standard or :none
397
+ # @return [SlideMaster]
398
+ def add(name: "Office Theme", placeholders: :standard)
399
+ unless %i[standard none].include?(placeholders)
400
+ raise ArgumentError, "placeholders must be :standard or :none, got #{placeholders.inspect}"
401
+ end
402
+
403
+ master_part = Parts::SlideMasterPart.new_master(part.package, name: name)
404
+ register(master_part)
405
+ master = master_part.slide_master
406
+ master.placeholders.add_standard_set(slide_size) if placeholders == :standard
407
+ yield master if block_given?
408
+ master
409
+ end
410
+
411
+ def inspect = "#<Pptx::SlideMasters size=#{size}>"
412
+
413
+ private
414
+
415
+ # Master ids are numbered from 2147483648 upwards, above the slide-id range.
416
+ def register(master_part)
417
+ r_id = part.relate_to(master_part, Opc::RELATIONSHIP_TYPE::SLIDE_MASTER)
418
+ entry = @sld_master_id_list.add_sldMasterId
419
+ entry.rId = r_id
420
+ used = @sld_master_id_list.sldMasterId_list.filter_map(&:id)
421
+ entry.id = used.empty? ? 2_147_483_648 : used.max + 1
422
+ r_id
423
+ end
424
+
425
+ def slide_size
426
+ size_element = part.element.sldSz
427
+ [size_element.cx, size_element.cy]
428
+ end
429
+ end
430
+ end