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
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
|