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,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("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
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
|