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.
Files changed (101) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +83 -0
  3. data/LICENSE +21 -0
  4. data/NOTICE +48 -0
  5. data/README.md +272 -0
  6. data/lib/ruby_pptx/action.rb +120 -0
  7. data/lib/ruby_pptx/autoshape_spec.rb +337 -0
  8. data/lib/ruby_pptx/builder.rb +228 -0
  9. data/lib/ruby_pptx/chart/categories.rb +208 -0
  10. data/lib/ruby_pptx/chart/chart.rb +443 -0
  11. data/lib/ruby_pptx/chart/combo.rb +230 -0
  12. data/lib/ruby_pptx/chart/data.rb +147 -0
  13. data/lib/ruby_pptx/chart/format.rb +647 -0
  14. data/lib/ruby_pptx/chart/workbook_writer.rb +212 -0
  15. data/lib/ruby_pptx/chart/xml_writer.rb +953 -0
  16. data/lib/ruby_pptx/chart/xy_data.rb +182 -0
  17. data/lib/ruby_pptx/core_ext.rb +12 -0
  18. data/lib/ruby_pptx/dml/color.rb +155 -0
  19. data/lib/ruby_pptx/dml/effect.rb +33 -0
  20. data/lib/ruby_pptx/dml/fill.rb +234 -0
  21. data/lib/ruby_pptx/element_proxy.rb +53 -0
  22. data/lib/ruby_pptx/enum/action.rb +46 -0
  23. data/lib/ruby_pptx/enum/base.rb +139 -0
  24. data/lib/ruby_pptx/enum/chart.rb +278 -0
  25. data/lib/ruby_pptx/enum/dml.rb +237 -0
  26. data/lib/ruby_pptx/enum/lang.rb +441 -0
  27. data/lib/ruby_pptx/enum/prog_id.rb +38 -0
  28. data/lib/ruby_pptx/enum/shapes.rb +514 -0
  29. data/lib/ruby_pptx/enum/text.rb +108 -0
  30. data/lib/ruby_pptx/errors.rb +15 -0
  31. data/lib/ruby_pptx/geometry.rb +89 -0
  32. data/lib/ruby_pptx/image.rb +347 -0
  33. data/lib/ruby_pptx/length.rb +120 -0
  34. data/lib/ruby_pptx/media.rb +64 -0
  35. data/lib/ruby_pptx/numeric_lengths.rb +30 -0
  36. data/lib/ruby_pptx/opc/constants.rb +205 -0
  37. data/lib/ruby_pptx/opc/oxml.rb +99 -0
  38. data/lib/ruby_pptx/opc/pack_uri.rb +146 -0
  39. data/lib/ruby_pptx/opc/package.rb +482 -0
  40. data/lib/ruby_pptx/opc/serialized.rb +229 -0
  41. data/lib/ruby_pptx/opc/spec.rb +37 -0
  42. data/lib/ruby_pptx/oxml/action.rb +43 -0
  43. data/lib/ruby_pptx/oxml/chart.rb +785 -0
  44. data/lib/ruby_pptx/oxml/content_model.rb +186 -0
  45. data/lib/ruby_pptx/oxml/core_properties.rb +145 -0
  46. data/lib/ruby_pptx/oxml/dml/color.rb +115 -0
  47. data/lib/ruby_pptx/oxml/dml/fill.rb +137 -0
  48. data/lib/ruby_pptx/oxml/element.rb +329 -0
  49. data/lib/ruby_pptx/oxml/ns.rb +115 -0
  50. data/lib/ruby_pptx/oxml/presentation.rb +110 -0
  51. data/lib/ruby_pptx/oxml/section.rb +65 -0
  52. data/lib/ruby_pptx/oxml/shapes/autoshape.rb +310 -0
  53. data/lib/ruby_pptx/oxml/shapes/groupshape.rb +217 -0
  54. data/lib/ruby_pptx/oxml/shapes/other.rb +405 -0
  55. data/lib/ruby_pptx/oxml/shapes/shared.rb +292 -0
  56. data/lib/ruby_pptx/oxml/simple_types.rb +563 -0
  57. data/lib/ruby_pptx/oxml/slide.rb +290 -0
  58. data/lib/ruby_pptx/oxml/table.rb +253 -0
  59. data/lib/ruby_pptx/oxml/text.rb +324 -0
  60. data/lib/ruby_pptx/oxml/theme.rb +93 -0
  61. data/lib/ruby_pptx/package.rb +139 -0
  62. data/lib/ruby_pptx/parts/chart.rb +75 -0
  63. data/lib/ruby_pptx/parts/core_properties.rb +40 -0
  64. data/lib/ruby_pptx/parts/embedded_package.rb +43 -0
  65. data/lib/ruby_pptx/parts/image.rb +55 -0
  66. data/lib/ruby_pptx/parts/media.rb +17 -0
  67. data/lib/ruby_pptx/parts/presentation.rb +79 -0
  68. data/lib/ruby_pptx/parts/slide.rb +221 -0
  69. data/lib/ruby_pptx/parts/theme.rb +26 -0
  70. data/lib/ruby_pptx/pattern_matching.rb +50 -0
  71. data/lib/ruby_pptx/presentation.rb +128 -0
  72. data/lib/ruby_pptx/refinements.rb +21 -0
  73. data/lib/ruby_pptx/section.rb +130 -0
  74. data/lib/ruby_pptx/shapes/adjustments.rb +83 -0
  75. data/lib/ruby_pptx/shapes/authoring.rb +110 -0
  76. data/lib/ruby_pptx/shapes/base.rb +138 -0
  77. data/lib/ruby_pptx/shapes/freeform.rb +141 -0
  78. data/lib/ruby_pptx/shapes/placeholder.rb +184 -0
  79. data/lib/ruby_pptx/shapes/shape.rb +337 -0
  80. data/lib/ruby_pptx/shapes/shape_tree.rb +634 -0
  81. data/lib/ruby_pptx/sliceable.rb +34 -0
  82. data/lib/ruby_pptx/slide.rb +430 -0
  83. data/lib/ruby_pptx/table.rb +316 -0
  84. data/lib/ruby_pptx/table_paging.rb +91 -0
  85. data/lib/ruby_pptx/templates/default.pptx +0 -0
  86. data/lib/ruby_pptx/templates/docx-icon.emf +0 -0
  87. data/lib/ruby_pptx/templates/generic-icon.emf +0 -0
  88. data/lib/ruby_pptx/templates/media-speaker.png +0 -0
  89. data/lib/ruby_pptx/templates/notes.xml +23 -0
  90. data/lib/ruby_pptx/templates/notesMaster.xml +352 -0
  91. data/lib/ruby_pptx/templates/pptx-icon.emf +0 -0
  92. data/lib/ruby_pptx/templates/slideMaster.xml +277 -0
  93. data/lib/ruby_pptx/templates/theme.xml +321 -0
  94. data/lib/ruby_pptx/templates/xlsx-icon.emf +0 -0
  95. data/lib/ruby_pptx/text/fitter.rb +72 -0
  96. data/lib/ruby_pptx/text/font_metrics.rb +221 -0
  97. data/lib/ruby_pptx/text/text.rb +393 -0
  98. data/lib/ruby_pptx/theme.rb +98 -0
  99. data/lib/ruby_pptx/version.rb +5 -0
  100. data/lib/ruby_pptx.rb +90 -0
  101. metadata +174 -0
@@ -0,0 +1,316 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/pattern_matching"
4
+
5
+ require "ruby_pptx/sliceable"
6
+
7
+ require "ruby_pptx/element_proxy"
8
+ require "ruby_pptx/text/text"
9
+ require "ruby_pptx/dml/fill"
10
+
11
+ module Pptx
12
+ # A table inside a graphic frame.
13
+ #
14
+ # table = shape.table
15
+ # table.cell(0, 0).text = "Region"
16
+ # table.columns[0].width = Pptx.inches(2)
17
+ class Table
18
+ attr_reader :element, :parent
19
+
20
+ def initialize(tbl, parent)
21
+ @element = tbl
22
+ @parent = parent
23
+ end
24
+
25
+ def part = @parent.part
26
+
27
+ # The cell at +row_idx+, +col_idx+.
28
+ def cell(row_idx, col_idx) = Cell.new(@element.tc(row_idx, col_idx), self)
29
+
30
+ # `table[0, 1]` reads better than `table.cell(0, 1)` in a loop.
31
+ alias [] cell
32
+
33
+ def rows = @rows ||= TableRows.new(@element, self)
34
+
35
+ def columns = @columns ||= TableColumns.new(@element, self)
36
+
37
+ def row_count = @element.tr_list.size
38
+
39
+ def column_count = @element.tblGrid.gridCol_list.size
40
+
41
+ # Whether the first row is styled as a header. The same pattern applies to
42
+ # first_col, last_row, last_col, horz_banding and vert_banding.
43
+ def first_row? = @element.firstRow
44
+
45
+ def first_row=(value)
46
+ @element.firstRow = value
47
+ end
48
+
49
+ def first_col? = @element.firstCol
50
+
51
+ def first_col=(value)
52
+ @element.firstCol = value
53
+ end
54
+
55
+ def last_row? = @element.lastRow
56
+
57
+ def last_row=(value)
58
+ @element.lastRow = value
59
+ end
60
+
61
+ def last_col? = @element.lastCol
62
+
63
+ def last_col=(value)
64
+ @element.lastCol = value
65
+ end
66
+
67
+ # Whether alternate rows are shaded. Spelled out where python-pptx says
68
+ # `horz_banding`.
69
+ def banded_rows? = @element.bandRow
70
+
71
+ def banded_rows=(value)
72
+ @element.bandRow = value
73
+ end
74
+
75
+ def banded_columns? = @element.bandCol
76
+
77
+ def banded_columns=(value)
78
+ @element.bandCol = value
79
+ end
80
+
81
+ # @api private
82
+ # Resize the containing graphic frame to the sum of the row heights.
83
+ #
84
+ # A row calls this when its height changes: the frame and the table have
85
+ # to agree on the total, or PowerPoint shows the table clipped or adrift
86
+ # inside its frame.
87
+ def notify_height_changed
88
+ @parent.height = Length.emu(rows.sum { |row| row.height.emu })
89
+ end
90
+
91
+ # @api private
92
+ # As {#notify_height_changed}, for column widths.
93
+ def notify_width_changed
94
+ @parent.width = Length.emu(columns.sum { |column| column.width.emu })
95
+ end
96
+
97
+ # Iterate every cell, row by row.
98
+ def each_cell
99
+ return enum_for(:each_cell) { row_count * column_count } unless block_given?
100
+
101
+ row_count.times { |r| column_count.times { |c| yield cell(r, c) } }
102
+ self
103
+ end
104
+
105
+ def inspect = "#<Pptx::Table #{row_count}x#{column_count}>"
106
+ end
107
+
108
+ # The rows of a table.
109
+ class TableRows
110
+ include DeconstructToArray
111
+
112
+ include Sliceable
113
+ include Enumerable
114
+
115
+ def initialize(tbl, table)
116
+ @element = tbl
117
+ @table = table
118
+ end
119
+
120
+ def each
121
+ return enum_for(:each) { size } unless block_given?
122
+
123
+ @element.tr_list.each { |tr| yield TableRow.new(tr, @table) }
124
+ self
125
+ end
126
+
127
+ def size = @element.tr_list.size
128
+ alias length size
129
+
130
+ def [](index, length = nil)
131
+ slice_members(@element.tr_list, index, length) { |tr| TableRow.new(tr, @table) }
132
+ end
133
+ end
134
+
135
+ # The columns of a table.
136
+ class TableColumns
137
+ include DeconstructToArray
138
+
139
+ include Sliceable
140
+ include Enumerable
141
+
142
+ def initialize(tbl, table)
143
+ @element = tbl
144
+ @table = table
145
+ end
146
+
147
+ def each
148
+ return enum_for(:each) { size } unless block_given?
149
+
150
+ @element.tblGrid.gridCol_list.each { |col| yield TableColumn.new(col, @table) }
151
+ self
152
+ end
153
+
154
+ def size = @element.tblGrid.gridCol_list.size
155
+ alias length size
156
+
157
+ def [](index, length = nil)
158
+ slice_members(@element.tblGrid.gridCol_list, index, length) do |col|
159
+ TableColumn.new(col, @table)
160
+ end
161
+ end
162
+ end
163
+
164
+ # One row of a table.
165
+ class TableRow < ElementProxy
166
+ def initialize(tr, table)
167
+ super(tr)
168
+ @table = table
169
+ end
170
+
171
+ def height = @element.h
172
+
173
+ def height=(value)
174
+ @element.h = value
175
+ @table.notify_height_changed
176
+ end
177
+
178
+ def cells = @element.tc_list.map { |tc| Cell.new(tc, @table) }
179
+
180
+ def inspect = "#<Pptx::TableRow height=#{height&.inches}in>"
181
+ end
182
+
183
+ # One column of a table.
184
+ class TableColumn < ElementProxy
185
+ def initialize(grid_col, table)
186
+ super(grid_col)
187
+ @table = table
188
+ end
189
+
190
+ def width = @element.w
191
+
192
+ def width=(value)
193
+ @element.w = value
194
+ @table.notify_width_changed
195
+ end
196
+
197
+ def inspect = "#<Pptx::TableColumn width=#{width&.inches}in>"
198
+ end
199
+
200
+ # One cell of a table.
201
+ class Cell
202
+ include PatternMatching
203
+
204
+ pattern_keys :text, :span_width, :span_height, :merge_origin?, :spanned?, :vertical_anchor
205
+
206
+ attr_reader :element, :parent
207
+
208
+ def initialize(tc, parent)
209
+ @element = tc
210
+ @parent = parent
211
+ end
212
+
213
+ def part = @parent.part
214
+
215
+ def text_frame = @text_frame ||= TextFrame.new(@element.get_or_add_txBody, self)
216
+
217
+ def text = text_frame.text
218
+
219
+ def text=(value)
220
+ text_frame.text = value
221
+ end
222
+
223
+ def fill = @fill ||= FillFormat.from_fill_parent(cell_properties)
224
+
225
+ # @return [Pptx::Enum::MSO_ANCHOR, nil]
226
+ def vertical_anchor = cell_properties.anchor
227
+
228
+ def vertical_anchor=(value)
229
+ cell_properties.anchor = value
230
+ end
231
+
232
+ def margin_left = cell_properties.marL
233
+
234
+ def margin_left=(value)
235
+ cell_properties.marL = value
236
+ end
237
+
238
+ def margin_right = cell_properties.marR
239
+
240
+ def margin_right=(value)
241
+ cell_properties.marR = value
242
+ end
243
+
244
+ def margin_top = cell_properties.marT
245
+
246
+ def margin_top=(value)
247
+ cell_properties.marT = value
248
+ end
249
+
250
+ def margin_bottom = cell_properties.marB
251
+
252
+ def margin_bottom=(value)
253
+ cell_properties.marB = value
254
+ end
255
+
256
+ # How many columns this cell spans.
257
+ #
258
+ # Only a merge origin carries the real span; on any other cell this reads
259
+ # 1 whether or not it is part of a merge. Test {#merge_origin?} first.
260
+ def span_width = @element.gridSpan
261
+
262
+ # How many rows this cell spans. See {#span_width} on when to trust it.
263
+ def span_height = @element.rowSpan
264
+
265
+ # True when this cell is the top-left of a merged range.
266
+ def merge_origin? = @element.merge_origin?
267
+
268
+ # True when this cell is covered by a merge rather than being its origin.
269
+ def spanned? = @element.spanned?
270
+
271
+ # Merge this cell with +other+, which is the opposite corner of the range.
272
+ #
273
+ # Either diagonal may be given, in either order. The text of every cell in
274
+ # the range is gathered into the top-left one, which is the cell that stays
275
+ # visible.
276
+ #
277
+ # @raise [Error] when the cells are in different tables, or the range
278
+ # already contains a merge -- merging over a merge produces a table
279
+ # PowerPoint cannot lay out
280
+ def merge(other)
281
+ raise Error, "cannot merge cells from different tables" unless @element.tbl == other.element.tbl
282
+
283
+ range = @element.tbl.cell_range(@element, other.element)
284
+ raise Error, "the range already contains a merged cell" if range.contains_merged_cell?
285
+
286
+ range.move_content_to_origin
287
+ range.top_row_cells.each { |tc| tc.rowSpan = range.row_count }
288
+ range.left_column_cells.each { |tc| tc.gridSpan = range.column_count }
289
+ range.cells_except_left_column.each { |tc| tc.hMerge = true }
290
+ range.cells_except_top_row.each { |tc| tc.vMerge = true }
291
+ self
292
+ end
293
+
294
+ # Undo a merge, giving back a separate cell for each grid position it
295
+ # covered. The text stays in the origin cell.
296
+ #
297
+ # @raise [Error] unless this is a merge origin
298
+ def split
299
+ raise Error, "only a merge-origin cell can be split" unless merge_origin?
300
+
301
+ Oxml::CellRange.from_merge_origin(@element).cells.each do |tc|
302
+ tc.rowSpan = 1
303
+ tc.gridSpan = 1
304
+ tc.hMerge = false
305
+ tc.vMerge = false
306
+ end
307
+ self
308
+ end
309
+
310
+ def inspect = "#<Pptx::Cell #{text.inspect}>"
311
+
312
+ private
313
+
314
+ def cell_properties = @element.get_or_add_tcPr
315
+ end
316
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pptx
4
+ # Splits a long table across as many slides as it needs.
5
+ #
6
+ # PowerPoint has no notion of a table that flows: a table lives on one slide
7
+ # and is clipped if it outgrows it. Paging it is therefore something the
8
+ # caller has to do, and this does it — repeating the header on each page so
9
+ # every slide stands on its own.
10
+ #
11
+ # prs.slides.add_table_pages(rows,
12
+ # layout: prs.slide_layouts["Blank"],
13
+ # left: Pptx.inches(0.5), top: Pptx.inches(1),
14
+ # width: Pptx.inches(9), height: Pptx.inches(5))
15
+ module TablePaging
16
+ # The row height assumed when working out how many rows fit, unless the
17
+ # caller says otherwise.
18
+ DEFAULT_ROW_HEIGHT = Length.emu(365_760) # 0.4in
19
+
20
+ module_function
21
+
22
+ # Split +rows+ into pages of at most +rows_per_page+ body rows, repeating
23
+ # +header+ at the top of each.
24
+ #
25
+ # @return [Array<Array<Array>>] one array of rows per page
26
+ def paginate(rows, rows_per_page:, header: nil)
27
+ body = rows.to_a
28
+ return [] if body.empty?
29
+
30
+ raise ArgumentError, "rows_per_page must be positive" unless rows_per_page.positive?
31
+
32
+ body.each_slice(rows_per_page).map do |page|
33
+ header ? [header] + page : page
34
+ end
35
+ end
36
+
37
+ # How many body rows fit in +height+, leaving room for a repeated header.
38
+ def rows_per_page(height:, row_height: DEFAULT_ROW_HEIGHT, header: false)
39
+ available = Length.coerce(height).emu
40
+ per_row = Length.coerce(row_height).emu
41
+ raise ArgumentError, "row_height must be positive" unless per_row.positive?
42
+
43
+ fitting = available / per_row
44
+ fitting -= 1 if header
45
+ [fitting, 1].max
46
+ end
47
+ end
48
+
49
+ class Slides
50
+ # Add one slide per page of +rows+, each carrying a table.
51
+ #
52
+ # The first row is treated as a header and repeated on every page unless
53
+ # +header+ is false. When +rows_per_page+ is omitted it is derived from
54
+ # +height+ and +row_height+.
55
+ #
56
+ # @return [Array<Slide>] the slides created, in order
57
+ def add_table_pages(rows, layout:, left:, top:, width:, height:,
58
+ rows_per_page: nil, header: true,
59
+ row_height: TablePaging::DEFAULT_ROW_HEIGHT)
60
+ rows = rows.to_a
61
+ return [] if rows.empty?
62
+
63
+ header_row = header ? rows.first : nil
64
+ body = header ? rows.drop(1) : rows
65
+ return [] if body.empty?
66
+
67
+ per_page = rows_per_page || TablePaging.rows_per_page(
68
+ height: height, row_height: row_height, header: !header_row.nil?
69
+ )
70
+
71
+ TablePaging.paginate(body, rows_per_page: per_page, header: header_row).map do |page_rows|
72
+ slide = add(layout)
73
+ fill_table(slide, page_rows, left, top, width, height)
74
+ slide
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def fill_table(slide, page_rows, left, top, width, height)
81
+ columns = page_rows.map(&:size).max
82
+ frame = slide.shapes.add_table(page_rows.size, columns,
83
+ at: [left, top], size: [width, height])
84
+ table = frame.table
85
+ page_rows.each_with_index do |row, row_index|
86
+ row.each_with_index { |value, col| table.cell(row_index, col).text = value.to_s }
87
+ end
88
+ table
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,23 @@
1
+ <?xml version='1.0' encoding='UTF-8' standalone='yes'?>
2
+ <p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
3
+ <p:cSld>
4
+ <p:spTree>
5
+ <p:nvGrpSpPr>
6
+ <p:cNvPr id="1" name=""/>
7
+ <p:cNvGrpSpPr/>
8
+ <p:nvPr/>
9
+ </p:nvGrpSpPr>
10
+ <p:grpSpPr>
11
+ <a:xfrm>
12
+ <a:off x="0" y="0"/>
13
+ <a:ext cx="0" cy="0"/>
14
+ <a:chOff x="0" y="0"/>
15
+ <a:chExt cx="0" cy="0"/>
16
+ </a:xfrm>
17
+ </p:grpSpPr>
18
+ </p:spTree>
19
+ </p:cSld>
20
+ <p:clrMapOvr>
21
+ <a:masterClrMapping/>
22
+ </p:clrMapOvr>
23
+ </p:notes>