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,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/shapes/shape"
|
|
4
|
+
|
|
5
|
+
module Pptx
|
|
6
|
+
# Position and size that fall back to the placeholder being inherited from.
|
|
7
|
+
#
|
|
8
|
+
# A placeholder usually carries no `a:xfrm` of its own: it takes its geometry
|
|
9
|
+
# from the layout, which takes it from the master. Reading it back therefore
|
|
10
|
+
# has to walk that chain, or every placeholder on a freshly built slide looks
|
|
11
|
+
# like it has no position at all.
|
|
12
|
+
#
|
|
13
|
+
# Writing is not inherited -- setting a value applies it to this shape, which
|
|
14
|
+
# is what stops it tracking the layout from then on.
|
|
15
|
+
module InheritsDimensions
|
|
16
|
+
def left = effective(:left)
|
|
17
|
+
def top = effective(:top)
|
|
18
|
+
def width = effective(:width)
|
|
19
|
+
def height = effective(:height)
|
|
20
|
+
|
|
21
|
+
# The placeholder one level up that this one inherits from, or nil.
|
|
22
|
+
def base_placeholder = raise(NotImplementedError, "#{self.class} must define base_placeholder")
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def effective(attribute)
|
|
27
|
+
own = super_value(attribute)
|
|
28
|
+
return own unless own.nil?
|
|
29
|
+
|
|
30
|
+
base_placeholder&.public_send(attribute)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def super_value(attribute)
|
|
34
|
+
method(attribute).super_method.call
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A placeholder on a slide.
|
|
39
|
+
#
|
|
40
|
+
# Position, size and formatting are inherited from the matching placeholder
|
|
41
|
+
# on the layout unless this shape overrides them.
|
|
42
|
+
class SlidePlaceholder < Shape
|
|
43
|
+
include InheritsDimensions
|
|
44
|
+
|
|
45
|
+
def shape_type = Enum::MSO_SHAPE_TYPE::PLACEHOLDER
|
|
46
|
+
|
|
47
|
+
# Matched to the layout by `idx`, which is what ties the two together.
|
|
48
|
+
def base_placeholder
|
|
49
|
+
part.slide_layout.placeholders.by_idx(element.ph_idx)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def inspect
|
|
53
|
+
"#<#{self.class.name} idx=#{placeholder_format.idx} type=#{placeholder_format.type}>"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# A slide placeholder that can be filled with something other than text:
|
|
58
|
+
# the new content takes over the placeholder's slot, id and name, and keeps
|
|
59
|
+
# the `p:ph` that ties it to the layout.
|
|
60
|
+
module FillablePlaceholder
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Put +new_element+ where this placeholder is and move the `p:ph` onto
|
|
64
|
+
# it. The original element leaves the tree; this object is spent.
|
|
65
|
+
def replace_placeholder_with(new_element)
|
|
66
|
+
adopted = @element.build_from_xml(new_element.node.to_xml)
|
|
67
|
+
adopted.nvXxPr.nvPr.insert_ph(@element.ph)
|
|
68
|
+
@element.node.add_previous_sibling(adopted.node)
|
|
69
|
+
@element.parent.remove(@element)
|
|
70
|
+
ShapeFactory.build_for_slide(adopted, @parent)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# A picture placeholder: fill it with {#insert_picture}.
|
|
75
|
+
class PicturePlaceholder < SlidePlaceholder
|
|
76
|
+
include FillablePlaceholder
|
|
77
|
+
|
|
78
|
+
# Fill this placeholder with an image, cropped to cover it exactly.
|
|
79
|
+
#
|
|
80
|
+
# The picture keeps no size of its own -- it inherits the placeholder's
|
|
81
|
+
# from the layout -- so the image is cropped, never distorted, to fit.
|
|
82
|
+
#
|
|
83
|
+
# @param image_file [String, IO] a path or an open stream
|
|
84
|
+
# @return [PlaceholderPicture]
|
|
85
|
+
def insert_picture(image_file)
|
|
86
|
+
image_part, r_id = part.get_or_add_image_part(image_file)
|
|
87
|
+
pic = Oxml::CT_Picture.new_ph_pic(shape_id, name, image_part.desc, r_id)
|
|
88
|
+
pic.crop_to_fit(image_part.image.size, [width.emu, height.emu])
|
|
89
|
+
replace_placeholder_with(pic)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A table placeholder: fill it with {#insert_table}.
|
|
94
|
+
class TablePlaceholder < SlidePlaceholder
|
|
95
|
+
include FillablePlaceholder
|
|
96
|
+
|
|
97
|
+
# PowerPoint's default row height, which sets the new table's height.
|
|
98
|
+
ROW_HEIGHT = 370_840
|
|
99
|
+
|
|
100
|
+
# Fill this placeholder with an empty table at its position and width.
|
|
101
|
+
#
|
|
102
|
+
# @return [PlaceholderGraphicFrame] whose #table is the new table
|
|
103
|
+
def insert_table(rows, cols)
|
|
104
|
+
frame = Oxml::CT_GraphicalObjectFrame.new_table_graphic_frame(
|
|
105
|
+
shape_id, name, rows, cols, left, top, width, Length.emu(rows * ROW_HEIGHT)
|
|
106
|
+
)
|
|
107
|
+
replace_placeholder_with(frame)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A chart placeholder: fill it with {#insert_chart}.
|
|
112
|
+
class ChartPlaceholder < SlidePlaceholder
|
|
113
|
+
include FillablePlaceholder
|
|
114
|
+
|
|
115
|
+
# Fill this placeholder with a chart occupying exactly its area.
|
|
116
|
+
#
|
|
117
|
+
# @return [PlaceholderGraphicFrame] whose #chart is the new chart
|
|
118
|
+
def insert_chart(chart_type, chart_data)
|
|
119
|
+
r_id = part.add_chart_part(chart_type, chart_data)
|
|
120
|
+
frame = Oxml::CT_GraphicalObjectFrame.new_chart_graphic_frame(
|
|
121
|
+
shape_id, name, r_id, left, top, width, height
|
|
122
|
+
)
|
|
123
|
+
replace_placeholder_with(frame)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# A picture that fills a placeholder. It is a {Picture} in every respect
|
|
128
|
+
# except that it inherits its geometry from the layout.
|
|
129
|
+
class PlaceholderPicture < Picture
|
|
130
|
+
include InheritsDimensions
|
|
131
|
+
|
|
132
|
+
def shape_type = Enum::MSO_SHAPE_TYPE::PLACEHOLDER
|
|
133
|
+
|
|
134
|
+
def base_placeholder = part.slide_layout.placeholders.by_idx(element.ph_idx)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# A table or chart that fills a placeholder.
|
|
138
|
+
class PlaceholderGraphicFrame < GraphicFrame
|
|
139
|
+
def placeholder? = true
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# A placeholder on a slide layout.
|
|
143
|
+
class LayoutPlaceholder < Shape
|
|
144
|
+
include InheritsDimensions
|
|
145
|
+
|
|
146
|
+
# A layout placeholder inherits from the master placeholder of the
|
|
147
|
+
# corresponding type rather than the same one: a chart, table or picture
|
|
148
|
+
# placeholder all take their geometry from the master's body.
|
|
149
|
+
BASE_TYPES = {
|
|
150
|
+
BODY: :BODY, CHART: :BODY, BITMAP: :BODY, ORG_CHART: :BODY, MEDIA_CLIP: :BODY,
|
|
151
|
+
OBJECT: :BODY, PICTURE: :BODY, SUBTITLE: :BODY, TABLE: :BODY,
|
|
152
|
+
CENTER_TITLE: :TITLE, TITLE: :TITLE,
|
|
153
|
+
DATE: :DATE, FOOTER: :FOOTER, SLIDE_NUMBER: :SLIDE_NUMBER
|
|
154
|
+
}.freeze
|
|
155
|
+
|
|
156
|
+
def shape_type = Enum::MSO_SHAPE_TYPE::PLACEHOLDER
|
|
157
|
+
|
|
158
|
+
def base_placeholder
|
|
159
|
+
base_type = BASE_TYPES[element.ph_type&.name]
|
|
160
|
+
return nil if base_type.nil?
|
|
161
|
+
|
|
162
|
+
part.slide_master.placeholders.by_type(Enum::PP_PLACEHOLDER_TYPE.fetch(base_type))
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# A placeholder on a notes page.
|
|
167
|
+
#
|
|
168
|
+
# Inherits its geometry from the notes-master placeholder of the same type.
|
|
169
|
+
class NotesSlidePlaceholder < Shape
|
|
170
|
+
include InheritsDimensions
|
|
171
|
+
|
|
172
|
+
def shape_type = Enum::MSO_SHAPE_TYPE::PLACEHOLDER
|
|
173
|
+
|
|
174
|
+
def base_placeholder = part.notes_master.placeholders.by_type(element.ph_type)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# A placeholder on a slide master.
|
|
178
|
+
#
|
|
179
|
+
# There is nothing above a master to inherit from, so its geometry is
|
|
180
|
+
# whatever it carries itself.
|
|
181
|
+
class MasterPlaceholder < Shape
|
|
182
|
+
def shape_type = Enum::MSO_SHAPE_TYPE::PLACEHOLDER
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/shapes/base"
|
|
4
|
+
require "ruby_pptx/text/text"
|
|
5
|
+
require "ruby_pptx/dml/fill"
|
|
6
|
+
require "ruby_pptx/table"
|
|
7
|
+
|
|
8
|
+
module Pptx
|
|
9
|
+
# A `p:sp`: an auto shape, a text box, or a placeholder.
|
|
10
|
+
class Shape < BaseShape
|
|
11
|
+
def text_frame? = true
|
|
12
|
+
|
|
13
|
+
# The text inside this shape, creating an empty text body if it has none.
|
|
14
|
+
def text_frame
|
|
15
|
+
@text_frame ||= TextFrame.new(@element.get_or_add_txBody, self)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Shortcut for `text_frame.text`.
|
|
19
|
+
def text = text_frame.text
|
|
20
|
+
|
|
21
|
+
def text=(value)
|
|
22
|
+
text_frame.text = value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The shape's fill.
|
|
26
|
+
def fill = @fill ||= FillFormat.from_fill_parent(@element.spPr)
|
|
27
|
+
|
|
28
|
+
# The shape's outline.
|
|
29
|
+
def line = @line ||= LineFormat.new(@element.spPr)
|
|
30
|
+
|
|
31
|
+
# The adjustment handles of this shape's geometry; empty for a freeform.
|
|
32
|
+
#
|
|
33
|
+
# @return [Adjustments]
|
|
34
|
+
def adjustments = @adjustments ||= Adjustments.new(@element.spPr.prstGeom)
|
|
35
|
+
|
|
36
|
+
# The preset geometry of an auto shape.
|
|
37
|
+
#
|
|
38
|
+
# @return [Pptx::Enum::MSO_SHAPE, nil] nil unless this is an auto shape
|
|
39
|
+
def auto_shape_type = @element.autoshape? ? @element.prst : nil
|
|
40
|
+
|
|
41
|
+
def shape_type
|
|
42
|
+
return Enum::MSO_SHAPE_TYPE::PLACEHOLDER if placeholder?
|
|
43
|
+
return Enum::MSO_SHAPE_TYPE::TEXT_BOX if @element.textbox?
|
|
44
|
+
return Enum::MSO_SHAPE_TYPE::FREEFORM if @element.custom_geometry?
|
|
45
|
+
return Enum::MSO_SHAPE_TYPE::AUTO_SHAPE if @element.autoshape?
|
|
46
|
+
|
|
47
|
+
Enum::MSO_SHAPE_TYPE::AUTO_SHAPE
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A `p:pic` holding an image.
|
|
52
|
+
class BasePicture < BaseShape
|
|
53
|
+
# Cropping, as the fraction of the image cut from each side: 0.25 is a
|
|
54
|
+
# quarter. Negative values extend the side past the image edge, and values
|
|
55
|
+
# above 1.0 are allowed, as in PowerPoint.
|
|
56
|
+
%i[left top right bottom].each do |side|
|
|
57
|
+
attribute = :"srcRect_#{side.to_s[0]}"
|
|
58
|
+
define_method(:"crop_#{side}") { @element.public_send(attribute) }
|
|
59
|
+
define_method(:"crop_#{side}=") { |value| @element.public_send(:"#{attribute}=", value) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The outline drawn around the picture.
|
|
63
|
+
def line = @line ||= LineFormat.new(@element.spPr)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A `p:pic` showing a still image.
|
|
67
|
+
class Picture < BasePicture
|
|
68
|
+
def shape_type = Enum::MSO_SHAPE_TYPE::PICTURE
|
|
69
|
+
|
|
70
|
+
# The image this picture shows: its bytes, format, size and resolution.
|
|
71
|
+
#
|
|
72
|
+
# @return [Pptx::Image]
|
|
73
|
+
# @raise [Error] when the picture links its image rather than embedding it
|
|
74
|
+
def image
|
|
75
|
+
r_id = @element.blip_rId
|
|
76
|
+
raise Error, "this picture has no embedded image" if r_id.nil?
|
|
77
|
+
|
|
78
|
+
part.related_part(r_id).image
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The auto shape the picture is masked by. A new picture is a rectangle,
|
|
82
|
+
# which crops nothing; an ellipse shows it through an oval.
|
|
83
|
+
#
|
|
84
|
+
# @return [Pptx::Enum::Member, nil] nil for a freeform mask, or no geometry
|
|
85
|
+
def auto_shape_type = @element.spPr.prstGeom&.prst
|
|
86
|
+
|
|
87
|
+
def auto_shape_type=(value)
|
|
88
|
+
member = Enum::MSO_AUTO_SHAPE_TYPE.fetch(value)
|
|
89
|
+
sp_pr = @element.spPr
|
|
90
|
+
geometry = sp_pr.prstGeom
|
|
91
|
+
if geometry.nil?
|
|
92
|
+
sp_pr.remove_custGeom
|
|
93
|
+
geometry = sp_pr.get_or_add_prstGeom
|
|
94
|
+
end
|
|
95
|
+
geometry.prst = member
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A `p:pic` that carries video rather than a still image.
|
|
100
|
+
#
|
|
101
|
+
# Not a {Picture}: the image a movie holds is its poster frame, which is
|
|
102
|
+
# what {#poster_frame} returns, and a movie cannot be masked by a shape.
|
|
103
|
+
class Movie < BasePicture
|
|
104
|
+
def shape_type = Enum::MSO_SHAPE_TYPE::MEDIA
|
|
105
|
+
|
|
106
|
+
def media_type = Enum::PP_MEDIA_TYPE::MOVIE
|
|
107
|
+
|
|
108
|
+
# The still shown before the movie plays, or nil if there is none.
|
|
109
|
+
#
|
|
110
|
+
# @return [Pptx::Image, nil]
|
|
111
|
+
def poster_frame
|
|
112
|
+
r_id = @element.blip_rId
|
|
113
|
+
r_id && part.related_part(r_id).image
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Playback settings. PowerPoint keeps these in the timing tree and
|
|
117
|
+
# python-pptx exposes none of them yet; the object exists so code written
|
|
118
|
+
# against either library finds it.
|
|
119
|
+
def media_format = @media_format ||= MediaFormat.new(@element)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Playback formatting for a movie. Deliberately empty; see {Movie#media_format}.
|
|
123
|
+
class MediaFormat < ElementProxy; end
|
|
124
|
+
|
|
125
|
+
# A `p:cxnSp`: a line, optionally attached to shapes at either end.
|
|
126
|
+
#
|
|
127
|
+
# A connector is stored as a bounding box plus flip flags rather than as two
|
|
128
|
+
# points, so moving an end point may flip the box rather than move it. The
|
|
129
|
+
# accessors here hide that.
|
|
130
|
+
class Connector < BaseShape
|
|
131
|
+
def shape_type = Enum::MSO_SHAPE_TYPE::LINE
|
|
132
|
+
|
|
133
|
+
def line = @line ||= LineFormat.new(@element.spPr)
|
|
134
|
+
|
|
135
|
+
def begin_x = Length.emu(@element.flipH ? left.to_i + width.to_i : left.to_i)
|
|
136
|
+
|
|
137
|
+
def begin_x=(value)
|
|
138
|
+
move_x(Length.coerce(value).emu, begin_point: true)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def begin_y = Length.emu(@element.flipV ? top.to_i + height.to_i : top.to_i)
|
|
142
|
+
|
|
143
|
+
def begin_y=(value)
|
|
144
|
+
move_y(Length.coerce(value).emu, begin_point: true)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def end_x = Length.emu(@element.flipH ? left.to_i : left.to_i + width.to_i)
|
|
148
|
+
|
|
149
|
+
def end_x=(value)
|
|
150
|
+
move_x(Length.coerce(value).emu, begin_point: false)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def end_y = Length.emu(@element.flipV ? top.to_i : top.to_i + height.to_i)
|
|
154
|
+
|
|
155
|
+
def end_y=(value)
|
|
156
|
+
move_y(Length.coerce(value).emu, begin_point: false)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Attach the start of this connector to +shape+ at one of its connection
|
|
160
|
+
# points, and move the start there.
|
|
161
|
+
#
|
|
162
|
+
# Connection points are numbered from 0, conventionally starting at the top
|
|
163
|
+
# centre and going counter-clockwise. That is a convention rather than a
|
|
164
|
+
# rule, so the result is only predictable for rectangular shapes.
|
|
165
|
+
def begin_connect(shape, connection_point)
|
|
166
|
+
connection = @element.nvCxnSpPr.cNvCxnSpPr.get_or_add_stCxn
|
|
167
|
+
connection.id = shape.shape_id
|
|
168
|
+
connection.idx = connection_point
|
|
169
|
+
x, y = connection_point_of(shape, connection_point)
|
|
170
|
+
self.begin_x = x
|
|
171
|
+
self.begin_y = y
|
|
172
|
+
self
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# As {#begin_connect}, for the far end.
|
|
176
|
+
def end_connect(shape, connection_point)
|
|
177
|
+
connection = @element.nvCxnSpPr.cNvCxnSpPr.get_or_add_endCxn
|
|
178
|
+
connection.id = shape.shape_id
|
|
179
|
+
connection.idx = connection_point
|
|
180
|
+
x, y = connection_point_of(shape, connection_point)
|
|
181
|
+
self.end_x = x
|
|
182
|
+
self.end_y = y
|
|
183
|
+
self
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def begin_connected? = !@element.nvCxnSpPr.cNvCxnSpPr.stCxn.nil?
|
|
187
|
+
|
|
188
|
+
def end_connected? = !@element.nvCxnSpPr.cNvCxnSpPr.endCxn.nil?
|
|
189
|
+
|
|
190
|
+
def inspect
|
|
191
|
+
"#<Pptx::Connector (#{begin_x.inches.round(2)}, #{begin_y.inches.round(2)}) -> " \
|
|
192
|
+
"(#{end_x.inches.round(2)}, #{end_y.inches.round(2)})>"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
# The four points PowerPoint puts on a rectangle: top, left, bottom, right.
|
|
198
|
+
def connection_point_of(shape, index)
|
|
199
|
+
x = shape.left.to_i
|
|
200
|
+
y = shape.top.to_i
|
|
201
|
+
cx = shape.width.to_i
|
|
202
|
+
cy = shape.height.to_i
|
|
203
|
+
case index
|
|
204
|
+
when 0 then [x + (cx / 2), y]
|
|
205
|
+
when 1 then [x, y + (cy / 2)]
|
|
206
|
+
when 2 then [x + (cx / 2), y + cy]
|
|
207
|
+
when 3 then [x + cx, y + (cy / 2)]
|
|
208
|
+
else
|
|
209
|
+
raise ArgumentError, "connection point #{index} is out of range (0-3)"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Move one end along an axis, keeping the other end where it is.
|
|
214
|
+
#
|
|
215
|
+
# The box has no notion of direction, so pulling an end past the other one
|
|
216
|
+
# flips the box rather than giving it a negative size.
|
|
217
|
+
def move_x(new_value, begin_point:)
|
|
218
|
+
fixed = begin_point ? end_x.emu : begin_x.emu
|
|
219
|
+
@element.x = [new_value, fixed].min
|
|
220
|
+
@element.cx = (new_value - fixed).abs
|
|
221
|
+
@element.flipH = begin_point ? new_value > fixed : new_value < fixed
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def move_y(new_value, begin_point:)
|
|
225
|
+
fixed = begin_point ? end_y.emu : begin_y.emu
|
|
226
|
+
@element.y = [new_value, fixed].min
|
|
227
|
+
@element.cy = (new_value - fixed).abs
|
|
228
|
+
@element.flipV = begin_point ? new_value > fixed : new_value < fixed
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# A `p:graphicFrame`, which holds a table, a chart or an embedded object.
|
|
233
|
+
class GraphicFrame < BaseShape
|
|
234
|
+
# A graphic frame's shadow belongs to what it holds -- a chart and a
|
|
235
|
+
# table keep theirs in different places -- so there is no single one to
|
|
236
|
+
# hand back. python-pptx declines the same way.
|
|
237
|
+
def shadow
|
|
238
|
+
raise Error, "a graphic frame has no shadow of its own; format the chart or table instead"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def table? = @element.table?
|
|
242
|
+
def chart? = @element.chart?
|
|
243
|
+
|
|
244
|
+
# The table inside this frame.
|
|
245
|
+
#
|
|
246
|
+
# @raise [Error] when the frame holds something other than a table
|
|
247
|
+
def table
|
|
248
|
+
raise Error, "this graphic frame does not contain a table" unless table?
|
|
249
|
+
|
|
250
|
+
@table ||= Table.new(@element.tbl, self)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# The chart inside this frame.
|
|
254
|
+
#
|
|
255
|
+
# @raise [Error] when the frame holds something other than a chart
|
|
256
|
+
def chart
|
|
257
|
+
raise Error, "this graphic frame does not contain a chart" unless chart?
|
|
258
|
+
|
|
259
|
+
@chart ||= part.related_part(@element.chart_rId).chart
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# An OLE object is linked when it points at a file outside the
|
|
263
|
+
# presentation rather than carrying one. nil for anything else a frame
|
|
264
|
+
# can hold, such as a diagram, as in python-pptx.
|
|
265
|
+
def shape_type
|
|
266
|
+
return Enum::MSO_SHAPE_TYPE::TABLE if table?
|
|
267
|
+
return Enum::MSO_SHAPE_TYPE::CHART if chart?
|
|
268
|
+
return nil unless ole_object?
|
|
269
|
+
|
|
270
|
+
if @element.embedded_ole_object?
|
|
271
|
+
Enum::MSO_SHAPE_TYPE::EMBEDDED_OLE_OBJECT
|
|
272
|
+
else
|
|
273
|
+
Enum::MSO_SHAPE_TYPE::LINKED_OLE_OBJECT
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def ole_object? = @element.ole_object?
|
|
278
|
+
|
|
279
|
+
# The OLE object this frame holds.
|
|
280
|
+
#
|
|
281
|
+
# @raise [Error] when the frame holds something else
|
|
282
|
+
def ole_format
|
|
283
|
+
raise Error, "this graphic frame does not contain an OLE object" unless ole_object?
|
|
284
|
+
|
|
285
|
+
@ole_format ||= OleFormat.new(@element.oleObj, self)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# An embedded or linked OLE object: the file, and how it is shown.
|
|
290
|
+
class OleFormat < ElementProxy
|
|
291
|
+
def initialize(ole_obj, frame)
|
|
292
|
+
super(ole_obj)
|
|
293
|
+
@frame = frame
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# The embedded file's bytes, or nil for a linked object.
|
|
297
|
+
def blob
|
|
298
|
+
r_id = @element.rId
|
|
299
|
+
r_id && @frame.part.related_part(r_id).blob
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# The ProgID naming the program that opens the object, e.g. "Excel.Sheet.12".
|
|
303
|
+
def prog_id = @element.progId
|
|
304
|
+
|
|
305
|
+
# Whether it appears as an icon rather than as a picture of its content.
|
|
306
|
+
def show_as_icon? = @element.showAsIcon
|
|
307
|
+
|
|
308
|
+
def inspect = "#<Pptx::OleFormat #{prog_id.inspect}>"
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# A `p:grpSp` containing other shapes.
|
|
312
|
+
#
|
|
313
|
+
# A group has no position or size of its own: both follow from what it
|
|
314
|
+
# contains, and are recomputed whenever its contents change.
|
|
315
|
+
class GroupShape < BaseShape
|
|
316
|
+
# A group keeps its effects in `p:grpSpPr` rather than `p:spPr`.
|
|
317
|
+
def shadow = @shadow ||= ShadowFormat.new(@element.grpSpPr)
|
|
318
|
+
|
|
319
|
+
def shape_type = Enum::MSO_SHAPE_TYPE::GROUP
|
|
320
|
+
|
|
321
|
+
# The shapes inside this group.
|
|
322
|
+
def shapes = @shapes ||= GroupShapes.new(@element, self)
|
|
323
|
+
|
|
324
|
+
# A group holds no text of its own, though the shapes in it may.
|
|
325
|
+
def text_frame? = false
|
|
326
|
+
|
|
327
|
+
# PowerPoint offers no click behaviour on a group; the shapes inside carry
|
|
328
|
+
# their own.
|
|
329
|
+
def click_action
|
|
330
|
+
raise Error, "a group shape cannot have a click action"
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def hyperlink
|
|
334
|
+
raise Error, "a group shape cannot have a hyperlink"
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|