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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +83 -0
- data/LICENSE +21 -0
- data/NOTICE +48 -0
- data/README.md +272 -0
- data/lib/ruby_pptx/action.rb +120 -0
- data/lib/ruby_pptx/autoshape_spec.rb +337 -0
- data/lib/ruby_pptx/builder.rb +228 -0
- data/lib/ruby_pptx/chart/categories.rb +208 -0
- data/lib/ruby_pptx/chart/chart.rb +443 -0
- data/lib/ruby_pptx/chart/combo.rb +230 -0
- data/lib/ruby_pptx/chart/data.rb +147 -0
- data/lib/ruby_pptx/chart/format.rb +647 -0
- data/lib/ruby_pptx/chart/workbook_writer.rb +212 -0
- data/lib/ruby_pptx/chart/xml_writer.rb +953 -0
- data/lib/ruby_pptx/chart/xy_data.rb +182 -0
- data/lib/ruby_pptx/core_ext.rb +12 -0
- data/lib/ruby_pptx/dml/color.rb +155 -0
- data/lib/ruby_pptx/dml/effect.rb +33 -0
- data/lib/ruby_pptx/dml/fill.rb +234 -0
- data/lib/ruby_pptx/element_proxy.rb +53 -0
- data/lib/ruby_pptx/enum/action.rb +46 -0
- data/lib/ruby_pptx/enum/base.rb +139 -0
- data/lib/ruby_pptx/enum/chart.rb +278 -0
- data/lib/ruby_pptx/enum/dml.rb +237 -0
- data/lib/ruby_pptx/enum/lang.rb +441 -0
- data/lib/ruby_pptx/enum/prog_id.rb +38 -0
- data/lib/ruby_pptx/enum/shapes.rb +514 -0
- data/lib/ruby_pptx/enum/text.rb +108 -0
- data/lib/ruby_pptx/errors.rb +15 -0
- data/lib/ruby_pptx/geometry.rb +89 -0
- data/lib/ruby_pptx/image.rb +347 -0
- data/lib/ruby_pptx/length.rb +120 -0
- data/lib/ruby_pptx/media.rb +64 -0
- data/lib/ruby_pptx/numeric_lengths.rb +30 -0
- data/lib/ruby_pptx/opc/constants.rb +205 -0
- data/lib/ruby_pptx/opc/oxml.rb +99 -0
- data/lib/ruby_pptx/opc/pack_uri.rb +146 -0
- data/lib/ruby_pptx/opc/package.rb +482 -0
- data/lib/ruby_pptx/opc/serialized.rb +229 -0
- data/lib/ruby_pptx/opc/spec.rb +37 -0
- data/lib/ruby_pptx/oxml/action.rb +43 -0
- data/lib/ruby_pptx/oxml/chart.rb +785 -0
- data/lib/ruby_pptx/oxml/content_model.rb +186 -0
- data/lib/ruby_pptx/oxml/core_properties.rb +145 -0
- data/lib/ruby_pptx/oxml/dml/color.rb +115 -0
- data/lib/ruby_pptx/oxml/dml/fill.rb +137 -0
- data/lib/ruby_pptx/oxml/element.rb +329 -0
- data/lib/ruby_pptx/oxml/ns.rb +115 -0
- data/lib/ruby_pptx/oxml/presentation.rb +110 -0
- data/lib/ruby_pptx/oxml/section.rb +65 -0
- data/lib/ruby_pptx/oxml/shapes/autoshape.rb +310 -0
- data/lib/ruby_pptx/oxml/shapes/groupshape.rb +217 -0
- data/lib/ruby_pptx/oxml/shapes/other.rb +405 -0
- data/lib/ruby_pptx/oxml/shapes/shared.rb +292 -0
- data/lib/ruby_pptx/oxml/simple_types.rb +563 -0
- data/lib/ruby_pptx/oxml/slide.rb +290 -0
- data/lib/ruby_pptx/oxml/table.rb +253 -0
- data/lib/ruby_pptx/oxml/text.rb +324 -0
- data/lib/ruby_pptx/oxml/theme.rb +93 -0
- data/lib/ruby_pptx/package.rb +139 -0
- data/lib/ruby_pptx/parts/chart.rb +75 -0
- data/lib/ruby_pptx/parts/core_properties.rb +40 -0
- data/lib/ruby_pptx/parts/embedded_package.rb +43 -0
- data/lib/ruby_pptx/parts/image.rb +55 -0
- data/lib/ruby_pptx/parts/media.rb +17 -0
- data/lib/ruby_pptx/parts/presentation.rb +79 -0
- data/lib/ruby_pptx/parts/slide.rb +221 -0
- data/lib/ruby_pptx/parts/theme.rb +26 -0
- data/lib/ruby_pptx/pattern_matching.rb +50 -0
- data/lib/ruby_pptx/presentation.rb +128 -0
- data/lib/ruby_pptx/refinements.rb +21 -0
- data/lib/ruby_pptx/section.rb +130 -0
- data/lib/ruby_pptx/shapes/adjustments.rb +83 -0
- data/lib/ruby_pptx/shapes/authoring.rb +110 -0
- data/lib/ruby_pptx/shapes/base.rb +138 -0
- data/lib/ruby_pptx/shapes/freeform.rb +141 -0
- data/lib/ruby_pptx/shapes/placeholder.rb +184 -0
- data/lib/ruby_pptx/shapes/shape.rb +337 -0
- data/lib/ruby_pptx/shapes/shape_tree.rb +634 -0
- data/lib/ruby_pptx/sliceable.rb +34 -0
- data/lib/ruby_pptx/slide.rb +430 -0
- data/lib/ruby_pptx/table.rb +316 -0
- data/lib/ruby_pptx/table_paging.rb +91 -0
- data/lib/ruby_pptx/templates/default.pptx +0 -0
- data/lib/ruby_pptx/templates/docx-icon.emf +0 -0
- data/lib/ruby_pptx/templates/generic-icon.emf +0 -0
- data/lib/ruby_pptx/templates/media-speaker.png +0 -0
- data/lib/ruby_pptx/templates/notes.xml +23 -0
- data/lib/ruby_pptx/templates/notesMaster.xml +352 -0
- data/lib/ruby_pptx/templates/pptx-icon.emf +0 -0
- data/lib/ruby_pptx/templates/slideMaster.xml +277 -0
- data/lib/ruby_pptx/templates/theme.xml +321 -0
- data/lib/ruby_pptx/templates/xlsx-icon.emf +0 -0
- data/lib/ruby_pptx/text/fitter.rb +72 -0
- data/lib/ruby_pptx/text/font_metrics.rb +221 -0
- data/lib/ruby_pptx/text/text.rb +393 -0
- data/lib/ruby_pptx/theme.rb +98 -0
- data/lib/ruby_pptx/version.rb +5 -0
- data/lib/ruby_pptx.rb +90 -0
- metadata +174 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/oxml/shapes/shared"
|
|
4
|
+
require "ruby_pptx/oxml/text"
|
|
5
|
+
|
|
6
|
+
module Pptx
|
|
7
|
+
module Oxml
|
|
8
|
+
# `a:avLst`, the adjustment values of a preset geometry.
|
|
9
|
+
class CT_GeomGuideList < Element
|
|
10
|
+
tag "a:avLst"
|
|
11
|
+
zero_or_more "a:gd", as: :gd
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# `a:gd`, one geometry guide (adjustment value).
|
|
15
|
+
class CT_GeomGuide < Element
|
|
16
|
+
tag "a:gd"
|
|
17
|
+
required_attr "name", type: SimpleTypes::XsdString
|
|
18
|
+
required_attr "fmla", type: SimpleTypes::XsdString
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# `a:prstGeom`, a preset (built-in) shape geometry.
|
|
22
|
+
class CT_PresetGeometry2D < Element
|
|
23
|
+
tag "a:prstGeom"
|
|
24
|
+
zero_or_one "a:avLst", successors: []
|
|
25
|
+
required_attr "prst", type: Enum::MSO_AUTO_SHAPE_TYPE
|
|
26
|
+
|
|
27
|
+
# The adjustment guides, in document order.
|
|
28
|
+
def gd_list = avLst ? avLst.gd_list : []
|
|
29
|
+
|
|
30
|
+
# Replace the adjustment values with +name_value_pairs+.
|
|
31
|
+
def rewrite_guides(name_value_pairs)
|
|
32
|
+
remove_avLst
|
|
33
|
+
av_lst = get_or_add_avLst
|
|
34
|
+
name_value_pairs.each do |name, value|
|
|
35
|
+
av_lst.add_gd(name: name, fmla: "val #{value}")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# `a:custGeom`, a freeform shape geometry.
|
|
41
|
+
class CT_CustomGeometry2D < Element
|
|
42
|
+
tag "a:custGeom"
|
|
43
|
+
zero_or_one "a:pathLst", successors: []
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# `a:pt`, one point of a path.
|
|
47
|
+
class CT_AdjPoint2D < Element
|
|
48
|
+
tag "a:pt"
|
|
49
|
+
required_attr "x", type: SimpleTypes::ST_Coordinate
|
|
50
|
+
required_attr "y", type: SimpleTypes::ST_Coordinate
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# `a:moveTo` and `a:lnTo`: lift the pen to a point, or draw to it.
|
|
54
|
+
class CT_Path2DPoint < Element
|
|
55
|
+
tag "a:moveTo", "a:lnTo"
|
|
56
|
+
zero_or_one "a:pt", successors: []
|
|
57
|
+
|
|
58
|
+
def point_at(x, y)
|
|
59
|
+
point = get_or_add_pt
|
|
60
|
+
point.x = x
|
|
61
|
+
point.y = y
|
|
62
|
+
point
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# `a:close`, ending a contour by joining it back to its start.
|
|
67
|
+
class CT_Path2DClose < Element
|
|
68
|
+
tag "a:close"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# `a:path`, one contour of a freeform shape.
|
|
72
|
+
#
|
|
73
|
+
# Its `w` and `h` are in the path's own coordinate space, not EMU: the
|
|
74
|
+
# shape's extents scale that space onto the slide.
|
|
75
|
+
class CT_Path2D < Element
|
|
76
|
+
tag "a:path"
|
|
77
|
+
zero_or_more "a:close", successors: [], as: :close
|
|
78
|
+
zero_or_more "a:lnTo", successors: [], as: :lnTo
|
|
79
|
+
zero_or_more "a:moveTo", successors: [], as: :moveTo
|
|
80
|
+
optional_attr "w", type: SimpleTypes::ST_PositiveCoordinate
|
|
81
|
+
optional_attr "h", type: SimpleTypes::ST_PositiveCoordinate
|
|
82
|
+
|
|
83
|
+
def move_to(x, y) = add_moveTo.point_at(x, y)
|
|
84
|
+
|
|
85
|
+
def line_to(x, y) = add_lnTo.point_at(x, y)
|
|
86
|
+
|
|
87
|
+
def close_contour = add_close
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# `a:pathLst`, the contours of a freeform shape.
|
|
91
|
+
class CT_Path2DList < Element
|
|
92
|
+
tag "a:pathLst"
|
|
93
|
+
zero_or_more "a:path", successors: [], as: :path
|
|
94
|
+
|
|
95
|
+
def add_path_of(width, height)
|
|
96
|
+
add_path(w: width, h: height)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# `p:cNvSpPr`, the non-visual properties specific to a `p:sp`.
|
|
101
|
+
class CT_NonVisualDrawingShapeProps < Element
|
|
102
|
+
tag "p:cNvSpPr"
|
|
103
|
+
zero_or_one "a:spLocks", successors: %w[a:extLst]
|
|
104
|
+
optional_attr "txBox", type: SimpleTypes::XsdBoolean
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# `p:nvSpPr`, the non-visual properties group of a `p:sp`.
|
|
108
|
+
class CT_ShapeNonVisual < Element
|
|
109
|
+
tag "p:nvSpPr"
|
|
110
|
+
one_and_only_one "p:cNvPr"
|
|
111
|
+
one_and_only_one "p:cNvSpPr"
|
|
112
|
+
one_and_only_one "p:nvPr"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# `p:sp`, an auto shape, text box, placeholder or freeform.
|
|
116
|
+
class CT_Shape < Element
|
|
117
|
+
include BaseShapeElement
|
|
118
|
+
|
|
119
|
+
tag "p:sp"
|
|
120
|
+
one_and_only_one "p:nvSpPr"
|
|
121
|
+
one_and_only_one "p:spPr"
|
|
122
|
+
zero_or_one "p:txBody", successors: %w[p:extLst]
|
|
123
|
+
|
|
124
|
+
# Placeholder types that come with a text frame when created.
|
|
125
|
+
TEXTUAL_PLACEHOLDERS = [
|
|
126
|
+
Enum::PP_PLACEHOLDER::TITLE, Enum::PP_PLACEHOLDER::CENTER_TITLE,
|
|
127
|
+
Enum::PP_PLACEHOLDER::SUBTITLE, Enum::PP_PLACEHOLDER::BODY,
|
|
128
|
+
Enum::PP_PLACEHOLDER::OBJECT
|
|
129
|
+
].freeze
|
|
130
|
+
|
|
131
|
+
class << self
|
|
132
|
+
# A `p:sp` configured as a placeholder inheriting from a layout.
|
|
133
|
+
#
|
|
134
|
+
# The shape carries no geometry or position of its own: a placeholder
|
|
135
|
+
# inherits both from the corresponding placeholder on its layout.
|
|
136
|
+
def new_placeholder_sp(id, name, ph_type, orient, sz, idx)
|
|
137
|
+
sp = Element.parse(<<~XML)
|
|
138
|
+
<p:sp #{Ns.nsdecls("a", "p")}>
|
|
139
|
+
<p:nvSpPr>
|
|
140
|
+
<p:cNvPr id="#{id}" name="#{escape(name)}"/>
|
|
141
|
+
<p:cNvSpPr>
|
|
142
|
+
<a:spLocks noGrp="1"/>
|
|
143
|
+
</p:cNvSpPr>
|
|
144
|
+
<p:nvPr/>
|
|
145
|
+
</p:nvSpPr>
|
|
146
|
+
<p:spPr/>
|
|
147
|
+
</p:sp>
|
|
148
|
+
XML
|
|
149
|
+
|
|
150
|
+
ph = sp.nvSpPr.nvPr.get_or_add_ph
|
|
151
|
+
ph.type = ph_type
|
|
152
|
+
ph.idx = idx
|
|
153
|
+
ph.orient = orient
|
|
154
|
+
ph.sz = sz
|
|
155
|
+
|
|
156
|
+
sp.append(CT_TextBody.new_element(sp)) if TEXTUAL_PLACEHOLDERS.include?(ph_type)
|
|
157
|
+
|
|
158
|
+
sp
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# A `p:sp` configured as an auto shape with preset geometry.
|
|
162
|
+
def new_autoshape_sp(id, name, prst, x, y, cx, cy)
|
|
163
|
+
Element.parse(<<~XML)
|
|
164
|
+
<p:sp #{Ns.nsdecls("a", "p")}>
|
|
165
|
+
<p:nvSpPr>
|
|
166
|
+
<p:cNvPr id="#{id}" name="#{escape(name)}"/>
|
|
167
|
+
<p:cNvSpPr/>
|
|
168
|
+
<p:nvPr/>
|
|
169
|
+
</p:nvSpPr>
|
|
170
|
+
<p:spPr>
|
|
171
|
+
<a:xfrm>
|
|
172
|
+
<a:off x="#{x.to_i}" y="#{y.to_i}"/>
|
|
173
|
+
<a:ext cx="#{cx.to_i}" cy="#{cy.to_i}"/>
|
|
174
|
+
</a:xfrm>
|
|
175
|
+
<a:prstGeom prst="#{prst}">
|
|
176
|
+
<a:avLst/>
|
|
177
|
+
</a:prstGeom>
|
|
178
|
+
</p:spPr>
|
|
179
|
+
#{DEFAULT_STYLE}
|
|
180
|
+
<p:txBody>
|
|
181
|
+
<a:bodyPr rtlCol="0" anchor="ctr"/>
|
|
182
|
+
<a:lstStyle/>
|
|
183
|
+
<a:p>
|
|
184
|
+
<a:pPr algn="ctr"/>
|
|
185
|
+
</a:p>
|
|
186
|
+
</p:txBody>
|
|
187
|
+
</p:sp>
|
|
188
|
+
XML
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# A `p:sp` configured as a freeform: custom geometry with an empty
|
|
192
|
+
# path list, ready for contours to be added.
|
|
193
|
+
def new_freeform_sp(id, name, x, y, cx, cy)
|
|
194
|
+
Element.parse(<<~XML)
|
|
195
|
+
<p:sp #{Ns.nsdecls("a", "p")}>
|
|
196
|
+
<p:nvSpPr>
|
|
197
|
+
<p:cNvPr id="#{id}" name="#{escape(name)}"/>
|
|
198
|
+
<p:cNvSpPr/>
|
|
199
|
+
<p:nvPr/>
|
|
200
|
+
</p:nvSpPr>
|
|
201
|
+
<p:spPr>
|
|
202
|
+
<a:xfrm>
|
|
203
|
+
<a:off x="#{x.to_i}" y="#{y.to_i}"/>
|
|
204
|
+
<a:ext cx="#{cx.to_i}" cy="#{cy.to_i}"/>
|
|
205
|
+
</a:xfrm>
|
|
206
|
+
<a:custGeom>
|
|
207
|
+
<a:avLst/>
|
|
208
|
+
<a:gdLst/>
|
|
209
|
+
<a:ahLst/>
|
|
210
|
+
<a:cxnLst/>
|
|
211
|
+
<a:rect l="l" t="t" r="r" b="b"/>
|
|
212
|
+
<a:pathLst/>
|
|
213
|
+
</a:custGeom>
|
|
214
|
+
</p:spPr>
|
|
215
|
+
#{DEFAULT_STYLE}
|
|
216
|
+
<p:txBody>
|
|
217
|
+
<a:bodyPr rtlCol="0" anchor="ctr"/>
|
|
218
|
+
<a:lstStyle/>
|
|
219
|
+
<a:p>
|
|
220
|
+
<a:pPr algn="ctr"/>
|
|
221
|
+
</a:p>
|
|
222
|
+
</p:txBody>
|
|
223
|
+
</p:sp>
|
|
224
|
+
XML
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# A `p:sp` configured as a text box.
|
|
228
|
+
def new_textbox_sp(id, name, x, y, cx, cy)
|
|
229
|
+
Element.parse(<<~XML)
|
|
230
|
+
<p:sp #{Ns.nsdecls("a", "p")}>
|
|
231
|
+
<p:nvSpPr>
|
|
232
|
+
<p:cNvPr id="#{id}" name="#{escape(name)}"/>
|
|
233
|
+
<p:cNvSpPr txBox="1"/>
|
|
234
|
+
<p:nvPr/>
|
|
235
|
+
</p:nvSpPr>
|
|
236
|
+
<p:spPr>
|
|
237
|
+
<a:xfrm>
|
|
238
|
+
<a:off x="#{x.to_i}" y="#{y.to_i}"/>
|
|
239
|
+
<a:ext cx="#{cx.to_i}" cy="#{cy.to_i}"/>
|
|
240
|
+
</a:xfrm>
|
|
241
|
+
<a:prstGeom prst="rect">
|
|
242
|
+
<a:avLst/>
|
|
243
|
+
</a:prstGeom>
|
|
244
|
+
<a:noFill/>
|
|
245
|
+
</p:spPr>
|
|
246
|
+
<p:txBody>
|
|
247
|
+
<a:bodyPr wrap="none">
|
|
248
|
+
<a:spAutoFit/>
|
|
249
|
+
</a:bodyPr>
|
|
250
|
+
<a:lstStyle/>
|
|
251
|
+
<a:p/>
|
|
252
|
+
</p:txBody>
|
|
253
|
+
</p:sp>
|
|
254
|
+
XML
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def escape(text) = text.to_s.gsub("&", "&").gsub("<", "<").gsub('"', """)
|
|
258
|
+
|
|
259
|
+
DEFAULT_STYLE = <<~XML
|
|
260
|
+
<p:style>
|
|
261
|
+
<a:lnRef idx="1">
|
|
262
|
+
<a:schemeClr val="accent1"/>
|
|
263
|
+
</a:lnRef>
|
|
264
|
+
<a:fillRef idx="3">
|
|
265
|
+
<a:schemeClr val="accent1"/>
|
|
266
|
+
</a:fillRef>
|
|
267
|
+
<a:effectRef idx="2">
|
|
268
|
+
<a:schemeClr val="accent1"/>
|
|
269
|
+
</a:effectRef>
|
|
270
|
+
<a:fontRef idx="minor">
|
|
271
|
+
<a:schemeClr val="lt1"/>
|
|
272
|
+
</a:fontRef>
|
|
273
|
+
</p:style>
|
|
274
|
+
XML
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# A text body added to a shape that has none needs the minimum structure
|
|
278
|
+
# the schema requires, not an empty element.
|
|
279
|
+
def new_txBody = CT_TextBody.new_element(self)
|
|
280
|
+
|
|
281
|
+
# Start a new contour on this shape's custom geometry.
|
|
282
|
+
#
|
|
283
|
+
# @raise [Error] unless the shape has custom geometry
|
|
284
|
+
def add_path(width, height)
|
|
285
|
+
geometry = spPr.custGeom
|
|
286
|
+
raise Error, "shape does not have custom geometry" if geometry.nil?
|
|
287
|
+
|
|
288
|
+
geometry.get_or_add_pathLst.add_path_of(width, height)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def prstGeom = spPr.prstGeom
|
|
292
|
+
|
|
293
|
+
def prst = prstGeom&.prst
|
|
294
|
+
|
|
295
|
+
def ln = spPr.ln
|
|
296
|
+
|
|
297
|
+
def get_or_add_ln = spPr.get_or_add_ln
|
|
298
|
+
|
|
299
|
+
# A shape has custom geometry -- is a freeform -- when it carries
|
|
300
|
+
# `a:custGeom` in place of `a:prstGeom`.
|
|
301
|
+
def custom_geometry? = !spPr.custGeom.nil?
|
|
302
|
+
|
|
303
|
+
# A text box is an `p:sp` whose `p:cNvSpPr` says `txBox="1"`.
|
|
304
|
+
def textbox? = nvSpPr.cNvSpPr.txBox == true
|
|
305
|
+
|
|
306
|
+
# An auto shape has preset geometry and is not a text box.
|
|
307
|
+
def autoshape? = !prstGeom.nil? && nvSpPr.cNvSpPr.txBox != true
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/oxml/shapes/shared"
|
|
4
|
+
require "ruby_pptx/oxml/shapes/autoshape"
|
|
5
|
+
require "ruby_pptx/oxml/shapes/other"
|
|
6
|
+
|
|
7
|
+
module Pptx
|
|
8
|
+
module Oxml
|
|
9
|
+
# `p:nvGrpSpPr`, the non-visual properties of a group shape.
|
|
10
|
+
class CT_GroupShapeNonVisual < Element
|
|
11
|
+
tag "p:nvGrpSpPr"
|
|
12
|
+
one_and_only_one "p:cNvPr"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# `p:spTree` and `p:grpSp`: a shape tree or a group of shapes.
|
|
16
|
+
class CT_GroupShape < Element
|
|
17
|
+
include BaseShapeElement
|
|
18
|
+
|
|
19
|
+
tag "p:spTree", "p:grpSp"
|
|
20
|
+
|
|
21
|
+
one_and_only_one "p:nvGrpSpPr"
|
|
22
|
+
one_and_only_one "p:grpSpPr"
|
|
23
|
+
|
|
24
|
+
# The child tags that count as shapes. Anything else in a shape tree
|
|
25
|
+
# (extension lists and so on) is not part of the shape sequence.
|
|
26
|
+
SHAPE_TAGS = %w[p:sp p:grpSp p:graphicFrame p:cxnSp p:pic p:contentPart].freeze
|
|
27
|
+
|
|
28
|
+
# A group's transform hangs off `p:grpSpPr`, not `p:spPr`.
|
|
29
|
+
def xfrm = grpSpPr.xfrm
|
|
30
|
+
|
|
31
|
+
def get_or_add_xfrm = grpSpPr.get_or_add_xfrm
|
|
32
|
+
|
|
33
|
+
def chOff = get_or_add_xfrm.get_or_add_chOff
|
|
34
|
+
|
|
35
|
+
def chExt = get_or_add_xfrm.get_or_add_chExt
|
|
36
|
+
|
|
37
|
+
# Each shape child, in document order.
|
|
38
|
+
def shape_elements
|
|
39
|
+
return enum_for(:shape_elements) unless block_given?
|
|
40
|
+
|
|
41
|
+
tags = SHAPE_TAGS.map { |t| Ns.qn(t) }
|
|
42
|
+
@node.element_children.each do |child|
|
|
43
|
+
yield Element.wrap(child) if tags.include?(Ns.clark_name_of(child))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Each placeholder shape child, in document order.
|
|
48
|
+
def placeholder_elements
|
|
49
|
+
return enum_for(:placeholder_elements) unless block_given?
|
|
50
|
+
|
|
51
|
+
shape_elements { |shape| yield shape if shape.placeholder? }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def add_placeholder(id, name, ph_type, orient, sz, idx)
|
|
55
|
+
sp = adopt_xml(CT_Shape.new_placeholder_sp(id, name, ph_type, orient, sz, idx))
|
|
56
|
+
insert_element_before(sp, "p:extLst")
|
|
57
|
+
sp
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def add_autoshape(id, name, prst, x, y, cx, cy)
|
|
61
|
+
sp = adopt_xml(CT_Shape.new_autoshape_sp(id, name, prst, x, y, cx, cy))
|
|
62
|
+
insert_element_before(sp, "p:extLst")
|
|
63
|
+
sp
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def add_pic(id, name, desc, r_id, x, y, cx, cy)
|
|
67
|
+
pic = adopt_xml(CT_Picture.new_pic(id, name, desc, r_id, x, y, cx, cy))
|
|
68
|
+
insert_element_before(pic, "p:extLst")
|
|
69
|
+
pic
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def add_graphic_frame_chart(id, name, r_id, x, y, cx, cy)
|
|
73
|
+
frame = adopt_xml(
|
|
74
|
+
CT_GraphicalObjectFrame.new_chart_graphic_frame(id, name, r_id, x, y, cx, cy)
|
|
75
|
+
)
|
|
76
|
+
insert_element_before(frame, "p:extLst")
|
|
77
|
+
frame
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def add_video_pic(id, name, video_r_id, media_r_id, poster_r_id, x, y, cx, cy)
|
|
81
|
+
pic = adopt_xml(CT_Picture.new_video_pic(id, name, video_r_id, media_r_id,
|
|
82
|
+
poster_r_id, x, y, cx, cy))
|
|
83
|
+
insert_element_before(pic, "p:extLst")
|
|
84
|
+
pic
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def add_graphic_frame_ole_object(id, name, ole_r_id, prog_id, icon_r_id, x, y, cx, cy, img_w, img_h)
|
|
88
|
+
frame = adopt_xml(
|
|
89
|
+
CT_GraphicalObjectFrame.new_ole_object_graphic_frame(id, name, ole_r_id, prog_id, icon_r_id,
|
|
90
|
+
x, y, cx, cy, img_w, img_h)
|
|
91
|
+
)
|
|
92
|
+
insert_element_before(frame, "p:extLst")
|
|
93
|
+
frame
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def add_graphic_frame_table(id, name, rows, cols, x, y, cx, cy)
|
|
97
|
+
frame = adopt_xml(
|
|
98
|
+
CT_GraphicalObjectFrame.new_table_graphic_frame(id, name, rows, cols, x, y, cx, cy)
|
|
99
|
+
)
|
|
100
|
+
insert_element_before(frame, "p:extLst")
|
|
101
|
+
frame
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def add_cxnSp(id, name, prst, x, y, cx, cy, flip_h, flip_v)
|
|
105
|
+
cxn_sp = adopt_xml(CT_Connector.new_cxnSp(id, name, prst, x, y, cx, cy, flip_h, flip_v))
|
|
106
|
+
insert_element_before(cxn_sp, "p:extLst")
|
|
107
|
+
cxn_sp
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# An empty `p:grpSp` at the origin with no size; its extents follow from
|
|
111
|
+
# whatever is put in it.
|
|
112
|
+
def add_grpSp
|
|
113
|
+
id = next_shape_id
|
|
114
|
+
grp_sp = adopt_xml(CT_GroupShape.new_grpSp(id, "Group #{id - 1}"))
|
|
115
|
+
insert_element_before(grp_sp, "p:extLst")
|
|
116
|
+
grp_sp
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def self.new_grpSp(id, name)
|
|
120
|
+
Element.parse(<<~XML)
|
|
121
|
+
<p:grpSp #{Ns.nsdecls("a", "p", "r")}>
|
|
122
|
+
<p:nvGrpSpPr>
|
|
123
|
+
<p:cNvPr id="#{id}" name="#{CT_Picture.escape(name)}"/>
|
|
124
|
+
<p:cNvGrpSpPr/>
|
|
125
|
+
<p:nvPr/>
|
|
126
|
+
</p:nvGrpSpPr>
|
|
127
|
+
<p:grpSpPr>
|
|
128
|
+
<a:xfrm>
|
|
129
|
+
<a:off x="0" y="0"/>
|
|
130
|
+
<a:ext cx="0" cy="0"/>
|
|
131
|
+
<a:chOff x="0" y="0"/>
|
|
132
|
+
<a:chExt cx="0" cy="0"/>
|
|
133
|
+
</a:xfrm>
|
|
134
|
+
</p:grpSpPr>
|
|
135
|
+
</p:grpSp>
|
|
136
|
+
XML
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Resize this group to just contain its shapes.
|
|
140
|
+
#
|
|
141
|
+
# A group's own extents are not stored by PowerPoint as anything the
|
|
142
|
+
# caller sets; they follow from the contents. Changing a group can also
|
|
143
|
+
# change the group containing it, so this walks upward.
|
|
144
|
+
def recalculate_extents
|
|
145
|
+
return self unless nsptag == "p:grpSp"
|
|
146
|
+
|
|
147
|
+
x, y, cx, cy = child_extents
|
|
148
|
+
chOff.x = self.x = x
|
|
149
|
+
chOff.y = self.y = y
|
|
150
|
+
chExt.cx = self.cx = cx
|
|
151
|
+
chExt.cy = self.cy = cy
|
|
152
|
+
parent.recalculate_extents if parent.respond_to?(:recalculate_extents)
|
|
153
|
+
self
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The bounding box of the contained shapes, or all zeroes when empty.
|
|
157
|
+
def child_extents
|
|
158
|
+
children = shape_elements.to_a
|
|
159
|
+
return [0, 0, 0, 0] if children.empty?
|
|
160
|
+
|
|
161
|
+
min_x = children.map { |child| child.x.to_i }.min
|
|
162
|
+
min_y = children.map { |child| child.y.to_i }.min
|
|
163
|
+
max_x = children.map { |child| child.x.to_i + child.cx.to_i }.max
|
|
164
|
+
max_y = children.map { |child| child.y.to_i + child.cy.to_i }.max
|
|
165
|
+
[min_x, min_y, max_x - min_x, max_y - min_y]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def add_freeform_sp(x, y, cx, cy)
|
|
169
|
+
id = next_shape_id
|
|
170
|
+
sp = adopt_xml(CT_Shape.new_freeform_sp(id, "Freeform #{id - 1}", x, y, cx, cy))
|
|
171
|
+
insert_element_before(sp, "p:extLst")
|
|
172
|
+
sp
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def add_textbox(id, name, x, y, cx, cy)
|
|
176
|
+
sp = adopt_xml(CT_Shape.new_textbox_sp(id, name, x, y, cx, cy))
|
|
177
|
+
insert_element_before(sp, "p:extLst")
|
|
178
|
+
sp
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# The largest @id anywhere in the document.
|
|
182
|
+
#
|
|
183
|
+
# Ids have document scope and are not used only by shapes, so every @id
|
|
184
|
+
# counts. The minimum in practice is 1, because the `p:spTree` element
|
|
185
|
+
# itself is always id="1".
|
|
186
|
+
def max_shape_id
|
|
187
|
+
used_ids.max || 0
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# The lowest unused positive @id, filling gaps.
|
|
191
|
+
#
|
|
192
|
+
# Note this differs from the shape collection's own allocator, which
|
|
193
|
+
# takes max+1; both behaviours are preserved from python-pptx.
|
|
194
|
+
def next_shape_id
|
|
195
|
+
used = used_ids
|
|
196
|
+
(1..(used.size + 1)).find { |n| !used.include?(n) }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private
|
|
200
|
+
|
|
201
|
+
def used_ids
|
|
202
|
+
xpath("//@id").filter_map do |attr|
|
|
203
|
+
value = attr.value
|
|
204
|
+
Integer(value, 10) if /\A\d+\z/.match?(value)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# A shape built from an XML literal is parsed into its own document, so
|
|
209
|
+
# it must be rebuilt inside this tree's document before insertion.
|
|
210
|
+
def adopt_xml(element)
|
|
211
|
+
return element if element.document.equal?(document)
|
|
212
|
+
|
|
213
|
+
build_from_xml(element.node.to_xml)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|