pptx 0.0.1

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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +57 -0
  3. data/lib/pptx.rb +24 -0
  4. data/lib/pptx/base.pptx/[Content_Types].xml +30 -0
  5. data/lib/pptx/base.pptx/docProps/app.xml +41 -0
  6. data/lib/pptx/base.pptx/docProps/core.xml +13 -0
  7. data/lib/pptx/base.pptx/docProps/thumbnail.jpeg +0 -0
  8. data/lib/pptx/base.pptx/ppt/_rels/presentation.xml.rels +9 -0
  9. data/lib/pptx/base.pptx/ppt/presProps.xml +11 -0
  10. data/lib/pptx/base.pptx/ppt/presentation.xml +105 -0
  11. data/lib/pptx/base.pptx/ppt/printerSettings/printerSettings1.bin +0 -0
  12. data/lib/pptx/base.pptx/ppt/slideLayouts/_rels/slideLayout1.xml.rels +4 -0
  13. data/lib/pptx/base.pptx/ppt/slideLayouts/_rels/slideLayout10.xml.rels +4 -0
  14. data/lib/pptx/base.pptx/ppt/slideLayouts/_rels/slideLayout11.xml.rels +4 -0
  15. data/lib/pptx/base.pptx/ppt/slideLayouts/_rels/slideLayout2.xml.rels +4 -0
  16. data/lib/pptx/base.pptx/ppt/slideLayouts/_rels/slideLayout3.xml.rels +4 -0
  17. data/lib/pptx/base.pptx/ppt/slideLayouts/_rels/slideLayout4.xml.rels +4 -0
  18. data/lib/pptx/base.pptx/ppt/slideLayouts/_rels/slideLayout5.xml.rels +4 -0
  19. data/lib/pptx/base.pptx/ppt/slideLayouts/_rels/slideLayout6.xml.rels +4 -0
  20. data/lib/pptx/base.pptx/ppt/slideLayouts/_rels/slideLayout7.xml.rels +4 -0
  21. data/lib/pptx/base.pptx/ppt/slideLayouts/_rels/slideLayout8.xml.rels +4 -0
  22. data/lib/pptx/base.pptx/ppt/slideLayouts/_rels/slideLayout9.xml.rels +4 -0
  23. data/lib/pptx/base.pptx/ppt/slideLayouts/slideLayout1.xml +240 -0
  24. data/lib/pptx/base.pptx/ppt/slideLayouts/slideLayout10.xml +168 -0
  25. data/lib/pptx/base.pptx/ppt/slideLayouts/slideLayout11.xml +178 -0
  26. data/lib/pptx/base.pptx/ppt/slideLayouts/slideLayout2.xml +168 -0
  27. data/lib/pptx/base.pptx/ppt/slideLayouts/slideLayout3.xml +244 -0
  28. data/lib/pptx/base.pptx/ppt/slideLayouts/slideLayout4.xml +286 -0
  29. data/lib/pptx/base.pptx/ppt/slideLayouts/slideLayout5.xml +420 -0
  30. data/lib/pptx/base.pptx/ppt/slideLayouts/slideLayout6.xml +116 -0
  31. data/lib/pptx/base.pptx/ppt/slideLayouts/slideLayout7.xml +93 -0
  32. data/lib/pptx/base.pptx/ppt/slideLayouts/slideLayout8.xml +275 -0
  33. data/lib/pptx/base.pptx/ppt/slideLayouts/slideLayout9.xml +251 -0
  34. data/lib/pptx/base.pptx/ppt/slideMasters/_rels/slideMaster1.xml.rels +15 -0
  35. data/lib/pptx/base.pptx/ppt/slideMasters/slideMaster1.xml +508 -0
  36. data/lib/pptx/base.pptx/ppt/tableStyles.xml +2 -0
  37. data/lib/pptx/base.pptx/ppt/theme/theme1.xml +318 -0
  38. data/lib/pptx/base.pptx/ppt/viewProps.xml +32 -0
  39. data/lib/pptx/generate_sample.rb +57 -0
  40. data/lib/pptx/opc.rb +14 -0
  41. data/lib/pptx/opc/base_part.rb +36 -0
  42. data/lib/pptx/opc/binary_part.rb +20 -0
  43. data/lib/pptx/opc/content_types.rb +18 -0
  44. data/lib/pptx/opc/file_part.rb +28 -0
  45. data/lib/pptx/opc/package.rb +115 -0
  46. data/lib/pptx/opc/package_streamer.rb +48 -0
  47. data/lib/pptx/opc/part.rb +27 -0
  48. data/lib/pptx/opc/relationships.rb +32 -0
  49. data/lib/pptx/opc/s3_object_part.rb +25 -0
  50. data/lib/pptx/presentation.rb +33 -0
  51. data/lib/pptx/shapes.rb +10 -0
  52. data/lib/pptx/shapes/filled_rectangle.rb +32 -0
  53. data/lib/pptx/shapes/picture.rb +40 -0
  54. data/lib/pptx/shapes/shape.rb +46 -0
  55. data/lib/pptx/shapes/slide_number.rb +39 -0
  56. data/lib/pptx/shapes/textbox.rb +95 -0
  57. data/lib/pptx/slide.rb +79 -0
  58. data/lib/pptx/version.rb +3 -0
  59. metadata +156 -0
@@ -0,0 +1,20 @@
1
+ module PPTX
2
+ module OPC
3
+ class BinaryPart < BasePart
4
+ def initialize(package, original_name)
5
+ name = "ppt/media/binary-#{SecureRandom.hex(10)}#{File.extname(original_name)}"
6
+ super(package, name)
7
+ @original_name = original_name
8
+ end
9
+
10
+ def doc
11
+ throw "Binary parts can't be parsed as XML"
12
+ end
13
+
14
+ def marshal
15
+ throw 'Implement me!'
16
+ end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,18 @@
1
+ module PPTX
2
+ module OPC
3
+ class ContentTypes < BasePart
4
+ NS = 'http://schemas.openxmlformats.org/package/2006/content-types'
5
+
6
+ def set_override(part_name, value)
7
+ type_list = doc.xpath('c:Types', c: NS).first
8
+ override = type_list.xpath("./xmlns:Override[@PartName='#{part_name}']").first
9
+ unless override
10
+ override = Nokogiri::XML::Node.new('Override', doc)
11
+ override['PartName'] = '/' + part_name
12
+ type_list.add_child(override)
13
+ end
14
+ override['ContentType'] = value
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ module PPTX
2
+ module OPC
3
+ class FilePart < BinaryPart
4
+ def initialize(package, file)
5
+ super(package, File.basename(file))
6
+ @file = file
7
+ @chunk_size = 16 * 1024
8
+ end
9
+
10
+ def marshal
11
+ IO.read(@file)
12
+ end
13
+
14
+ def size
15
+ File.size(@file)
16
+ end
17
+
18
+ def stream(out)
19
+ File.open(@file, 'r') do |file|
20
+ while chunk = file.read(@chunk_size)
21
+ puts "Streaming file: #{chunk && chunk.bytesize} bytes"
22
+ out << chunk
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,115 @@
1
+ require 'nokogiri'
2
+ require 'zip'
3
+
4
+ module PPTX
5
+ module OPC
6
+ # Package allows the following:
7
+ # * Create new package based on an existing PPTX file
8
+ # (extracted ZIP for better version control)
9
+ # * Replace certain files with generated parts
10
+ class Package
11
+ # Create a new package using the directory strucuture base
12
+ def initialize(base = nil)
13
+ base ||= base || File.join(File.dirname(__FILE__), '..', 'base.pptx')
14
+ @base = File.absolute_path(base)
15
+ @parts = Hash[default_part_names.map {|fn| [fn, nil]}]
16
+ end
17
+
18
+ def base
19
+ @base
20
+ end
21
+
22
+ def content_types
23
+ @content_types ||= ContentTypes.new(self, '[Content_Types].xml')
24
+ end
25
+
26
+ def has_part?(name)
27
+ @parts.has_key?(name)
28
+ end
29
+
30
+ def part(name)
31
+ @parts[name]
32
+ end
33
+
34
+ # Returns a Hash of name -> Part
35
+ # Do not modify the hash directly, but use set_part.
36
+ def parts
37
+ @parts
38
+ end
39
+
40
+ def template_part(name)
41
+ file = File.join(@base, name)
42
+ IO.read(file) if File.exist? file
43
+ end
44
+
45
+ def marshal_part(name)
46
+ part = @parts[name]
47
+ if part
48
+ if part.is_a? BasePart
49
+ part.marshal
50
+ elsif part.instance_of? String
51
+ # TODO wrap binary parts, probably as references when implementing streaming
52
+ part
53
+ else
54
+ raise "marshal_part: Unknown part type: '#{p.class}' for part '#{name}'"
55
+ end
56
+ else
57
+ template_part name
58
+ end
59
+ end
60
+
61
+ # Used e.g. for creating slides with proper numbering (i.e. slide1.xml, slide2.xml)
62
+ def next_part_with_prefix(prefix, suffix)
63
+ num = @parts.select { |n, _| n.start_with?(prefix) && n.end_with?(suffix) }
64
+ .map { |n, _| n[prefix.length..-1][0..-(suffix.size + 1)].to_i }
65
+ .max
66
+ num = (num || 0) + 1
67
+ "#{prefix}#{num}#{suffix}"
68
+ end
69
+
70
+ def set_part(name, obj, content_type = nil)
71
+ @parts[name] = obj
72
+ if content_type
73
+ content_types.set_override(name, content_type)
74
+ end
75
+ end
76
+
77
+ def presentation
78
+ @presentation ||= PPTX::Presentation.new(self, 'ppt/presentation.xml')
79
+ end
80
+
81
+ # Stream a ZIP file while it's being generated
82
+ # The returned PackageStreamer instance has an each method that takes a block, which
83
+ # is called multiple times with chunks of data.
84
+ # When adding BinaryParts (e.g. S3ObjectPart or FilePart), these are also
85
+ # streamed into the ZIP file, so you can e.g. stream objects directly from S3
86
+ # to the client, encoded in the ZIP file.
87
+ # Use this e.g. in a Rails controller as `self.response_body`.
88
+ def stream_zip(ignore_part_exceptions=false)
89
+ PackageStreamer.new(self, ignore_part_exceptions)
90
+ end
91
+
92
+ def to_zip
93
+ buffer = ::StringIO.new('')
94
+
95
+ Zip::OutputStream.write_buffer(buffer) do |out|
96
+ @parts.each do |name, _|
97
+ out.put_next_entry name
98
+ out.write marshal_part(name)
99
+ end
100
+ end
101
+
102
+ buffer.string
103
+ end
104
+
105
+ private
106
+ def default_part_names
107
+ @default_part_names ||= Dir.glob(File.join(@base, '**/*'), File::FNM_DOTMATCH)
108
+ .reject {|fn| File.directory?(fn) }
109
+ .reject {|fn| fn.end_with?('.DS_Store')}
110
+ .map {|fn| fn[(@base.length + 1)..-1]}
111
+ end
112
+
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,48 @@
1
+ module PPTX
2
+ module OPC
3
+ class PackageStreamer
4
+ # Use ignore_part_exceptions to ignore exceptions while marshalling or streaming
5
+ # a part. Useful to ignore missing files on S3.
6
+ def initialize(package, ignore_part_exceptions=false)
7
+ @package = package
8
+ @ignore_part_exceptions = ignore_part_exceptions
9
+ end
10
+
11
+ # This method uses parts of zipline to implement streaming.
12
+ #
13
+ # This is how Zipline usually works:
14
+ # * Rails calls response_body.each
15
+ # * response_body is Zipline::ZipGenerator, it creates a Zipline::FakeStream with the
16
+ # block given to each
17
+ # * The FakeStream is used as output for ZipLine::OutputStream (which inherits
18
+ # from Zip::OutputStream)
19
+ # * ZipGenerator goes through files and calls write_file on the OutputStream for each file
20
+ # * OutputStream writes the data as String to FakeStream via the << method
21
+ # * FakeStream yields the String to the block given by Rails
22
+ #
23
+ # This method uses Zipline's FakeStream and OutputStream, but writes the entries
24
+ # to the zip file on it's own, so we can write data for a file in multiple steps
25
+ # and don't have to rely on Zipline's crazy logic (e.g. on what can be streamed and
26
+ # what not - only IO and StringIO). Basically, this replaces ZipGenerator.
27
+ def each(&block)
28
+ fake_stream = Zipline::FakeStream.new(&block)
29
+ Zipline::OutputStream.open(fake_stream) do |zip|
30
+ @package.parts.each do |name, part|
31
+ begin
32
+ if part.respond_to?(:stream) && part.respond_to?(:size)
33
+ zip.put_next_entry name, part.size
34
+ part.stream zip
35
+ else
36
+ data = @package.marshal_part(name)
37
+ zip.put_next_entry name, data.size
38
+ zip << data
39
+ end
40
+ rescue => e
41
+ raise e unless @ignore_part_exceptions
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,27 @@
1
+ require 'pathname'
2
+
3
+ module PPTX
4
+ module OPC
5
+ class Part < BasePart
6
+ def initialize(package, part_name)
7
+ super(package, part_name)
8
+ @relationships = Relationships.new(package, relationship_part_name)
9
+ end
10
+
11
+ def relative_part_name(name)
12
+ source = Pathname.new(File.dirname(part_name))
13
+ target = Pathname.new(name)
14
+ target.relative_path_from(source).to_s
15
+ end
16
+
17
+ def relationships
18
+ @relationships
19
+ end
20
+
21
+ def relationship_part_name(part_name = nil)
22
+ part_name ||= @part_name
23
+ File.join(File.dirname(part_name), '_rels', File.basename(part_name) + '.rels')
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ require 'securerandom'
2
+
3
+ module PPTX
4
+ module OPC
5
+ class Relationships < BasePart
6
+ NS = 'http://schemas.openxmlformats.org/package/2006/relationships'
7
+
8
+ def base_xml
9
+ '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
10
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
11
+ </Relationships>
12
+ '''
13
+ end
14
+
15
+ def add(relative_part_name, type)
16
+ ref_id = "rId#{SecureRandom.hex(10)}"
17
+
18
+ relationship = Nokogiri::XML::Node.new('Relationship', doc)
19
+ relationship['Id'] = ref_id
20
+ relationship['Target'] = relative_part_name
21
+ relationship['Type'] = type
22
+ list_xml.add_child(relationship)
23
+
24
+ ref_id
25
+ end
26
+
27
+ def list_xml
28
+ @list_xml ||= doc.xpath('r:Relationships', r: NS).first
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ module PPTX
2
+ module OPC
3
+ class S3ObjectPart < BinaryPart
4
+ def initialize(package, s3obj, size)
5
+ super(package, File.basename(s3obj.key))
6
+ @object = s3obj
7
+ @size = size
8
+ end
9
+
10
+ def marshal
11
+ @object.read
12
+ end
13
+
14
+ def size
15
+ @size
16
+ end
17
+
18
+ def stream(out)
19
+ @object.read do |chunk|
20
+ out << chunk
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ module PPTX
2
+ class Presentation < OPC::Part
3
+ NS = 'http://schemas.openxmlformats.org/presentationml/2006/main'
4
+ MIN_SLIDE_ID = 256
5
+
6
+ def initialize(package, part_name)
7
+ super(package, part_name)
8
+ end
9
+
10
+ def add_slide(slide)
11
+ # TODO remove me - remove existing slide from template
12
+ # slide_list_xml.xpath('./p:sldId').each do |slide|
13
+ # slide_list.children.delete(slide)
14
+ # end
15
+ ref_id = relationships.add(relative_part_name(slide.part_name), RELTYPE_SLIDE)
16
+
17
+ # add slide to sldIdList in presentation
18
+ slide_id = Nokogiri::XML::Node.new('p:sldId', doc)
19
+ slide_id['id'] = next_slide_id
20
+ slide_id['r:id'] = ref_id
21
+ slide_list_xml.add_child(slide_id)
22
+ end
23
+
24
+ def next_slide_id
25
+ ids = slide_list_xml.xpath('./p:sldId').map { |sid| sid['id'].to_i }
26
+ return (ids.max || MIN_SLIDE_ID - 1) + 1
27
+ end
28
+
29
+ def slide_list_xml
30
+ @slide_list_xml ||= doc.xpath('/p:presentation/p:sldIdLst', p: NS).first
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,10 @@
1
+ module PPTX
2
+ module Shapes
3
+ end
4
+ end
5
+
6
+ require_relative 'shapes/shape'
7
+ require_relative 'shapes/filled_rectangle'
8
+ require_relative 'shapes/picture'
9
+ require_relative 'shapes/textbox'
10
+ require_relative 'shapes/slide_number'
@@ -0,0 +1,32 @@
1
+ module PPTX
2
+ module Shapes
3
+ class FilledRectangle < Shape
4
+ def initialize(transform, color)
5
+ super(transform)
6
+ @color = color
7
+ end
8
+
9
+ def base_xml
10
+ # TODO replace cNvPr descr, id and name
11
+ """
12
+ <p:sp xmlns:a='http://schemas.openxmlformats.org/drawingml/2006/main'
13
+ xmlns:p='http://schemas.openxmlformats.org/presentationml/2006/main'>
14
+ <p:nvSpPr>
15
+ <p:cNvPr id='2' name='Rectangle'/>
16
+ <p:cNvSpPr/>
17
+ <p:nvPr/>
18
+ </p:nvSpPr>
19
+ <p:spPr>
20
+ </p:spPr>
21
+ </p:sp>
22
+ """
23
+ end
24
+
25
+ def build_node
26
+ base_node.tap do |node|
27
+ node.xpath('.//p:spPr', p: Presentation::NS).first.add_child build_solid_fill @color
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,40 @@
1
+ module PPTX
2
+ module Shapes
3
+ class Picture < Shape
4
+ def initialize(transform, relationship_id)
5
+ super(transform)
6
+ @relationship_id = relationship_id
7
+ end
8
+
9
+ def base_xml
10
+ # TODO replace cNvPr descr, id and name
11
+ """
12
+ <p:pic xmlns:a='http://schemas.openxmlformats.org/drawingml/2006/main'
13
+ xmlns:p='http://schemas.openxmlformats.org/presentationml/2006/main'>
14
+ <p:nvPicPr>
15
+ <p:cNvPr descr='test_photo.jpg' id='2' name='Picture 1'/>
16
+ <p:cNvPicPr>
17
+ <a:picLocks noChangeAspect='1'/>
18
+ </p:cNvPicPr>
19
+ <p:nvPr/>
20
+ </p:nvPicPr>
21
+ <p:blipFill>
22
+ <a:blip r:embed='REPLACEME'/>
23
+ <a:stretch>
24
+ <a:fillRect/>
25
+ </a:stretch>
26
+ </p:blipFill>
27
+ <p:spPr>
28
+ </p:spPr>
29
+ </p:pic>
30
+ """
31
+ end
32
+
33
+ def build_node
34
+ base_node.tap do |node|
35
+ node.xpath('.//a:blip', a: DRAWING_NS).first['r:embed'] = @relationship_id
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,46 @@
1
+ module PPTX
2
+ module Shapes
3
+ class Shape
4
+ def initialize(transform)
5
+ @transform = transform
6
+ end
7
+
8
+ def base_node
9
+ @base_node ||= Nokogiri::XML::DocumentFragment.parse(base_xml).tap do |node|
10
+ set_shape_properties(node, *@transform)
11
+ end
12
+ end
13
+
14
+ def set_shape_properties(node, x, y, width, height)
15
+ xml = """
16
+ <p:spPr xmlns:a='http://schemas.openxmlformats.org/drawingml/2006/main'
17
+ xmlns:p='http://schemas.openxmlformats.org/presentationml/2006/main'>
18
+ <a:xfrm>
19
+ <a:off x='#{x.to_i}' y='#{y.to_i}'/>
20
+ <a:ext cx='#{width.to_i}' cy='#{height.to_i}'/>
21
+ </a:xfrm>
22
+ <a:prstGeom prst='rect'>
23
+ <a:avLst/>
24
+ </a:prstGeom>
25
+ </p:spPr>
26
+ """
27
+
28
+ node.xpath('.//p:spPr', p: Presentation::NS)
29
+ .first
30
+ .replace(Nokogiri::XML::DocumentFragment.parse(xml))
31
+ end
32
+
33
+ def build_solid_fill(rgb_color)
34
+ fill_xml = """
35
+ <a:solidFill xmlns:a='http://schemas.openxmlformats.org/drawingml/2006/main'>
36
+ <a:srgbClr val='SETME'/>
37
+ </a:solidFill>
38
+ """
39
+
40
+ Nokogiri::XML::DocumentFragment.parse(fill_xml).tap do |node|
41
+ node.xpath('.//a:srgbClr', a: DRAWING_NS).first['val'] = rgb_color
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end