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,324 @@
|
|
|
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/oxml/dml/fill"
|
|
7
|
+
require "ruby_pptx/enum/text"
|
|
8
|
+
require "ruby_pptx/enum/lang"
|
|
9
|
+
|
|
10
|
+
module Pptx
|
|
11
|
+
module Oxml
|
|
12
|
+
# The fill choice and its successor list, shared by run properties and
|
|
13
|
+
# shape properties.
|
|
14
|
+
FILL_CHOICES = %w[a:noFill a:solidFill a:gradFill a:blipFill a:pattFill a:grpFill].freeze
|
|
15
|
+
|
|
16
|
+
# `a:t`, the literal text of a run.
|
|
17
|
+
class CT_TextCharacterText < Element
|
|
18
|
+
tag "a:t"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# `a:r`, a run: a span of text sharing one set of character properties.
|
|
22
|
+
class CT_RegularTextRun < Element
|
|
23
|
+
tag "a:r"
|
|
24
|
+
zero_or_one "a:rPr", successors: %w[a:t]
|
|
25
|
+
one_and_only_one "a:t"
|
|
26
|
+
|
|
27
|
+
def text = t.text
|
|
28
|
+
|
|
29
|
+
def text=(value)
|
|
30
|
+
t.text = CT_RegularTextRun.escape_control_chars(value.to_s)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# XML 1.0 cannot carry most control characters, so PowerPoint writes
|
|
34
|
+
# them as a literal escape like "_x0007_". Tab and line-feed are legal
|
|
35
|
+
# and pass through unchanged.
|
|
36
|
+
def self.escape_control_chars(string)
|
|
37
|
+
string.gsub(/([\x00-\x08\x0B-\x1F])/) { format("_x%04X_", ::Regexp.last_match(1).ord) }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# `a:br`, a soft line break within a paragraph.
|
|
42
|
+
class CT_TextLineBreak < Element
|
|
43
|
+
tag "a:br"
|
|
44
|
+
zero_or_one "a:rPr", successors: []
|
|
45
|
+
|
|
46
|
+
# A break carries no text of its own; it *is* the line feed, which we
|
|
47
|
+
# report as a vertical tab to match how PowerPoint puts it on the
|
|
48
|
+
# clipboard.
|
|
49
|
+
def text = "\v"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# `a:fld`, a slide-number or date field.
|
|
53
|
+
class CT_TextField < Element
|
|
54
|
+
tag "a:fld"
|
|
55
|
+
zero_or_one "a:rPr", successors: %w[a:pPr a:t]
|
|
56
|
+
zero_or_one "a:t", successors: []
|
|
57
|
+
|
|
58
|
+
def text = t&.text.to_s
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# `a:latin`, `a:ea`, `a:cs` and `a:sym`: a typeface reference.
|
|
62
|
+
class CT_TextFont < Element
|
|
63
|
+
tag "a:latin", "a:ea", "a:cs", "a:sym"
|
|
64
|
+
required_attr "typeface", type: SimpleTypes::ST_TextTypeface
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# `a:rPr`, `a:defRPr` and `a:endParaRPr`: character properties.
|
|
68
|
+
class CT_TextCharacterProperties < Element
|
|
69
|
+
tag "a:rPr", "a:defRPr", "a:endParaRPr"
|
|
70
|
+
|
|
71
|
+
FILL_SUCCESSORS = %w[a:effectLst a:effectDag a:highlight a:uLnTx a:uLn
|
|
72
|
+
a:uFillTx a:uFill a:latin a:ea a:cs a:sym
|
|
73
|
+
a:hlinkClick a:hlinkMouseOver a:rtl a:extLst].freeze
|
|
74
|
+
|
|
75
|
+
zero_or_one_choice FILL_CHOICES.map { |t| choice(t) },
|
|
76
|
+
successors: FILL_SUCCESSORS, as: :eg_fillProperties
|
|
77
|
+
zero_or_one "a:latin",
|
|
78
|
+
successors: %w[a:ea a:cs a:sym a:hlinkClick a:hlinkMouseOver a:rtl a:extLst]
|
|
79
|
+
zero_or_one "a:hlinkClick", successors: %w[a:hlinkMouseOver a:rtl a:extLst]
|
|
80
|
+
|
|
81
|
+
optional_attr "lang", type: Enum::MSO_LANGUAGE_ID
|
|
82
|
+
optional_attr "sz", type: SimpleTypes::ST_TextFontSize
|
|
83
|
+
optional_attr "b", type: SimpleTypes::XsdBoolean
|
|
84
|
+
optional_attr "i", type: SimpleTypes::XsdBoolean
|
|
85
|
+
optional_attr "u", type: Enum::MSO_TEXT_UNDERLINE_TYPE
|
|
86
|
+
|
|
87
|
+
def new_gradFill = CT_GradientFillProperties.new_grad_fill(self)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# `a:bodyPr`, the properties of a text body: margins, anchoring, autofit.
|
|
91
|
+
class CT_TextBodyProperties < Element
|
|
92
|
+
tag "a:bodyPr"
|
|
93
|
+
|
|
94
|
+
zero_or_one_choice [choice("a:noAutofit"), choice("a:normAutofit"), choice("a:spAutoFit")],
|
|
95
|
+
successors: %w[a:scene3d a:sp3d a:flatTx a:extLst], as: :eg_textAutoFit
|
|
96
|
+
|
|
97
|
+
optional_attr "lIns", type: SimpleTypes::ST_Coordinate32, default: Pptx::Length.emu(91_440)
|
|
98
|
+
optional_attr "tIns", type: SimpleTypes::ST_Coordinate32, default: Pptx::Length.emu(45_720)
|
|
99
|
+
optional_attr "rIns", type: SimpleTypes::ST_Coordinate32, default: Pptx::Length.emu(91_440)
|
|
100
|
+
optional_attr "bIns", type: SimpleTypes::ST_Coordinate32, default: Pptx::Length.emu(45_720)
|
|
101
|
+
optional_attr "anchor", type: Enum::MSO_VERTICAL_ANCHOR
|
|
102
|
+
optional_attr "wrap", type: SimpleTypes::ST_TextWrappingType
|
|
103
|
+
|
|
104
|
+
# @return [Pptx::Enum::MSO_AUTO_SIZE, nil] nil when inherited
|
|
105
|
+
def autofit
|
|
106
|
+
return Enum::MSO_AUTO_SIZE::NONE unless find("a:noAutofit").nil?
|
|
107
|
+
return Enum::MSO_AUTO_SIZE::TEXT_TO_FIT_SHAPE unless find("a:normAutofit").nil?
|
|
108
|
+
return Enum::MSO_AUTO_SIZE::SHAPE_TO_FIT_TEXT unless find("a:spAutoFit").nil?
|
|
109
|
+
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def autofit=(value)
|
|
114
|
+
remove_eg_textAutoFit
|
|
115
|
+
return if value.nil?
|
|
116
|
+
|
|
117
|
+
member = Enum::MSO_AUTO_SIZE.fetch(value)
|
|
118
|
+
case member.name
|
|
119
|
+
when :NONE then add_noAutofit
|
|
120
|
+
when :TEXT_TO_FIT_SHAPE then add_normAutofit
|
|
121
|
+
when :SHAPE_TO_FIT_TEXT then add_spAutoFit
|
|
122
|
+
else
|
|
123
|
+
raise ArgumentError, "#{member} cannot be assigned as an autofit setting"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# `a:normAutofit`, shrink-text-on-overflow with its font scale.
|
|
129
|
+
class CT_TextNormalAutofit < Element
|
|
130
|
+
tag "a:normAutofit"
|
|
131
|
+
optional_attr "fontScale", type: SimpleTypes::ST_TextFontScalePercentOrPercentString,
|
|
132
|
+
default: 100.0
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# `a:spcPct`, spacing as a multiple of line height.
|
|
136
|
+
class CT_TextSpacingPercent < Element
|
|
137
|
+
tag "a:spcPct"
|
|
138
|
+
required_attr "val", type: SimpleTypes::ST_TextSpacingPercentOrPercentString
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# `a:spcPts`, spacing as a fixed distance.
|
|
142
|
+
class CT_TextSpacingPoint < Element
|
|
143
|
+
tag "a:spcPts"
|
|
144
|
+
required_attr "val", type: SimpleTypes::ST_TextSpacingPoint
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# `a:lnSpc`, `a:spcBef` and `a:spcAft`: a spacing value, expressed either
|
|
148
|
+
# as a multiple of line height or as a fixed distance.
|
|
149
|
+
class CT_TextSpacing < Element
|
|
150
|
+
tag "a:lnSpc", "a:spcBef", "a:spcAft"
|
|
151
|
+
# Strictly a one-and-only-one choice; modelling it as two optional
|
|
152
|
+
# children is simpler and the setters keep only one present.
|
|
153
|
+
zero_or_one "a:spcPct", successors: []
|
|
154
|
+
zero_or_one "a:spcPts", successors: []
|
|
155
|
+
|
|
156
|
+
# Spacing as a multiple of line height, e.g. 1.75 lines.
|
|
157
|
+
def set_spc_pct(value)
|
|
158
|
+
remove_spcPts
|
|
159
|
+
get_or_add_spcPct.val = value
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Spacing as a fixed distance.
|
|
163
|
+
def set_spc_pts(value)
|
|
164
|
+
remove_spcPct
|
|
165
|
+
get_or_add_spcPts.val = value
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# `a:pPr`, paragraph properties.
|
|
170
|
+
class CT_TextParagraphProperties < Element
|
|
171
|
+
tag "a:pPr"
|
|
172
|
+
TAG_SEQ = %w[a:lnSpc a:spcBef a:spcAft a:buClrTx a:buClr a:buSzTx a:buSzPct
|
|
173
|
+
a:buSzPts a:buFontTx a:buFont a:buNone a:buAutoNum a:buChar
|
|
174
|
+
a:buBlip a:tabLst a:defRPr a:extLst].freeze
|
|
175
|
+
|
|
176
|
+
zero_or_one "a:lnSpc", successors: TAG_SEQ[1..]
|
|
177
|
+
zero_or_one "a:spcBef", successors: TAG_SEQ[2..]
|
|
178
|
+
zero_or_one "a:spcAft", successors: TAG_SEQ[3..]
|
|
179
|
+
zero_or_one "a:defRPr", successors: TAG_SEQ[16..]
|
|
180
|
+
optional_attr "lvl", type: SimpleTypes::ST_TextIndentLevelType, default: 0
|
|
181
|
+
optional_attr "algn", type: Enum::PP_PARAGRAPH_ALIGNMENT
|
|
182
|
+
|
|
183
|
+
# A Float means a number of lines; a {Pptx::Length} means a fixed
|
|
184
|
+
# distance. nil when no spacing is set.
|
|
185
|
+
def line_spacing
|
|
186
|
+
spacing = lnSpc
|
|
187
|
+
return nil if spacing.nil?
|
|
188
|
+
|
|
189
|
+
spacing.spcPts ? spacing.spcPts.val : spacing.spcPct&.val
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def line_spacing=(value)
|
|
193
|
+
remove_lnSpc
|
|
194
|
+
return if value.nil?
|
|
195
|
+
|
|
196
|
+
spacing = get_or_add_lnSpc
|
|
197
|
+
value.is_a?(Pptx::Length) ? spacing.set_spc_pts(value) : spacing.set_spc_pct(value)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def space_before = spcBef&.spcPts&.val
|
|
201
|
+
|
|
202
|
+
def space_before=(value)
|
|
203
|
+
remove_spcBef
|
|
204
|
+
get_or_add_spcBef.set_spc_pts(value) unless value.nil?
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def space_after = spcAft&.spcPts&.val
|
|
208
|
+
|
|
209
|
+
def space_after=(value)
|
|
210
|
+
remove_spcAft
|
|
211
|
+
get_or_add_spcAft.set_spc_pts(value) unless value.nil?
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# `a:p`, a paragraph.
|
|
216
|
+
class CT_TextParagraph < Element
|
|
217
|
+
tag "a:p"
|
|
218
|
+
zero_or_one "a:pPr", successors: %w[a:r a:br a:fld a:endParaRPr]
|
|
219
|
+
zero_or_more "a:r", successors: %w[a:endParaRPr], as: :r
|
|
220
|
+
zero_or_more "a:br", successors: %w[a:endParaRPr], as: :br
|
|
221
|
+
zero_or_one "a:endParaRPr", successors: []
|
|
222
|
+
|
|
223
|
+
CONTENT_TAGS = %w[a:r a:br a:fld].freeze
|
|
224
|
+
|
|
225
|
+
# A run is created with its required `a:t` already in place.
|
|
226
|
+
def new_r = build_from_xml(%(<a:r #{Ns.nsdecls("a")}><a:t/></a:r>))
|
|
227
|
+
|
|
228
|
+
def add_run(text = nil)
|
|
229
|
+
run = add_r
|
|
230
|
+
run.text = text if text && !text.empty?
|
|
231
|
+
run
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def add_line_break = add_br
|
|
235
|
+
|
|
236
|
+
# Append +text+ as runs, turning each "\n" or "\v" into a line break.
|
|
237
|
+
#
|
|
238
|
+
# Breaks go *between* items, so leading text never produces one, and an
|
|
239
|
+
# empty segment contributes no run.
|
|
240
|
+
def append_text(text)
|
|
241
|
+
text.to_s.split(/[\n\v]/, -1).each_with_index do |segment, index|
|
|
242
|
+
add_line_break if index.positive?
|
|
243
|
+
add_run(segment) unless segment.empty?
|
|
244
|
+
end
|
|
245
|
+
self
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# The `a:r`, `a:br` and `a:fld` children, in document order.
|
|
249
|
+
def content_children
|
|
250
|
+
tags = CONTENT_TAGS.map { |t| Ns.qn(t) }
|
|
251
|
+
@node.element_children
|
|
252
|
+
.select { |c| tags.include?(Ns.clark_name_of(c)) }
|
|
253
|
+
.map { |c| Element.wrap(c) }
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def text = content_children.map(&:text).join
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# `p:txBody`, `a:txBody` and `c:txPr`: a body of text.
|
|
260
|
+
class CT_TextBody < Element
|
|
261
|
+
# `c:rich` is a text body under a different name: it is what a chart or
|
|
262
|
+
# axis title holds.
|
|
263
|
+
tag "p:txBody", "a:txBody", "c:txPr", "c:rich"
|
|
264
|
+
one_and_only_one "a:bodyPr"
|
|
265
|
+
one_or_more "a:p", successors: [], as: :p
|
|
266
|
+
|
|
267
|
+
# A chart's `c:txPr` holds a paragraph for its formatting alone; the
|
|
268
|
+
# default run properties of that first paragraph are where a chart,
|
|
269
|
+
# axis, legend or data-label font lives.
|
|
270
|
+
def defRPr = p_list.first.get_or_add_pPr.get_or_add_defRPr
|
|
271
|
+
|
|
272
|
+
# The `c:txPr` a chart element gains when its font is first set.
|
|
273
|
+
TXPR_XML = <<~XML.freeze
|
|
274
|
+
<c:txPr #{Ns.nsdecls("c", "a")}>
|
|
275
|
+
<a:bodyPr/>
|
|
276
|
+
<a:lstStyle/>
|
|
277
|
+
<a:p>
|
|
278
|
+
<a:pPr>
|
|
279
|
+
<a:defRPr/>
|
|
280
|
+
</a:pPr>
|
|
281
|
+
</a:p>
|
|
282
|
+
</c:txPr>
|
|
283
|
+
XML
|
|
284
|
+
|
|
285
|
+
class << self
|
|
286
|
+
# A `p:txBody` with one empty paragraph, built in +context+'s document.
|
|
287
|
+
def new_element(context) = context.build_from_xml(txbody_xml)
|
|
288
|
+
|
|
289
|
+
# A standalone `p:txBody`, for callers with no element to build from.
|
|
290
|
+
def parse_new = Element.parse(txbody_xml)
|
|
291
|
+
|
|
292
|
+
def txbody_xml
|
|
293
|
+
<<~XML
|
|
294
|
+
<p:txBody #{Ns.nsdecls("a", "p")}>
|
|
295
|
+
<a:bodyPr/>
|
|
296
|
+
<a:lstStyle/>
|
|
297
|
+
<a:p/>
|
|
298
|
+
</p:txBody>
|
|
299
|
+
XML
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Remove every paragraph, leaving any other children alone.
|
|
304
|
+
def clear_content
|
|
305
|
+
p_list.each { |paragraph| remove(paragraph) }
|
|
306
|
+
self
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Restore the minimum the schema requires: at least one paragraph.
|
|
310
|
+
def unclear_content
|
|
311
|
+
add_p if p_list.empty?
|
|
312
|
+
self
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# True when the body holds a single empty paragraph.
|
|
316
|
+
def empty?
|
|
317
|
+
paragraphs = p_list
|
|
318
|
+
raise InvalidXmlError, "a text body must have at least one a:p" if paragraphs.empty?
|
|
319
|
+
|
|
320
|
+
paragraphs.size == 1 && paragraphs.first.text.empty?
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
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/oxml/dml/color"
|
|
7
|
+
require "ruby_pptx/oxml/text"
|
|
8
|
+
|
|
9
|
+
module Pptx
|
|
10
|
+
module Oxml
|
|
11
|
+
# `a:theme`, the root of a theme part.
|
|
12
|
+
#
|
|
13
|
+
# Only the parts a caller can reasonably edit are modelled: the colour
|
|
14
|
+
# scheme and the font scheme. The format scheme -- fills, lines, effects
|
|
15
|
+
# and background fills, keyed by the `idx` values that shapes and slide
|
|
16
|
+
# backgrounds refer to -- is carried through from the base theme unchanged.
|
|
17
|
+
# See PORTING.md for why.
|
|
18
|
+
class CT_OfficeStyleSheet < Element
|
|
19
|
+
tag "a:theme"
|
|
20
|
+
one_and_only_one "a:themeElements"
|
|
21
|
+
required_attr "name", type: SimpleTypes::XsdString
|
|
22
|
+
|
|
23
|
+
def self.new_default = Oxml.parse_from_template("theme")
|
|
24
|
+
|
|
25
|
+
def clrScheme = themeElements.clrScheme
|
|
26
|
+
def fontScheme = themeElements.fontScheme
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# `a:themeElements`, the four schemes that make up a theme.
|
|
30
|
+
class CT_BaseStyles < Element
|
|
31
|
+
tag "a:themeElements"
|
|
32
|
+
one_and_only_one "a:clrScheme"
|
|
33
|
+
one_and_only_one "a:fontScheme"
|
|
34
|
+
one_and_only_one "a:fmtScheme"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# `a:clrScheme`, the twelve theme colours.
|
|
38
|
+
class CT_ColorScheme < Element
|
|
39
|
+
tag "a:clrScheme"
|
|
40
|
+
required_attr "name", type: SimpleTypes::XsdString
|
|
41
|
+
|
|
42
|
+
# In schema order, which is also the order PowerPoint shows them in.
|
|
43
|
+
SLOTS = %w[dk1 lt1 dk2 lt2 accent1 accent2 accent3 accent4 accent5
|
|
44
|
+
accent6 hlink folHlink].freeze
|
|
45
|
+
|
|
46
|
+
SLOTS.each do |slot|
|
|
47
|
+
one_and_only_one "a:#{slot}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def slot(name)
|
|
51
|
+
raise NotFoundError, "no theme colour named #{name}" unless SLOTS.include?(name.to_s)
|
|
52
|
+
|
|
53
|
+
send(name.to_s)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# One slot of a colour scheme, such as `a:accent1`.
|
|
58
|
+
#
|
|
59
|
+
# Every slot holds the same six-way colour choice as any other colour in
|
|
60
|
+
# DrawingML, so {Pptx::ColorFormat} drives them unchanged.
|
|
61
|
+
class CT_ThemeColor < Element
|
|
62
|
+
include ColorChoice
|
|
63
|
+
|
|
64
|
+
tag(*CT_ColorScheme::SLOTS.map { |slot| "a:#{slot}" })
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# `a:fontScheme`, the major (heading) and minor (body) font collections.
|
|
68
|
+
class CT_FontScheme < Element
|
|
69
|
+
tag "a:fontScheme"
|
|
70
|
+
required_attr "name", type: SimpleTypes::XsdString
|
|
71
|
+
one_and_only_one "a:majorFont"
|
|
72
|
+
one_and_only_one "a:minorFont"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# `a:majorFont` and `a:minorFont`.
|
|
76
|
+
#
|
|
77
|
+
# The Latin typeface is the one that matters here; the East Asian and
|
|
78
|
+
# complex-script slots are required by the schema and are left as the base
|
|
79
|
+
# theme has them unless set.
|
|
80
|
+
class CT_FontCollection < Element
|
|
81
|
+
tag "a:majorFont", "a:minorFont"
|
|
82
|
+
one_and_only_one "a:latin"
|
|
83
|
+
one_and_only_one "a:ea"
|
|
84
|
+
one_and_only_one "a:cs"
|
|
85
|
+
|
|
86
|
+
def typeface = latin.typeface
|
|
87
|
+
|
|
88
|
+
def typeface=(value)
|
|
89
|
+
latin.typeface = value
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/opc/package"
|
|
4
|
+
require "ruby_pptx/opc/constants"
|
|
5
|
+
require "ruby_pptx/parts/core_properties"
|
|
6
|
+
require "ruby_pptx/parts/media"
|
|
7
|
+
require "ruby_pptx/parts/presentation"
|
|
8
|
+
require "ruby_pptx/parts/slide"
|
|
9
|
+
require "ruby_pptx/parts/theme"
|
|
10
|
+
|
|
11
|
+
module Pptx
|
|
12
|
+
# A .pptx package: an OPC package that knows about PowerPoint's parts.
|
|
13
|
+
class Package < Opc::OpcPackage
|
|
14
|
+
# The core-properties part, created if the package has none.
|
|
15
|
+
def core_properties
|
|
16
|
+
@core_properties ||= begin
|
|
17
|
+
part_related_by(Opc::RELATIONSHIP_TYPE::CORE_PROPERTIES)
|
|
18
|
+
rescue NotFoundError
|
|
19
|
+
Parts::CorePropertiesPart.default(self).tap do |part|
|
|
20
|
+
relate_to(part, Opc::RELATIONSHIP_TYPE::CORE_PROPERTIES)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def presentation_part = main_document_part
|
|
26
|
+
|
|
27
|
+
# The media part holding +video+, created if the package has no part with
|
|
28
|
+
# the same content. Matched by SHA-1, as images are.
|
|
29
|
+
def get_or_add_media_part(video)
|
|
30
|
+
find_media_part(video.sha1) || Parts::MediaPart.new_media(self, video)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The image part holding +image_file+, created if the package has no part
|
|
34
|
+
# with the same content.
|
|
35
|
+
#
|
|
36
|
+
# Images are matched by SHA-1 of their bytes, so the same picture used on
|
|
37
|
+
# several slides is stored once.
|
|
38
|
+
def get_or_add_image_part(image_file)
|
|
39
|
+
image = Image.from_file(image_file)
|
|
40
|
+
find_image_part(image.sha1) || Parts::ImagePart.new_image(self, image)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The next free `/ppt/media/imageN.<ext>` partname, reusing gaps.
|
|
44
|
+
def next_image_partname(ext) = next_media_like_partname("image", ext)
|
|
45
|
+
|
|
46
|
+
# The next free `/ppt/media/mediaN.<ext>` partname, reusing gaps.
|
|
47
|
+
def next_media_partname(ext) = next_media_like_partname("media", ext)
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def find_media_part(sha1)
|
|
52
|
+
media_parts.find { |part| part.respond_to?(:sha1) && part.sha1 == sha1 }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Parts reached by a media relationship, each once.
|
|
56
|
+
def media_parts
|
|
57
|
+
seen = {}.compare_by_identity
|
|
58
|
+
each_rel.filter_map do |rel|
|
|
59
|
+
next if rel.external? || rel.reltype != Opc::RELATIONSHIP_TYPE::MEDIA
|
|
60
|
+
|
|
61
|
+
part = rel.target_part
|
|
62
|
+
next if seen.key?(part)
|
|
63
|
+
|
|
64
|
+
seen[part] = true
|
|
65
|
+
part
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def find_image_part(sha1)
|
|
70
|
+
image_parts.find { |part| part.respond_to?(:sha1) && part.sha1 == sha1 }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Parts reached by an image relationship, each once.
|
|
74
|
+
#
|
|
75
|
+
# Scoped by relationship rather than by class on purpose: the package
|
|
76
|
+
# thumbnail is also an image part, but it is related as a thumbnail, and
|
|
77
|
+
# matching a new picture against it would relate the picture by the wrong
|
|
78
|
+
# reltype.
|
|
79
|
+
def image_parts
|
|
80
|
+
seen = {}.compare_by_identity
|
|
81
|
+
each_rel.filter_map do |rel|
|
|
82
|
+
next if rel.external? || rel.reltype != Opc::RELATIONSHIP_TYPE::IMAGE
|
|
83
|
+
|
|
84
|
+
part = rel.target_part
|
|
85
|
+
next if seen.key?(part)
|
|
86
|
+
|
|
87
|
+
seen[part] = true
|
|
88
|
+
part
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def next_media_like_partname(stem, ext)
|
|
93
|
+
prefix = "/ppt/media/#{stem}"
|
|
94
|
+
indexes = parts.filter_map do |part|
|
|
95
|
+
name = part.partname.to_s
|
|
96
|
+
part.partname.idx if name.start_with?(prefix)
|
|
97
|
+
end.sort
|
|
98
|
+
|
|
99
|
+
idx = indexes.each_with_index.find { |used, i| (i + 1) < used }&.last&.succ ||
|
|
100
|
+
(indexes.size + 1)
|
|
101
|
+
Opc::PackURI.new("#{prefix}#{idx}.#{ext}")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Register the part classes for the content types this library models. Anything
|
|
107
|
+
# not listed here loads as a plain Opc::Part, which is enough to carry it
|
|
108
|
+
# through a round-trip untouched.
|
|
109
|
+
{
|
|
110
|
+
Pptx::Opc::CONTENT_TYPE::PML_PRESENTATION_MAIN => Pptx::Parts::PresentationPart,
|
|
111
|
+
Pptx::Opc::CONTENT_TYPE::PML_PRES_MACRO_MAIN => Pptx::Parts::PresentationPart,
|
|
112
|
+
Pptx::Opc::CONTENT_TYPE::PML_SLIDE => Pptx::Parts::SlidePart,
|
|
113
|
+
Pptx::Opc::CONTENT_TYPE::PML_SLIDE_LAYOUT => Pptx::Parts::SlideLayoutPart,
|
|
114
|
+
Pptx::Opc::CONTENT_TYPE::PML_SLIDE_MASTER => Pptx::Parts::SlideMasterPart,
|
|
115
|
+
Pptx::Opc::CONTENT_TYPE::PML_NOTES_MASTER => Pptx::Parts::NotesMasterPart,
|
|
116
|
+
Pptx::Opc::CONTENT_TYPE::OFC_THEME => Pptx::Parts::ThemePart,
|
|
117
|
+
Pptx::Opc::CONTENT_TYPE::PML_NOTES_SLIDE => Pptx::Parts::NotesSlidePart,
|
|
118
|
+
Pptx::Opc::CONTENT_TYPE::OPC_CORE_PROPERTIES => Pptx::Parts::CorePropertiesPart,
|
|
119
|
+
Pptx::Opc::CONTENT_TYPE::PNG => Pptx::Parts::ImagePart,
|
|
120
|
+
Pptx::Opc::CONTENT_TYPE::JPEG => Pptx::Parts::ImagePart,
|
|
121
|
+
Pptx::Opc::CONTENT_TYPE::GIF => Pptx::Parts::ImagePart,
|
|
122
|
+
Pptx::Opc::CONTENT_TYPE::BMP => Pptx::Parts::ImagePart,
|
|
123
|
+
Pptx::Opc::CONTENT_TYPE::TIFF => Pptx::Parts::ImagePart,
|
|
124
|
+
Pptx::Opc::CONTENT_TYPE::MS_PHOTO => Pptx::Parts::ImagePart,
|
|
125
|
+
Pptx::Opc::CONTENT_TYPE::X_EMF => Pptx::Parts::ImagePart,
|
|
126
|
+
Pptx::Opc::CONTENT_TYPE::X_WMF => Pptx::Parts::ImagePart,
|
|
127
|
+
Pptx::Opc::CONTENT_TYPE::DML_CHART => Pptx::Parts::ChartPart,
|
|
128
|
+
Pptx::Opc::CONTENT_TYPE::SML_SHEET => Pptx::Parts::EmbeddedXlsxPart,
|
|
129
|
+
Pptx::Opc::CONTENT_TYPE::ASF => Pptx::Parts::MediaPart,
|
|
130
|
+
Pptx::Opc::CONTENT_TYPE::AVI => Pptx::Parts::MediaPart,
|
|
131
|
+
Pptx::Opc::CONTENT_TYPE::MOV => Pptx::Parts::MediaPart,
|
|
132
|
+
Pptx::Opc::CONTENT_TYPE::MP4 => Pptx::Parts::MediaPart,
|
|
133
|
+
Pptx::Opc::CONTENT_TYPE::MPG => Pptx::Parts::MediaPart,
|
|
134
|
+
Pptx::Opc::CONTENT_TYPE::MS_VIDEO => Pptx::Parts::MediaPart,
|
|
135
|
+
Pptx::Opc::CONTENT_TYPE::SWF => Pptx::Parts::MediaPart,
|
|
136
|
+
Pptx::Opc::CONTENT_TYPE::VIDEO => Pptx::Parts::MediaPart,
|
|
137
|
+
Pptx::Opc::CONTENT_TYPE::WMV => Pptx::Parts::MediaPart,
|
|
138
|
+
Pptx::Opc::CONTENT_TYPE::X_MS_VIDEO => Pptx::Parts::MediaPart
|
|
139
|
+
}.each { |content_type, part_class| Pptx::Opc::PartFactory.register(content_type, part_class) }
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/opc/package"
|
|
4
|
+
require "ruby_pptx/opc/constants"
|
|
5
|
+
require "ruby_pptx/oxml/chart"
|
|
6
|
+
require "ruby_pptx/chart/chart"
|
|
7
|
+
|
|
8
|
+
module Pptx
|
|
9
|
+
module Parts
|
|
10
|
+
# Another OPC package embedded inside this one -- here, the Excel workbook
|
|
11
|
+
# that backs a chart.
|
|
12
|
+
class EmbeddedXlsxPart < Opc::Part
|
|
13
|
+
PARTNAME_TEMPLATE = "/ppt/embeddings/Microsoft_Excel_Sheet%d.xlsx"
|
|
14
|
+
|
|
15
|
+
def self.new_xlsx(blob, package)
|
|
16
|
+
new(package.next_partname(PARTNAME_TEMPLATE),
|
|
17
|
+
Opc::CONTENT_TYPE::SML_SHEET, package, blob)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# A chart part, `/ppt/charts/chartN.xml`.
|
|
22
|
+
class ChartPart < Opc::XmlPart
|
|
23
|
+
PARTNAME_TEMPLATE = "/ppt/charts/chart%d.xml"
|
|
24
|
+
|
|
25
|
+
# A new chart part depicting +chart_data+, together with the embedded
|
|
26
|
+
# workbook that backs it.
|
|
27
|
+
def self.new_chart(chart_type, chart_data, package)
|
|
28
|
+
part = load(package.next_partname(PARTNAME_TEMPLATE), Opc::CONTENT_TYPE::DML_CHART,
|
|
29
|
+
package, ChartXmlWriter.write(chart_type, chart_data))
|
|
30
|
+
part.workbook.replace_with(chart_data.xlsx_blob)
|
|
31
|
+
part
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# A chart part drawing several plots over the same categories.
|
|
35
|
+
def self.new_combo_chart(builder, package)
|
|
36
|
+
part = load(package.next_partname(PARTNAME_TEMPLATE), Opc::CONTENT_TYPE::DML_CHART,
|
|
37
|
+
package, ComboChartWriter.new(builder.validate!).xml)
|
|
38
|
+
part.workbook.replace_with(builder.chart_data.xlsx_blob)
|
|
39
|
+
part
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def chart = @chart ||= Chart.new(element, self)
|
|
43
|
+
|
|
44
|
+
def workbook = @workbook ||= ChartWorkbook.new(element, self)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The embedded workbook behind a chart.
|
|
48
|
+
class ChartWorkbook
|
|
49
|
+
def initialize(chart_space, chart_part)
|
|
50
|
+
@chart_space = chart_space
|
|
51
|
+
@chart_part = chart_part
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [EmbeddedXlsxPart, nil]
|
|
55
|
+
def xlsx_part
|
|
56
|
+
r_id = @chart_space.xlsx_part_rId
|
|
57
|
+
r_id && @chart_part.related_part(r_id)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Put +xlsx_blob+ behind this chart, adding the part and the
|
|
61
|
+
# `c:externalData` link if the chart does not have one yet.
|
|
62
|
+
def replace_with(xlsx_blob)
|
|
63
|
+
existing = xlsx_part
|
|
64
|
+
return existing.blob = xlsx_blob if existing
|
|
65
|
+
|
|
66
|
+
part = EmbeddedXlsxPart.new_xlsx(xlsx_blob, @chart_part.package)
|
|
67
|
+
r_id = @chart_part.relate_to(part, Opc::RELATIONSHIP_TYPE::PACKAGE)
|
|
68
|
+
external_data = @chart_space.get_or_add_externalData
|
|
69
|
+
external_data.rId = r_id
|
|
70
|
+
external_data.ensure_auto_update
|
|
71
|
+
part
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/opc/package"
|
|
4
|
+
require "ruby_pptx/opc/constants"
|
|
5
|
+
require "ruby_pptx/oxml/core_properties"
|
|
6
|
+
|
|
7
|
+
module Pptx
|
|
8
|
+
module Parts
|
|
9
|
+
# `/docProps/core.xml`, the Dublin Core metadata for the package.
|
|
10
|
+
#
|
|
11
|
+
# Reads and writes are delegated straight to the root element, so this
|
|
12
|
+
# class is mostly a named home for them.
|
|
13
|
+
class CorePropertiesPart < Opc::XmlPart
|
|
14
|
+
DELEGATED = (Oxml::CT_CoreProperties::TEXT_PROPERTIES.keys +
|
|
15
|
+
Oxml::CT_CoreProperties::DATETIME_PROPERTIES.keys +
|
|
16
|
+
[:revision]).freeze
|
|
17
|
+
|
|
18
|
+
DELEGATED.each do |name|
|
|
19
|
+
define_method(name) { element.public_send(name) }
|
|
20
|
+
define_method("#{name}=") { |value| element.public_send("#{name}=", value) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# A core-properties part for a package that has none, matching what
|
|
24
|
+
# PowerPoint would write for a new presentation.
|
|
25
|
+
def self.default(package)
|
|
26
|
+
part = new(
|
|
27
|
+
Opc::PackURI.new("/docProps/core.xml"),
|
|
28
|
+
Opc::CONTENT_TYPE::OPC_CORE_PROPERTIES,
|
|
29
|
+
package,
|
|
30
|
+
Oxml::CT_CoreProperties.new_element
|
|
31
|
+
)
|
|
32
|
+
part.title = "PowerPoint Presentation"
|
|
33
|
+
part.last_modified_by = "ruby_pptx"
|
|
34
|
+
part.revision = 1
|
|
35
|
+
part.modified = Time.now.utc
|
|
36
|
+
part
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|