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,563 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/errors"
|
|
4
|
+
require "ruby_pptx/length"
|
|
5
|
+
|
|
6
|
+
module Pptx
|
|
7
|
+
module Oxml
|
|
8
|
+
# Scalar types that appear as XML attribute values.
|
|
9
|
+
#
|
|
10
|
+
# Each is a class exposing `from_xml` / `to_xml`, mirroring the simple
|
|
11
|
+
# types in the ECMA-376 schema. `to_xml` validates before converting, so an
|
|
12
|
+
# out-of-range value raises here rather than producing a file PowerPoint
|
|
13
|
+
# refuses to open.
|
|
14
|
+
module SimpleTypes
|
|
15
|
+
# Base for every simple type. Subclasses override `convert_from_xml`,
|
|
16
|
+
# `convert_to_xml` and `validate`; the class-method inheritance mirrors
|
|
17
|
+
# the Python original exactly.
|
|
18
|
+
class BaseSimpleType
|
|
19
|
+
class << self
|
|
20
|
+
def from_xml(xml_value) = convert_from_xml(xml_value)
|
|
21
|
+
|
|
22
|
+
def to_xml(value)
|
|
23
|
+
value = normalize(value)
|
|
24
|
+
validate(value)
|
|
25
|
+
convert_to_xml(value)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# python-pptx's `Emu` is an `int` subclass, so a length could be
|
|
29
|
+
# passed anywhere an integer was expected. Our {Pptx::Length} is a
|
|
30
|
+
# separate class, so unwrap it here and every type gains the same
|
|
31
|
+
# affordance: `shape.width = Pptx.inches(1)` works.
|
|
32
|
+
def normalize(value) = value.is_a?(Pptx::Length) ? value.emu : value
|
|
33
|
+
|
|
34
|
+
# Python's `round()` is round-half-to-even; Ruby's `Float#round`
|
|
35
|
+
# defaults to half-up. Every rounding conversion here goes through
|
|
36
|
+
# this helper so values landing exactly on .5 match python-pptx.
|
|
37
|
+
def round_half_even(value) = value.round(half: :even)
|
|
38
|
+
|
|
39
|
+
def validate_float(value)
|
|
40
|
+
return if value.is_a?(Numeric) && !value.is_a?(Complex)
|
|
41
|
+
|
|
42
|
+
raise TypeError, "value must be a number, got #{value.class}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate_int(value)
|
|
46
|
+
return if value.is_a?(Integer)
|
|
47
|
+
|
|
48
|
+
raise TypeError, "value must be an integral type, got #{value.class}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_float_in_range(value, min_inclusive, max_inclusive)
|
|
52
|
+
validate_float(value)
|
|
53
|
+
return if value.between?(min_inclusive, max_inclusive)
|
|
54
|
+
|
|
55
|
+
raise RangeError,
|
|
56
|
+
"value must be in range #{min_inclusive} to #{max_inclusive} inclusive, " \
|
|
57
|
+
"got #{value}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_int_in_range(value, min_inclusive, max_inclusive)
|
|
61
|
+
validate_int(value)
|
|
62
|
+
return if value.between?(min_inclusive, max_inclusive)
|
|
63
|
+
|
|
64
|
+
raise RangeError,
|
|
65
|
+
"value must be in range #{min_inclusive} to #{max_inclusive} inclusive, " \
|
|
66
|
+
"got #{value}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate_string(value)
|
|
70
|
+
return value if value.is_a?(String)
|
|
71
|
+
|
|
72
|
+
raise TypeError, "value must be a string, got #{value.class}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class BaseFloatType < BaseSimpleType
|
|
78
|
+
class << self
|
|
79
|
+
def convert_from_xml(str_value) = Float(str_value)
|
|
80
|
+
def convert_to_xml(value) = Float(value).to_s
|
|
81
|
+
def validate(value) = validate_float(value)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class BaseIntType < BaseSimpleType
|
|
86
|
+
class << self
|
|
87
|
+
def convert_from_percent_literal(str_value) = Integer(str_value.delete("%"), 10)
|
|
88
|
+
def convert_from_xml(str_value) = Integer(str_value, 10)
|
|
89
|
+
def convert_to_xml(value) = value.to_i.to_s
|
|
90
|
+
def validate(value) = validate_int(value)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class BaseStringType < BaseSimpleType
|
|
95
|
+
class << self
|
|
96
|
+
def convert_from_xml(str_value) = str_value
|
|
97
|
+
def convert_to_xml(value) = value
|
|
98
|
+
def validate(value) = validate_string(value)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# A string type restricted to an enumerated set of values.
|
|
103
|
+
class BaseStringEnumerationType < BaseStringType
|
|
104
|
+
class << self
|
|
105
|
+
# Declare the permitted values, each also defined as a constant.
|
|
106
|
+
#
|
|
107
|
+
# values BAR: "bar", COL: "col"
|
|
108
|
+
def values(**mapping)
|
|
109
|
+
mapping.each { |name, value| const_set(name, value) }
|
|
110
|
+
@members = mapping.values.freeze
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def members = @members || superclass.members
|
|
114
|
+
|
|
115
|
+
def validate(value)
|
|
116
|
+
validate_string(value)
|
|
117
|
+
return if members.include?(value)
|
|
118
|
+
|
|
119
|
+
raise ArgumentError, "must be one of #{members.inspect}, got #{value.inspect}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# -- xsd built-ins ---------------------------------------------------
|
|
125
|
+
|
|
126
|
+
# Not validated against the URI grammar; the payoff does not justify it.
|
|
127
|
+
class XsdAnyUri < BaseStringType; end
|
|
128
|
+
|
|
129
|
+
class XsdBoolean < BaseSimpleType
|
|
130
|
+
class << self
|
|
131
|
+
def convert_from_xml(str_value)
|
|
132
|
+
unless %w[1 0 true false].include?(str_value)
|
|
133
|
+
raise InvalidXmlError,
|
|
134
|
+
"value must be one of '1', '0', 'true' or 'false', got #{str_value.inspect}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
%w[1 true].include?(str_value)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def convert_to_xml(value) = value ? "1" : "0"
|
|
141
|
+
|
|
142
|
+
def validate(value)
|
|
143
|
+
return if [true, false].include?(value)
|
|
144
|
+
|
|
145
|
+
raise TypeError, "only true or false may be assigned, got #{value.inspect}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
class XsdDouble < BaseFloatType; end
|
|
151
|
+
|
|
152
|
+
# Must start with a letter or underscore and contain no colon. Not fully
|
|
153
|
+
# validated because it is not reachable from the public API.
|
|
154
|
+
class XsdId < BaseStringType; end
|
|
155
|
+
|
|
156
|
+
class XsdInt < BaseIntType
|
|
157
|
+
def self.validate(value) = validate_int_in_range(value, -2_147_483_648, 2_147_483_647)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
class XsdLong < BaseIntType
|
|
161
|
+
def self.validate(value)
|
|
162
|
+
validate_int_in_range(value, -9_223_372_036_854_775_808, 9_223_372_036_854_775_807)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
class XsdString < BaseStringType; end
|
|
167
|
+
class XsdStringEnumeration < BaseStringEnumerationType; end
|
|
168
|
+
|
|
169
|
+
# xsd:string with whitespace collapsed.
|
|
170
|
+
class XsdToken < BaseStringType; end
|
|
171
|
+
class XsdTokenEnumeration < BaseStringEnumerationType; end
|
|
172
|
+
|
|
173
|
+
class XsdUnsignedByte < BaseIntType
|
|
174
|
+
def self.validate(value) = validate_int_in_range(value, 0, 255)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
class XsdUnsignedInt < BaseIntType
|
|
178
|
+
def self.validate(value) = validate_int_in_range(value, 0, 4_294_967_295)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
class XsdUnsignedShort < BaseIntType
|
|
182
|
+
def self.validate(value) = validate_int_in_range(value, 0, 65_535)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# -- ST_* types from the ECMA-376 schema ------------------------------
|
|
186
|
+
|
|
187
|
+
# `rot` on `<a:xfrm>`: 60000ths of a degree, exposed as float degrees.
|
|
188
|
+
class ST_Angle < XsdInt
|
|
189
|
+
DEGREE_INCREMENTS = 60_000
|
|
190
|
+
THREE_SIXTY = 360 * DEGREE_INCREMENTS
|
|
191
|
+
|
|
192
|
+
class << self
|
|
193
|
+
def convert_from_xml(str_value)
|
|
194
|
+
(Integer(str_value, 10) % THREE_SIXTY).to_f / DEGREE_INCREMENTS
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Normalized to a positive value; the modulo absorbs negative and
|
|
198
|
+
# greater-than-360 inputs.
|
|
199
|
+
def convert_to_xml(value)
|
|
200
|
+
(round_half_even(value * DEGREE_INCREMENTS) % THREE_SIXTY).to_s
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def validate(value) = BaseFloatType.validate(value)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# `val` on c:majorUnit and friends.
|
|
208
|
+
class ST_AxisUnit < XsdDouble
|
|
209
|
+
def self.validate(value)
|
|
210
|
+
super
|
|
211
|
+
raise RangeError, "must be positive numeric value, got #{value}" if value <= 0.0
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
class ST_BarDir < XsdStringEnumeration
|
|
216
|
+
values BAR: "bar", COL: "col"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Integer percent in 0..300, optionally written with a '%' suffix.
|
|
220
|
+
class ST_BubbleScale < BaseIntType
|
|
221
|
+
class << self
|
|
222
|
+
def convert_from_xml(str_value)
|
|
223
|
+
str_value.include?("%") ? convert_from_percent_literal(str_value) : super
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def validate(value) = validate_int_in_range(value, 0, 300)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# The schema gives a fierce regular expression; matching it would catch
|
|
231
|
+
# programming errors only, so it is not enforced.
|
|
232
|
+
class ST_ContentType < XsdString; end
|
|
233
|
+
|
|
234
|
+
# xsd:union of ST_CoordinateUnqualified and ST_UniversalMeasure.
|
|
235
|
+
class ST_Coordinate < BaseSimpleType
|
|
236
|
+
class << self
|
|
237
|
+
def convert_from_xml(str_value)
|
|
238
|
+
if str_value.match?(/[imp]/)
|
|
239
|
+
ST_UniversalMeasure.convert_from_xml(str_value)
|
|
240
|
+
else
|
|
241
|
+
Pptx::Length.emu(Integer(str_value, 10))
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def convert_to_xml(value) = value.to_i.to_s
|
|
246
|
+
def validate(value) = ST_CoordinateUnqualified.validate(value)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# xsd:union of ST_Coordinate32Unqualified and ST_UniversalMeasure.
|
|
251
|
+
class ST_Coordinate32 < BaseSimpleType
|
|
252
|
+
class << self
|
|
253
|
+
def convert_from_xml(str_value)
|
|
254
|
+
if str_value.match?(/[imp]/)
|
|
255
|
+
ST_UniversalMeasure.convert_from_xml(str_value)
|
|
256
|
+
else
|
|
257
|
+
ST_Coordinate32Unqualified.convert_from_xml(str_value)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def convert_to_xml(value) = ST_Coordinate32Unqualified.convert_to_xml(value)
|
|
262
|
+
def validate(value) = ST_Coordinate32Unqualified.validate(value)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
class ST_Coordinate32Unqualified < XsdInt
|
|
267
|
+
def self.convert_from_xml(str_value) = Pptx::Length.emu(Integer(str_value, 10))
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
class ST_CoordinateUnqualified < XsdLong
|
|
271
|
+
def self.validate(value) = validate_int_in_range(value, -27_273_042_329_600, 27_273_042_316_900)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# `orient` on `<p:ph>`.
|
|
275
|
+
class ST_Direction < XsdTokenEnumeration
|
|
276
|
+
values HORZ: "horz", VERT: "vert"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
class ST_DrawingElementId < XsdUnsignedInt; end
|
|
280
|
+
|
|
281
|
+
class ST_Extension < XsdString; end
|
|
282
|
+
|
|
283
|
+
# Integer percent in 0..500, optionally written with a '%' suffix.
|
|
284
|
+
class ST_GapAmount < BaseIntType
|
|
285
|
+
class << self
|
|
286
|
+
def convert_from_xml(str_value)
|
|
287
|
+
str_value.include?("%") ? convert_from_percent_literal(str_value) : super
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def validate(value) = validate_int_in_range(value, 0, 500)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# `val` on <c:grouping>; doubles as ST_BarGrouping, which shares the tag.
|
|
295
|
+
class ST_Grouping < XsdStringEnumeration
|
|
296
|
+
values CLUSTERED: "clustered", PERCENT_STACKED: "percentStacked",
|
|
297
|
+
STACKED: "stacked", STANDARD: "standard"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
class ST_HexColorRGB < BaseStringType
|
|
301
|
+
class << self
|
|
302
|
+
# Upper-cased purely for consistency of output.
|
|
303
|
+
def convert_to_xml(value) = value.upcase
|
|
304
|
+
|
|
305
|
+
def validate(value)
|
|
306
|
+
str_value = validate_string(value)
|
|
307
|
+
unless str_value.length == 6
|
|
308
|
+
raise ArgumentError, "RGB string must be six characters long, got #{str_value.inspect}"
|
|
309
|
+
end
|
|
310
|
+
return if str_value.match?(/\A\h{6}\z/)
|
|
311
|
+
|
|
312
|
+
raise ArgumentError, "RGB string must be a valid hex string, got #{str_value.inspect}"
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# `val` on c:xMode and other CT_LayoutMode elements.
|
|
318
|
+
class ST_LayoutMode < XsdStringEnumeration
|
|
319
|
+
values EDGE: "edge", FACTOR: "factor"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# 0..1000 inclusive, optionally written with a '%' suffix.
|
|
323
|
+
class ST_LblOffset < XsdUnsignedShort
|
|
324
|
+
class << self
|
|
325
|
+
def convert_from_xml(str_value)
|
|
326
|
+
str_value.end_with?("%") ? convert_from_percent_literal(str_value) : Integer(str_value, 10)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def validate(value) = validate_int_in_range(value, 0, 1000)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
class ST_LineWidth < XsdInt
|
|
334
|
+
class << self
|
|
335
|
+
def convert_from_xml(str_value) = Pptx::Length.emu(Integer(str_value, 10))
|
|
336
|
+
|
|
337
|
+
def validate(value)
|
|
338
|
+
super
|
|
339
|
+
return if value.between?(0, 20_116_800)
|
|
340
|
+
|
|
341
|
+
raise RangeError,
|
|
342
|
+
"value must be in range 0-20116800 inclusive (0-1584 points), got #{value}"
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
class ST_MarkerSize < XsdUnsignedByte
|
|
348
|
+
def self.validate(value) = validate_int_in_range(value, 2, 72)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# `val` on c:orientation (CT_Orientation).
|
|
352
|
+
class ST_Orientation < XsdStringEnumeration
|
|
353
|
+
values MAX_MIN: "maxMin", MIN_MAX: "minMax"
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Integer percent in -100..100, optionally written with a '%' suffix.
|
|
357
|
+
class ST_Overlap < BaseIntType
|
|
358
|
+
class << self
|
|
359
|
+
def convert_from_xml(str_value)
|
|
360
|
+
str_value.include?("%") ? convert_from_percent_literal(str_value) : super
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def validate(value) = validate_int_in_range(value, -100, 100)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Either an integer literal in 1000ths of a percent ("42000"), or a float
|
|
368
|
+
# with a '%' suffix ("42.0%"). Exposed as a float fraction, so 0.42.
|
|
369
|
+
class ST_Percentage < BaseIntType
|
|
370
|
+
class << self
|
|
371
|
+
def convert_from_xml(str_value)
|
|
372
|
+
return convert_from_percent_literal_float(str_value) if str_value.include?("%")
|
|
373
|
+
|
|
374
|
+
Integer(str_value, 10) / 100_000.0
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def convert_to_xml(value) = round_half_even(value * 100_000.0).to_s
|
|
378
|
+
|
|
379
|
+
def validate(value) = validate_float_in_range(value, -21_474.83648, 21_474.83647)
|
|
380
|
+
|
|
381
|
+
def convert_from_percent_literal_float(str_value)
|
|
382
|
+
Float(str_value.delete_suffix("%")) / 100.0
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# `sz` on <p:ph>.
|
|
388
|
+
class ST_PlaceholderSize < XsdTokenEnumeration
|
|
389
|
+
values FULL: "full", HALF: "half", QUARTER: "quarter"
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
class ST_PositiveCoordinate < XsdLong
|
|
393
|
+
class << self
|
|
394
|
+
def convert_from_xml(str_value) = Pptx::Length.emu(super)
|
|
395
|
+
def validate(value) = validate_int_in_range(value, 0, 27_273_042_316_900)
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# `a:lin@ang`: positive angles less than 360 degrees.
|
|
400
|
+
class ST_PositiveFixedAngle < ST_Angle
|
|
401
|
+
def self.convert_to_xml(degrees)
|
|
402
|
+
if degrees < 0.0
|
|
403
|
+
degrees = (degrees % -360) + 360
|
|
404
|
+
elsif degrees > 0.0
|
|
405
|
+
degrees %= 360
|
|
406
|
+
end
|
|
407
|
+
round_half_even(degrees * DEGREE_INCREMENTS).to_s
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# ST_Percentage constrained to 0.0..1.0.
|
|
412
|
+
class ST_PositiveFixedPercentage < ST_Percentage
|
|
413
|
+
def self.validate(value) = validate_float_in_range(value, 0.0, 1.0)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
class ST_RelationshipId < XsdString; end
|
|
417
|
+
|
|
418
|
+
class ST_SlideId < XsdUnsignedInt
|
|
419
|
+
def self.validate(value) = validate_int_in_range(value, 256, 2_147_483_647)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# The 36 layout kinds ST_SlideLayoutType allows. PowerPoint uses this to
|
|
423
|
+
# decide which layout to offer for a given command, so an unrecognised
|
|
424
|
+
# value is rejected rather than written through.
|
|
425
|
+
class ST_SlideLayoutType < XsdString
|
|
426
|
+
VALUES = %w[
|
|
427
|
+
title tx twoColTx tbl txAndChart chartAndTx dgm chart txAndClipArt clipArtAndTx
|
|
428
|
+
titleOnly blank txAndObj objAndTx objOnly obj txAndMedia mediaAndTx objOverTx txOverObj
|
|
429
|
+
txAndTwoObj twoObjAndTx twoObjOverTx fourObj vertTx clipArtAndVertTx vertTitleAndTx
|
|
430
|
+
vertTitleAndTxOverChart twoObj objAndTwoObj twoObjAndObj cust secHead twoTxTwoObj objTx
|
|
431
|
+
picTx
|
|
432
|
+
].freeze
|
|
433
|
+
|
|
434
|
+
def self.validate(value)
|
|
435
|
+
super
|
|
436
|
+
return if VALUES.include?(value)
|
|
437
|
+
|
|
438
|
+
raise ArgumentError, "#{value.inspect} is not a slide layout type"
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Slide-master and slide-layout ids share one range, which starts above
|
|
443
|
+
# the signed 32-bit maximum -- PowerPoint numbers them from 2147483648
|
|
444
|
+
# upwards precisely so they cannot be confused with slide ids.
|
|
445
|
+
class ST_SlideMasterId < XsdUnsignedInt
|
|
446
|
+
def self.validate(value) = validate_int_in_range(value, 2_147_483_648, 4_294_967_295)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
class ST_SlideLayoutId < ST_SlideMasterId; end
|
|
450
|
+
|
|
451
|
+
class ST_SlideSizeCoordinate < BaseIntType
|
|
452
|
+
class << self
|
|
453
|
+
def convert_from_xml(str_value) = Pptx::Length.emu(Integer(str_value, 10))
|
|
454
|
+
|
|
455
|
+
def validate(value)
|
|
456
|
+
validate_int(value)
|
|
457
|
+
return if value.between?(914_400, 51_206_400)
|
|
458
|
+
|
|
459
|
+
raise RangeError,
|
|
460
|
+
"value must be in range 914400 to 51206400 (1-56 inches), got #{value}"
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
class ST_Style < XsdUnsignedByte
|
|
466
|
+
def self.validate(value) = validate_int_in_range(value, 1, 48)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# `TargetMode` on a Relationship element.
|
|
470
|
+
class ST_TargetMode < XsdString
|
|
471
|
+
EXTERNAL = "External"
|
|
472
|
+
INTERNAL = "Internal"
|
|
473
|
+
|
|
474
|
+
def self.validate(value)
|
|
475
|
+
validate_string(value)
|
|
476
|
+
return if [EXTERNAL, INTERNAL].include?(value)
|
|
477
|
+
|
|
478
|
+
raise ArgumentError, "must be one of 'Internal' or 'External', got #{value.inspect}"
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# `fontScale` on <a:normAutofit>, as a float percent.
|
|
483
|
+
class ST_TextFontScalePercentOrPercentString < BaseFloatType
|
|
484
|
+
class << self
|
|
485
|
+
def convert_from_xml(str_value)
|
|
486
|
+
return Float(str_value.delete_suffix("%")) if str_value.end_with?("%")
|
|
487
|
+
|
|
488
|
+
Integer(str_value, 10) / 1000.0
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def convert_to_xml(value) = (value * 1000.0).to_i.to_s
|
|
492
|
+
|
|
493
|
+
def validate(value)
|
|
494
|
+
BaseFloatType.validate(value)
|
|
495
|
+
return if value.between?(1.0, 100.0)
|
|
496
|
+
|
|
497
|
+
raise RangeError, "value must be in range 1.0..100.0 (percent), got #{value}"
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
class ST_TextFontSize < BaseIntType
|
|
503
|
+
def self.validate(value) = validate_int_in_range(value, 100, 400_000)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
class ST_TextIndentLevelType < BaseIntType
|
|
507
|
+
def self.validate(value) = validate_int_in_range(value, 0, 8)
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Line spacing as a multiple of line height, so 1.75 <-> "175000".
|
|
511
|
+
class ST_TextSpacingPercentOrPercentString < BaseFloatType
|
|
512
|
+
class << self
|
|
513
|
+
def convert_from_xml(str_value)
|
|
514
|
+
return Float(str_value.delete_suffix("%")) / 100.0 if str_value.end_with?("%")
|
|
515
|
+
|
|
516
|
+
Integer(str_value, 10) / 100_000.0
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def convert_to_xml(value) = round_half_even(value * 100_000.0).to_s
|
|
520
|
+
|
|
521
|
+
def validate(value) = validate_float_in_range(value, 0.0, 132.0)
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Reads centipoints, exposes a {Pptx::Length}.
|
|
526
|
+
class ST_TextSpacingPoint < BaseIntType
|
|
527
|
+
class << self
|
|
528
|
+
def convert_from_xml(str_value) = Pptx::Length.centipoints(Integer(str_value, 10))
|
|
529
|
+
def convert_to_xml(value) = Pptx::Length.emu(value).centipoints.to_s
|
|
530
|
+
def validate(value) = validate_int_in_range(value, 0, 20_116_800)
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
class ST_TextTypeface < XsdString; end
|
|
535
|
+
|
|
536
|
+
# `wrap` on <a:bodyPr>.
|
|
537
|
+
class ST_TextWrappingType < XsdTokenEnumeration
|
|
538
|
+
values NONE: "none", SQUARE: "square"
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# A float with a two-character unit suffix, e.g. "1.5in".
|
|
542
|
+
class ST_UniversalMeasure < BaseSimpleType
|
|
543
|
+
MULTIPLIERS = {
|
|
544
|
+
"mm" => 36_000, "cm" => 360_000, "in" => 914_400,
|
|
545
|
+
"pt" => 12_700, "pc" => 152_400, "pi" => 152_400
|
|
546
|
+
}.freeze
|
|
547
|
+
|
|
548
|
+
class << self
|
|
549
|
+
def convert_from_xml(str_value)
|
|
550
|
+
units = str_value[-2..]
|
|
551
|
+
multiplier = MULTIPLIERS.fetch(units) do
|
|
552
|
+
raise InvalidXmlError, "unknown measurement unit #{units.inspect} in #{str_value.inspect}"
|
|
553
|
+
end
|
|
554
|
+
Pptx::Length.emu(round_half_even(Float(str_value[0...-2]) * multiplier))
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def convert_to_xml(value) = value.to_i.to_s
|
|
558
|
+
def validate(value) = validate_int(value)
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
end
|