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,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/oxml/element"
|
|
4
|
+
|
|
5
|
+
module Pptx
|
|
6
|
+
module Oxml
|
|
7
|
+
# The class macros that declare an element's content model.
|
|
8
|
+
#
|
|
9
|
+
# This replaces python-pptx's `MetaOxmlElement` metaclass. Where Python
|
|
10
|
+
# collected descriptor objects out of the class dict and post-processed
|
|
11
|
+
# them, Ruby can just generate the methods as each macro runs, which reads
|
|
12
|
+
# far more directly.
|
|
13
|
+
#
|
|
14
|
+
# Generated method names follow the declared property name +name+:
|
|
15
|
+
#
|
|
16
|
+
# | macro | generates |
|
|
17
|
+
# |---------------------|------------------------------------------------------|
|
|
18
|
+
# | `optional_attr` | `name`, `name=` |
|
|
19
|
+
# | `required_attr` | `name`, `name=` |
|
|
20
|
+
# | `one_and_only_one` | `name` (raises if absent) |
|
|
21
|
+
# | `zero_or_one` | `name`, `get_or_add_name`, `add_name`, `new_name`,
|
|
22
|
+
# `insert_name`, `remove_name` |
|
|
23
|
+
# | `zero_or_more` | `name_list`, `add_name`, `new_name`, `insert_name` |
|
|
24
|
+
# | `one_or_more` | as `zero_or_more` |
|
|
25
|
+
# | `zero_or_one_choice`| `name`, `remove_name`, plus per-choice accessors and `get_or_change_to_x` |
|
|
26
|
+
#
|
|
27
|
+
# python-pptx generates a private `_add_x` and, for repeating elements, a
|
|
28
|
+
# separate public `add_x`. Here they are one `add_x` taking keyword
|
|
29
|
+
# arguments, which is simpler but means a hand-written convenience wrapper
|
|
30
|
+
# cannot reuse the name -- call it something like `add_x_for` instead.
|
|
31
|
+
module ContentModel
|
|
32
|
+
# A member of an `EG_*` element group, for {#zero_or_one_choice}.
|
|
33
|
+
Choice = Data.define(:nsptag) do
|
|
34
|
+
def prop_name = Ns.split_tag(nsptag).last
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Declare one member of a choice group.
|
|
38
|
+
def choice(nsptag) = Choice.new(nsptag)
|
|
39
|
+
|
|
40
|
+
# An attribute that may be absent. Reading returns +default+ when it is;
|
|
41
|
+
# assigning +default+ removes it, so a document never carries an
|
|
42
|
+
# attribute that only restates the schema default.
|
|
43
|
+
def optional_attr(xml_name, type:, default: nil, as: nil)
|
|
44
|
+
name = as || attr_prop_name(xml_name)
|
|
45
|
+
|
|
46
|
+
define_method(name) do
|
|
47
|
+
raw = get(xml_name)
|
|
48
|
+
raw.nil? ? default : type.from_xml(raw)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
define_method("#{name}=") do |value|
|
|
52
|
+
if value == default || value.nil?
|
|
53
|
+
delete_attribute(xml_name)
|
|
54
|
+
else
|
|
55
|
+
set(xml_name, type.to_xml(value))
|
|
56
|
+
end
|
|
57
|
+
value
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# An attribute the schema requires. Reading raises when it is missing,
|
|
62
|
+
# because that means the document is invalid rather than merely sparse.
|
|
63
|
+
def required_attr(xml_name, type:, as: nil)
|
|
64
|
+
name = as || attr_prop_name(xml_name)
|
|
65
|
+
|
|
66
|
+
define_method(name) do
|
|
67
|
+
raw = get(xml_name)
|
|
68
|
+
if raw.nil?
|
|
69
|
+
raise InvalidXmlError,
|
|
70
|
+
"required #{xml_name.inspect} attribute not present on <#{nsptag}>"
|
|
71
|
+
end
|
|
72
|
+
type.from_xml(raw)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
define_method("#{name}=") { |value| set(xml_name, type.to_xml(value)) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A required child element.
|
|
79
|
+
def one_and_only_one(nsptag, as: nil)
|
|
80
|
+
name = as || Ns.split_tag(nsptag).last
|
|
81
|
+
|
|
82
|
+
define_method(name) do
|
|
83
|
+
find(nsptag) ||
|
|
84
|
+
raise(InvalidXmlError, "required <#{nsptag}> child element not present")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# An optional child element.
|
|
89
|
+
def zero_or_one(nsptag, successors: [], as: nil)
|
|
90
|
+
name = as || Ns.split_tag(nsptag).last
|
|
91
|
+
|
|
92
|
+
define_method(name) { find(nsptag) }
|
|
93
|
+
define_creator(name, nsptag)
|
|
94
|
+
define_inserter(name, successors)
|
|
95
|
+
define_adder(name)
|
|
96
|
+
define_once("get_or_add_#{name}") { find(nsptag) || public_send("add_#{name}") }
|
|
97
|
+
define_once("remove_#{name}") { remove_all(nsptag) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A child element that may repeat. The singular accessor is deliberately
|
|
101
|
+
# not generated -- with a repeating element there is no "the" one.
|
|
102
|
+
def zero_or_more(nsptag, successors: [], as: nil)
|
|
103
|
+
name = as || Ns.split_tag(nsptag).last
|
|
104
|
+
|
|
105
|
+
define_method("#{name}_list") { find_all(nsptag) }
|
|
106
|
+
define_creator(name, nsptag)
|
|
107
|
+
define_inserter(name, successors)
|
|
108
|
+
define_adder(name)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A child element that must appear at least once. Structurally identical
|
|
112
|
+
# to {#zero_or_more}; the distinction is documentation of the schema.
|
|
113
|
+
def one_or_more(nsptag, successors: [], as: nil)
|
|
114
|
+
zero_or_more(nsptag, successors: successors, as: as)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# An `EG_*` group, at most one member of which may be present.
|
|
118
|
+
def zero_or_one_choice(choices, as:, successors: [])
|
|
119
|
+
member_tags = choices.map(&:nsptag)
|
|
120
|
+
|
|
121
|
+
define_method(as) { first_child_found_in(*member_tags) }
|
|
122
|
+
define_once("remove_#{as}") { remove_all(*member_tags) }
|
|
123
|
+
|
|
124
|
+
choices.each { |c| define_choice(c, as, successors) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def define_choice(choice, group_name, successors)
|
|
130
|
+
name = choice.prop_name
|
|
131
|
+
nsptag = choice.nsptag
|
|
132
|
+
|
|
133
|
+
define_method(name) { find(nsptag) }
|
|
134
|
+
define_creator(name, nsptag)
|
|
135
|
+
define_inserter(name, successors)
|
|
136
|
+
define_adder(name)
|
|
137
|
+
|
|
138
|
+
# Swapping in a different group member means clearing whichever one is
|
|
139
|
+
# there now; two members present at once would be invalid.
|
|
140
|
+
define_once("get_or_change_to_#{name}") do
|
|
141
|
+
find(nsptag) || begin
|
|
142
|
+
public_send("remove_#{group_name}")
|
|
143
|
+
public_send("add_#{name}")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def define_creator(name, nsptag)
|
|
149
|
+
define_once("new_#{name}") { build(nsptag) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def define_inserter(name, successors)
|
|
153
|
+
define_once("insert_#{name}") do |child|
|
|
154
|
+
insert_element_before(child, *successors)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def define_adder(name)
|
|
159
|
+
define_once("add_#{name}") do |**attrs|
|
|
160
|
+
child = public_send("new_#{name}")
|
|
161
|
+
attrs.each { |attr, value| child.public_send("#{attr}=", value) }
|
|
162
|
+
public_send("insert_#{name}", child)
|
|
163
|
+
child
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Define +name+ unless the class (or an ancestor) already has it, so a
|
|
168
|
+
# hand-written override placed above the macro wins. A definition placed
|
|
169
|
+
# below the macro wins by simply replacing it.
|
|
170
|
+
def define_once(name, &)
|
|
171
|
+
return if method_defined?(name) || private_method_defined?(name)
|
|
172
|
+
|
|
173
|
+
define_method(name, &)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# "r:embed" -> "embed", "macro" -> "macro"
|
|
177
|
+
def attr_prop_name(xml_name)
|
|
178
|
+
xml_name.to_s.include?(":") ? Ns.split_tag(xml_name.to_s).last : xml_name.to_s
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
class Element
|
|
183
|
+
extend ContentModel
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "date"
|
|
5
|
+
require "ruby_pptx/oxml/element"
|
|
6
|
+
require "ruby_pptx/oxml/content_model"
|
|
7
|
+
require "ruby_pptx/oxml/ns"
|
|
8
|
+
|
|
9
|
+
module Pptx
|
|
10
|
+
module Oxml
|
|
11
|
+
# `cp:coreProperties`, the root of `/docProps/core.xml`.
|
|
12
|
+
#
|
|
13
|
+
# Implements the Dublin Core metadata elements. Text properties read as an
|
|
14
|
+
# empty string when absent and are capped at 255 characters, as the schema
|
|
15
|
+
# requires.
|
|
16
|
+
class CT_CoreProperties < Element
|
|
17
|
+
tag "cp:coreProperties"
|
|
18
|
+
|
|
19
|
+
TEXT_PROPERTIES = {
|
|
20
|
+
category: "cp:category",
|
|
21
|
+
content_status: "cp:contentStatus",
|
|
22
|
+
author: "dc:creator",
|
|
23
|
+
comments: "dc:description",
|
|
24
|
+
identifier: "dc:identifier",
|
|
25
|
+
keywords: "cp:keywords",
|
|
26
|
+
language: "dc:language",
|
|
27
|
+
last_modified_by: "cp:lastModifiedBy",
|
|
28
|
+
subject: "dc:subject",
|
|
29
|
+
title: "dc:title",
|
|
30
|
+
version: "cp:version"
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
DATETIME_PROPERTIES = {
|
|
34
|
+
created: "dcterms:created",
|
|
35
|
+
last_printed: "cp:lastPrinted",
|
|
36
|
+
modified: "dcterms:modified"
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# `dcterms:created` and `dcterms:modified` must carry an explicit
|
|
40
|
+
# xsi:type; the others must not.
|
|
41
|
+
W3CDTF_TYPED = %i[created modified].freeze
|
|
42
|
+
|
|
43
|
+
MAX_TEXT_LENGTH = 255
|
|
44
|
+
|
|
45
|
+
(TEXT_PROPERTIES.values + DATETIME_PROPERTIES.values + ["cp:revision"]).each do |nsptag|
|
|
46
|
+
zero_or_one nsptag, successors: []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.new_element
|
|
50
|
+
Element.parse(%(<cp:coreProperties #{Ns.nsdecls("cp", "dc", "dcterms")}/>))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
TEXT_PROPERTIES.each do |name, nsptag|
|
|
54
|
+
define_method(name) { find(nsptag)&.text.to_s }
|
|
55
|
+
|
|
56
|
+
define_method("#{name}=") do |value|
|
|
57
|
+
value = value.to_s
|
|
58
|
+
if value.length > MAX_TEXT_LENGTH
|
|
59
|
+
raise ArgumentError,
|
|
60
|
+
"#{name} exceeds the #{MAX_TEXT_LENGTH}-character limit (got #{value.length})"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
public_send("get_or_add_#{Ns.split_tag(nsptag).last}").text = value
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
DATETIME_PROPERTIES.each do |name, nsptag|
|
|
68
|
+
define_method(name) do
|
|
69
|
+
raw = find(nsptag)&.text
|
|
70
|
+
raw.nil? || raw.empty? ? nil : CT_CoreProperties.parse_w3cdtf(raw)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
define_method("#{name}=") do |value|
|
|
74
|
+
raise TypeError, "#{name} requires a Time, got #{value.class}" unless value.is_a?(Time)
|
|
75
|
+
|
|
76
|
+
element = public_send("get_or_add_#{Ns.split_tag(nsptag).last}")
|
|
77
|
+
element.text = value.getutc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
78
|
+
if W3CDTF_TYPED.include?(name)
|
|
79
|
+
# PowerPoint declares xsi on the root, not on each child that uses
|
|
80
|
+
# it, so declare it here first and let the child find it in scope.
|
|
81
|
+
declare_namespace("xsi")
|
|
82
|
+
element.set("xsi:type", "dcterms:W3CDTF")
|
|
83
|
+
end
|
|
84
|
+
value
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A non-integer, negative or absent revision all read as 0.
|
|
89
|
+
def revision
|
|
90
|
+
raw = find("cp:revision")&.text
|
|
91
|
+
return 0 if raw.nil?
|
|
92
|
+
|
|
93
|
+
value = Integer(raw, 10, exception: false) || 0
|
|
94
|
+
value.negative? ? 0 : value
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def revision=(value)
|
|
98
|
+
unless value.is_a?(Integer) && value >= 1
|
|
99
|
+
raise ArgumentError, "revision requires a positive Integer, got #{value.inspect}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
get_or_add_revision.text = value.to_s
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Parse a W3CDTF timestamp, which may be a year, a year-month, a date,
|
|
106
|
+
# or a full timestamp with a `Z` or numeric offset. Returns UTC, or nil
|
|
107
|
+
# when the value does not parse.
|
|
108
|
+
#
|
|
109
|
+
# Each format must consume the *whole* string. Ruby's `strptime` is happy
|
|
110
|
+
# to parse a prefix and discard the rest, so without that check "%Y"
|
|
111
|
+
# would match "2003-12-31T10:14:55" and silently yield 2003-01-01.
|
|
112
|
+
FORMATS = ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%d", "%Y-%m", "%Y"].freeze
|
|
113
|
+
|
|
114
|
+
def self.parse_w3cdtf(value)
|
|
115
|
+
parseable = value[0, 19]
|
|
116
|
+
offset = value[19..].to_s
|
|
117
|
+
|
|
118
|
+
parts = FORMATS.lazy.filter_map { |format| full_match(parseable, format) }.first
|
|
119
|
+
return nil if parts.nil?
|
|
120
|
+
|
|
121
|
+
time = Time.utc(parts[:year], parts[:mon] || 1, parts[:mday] || 1,
|
|
122
|
+
parts[:hour] || 0, parts[:min] || 0, parts[:sec] || 0)
|
|
123
|
+
apply_offset(time, offset)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Parsed fields for +value+, but only if +format+ consumed all of it.
|
|
127
|
+
def self.full_match(value, format)
|
|
128
|
+
parts = Date._strptime(value, format)
|
|
129
|
+
parts if parts && parts[:leftover].nil? && parts[:year]
|
|
130
|
+
end
|
|
131
|
+
private_class_method :full_match
|
|
132
|
+
|
|
133
|
+
# A trailing "+02:00" means the stamp is local to that offset; shift it
|
|
134
|
+
# back to UTC. A "Z" or an absent offset is already UTC.
|
|
135
|
+
def self.apply_offset(time, offset)
|
|
136
|
+
match = /\A([+-])(\d\d):(\d\d)\z/.match(offset)
|
|
137
|
+
return time unless match
|
|
138
|
+
|
|
139
|
+
sign = match[1] == "+" ? -1 : 1
|
|
140
|
+
time + (sign * ((Integer(match[2], 10) * 3600) + (Integer(match[3], 10) * 60)))
|
|
141
|
+
end
|
|
142
|
+
private_class_method :apply_offset
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
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/dml"
|
|
7
|
+
|
|
8
|
+
module Pptx
|
|
9
|
+
module Oxml
|
|
10
|
+
# The colour elements share a luminance-adjustment pair: `a:lumMod` scales
|
|
11
|
+
# the colour (a shade) and `a:lumOff` lightens it (a tint).
|
|
12
|
+
module BaseColorElement
|
|
13
|
+
def self.included(base)
|
|
14
|
+
base.class_eval do
|
|
15
|
+
zero_or_one "a:lumMod", successors: []
|
|
16
|
+
zero_or_one "a:lumOff", successors: []
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_lumMod(value) = add_lumMod_element(value)
|
|
21
|
+
|
|
22
|
+
def add_lumOff(value) = add_lumOff_element(value)
|
|
23
|
+
|
|
24
|
+
# Remove any luminance adjustment, returning self.
|
|
25
|
+
def clear_lum
|
|
26
|
+
remove_lumMod
|
|
27
|
+
remove_lumOff
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def add_lumMod_element(value)
|
|
34
|
+
el = get_or_add_lumMod
|
|
35
|
+
el.val = value
|
|
36
|
+
el
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_lumOff_element(value)
|
|
40
|
+
el = get_or_add_lumOff
|
|
41
|
+
el.val = value
|
|
42
|
+
el
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# `a:lumMod` and `a:lumOff`.
|
|
47
|
+
class CT_Percentage < Element
|
|
48
|
+
tag "a:lumMod", "a:lumOff"
|
|
49
|
+
required_attr "val", type: SimpleTypes::ST_Percentage
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# `a:srgbClr`, a literal RGB colour.
|
|
53
|
+
class CT_SRgbColor < Element
|
|
54
|
+
include BaseColorElement
|
|
55
|
+
|
|
56
|
+
tag "a:srgbClr"
|
|
57
|
+
required_attr "val", type: SimpleTypes::ST_HexColorRGB
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# `a:schemeClr`, a reference to a theme colour.
|
|
61
|
+
class CT_SchemeColor < Element
|
|
62
|
+
include BaseColorElement
|
|
63
|
+
|
|
64
|
+
tag "a:schemeClr"
|
|
65
|
+
required_attr "val", type: Enum::MSO_THEME_COLOR
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# `a:hslClr`, a hue/saturation/luminance colour.
|
|
69
|
+
class CT_HslColor < Element
|
|
70
|
+
include BaseColorElement
|
|
71
|
+
|
|
72
|
+
tag "a:hslClr"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# `a:sysClr`, a system colour such as "windowText".
|
|
76
|
+
class CT_SystemColor < Element
|
|
77
|
+
include BaseColorElement
|
|
78
|
+
|
|
79
|
+
tag "a:sysClr"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# `a:prstClr`, a named preset colour.
|
|
83
|
+
class CT_PresetColor < Element
|
|
84
|
+
include BaseColorElement
|
|
85
|
+
|
|
86
|
+
tag "a:prstClr"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# `a:scrgbClr`, a percentage-based RGB colour.
|
|
90
|
+
class CT_ScRgbColor < Element
|
|
91
|
+
include BaseColorElement
|
|
92
|
+
|
|
93
|
+
tag "a:scrgbClr"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The six-way colour choice shared by everything that carries a colour.
|
|
97
|
+
module ColorChoice
|
|
98
|
+
COLOR_CHOICES = %w[a:scrgbClr a:srgbClr a:hslClr a:sysClr a:schemeClr a:prstClr].freeze
|
|
99
|
+
|
|
100
|
+
def self.included(base)
|
|
101
|
+
base.class_eval do
|
|
102
|
+
zero_or_one_choice ColorChoice::COLOR_CHOICES.map { |t| choice(t) },
|
|
103
|
+
successors: [], as: :eg_colorChoice
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# `a:fgClr` and `a:bgClr`, which are colours and nothing else.
|
|
109
|
+
class CT_Color < Element
|
|
110
|
+
include ColorChoice
|
|
111
|
+
|
|
112
|
+
tag "a:fgClr", "a:bgClr"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/oxml/dml/color"
|
|
4
|
+
|
|
5
|
+
module Pptx
|
|
6
|
+
module Oxml
|
|
7
|
+
# `a:solidFill`, a single flat colour.
|
|
8
|
+
class CT_SolidColorFillProperties < Element
|
|
9
|
+
include ColorChoice
|
|
10
|
+
|
|
11
|
+
tag "a:solidFill"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# `a:noFill`, explicitly transparent.
|
|
15
|
+
class CT_NoFillProperties < Element
|
|
16
|
+
tag "a:noFill"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# `a:grpFill`, inheriting the group's fill.
|
|
20
|
+
class CT_GroupFillProperties < Element
|
|
21
|
+
tag "a:grpFill"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# `a:gs`, one stop in a gradient.
|
|
25
|
+
class CT_GradientStop < Element
|
|
26
|
+
include ColorChoice
|
|
27
|
+
|
|
28
|
+
tag "a:gs"
|
|
29
|
+
required_attr "pos", type: SimpleTypes::ST_PositiveFixedPercentage
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# `a:gsLst`, the stops of a gradient.
|
|
33
|
+
class CT_GradientStopList < Element
|
|
34
|
+
tag "a:gsLst"
|
|
35
|
+
one_or_more "a:gs", as: :gs
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# `a:lin`, the direction of a linear gradient.
|
|
39
|
+
class CT_LinearShadeProperties < Element
|
|
40
|
+
tag "a:lin"
|
|
41
|
+
optional_attr "ang", type: SimpleTypes::ST_PositiveFixedAngle
|
|
42
|
+
optional_attr "scaled", type: SimpleTypes::XsdBoolean
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# `a:gradFill`, a gradient fill.
|
|
46
|
+
class CT_GradientFillProperties < Element
|
|
47
|
+
tag "a:gradFill"
|
|
48
|
+
zero_or_one "a:gsLst", successors: %w[a:lin a:path a:tileRect]
|
|
49
|
+
zero_or_one "a:lin", successors: %w[a:path a:tileRect]
|
|
50
|
+
zero_or_one "a:path", successors: %w[a:tileRect]
|
|
51
|
+
|
|
52
|
+
# python-pptx's default, verbatim: PowerPoint's "White" template
|
|
53
|
+
# gradient, two accent-1 stops on a linear path. The `a:lin` carries no
|
|
54
|
+
# angle, so the direction is inherited until one is set.
|
|
55
|
+
DEFAULT_XML = <<~XML.freeze
|
|
56
|
+
<a:gradFill #{Ns.nsdecls("a")} rotWithShape="1">
|
|
57
|
+
<a:gsLst>
|
|
58
|
+
<a:gs pos="0">
|
|
59
|
+
<a:schemeClr val="accent1">
|
|
60
|
+
<a:tint val="100000"/>
|
|
61
|
+
<a:shade val="100000"/>
|
|
62
|
+
<a:satMod val="130000"/>
|
|
63
|
+
</a:schemeClr>
|
|
64
|
+
</a:gs>
|
|
65
|
+
<a:gs pos="100000">
|
|
66
|
+
<a:schemeClr val="accent1">
|
|
67
|
+
<a:tint val="50000"/>
|
|
68
|
+
<a:shade val="100000"/>
|
|
69
|
+
<a:satMod val="350000"/>
|
|
70
|
+
</a:schemeClr>
|
|
71
|
+
</a:gs>
|
|
72
|
+
</a:gsLst>
|
|
73
|
+
<a:lin scaled="0"/>
|
|
74
|
+
</a:gradFill>
|
|
75
|
+
XML
|
|
76
|
+
|
|
77
|
+
def self.new_grad_fill(context) = context.build_from_xml(DEFAULT_XML)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# `a:blip`, the image reference inside a picture fill.
|
|
81
|
+
class CT_Blip < Element
|
|
82
|
+
tag "a:blip"
|
|
83
|
+
optional_attr "r:embed", type: SimpleTypes::ST_RelationshipId, as: :embed
|
|
84
|
+
zero_or_one "a:extLst", successors: []
|
|
85
|
+
|
|
86
|
+
# The Office 2016 extension that carries an SVG beside the raster the
|
|
87
|
+
# blip itself points at. Consumers that understand it draw the vector;
|
|
88
|
+
# those that do not draw the fallback.
|
|
89
|
+
SVG_EXT_URI = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}"
|
|
90
|
+
|
|
91
|
+
# Point this blip's SVG extension at the part related by +r_id+.
|
|
92
|
+
def add_svg_blip(r_id)
|
|
93
|
+
ext_list = get_or_add_extLst
|
|
94
|
+
ext_list.append(ext_list.build_from_xml(<<~XML))
|
|
95
|
+
<a:ext #{Ns.nsdecls("a")} uri="#{SVG_EXT_URI}">
|
|
96
|
+
<asvg:svgBlip #{Ns.nsdecls("asvg", "r")} r:embed="#{r_id}"/>
|
|
97
|
+
</a:ext>
|
|
98
|
+
XML
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The relationship id of the SVG, or nil when this is a plain raster.
|
|
103
|
+
def svg_rId = xpath("./a:extLst/a:ext/asvg:svgBlip/@r:embed").first&.value
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# `a:extLst` as it appears on a blip.
|
|
107
|
+
class CT_BlipExtensionList < Element
|
|
108
|
+
tag "a:extLst"
|
|
109
|
+
zero_or_more "a:ext", as: :ext
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# `a:srcRect`: how much of an image is cropped from each side, as a
|
|
113
|
+
# fraction of its size. Negative values extend past the image edge.
|
|
114
|
+
class CT_RelativeRect < Element
|
|
115
|
+
tag "a:srcRect"
|
|
116
|
+
optional_attr "l", type: SimpleTypes::ST_Percentage, default: 0.0
|
|
117
|
+
optional_attr "t", type: SimpleTypes::ST_Percentage, default: 0.0
|
|
118
|
+
optional_attr "r", type: SimpleTypes::ST_Percentage, default: 0.0
|
|
119
|
+
optional_attr "b", type: SimpleTypes::ST_Percentage, default: 0.0
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# `a:blipFill`, a picture fill; `p:blipFill` on a picture is the same type.
|
|
123
|
+
class CT_BlipFillProperties < Element
|
|
124
|
+
tag "a:blipFill", "p:blipFill"
|
|
125
|
+
zero_or_one "a:blip", successors: %w[a:srcRect a:tile a:stretch]
|
|
126
|
+
zero_or_one "a:srcRect", successors: %w[a:tile a:stretch]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# `a:pattFill`, a two-colour pattern fill.
|
|
130
|
+
class CT_PatternFillProperties < Element
|
|
131
|
+
tag "a:pattFill"
|
|
132
|
+
zero_or_one "a:fgClr", successors: %w[a:bgClr]
|
|
133
|
+
zero_or_one "a:bgClr", successors: []
|
|
134
|
+
optional_attr "prst", type: Enum::MSO_PATTERN_TYPE
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|