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,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zip"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
module Pptx
|
|
7
|
+
# Writes the small Excel workbook embedded alongside a chart.
|
|
8
|
+
#
|
|
9
|
+
# PowerPoint opens this workbook when the user chooses "Edit Data"; it is not
|
|
10
|
+
# used for rendering, which is driven entirely by the cached values in the
|
|
11
|
+
# chart XML itself.
|
|
12
|
+
#
|
|
13
|
+
# python-pptx delegates this to XlsxWriter. This writes the minimum a
|
|
14
|
+
# conforming consumer needs directly, which keeps the gem free of a
|
|
15
|
+
# spreadsheet dependency. Strings are written inline rather than through a
|
|
16
|
+
# shared-strings part, which is valid and removes a whole part from the
|
|
17
|
+
# package.
|
|
18
|
+
class ChartWorkbookWriter
|
|
19
|
+
SHEET_NAME = "Sheet1"
|
|
20
|
+
|
|
21
|
+
NS_SPREADSHEETML = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
|
22
|
+
NS_OFC_RELS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
23
|
+
NS_PKG_RELS = "http://schemas.openxmlformats.org/package/2006/relationships"
|
|
24
|
+
|
|
25
|
+
CT_SHEET_MAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
|
|
26
|
+
CT_WORKSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
|
|
27
|
+
RT_OFFICE_DOCUMENT = "#{NS_OFC_RELS}/officeDocument".freeze
|
|
28
|
+
RT_WORKSHEET = "#{NS_OFC_RELS}/worksheet".freeze
|
|
29
|
+
RT_STYLES = "#{NS_OFC_RELS}/styles".freeze
|
|
30
|
+
CT_STYLES = "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"
|
|
31
|
+
# The first id free for a custom number format; 0-163 are built in.
|
|
32
|
+
DATE_FORMAT_ID = 164
|
|
33
|
+
|
|
34
|
+
def initialize(chart_data)
|
|
35
|
+
@chart_data = chart_data
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [String] the .xlsx bytes
|
|
39
|
+
def blob
|
|
40
|
+
buffer = Zip::OutputStream.write_buffer(StringIO.new) do |zos|
|
|
41
|
+
entries.each do |name, content|
|
|
42
|
+
zos.put_next_entry(name)
|
|
43
|
+
zos.write(content)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
buffer.rewind
|
|
47
|
+
buffer.read
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def entries
|
|
53
|
+
parts = {
|
|
54
|
+
"[Content_Types].xml" => content_types_xml,
|
|
55
|
+
"_rels/.rels" => package_rels_xml,
|
|
56
|
+
"xl/workbook.xml" => workbook_xml,
|
|
57
|
+
"xl/_rels/workbook.xml.rels" => workbook_rels_xml,
|
|
58
|
+
"xl/worksheets/sheet1.xml" => worksheet_xml
|
|
59
|
+
}
|
|
60
|
+
parts["xl/styles.xml"] = styles_xml if dates?
|
|
61
|
+
parts
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Only date categories need a cell style -- a number format that makes
|
|
65
|
+
# the serial numbers read as dates -- so only they get a styles part.
|
|
66
|
+
# XY and bubble data have no categories, so never dates.
|
|
67
|
+
def dates? = @chart_data.respond_to?(:categories) && @chart_data.categories.dates?
|
|
68
|
+
|
|
69
|
+
def declaration = %(<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n)
|
|
70
|
+
|
|
71
|
+
def content_types_xml
|
|
72
|
+
<<~XML
|
|
73
|
+
#{declaration.chomp}
|
|
74
|
+
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
75
|
+
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
76
|
+
<Default Extension="xml" ContentType="application/xml"/>
|
|
77
|
+
<Override PartName="/xl/workbook.xml" ContentType="#{CT_SHEET_MAIN}"/>
|
|
78
|
+
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="#{CT_WORKSHEET}"/>
|
|
79
|
+
#{%(<Override PartName="/xl/styles.xml" ContentType="#{CT_STYLES}"/>) if dates?}
|
|
80
|
+
</Types>
|
|
81
|
+
XML
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def package_rels_xml
|
|
85
|
+
<<~XML
|
|
86
|
+
#{declaration.chomp}
|
|
87
|
+
<Relationships xmlns="#{NS_PKG_RELS}">
|
|
88
|
+
<Relationship Id="rId1" Type="#{RT_OFFICE_DOCUMENT}" Target="xl/workbook.xml"/>
|
|
89
|
+
</Relationships>
|
|
90
|
+
XML
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def workbook_xml
|
|
94
|
+
<<~XML
|
|
95
|
+
#{declaration.chomp}
|
|
96
|
+
<workbook xmlns="#{NS_SPREADSHEETML}" xmlns:r="#{NS_OFC_RELS}">
|
|
97
|
+
<sheets><sheet name="#{SHEET_NAME}" sheetId="1" r:id="rId1"/></sheets>
|
|
98
|
+
</workbook>
|
|
99
|
+
XML
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def workbook_rels_xml
|
|
103
|
+
<<~XML
|
|
104
|
+
#{declaration.chomp}
|
|
105
|
+
<Relationships xmlns="#{NS_PKG_RELS}">
|
|
106
|
+
<Relationship Id="rId1" Type="#{RT_WORKSHEET}" Target="worksheets/sheet1.xml"/>
|
|
107
|
+
#{%(<Relationship Id="rId2" Type="#{RT_STYLES}" Target="styles.xml"/>) if dates?}
|
|
108
|
+
</Relationships>
|
|
109
|
+
XML
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def worksheet_xml
|
|
113
|
+
"#{declaration}<worksheet xmlns=\"#{NS_SPREADSHEETML}\"><sheetData>" \
|
|
114
|
+
"#{rows_xml}</sheetData></worksheet>\n"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Row 1 holds the series names, after the category columns. Each later
|
|
118
|
+
# row holds that row's category labels and one value per series.
|
|
119
|
+
#
|
|
120
|
+
# Grouped categories take one column per level, the leaf level
|
|
121
|
+
# rightmost; a group's label sits on the row where its run begins, as
|
|
122
|
+
# python-pptx writes it.
|
|
123
|
+
def rows_xml
|
|
124
|
+
rows = Hash.new { |hash, row| hash[row] = [] }
|
|
125
|
+
@chart_data.series.each do |series|
|
|
126
|
+
rows[1] << [series.index + depth, string_cell("#{@chart_data.column_letter(series)}1", series.name)]
|
|
127
|
+
end
|
|
128
|
+
category_cells(rows)
|
|
129
|
+
series_cells(rows)
|
|
130
|
+
rows.keys.sort.map do |row|
|
|
131
|
+
%(<row r="#{row}">#{rows[row].sort_by(&:first).map(&:last).join}</row>)
|
|
132
|
+
end.join
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def depth = @chart_data.categories.depth
|
|
136
|
+
|
|
137
|
+
def category_cells(rows)
|
|
138
|
+
@chart_data.categories.levels.each_with_index do |level, level_index|
|
|
139
|
+
column = depth - level_index - 1
|
|
140
|
+
letter = ChartData.column_reference(column + 1)
|
|
141
|
+
level.each do |offset, label|
|
|
142
|
+
row = ChartData::FIRST_DATA_ROW + offset
|
|
143
|
+
rows[row] << [column, category_cell("#{letter}#{row}", label)]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def series_cells(rows)
|
|
149
|
+
@chart_data.series.each do |series|
|
|
150
|
+
letter = @chart_data.column_letter(series)
|
|
151
|
+
series.values.each_with_index do |value, index|
|
|
152
|
+
row = ChartData::FIRST_DATA_ROW + index
|
|
153
|
+
rows[row] << [series.index + depth, number_cell("#{letter}#{row}", value)]
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def category_cell(reference, label)
|
|
159
|
+
return date_cell(reference, label) if label.is_a?(Date) || label.is_a?(Time)
|
|
160
|
+
|
|
161
|
+
cell_for(reference, label)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# A date is its Excel serial number, styled with the date format.
|
|
165
|
+
def date_cell(reference, date)
|
|
166
|
+
category = ChartDataCategory.new(date, nil)
|
|
167
|
+
%(<c r="#{reference}" s="1"><v>#{category.numeric_str_val.to_f.to_i}</v></c>)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def styles_xml
|
|
171
|
+
format_code = escape(@chart_data.categories.number_format)
|
|
172
|
+
<<~XML
|
|
173
|
+
#{declaration.chomp}
|
|
174
|
+
<styleSheet xmlns="#{NS_SPREADSHEETML}">
|
|
175
|
+
<numFmts count="1"><numFmt numFmtId="#{DATE_FORMAT_ID}" formatCode="#{format_code}"/></numFmts>
|
|
176
|
+
<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>
|
|
177
|
+
<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>
|
|
178
|
+
<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>
|
|
179
|
+
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
|
|
180
|
+
<cellXfs count="2"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="#{DATE_FORMAT_ID}" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/></cellXfs>
|
|
181
|
+
</styleSheet>
|
|
182
|
+
XML
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def cell_for(reference, value)
|
|
186
|
+
value.is_a?(Numeric) ? number_cell(reference, value) : string_cell(reference, value)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def string_cell(reference, value)
|
|
190
|
+
%(<c r="#{reference}" t="inlineStr"><is><t>#{escape(value)}</t></is></c>)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# A nil value means "no data for this category", which is written as an
|
|
194
|
+
# empty cell rather than a zero.
|
|
195
|
+
def number_cell(reference, value)
|
|
196
|
+
return %(<c r="#{reference}"/>) if value.nil?
|
|
197
|
+
|
|
198
|
+
%(<c r="#{reference}"><v>#{format_number(value)}</v></c>)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Integral floats are written without a trailing ".0", as Excel does.
|
|
202
|
+
def format_number(value)
|
|
203
|
+
return value.to_s unless value.is_a?(Float)
|
|
204
|
+
|
|
205
|
+
value == value.truncate ? value.to_i.to_s : value.to_s
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def escape(value)
|
|
209
|
+
value.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|