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,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/opc/package"
4
+ require "ruby_pptx/opc/constants"
5
+ require "ruby_pptx/enum/prog_id"
6
+ require "ruby_pptx/parts/chart"
7
+
8
+ module Pptx
9
+ module Parts
10
+ # A file embedded in the presentation as an OLE object.
11
+ #
12
+ # An Office document -- one of Enum::PROG_ID -- is stored as the document
13
+ # itself, under a name and content type saying what it is. Anything else
14
+ # is stored as an opaque `oleObjectN.bin`.
15
+ module EmbeddedPackagePart
16
+ OFFICE_PARTS = {
17
+ DOCX: ["/ppt/embeddings/Microsoft_Word_Document%d.docx", Opc::CONTENT_TYPE::WML_DOCUMENT],
18
+ PPTX: ["/ppt/embeddings/Microsoft_PowerPoint_Presentation%d.pptx",
19
+ Opc::CONTENT_TYPE::PML_PRESENTATION],
20
+ XLSX: [EmbeddedXlsxPart::PARTNAME_TEMPLATE, Opc::CONTENT_TYPE::SML_SHEET]
21
+ }.freeze
22
+
23
+ GENERIC_PARTNAME = "/ppt/embeddings/oleObject%d.bin"
24
+
25
+ module_function
26
+
27
+ # @param prog_id [Enum::PROG_ID::Member, String]
28
+ # @return [Opc::Part]
29
+ def new_part(prog_id, blob, package)
30
+ unless prog_id.is_a?(Enum::PROG_ID::Member)
31
+ return Opc::Part.new(package.next_partname(GENERIC_PARTNAME),
32
+ Opc::CONTENT_TYPE::OFC_OLE_OBJECT, package, blob)
33
+ end
34
+
35
+ # An embedded workbook is the same kind of part a chart keeps its data in.
36
+ return EmbeddedXlsxPart.new_xlsx(blob, package) if prog_id.name == :XLSX
37
+
38
+ template, content_type = OFFICE_PARTS.fetch(prog_id.name)
39
+ Opc::Part.new(package.next_partname(template), content_type, package, blob)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/opc/package"
4
+ require "ruby_pptx/image"
5
+
6
+ module Pptx
7
+ module Parts
8
+ # An image part, `/ppt/media/imageN.<ext>`.
9
+ class ImagePart < Opc::Part
10
+ attr_reader :source_filename
11
+
12
+ # A new image part holding +image+.
13
+ def self.new_image(package, image)
14
+ new(package.next_image_partname(image.ext), image.content_type, package,
15
+ image.blob, image.filename)
16
+ end
17
+
18
+ def initialize(partname, content_type, package, blob = nil, source_filename = nil)
19
+ super(partname, content_type, package, blob)
20
+ @source_filename = source_filename
21
+ end
22
+
23
+ def image = @image ||= Image.from_blob(blob, @source_filename)
24
+
25
+ def ext = partname.ext
26
+
27
+ # The name PowerPoint shows in the alt-text/description field: the file
28
+ # the image came from, or a generic name when it came from a stream.
29
+ def desc = @source_filename || "image.#{ext}"
30
+
31
+ def sha1 = image.sha1
32
+
33
+ # Resolve a requested size against the image's native size.
34
+ #
35
+ # Supplying neither dimension uses the native size; supplying one scales
36
+ # the other to preserve the aspect ratio; supplying both stretches the
37
+ # image to fit.
38
+ #
39
+ # @return [Array(Pptx::Length, Pptx::Length)]
40
+ def scale(width, height)
41
+ native_width, native_height = image.native_size
42
+ return [Length.coerce(width), Length.coerce(height)] if width && height
43
+ return [native_width, native_height] unless width || height
44
+
45
+ if width
46
+ factor = Length.coerce(width).emu.to_f / native_width.emu
47
+ [Length.coerce(width), Length.emu((native_height.emu * factor).round)]
48
+ else
49
+ factor = Length.coerce(height).emu.to_f / native_height.emu
50
+ [Length.emu((native_width.emu * factor).round), Length.coerce(height)]
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/opc/package"
4
+ require "ruby_pptx/media"
5
+
6
+ module Pptx
7
+ module Parts
8
+ # A media part, `/ppt/media/mediaN.<ext>`: the audio or video itself.
9
+ class MediaPart < Opc::Part
10
+ def self.new_media(package, video)
11
+ new(package.next_media_partname(video.ext), video.content_type, package, video.blob)
12
+ end
13
+
14
+ def sha1 = @sha1 ||= Digest::SHA1.hexdigest(blob)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/opc/package"
4
+ require "ruby_pptx/opc/constants"
5
+ require "ruby_pptx/oxml/presentation"
6
+ require "ruby_pptx/parts/slide"
7
+ require "ruby_pptx/presentation"
8
+
9
+ module Pptx
10
+ module Parts
11
+ # The presentation part, `/ppt/presentation.xml`; the package's main
12
+ # document part.
13
+ class PresentationPart < Opc::XmlPart
14
+ def presentation = @presentation ||= Presentation.new(element, self)
15
+
16
+ # Create a blank slide part inheriting from +slide_layout+.
17
+ #
18
+ # @return [Array(String, Pptx::Slide)] the new relationship id and slide
19
+ def add_slide(slide_layout)
20
+ slide_part = SlidePart.new_slide(next_slide_partname, package, slide_layout.part)
21
+ r_id = relate_to(slide_part, Opc::RELATIONSHIP_TYPE::SLIDE)
22
+ [r_id, slide_part.slide]
23
+ end
24
+
25
+ def core_properties = package.core_properties
26
+
27
+ # The presentation's notes master, created from the default template on
28
+ # first use.
29
+ #
30
+ # Like python-pptx, this relates the new part to the presentation but
31
+ # does not add a `p:notesMasterIdLst` entry for it.
32
+ def notes_master_part
33
+ existing = rels.find { |r| r.reltype == Opc::RELATIONSHIP_TYPE::NOTES_MASTER }
34
+ return existing.target_part if existing
35
+
36
+ part = NotesMasterPart.create_default(package)
37
+ relate_to(part, Opc::RELATIONSHIP_TYPE::NOTES_MASTER)
38
+ part
39
+ end
40
+
41
+ def related_slide(r_id) = related_part(r_id).slide
42
+
43
+ def related_slide_master(r_id) = related_part(r_id).slide_master
44
+
45
+ # The slide with presentation-wide id +slide_id+.
46
+ #
47
+ # @return [Slide, nil]
48
+ def slide_by_id(slide_id)
49
+ entry = element.sldIdLst&.sldId_list&.find { |s| s.id == slide_id }
50
+ entry && related_part(entry.rId).slide
51
+ end
52
+
53
+ # The slide id assigned to +slide_part+.
54
+ def slide_id_for(slide_part)
55
+ entry = element.sldIdLst&.sldId_list&.find { |s| related_part(s.rId).equal?(slide_part) }
56
+ entry&.id || raise(NotFoundError, "slide part is not in this presentation")
57
+ end
58
+
59
+ # Renumber the slide parts so their partnames form a gapless sequence in
60
+ # the order the slides appear.
61
+ #
62
+ # Slide order lives in `p:sldIdLst`, not in the partnames, so the two can
63
+ # drift apart after slides are added or deleted. PowerPoint tolerates
64
+ # that, but keeping them aligned makes saved packages predictable.
65
+ def renumber_slide_parts(r_ids)
66
+ r_ids.each_with_index do |r_id, index|
67
+ related_part(r_id).partname = Opc::PackURI.new("/ppt/slides/slide#{index + 1}.xml")
68
+ end
69
+ end
70
+
71
+ def next_slide_partname
72
+ count = element.get_or_add_sldIdLst.size
73
+ Opc::PackURI.new("/ppt/slides/slide#{count + 1}.xml")
74
+ end
75
+
76
+ def save(path_or_stream) = package.save(path_or_stream)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/opc/package"
4
+ require "ruby_pptx/opc/constants"
5
+ require "ruby_pptx/oxml/slide"
6
+ require "ruby_pptx/slide"
7
+ require "ruby_pptx/parts/chart"
8
+
9
+ module Pptx
10
+ module Parts
11
+ # Common to every slide-like part: slides, layouts, masters, notes slides
12
+ # and the notes master.
13
+ class BaseSlidePart < Opc::XmlPart
14
+ # The internal name of this slide.
15
+ def name = element.cSld.name
16
+
17
+ # Create a chart part for +chart_data+ and relate this slide to it.
18
+ #
19
+ # @return [String] the relationship id of the new chart part
20
+ def add_chart_part(chart_type, chart_data)
21
+ chart_part = ChartPart.new_chart(chart_type, chart_data, package)
22
+ relate_to(chart_part, Opc::RELATIONSHIP_TYPE::CHART)
23
+ end
24
+
25
+ # Create a combo chart part and relate this slide to it.
26
+ #
27
+ # @return [String] the relationship id of the new chart part
28
+ def add_combo_chart_part(builder)
29
+ chart_part = ChartPart.new_combo_chart(builder, package)
30
+ relate_to(chart_part, Opc::RELATIONSHIP_TYPE::CHART)
31
+ end
32
+
33
+ # Relate this slide to the media part holding +video+.
34
+ #
35
+ # Two relationships are made to the same part, one MEDIA and one VIDEO.
36
+ # PowerPoint has embedded media two different ways over the years and
37
+ # writes both so either era can find it.
38
+ #
39
+ # @return [Array(String, String)] the media rId and the video rId
40
+ def get_or_add_video_media_part(video)
41
+ media_part = package.get_or_add_media_part(video)
42
+ [relate_to(media_part, Opc::RELATIONSHIP_TYPE::MEDIA),
43
+ relate_to(media_part, Opc::RELATIONSHIP_TYPE::VIDEO)]
44
+ end
45
+
46
+ # The image part for +image_file+, related to this slide.
47
+ #
48
+ # Both the part and the relationship are reused when they already exist,
49
+ # so the same image added twice is stored once.
50
+ #
51
+ # @return [Array(Pptx::Parts::ImagePart, String)] the part and its rId
52
+ # Embed +object_file+ as an OLE object and relate this slide to it.
53
+ #
54
+ # An Office document is related as a package, anything else as an OLE
55
+ # object -- the two relationship types PowerPoint uses.
56
+ #
57
+ # @return [String] the relationship id
58
+ def add_embedded_ole_object_part(prog_id, object_file)
59
+ blob = object_file.respond_to?(:read) ? object_file.read : File.binread(object_file)
60
+ part = EmbeddedPackagePart.new_part(prog_id, blob, package)
61
+ reltype = if prog_id.is_a?(Enum::PROG_ID::Member)
62
+ Opc::RELATIONSHIP_TYPE::PACKAGE
63
+ else
64
+ Opc::RELATIONSHIP_TYPE::OLE_OBJECT
65
+ end
66
+ relate_to(part, reltype)
67
+ end
68
+
69
+ def get_or_add_image_part(image_file)
70
+ image_part = package.get_or_add_image_part(image_file)
71
+ [image_part, relate_to(image_part, Opc::RELATIONSHIP_TYPE::IMAGE)]
72
+ end
73
+ end
74
+
75
+ # A slide part, `/ppt/slides/slideN.xml`.
76
+ class SlidePart < BaseSlidePart
77
+ # A new, empty slide part inheriting from +slide_layout_part+.
78
+ def self.new_slide(partname, package, slide_layout_part)
79
+ part = new(partname, Opc::CONTENT_TYPE::PML_SLIDE, package, Oxml::CT_Slide.new_element)
80
+ part.relate_to(slide_layout_part, Opc::RELATIONSHIP_TYPE::SLIDE_LAYOUT)
81
+ part
82
+ end
83
+
84
+ def slide = @slide ||= Slide.new(element, self)
85
+
86
+ # The layout part this slide inherits from.
87
+ def slide_layout
88
+ part_related_by(Opc::RELATIONSHIP_TYPE::SLIDE_LAYOUT).slide_layout
89
+ end
90
+
91
+ # The presentation-wide id of this slide.
92
+ def slide_id = package.presentation_part.slide_id_for(self)
93
+
94
+ def notes_slide? = rels.any? { |r| r.reltype == Opc::RELATIONSHIP_TYPE::NOTES_SLIDE }
95
+
96
+ # This slide's notes slide, created on first use.
97
+ def notes_slide
98
+ existing = rels.find { |r| r.reltype == Opc::RELATIONSHIP_TYPE::NOTES_SLIDE }
99
+ return existing.target_part.notes_slide if existing
100
+
101
+ notes_part = NotesSlidePart.new_notes_slide(package, self)
102
+ relate_to(notes_part, Opc::RELATIONSHIP_TYPE::NOTES_SLIDE)
103
+ notes_part.notes_slide
104
+ end
105
+ end
106
+
107
+ # A slide-layout part, `/ppt/slideLayouts/slideLayoutN.xml`.
108
+ class SlideLayoutPart < BaseSlidePart
109
+ PARTNAME_TEMPLATE = "/ppt/slideLayouts/slideLayout%d.xml"
110
+
111
+ # A new layout named +name+, inheriting from +slide_master_part+.
112
+ def self.new_layout(package, slide_master_part, name, layout_type)
113
+ part = new(package.next_partname(PARTNAME_TEMPLATE),
114
+ Opc::CONTENT_TYPE::PML_SLIDE_LAYOUT, package,
115
+ Oxml::CT_SlideLayout.new_element(name, layout_type))
116
+ part.relate_to(slide_master_part, Opc::RELATIONSHIP_TYPE::SLIDE_MASTER)
117
+ part
118
+ end
119
+
120
+ def slide_layout = @slide_layout ||= SlideLayout.new(element, self)
121
+
122
+ def slide_master
123
+ part_related_by(Opc::RELATIONSHIP_TYPE::SLIDE_MASTER).slide_master
124
+ end
125
+ end
126
+
127
+ # A slide-master part, `/ppt/slideMasters/slideMasterN.xml`.
128
+ class SlideMasterPart < BaseSlidePart
129
+ PARTNAME_TEMPLATE = "/ppt/slideMasters/slideMaster%d.xml"
130
+ THEME_PARTNAME_TEMPLATE = "/ppt/theme/theme%d.xml"
131
+
132
+ # A new master with its own theme part and no layouts yet.
133
+ #
134
+ # The theme is a separate part related from the master, which is where
135
+ # the master's colours and fonts actually live.
136
+ def self.new_master(package, name: "Office Theme")
137
+ part = new(package.next_partname(PARTNAME_TEMPLATE),
138
+ Opc::CONTENT_TYPE::PML_SLIDE_MASTER, package,
139
+ Oxml::CT_SlideMaster.new_element)
140
+ theme = ThemePart.new_theme(package.next_partname(THEME_PARTNAME_TEMPLATE), package,
141
+ name: name)
142
+ part.relate_to(theme, Opc::RELATIONSHIP_TYPE::THEME)
143
+ part
144
+ end
145
+
146
+ def slide_master = @slide_master ||= SlideMaster.new(element, self)
147
+
148
+ # The theme this master draws its colours and fonts from.
149
+ def theme_part = part_related_by(Opc::RELATIONSHIP_TYPE::THEME)
150
+
151
+ # Add +slide_layout_part+ to this master's layout list.
152
+ #
153
+ # @return [String] the relationship id
154
+ def add_slide_layout(slide_layout_part)
155
+ r_id = relate_to(slide_layout_part, Opc::RELATIONSHIP_TYPE::SLIDE_LAYOUT)
156
+ entry = element.get_or_add_sldLayoutIdLst.add_sldLayoutId
157
+ entry.rId = r_id
158
+ entry.id = next_slide_layout_id
159
+ r_id
160
+ end
161
+
162
+ def related_slide_layout(r_id) = related_part(r_id).slide_layout
163
+
164
+ private
165
+
166
+ # Layout ids are numbered from 2147483649 upwards, continuing from the
167
+ # highest already in use so a removed layout does not free its id for
168
+ # reuse -- PowerPoint keeps references to them elsewhere in the package.
169
+ def next_slide_layout_id
170
+ used = element.get_or_add_sldLayoutIdLst.sldLayoutId_list.filter_map(&:id)
171
+ used.empty? ? 2_147_483_649 : used.max + 1
172
+ end
173
+ end
174
+
175
+ # A notes-master part.
176
+ class NotesMasterPart < BaseSlidePart
177
+ PARTNAME = "/ppt/notesMasters/notesMaster1.xml"
178
+
179
+ # A default notes master with the theme it needs, built from the
180
+ # templates shipped with the gem.
181
+ #
182
+ # The theme is the base theme as-is. It is not routed through
183
+ # ThemePart.new_theme, which renames the colour and font schemes along
184
+ # with the theme and so would not match what python-pptx writes.
185
+ def self.create_default(package)
186
+ part = new(Opc::PackURI.new(PARTNAME), Opc::CONTENT_TYPE::PML_NOTES_MASTER, package,
187
+ Oxml::CT_NotesMaster.new_default)
188
+ theme = ThemePart.new(package.next_partname(SlideMasterPart::THEME_PARTNAME_TEMPLATE),
189
+ Opc::CONTENT_TYPE::OFC_THEME, package,
190
+ Oxml::CT_OfficeStyleSheet.new_default)
191
+ part.relate_to(theme, Opc::RELATIONSHIP_TYPE::THEME)
192
+ part
193
+ end
194
+
195
+ def notes_master = @notes_master ||= NotesMaster.new(element, self)
196
+ end
197
+
198
+ # A notes-slide part, `/ppt/notesSlides/notesSlideN.xml`.
199
+ class NotesSlidePart < BaseSlidePart
200
+ PARTNAME_TEMPLATE = "/ppt/notesSlides/notesSlide%d.xml"
201
+
202
+ # A new notes slide for +slide_part+, related to the notes master and
203
+ # carrying clones of the master's slide-image, body and slide-number
204
+ # placeholders. The notes master is created first if the presentation
205
+ # has none.
206
+ def self.new_notes_slide(package, slide_part)
207
+ notes_master_part = package.presentation_part.notes_master_part
208
+ part = new(package.next_partname(PARTNAME_TEMPLATE), Opc::CONTENT_TYPE::PML_NOTES_SLIDE,
209
+ package, Oxml::CT_NotesSlide.new_element)
210
+ part.relate_to(notes_master_part, Opc::RELATIONSHIP_TYPE::NOTES_MASTER)
211
+ part.relate_to(slide_part, Opc::RELATIONSHIP_TYPE::SLIDE)
212
+ part.notes_slide.clone_master_placeholders(notes_master_part.notes_master)
213
+ part
214
+ end
215
+
216
+ def notes_slide = @notes_slide ||= NotesSlide.new(element, self)
217
+
218
+ def notes_master = part_related_by(Opc::RELATIONSHIP_TYPE::NOTES_MASTER).notes_master
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/opc/package"
4
+ require "ruby_pptx/opc/constants"
5
+ require "ruby_pptx/oxml/theme"
6
+ require "ruby_pptx/theme"
7
+
8
+ module Pptx
9
+ module Parts
10
+ # A theme part, `/ppt/theme/themeN.xml`.
11
+ #
12
+ # Every slide master relates to exactly one, and it is where the master's
13
+ # colours and fonts actually live -- the master itself only maps them.
14
+ class ThemePart < Opc::XmlPart
15
+ # A new theme part starting from the base theme shipped with the gem.
16
+ def self.new_theme(partname, package, name: "Office Theme")
17
+ part = new(partname, Opc::CONTENT_TYPE::OFC_THEME, package,
18
+ Oxml::CT_OfficeStyleSheet.new_default)
19
+ part.theme.name = name
20
+ part
21
+ end
22
+
23
+ def theme = @theme ||= Theme.new(element, self)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pptx
4
+ # `case`/`in` support for the objects a caller reads a deck with.
5
+ #
6
+ # case shape
7
+ # in {shape_type: :PICTURE, name:} then puts "picture #{name}"
8
+ # in {shape_type: :PLACEHOLDER, placeholder_format: {type: :TITLE}}
9
+ # then puts shape.text_frame.text
10
+ # in {width: Pptx::Length => w} if w > Pptx.inches(5)
11
+ # then puts "wide"
12
+ # end
13
+ #
14
+ # **Enum-valued attributes read as their symbolic name in a pattern.**
15
+ # `shape.shape_type` returns a {Pptx::Enum::Member}, because that member
16
+ # carries the MS API value and the XML value and is the honest return type.
17
+ # In a pattern none of that helps -- `in {shape_type: :PICTURE}` is what
18
+ # anyone would write -- so the pattern-matching view reports the symbol.
19
+ #
20
+ # Only the keys a pattern actually asks for are computed, so matching on a
21
+ # shape's name does not walk its text.
22
+ module PatternMatching
23
+ def self.included(base) = base.extend(ClassMethods)
24
+
25
+ # A member reads as its name; anything else is passed through.
26
+ def self.for_pattern(value)
27
+ value.is_a?(Enum::Member) ? value.to_sym : value
28
+ end
29
+
30
+ module ClassMethods
31
+ # Declare the keys `case`/`in` may match on. Each names a public method.
32
+ def pattern_keys(*names)
33
+ names.freeze
34
+ define_method(:deconstruct_keys) do |keys|
35
+ wanted = keys.nil? ? names : names & keys
36
+ wanted.to_h { |name| [name, PatternMatching.for_pattern(public_send(name))] }
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ # Array patterns for the collections, so `in [first, *rest]` works.
43
+ #
44
+ # Kept separate from {PatternMatching} because deconstructing a collection
45
+ # means enumerating all of it, which is the opposite of the lazy wrapping
46
+ # everything else here does. Use it where the whole list is wanted anyway.
47
+ module DeconstructToArray
48
+ def deconstruct = to_a
49
+ end
50
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/pattern_matching"
4
+
5
+ require "ruby_pptx/element_proxy"
6
+ require "ruby_pptx/slide"
7
+ require "ruby_pptx/section"
8
+
9
+ module Pptx
10
+ # A PowerPoint presentation.
11
+ #
12
+ # Open one with {Presentation.open}, or start from the built-in template with
13
+ # {Presentation.new}:
14
+ #
15
+ # prs = Pptx::Presentation.open("deck.pptx")
16
+ # prs.slides.each { |slide| puts slide.name }
17
+ # prs.slide_width = Pptx.inches(13.333)
18
+ # prs.save("wide.pptx")
19
+ class Presentation < PartElementProxy
20
+ include PatternMatching
21
+
22
+ pattern_keys :slides, :slide_layouts, :slide_masters, :slide_width, :slide_height
23
+
24
+ class << self
25
+ # Open a presentation from a path or an IO stream.
26
+ #
27
+ # Given a block, yields the presentation and returns the block's value,
28
+ # as `File.open` does.
29
+ #
30
+ # @raise [PackageNotFoundError] when the file is not an OPC package
31
+ # @raise [Error] when the package is not a PowerPoint presentation
32
+ def open(pptx = nil)
33
+ pptx ||= default_template_path
34
+ presentation_part = Package.open(pptx).main_document_part
35
+ unless VALID_CONTENT_TYPES.include?(presentation_part.content_type)
36
+ raise Error,
37
+ "not a PowerPoint file; content type is #{presentation_part.content_type.inspect}"
38
+ end
39
+
40
+ presentation = presentation_part.presentation
41
+ block_given? ? yield(presentation) : presentation
42
+ end
43
+
44
+ # A new presentation based on the built-in default template.
45
+ def new_default = Presentation.open(nil)
46
+
47
+ def default_template_path
48
+ File.expand_path("templates/default.pptx", __dir__)
49
+ end
50
+ end
51
+
52
+ VALID_CONTENT_TYPES = [
53
+ Opc::CONTENT_TYPE::PML_PRESENTATION_MAIN,
54
+ Opc::CONTENT_TYPE::PML_PRES_MACRO_MAIN
55
+ ].freeze
56
+
57
+ # The Dublin Core metadata for this presentation.
58
+ def core_properties = part.core_properties
59
+
60
+ # The slides in this presentation.
61
+ def slides
62
+ @slides ||= begin
63
+ sld_id_list = @element.get_or_add_sldIdLst
64
+ part.renumber_slide_parts(sld_id_list.sldId_list.map(&:rId))
65
+ Slides.new(sld_id_list, self)
66
+ end
67
+ end
68
+
69
+ # The slide masters in this presentation.
70
+ def slide_masters
71
+ @slide_masters ||= SlideMasters.new(@element.get_or_add_sldMasterIdLst, self)
72
+ end
73
+
74
+ # The notes master, created from the default template on first use.
75
+ #
76
+ # @return [NotesMaster]
77
+ def notes_master = part.notes_master_part.notes_master
78
+
79
+ # The first slide master, which is the only one in most presentations.
80
+ def slide_master = slide_masters[0]
81
+
82
+ # The layouts of the first slide master.
83
+ #
84
+ # A presentation may have several masters, each with its own layouts; this
85
+ # is a convenience for the common case of one.
86
+ def slide_layouts = slide_master.slide_layouts
87
+
88
+ # The sections grouping this presentation's slides.
89
+ #
90
+ # Sections are a PowerPoint 2010 extension; a presentation with none has
91
+ # an empty collection and writes no extension element.
92
+ def sections = @sections ||= Sections.new(self)
93
+
94
+ # @return [Length, nil] nil when the presentation defines no slide size
95
+ def slide_width = @element.sldSz&.cx
96
+
97
+ def slide_width=(width)
98
+ @element.get_or_add_sldSz.cx = width
99
+ end
100
+
101
+ # @return [Length, nil]
102
+ def slide_height = @element.sldSz&.cy
103
+
104
+ def slide_height=(height)
105
+ @element.get_or_add_sldSz.cy = height
106
+ end
107
+
108
+ # Write this presentation to a path or an IO stream.
109
+ def save(path_or_stream)
110
+ part.save(path_or_stream)
111
+ self
112
+ end
113
+
114
+ # The .pptx bytes, for callers who want to hand them to something other
115
+ # than the filesystem.
116
+ #
117
+ # @return [String]
118
+ def to_blob
119
+ buffer = StringIO.new(+"", "w+b")
120
+ save(buffer)
121
+ buffer.string
122
+ end
123
+
124
+ def inspect
125
+ "#<Pptx::Presentation slides=#{slides.size} #{slide_width&.inches}x#{slide_height&.inches}in>"
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/numeric_lengths"
4
+
5
+ module Pptx
6
+ # Numeric length sugar, scoped to the file that asks for it.
7
+ #
8
+ # require "ruby_pptx/refinements"
9
+ # using Pptx::Lengths
10
+ #
11
+ # slide.shapes.add_shape(:rectangle, at: [1.inch, 2.inch], size: [3.inch, 1.inch])
12
+ #
13
+ # A refinement is the polite version of `ruby_pptx/core_ext`: `Numeric` gains
14
+ # these methods inside this file and nowhere else, so nothing else sharing
15
+ # the process can see them.
16
+ module Lengths
17
+ refine Numeric do
18
+ import_methods NumericLengths
19
+ end
20
+ end
21
+ end