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,405 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/oxml/shapes/shared"
4
+ require "ruby_pptx/oxml/table"
5
+ require "ruby_pptx/oxml/chart"
6
+
7
+ module Pptx
8
+ module Oxml
9
+ # The non-visual property groups of the non-`p:sp` shapes. They differ in
10
+ # what else they hold, but each starts with the `p:cNvPr` that carries the
11
+ # shape's id and name, which is all this library needs from them.
12
+ class CT_ShapeNonVisualCommon < Element
13
+ tag "p:nvPicPr", "p:nvGraphicFramePr"
14
+ one_and_only_one "p:cNvPr"
15
+ zero_or_one "p:nvPr", successors: []
16
+ end
17
+
18
+ # `p:pic`, a picture (or a movie, which is a picture with media relations).
19
+ class CT_Picture < Element
20
+ include BaseShapeElement
21
+
22
+ tag "p:pic"
23
+ one_and_only_one "p:nvPicPr"
24
+ one_and_only_one "p:blipFill"
25
+ one_and_only_one "p:spPr"
26
+
27
+ # The `a:blip` inside this picture's fill.
28
+ def blip = xpath("./p:blipFill/a:blip").first
29
+
30
+ # The relationship id of the image this picture shows, or nil.
31
+ def blip_rId = blip&.embed
32
+
33
+ # Cropping, as a fraction of the image cropped from each side. Reading
34
+ # gives 0.0 when there is no `a:srcRect`; writing creates one.
35
+ %w[l t r b].each do |side|
36
+ define_method(:"srcRect_#{side}") { blipFill.srcRect&.public_send(side) || 0.0 }
37
+ define_method(:"srcRect_#{side}=") do |value|
38
+ blipFill.get_or_add_srcRect.public_send(:"#{side}=", value)
39
+ end
40
+ end
41
+
42
+ def ln = spPr.ln
43
+
44
+ def get_or_add_ln = spPr.get_or_add_ln
45
+
46
+ # Crop so an image of +image_size+ fills +view_size+ exactly when
47
+ # stretched with its aspect ratio kept: the excess is cut equally from
48
+ # both ends of whichever dimension is too long. Both sizes are
49
+ # (width, height); only their ratios matter, so the units need not agree.
50
+ def crop_to_fit(image_size, view_size)
51
+ left, top, right, bottom = fill_cropping(image_size, view_size)
52
+ rect = blipFill.get_or_add_srcRect
53
+ rect.l = left
54
+ rect.t = top
55
+ rect.r = right
56
+ rect.b = bottom
57
+ end
58
+
59
+ # A picture placeholder once filled: a `p:pic` with no `a:xfrm`, so its
60
+ # position and size keep coming from the layout.
61
+ def self.new_ph_pic(id, name, desc, r_id)
62
+ Element.parse(<<~XML)
63
+ <p:pic #{Ns.nsdecls("p", "a", "r")}>
64
+ <p:nvPicPr>
65
+ <p:cNvPr id="#{id}" name="#{escape(name)}" descr="#{escape(desc)}"/>
66
+ <p:cNvPicPr>
67
+ <a:picLocks noGrp="1" noChangeAspect="1"/>
68
+ </p:cNvPicPr>
69
+ <p:nvPr/>
70
+ </p:nvPicPr>
71
+ <p:blipFill>
72
+ <a:blip r:embed="#{r_id}"/>
73
+ <a:stretch>
74
+ <a:fillRect/>
75
+ </a:stretch>
76
+ </p:blipFill>
77
+ <p:spPr/>
78
+ </p:pic>
79
+ XML
80
+ end
81
+
82
+ # A movie is a `p:pic` whose non-visual properties name a video file.
83
+ def movie? = !xpath("./p:nvPicPr/p:nvPr/a:videoFile").empty?
84
+
85
+ # A `p:pic` displaying the image related by +r_id+.
86
+ def self.new_pic(id, name, desc, r_id, x, y, cx, cy)
87
+ Element.parse(<<~XML)
88
+ <p:pic #{Ns.nsdecls("a", "p", "r")}>
89
+ <p:nvPicPr>
90
+ <p:cNvPr id="#{id}" name="#{escape(name)}" descr="#{escape(desc)}"/>
91
+ <p:cNvPicPr>
92
+ <a:picLocks noChangeAspect="1"/>
93
+ </p:cNvPicPr>
94
+ <p:nvPr/>
95
+ </p:nvPicPr>
96
+ <p:blipFill>
97
+ <a:blip r:embed="#{r_id}"/>
98
+ <a:stretch>
99
+ <a:fillRect/>
100
+ </a:stretch>
101
+ </p:blipFill>
102
+ <p:spPr>
103
+ <a:xfrm>
104
+ <a:off x="#{x.to_i}" y="#{y.to_i}"/>
105
+ <a:ext cx="#{cx.to_i}" cy="#{cy.to_i}"/>
106
+ </a:xfrm>
107
+ <a:prstGeom prst="rect">
108
+ <a:avLst/>
109
+ </a:prstGeom>
110
+ </p:spPr>
111
+ </p:pic>
112
+ XML
113
+ end
114
+
115
+ # A `p:pic` showing a video.
116
+ #
117
+ # The video is referenced three times over: as a legacy `a:videoFile`
118
+ # link, as a `p14:media` embed for PowerPoint 2010 and later, and as the
119
+ # `ppaction://media` click action. The blip points at a poster frame,
120
+ # which is the still PowerPoint shows before the video plays.
121
+ def self.new_video_pic(id, name, video_r_id, media_r_id, poster_r_id, x, y, cx, cy)
122
+ Element.parse(<<~XML)
123
+ <p:pic #{Ns.nsdecls("a", "p", "r")}>
124
+ <p:nvPicPr>
125
+ <p:cNvPr id="#{id}" name="#{escape(name)}">
126
+ <a:hlinkClick r:id="" action="ppaction://media"/>
127
+ </p:cNvPr>
128
+ <p:cNvPicPr>
129
+ <a:picLocks noChangeAspect="1"/>
130
+ </p:cNvPicPr>
131
+ <p:nvPr>
132
+ <a:videoFile r:link="#{video_r_id}"/>
133
+ <p:extLst>
134
+ <p:ext uri="{DAA4B4D4-6D71-4841-9C94-3DE7FCFB9230}">
135
+ <p14:media #{Ns.nsdecls("p14")} r:embed="#{media_r_id}"/>
136
+ </p:ext>
137
+ </p:extLst>
138
+ </p:nvPr>
139
+ </p:nvPicPr>
140
+ <p:blipFill>
141
+ <a:blip r:embed="#{poster_r_id}"/>
142
+ <a:stretch>
143
+ <a:fillRect/>
144
+ </a:stretch>
145
+ </p:blipFill>
146
+ <p:spPr>
147
+ <a:xfrm>
148
+ <a:off x="#{x.to_i}" y="#{y.to_i}"/>
149
+ <a:ext cx="#{cx.to_i}" cy="#{cy.to_i}"/>
150
+ </a:xfrm>
151
+ <a:prstGeom prst="rect">
152
+ <a:avLst/>
153
+ </a:prstGeom>
154
+ </p:spPr>
155
+ </p:pic>
156
+ XML
157
+ end
158
+
159
+ def self.escape(text)
160
+ text.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")
161
+ end
162
+
163
+ private
164
+
165
+ # (left, top, right, bottom) fractions to crop. The arithmetic is
166
+ # python-pptx's, step for step, so the percentages round identically.
167
+ def fill_cropping(image_size, view_size)
168
+ view_ratio = view_size[0].to_f / view_size[1]
169
+ image_ratio = image_size[0].to_f / image_size[1]
170
+ if view_ratio < image_ratio # image too wide
171
+ crop = (1.0 - (view_ratio / image_ratio)) / 2.0
172
+ [crop, 0.0, crop, 0.0]
173
+ elsif view_ratio > image_ratio # image too tall
174
+ crop = (1.0 - (image_ratio / view_ratio)) / 2.0
175
+ [0.0, crop, 0.0, crop]
176
+ else
177
+ [0.0, 0.0, 0.0, 0.0]
178
+ end
179
+ end
180
+ end
181
+
182
+ # `p:nvCxnSpPr`, a connector's non-visual properties.
183
+ class CT_ConnectorNonVisual < Element
184
+ tag "p:nvCxnSpPr"
185
+ one_and_only_one "p:cNvPr"
186
+ one_and_only_one "p:cNvCxnSpPr"
187
+ zero_or_one "p:nvPr", successors: []
188
+ end
189
+
190
+ # `p:cNvCxnSpPr`, which records what each end of a connector attaches to.
191
+ class CT_NonVisualConnectorProperties < Element
192
+ tag "p:cNvCxnSpPr"
193
+ zero_or_one "a:stCxn", successors: %w[a:endCxn a:extLst]
194
+ zero_or_one "a:endCxn", successors: %w[a:extLst]
195
+ end
196
+
197
+ # `a:stCxn` and `a:endCxn`: one end of a connector attached to a shape's
198
+ # connection point.
199
+ class CT_Connection < Element
200
+ tag "a:stCxn", "a:endCxn"
201
+ required_attr "id", type: SimpleTypes::ST_DrawingElementId
202
+ required_attr "idx", type: SimpleTypes::XsdUnsignedInt
203
+ end
204
+
205
+ # `p:cxnSp`, a connector.
206
+ class CT_Connector < Element
207
+ include BaseShapeElement
208
+
209
+ tag "p:cxnSp"
210
+ one_and_only_one "p:nvCxnSpPr"
211
+ one_and_only_one "p:spPr"
212
+
213
+ # A connector is stored as a bounding box plus flip flags rather than as
214
+ # two points, so a line running right-to-left is the same box with
215
+ # flipH set.
216
+ def self.new_cxnSp(id, name, prst, x, y, cx, cy, flip_h, flip_v)
217
+ flip = +""
218
+ flip << %( flipH="1") if flip_h
219
+ flip << %( flipV="1") if flip_v
220
+
221
+ Element.parse(<<~XML)
222
+ <p:cxnSp #{Ns.nsdecls("a", "p")}>
223
+ <p:nvCxnSpPr>
224
+ <p:cNvPr id="#{id}" name="#{CT_Picture.escape(name)}"/>
225
+ <p:cNvCxnSpPr/>
226
+ <p:nvPr/>
227
+ </p:nvCxnSpPr>
228
+ <p:spPr>
229
+ <a:xfrm#{flip}>
230
+ <a:off x="#{x.to_i}" y="#{y.to_i}"/>
231
+ <a:ext cx="#{cx.to_i}" cy="#{cy.to_i}"/>
232
+ </a:xfrm>
233
+ <a:prstGeom prst="#{prst}">
234
+ <a:avLst/>
235
+ </a:prstGeom>
236
+ </p:spPr>
237
+ <p:style>
238
+ <a:lnRef idx="2">
239
+ <a:schemeClr val="accent1"/>
240
+ </a:lnRef>
241
+ <a:fillRef idx="0">
242
+ <a:schemeClr val="accent1"/>
243
+ </a:fillRef>
244
+ <a:effectRef idx="1">
245
+ <a:schemeClr val="accent1"/>
246
+ </a:effectRef>
247
+ <a:fontRef idx="minor">
248
+ <a:schemeClr val="tx1"/>
249
+ </a:fontRef>
250
+ </p:style>
251
+ </p:cxnSp>
252
+ XML
253
+ end
254
+ end
255
+
256
+ # `p:oleObj`, an OLE object inside a graphic frame.
257
+ class CT_OleObject < Element
258
+ tag "p:oleObj"
259
+ optional_attr "progId", type: SimpleTypes::XsdString
260
+ optional_attr "r:id", type: SimpleTypes::XsdString, as: :rId
261
+ optional_attr "showAsIcon", type: SimpleTypes::XsdBoolean, default: false
262
+ end
263
+
264
+ # `p:graphicFrame`, the container for a table, chart or embedded object.
265
+ class CT_GraphicalObjectFrame < Element
266
+ include BaseShapeElement
267
+
268
+ tag "p:graphicFrame"
269
+ one_and_only_one "p:nvGraphicFramePr"
270
+ one_and_only_one "p:xfrm"
271
+ one_and_only_one "a:graphic"
272
+
273
+ URI_TABLE = "http://schemas.openxmlformats.org/drawingml/2006/table"
274
+ URI_CHART = "http://schemas.openxmlformats.org/drawingml/2006/chart"
275
+ URI_OLE_OBJECT =
276
+ "http://schemas.openxmlformats.org/presentationml/2006/ole"
277
+
278
+ # A graphic frame keeps its transform directly, not under `p:spPr`.
279
+ def xfrm = find("p:xfrm")
280
+
281
+ def get_or_add_xfrm = xfrm
282
+
283
+ def graphic_data_uri = xpath("./a:graphic/a:graphicData/@uri").first&.value
284
+
285
+ # The relationship id of the chart part this frame refers to.
286
+ def chart_rId = xpath("./a:graphic/a:graphicData/c:chart/@r:id").first&.value
287
+
288
+ def table? = graphic_data_uri == URI_TABLE
289
+ def chart? = graphic_data_uri == URI_CHART
290
+
291
+ # The `a:tbl` inside this frame, or nil when it holds something else.
292
+ def tbl = xpath("./a:graphic/a:graphicData/a:tbl").first
293
+
294
+ def ole_object? = graphic_data_uri == URI_OLE_OBJECT
295
+
296
+ # The `p:oleObj` inside this frame, or nil.
297
+ def oleObj = xpath("./a:graphic/a:graphicData/p:oleObj").first
298
+
299
+ # An OLE object carries its file (`p:embed`) or points elsewhere for it.
300
+ def embedded_ole_object? = !xpath("./a:graphic/a:graphicData/p:oleObj/p:embed").empty?
301
+
302
+ class << self
303
+ # A frame holding an OLE object shown as an icon: the embedded file is
304
+ # related by +ole_r_id+, the icon image by +icon_r_id+. Written as
305
+ # python-pptx writes it.
306
+ def new_ole_object_graphic_frame(id, name, ole_r_id, prog_id, icon_r_id, x, y, cx, cy, img_w, img_h)
307
+ Element.parse(<<~XML)
308
+ <p:graphicFrame #{Ns.nsdecls("a", "p", "r")}>
309
+ <p:nvGraphicFramePr>
310
+ <p:cNvPr id="#{id}" name="#{CT_Picture.escape(name)}"/>
311
+ <p:cNvGraphicFramePr>
312
+ <a:graphicFrameLocks noGrp="1"/>
313
+ </p:cNvGraphicFramePr>
314
+ <p:nvPr/>
315
+ </p:nvGraphicFramePr>
316
+ <p:xfrm>
317
+ <a:off x="#{x.to_i}" y="#{y.to_i}"/>
318
+ <a:ext cx="#{cx.to_i}" cy="#{cy.to_i}"/>
319
+ </p:xfrm>
320
+ <a:graphic>
321
+ <a:graphicData uri="#{URI_OLE_OBJECT}">
322
+ <p:oleObj showAsIcon="1" r:id="#{ole_r_id}" imgW="#{img_w.to_i}" imgH="#{img_h.to_i}" progId="#{CT_Picture.escape(prog_id)}">
323
+ <p:embed/>
324
+ <p:pic>
325
+ <p:nvPicPr>
326
+ <p:cNvPr id="0" name=""/>
327
+ <p:cNvPicPr/>
328
+ <p:nvPr/>
329
+ </p:nvPicPr>
330
+ <p:blipFill>
331
+ <a:blip r:embed="#{icon_r_id}"/>
332
+ <a:stretch>
333
+ <a:fillRect/>
334
+ </a:stretch>
335
+ </p:blipFill>
336
+ <p:spPr>
337
+ <a:xfrm>
338
+ <a:off x="#{x.to_i}" y="#{y.to_i}"/>
339
+ <a:ext cx="#{cx.to_i}" cy="#{cy.to_i}"/>
340
+ </a:xfrm>
341
+ <a:prstGeom prst="rect">
342
+ <a:avLst/>
343
+ </a:prstGeom>
344
+ </p:spPr>
345
+ </p:pic>
346
+ </p:oleObj>
347
+ </a:graphicData>
348
+ </a:graphic>
349
+ </p:graphicFrame>
350
+ XML
351
+ end
352
+
353
+ # An empty `p:graphicFrame`. It is not a valid shape until a graphical
354
+ # object such as a table is placed inside it.
355
+ def new_graphic_frame(id, name, x, y, cx, cy)
356
+ Element.parse(<<~XML)
357
+ <p:graphicFrame #{Ns.nsdecls("a", "p")}>
358
+ <p:nvGraphicFramePr>
359
+ <p:cNvPr id="#{id}" name="#{CT_Picture.escape(name)}"/>
360
+ <p:cNvGraphicFramePr>
361
+ <a:graphicFrameLocks noGrp="1"/>
362
+ </p:cNvGraphicFramePr>
363
+ <p:nvPr/>
364
+ </p:nvGraphicFramePr>
365
+ <p:xfrm>
366
+ <a:off x="#{x.to_i}" y="#{y.to_i}"/>
367
+ <a:ext cx="#{cx.to_i}" cy="#{cy.to_i}"/>
368
+ </p:xfrm>
369
+ <a:graphic>
370
+ <a:graphicData/>
371
+ </a:graphic>
372
+ </p:graphicFrame>
373
+ XML
374
+ end
375
+
376
+ # A `p:graphicFrame` referring to the chart part related by +r_id+.
377
+ def new_chart_graphic_frame(id, name, r_id, x, y, cx, cy)
378
+ frame = new_graphic_frame(id, name, x, y, cx, cy)
379
+ graphic_data = frame.xpath("./a:graphic/a:graphicData").first
380
+ graphic_data.set("uri", URI_CHART)
381
+ graphic_data.append(
382
+ graphic_data.build_from_xml(CT_Chart.new_chart(r_id).node.to_xml)
383
+ )
384
+ frame
385
+ end
386
+
387
+ # A `p:graphicFrame` containing a table of +rows+ by +cols+.
388
+ def new_table_graphic_frame(id, name, rows, cols, x, y, cx, cy)
389
+ frame = new_graphic_frame(id, name, x, y, cx, cy)
390
+ graphic_data = frame.xpath("./a:graphic/a:graphicData").first
391
+ graphic_data.set("uri", URI_TABLE)
392
+ graphic_data.append(CT_Table.new_tbl(graphic_data, rows, cols, cx, cy))
393
+ frame
394
+ end
395
+ end
396
+ end
397
+
398
+ # `p:contentPart`, which this library carries through without modelling.
399
+ class CT_ContentPart < Element
400
+ include BaseShapeElement
401
+
402
+ tag "p:contentPart"
403
+ end
404
+ end
405
+ end
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/oxml/element"
4
+ require "ruby_pptx/oxml/content_model"
5
+ require "ruby_pptx/oxml/simple_types"
6
+ require "ruby_pptx/enum/shapes"
7
+ require "ruby_pptx/enum/text"
8
+
9
+ module Pptx
10
+ module Oxml
11
+ # Behaviour shared by the shape element classes -- `p:sp`, `p:pic`,
12
+ # `p:cxnSp`, `p:graphicFrame` and `p:grpSp`.
13
+ #
14
+ # Position and size live in an `a:xfrm` grandchild that may be absent, so
15
+ # the accessors here read through to it and create it only on write.
16
+ module BaseShapeElement
17
+ def x = xfrm_attr(:x)
18
+
19
+ def x=(value)
20
+ set_xfrm_attr(:x, value)
21
+ end
22
+
23
+ def y = xfrm_attr(:y)
24
+
25
+ def y=(value)
26
+ set_xfrm_attr(:y, value)
27
+ end
28
+
29
+ def cx = xfrm_attr(:cx)
30
+
31
+ def cx=(value)
32
+ set_xfrm_attr(:cx, value)
33
+ end
34
+
35
+ def cy = xfrm_attr(:cy)
36
+
37
+ def cy=(value)
38
+ set_xfrm_attr(:cy, value)
39
+ end
40
+
41
+ def flipH = !xfrm_attr(:flipH).nil? && xfrm_attr(:flipH)
42
+
43
+ def flipH=(value)
44
+ set_xfrm_attr(:flipH, value)
45
+ end
46
+
47
+ def flipV = !xfrm_attr(:flipV).nil? && xfrm_attr(:flipV)
48
+
49
+ def flipV=(value)
50
+ set_xfrm_attr(:flipV, value)
51
+ end
52
+
53
+ # Clockwise rotation in degrees; 0.0 when not set.
54
+ def rot = xfrm&.rot || 0.0
55
+
56
+ def rot=(value)
57
+ get_or_add_xfrm.rot = value
58
+ end
59
+
60
+ # The `a:xfrm` grandchild, or nil. `p:grpSp` overrides this, since its
61
+ # transform hangs off `p:grpSpPr` rather than `p:spPr`.
62
+ def xfrm = spPr.xfrm
63
+
64
+ def get_or_add_xfrm = spPr.get_or_add_xfrm
65
+
66
+ def shape_id = nvXxPr.cNvPr.id
67
+
68
+ def shape_name = nvXxPr.cNvPr.name
69
+
70
+ def txBody = find("p:txBody")
71
+
72
+ def placeholder? = !ph.nil?
73
+
74
+ # The `p:ph` descendant marking this as a placeholder, or nil.
75
+ def ph = xpath("./*[1]/p:nvPr/p:ph").first
76
+
77
+ def ph_idx = require_ph.idx
78
+ def ph_orient = require_ph.orient
79
+ def ph_sz = require_ph.sz
80
+ def ph_type = require_ph.type
81
+
82
+ # The non-visual properties element, whose tag varies by shape type
83
+ # (`p:nvSpPr`, `p:nvPicPr`, ...). It is always the first child.
84
+ def nvXxPr = xpath("./*[1]").first
85
+
86
+ private
87
+
88
+ def require_ph
89
+ ph || raise(Error, "not a placeholder shape")
90
+ end
91
+
92
+ def xfrm_attr(name)
93
+ xfrm&.public_send(name)
94
+ end
95
+
96
+ def set_xfrm_attr(name, value)
97
+ get_or_add_xfrm.public_send("#{name}=", value)
98
+ value
99
+ end
100
+ end
101
+
102
+ # `a:off` and `a:chOff`: a position, in slide space or in a group's own
103
+ # child coordinate space.
104
+ class CT_Point2D < Element
105
+ tag "a:off", "a:chOff"
106
+ required_attr "x", type: SimpleTypes::ST_Coordinate
107
+ required_attr "y", type: SimpleTypes::ST_Coordinate
108
+ end
109
+
110
+ # `a:ext` and `a:chExt`: a size, in slide space or in a group's child space.
111
+ #
112
+ # `a:ext` names two unrelated things in DrawingML: this extent inside an
113
+ # `a:xfrm`, and an entry in an `a:extLst` identified by its `uri`. The
114
+ # registry is keyed by tag, so one class carries both; any element uses
115
+ # only its own half.
116
+ class CT_PositiveSize2D < Element
117
+ tag "a:ext", "a:chExt"
118
+ required_attr "cx", type: SimpleTypes::ST_PositiveCoordinate
119
+ required_attr "cy", type: SimpleTypes::ST_PositiveCoordinate
120
+ # -- the extension-list form --
121
+ optional_attr "uri", type: SimpleTypes::XsdString
122
+ end
123
+
124
+ # `a:xfrm`, a 2-D transform.
125
+ #
126
+ # Also covers CT_GroupTransform2D, which shares the tag inside a group
127
+ # shape and adds the child-space `a:chOff` and `a:chExt`.
128
+ class CT_Transform2D < Element
129
+ tag "a:xfrm", "p:xfrm"
130
+ zero_or_one "a:off", successors: %w[a:ext a:chOff a:chExt]
131
+ zero_or_one "a:ext", successors: %w[a:chOff a:chExt]
132
+ zero_or_one "a:chOff", successors: %w[a:chExt]
133
+ zero_or_one "a:chExt", successors: []
134
+ optional_attr "rot", type: SimpleTypes::ST_Angle, default: 0.0
135
+ optional_attr "flipH", type: SimpleTypes::XsdBoolean, default: false
136
+ optional_attr "flipV", type: SimpleTypes::XsdBoolean, default: false
137
+
138
+ def x = off&.x
139
+
140
+ def x=(value)
141
+ get_or_add_off.x = value
142
+ end
143
+
144
+ def y = off&.y
145
+
146
+ def y=(value)
147
+ get_or_add_off.y = value
148
+ end
149
+
150
+ def cx = ext&.cx
151
+
152
+ def cx=(value)
153
+ get_or_add_ext.cx = value
154
+ end
155
+
156
+ def cy = ext&.cy
157
+
158
+ def cy=(value)
159
+ get_or_add_ext.cy = value
160
+ end
161
+
162
+ # Both children carry required attributes, so a newly created one must
163
+ # be given zeroes rather than left empty and invalid.
164
+ def new_off
165
+ build("a:off").tap do |off|
166
+ off.x = 0
167
+ off.y = 0
168
+ end
169
+ end
170
+
171
+ def new_ext
172
+ build("a:ext").tap do |ext|
173
+ ext.cx = 0
174
+ ext.cy = 0
175
+ end
176
+ end
177
+
178
+ def new_chOff
179
+ build("a:chOff").tap do |off|
180
+ off.x = 0
181
+ off.y = 0
182
+ end
183
+ end
184
+
185
+ def new_chExt
186
+ build("a:chExt").tap do |ext|
187
+ ext.cx = 0
188
+ ext.cy = 0
189
+ end
190
+ end
191
+ end
192
+
193
+ # `p:cNvPr`, the non-visual drawing properties every shape carries.
194
+ class CT_NonVisualDrawingProps < Element
195
+ tag "p:cNvPr"
196
+ zero_or_one "a:hlinkClick", successors: %w[a:hlinkHover a:extLst]
197
+ zero_or_one "a:hlinkHover", successors: %w[a:extLst]
198
+ required_attr "id", type: SimpleTypes::ST_DrawingElementId
199
+ required_attr "name", type: SimpleTypes::XsdString
200
+ end
201
+
202
+ # `p:nvPr`, the application non-visual properties, which is where a
203
+ # placeholder declares itself.
204
+ class CT_ApplicationNonVisualDrawingProps < Element
205
+ tag "p:nvPr"
206
+ zero_or_one "p:ph",
207
+ successors: %w[a:audioCd a:wavAudioFile a:audioFile a:videoFile
208
+ a:quickTimeFile p:custDataLst p:extLst]
209
+ end
210
+
211
+ # `p:ph`, marking a shape as a placeholder.
212
+ class CT_Placeholder < Element
213
+ tag "p:ph"
214
+ optional_attr "type", type: Enum::PP_PLACEHOLDER, default: Enum::PP_PLACEHOLDER::OBJECT
215
+ optional_attr "orient", type: SimpleTypes::ST_Direction,
216
+ default: SimpleTypes::ST_Direction::HORZ
217
+ optional_attr "sz", type: SimpleTypes::ST_PlaceholderSize,
218
+ default: SimpleTypes::ST_PlaceholderSize::FULL
219
+ optional_attr "idx", type: SimpleTypes::XsdUnsignedInt, default: 0
220
+ end
221
+
222
+ # `a:ln`, line (outline) properties.
223
+ # `a:prstDash`, one of the preset dash patterns.
224
+ class CT_PresetLineDashProperties < Element
225
+ tag "a:prstDash"
226
+ optional_attr "val", type: Enum::MSO_LINE_DASH_STYLE
227
+ end
228
+
229
+ class CT_LineProperties < Element
230
+ tag "a:ln"
231
+ FILL_SUCCESSORS = %w[a:prstDash a:custDash a:round a:bevel a:miter
232
+ a:headEnd a:tailEnd a:extLst].freeze
233
+
234
+ zero_or_one_choice [choice("a:noFill"), choice("a:solidFill"),
235
+ choice("a:gradFill"), choice("a:pattFill")],
236
+ successors: FILL_SUCCESSORS, as: :eg_lineFillProperties
237
+ zero_or_one "a:prstDash", successors: FILL_SUCCESSORS[1..]
238
+ zero_or_one "a:custDash", successors: FILL_SUCCESSORS[2..]
239
+ optional_attr "w", type: SimpleTypes::ST_LineWidth, default: Pptx::Length.emu(0)
240
+
241
+ # The fill layer names this differently; both point at the same group.
242
+ def eg_fillProperties = eg_lineFillProperties
243
+
244
+ def prstDash_val = prstDash&.val
245
+
246
+ def prstDash_val=(value)
247
+ remove_custDash
248
+ get_or_add_prstDash.val = value
249
+ end
250
+ end
251
+
252
+ # Shape properties: geometry, fill, line and transform.
253
+ #
254
+ # The same content model appears under three prefixes -- `p:spPr` on a
255
+ # shape, `c:spPr` on a chart element, `a:spPr` in DrawingML -- so one
256
+ # class covers all of them.
257
+ class CT_ShapeProperties < Element
258
+ tag "p:spPr", "c:spPr", "a:spPr"
259
+ TAG_SEQ = %w[a:xfrm a:custGeom a:prstGeom a:noFill a:solidFill a:gradFill
260
+ a:blipFill a:pattFill a:grpFill a:ln a:effectLst a:effectDag
261
+ a:scene3d a:sp3d a:extLst].freeze
262
+
263
+ zero_or_one "a:xfrm", successors: TAG_SEQ[1..]
264
+ zero_or_one "a:custGeom", successors: TAG_SEQ[2..]
265
+ zero_or_one "a:prstGeom", successors: TAG_SEQ[3..]
266
+ zero_or_one_choice [choice("a:noFill"), choice("a:solidFill"), choice("a:gradFill"),
267
+ choice("a:blipFill"), choice("a:pattFill"), choice("a:grpFill")],
268
+ successors: TAG_SEQ[9..], as: :eg_fillProperties
269
+ zero_or_one "a:ln", successors: TAG_SEQ[10..]
270
+ zero_or_one "a:effectLst", successors: TAG_SEQ[11..]
271
+
272
+ def x = xfrm&.x
273
+ def y = xfrm&.y
274
+ def cx = xfrm&.cx
275
+ def cy = xfrm&.cy
276
+
277
+ # A new gradient starts from PowerPoint's default rather than empty; an
278
+ # `a:gradFill` with no stops draws nothing.
279
+ def new_gradFill = CT_GradientFillProperties.new_grad_fill(self)
280
+ end
281
+
282
+ # `p:grpSpPr`, the properties of a group shape or shape tree.
283
+ class CT_GroupShapeProperties < Element
284
+ tag "p:grpSpPr"
285
+ TAG_SEQ = %w[a:xfrm a:noFill a:solidFill a:gradFill a:blipFill a:pattFill
286
+ a:grpFill a:effectLst a:effectDag a:scene3d a:extLst].freeze
287
+
288
+ zero_or_one "a:xfrm", successors: TAG_SEQ[1..]
289
+ zero_or_one "a:effectLst", successors: TAG_SEQ[8..]
290
+ end
291
+ end
292
+ end