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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6cd5ac8827fcd5b81d86a1b0a0faa9f619f776dd005580589bbe88fd3f666d4a
4
+ data.tar.gz: 6dafdf4a15d53bb8d4a795d3e2c0a18beebab2bd35070076d69e2a7cddddd29f
5
+ SHA512:
6
+ metadata.gz: b54ec7a6115da3d4da42ee124faefb0fee0e51d200525af1c05527fe9a6c579d279ca7da59293ce71b81a56a03c4e1567b228cb86caa234e28f1b0faf44bd9ef
7
+ data.tar.gz: 2d991bf2f20c571783f5d1243d586b9e0aa9b4f2db655872c1b60a71ada29495b5e91ab587674e82fa98d34a8cb552e51f5c6fc20b76afa680691cf84bbccce8
data/CHANGELOG.md ADDED
@@ -0,0 +1,83 @@
1
+ # Changelog
2
+
3
+ All notable changes to this gem are recorded here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Before 1.0 a minor
6
+ version may change the public API.
7
+
8
+ ## [0.1.0] - 2026-09-28
9
+
10
+ The first release: a Ruby port of [python-pptx](https://github.com/scanny/python-pptx)
11
+ 1.0.2, covering its whole public API, with an API redesigned for Ruby over an
12
+ object model kept close to the original.
13
+
14
+ ### Added
15
+
16
+ **Everything python-pptx does.** Every public class and member of python-pptx
17
+ 1.0.2 has a counterpart; `spec/ruby_pptx/api_completeness_spec.rb` checks this
18
+ against python-pptx itself and fails if a gap appears.
19
+
20
+ - Presentations: open, create from the default template, save; slide size;
21
+ core properties.
22
+ - Slides, layouts and masters: add slides; find layouts by name and delete
23
+ unused ones; backgrounds; speaker notes (`slide.notes = "…"`) and the notes
24
+ master.
25
+ - Shapes: auto shapes with adjustment handles, text boxes, pictures, movies,
26
+ connectors attached to shapes, groups, freeforms, tables with merged cells,
27
+ and OLE objects shown as icons.
28
+ - Placeholders, with geometry inherited from layout and master, and
29
+ `insert_picture` (cropped to fit), `insert_table` and `insert_chart`.
30
+ - Text: paragraphs, runs, fonts, alignment, spacing, autofit, hyperlinks and
31
+ click actions, and `fit_text` to shrink text into its shape.
32
+ - Formatting: solid, patterned, gradient and background fills; line width,
33
+ colour and dash style; shadows; theme and RGB colours with brightness.
34
+ - Pictures: PNG, JPEG, GIF, BMP, TIFF, WMF and EMF, read without Pillow;
35
+ cropping; masking with a shape; reading the embedded image back.
36
+ - Charts: bar, column, line, pie, doughnut, area, radar, XY and bubble, in
37
+ every python-pptx variant; grouped and date categories; replacing a chart's
38
+ data; titles, legends, axes, tick marks and labels, gridlines, data labels
39
+ per plot, series and point; markers; fonts; chart style; the exact chart
40
+ type read back from an existing chart. The embedded workbook is written
41
+ without XlsxWriter.
42
+
43
+ **Beyond python-pptx.**
44
+
45
+ - Slide sections.
46
+ - Paging a long table across as many slides as it needs.
47
+ - Combo charts, with a secondary axis.
48
+ - SVG pictures, with a raster fallback for older viewers.
49
+ - Defining a slide master, its theme and its layouts in code.
50
+ - `Pptx.build`, a declarative way to write a whole deck.
51
+
52
+ **A Ruby API.**
53
+
54
+ - Keyword arguments for position and size (`at:`, `size:`), which also accept
55
+ `Pptx::Point` and `Pptx::Size` value objects with arithmetic.
56
+ - Explicit lengths (`Pptx.inches(1)`, `Pptx.pt(18)`), and `1.inch` through
57
+ either the `Pptx::Lengths` refinement or the opt-in `ruby_pptx/core_ext`.
58
+ - Enumerable collections that index like arrays, ranges included.
59
+ - `case`/`in` pattern matching on shapes, slides, text, charts, lengths and
60
+ colours.
61
+ - Predicates (`chart.legend?`, `frame.chart?`) and nil for "inherited" or
62
+ "absent" throughout; reading a property never changes the document.
63
+
64
+ ### Differences from python-pptx
65
+
66
+ Output matches python-pptx part for part, except where python-pptx is wrong
67
+ or where matching it cannot be done. Each case is listed, with its reason, in
68
+ [PORTING.md](PORTING.md):
69
+
70
+ - `fit_text` measures glyph outlines rather than rasterizing with Pillow, so
71
+ it can choose a size one point different on text that only just fits. It
72
+ also takes a `font_file:` rather than searching for one, since python-pptx's
73
+ search works only on macOS and Windows.
74
+ - The chart's embedded workbook is written directly; its contents match, its
75
+ bytes do not.
76
+ - python-pptx bugs this does not reproduce: `fit_text` raising `TypeError` on a
77
+ word wider than the shape; reading the angle of a fresh gradient raising
78
+ `TypeError`; an empty category label reading as `"None"`; an axis with no
79
+ delete setting reading as hidden; EMF images stored as WMF.
80
+ - Getters that write to the document in python-pptx -- data-label flags, axis
81
+ and chart titles -- do not here.
82
+
83
+ [0.1.0]: https://github.com/Largo/ruby_pptx/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/NOTICE ADDED
@@ -0,0 +1,48 @@
1
+ # Third-party notices
2
+
3
+ ## python-pptx
4
+
5
+ This gem is a port of python-pptx (https://github.com/scanny/python-pptx).
6
+ Much of its structure, and the behaviour it reproduces, derives directly from
7
+ that project. The following files are copied from it verbatim:
8
+
9
+ lib/ruby_pptx/templates/default.pptx
10
+ lib/ruby_pptx/templates/notes.xml
11
+ lib/ruby_pptx/templates/notesMaster.xml
12
+ lib/ruby_pptx/templates/theme.xml
13
+ lib/ruby_pptx/templates/media-speaker.png
14
+ lib/ruby_pptx/templates/docx-icon.emf
15
+ lib/ruby_pptx/templates/pptx-icon.emf
16
+ lib/ruby_pptx/templates/xlsx-icon.emf
17
+ lib/ruby_pptx/templates/generic-icon.emf
18
+
19
+ The following file is derived from python-pptx's default template: its colour
20
+ map and text styles are taken verbatim from the slide master inside
21
+ `default.pptx`, with the shape tree emptied and the layout list cleared, so
22
+ that a master created by this gem starts from the same defaults PowerPoint
23
+ itself would use:
24
+
25
+ lib/ruby_pptx/templates/slideMaster.xml
26
+
27
+ python-pptx is distributed under the MIT License, reproduced in full below.
28
+
29
+ The MIT License (MIT)
30
+ Copyright (c) 2013 Steve Canny, https://github.com/scanny
31
+
32
+ Permission is hereby granted, free of charge, to any person obtaining a copy
33
+ of this software and associated documentation files (the "Software"), to deal
34
+ in the Software without restriction, including without limitation the rights
35
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
36
+ copies of the Software, and to permit persons to whom the Software is
37
+ furnished to do so, subject to the following conditions:
38
+
39
+ The above copyright notice and this permission notice shall be included in
40
+ all copies or substantial portions of the Software.
41
+
42
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
43
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
44
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
45
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
46
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
47
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
48
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,272 @@
1
+ # ruby_pptx
2
+
3
+ Create, read and update PowerPoint (`.pptx`) files from Ruby.
4
+
5
+ A port of [python-pptx](https://github.com/scanny/python-pptx) — the same
6
+ battle-tested OOXML object model underneath, with a public API redesigned for
7
+ Ruby. See [PORTING.md](PORTING.md) for the architecture and the milestone plan.
8
+
9
+ > **Status:** every public class and member of python-pptx 1.0.2 has a
10
+ > counterpart here, which `spec/ruby_pptx/api_completeness_spec.rb` checks
11
+ > rather than asserts. Output is verified against python-pptx part for part.
12
+ > Where the two deliberately differ -- mostly python-pptx bugs this does not
13
+ > reproduce -- [PORTING.md](PORTING.md) lists each one.
14
+
15
+ ```ruby
16
+ require "ruby_pptx"
17
+
18
+ prs = Pptx::Presentation.new_default # or .open("deck.pptx")
19
+ slide = prs.slides.add(prs.slide_layouts["Title and Content"])
20
+
21
+ slide.shapes.title.text = "Quarterly Review"
22
+
23
+ body = slide.placeholders[1].text_frame
24
+ body.text = "Revenue up 12%\nCosts flat"
25
+ body.paragraphs.first.runs.first.font.tap do |font|
26
+ font.bold = true
27
+ font.size = Pptx.pt(24)
28
+ font.color.rgb = Pptx::RGBColor["C0504D"]
29
+ end
30
+
31
+ box = slide.shapes.add_shape(:rounded_rectangle,
32
+ at: [Pptx.inches(1), Pptx.inches(5)],
33
+ size: [Pptx.inches(3), Pptx.inches(1)])
34
+ box.fill.solid
35
+ box.fill.fore_color.rgb = Pptx::RGBColor["1F497D"]
36
+ box.text_frame.text = "Next steps"
37
+
38
+ slide.shapes.add_picture("logo.png", at: [Pptx.inches(7), Pptx.inches(0.5)],
39
+ width: Pptx.inches(2))
40
+
41
+ table_frame = slide.shapes.add_table(2, 3, at: [Pptx.inches(1), Pptx.inches(3)],
42
+ size: [Pptx.inches(8), Pptx.inches(2)])
43
+ table_frame.table[0, 0].text = "Region"
44
+
45
+ data = Pptx::ChartData.new
46
+ data.categories = ["East", "West", "Midwest"]
47
+ data.add_series("Q1", [1.2, 2.0, 3.5])
48
+ slide.shapes.add_chart(:column_clustered, data,
49
+ at: [Pptx.inches(1), Pptx.inches(3)],
50
+ size: [Pptx.inches(8), Pptx.inches(4)])
51
+
52
+ # Scatter and bubble charts take points rather than a value per category.
53
+ xy = Pptx::XyChartData.new
54
+ xy.add_series("Alpha", points: [[1, 10], [2, 20]])
55
+
56
+ box.hyperlink = "https://example.com"
57
+
58
+ # Connectors attach to shapes, and groups size themselves around their contents.
59
+ line = slide.shapes.add_connector(:straight, begin_at: [0, 0], end_at: [0, 0])
60
+ line.begin_connect(box, 3)
61
+ group = slide.shapes.add_group_shape([box, table_frame])
62
+
63
+ slide.shapes.add_movie("clip.mp4", at: [x, y], size: [w, h], content_type: "video/mp4")
64
+
65
+ prs.save("out.pptx")
66
+ ```
67
+
68
+ Or build a whole deck declaratively:
69
+
70
+ ```ruby
71
+ deck = Pptx.build do |d|
72
+ d.slide_size = :widescreen
73
+
74
+ d.slide("Title Slide") do |s|
75
+ s.title = "Annual Report"
76
+ s.subtitle = "Prepared in Ruby"
77
+ end
78
+
79
+ d.section("Detail") do
80
+ d.slide("Blank") do |s|
81
+ s.shape :rounded_rectangle, at: [Pptx.inches(1), Pptx.inches(1)],
82
+ size: [Pptx.inches(3), Pptx.inches(1)],
83
+ fill: "1F497D", text: "Next steps"
84
+ s.chart :column_clustered, categories: %w[East West],
85
+ series: { "Q1" => [1, 2] },
86
+ at: [Pptx.inches(1), Pptx.inches(3)],
87
+ size: [Pptx.inches(6), Pptx.inches(4)]
88
+ end
89
+ end
90
+ end
91
+
92
+ deck.save("out.pptx")
93
+ ```
94
+
95
+ Five things python-pptx does not do: **SVG pictures**, **slide sections**,
96
+ **paging a long table across as many slides as it needs**, **combo charts**
97
+ with a secondary axis, and **defining a slide master in code**.
98
+
99
+ ```ruby
100
+ data = Pptx::ChartData.new
101
+ data.categories = %w[Q1 Q2 Q3]
102
+ data.add_series("Revenue", [120, 135, 150])
103
+ data.add_series("Margin", [0.21, 0.24, 0.22])
104
+
105
+ slide.shapes.add_combo_chart(data, at: [x, y], size: [w, h]) do |combo|
106
+ combo.plot :column_clustered, series: "Revenue"
107
+ combo.plot :line, series: "Margin", secondary_axis: true
108
+ end
109
+ ```
110
+
111
+ ```ruby
112
+ # PowerPoint wants a raster stand-in beside the vector, and this gem has no
113
+ # rasterizer, so you supply it.
114
+ slide.shapes.add_picture("logo.svg", at: [x, y], fallback: "logo.png")
115
+ ```
116
+
117
+ Text can be shrunk to fit the shape holding it. Measuring needs the actual
118
+ glyph outlines, so you pass the font file; `font_family` is what gets written
119
+ into the deck.
120
+
121
+ ```ruby
122
+ box.text_frame.fit_text(font_file: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
123
+ font_family: "DejaVu Sans",
124
+ max_size: 28)
125
+ #=> 28 (the point size applied, never above max_size)
126
+ ```
127
+
128
+ That turns word wrap on, autofit off, and applies the size to every run. Sizes
129
+ are measured from the font's own metrics rather than by rendering, so they can
130
+ differ from python-pptx's by a point on text that only just fits; PORTING.md
131
+ has the measured comparison.
132
+
133
+ A slide master can be built from scratch, rather than only edited in a
134
+ template. The master gets its own theme, so its colours and fonts are
135
+ independent of any other master in the deck.
136
+
137
+ ```ruby
138
+ master = deck.slide_masters.add(name: "Corporate")
139
+ master.theme.colors.update(accent1: "1F497D", accent2: "C0504D")
140
+ master.theme.fonts.major = "Georgia"
141
+ master.theme.fonts.minor = "Verdana"
142
+
143
+ layout = master.slide_layouts.add("Title and Content", type: "obj") do |l|
144
+ l.placeholders.add(:title, at: [Pptx.inches(0.5), Pptx.inches(0.3)],
145
+ size: [Pptx.inches(9), Pptx.inches(1.25)])
146
+ l.placeholders.add(:body, idx: 1, at: [Pptx.inches(0.5), Pptx.inches(1.75)],
147
+ size: [Pptx.inches(9), Pptx.inches(4.5)])
148
+ end
149
+
150
+ slide = deck.slides.add(layout)
151
+ slide.shapes.title.text = "Built from a hand-made master"
152
+ ```
153
+
154
+ The master starts with the five placeholders PowerPoint expects — title, body,
155
+ date, footer and slide number — scaled to the deck's slide size. Pass
156
+ `placeholders: :none` for a bare one.
157
+
158
+ ```ruby
159
+ deck.sections.add("Appendix", slides: deck.slides.to_a.last(2))
160
+
161
+ deck.slides.add_table_pages(rows,
162
+ layout: deck.slide_layouts["Blank"],
163
+ left: Pptx.inches(0.5), top: Pptx.inches(1),
164
+ width: Pptx.inches(9), height: Pptx.inches(5))
165
+ ```
166
+
167
+ Lengths are explicit rather than bare numbers:
168
+
169
+ ```ruby
170
+ Pptx.inches(1).emu #=> 914400
171
+ Pptx.cm(2.54).pt #=> 72.0
172
+ ```
173
+
174
+ Numeric sugar is opt-in, and comes two ways. Prefer the refinement: it is
175
+ scoped to the file that asks for it, so it cannot surprise anything else
176
+ sharing the process.
177
+
178
+ ```ruby
179
+ require "ruby_pptx/refinements"
180
+ using Pptx::Lengths # this file only
181
+ 1.inch == 72.points #=> true
182
+
183
+ require "ruby_pptx/core_ext" # or patch Numeric process-wide
184
+ ```
185
+
186
+ `at:` and `size:` have always taken a two-element array, and still do. Passing
187
+ a `Point` or a `Size` instead costs nothing and buys named readers, arithmetic
188
+ and pattern matching:
189
+
190
+ ```ruby
191
+ origin = Pptx.point(Pptx.inches(1), Pptx.inches(1))
192
+ box = Pptx.size(Pptx.inches(3), Pptx.inches(1))
193
+
194
+ slide.shapes.add_shape(:rectangle, at: origin, size: box)
195
+ slide.shapes.add_shape(:rectangle, at: origin + [0, Pptx.inches(1.5)], size: box * 2)
196
+ ```
197
+
198
+ Reading a deck back supports `case`/`in`. Enum-valued attributes read as their
199
+ symbolic name in a pattern, and only the keys a pattern asks for are computed:
200
+
201
+ ```ruby
202
+ slide.shapes.each do |shape|
203
+ case shape
204
+ in {shape_type: :PICTURE, name:} then puts "picture #{name}"
205
+ in {shape_type: :PLACEHOLDER, placeholder_format: {type: :TITLE}}
206
+ then puts shape.text_frame.text
207
+ in {width:} if width > Pptx.inches(5) then puts "#{shape.name} is wide"
208
+ else next
209
+ end
210
+ end
211
+ ```
212
+
213
+ Collections index like arrays — `slides[2]`, `slides[-1]`, `slides[1..3]`,
214
+ `slides[1, 2]` — and deconstruct into array patterns.
215
+
216
+ ## Development
217
+
218
+ ```bash
219
+ bundle install
220
+ bundle exec rspec
221
+ bundle exec rubocop
222
+ ```
223
+
224
+ `.rubocop.yml` records where this codebase deliberately departs from the
225
+ default style — chiefly that the `oxml` layer keeps the OOXML schema's own
226
+ names (`CT_Shape`, `#cSld`, `accent1`) so the XML, the specification and the
227
+ Ruby read side by side.
228
+
229
+ Specs compare the packages this gem writes against the ones python-pptx writes
230
+ for the same operation, part by part, on canonicalised XML. To run those:
231
+
232
+ ```bash
233
+ pip install -r spec/oracle-requirements.txt
234
+ ```
235
+
236
+ Without it, the oracle-backed specs skip and the rest still run. CI sets
237
+ `REQUIRE_ORACLE=1`, which turns those skips into failures so a broken Python
238
+ environment cannot quietly reduce the suite to its unit tests. The same goes
239
+ for the `fit_text` specs, which measure with fonts from Debian's
240
+ `fonts-dejavu-core` and `fonts-urw-base35` packages.
241
+
242
+ A slide master has no oracle — python-pptx can read one but not create one —
243
+ so it is validated against the published ISO/IEC 29500-4 schemas instead.
244
+ Those are not redistributed here; point `OOXML_SCHEMAS` at a directory holding
245
+ `pml.xsd`, `dml-main.xsd` and the `shared-*.xsd` files they import:
246
+
247
+ ```bash
248
+ OOXML_SCHEMAS=/path/to/schemas bundle exec rspec
249
+ ```
250
+
251
+ ### Releasing
252
+
253
+ Bump `Pptx::VERSION`, add its `CHANGELOG.md` entry, then push a matching tag:
254
+
255
+ ```bash
256
+ git tag -a v0.1.0 -m "ruby_pptx 0.1.0" && git push origin v0.1.0
257
+ ```
258
+
259
+ `.github/workflows/release.yml` runs the full CI suite, builds the gem, installs
260
+ it into an empty gem home to prove it loads, and creates a GitHub release with
261
+ the gem attached and the changelog entry as its notes. It pushes to RubyGems
262
+ only once a `RUBYGEMS_API_KEY` secret is set.
263
+
264
+ ## License
265
+
266
+ MIT — see [LICENSE](LICENSE).
267
+
268
+ This gem is a port of [python-pptx](https://github.com/scanny/python-pptx) by
269
+ Steve Canny, which is also MIT licensed, and it vendors nine template files
270
+ (the default deck, notes and theme templates, the video poster frame and the
271
+ OLE object icons) from that project verbatim, plus one derived from them. See
272
+ [NOTICE](NOTICE) for the full attribution and the upstream licence text.
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/oxml/action"
4
+ require "ruby_pptx/enum/action"
5
+
6
+ module Pptx
7
+ # What happens when a shape or run is clicked.
8
+ #
9
+ # Most callers want {Pptx::BaseShape#hyperlink}, which is a shortcut to this
10
+ # object's URL. Reach for `click_action` when the click does something other
11
+ # than open a link -- jumping to a slide, for instance.
12
+ class ActionSetting
13
+ # `ppaction://hlinkshowjump?jump=...` covers the relative moves.
14
+ RELATIVE_JUMPS = {
15
+ "firstslide" => :FIRST_SLIDE, "lastslide" => :LAST_SLIDE,
16
+ "lastslideviewed" => :LAST_SLIDE_VIEWED, "nextslide" => :NEXT_SLIDE,
17
+ "previousslide" => :PREVIOUS_SLIDE, "endshow" => :END_SHOW
18
+ }.freeze
19
+
20
+ # Every other action is identified by the host of its `ppaction://` URL.
21
+ # A hyperlink has no action attribute at all.
22
+ ACTION_VERBS = {
23
+ nil => :HYPERLINK, "hlinksldjump" => :NAMED_SLIDE, "hlinkpres" => :PLAY,
24
+ "hlinkfile" => :OPEN_FILE, "customshow" => :NAMED_SLIDE_SHOW,
25
+ "ole" => :OLE_VERB, "macro" => :RUN_MACRO, "program" => :RUN_PROGRAM
26
+ }.freeze
27
+
28
+ # @param x_pr [Pptx::Oxml::Element] a `p:cNvPr` or an `a:rPr`
29
+ # @param parent the shape or run this action belongs to
30
+ def initialize(x_pr, parent, hover: false)
31
+ @element = x_pr
32
+ @parent = parent
33
+ @hover = hover
34
+ end
35
+
36
+ def part = @parent.part
37
+
38
+ # @return [Pptx::Enum::PP_ACTION] NONE when nothing happens on click
39
+ def action
40
+ link = hlink
41
+ return Enum::PP_ACTION::NONE if link.nil?
42
+
43
+ verb = link.action_verb
44
+ if verb == "hlinkshowjump"
45
+ name = RELATIVE_JUMPS[link.action_fields["jump"]]
46
+ return name ? Enum::PP_ACTION.fetch(name) : Enum::PP_ACTION::NONE
47
+ end
48
+
49
+ name = ACTION_VERBS[verb]
50
+ name ? Enum::PP_ACTION.fetch(name) : Enum::PP_ACTION::NONE
51
+ end
52
+
53
+ # The relationship target of this click, whatever kind it is.
54
+ #
55
+ # For a hyperlink that is the URL. For a slide jump it is the target
56
+ # slide's partname, which is what python-pptx reports here too -- its
57
+ # docstring says otherwise, but the code returns the ref for any action
58
+ # carrying a relationship. {#url} is the one that means "a hyperlink".
59
+ def address
60
+ link = hlink
61
+ return nil if link.nil?
62
+
63
+ r_id = link.rId
64
+ return nil if r_id.nil? || r_id.empty?
65
+
66
+ part.target_ref(r_id)
67
+ end
68
+
69
+ # Set, change or (with nil) remove the hyperlink.
70
+ def address=(url)
71
+ clear
72
+ return if url.nil? || url.empty?
73
+
74
+ get_or_add_hlink.rId = part.relate_to(url, Opc::RELATIONSHIP_TYPE::HYPERLINK, external: true)
75
+ end
76
+
77
+ # The URL this click opens, or nil when the click is not a hyperlink.
78
+ #
79
+ # Unlike {#address} this does not report a slide partname for a jump: a
80
+ # thing called a URL should be a URL.
81
+ def url = action == Enum::PP_ACTION::HYPERLINK ? address : nil
82
+
83
+ # The slide this click jumps to, or nil.
84
+ def target_slide
85
+ return nil unless action == Enum::PP_ACTION::NAMED_SLIDE
86
+
87
+ part.related_part(hlink.rId).slide
88
+ end
89
+
90
+ def target_slide=(slide)
91
+ clear
92
+ return if slide.nil?
93
+
94
+ link = get_or_add_hlink
95
+ link.action = "#{Oxml::CT_Hyperlink::ACTION_SCHEME}hlinksldjump"
96
+ link.rId = part.relate_to(slide.part, Opc::RELATIONSHIP_TYPE::SLIDE)
97
+ end
98
+
99
+ # Remove whatever this click does, and the relationship behind it.
100
+ def clear
101
+ link = hlink
102
+ return self if link.nil?
103
+
104
+ r_id = link.rId
105
+ part.drop_rel(r_id) if r_id && !r_id.empty?
106
+ @element.remove(link)
107
+ self
108
+ end
109
+
110
+ def inspect = "#<Pptx::ActionSetting #{action.name} #{address.inspect}>"
111
+
112
+ private
113
+
114
+ def hlink = @element.find(@hover ? "a:hlinkHover" : "a:hlinkClick")
115
+
116
+ def get_or_add_hlink
117
+ @hover ? @element.get_or_add_hlinkHover : @element.get_or_add_hlinkClick
118
+ end
119
+ end
120
+ end