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,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/opc/package"
|
|
4
|
+
require "ruby_pptx/opc/constants"
|
|
5
|
+
require "ruby_pptx/enum/prog_id"
|
|
6
|
+
require "ruby_pptx/parts/chart"
|
|
7
|
+
|
|
8
|
+
module Pptx
|
|
9
|
+
module Parts
|
|
10
|
+
# A file embedded in the presentation as an OLE object.
|
|
11
|
+
#
|
|
12
|
+
# An Office document -- one of Enum::PROG_ID -- is stored as the document
|
|
13
|
+
# itself, under a name and content type saying what it is. Anything else
|
|
14
|
+
# is stored as an opaque `oleObjectN.bin`.
|
|
15
|
+
module EmbeddedPackagePart
|
|
16
|
+
OFFICE_PARTS = {
|
|
17
|
+
DOCX: ["/ppt/embeddings/Microsoft_Word_Document%d.docx", Opc::CONTENT_TYPE::WML_DOCUMENT],
|
|
18
|
+
PPTX: ["/ppt/embeddings/Microsoft_PowerPoint_Presentation%d.pptx",
|
|
19
|
+
Opc::CONTENT_TYPE::PML_PRESENTATION],
|
|
20
|
+
XLSX: [EmbeddedXlsxPart::PARTNAME_TEMPLATE, Opc::CONTENT_TYPE::SML_SHEET]
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
GENERIC_PARTNAME = "/ppt/embeddings/oleObject%d.bin"
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# @param prog_id [Enum::PROG_ID::Member, String]
|
|
28
|
+
# @return [Opc::Part]
|
|
29
|
+
def new_part(prog_id, blob, package)
|
|
30
|
+
unless prog_id.is_a?(Enum::PROG_ID::Member)
|
|
31
|
+
return Opc::Part.new(package.next_partname(GENERIC_PARTNAME),
|
|
32
|
+
Opc::CONTENT_TYPE::OFC_OLE_OBJECT, package, blob)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# An embedded workbook is the same kind of part a chart keeps its data in.
|
|
36
|
+
return EmbeddedXlsxPart.new_xlsx(blob, package) if prog_id.name == :XLSX
|
|
37
|
+
|
|
38
|
+
template, content_type = OFFICE_PARTS.fetch(prog_id.name)
|
|
39
|
+
Opc::Part.new(package.next_partname(template), content_type, package, blob)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/opc/package"
|
|
4
|
+
require "ruby_pptx/image"
|
|
5
|
+
|
|
6
|
+
module Pptx
|
|
7
|
+
module Parts
|
|
8
|
+
# An image part, `/ppt/media/imageN.<ext>`.
|
|
9
|
+
class ImagePart < Opc::Part
|
|
10
|
+
attr_reader :source_filename
|
|
11
|
+
|
|
12
|
+
# A new image part holding +image+.
|
|
13
|
+
def self.new_image(package, image)
|
|
14
|
+
new(package.next_image_partname(image.ext), image.content_type, package,
|
|
15
|
+
image.blob, image.filename)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(partname, content_type, package, blob = nil, source_filename = nil)
|
|
19
|
+
super(partname, content_type, package, blob)
|
|
20
|
+
@source_filename = source_filename
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def image = @image ||= Image.from_blob(blob, @source_filename)
|
|
24
|
+
|
|
25
|
+
def ext = partname.ext
|
|
26
|
+
|
|
27
|
+
# The name PowerPoint shows in the alt-text/description field: the file
|
|
28
|
+
# the image came from, or a generic name when it came from a stream.
|
|
29
|
+
def desc = @source_filename || "image.#{ext}"
|
|
30
|
+
|
|
31
|
+
def sha1 = image.sha1
|
|
32
|
+
|
|
33
|
+
# Resolve a requested size against the image's native size.
|
|
34
|
+
#
|
|
35
|
+
# Supplying neither dimension uses the native size; supplying one scales
|
|
36
|
+
# the other to preserve the aspect ratio; supplying both stretches the
|
|
37
|
+
# image to fit.
|
|
38
|
+
#
|
|
39
|
+
# @return [Array(Pptx::Length, Pptx::Length)]
|
|
40
|
+
def scale(width, height)
|
|
41
|
+
native_width, native_height = image.native_size
|
|
42
|
+
return [Length.coerce(width), Length.coerce(height)] if width && height
|
|
43
|
+
return [native_width, native_height] unless width || height
|
|
44
|
+
|
|
45
|
+
if width
|
|
46
|
+
factor = Length.coerce(width).emu.to_f / native_width.emu
|
|
47
|
+
[Length.coerce(width), Length.emu((native_height.emu * factor).round)]
|
|
48
|
+
else
|
|
49
|
+
factor = Length.coerce(height).emu.to_f / native_height.emu
|
|
50
|
+
[Length.emu((native_width.emu * factor).round), Length.coerce(height)]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/opc/package"
|
|
4
|
+
require "ruby_pptx/media"
|
|
5
|
+
|
|
6
|
+
module Pptx
|
|
7
|
+
module Parts
|
|
8
|
+
# A media part, `/ppt/media/mediaN.<ext>`: the audio or video itself.
|
|
9
|
+
class MediaPart < Opc::Part
|
|
10
|
+
def self.new_media(package, video)
|
|
11
|
+
new(package.next_media_partname(video.ext), video.content_type, package, video.blob)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def sha1 = @sha1 ||= Digest::SHA1.hexdigest(blob)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/opc/package"
|
|
4
|
+
require "ruby_pptx/opc/constants"
|
|
5
|
+
require "ruby_pptx/oxml/presentation"
|
|
6
|
+
require "ruby_pptx/parts/slide"
|
|
7
|
+
require "ruby_pptx/presentation"
|
|
8
|
+
|
|
9
|
+
module Pptx
|
|
10
|
+
module Parts
|
|
11
|
+
# The presentation part, `/ppt/presentation.xml`; the package's main
|
|
12
|
+
# document part.
|
|
13
|
+
class PresentationPart < Opc::XmlPart
|
|
14
|
+
def presentation = @presentation ||= Presentation.new(element, self)
|
|
15
|
+
|
|
16
|
+
# Create a blank slide part inheriting from +slide_layout+.
|
|
17
|
+
#
|
|
18
|
+
# @return [Array(String, Pptx::Slide)] the new relationship id and slide
|
|
19
|
+
def add_slide(slide_layout)
|
|
20
|
+
slide_part = SlidePart.new_slide(next_slide_partname, package, slide_layout.part)
|
|
21
|
+
r_id = relate_to(slide_part, Opc::RELATIONSHIP_TYPE::SLIDE)
|
|
22
|
+
[r_id, slide_part.slide]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def core_properties = package.core_properties
|
|
26
|
+
|
|
27
|
+
# The presentation's notes master, created from the default template on
|
|
28
|
+
# first use.
|
|
29
|
+
#
|
|
30
|
+
# Like python-pptx, this relates the new part to the presentation but
|
|
31
|
+
# does not add a `p:notesMasterIdLst` entry for it.
|
|
32
|
+
def notes_master_part
|
|
33
|
+
existing = rels.find { |r| r.reltype == Opc::RELATIONSHIP_TYPE::NOTES_MASTER }
|
|
34
|
+
return existing.target_part if existing
|
|
35
|
+
|
|
36
|
+
part = NotesMasterPart.create_default(package)
|
|
37
|
+
relate_to(part, Opc::RELATIONSHIP_TYPE::NOTES_MASTER)
|
|
38
|
+
part
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def related_slide(r_id) = related_part(r_id).slide
|
|
42
|
+
|
|
43
|
+
def related_slide_master(r_id) = related_part(r_id).slide_master
|
|
44
|
+
|
|
45
|
+
# The slide with presentation-wide id +slide_id+.
|
|
46
|
+
#
|
|
47
|
+
# @return [Slide, nil]
|
|
48
|
+
def slide_by_id(slide_id)
|
|
49
|
+
entry = element.sldIdLst&.sldId_list&.find { |s| s.id == slide_id }
|
|
50
|
+
entry && related_part(entry.rId).slide
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The slide id assigned to +slide_part+.
|
|
54
|
+
def slide_id_for(slide_part)
|
|
55
|
+
entry = element.sldIdLst&.sldId_list&.find { |s| related_part(s.rId).equal?(slide_part) }
|
|
56
|
+
entry&.id || raise(NotFoundError, "slide part is not in this presentation")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Renumber the slide parts so their partnames form a gapless sequence in
|
|
60
|
+
# the order the slides appear.
|
|
61
|
+
#
|
|
62
|
+
# Slide order lives in `p:sldIdLst`, not in the partnames, so the two can
|
|
63
|
+
# drift apart after slides are added or deleted. PowerPoint tolerates
|
|
64
|
+
# that, but keeping them aligned makes saved packages predictable.
|
|
65
|
+
def renumber_slide_parts(r_ids)
|
|
66
|
+
r_ids.each_with_index do |r_id, index|
|
|
67
|
+
related_part(r_id).partname = Opc::PackURI.new("/ppt/slides/slide#{index + 1}.xml")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def next_slide_partname
|
|
72
|
+
count = element.get_or_add_sldIdLst.size
|
|
73
|
+
Opc::PackURI.new("/ppt/slides/slide#{count + 1}.xml")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def save(path_or_stream) = package.save(path_or_stream)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/opc/package"
|
|
4
|
+
require "ruby_pptx/opc/constants"
|
|
5
|
+
require "ruby_pptx/oxml/slide"
|
|
6
|
+
require "ruby_pptx/slide"
|
|
7
|
+
require "ruby_pptx/parts/chart"
|
|
8
|
+
|
|
9
|
+
module Pptx
|
|
10
|
+
module Parts
|
|
11
|
+
# Common to every slide-like part: slides, layouts, masters, notes slides
|
|
12
|
+
# and the notes master.
|
|
13
|
+
class BaseSlidePart < Opc::XmlPart
|
|
14
|
+
# The internal name of this slide.
|
|
15
|
+
def name = element.cSld.name
|
|
16
|
+
|
|
17
|
+
# Create a chart part for +chart_data+ and relate this slide to it.
|
|
18
|
+
#
|
|
19
|
+
# @return [String] the relationship id of the new chart part
|
|
20
|
+
def add_chart_part(chart_type, chart_data)
|
|
21
|
+
chart_part = ChartPart.new_chart(chart_type, chart_data, package)
|
|
22
|
+
relate_to(chart_part, Opc::RELATIONSHIP_TYPE::CHART)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Create a combo chart part and relate this slide to it.
|
|
26
|
+
#
|
|
27
|
+
# @return [String] the relationship id of the new chart part
|
|
28
|
+
def add_combo_chart_part(builder)
|
|
29
|
+
chart_part = ChartPart.new_combo_chart(builder, package)
|
|
30
|
+
relate_to(chart_part, Opc::RELATIONSHIP_TYPE::CHART)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Relate this slide to the media part holding +video+.
|
|
34
|
+
#
|
|
35
|
+
# Two relationships are made to the same part, one MEDIA and one VIDEO.
|
|
36
|
+
# PowerPoint has embedded media two different ways over the years and
|
|
37
|
+
# writes both so either era can find it.
|
|
38
|
+
#
|
|
39
|
+
# @return [Array(String, String)] the media rId and the video rId
|
|
40
|
+
def get_or_add_video_media_part(video)
|
|
41
|
+
media_part = package.get_or_add_media_part(video)
|
|
42
|
+
[relate_to(media_part, Opc::RELATIONSHIP_TYPE::MEDIA),
|
|
43
|
+
relate_to(media_part, Opc::RELATIONSHIP_TYPE::VIDEO)]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The image part for +image_file+, related to this slide.
|
|
47
|
+
#
|
|
48
|
+
# Both the part and the relationship are reused when they already exist,
|
|
49
|
+
# so the same image added twice is stored once.
|
|
50
|
+
#
|
|
51
|
+
# @return [Array(Pptx::Parts::ImagePart, String)] the part and its rId
|
|
52
|
+
# Embed +object_file+ as an OLE object and relate this slide to it.
|
|
53
|
+
#
|
|
54
|
+
# An Office document is related as a package, anything else as an OLE
|
|
55
|
+
# object -- the two relationship types PowerPoint uses.
|
|
56
|
+
#
|
|
57
|
+
# @return [String] the relationship id
|
|
58
|
+
def add_embedded_ole_object_part(prog_id, object_file)
|
|
59
|
+
blob = object_file.respond_to?(:read) ? object_file.read : File.binread(object_file)
|
|
60
|
+
part = EmbeddedPackagePart.new_part(prog_id, blob, package)
|
|
61
|
+
reltype = if prog_id.is_a?(Enum::PROG_ID::Member)
|
|
62
|
+
Opc::RELATIONSHIP_TYPE::PACKAGE
|
|
63
|
+
else
|
|
64
|
+
Opc::RELATIONSHIP_TYPE::OLE_OBJECT
|
|
65
|
+
end
|
|
66
|
+
relate_to(part, reltype)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def get_or_add_image_part(image_file)
|
|
70
|
+
image_part = package.get_or_add_image_part(image_file)
|
|
71
|
+
[image_part, relate_to(image_part, Opc::RELATIONSHIP_TYPE::IMAGE)]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# A slide part, `/ppt/slides/slideN.xml`.
|
|
76
|
+
class SlidePart < BaseSlidePart
|
|
77
|
+
# A new, empty slide part inheriting from +slide_layout_part+.
|
|
78
|
+
def self.new_slide(partname, package, slide_layout_part)
|
|
79
|
+
part = new(partname, Opc::CONTENT_TYPE::PML_SLIDE, package, Oxml::CT_Slide.new_element)
|
|
80
|
+
part.relate_to(slide_layout_part, Opc::RELATIONSHIP_TYPE::SLIDE_LAYOUT)
|
|
81
|
+
part
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def slide = @slide ||= Slide.new(element, self)
|
|
85
|
+
|
|
86
|
+
# The layout part this slide inherits from.
|
|
87
|
+
def slide_layout
|
|
88
|
+
part_related_by(Opc::RELATIONSHIP_TYPE::SLIDE_LAYOUT).slide_layout
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The presentation-wide id of this slide.
|
|
92
|
+
def slide_id = package.presentation_part.slide_id_for(self)
|
|
93
|
+
|
|
94
|
+
def notes_slide? = rels.any? { |r| r.reltype == Opc::RELATIONSHIP_TYPE::NOTES_SLIDE }
|
|
95
|
+
|
|
96
|
+
# This slide's notes slide, created on first use.
|
|
97
|
+
def notes_slide
|
|
98
|
+
existing = rels.find { |r| r.reltype == Opc::RELATIONSHIP_TYPE::NOTES_SLIDE }
|
|
99
|
+
return existing.target_part.notes_slide if existing
|
|
100
|
+
|
|
101
|
+
notes_part = NotesSlidePart.new_notes_slide(package, self)
|
|
102
|
+
relate_to(notes_part, Opc::RELATIONSHIP_TYPE::NOTES_SLIDE)
|
|
103
|
+
notes_part.notes_slide
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# A slide-layout part, `/ppt/slideLayouts/slideLayoutN.xml`.
|
|
108
|
+
class SlideLayoutPart < BaseSlidePart
|
|
109
|
+
PARTNAME_TEMPLATE = "/ppt/slideLayouts/slideLayout%d.xml"
|
|
110
|
+
|
|
111
|
+
# A new layout named +name+, inheriting from +slide_master_part+.
|
|
112
|
+
def self.new_layout(package, slide_master_part, name, layout_type)
|
|
113
|
+
part = new(package.next_partname(PARTNAME_TEMPLATE),
|
|
114
|
+
Opc::CONTENT_TYPE::PML_SLIDE_LAYOUT, package,
|
|
115
|
+
Oxml::CT_SlideLayout.new_element(name, layout_type))
|
|
116
|
+
part.relate_to(slide_master_part, Opc::RELATIONSHIP_TYPE::SLIDE_MASTER)
|
|
117
|
+
part
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def slide_layout = @slide_layout ||= SlideLayout.new(element, self)
|
|
121
|
+
|
|
122
|
+
def slide_master
|
|
123
|
+
part_related_by(Opc::RELATIONSHIP_TYPE::SLIDE_MASTER).slide_master
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# A slide-master part, `/ppt/slideMasters/slideMasterN.xml`.
|
|
128
|
+
class SlideMasterPart < BaseSlidePart
|
|
129
|
+
PARTNAME_TEMPLATE = "/ppt/slideMasters/slideMaster%d.xml"
|
|
130
|
+
THEME_PARTNAME_TEMPLATE = "/ppt/theme/theme%d.xml"
|
|
131
|
+
|
|
132
|
+
# A new master with its own theme part and no layouts yet.
|
|
133
|
+
#
|
|
134
|
+
# The theme is a separate part related from the master, which is where
|
|
135
|
+
# the master's colours and fonts actually live.
|
|
136
|
+
def self.new_master(package, name: "Office Theme")
|
|
137
|
+
part = new(package.next_partname(PARTNAME_TEMPLATE),
|
|
138
|
+
Opc::CONTENT_TYPE::PML_SLIDE_MASTER, package,
|
|
139
|
+
Oxml::CT_SlideMaster.new_element)
|
|
140
|
+
theme = ThemePart.new_theme(package.next_partname(THEME_PARTNAME_TEMPLATE), package,
|
|
141
|
+
name: name)
|
|
142
|
+
part.relate_to(theme, Opc::RELATIONSHIP_TYPE::THEME)
|
|
143
|
+
part
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def slide_master = @slide_master ||= SlideMaster.new(element, self)
|
|
147
|
+
|
|
148
|
+
# The theme this master draws its colours and fonts from.
|
|
149
|
+
def theme_part = part_related_by(Opc::RELATIONSHIP_TYPE::THEME)
|
|
150
|
+
|
|
151
|
+
# Add +slide_layout_part+ to this master's layout list.
|
|
152
|
+
#
|
|
153
|
+
# @return [String] the relationship id
|
|
154
|
+
def add_slide_layout(slide_layout_part)
|
|
155
|
+
r_id = relate_to(slide_layout_part, Opc::RELATIONSHIP_TYPE::SLIDE_LAYOUT)
|
|
156
|
+
entry = element.get_or_add_sldLayoutIdLst.add_sldLayoutId
|
|
157
|
+
entry.rId = r_id
|
|
158
|
+
entry.id = next_slide_layout_id
|
|
159
|
+
r_id
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def related_slide_layout(r_id) = related_part(r_id).slide_layout
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
# Layout ids are numbered from 2147483649 upwards, continuing from the
|
|
167
|
+
# highest already in use so a removed layout does not free its id for
|
|
168
|
+
# reuse -- PowerPoint keeps references to them elsewhere in the package.
|
|
169
|
+
def next_slide_layout_id
|
|
170
|
+
used = element.get_or_add_sldLayoutIdLst.sldLayoutId_list.filter_map(&:id)
|
|
171
|
+
used.empty? ? 2_147_483_649 : used.max + 1
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# A notes-master part.
|
|
176
|
+
class NotesMasterPart < BaseSlidePart
|
|
177
|
+
PARTNAME = "/ppt/notesMasters/notesMaster1.xml"
|
|
178
|
+
|
|
179
|
+
# A default notes master with the theme it needs, built from the
|
|
180
|
+
# templates shipped with the gem.
|
|
181
|
+
#
|
|
182
|
+
# The theme is the base theme as-is. It is not routed through
|
|
183
|
+
# ThemePart.new_theme, which renames the colour and font schemes along
|
|
184
|
+
# with the theme and so would not match what python-pptx writes.
|
|
185
|
+
def self.create_default(package)
|
|
186
|
+
part = new(Opc::PackURI.new(PARTNAME), Opc::CONTENT_TYPE::PML_NOTES_MASTER, package,
|
|
187
|
+
Oxml::CT_NotesMaster.new_default)
|
|
188
|
+
theme = ThemePart.new(package.next_partname(SlideMasterPart::THEME_PARTNAME_TEMPLATE),
|
|
189
|
+
Opc::CONTENT_TYPE::OFC_THEME, package,
|
|
190
|
+
Oxml::CT_OfficeStyleSheet.new_default)
|
|
191
|
+
part.relate_to(theme, Opc::RELATIONSHIP_TYPE::THEME)
|
|
192
|
+
part
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def notes_master = @notes_master ||= NotesMaster.new(element, self)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# A notes-slide part, `/ppt/notesSlides/notesSlideN.xml`.
|
|
199
|
+
class NotesSlidePart < BaseSlidePart
|
|
200
|
+
PARTNAME_TEMPLATE = "/ppt/notesSlides/notesSlide%d.xml"
|
|
201
|
+
|
|
202
|
+
# A new notes slide for +slide_part+, related to the notes master and
|
|
203
|
+
# carrying clones of the master's slide-image, body and slide-number
|
|
204
|
+
# placeholders. The notes master is created first if the presentation
|
|
205
|
+
# has none.
|
|
206
|
+
def self.new_notes_slide(package, slide_part)
|
|
207
|
+
notes_master_part = package.presentation_part.notes_master_part
|
|
208
|
+
part = new(package.next_partname(PARTNAME_TEMPLATE), Opc::CONTENT_TYPE::PML_NOTES_SLIDE,
|
|
209
|
+
package, Oxml::CT_NotesSlide.new_element)
|
|
210
|
+
part.relate_to(notes_master_part, Opc::RELATIONSHIP_TYPE::NOTES_MASTER)
|
|
211
|
+
part.relate_to(slide_part, Opc::RELATIONSHIP_TYPE::SLIDE)
|
|
212
|
+
part.notes_slide.clone_master_placeholders(notes_master_part.notes_master)
|
|
213
|
+
part
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def notes_slide = @notes_slide ||= NotesSlide.new(element, self)
|
|
217
|
+
|
|
218
|
+
def notes_master = part_related_by(Opc::RELATIONSHIP_TYPE::NOTES_MASTER).notes_master
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/opc/package"
|
|
4
|
+
require "ruby_pptx/opc/constants"
|
|
5
|
+
require "ruby_pptx/oxml/theme"
|
|
6
|
+
require "ruby_pptx/theme"
|
|
7
|
+
|
|
8
|
+
module Pptx
|
|
9
|
+
module Parts
|
|
10
|
+
# A theme part, `/ppt/theme/themeN.xml`.
|
|
11
|
+
#
|
|
12
|
+
# Every slide master relates to exactly one, and it is where the master's
|
|
13
|
+
# colours and fonts actually live -- the master itself only maps them.
|
|
14
|
+
class ThemePart < Opc::XmlPart
|
|
15
|
+
# A new theme part starting from the base theme shipped with the gem.
|
|
16
|
+
def self.new_theme(partname, package, name: "Office Theme")
|
|
17
|
+
part = new(partname, Opc::CONTENT_TYPE::OFC_THEME, package,
|
|
18
|
+
Oxml::CT_OfficeStyleSheet.new_default)
|
|
19
|
+
part.theme.name = name
|
|
20
|
+
part
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def theme = @theme ||= Theme.new(element, self)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pptx
|
|
4
|
+
# `case`/`in` support for the objects a caller reads a deck with.
|
|
5
|
+
#
|
|
6
|
+
# case shape
|
|
7
|
+
# in {shape_type: :PICTURE, name:} then puts "picture #{name}"
|
|
8
|
+
# in {shape_type: :PLACEHOLDER, placeholder_format: {type: :TITLE}}
|
|
9
|
+
# then puts shape.text_frame.text
|
|
10
|
+
# in {width: Pptx::Length => w} if w > Pptx.inches(5)
|
|
11
|
+
# then puts "wide"
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# **Enum-valued attributes read as their symbolic name in a pattern.**
|
|
15
|
+
# `shape.shape_type` returns a {Pptx::Enum::Member}, because that member
|
|
16
|
+
# carries the MS API value and the XML value and is the honest return type.
|
|
17
|
+
# In a pattern none of that helps -- `in {shape_type: :PICTURE}` is what
|
|
18
|
+
# anyone would write -- so the pattern-matching view reports the symbol.
|
|
19
|
+
#
|
|
20
|
+
# Only the keys a pattern actually asks for are computed, so matching on a
|
|
21
|
+
# shape's name does not walk its text.
|
|
22
|
+
module PatternMatching
|
|
23
|
+
def self.included(base) = base.extend(ClassMethods)
|
|
24
|
+
|
|
25
|
+
# A member reads as its name; anything else is passed through.
|
|
26
|
+
def self.for_pattern(value)
|
|
27
|
+
value.is_a?(Enum::Member) ? value.to_sym : value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module ClassMethods
|
|
31
|
+
# Declare the keys `case`/`in` may match on. Each names a public method.
|
|
32
|
+
def pattern_keys(*names)
|
|
33
|
+
names.freeze
|
|
34
|
+
define_method(:deconstruct_keys) do |keys|
|
|
35
|
+
wanted = keys.nil? ? names : names & keys
|
|
36
|
+
wanted.to_h { |name| [name, PatternMatching.for_pattern(public_send(name))] }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Array patterns for the collections, so `in [first, *rest]` works.
|
|
43
|
+
#
|
|
44
|
+
# Kept separate from {PatternMatching} because deconstructing a collection
|
|
45
|
+
# means enumerating all of it, which is the opposite of the lazy wrapping
|
|
46
|
+
# everything else here does. Use it where the whole list is wanted anyway.
|
|
47
|
+
module DeconstructToArray
|
|
48
|
+
def deconstruct = to_a
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/pattern_matching"
|
|
4
|
+
|
|
5
|
+
require "ruby_pptx/element_proxy"
|
|
6
|
+
require "ruby_pptx/slide"
|
|
7
|
+
require "ruby_pptx/section"
|
|
8
|
+
|
|
9
|
+
module Pptx
|
|
10
|
+
# A PowerPoint presentation.
|
|
11
|
+
#
|
|
12
|
+
# Open one with {Presentation.open}, or start from the built-in template with
|
|
13
|
+
# {Presentation.new}:
|
|
14
|
+
#
|
|
15
|
+
# prs = Pptx::Presentation.open("deck.pptx")
|
|
16
|
+
# prs.slides.each { |slide| puts slide.name }
|
|
17
|
+
# prs.slide_width = Pptx.inches(13.333)
|
|
18
|
+
# prs.save("wide.pptx")
|
|
19
|
+
class Presentation < PartElementProxy
|
|
20
|
+
include PatternMatching
|
|
21
|
+
|
|
22
|
+
pattern_keys :slides, :slide_layouts, :slide_masters, :slide_width, :slide_height
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# Open a presentation from a path or an IO stream.
|
|
26
|
+
#
|
|
27
|
+
# Given a block, yields the presentation and returns the block's value,
|
|
28
|
+
# as `File.open` does.
|
|
29
|
+
#
|
|
30
|
+
# @raise [PackageNotFoundError] when the file is not an OPC package
|
|
31
|
+
# @raise [Error] when the package is not a PowerPoint presentation
|
|
32
|
+
def open(pptx = nil)
|
|
33
|
+
pptx ||= default_template_path
|
|
34
|
+
presentation_part = Package.open(pptx).main_document_part
|
|
35
|
+
unless VALID_CONTENT_TYPES.include?(presentation_part.content_type)
|
|
36
|
+
raise Error,
|
|
37
|
+
"not a PowerPoint file; content type is #{presentation_part.content_type.inspect}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
presentation = presentation_part.presentation
|
|
41
|
+
block_given? ? yield(presentation) : presentation
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A new presentation based on the built-in default template.
|
|
45
|
+
def new_default = Presentation.open(nil)
|
|
46
|
+
|
|
47
|
+
def default_template_path
|
|
48
|
+
File.expand_path("templates/default.pptx", __dir__)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
VALID_CONTENT_TYPES = [
|
|
53
|
+
Opc::CONTENT_TYPE::PML_PRESENTATION_MAIN,
|
|
54
|
+
Opc::CONTENT_TYPE::PML_PRES_MACRO_MAIN
|
|
55
|
+
].freeze
|
|
56
|
+
|
|
57
|
+
# The Dublin Core metadata for this presentation.
|
|
58
|
+
def core_properties = part.core_properties
|
|
59
|
+
|
|
60
|
+
# The slides in this presentation.
|
|
61
|
+
def slides
|
|
62
|
+
@slides ||= begin
|
|
63
|
+
sld_id_list = @element.get_or_add_sldIdLst
|
|
64
|
+
part.renumber_slide_parts(sld_id_list.sldId_list.map(&:rId))
|
|
65
|
+
Slides.new(sld_id_list, self)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The slide masters in this presentation.
|
|
70
|
+
def slide_masters
|
|
71
|
+
@slide_masters ||= SlideMasters.new(@element.get_or_add_sldMasterIdLst, self)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The notes master, created from the default template on first use.
|
|
75
|
+
#
|
|
76
|
+
# @return [NotesMaster]
|
|
77
|
+
def notes_master = part.notes_master_part.notes_master
|
|
78
|
+
|
|
79
|
+
# The first slide master, which is the only one in most presentations.
|
|
80
|
+
def slide_master = slide_masters[0]
|
|
81
|
+
|
|
82
|
+
# The layouts of the first slide master.
|
|
83
|
+
#
|
|
84
|
+
# A presentation may have several masters, each with its own layouts; this
|
|
85
|
+
# is a convenience for the common case of one.
|
|
86
|
+
def slide_layouts = slide_master.slide_layouts
|
|
87
|
+
|
|
88
|
+
# The sections grouping this presentation's slides.
|
|
89
|
+
#
|
|
90
|
+
# Sections are a PowerPoint 2010 extension; a presentation with none has
|
|
91
|
+
# an empty collection and writes no extension element.
|
|
92
|
+
def sections = @sections ||= Sections.new(self)
|
|
93
|
+
|
|
94
|
+
# @return [Length, nil] nil when the presentation defines no slide size
|
|
95
|
+
def slide_width = @element.sldSz&.cx
|
|
96
|
+
|
|
97
|
+
def slide_width=(width)
|
|
98
|
+
@element.get_or_add_sldSz.cx = width
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @return [Length, nil]
|
|
102
|
+
def slide_height = @element.sldSz&.cy
|
|
103
|
+
|
|
104
|
+
def slide_height=(height)
|
|
105
|
+
@element.get_or_add_sldSz.cy = height
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Write this presentation to a path or an IO stream.
|
|
109
|
+
def save(path_or_stream)
|
|
110
|
+
part.save(path_or_stream)
|
|
111
|
+
self
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The .pptx bytes, for callers who want to hand them to something other
|
|
115
|
+
# than the filesystem.
|
|
116
|
+
#
|
|
117
|
+
# @return [String]
|
|
118
|
+
def to_blob
|
|
119
|
+
buffer = StringIO.new(+"", "w+b")
|
|
120
|
+
save(buffer)
|
|
121
|
+
buffer.string
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def inspect
|
|
125
|
+
"#<Pptx::Presentation slides=#{slides.size} #{slide_width&.inches}x#{slide_height&.inches}in>"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/numeric_lengths"
|
|
4
|
+
|
|
5
|
+
module Pptx
|
|
6
|
+
# Numeric length sugar, scoped to the file that asks for it.
|
|
7
|
+
#
|
|
8
|
+
# require "ruby_pptx/refinements"
|
|
9
|
+
# using Pptx::Lengths
|
|
10
|
+
#
|
|
11
|
+
# slide.shapes.add_shape(:rectangle, at: [1.inch, 2.inch], size: [3.inch, 1.inch])
|
|
12
|
+
#
|
|
13
|
+
# A refinement is the polite version of `ruby_pptx/core_ext`: `Numeric` gains
|
|
14
|
+
# these methods inside this file and nowhere else, so nothing else sharing
|
|
15
|
+
# the process can see them.
|
|
16
|
+
module Lengths
|
|
17
|
+
refine Numeric do
|
|
18
|
+
import_methods NumericLengths
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|