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,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/shapes/shape"
4
+
5
+ module Pptx
6
+ # Position and size that fall back to the placeholder being inherited from.
7
+ #
8
+ # A placeholder usually carries no `a:xfrm` of its own: it takes its geometry
9
+ # from the layout, which takes it from the master. Reading it back therefore
10
+ # has to walk that chain, or every placeholder on a freshly built slide looks
11
+ # like it has no position at all.
12
+ #
13
+ # Writing is not inherited -- setting a value applies it to this shape, which
14
+ # is what stops it tracking the layout from then on.
15
+ module InheritsDimensions
16
+ def left = effective(:left)
17
+ def top = effective(:top)
18
+ def width = effective(:width)
19
+ def height = effective(:height)
20
+
21
+ # The placeholder one level up that this one inherits from, or nil.
22
+ def base_placeholder = raise(NotImplementedError, "#{self.class} must define base_placeholder")
23
+
24
+ private
25
+
26
+ def effective(attribute)
27
+ own = super_value(attribute)
28
+ return own unless own.nil?
29
+
30
+ base_placeholder&.public_send(attribute)
31
+ end
32
+
33
+ def super_value(attribute)
34
+ method(attribute).super_method.call
35
+ end
36
+ end
37
+
38
+ # A placeholder on a slide.
39
+ #
40
+ # Position, size and formatting are inherited from the matching placeholder
41
+ # on the layout unless this shape overrides them.
42
+ class SlidePlaceholder < Shape
43
+ include InheritsDimensions
44
+
45
+ def shape_type = Enum::MSO_SHAPE_TYPE::PLACEHOLDER
46
+
47
+ # Matched to the layout by `idx`, which is what ties the two together.
48
+ def base_placeholder
49
+ part.slide_layout.placeholders.by_idx(element.ph_idx)
50
+ end
51
+
52
+ def inspect
53
+ "#<#{self.class.name} idx=#{placeholder_format.idx} type=#{placeholder_format.type}>"
54
+ end
55
+ end
56
+
57
+ # A slide placeholder that can be filled with something other than text:
58
+ # the new content takes over the placeholder's slot, id and name, and keeps
59
+ # the `p:ph` that ties it to the layout.
60
+ module FillablePlaceholder
61
+ private
62
+
63
+ # Put +new_element+ where this placeholder is and move the `p:ph` onto
64
+ # it. The original element leaves the tree; this object is spent.
65
+ def replace_placeholder_with(new_element)
66
+ adopted = @element.build_from_xml(new_element.node.to_xml)
67
+ adopted.nvXxPr.nvPr.insert_ph(@element.ph)
68
+ @element.node.add_previous_sibling(adopted.node)
69
+ @element.parent.remove(@element)
70
+ ShapeFactory.build_for_slide(adopted, @parent)
71
+ end
72
+ end
73
+
74
+ # A picture placeholder: fill it with {#insert_picture}.
75
+ class PicturePlaceholder < SlidePlaceholder
76
+ include FillablePlaceholder
77
+
78
+ # Fill this placeholder with an image, cropped to cover it exactly.
79
+ #
80
+ # The picture keeps no size of its own -- it inherits the placeholder's
81
+ # from the layout -- so the image is cropped, never distorted, to fit.
82
+ #
83
+ # @param image_file [String, IO] a path or an open stream
84
+ # @return [PlaceholderPicture]
85
+ def insert_picture(image_file)
86
+ image_part, r_id = part.get_or_add_image_part(image_file)
87
+ pic = Oxml::CT_Picture.new_ph_pic(shape_id, name, image_part.desc, r_id)
88
+ pic.crop_to_fit(image_part.image.size, [width.emu, height.emu])
89
+ replace_placeholder_with(pic)
90
+ end
91
+ end
92
+
93
+ # A table placeholder: fill it with {#insert_table}.
94
+ class TablePlaceholder < SlidePlaceholder
95
+ include FillablePlaceholder
96
+
97
+ # PowerPoint's default row height, which sets the new table's height.
98
+ ROW_HEIGHT = 370_840
99
+
100
+ # Fill this placeholder with an empty table at its position and width.
101
+ #
102
+ # @return [PlaceholderGraphicFrame] whose #table is the new table
103
+ def insert_table(rows, cols)
104
+ frame = Oxml::CT_GraphicalObjectFrame.new_table_graphic_frame(
105
+ shape_id, name, rows, cols, left, top, width, Length.emu(rows * ROW_HEIGHT)
106
+ )
107
+ replace_placeholder_with(frame)
108
+ end
109
+ end
110
+
111
+ # A chart placeholder: fill it with {#insert_chart}.
112
+ class ChartPlaceholder < SlidePlaceholder
113
+ include FillablePlaceholder
114
+
115
+ # Fill this placeholder with a chart occupying exactly its area.
116
+ #
117
+ # @return [PlaceholderGraphicFrame] whose #chart is the new chart
118
+ def insert_chart(chart_type, chart_data)
119
+ r_id = part.add_chart_part(chart_type, chart_data)
120
+ frame = Oxml::CT_GraphicalObjectFrame.new_chart_graphic_frame(
121
+ shape_id, name, r_id, left, top, width, height
122
+ )
123
+ replace_placeholder_with(frame)
124
+ end
125
+ end
126
+
127
+ # A picture that fills a placeholder. It is a {Picture} in every respect
128
+ # except that it inherits its geometry from the layout.
129
+ class PlaceholderPicture < Picture
130
+ include InheritsDimensions
131
+
132
+ def shape_type = Enum::MSO_SHAPE_TYPE::PLACEHOLDER
133
+
134
+ def base_placeholder = part.slide_layout.placeholders.by_idx(element.ph_idx)
135
+ end
136
+
137
+ # A table or chart that fills a placeholder.
138
+ class PlaceholderGraphicFrame < GraphicFrame
139
+ def placeholder? = true
140
+ end
141
+
142
+ # A placeholder on a slide layout.
143
+ class LayoutPlaceholder < Shape
144
+ include InheritsDimensions
145
+
146
+ # A layout placeholder inherits from the master placeholder of the
147
+ # corresponding type rather than the same one: a chart, table or picture
148
+ # placeholder all take their geometry from the master's body.
149
+ BASE_TYPES = {
150
+ BODY: :BODY, CHART: :BODY, BITMAP: :BODY, ORG_CHART: :BODY, MEDIA_CLIP: :BODY,
151
+ OBJECT: :BODY, PICTURE: :BODY, SUBTITLE: :BODY, TABLE: :BODY,
152
+ CENTER_TITLE: :TITLE, TITLE: :TITLE,
153
+ DATE: :DATE, FOOTER: :FOOTER, SLIDE_NUMBER: :SLIDE_NUMBER
154
+ }.freeze
155
+
156
+ def shape_type = Enum::MSO_SHAPE_TYPE::PLACEHOLDER
157
+
158
+ def base_placeholder
159
+ base_type = BASE_TYPES[element.ph_type&.name]
160
+ return nil if base_type.nil?
161
+
162
+ part.slide_master.placeholders.by_type(Enum::PP_PLACEHOLDER_TYPE.fetch(base_type))
163
+ end
164
+ end
165
+
166
+ # A placeholder on a notes page.
167
+ #
168
+ # Inherits its geometry from the notes-master placeholder of the same type.
169
+ class NotesSlidePlaceholder < Shape
170
+ include InheritsDimensions
171
+
172
+ def shape_type = Enum::MSO_SHAPE_TYPE::PLACEHOLDER
173
+
174
+ def base_placeholder = part.notes_master.placeholders.by_type(element.ph_type)
175
+ end
176
+
177
+ # A placeholder on a slide master.
178
+ #
179
+ # There is nothing above a master to inherit from, so its geometry is
180
+ # whatever it carries itself.
181
+ class MasterPlaceholder < Shape
182
+ def shape_type = Enum::MSO_SHAPE_TYPE::PLACEHOLDER
183
+ end
184
+ end
@@ -0,0 +1,337 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/shapes/base"
4
+ require "ruby_pptx/text/text"
5
+ require "ruby_pptx/dml/fill"
6
+ require "ruby_pptx/table"
7
+
8
+ module Pptx
9
+ # A `p:sp`: an auto shape, a text box, or a placeholder.
10
+ class Shape < BaseShape
11
+ def text_frame? = true
12
+
13
+ # The text inside this shape, creating an empty text body if it has none.
14
+ def text_frame
15
+ @text_frame ||= TextFrame.new(@element.get_or_add_txBody, self)
16
+ end
17
+
18
+ # Shortcut for `text_frame.text`.
19
+ def text = text_frame.text
20
+
21
+ def text=(value)
22
+ text_frame.text = value
23
+ end
24
+
25
+ # The shape's fill.
26
+ def fill = @fill ||= FillFormat.from_fill_parent(@element.spPr)
27
+
28
+ # The shape's outline.
29
+ def line = @line ||= LineFormat.new(@element.spPr)
30
+
31
+ # The adjustment handles of this shape's geometry; empty for a freeform.
32
+ #
33
+ # @return [Adjustments]
34
+ def adjustments = @adjustments ||= Adjustments.new(@element.spPr.prstGeom)
35
+
36
+ # The preset geometry of an auto shape.
37
+ #
38
+ # @return [Pptx::Enum::MSO_SHAPE, nil] nil unless this is an auto shape
39
+ def auto_shape_type = @element.autoshape? ? @element.prst : nil
40
+
41
+ def shape_type
42
+ return Enum::MSO_SHAPE_TYPE::PLACEHOLDER if placeholder?
43
+ return Enum::MSO_SHAPE_TYPE::TEXT_BOX if @element.textbox?
44
+ return Enum::MSO_SHAPE_TYPE::FREEFORM if @element.custom_geometry?
45
+ return Enum::MSO_SHAPE_TYPE::AUTO_SHAPE if @element.autoshape?
46
+
47
+ Enum::MSO_SHAPE_TYPE::AUTO_SHAPE
48
+ end
49
+ end
50
+
51
+ # A `p:pic` holding an image.
52
+ class BasePicture < BaseShape
53
+ # Cropping, as the fraction of the image cut from each side: 0.25 is a
54
+ # quarter. Negative values extend the side past the image edge, and values
55
+ # above 1.0 are allowed, as in PowerPoint.
56
+ %i[left top right bottom].each do |side|
57
+ attribute = :"srcRect_#{side.to_s[0]}"
58
+ define_method(:"crop_#{side}") { @element.public_send(attribute) }
59
+ define_method(:"crop_#{side}=") { |value| @element.public_send(:"#{attribute}=", value) }
60
+ end
61
+
62
+ # The outline drawn around the picture.
63
+ def line = @line ||= LineFormat.new(@element.spPr)
64
+ end
65
+
66
+ # A `p:pic` showing a still image.
67
+ class Picture < BasePicture
68
+ def shape_type = Enum::MSO_SHAPE_TYPE::PICTURE
69
+
70
+ # The image this picture shows: its bytes, format, size and resolution.
71
+ #
72
+ # @return [Pptx::Image]
73
+ # @raise [Error] when the picture links its image rather than embedding it
74
+ def image
75
+ r_id = @element.blip_rId
76
+ raise Error, "this picture has no embedded image" if r_id.nil?
77
+
78
+ part.related_part(r_id).image
79
+ end
80
+
81
+ # The auto shape the picture is masked by. A new picture is a rectangle,
82
+ # which crops nothing; an ellipse shows it through an oval.
83
+ #
84
+ # @return [Pptx::Enum::Member, nil] nil for a freeform mask, or no geometry
85
+ def auto_shape_type = @element.spPr.prstGeom&.prst
86
+
87
+ def auto_shape_type=(value)
88
+ member = Enum::MSO_AUTO_SHAPE_TYPE.fetch(value)
89
+ sp_pr = @element.spPr
90
+ geometry = sp_pr.prstGeom
91
+ if geometry.nil?
92
+ sp_pr.remove_custGeom
93
+ geometry = sp_pr.get_or_add_prstGeom
94
+ end
95
+ geometry.prst = member
96
+ end
97
+ end
98
+
99
+ # A `p:pic` that carries video rather than a still image.
100
+ #
101
+ # Not a {Picture}: the image a movie holds is its poster frame, which is
102
+ # what {#poster_frame} returns, and a movie cannot be masked by a shape.
103
+ class Movie < BasePicture
104
+ def shape_type = Enum::MSO_SHAPE_TYPE::MEDIA
105
+
106
+ def media_type = Enum::PP_MEDIA_TYPE::MOVIE
107
+
108
+ # The still shown before the movie plays, or nil if there is none.
109
+ #
110
+ # @return [Pptx::Image, nil]
111
+ def poster_frame
112
+ r_id = @element.blip_rId
113
+ r_id && part.related_part(r_id).image
114
+ end
115
+
116
+ # Playback settings. PowerPoint keeps these in the timing tree and
117
+ # python-pptx exposes none of them yet; the object exists so code written
118
+ # against either library finds it.
119
+ def media_format = @media_format ||= MediaFormat.new(@element)
120
+ end
121
+
122
+ # Playback formatting for a movie. Deliberately empty; see {Movie#media_format}.
123
+ class MediaFormat < ElementProxy; end
124
+
125
+ # A `p:cxnSp`: a line, optionally attached to shapes at either end.
126
+ #
127
+ # A connector is stored as a bounding box plus flip flags rather than as two
128
+ # points, so moving an end point may flip the box rather than move it. The
129
+ # accessors here hide that.
130
+ class Connector < BaseShape
131
+ def shape_type = Enum::MSO_SHAPE_TYPE::LINE
132
+
133
+ def line = @line ||= LineFormat.new(@element.spPr)
134
+
135
+ def begin_x = Length.emu(@element.flipH ? left.to_i + width.to_i : left.to_i)
136
+
137
+ def begin_x=(value)
138
+ move_x(Length.coerce(value).emu, begin_point: true)
139
+ end
140
+
141
+ def begin_y = Length.emu(@element.flipV ? top.to_i + height.to_i : top.to_i)
142
+
143
+ def begin_y=(value)
144
+ move_y(Length.coerce(value).emu, begin_point: true)
145
+ end
146
+
147
+ def end_x = Length.emu(@element.flipH ? left.to_i : left.to_i + width.to_i)
148
+
149
+ def end_x=(value)
150
+ move_x(Length.coerce(value).emu, begin_point: false)
151
+ end
152
+
153
+ def end_y = Length.emu(@element.flipV ? top.to_i : top.to_i + height.to_i)
154
+
155
+ def end_y=(value)
156
+ move_y(Length.coerce(value).emu, begin_point: false)
157
+ end
158
+
159
+ # Attach the start of this connector to +shape+ at one of its connection
160
+ # points, and move the start there.
161
+ #
162
+ # Connection points are numbered from 0, conventionally starting at the top
163
+ # centre and going counter-clockwise. That is a convention rather than a
164
+ # rule, so the result is only predictable for rectangular shapes.
165
+ def begin_connect(shape, connection_point)
166
+ connection = @element.nvCxnSpPr.cNvCxnSpPr.get_or_add_stCxn
167
+ connection.id = shape.shape_id
168
+ connection.idx = connection_point
169
+ x, y = connection_point_of(shape, connection_point)
170
+ self.begin_x = x
171
+ self.begin_y = y
172
+ self
173
+ end
174
+
175
+ # As {#begin_connect}, for the far end.
176
+ def end_connect(shape, connection_point)
177
+ connection = @element.nvCxnSpPr.cNvCxnSpPr.get_or_add_endCxn
178
+ connection.id = shape.shape_id
179
+ connection.idx = connection_point
180
+ x, y = connection_point_of(shape, connection_point)
181
+ self.end_x = x
182
+ self.end_y = y
183
+ self
184
+ end
185
+
186
+ def begin_connected? = !@element.nvCxnSpPr.cNvCxnSpPr.stCxn.nil?
187
+
188
+ def end_connected? = !@element.nvCxnSpPr.cNvCxnSpPr.endCxn.nil?
189
+
190
+ def inspect
191
+ "#<Pptx::Connector (#{begin_x.inches.round(2)}, #{begin_y.inches.round(2)}) -> " \
192
+ "(#{end_x.inches.round(2)}, #{end_y.inches.round(2)})>"
193
+ end
194
+
195
+ private
196
+
197
+ # The four points PowerPoint puts on a rectangle: top, left, bottom, right.
198
+ def connection_point_of(shape, index)
199
+ x = shape.left.to_i
200
+ y = shape.top.to_i
201
+ cx = shape.width.to_i
202
+ cy = shape.height.to_i
203
+ case index
204
+ when 0 then [x + (cx / 2), y]
205
+ when 1 then [x, y + (cy / 2)]
206
+ when 2 then [x + (cx / 2), y + cy]
207
+ when 3 then [x + cx, y + (cy / 2)]
208
+ else
209
+ raise ArgumentError, "connection point #{index} is out of range (0-3)"
210
+ end
211
+ end
212
+
213
+ # Move one end along an axis, keeping the other end where it is.
214
+ #
215
+ # The box has no notion of direction, so pulling an end past the other one
216
+ # flips the box rather than giving it a negative size.
217
+ def move_x(new_value, begin_point:)
218
+ fixed = begin_point ? end_x.emu : begin_x.emu
219
+ @element.x = [new_value, fixed].min
220
+ @element.cx = (new_value - fixed).abs
221
+ @element.flipH = begin_point ? new_value > fixed : new_value < fixed
222
+ end
223
+
224
+ def move_y(new_value, begin_point:)
225
+ fixed = begin_point ? end_y.emu : begin_y.emu
226
+ @element.y = [new_value, fixed].min
227
+ @element.cy = (new_value - fixed).abs
228
+ @element.flipV = begin_point ? new_value > fixed : new_value < fixed
229
+ end
230
+ end
231
+
232
+ # A `p:graphicFrame`, which holds a table, a chart or an embedded object.
233
+ class GraphicFrame < BaseShape
234
+ # A graphic frame's shadow belongs to what it holds -- a chart and a
235
+ # table keep theirs in different places -- so there is no single one to
236
+ # hand back. python-pptx declines the same way.
237
+ def shadow
238
+ raise Error, "a graphic frame has no shadow of its own; format the chart or table instead"
239
+ end
240
+
241
+ def table? = @element.table?
242
+ def chart? = @element.chart?
243
+
244
+ # The table inside this frame.
245
+ #
246
+ # @raise [Error] when the frame holds something other than a table
247
+ def table
248
+ raise Error, "this graphic frame does not contain a table" unless table?
249
+
250
+ @table ||= Table.new(@element.tbl, self)
251
+ end
252
+
253
+ # The chart inside this frame.
254
+ #
255
+ # @raise [Error] when the frame holds something other than a chart
256
+ def chart
257
+ raise Error, "this graphic frame does not contain a chart" unless chart?
258
+
259
+ @chart ||= part.related_part(@element.chart_rId).chart
260
+ end
261
+
262
+ # An OLE object is linked when it points at a file outside the
263
+ # presentation rather than carrying one. nil for anything else a frame
264
+ # can hold, such as a diagram, as in python-pptx.
265
+ def shape_type
266
+ return Enum::MSO_SHAPE_TYPE::TABLE if table?
267
+ return Enum::MSO_SHAPE_TYPE::CHART if chart?
268
+ return nil unless ole_object?
269
+
270
+ if @element.embedded_ole_object?
271
+ Enum::MSO_SHAPE_TYPE::EMBEDDED_OLE_OBJECT
272
+ else
273
+ Enum::MSO_SHAPE_TYPE::LINKED_OLE_OBJECT
274
+ end
275
+ end
276
+
277
+ def ole_object? = @element.ole_object?
278
+
279
+ # The OLE object this frame holds.
280
+ #
281
+ # @raise [Error] when the frame holds something else
282
+ def ole_format
283
+ raise Error, "this graphic frame does not contain an OLE object" unless ole_object?
284
+
285
+ @ole_format ||= OleFormat.new(@element.oleObj, self)
286
+ end
287
+ end
288
+
289
+ # An embedded or linked OLE object: the file, and how it is shown.
290
+ class OleFormat < ElementProxy
291
+ def initialize(ole_obj, frame)
292
+ super(ole_obj)
293
+ @frame = frame
294
+ end
295
+
296
+ # The embedded file's bytes, or nil for a linked object.
297
+ def blob
298
+ r_id = @element.rId
299
+ r_id && @frame.part.related_part(r_id).blob
300
+ end
301
+
302
+ # The ProgID naming the program that opens the object, e.g. "Excel.Sheet.12".
303
+ def prog_id = @element.progId
304
+
305
+ # Whether it appears as an icon rather than as a picture of its content.
306
+ def show_as_icon? = @element.showAsIcon
307
+
308
+ def inspect = "#<Pptx::OleFormat #{prog_id.inspect}>"
309
+ end
310
+
311
+ # A `p:grpSp` containing other shapes.
312
+ #
313
+ # A group has no position or size of its own: both follow from what it
314
+ # contains, and are recomputed whenever its contents change.
315
+ class GroupShape < BaseShape
316
+ # A group keeps its effects in `p:grpSpPr` rather than `p:spPr`.
317
+ def shadow = @shadow ||= ShadowFormat.new(@element.grpSpPr)
318
+
319
+ def shape_type = Enum::MSO_SHAPE_TYPE::GROUP
320
+
321
+ # The shapes inside this group.
322
+ def shapes = @shapes ||= GroupShapes.new(@element, self)
323
+
324
+ # A group holds no text of its own, though the shapes in it may.
325
+ def text_frame? = false
326
+
327
+ # PowerPoint offers no click behaviour on a group; the shapes inside carry
328
+ # their own.
329
+ def click_action
330
+ raise Error, "a group shape cannot have a click action"
331
+ end
332
+
333
+ def hyperlink
334
+ raise Error, "a group shape cannot have a hyperlink"
335
+ end
336
+ end
337
+ end