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,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/chart/data"
|
|
4
|
+
|
|
5
|
+
module Pptx
|
|
6
|
+
# Data for a scatter chart: series of (x, y) points.
|
|
7
|
+
#
|
|
8
|
+
# data = Pptx::XyChartData.new
|
|
9
|
+
# data.add_series("Alpha") do |s|
|
|
10
|
+
# s.add_point(1, 10)
|
|
11
|
+
# s.add_point(2, 20)
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# Unlike a category chart, each series has its own x values, so the
|
|
15
|
+
# worksheet stacks the series in blocks rather than putting them side by
|
|
16
|
+
# side in shared columns.
|
|
17
|
+
class XyChartData
|
|
18
|
+
include Enumerable
|
|
19
|
+
|
|
20
|
+
WORKSHEET_NAME = "Sheet1"
|
|
21
|
+
DEFAULT_NUMBER_FORMAT = "General"
|
|
22
|
+
# Each series block is followed by a blank row, and preceded by its title
|
|
23
|
+
# row -- two rows of overhead per series.
|
|
24
|
+
ROWS_PER_SERIES_OVERHEAD = 2
|
|
25
|
+
|
|
26
|
+
attr_reader :series, :number_format
|
|
27
|
+
|
|
28
|
+
def initialize(number_format: DEFAULT_NUMBER_FORMAT)
|
|
29
|
+
@series = []
|
|
30
|
+
@number_format = number_format
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Add a series. Points may be given up front, added in a block, or both.
|
|
34
|
+
#
|
|
35
|
+
# @param points [Array<Array>] each [x, y]
|
|
36
|
+
# @return [XySeries]
|
|
37
|
+
def add_series(name, points: [], number_format: nil)
|
|
38
|
+
series = series_class.new(self, @series.size, name, number_format || @number_format)
|
|
39
|
+
@series << series
|
|
40
|
+
points.each { |point| series.add_point(*point) }
|
|
41
|
+
yield series if block_given?
|
|
42
|
+
series
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def each(&) = @series.each(&)
|
|
46
|
+
|
|
47
|
+
def size = @series.size
|
|
48
|
+
|
|
49
|
+
# The row a series' title sits on, counting the blocks before it.
|
|
50
|
+
#
|
|
51
|
+
# @api private
|
|
52
|
+
def title_row(series)
|
|
53
|
+
preceding_points = @series[0...series.index].sum(&:size)
|
|
54
|
+
(series.index * ROWS_PER_SERIES_OVERHEAD) + preceding_points + 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @api private
|
|
58
|
+
def series_name_ref(series) = "#{WORKSHEET_NAME}!$B$#{title_row(series)}"
|
|
59
|
+
|
|
60
|
+
# @api private
|
|
61
|
+
def x_values_ref(series) = column_ref(series, "A")
|
|
62
|
+
|
|
63
|
+
# @api private
|
|
64
|
+
def y_values_ref(series) = column_ref(series, "B")
|
|
65
|
+
|
|
66
|
+
# @api private
|
|
67
|
+
def column_ref(series, column)
|
|
68
|
+
first = title_row(series) + 1
|
|
69
|
+
last = first + [series.size, 1].max - 1
|
|
70
|
+
"#{WORKSHEET_NAME}!$#{column}$#{first}:$#{column}$#{last}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @api private
|
|
74
|
+
def xlsx_blob = XyWorkbookWriter.new(self).blob
|
|
75
|
+
|
|
76
|
+
# @api private
|
|
77
|
+
# Whether the worksheet carries a third column of bubble sizes.
|
|
78
|
+
def bubble? = false
|
|
79
|
+
|
|
80
|
+
def inspect = "#<#{self.class.name} series=#{size}>"
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def series_class = XySeries
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Data for a bubble chart: series of (x, y, size) points.
|
|
88
|
+
class BubbleChartData < XyChartData
|
|
89
|
+
# @api private
|
|
90
|
+
def bubble_sizes_ref(series) = column_ref(series, "C")
|
|
91
|
+
|
|
92
|
+
# @api private
|
|
93
|
+
def bubble? = true
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def series_class = BubbleSeries
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# One series of a scatter chart.
|
|
101
|
+
class XySeries
|
|
102
|
+
include Enumerable
|
|
103
|
+
|
|
104
|
+
attr_reader :chart_data, :index, :name, :number_format, :points
|
|
105
|
+
|
|
106
|
+
def initialize(chart_data, index, name, number_format)
|
|
107
|
+
@chart_data = chart_data
|
|
108
|
+
@index = index
|
|
109
|
+
@name = name
|
|
110
|
+
@number_format = number_format
|
|
111
|
+
@points = []
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @return [self] so points can be chained
|
|
115
|
+
def add_point(x, y)
|
|
116
|
+
@points << [x, y]
|
|
117
|
+
self
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def each(&) = @points.each(&)
|
|
121
|
+
|
|
122
|
+
def size = @points.size
|
|
123
|
+
|
|
124
|
+
def x_values = @points.map(&:first)
|
|
125
|
+
|
|
126
|
+
def y_values = @points.map { |point| point[1] }
|
|
127
|
+
|
|
128
|
+
def name_ref = @chart_data.series_name_ref(self)
|
|
129
|
+
def x_values_ref = @chart_data.x_values_ref(self)
|
|
130
|
+
def y_values_ref = @chart_data.y_values_ref(self)
|
|
131
|
+
|
|
132
|
+
def inspect = "#<#{self.class.name} #{@name.inspect} #{size} points>"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# One series of a bubble chart, whose points carry a size as well.
|
|
136
|
+
class BubbleSeries < XySeries
|
|
137
|
+
def add_point(x, y, size)
|
|
138
|
+
@points << [x, y, size]
|
|
139
|
+
self
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def bubble_sizes = @points.map { |point| point[2] }
|
|
143
|
+
|
|
144
|
+
def bubble_sizes_ref = @chart_data.bubble_sizes_ref(self)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Writes the workbook behind a scatter or bubble chart.
|
|
148
|
+
#
|
|
149
|
+
# The layout differs from a category chart: each series gets its own block of
|
|
150
|
+
# rows, since the x values belong to the series rather than being shared.
|
|
151
|
+
class XyWorkbookWriter < ChartWorkbookWriter
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def rows_xml
|
|
155
|
+
@chart_data.series.flat_map { |series| series_rows(series) }.join
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def series_rows(series)
|
|
159
|
+
title_row = @chart_data.title_row(series)
|
|
160
|
+
rows = [title_row_xml(series, title_row)]
|
|
161
|
+
series.points.each_with_index do |point, offset|
|
|
162
|
+
rows << point_row_xml(point, title_row + 1 + offset)
|
|
163
|
+
end
|
|
164
|
+
rows
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Column A is left empty on the title row; the series name sits in B, and
|
|
168
|
+
# a bubble chart labels its third column.
|
|
169
|
+
def title_row_xml(series, row)
|
|
170
|
+
cells = [string_cell("B#{row}", series.name)]
|
|
171
|
+
cells << string_cell("C#{row}", "Size") if @chart_data.bubble?
|
|
172
|
+
%(<row r="#{row}">#{cells.join}</row>)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def point_row_xml(point, row)
|
|
176
|
+
cells = point.each_with_index.map do |value, column|
|
|
177
|
+
number_cell("#{(65 + column).chr}#{row}", value)
|
|
178
|
+
end
|
|
179
|
+
%(<row r="#{row}">#{cells.join}</row>)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/numeric_lengths"
|
|
4
|
+
|
|
5
|
+
# Opt-in numeric sugar: `require "ruby_pptx/core_ext"` to write `1.inch` or `2.5.cm`
|
|
6
|
+
# instead of `Pptx.inches(1)`. Kept out of the default require so the gem does
|
|
7
|
+
# not monkey-patch Numeric behind your back.
|
|
8
|
+
#
|
|
9
|
+
# This patches Numeric for the whole process. `require "ruby_pptx/refinements"` and
|
|
10
|
+
# `using Pptx::Lengths` does the same thing scoped to one file, and is the
|
|
11
|
+
# better choice inside a library.
|
|
12
|
+
Numeric.include(Pptx::NumericLengths)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/pattern_matching"
|
|
4
|
+
|
|
5
|
+
require "ruby_pptx/element_proxy"
|
|
6
|
+
require "ruby_pptx/oxml/dml/color"
|
|
7
|
+
require "ruby_pptx/enum/dml"
|
|
8
|
+
|
|
9
|
+
module Pptx
|
|
10
|
+
# An RGB colour.
|
|
11
|
+
#
|
|
12
|
+
# Pptx::RGBColor.new(60, 47, 128)
|
|
13
|
+
# Pptx::RGBColor["3C2F80"]
|
|
14
|
+
class RGBColor
|
|
15
|
+
include PatternMatching
|
|
16
|
+
|
|
17
|
+
pattern_keys :r, :g, :b
|
|
18
|
+
|
|
19
|
+
attr_reader :r, :g, :b
|
|
20
|
+
|
|
21
|
+
# Array patterns too: `in [r, g, b]`.
|
|
22
|
+
def deconstruct = to_a
|
|
23
|
+
|
|
24
|
+
# Parse a hex string such as "3C2F80".
|
|
25
|
+
def self.from_string(hex)
|
|
26
|
+
unless /\A\h{6}\z/.match?(hex.to_s)
|
|
27
|
+
raise ArgumentError, "expected a six-digit hex string, got #{hex.inspect}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
new(hex[0, 2].to_i(16), hex[2, 2].to_i(16), hex[4, 2].to_i(16))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
alias [] from_string
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(r, g, b)
|
|
38
|
+
[r, g, b].each do |component|
|
|
39
|
+
unless component.is_a?(Integer) && component.between?(0, 255)
|
|
40
|
+
raise ArgumentError, "RGBColor takes three integers 0-255, got #{[r, g, b].inspect}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
@r = r
|
|
44
|
+
@g = g
|
|
45
|
+
@b = b
|
|
46
|
+
freeze
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_a = [@r, @g, @b]
|
|
50
|
+
|
|
51
|
+
# The uppercase hex form OOXML uses, e.g. "3C2F80".
|
|
52
|
+
def to_s = format("%02X%02X%02X", @r, @g, @b)
|
|
53
|
+
|
|
54
|
+
def ==(other) = other.is_a?(RGBColor) && other.to_a == to_a
|
|
55
|
+
alias eql? ==
|
|
56
|
+
|
|
57
|
+
def hash = to_a.hash
|
|
58
|
+
|
|
59
|
+
def inspect = "#<Pptx::RGBColor #{self}>"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The colour of a font, fill or line.
|
|
63
|
+
#
|
|
64
|
+
# A colour may be an explicit RGB value, a reference to a theme colour, or
|
|
65
|
+
# absent -- in which case it is inherited and {#type} is nil.
|
|
66
|
+
class ColorFormat
|
|
67
|
+
# @param color_choice_parent [Pptx::Oxml::Element] the element holding the
|
|
68
|
+
# colour choice, e.g. `a:solidFill`
|
|
69
|
+
def self.from_color_choice_parent(color_choice_parent) = new(color_choice_parent)
|
|
70
|
+
|
|
71
|
+
def initialize(color_choice_parent)
|
|
72
|
+
@parent = color_choice_parent
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Pptx::Enum::MSO_COLOR_TYPE, nil] nil when no colour is set here
|
|
76
|
+
def type
|
|
77
|
+
element = color_element
|
|
78
|
+
return nil if element.nil?
|
|
79
|
+
|
|
80
|
+
case element.nsptag
|
|
81
|
+
when "a:srgbClr" then Enum::MSO_COLOR_TYPE::RGB
|
|
82
|
+
when "a:schemeClr" then Enum::MSO_COLOR_TYPE::SCHEME
|
|
83
|
+
when "a:hslClr" then Enum::MSO_COLOR_TYPE::HSL
|
|
84
|
+
when "a:prstClr" then Enum::MSO_COLOR_TYPE::PRESET
|
|
85
|
+
when "a:scrgbClr" then Enum::MSO_COLOR_TYPE::SCRGB
|
|
86
|
+
when "a:sysClr" then Enum::MSO_COLOR_TYPE::SYSTEM
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @return [RGBColor, nil] nil unless an explicit RGB colour is set
|
|
91
|
+
def rgb
|
|
92
|
+
element = color_element
|
|
93
|
+
return nil unless element&.nsptag == "a:srgbClr"
|
|
94
|
+
|
|
95
|
+
RGBColor.from_string(element.val)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def rgb=(value)
|
|
99
|
+
value = RGBColor.from_string(value) if value.is_a?(String)
|
|
100
|
+
raise TypeError, "expected an RGBColor, got #{value.class}" unless value.is_a?(RGBColor)
|
|
101
|
+
|
|
102
|
+
@parent.get_or_change_to_srgbClr.val = value.to_s
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @return [Pptx::Enum::MSO_THEME_COLOR, nil] nil unless a theme colour is set
|
|
106
|
+
def theme_color
|
|
107
|
+
element = color_element
|
|
108
|
+
return nil unless element&.nsptag == "a:schemeClr"
|
|
109
|
+
|
|
110
|
+
element.val
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def theme_color=(value)
|
|
114
|
+
@parent.get_or_change_to_schemeClr.val = Enum::MSO_THEME_COLOR.fetch(value)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# A luminance adjustment between -1.0 and 1.0: negative is darker (a
|
|
118
|
+
# shade), positive is lighter (a tint).
|
|
119
|
+
def brightness
|
|
120
|
+
element = color_element
|
|
121
|
+
return 0.0 if element.nil?
|
|
122
|
+
|
|
123
|
+
lum_off = element.lumOff
|
|
124
|
+
return lum_off.val if lum_off
|
|
125
|
+
|
|
126
|
+
lum_mod = element.lumMod
|
|
127
|
+
return lum_mod.val - 1.0 if lum_mod
|
|
128
|
+
|
|
129
|
+
0.0
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def brightness=(value)
|
|
133
|
+
unless value.is_a?(Numeric) && value.between?(-1.0, 1.0)
|
|
134
|
+
raise ArgumentError, "brightness must be a number between -1.0 and 1.0, got #{value.inspect}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
element = color_element
|
|
138
|
+
raise Error, "cannot set brightness before a colour; set rgb or theme_color first" if element.nil?
|
|
139
|
+
|
|
140
|
+
element.clear_lum
|
|
141
|
+
if value.positive?
|
|
142
|
+
element.add_lumMod(1.0 - value)
|
|
143
|
+
element.add_lumOff(value)
|
|
144
|
+
elsif value.negative?
|
|
145
|
+
element.add_lumMod(1.0 + value)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def inspect = "#<Pptx::ColorFormat type=#{type&.name.inspect} rgb=#{rgb&.to_s.inspect}>"
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def color_element = @parent.eg_colorChoice
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pptx
|
|
4
|
+
# The shadow of a shape.
|
|
5
|
+
#
|
|
6
|
+
# Only inheritance can be controlled so far, as in python-pptx: a shape
|
|
7
|
+
# either takes its shadow from the theme and style hierarchy, or has its
|
|
8
|
+
# effects switched off explicitly.
|
|
9
|
+
#
|
|
10
|
+
# shape.shadow.inherit = false # no shadow, whatever the theme says
|
|
11
|
+
class ShadowFormat
|
|
12
|
+
# @param shape_properties [Pptx::Oxml::Element] `p:spPr` or `p:grpSpPr`,
|
|
13
|
+
# which both carry the `a:effectLst`
|
|
14
|
+
def initialize(shape_properties)
|
|
15
|
+
@element = shape_properties
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# True when the shape takes its shadow from the style hierarchy.
|
|
19
|
+
def inherit? = @element.effectLst.nil?
|
|
20
|
+
|
|
21
|
+
alias inherit inherit?
|
|
22
|
+
|
|
23
|
+
# Restore inheritance with true; with false, break it so no effects show.
|
|
24
|
+
#
|
|
25
|
+
# Inheritance is all or nothing: restoring it removes every explicit
|
|
26
|
+
# effect on the shape -- glow and reflection too, not only the shadow.
|
|
27
|
+
def inherit=(value)
|
|
28
|
+
value ? @element.remove_effectLst : @element.get_or_add_effectLst
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def inspect = "#<Pptx::ShadowFormat inherit=#{inherit?}>"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/dml/color"
|
|
4
|
+
require "ruby_pptx/oxml/dml/fill"
|
|
5
|
+
require "ruby_pptx/enum/dml"
|
|
6
|
+
require "ruby_pptx/element_proxy"
|
|
7
|
+
require "ruby_pptx/sliceable"
|
|
8
|
+
|
|
9
|
+
module Pptx
|
|
10
|
+
# The fill of a shape, text run, line or slide background.
|
|
11
|
+
#
|
|
12
|
+
# A fill starts out inherited -- {#type} is nil and nothing is written.
|
|
13
|
+
# Calling {#solid}, {#background} or {#gradient} makes it explicit, after
|
|
14
|
+
# which {#fore_color} and friends become available.
|
|
15
|
+
#
|
|
16
|
+
# shape.fill.solid
|
|
17
|
+
# shape.fill.fore_color.rgb = Pptx::RGBColor["C0504D"]
|
|
18
|
+
class FillFormat
|
|
19
|
+
# @param fill_parent [Pptx::Oxml::Element] the element carrying the fill
|
|
20
|
+
# choice group, e.g. `p:spPr` or `a:rPr`
|
|
21
|
+
def self.from_fill_parent(fill_parent) = new(fill_parent)
|
|
22
|
+
|
|
23
|
+
def initialize(fill_parent)
|
|
24
|
+
@parent = fill_parent
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Pptx::Enum::MSO_FILL_TYPE, nil] nil when the fill is inherited
|
|
28
|
+
def type
|
|
29
|
+
element = fill_element
|
|
30
|
+
return nil if element.nil?
|
|
31
|
+
|
|
32
|
+
TYPES[element.nsptag]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
TYPES = {
|
|
36
|
+
"a:noFill" => Enum::MSO_FILL_TYPE::BACKGROUND,
|
|
37
|
+
"a:solidFill" => Enum::MSO_FILL_TYPE::SOLID,
|
|
38
|
+
"a:gradFill" => Enum::MSO_FILL_TYPE::GRADIENT,
|
|
39
|
+
"a:blipFill" => Enum::MSO_FILL_TYPE::PICTURE,
|
|
40
|
+
"a:pattFill" => Enum::MSO_FILL_TYPE::PATTERNED,
|
|
41
|
+
"a:grpFill" => Enum::MSO_FILL_TYPE::GROUP
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
# Make this a solid (flat colour) fill. The colour itself is then set
|
|
45
|
+
# through {#fore_color}.
|
|
46
|
+
def solid
|
|
47
|
+
@parent.get_or_change_to_solidFill
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Make this fill transparent, so whatever is behind shows through.
|
|
52
|
+
def background
|
|
53
|
+
@parent.get_or_change_to_noFill
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Make this a gradient fill, with PowerPoint's default stops.
|
|
58
|
+
def gradient
|
|
59
|
+
@parent.get_or_change_to_gradFill
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Make this a patterned fill.
|
|
64
|
+
def patterned
|
|
65
|
+
@parent.get_or_change_to_pattFill
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The foreground colour.
|
|
70
|
+
#
|
|
71
|
+
# @raise [Error] unless the fill is solid or patterned
|
|
72
|
+
def fore_color
|
|
73
|
+
case type&.name
|
|
74
|
+
when :SOLID then ColorFormat.from_color_choice_parent(fill_element)
|
|
75
|
+
when :PATTERNED then ColorFormat.from_color_choice_parent(fill_element.get_or_add_fgClr)
|
|
76
|
+
else
|
|
77
|
+
raise Error, "fill type #{type&.name.inspect} has no foreground colour"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The background colour of a patterned fill.
|
|
82
|
+
#
|
|
83
|
+
# @raise [Error] unless the fill is patterned
|
|
84
|
+
def back_color
|
|
85
|
+
raise Error, "fill type #{type&.name.inspect} has no background colour" unless type&.name == :PATTERNED
|
|
86
|
+
|
|
87
|
+
ColorFormat.from_color_choice_parent(fill_element.get_or_add_bgClr)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @return [Pptx::Enum::MSO_PATTERN_TYPE, nil]
|
|
91
|
+
def pattern
|
|
92
|
+
raise Error, "fill type #{type&.name.inspect} is not patterned" unless type&.name == :PATTERNED
|
|
93
|
+
|
|
94
|
+
fill_element.prst
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def pattern=(value)
|
|
98
|
+
patterned unless type&.name == :PATTERNED
|
|
99
|
+
fill_element.prst = value
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The angle of a linear gradient in degrees, counter-clockwise from
|
|
103
|
+
# pointing right -- the way PowerPoint's dialog and trigonometry both
|
|
104
|
+
# count, although the file stores it clockwise. nil when inherited.
|
|
105
|
+
#
|
|
106
|
+
# @raise [Error] unless this is a gradient fill, or if it is not linear
|
|
107
|
+
def gradient_angle
|
|
108
|
+
gradient = require_gradient
|
|
109
|
+
raise Error, "not a linear gradient" unless gradient.path.nil?
|
|
110
|
+
|
|
111
|
+
clockwise = gradient.lin&.ang
|
|
112
|
+
return nil if clockwise.nil?
|
|
113
|
+
|
|
114
|
+
clockwise.zero? ? 0.0 : 360.0 - clockwise
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def gradient_angle=(value)
|
|
118
|
+
linear = require_gradient.lin
|
|
119
|
+
raise Error, "not a linear gradient" if linear.nil?
|
|
120
|
+
|
|
121
|
+
linear.ang = 360.0 - value
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# The colour stops the gradient passes through, in order.
|
|
125
|
+
#
|
|
126
|
+
# @return [GradientStops]
|
|
127
|
+
def gradient_stops = GradientStops.new(require_gradient.get_or_add_gsLst)
|
|
128
|
+
|
|
129
|
+
def inspect = "#<Pptx::FillFormat type=#{type&.name.inspect}>"
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def fill_element = @parent.eg_fillProperties
|
|
134
|
+
|
|
135
|
+
def require_gradient
|
|
136
|
+
raise Error, "fill type #{type&.name.inspect} is not a gradient" unless type&.name == :GRADIENT
|
|
137
|
+
|
|
138
|
+
fill_element
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The outline of a shape.
|
|
143
|
+
# The stops of a gradient fill.
|
|
144
|
+
#
|
|
145
|
+
# stops = shape.fill.gradient_stops
|
|
146
|
+
# stops[0].color.rgb = Pptx::RGBColor["1F497D"]
|
|
147
|
+
# stops[1].position = 0.75
|
|
148
|
+
class GradientStops
|
|
149
|
+
include Enumerable
|
|
150
|
+
include Sliceable
|
|
151
|
+
|
|
152
|
+
def initialize(gs_lst)
|
|
153
|
+
@element = gs_lst
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def each(&)
|
|
157
|
+
return enum_for(:each) { size } unless block_given?
|
|
158
|
+
|
|
159
|
+
@element.gs_list.each { |gs| yield GradientStop.new(gs) }
|
|
160
|
+
self
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def size = @element.gs_list.size
|
|
164
|
+
alias length size
|
|
165
|
+
|
|
166
|
+
def [](index, length = nil) = slice_members(@element.gs_list, index, length) { |gs| GradientStop.new(gs) }
|
|
167
|
+
|
|
168
|
+
def inspect = "#<Pptx::GradientStops size=#{size}>"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# One colour stop in a gradient.
|
|
172
|
+
class GradientStop < ElementProxy
|
|
173
|
+
def color = @color ||= ColorFormat.from_color_choice_parent(@element)
|
|
174
|
+
|
|
175
|
+
# Where along the gradient this stop sits, from 0.0 at the start to 1.0
|
|
176
|
+
# at the end.
|
|
177
|
+
def position = @element.pos
|
|
178
|
+
|
|
179
|
+
def position=(value)
|
|
180
|
+
@element.pos = Float(value)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def inspect = "#<Pptx::GradientStop position=#{position}>"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
class LineFormat
|
|
187
|
+
def initialize(parent)
|
|
188
|
+
@parent = parent
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# The `a:ln` element, added if not present.
|
|
192
|
+
def element = @element ||= @parent.get_or_add_ln
|
|
193
|
+
|
|
194
|
+
def fill = @fill ||= FillFormat.from_fill_parent(element)
|
|
195
|
+
|
|
196
|
+
# Shortcut to the line's solid colour, making the fill solid on first use.
|
|
197
|
+
def color
|
|
198
|
+
fill.solid if fill.type.nil?
|
|
199
|
+
fill.fore_color
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# @return [Pptx::Length, nil] nil when the width is inherited
|
|
203
|
+
#
|
|
204
|
+
# Reading never adds an `a:ln`; only assigning does.
|
|
205
|
+
def width
|
|
206
|
+
value = @parent.ln&.w
|
|
207
|
+
value.nil? || value.zero? ? nil : value
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# The dash pattern, or nil when it is inherited.
|
|
211
|
+
#
|
|
212
|
+
# @return [Pptx::Enum::Member, nil] a member of MSO_LINE_DASH_STYLE
|
|
213
|
+
def dash_style = @parent.ln&.prstDash_val
|
|
214
|
+
|
|
215
|
+
# Set the dash pattern, or restore inheritance with nil -- which also
|
|
216
|
+
# drops a custom dash, since either one would override the inherited
|
|
217
|
+
# style.
|
|
218
|
+
def dash_style=(value)
|
|
219
|
+
if value.nil?
|
|
220
|
+
line = @parent.ln or return
|
|
221
|
+
line.remove_prstDash
|
|
222
|
+
line.remove_custDash
|
|
223
|
+
else
|
|
224
|
+
element.prstDash_val = Enum::MSO_LINE_DASH_STYLE.fetch(value)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def width=(value)
|
|
229
|
+
element.w = value.nil? ? Pptx::Length.emu(0) : value
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def inspect = "#<Pptx::LineFormat width=#{width&.pt}>"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/sliceable"
|
|
4
|
+
|
|
5
|
+
module Pptx
|
|
6
|
+
# Base for the objects that make up the public API.
|
|
7
|
+
#
|
|
8
|
+
# Almost every class a caller touches is a thin proxy whose state lives
|
|
9
|
+
# entirely in the XML element it wraps. Two proxies wrapping the same element
|
|
10
|
+
# are equal, whether or not they are the same object.
|
|
11
|
+
class ElementProxy
|
|
12
|
+
include Sliceable
|
|
13
|
+
|
|
14
|
+
attr_reader :element
|
|
15
|
+
|
|
16
|
+
def initialize(element)
|
|
17
|
+
@element = element
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ==(other) = other.is_a?(ElementProxy) && other.element == @element
|
|
21
|
+
alias eql? ==
|
|
22
|
+
|
|
23
|
+
def hash = @element.hash
|
|
24
|
+
|
|
25
|
+
def inspect = "#<#{self.class.name} <#{@element.nsptag}>>"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A proxy that knows its parent, and through it the part it belongs to.
|
|
29
|
+
#
|
|
30
|
+
# An ancestor is occasionally needed to do something the proxy cannot, such
|
|
31
|
+
# as add or drop a relationship.
|
|
32
|
+
class ParentedElementProxy < ElementProxy
|
|
33
|
+
attr_reader :parent
|
|
34
|
+
|
|
35
|
+
def initialize(element, parent)
|
|
36
|
+
super(element)
|
|
37
|
+
@parent = parent
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The package part this object lives in.
|
|
41
|
+
def part = @parent.part
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A proxy wrapping a part's root element, such as `p:sld`.
|
|
45
|
+
class PartElementProxy < ElementProxy
|
|
46
|
+
attr_reader :part
|
|
47
|
+
|
|
48
|
+
def initialize(element, part)
|
|
49
|
+
super(element)
|
|
50
|
+
@part = part
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|