diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..b05485a6
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,32 @@
+name: Test
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ rspec:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
+ environment: test
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ruby/setup-ruby@v1
+ with:
+ bundler-cache: true
+ - name: Run test suite
+ run: bundle exec rspec
+ env:
+ CI_NAME: github-actions
+ CI_JOB_ID: ${{ github.run_id }}
+ CI_BUILD_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ CI_BRANCH: ${{ github.head_ref || github.ref_name }}
+ CI_PULL_REQUEST: ${{ github.event.pull_request.html_url || '' }}
+ COVERALLS_SERVICE_NAME: github-actions
+ COVERALLS_REPO_TOKEN: ${{secrets.COVERALLS_REPO_TOKEN}}
diff --git a/.gitignore b/.gitignore
index 09b4c415..b6075efe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,6 @@ spec/reports
test/tmp
test/version_tmp
tmp
-.pryrc
\ No newline at end of file
+.pryrc
+.DS_Store
+*.swp
diff --git a/README.md b/README.md
index d33fe79f..f029bc67 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
[](https://badge.fury.io/rb/powerpointk)
-[](https://ruby-gem-downloads-badge.herokuapp.com/powerpoint?type=total&total_label=downloads)
+[](https://coveralls.io/github/mgmodell/powerpoint?branch=master)
# 'powerpoint' gem -- for creating PowerPoint Slides in Ruby.
@@ -55,9 +55,25 @@ coords = {x: 124200, y: 3356451, cx: 2895600, cy: 1013460}
# Saving the pptx file to the current directory.
@deck.save('test.pptx')
```
+
+Transitions can be configured per slide with a transition type and duration in
+milliseconds:
+
+```ruby
+@deck.add_textual_slide(
+ 'Agenda',
+ ['Item 1', 'Item 2'],
+ { transition: { type: :fade, duration: 750 } }
+)
+```
+
+The transition type defaults to `fade`; supported types include `fade`,
+`dissolve`, `wipe`, `push`, and `zoom`.
## Compatibility
+Generated presentations use a 16:9 widescreen layout, and slide text is automatically sized to fit its placeholders.
+
'powerpoint' gem has been tested with LibreOffice (4.2.1.1) and Apache OpenOffice (4.0.1) on Mac OS X Mavericks, Microsoft PowerPoint 2010 on Windows 7 and Google Docs (latest version as of March 2014).
## Contributing
diff --git a/lib/powerpoint.rb b/lib/powerpoint.rb
index 71fb5f68..a82faa08 100644
--- a/lib/powerpoint.rb
+++ b/lib/powerpoint.rb
@@ -1,5 +1,6 @@
require "powerpoint/version"
require 'powerpoint/util'
+require 'powerpoint/slide/transition'
require 'powerpoint/slide/intro'
require 'powerpoint/slide/textual'
require 'powerpoint/slide/pictorial'
diff --git a/lib/powerpoint/compression.rb b/lib/powerpoint/compression.rb
index 2ddd977f..93f1b3cb 100644
--- a/lib/powerpoint/compression.rb
+++ b/lib/powerpoint/compression.rb
@@ -10,7 +10,7 @@ def self.decompress_pptx(in_path, out_path)
end
def self.compress_pptx(in_path, out_path)
- Zip::File.open(out_path, Zip::File::CREATE) do |zip_file|
+ Zip::File.open(out_path, create: true) do |zip_file|
Dir.glob("#{in_path}/**/*", ::File::FNM_DOTMATCH).each do |path|
zip_path = path.gsub("#{in_path}/","")
next if zip_path == "." || zip_path == ".." || zip_path.match(/DS_Store/)
diff --git a/lib/powerpoint/presentation.rb b/lib/powerpoint/presentation.rb
index c8a75d98..20cc697c 100644
--- a/lib/powerpoint/presentation.rb
+++ b/lib/powerpoint/presentation.rb
@@ -12,9 +12,13 @@ def initialize
@slides = []
end
- def add_intro(title, subtitile = nil)
+ def add_intro(title, subtitile = nil, options = {})
+ if subtitile.is_a?(Hash) && options.empty?
+ options = subtitile
+ subtitile = nil
+ end
existing_intro_slide = @slides.select {|s| s.class == Powerpoint::Slide::Intro}[0]
- slide = Powerpoint::Slide::Intro.new(presentation: self, title: title, subtitile: subtitile)
+ slide = Powerpoint::Slide::Intro.new(presentation: self, title: title, subtitile: subtitile, transition: options[:transition])
if existing_intro_slide
@slides[@slides.index(existing_intro_slide)] = slide
else
@@ -22,20 +26,20 @@ def add_intro(title, subtitile = nil)
end
end
- def add_textual_slide(title, content = [])
- @slides << Powerpoint::Slide::Textual.new(presentation: self, title: title, content: content)
+ def add_textual_slide(title, content = [], options = {})
+ @slides << Powerpoint::Slide::Textual.new(presentation: self, title: title, content: content, transition: options[:transition])
end
- def add_pictorial_slide(title, image_path, coords = {})
- @slides << Powerpoint::Slide::Pictorial.new(presentation: self, title: title, image_path: image_path, coords: coords)
+ def add_pictorial_slide(title, image_path, coords = {}, options = {})
+ @slides << Powerpoint::Slide::Pictorial.new(presentation: self, title: title, image_path: image_path, coords: coords, transition: options[:transition])
end
- def add_text_picture_slide(title, image_path, content = [])
- @slides << Powerpoint::Slide::TextPicSplit.new(presentation: self, title: title, image_path: image_path, content: content)
+ def add_text_picture_slide(title, image_path, content = [], options = {})
+ @slides << Powerpoint::Slide::TextPicSplit.new(presentation: self, title: title, image_path: image_path, content: content, transition: options[:transition])
end
- def add_picture_description_slide(title, image_path, content = [])
- @slides << Powerpoint::Slide::DescriptionPic.new(presentation: self, title: title, image_path: image_path, content: content)
+ def add_picture_description_slide(title, image_path, content = [], options = {})
+ @slides << Powerpoint::Slide::DescriptionPic.new(presentation: self, title: title, image_path: image_path, content: content, transition: options[:transition])
end
def save(path)
diff --git a/lib/powerpoint/slide/intro.rb b/lib/powerpoint/slide/intro.rb
index 69650915..7ba00ce7 100644
--- a/lib/powerpoint/slide/intro.rb
+++ b/lib/powerpoint/slide/intro.rb
@@ -5,6 +5,7 @@ module Powerpoint
module Slide
class Intro
include Powerpoint::Util
+ include Powerpoint::Slide::Transition
attr_reader :title, :subtitile
@@ -28,7 +29,7 @@ def save_rel_xml(extract_path, index)
private :save_rel_xml
def save_slide_xml(extract_path, index)
- render_view('intro_slide.xml.erb', "#{extract_path}/ppt/slides/slide#{index}.xml")
+ render_view('intro_slide.xml.erb', "#{extract_path}/ppt/slides/slide#{index}.xml", transition_xml: transition_xml)
end
private :save_slide_xml
end
diff --git a/lib/powerpoint/slide/pictorial.rb b/lib/powerpoint/slide/pictorial.rb
index 4b8f98bd..c4509537 100644
--- a/lib/powerpoint/slide/pictorial.rb
+++ b/lib/powerpoint/slide/pictorial.rb
@@ -7,6 +7,7 @@ module Powerpoint
module Slide
class Pictorial
include Powerpoint::Util
+ include Powerpoint::Slide::Transition
attr_reader :image_name, :title, :coords, :image_path
@@ -28,13 +29,18 @@ def file_type
end
def default_coords
- slide_width = pixle_to_pt(720)
+ slide_width = pixle_to_pt(960)
default_width = pixle_to_pt(550)
+ default_height = pixle_to_pt(420)
return {} unless dimensions = FastImage.size(image_path)
image_width, image_height = dimensions.map {|d| pixle_to_pt(d)}
- new_width = default_width < image_width ? default_width : image_width
- ratio = new_width / image_width.to_f
+ ratio = [
+ default_width / image_width.to_f,
+ default_height / image_height.to_f,
+ 1
+ ].min
+ new_width = (image_width * ratio).round
new_height = (image_height.to_f * ratio).round
{x: (slide_width / 2) - (new_width/2), y: pixle_to_pt(120), cx: new_width, cy: new_height}
end
@@ -46,7 +52,7 @@ def save_rel_xml(extract_path, index)
private :save_rel_xml
def save_slide_xml(extract_path, index)
- render_view('pictorial_slide.xml.erb', "#{extract_path}/ppt/slides/slide#{index}.xml")
+ render_view('pictorial_slide.xml.erb', "#{extract_path}/ppt/slides/slide#{index}.xml", transition_xml: transition_xml)
end
private :save_slide_xml
end
diff --git a/lib/powerpoint/slide/picture_description.rb b/lib/powerpoint/slide/picture_description.rb
index e53e0dd7..e203d7aa 100644
--- a/lib/powerpoint/slide/picture_description.rb
+++ b/lib/powerpoint/slide/picture_description.rb
@@ -7,6 +7,7 @@ module Powerpoint
module Slide
class DescriptionPic
include Powerpoint::Util
+ include Powerpoint::Slide::Transition
attr_reader :title, :content, :image_name, :image_path, :coords
@@ -28,7 +29,7 @@ def file_type
end
def default_coords
- slide_width = pixle_to_pt(720)
+ slide_width = pixle_to_pt(960)
default_width = pixle_to_pt(550)
default_height = pixle_to_pt(300)
@@ -55,7 +56,7 @@ def save_rel_xml(extract_path, index)
private :save_rel_xml
def save_slide_xml(extract_path, index)
- render_view('picture_description_slide.xml.erb', "#{extract_path}/ppt/slides/slide#{index}.xml")
+ render_view('picture_description_slide.xml.erb', "#{extract_path}/ppt/slides/slide#{index}.xml", transition_xml: transition_xml)
end
private :save_slide_xml
end
diff --git a/lib/powerpoint/slide/text_picture_split.rb b/lib/powerpoint/slide/text_picture_split.rb
index 425fddbd..162ba406 100644
--- a/lib/powerpoint/slide/text_picture_split.rb
+++ b/lib/powerpoint/slide/text_picture_split.rb
@@ -7,6 +7,7 @@ module Powerpoint
module Slide
class TextPicSplit
include Powerpoint::Util
+ include Powerpoint::Slide::Transition
attr_reader :title, :content, :image_name, :image_path, :coords
@@ -28,13 +29,18 @@ def file_type
end
def default_coords
- start_x = pixle_to_pt(360)
+ start_x = pixle_to_pt(480)
default_width = pixle_to_pt(300)
+ default_height = pixle_to_pt(420)
return {} unless dimensions = FastImage.size(image_path)
image_width, image_height = dimensions.map {|d| pixle_to_pt(d)}
- new_width = default_width < image_width ? default_width : image_width
- ratio = new_width / image_width.to_f
+ ratio = [
+ default_width / image_width.to_f,
+ default_height / image_height.to_f,
+ 1
+ ].min
+ new_width = (image_width * ratio).round
new_height = (image_height.to_f * ratio).round
{x: start_x, y: pixle_to_pt(120), cx: new_width, cy: new_height}
end
@@ -46,7 +52,7 @@ def save_rel_xml(extract_path, index)
private :save_rel_xml
def save_slide_xml(extract_path, index)
- render_view('text_picture_split_slide.xml.erb', "#{extract_path}/ppt/slides/slide#{index}.xml")
+ render_view('text_picture_split_slide.xml.erb', "#{extract_path}/ppt/slides/slide#{index}.xml", transition_xml: transition_xml)
end
private :save_slide_xml
end
diff --git a/lib/powerpoint/slide/textual.rb b/lib/powerpoint/slide/textual.rb
index a357f16f..eb5fcfde 100644
--- a/lib/powerpoint/slide/textual.rb
+++ b/lib/powerpoint/slide/textual.rb
@@ -5,6 +5,7 @@ module Powerpoint
module Slide
class Textual
include Powerpoint::Util
+ include Powerpoint::Slide::Transition
attr_reader :title, :content
@@ -24,7 +25,7 @@ def save_rel_xml(extract_path, index)
private :save_rel_xml
def save_slide_xml(extract_path, index)
- render_view('textual_slide.xml.erb', "#{extract_path}/ppt/slides/slide#{index}.xml")
+ render_view('textual_slide.xml.erb', "#{extract_path}/ppt/slides/slide#{index}.xml", transition_xml: transition_xml)
end
private :save_slide_xml
end
diff --git a/lib/powerpoint/slide/transition.rb b/lib/powerpoint/slide/transition.rb
new file mode 100644
index 00000000..5ff0bee9
--- /dev/null
+++ b/lib/powerpoint/slide/transition.rb
@@ -0,0 +1,26 @@
+module Powerpoint
+ module Slide
+ module Transition
+ TYPES = %w[
+ blinds checker circle comb cover cut diamond dissolve fade newsflash
+ plus pull push random randomBar split strips wedge wheel wipe zoom
+ ].freeze
+
+ def transition_xml
+ return '' unless @transition
+ raise ArgumentError, 'transition must be a Hash' unless @transition.is_a?(Hash)
+
+ type = (@transition[:type] || @transition['type'] || 'fade').to_s
+ raise ArgumentError, "unsupported transition type: #{type}" unless TYPES.include?(type)
+
+ duration = @transition[:duration] || @transition['duration']
+ if duration && (!duration.is_a?(Integer) || duration <= 0)
+ raise ArgumentError, 'transition duration must be a positive integer in milliseconds'
+ end
+
+ duration_attribute = duration ? %( p14:dur="#{duration}") : ''
+ %()
+ end
+ end
+ end
+end
diff --git a/lib/powerpoint/views/app.xml.erb b/lib/powerpoint/views/app.xml.erb
index 28b9fe30..62b528b9 100755
--- a/lib/powerpoint/views/app.xml.erb
+++ b/lib/powerpoint/views/app.xml.erb
@@ -3,8 +3,8 @@
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
0
43
- Microsoft Macintosh PowerPoint
- On-screen Show (4:3)
+ Microsoft Office PowerPoint
+ On-screen Show (16:9)
15
<%= slides.length %>
0
@@ -39,7 +39,7 @@
Calibri
Office Theme
<% slides.each do |slide| %>
- <%= slide.title %>
+ <%= ERB::Util.html_escape(slide.title) %>
<% end %>
@@ -47,5 +47,5 @@
false
false
false
- 15.0000
+ 16.0000
\ No newline at end of file
diff --git a/lib/powerpoint/views/intro_slide.xml.erb b/lib/powerpoint/views/intro_slide.xml.erb
index c77d3631..95d42e86 100644
--- a/lib/powerpoint/views/intro_slide.xml.erb
+++ b/lib/powerpoint/views/intro_slide.xml.erb
@@ -27,12 +27,12 @@
-
+
- <%= title %>
+ <%= ERB::Util.html_escape(title) %>
@@ -50,12 +50,12 @@
-
+
- <%= subtitile %>
+ <%= ERB::Util.html_escape(subtitile) %>
@@ -71,4 +71,5 @@
+ <%= transition_xml %>
\ No newline at end of file
diff --git a/lib/powerpoint/views/pictorial_slide.xml.erb b/lib/powerpoint/views/pictorial_slide.xml.erb
index 38c5ad82..403a72a8 100644
--- a/lib/powerpoint/views/pictorial_slide.xml.erb
+++ b/lib/powerpoint/views/pictorial_slide.xml.erb
@@ -27,12 +27,12 @@
-
+
- <%= title %>
+ <%= ERB::Util.html_escape(title) %>
@@ -84,4 +84,5 @@
+ <%= transition_xml %>
\ No newline at end of file
diff --git a/lib/powerpoint/views/picture_description_slide.xml.erb b/lib/powerpoint/views/picture_description_slide.xml.erb
index f1721803..f5ed0f11 100644
--- a/lib/powerpoint/views/picture_description_slide.xml.erb
+++ b/lib/powerpoint/views/picture_description_slide.xml.erb
@@ -27,12 +27,12 @@
-
+
- <%= title %>
+ <%= ERB::Util.html_escape(title) %>
@@ -86,13 +86,13 @@
-
+
<% content.each_with_index do |c, index| %>
- <%= c %>
+ <%= ERB::Util.html_escape(c) %>
<% if content.length == (index + 1) %>
@@ -112,4 +112,5 @@
+ <%= transition_xml %>
\ No newline at end of file
diff --git a/lib/powerpoint/views/presentation.xml.erb b/lib/powerpoint/views/presentation.xml.erb
index b3f3be07..16e8a853 100644
--- a/lib/powerpoint/views/presentation.xml.erb
+++ b/lib/powerpoint/views/presentation.xml.erb
@@ -8,7 +8,7 @@
<% end %>
-
+
diff --git a/lib/powerpoint/views/text_picture_split_slide.xml.erb b/lib/powerpoint/views/text_picture_split_slide.xml.erb
index 4c992875..148816c7 100644
--- a/lib/powerpoint/views/text_picture_split_slide.xml.erb
+++ b/lib/powerpoint/views/text_picture_split_slide.xml.erb
@@ -27,12 +27,12 @@
-
+
- <%= title %>
+ <%= ERB::Util.html_escape(title) %>
@@ -50,13 +50,13 @@
-
+
<% content.each_with_index do |c, index| %>
- <%= c %>
+ <%= ERB::Util.html_escape(c) %>
<% if content.length == (index + 1) %>
@@ -111,4 +111,5 @@
+ <%= transition_xml %>
\ No newline at end of file
diff --git a/lib/powerpoint/views/textual_slide.xml.erb b/lib/powerpoint/views/textual_slide.xml.erb
index 5bea44e7..768c78e0 100644
--- a/lib/powerpoint/views/textual_slide.xml.erb
+++ b/lib/powerpoint/views/textual_slide.xml.erb
@@ -27,12 +27,12 @@
-
+
- <%= title %>
+ <%= ERB::Util.html_escape(title) %>
@@ -50,13 +50,13 @@
-
+
<% content.each_with_index do |c, index| %>
- <%= c %>
+ <%= ERB::Util.html_escape(c) %>
<% if content.length == (index + 1) %>
@@ -75,4 +75,5 @@
+ <%= transition_xml %>
\ No newline at end of file
diff --git a/mise.toml b/mise.toml
new file mode 100644
index 00000000..96bdb30e
--- /dev/null
+++ b/mise.toml
@@ -0,0 +1,2 @@
+[tools]
+ruby = "4"
diff --git a/powerpoint.gemspec b/powerpoint.gemspec
index 2277e79c..dfc5dbfa 100644
--- a/powerpoint.gemspec
+++ b/powerpoint.gemspec
@@ -20,12 +20,14 @@ Gem::Specification.new do |spec|
spec.required_ruby_version = '>= 1.9.2'
- spec.add_development_dependency "bundler", "~> 1.3"
+ spec.add_development_dependency "bundler", "~> 4"
spec.add_development_dependency "rake"
- spec.add_development_dependency 'rspec', '~> 2.13.0'
+ spec.add_development_dependency 'rspec', '~> 3.13.2'
+ spec.add_development_dependency 'coveralls_reborn', '<=0.29.0'
spec.add_development_dependency 'pry'
+ spec.add_development_dependency 'simplecov', '~> 0.22'
- spec.add_dependency 'rubyzip', '~> 1'
- spec.add_dependency 'fastimage', '~> 1'
+ spec.add_dependency 'rubyzip', '~> 3'
+ spec.add_dependency 'fastimage', '~> 2'
end
diff --git a/samples/pptx/sample.pptx b/samples/pptx/sample.pptx
index 1221afc4..3da56732 100644
Binary files a/samples/pptx/sample.pptx and b/samples/pptx/sample.pptx differ
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 00000000..693f2887
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,11 @@
+require 'coveralls'
+
+Coveralls.wear! do
+ add_filter '/spec/'
+ minimum_coverage 90
+end
+
+require 'powerpoint'
+require 'tmpdir'
+require 'fileutils'
+require 'zip'
diff --git a/spec/test_spec.rb b/spec/test_spec.rb
index 378417c7..006f3c77 100644
--- a/spec/test_spec.rb
+++ b/spec/test_spec.rb
@@ -1,22 +1,375 @@
-require 'powerpoint'
-
-describe 'Powerpoint parsing a sample PPTX file' do
- before(:all) do
- @deck = Powerpoint::Presentation.new
- @deck.add_intro 'Bicycle Of the Mind', 'created by Steve Jobs'
- @deck.add_textual_slide 'Why Mac?', ['Its cool!', 'Its light!']
- @deck.add_textual_slide 'Why Iphone?', ['Its fast!', 'Its cheap!']
- @deck.add_pictorial_slide 'JPG Logo', 'samples/images/sample_png.png'
- @deck.add_text_picture_slide('Text Pic Split', 'samples/images/sample_png.png', content = ['Here is a string', 'here is another'])
- @deck.add_pictorial_slide 'PNG Logo', 'samples/images/sample_png.png'
- @deck.add_picture_description_slide('Pic Desc', 'samples/images/sample_png.png', content = ['Here is a string', 'here is another'])
- @deck.add_picture_description_slide('JPG Logo', 'samples/images/sample_jpg.jpg', content = ['descriptions'])
- @deck.add_pictorial_slide 'GIF Logo', 'samples/images/sample_gif.gif', {x: 124200, y: 3356451, cx: 2895600, cy: 1013460}
- @deck.add_textual_slide 'Why Android?', ['Its great!', 'Its sweet!']
- @deck.save 'samples/pptx/sample.pptx' # Examine the PPTX file
- end
-
- it 'Create a PPTX file successfully.' do
- #@deck.should_not be_nil
+require 'spec_helper'
+
+RSpec.describe Powerpoint::Presentation do
+ let(:png_image) { File.expand_path('../samples/images/sample_png.png', __dir__) }
+ let(:jpg_image) { File.expand_path('../samples/images/sample_jpg.jpg', __dir__) }
+ let(:gif_image) { File.expand_path('../samples/images/sample_gif.gif', __dir__) }
+
+ def pptx_entry_contents(pptx_path, entry_name)
+ Zip::File.open(pptx_path) { |zip_file| zip_file.read(entry_name) }
+ end
+
+ def pptx_entries(pptx_path)
+ Zip::File.open(pptx_path) { |zip_file| zip_file.map(&:name) }
+ end
+
+ it 'builds a valid pptx with expected slides, relationships and media' do
+ deck = described_class.new
+ deck.add_intro('Deck Title', 'Deck Subtitle')
+ deck.add_textual_slide('Agenda', ['Item 1', 'Item 2'])
+ deck.add_pictorial_slide('Picture', png_image, x: 100, y: 200, cx: 300, cy: 400)
+ deck.add_text_picture_slide('Split', jpg_image, ['Left text'])
+ deck.add_picture_description_slide('Description', gif_image, ['Caption'])
+
+ saved_path = nil
+ Dir.mktmpdir do |dir|
+ output = File.join(dir, 'sample.pptx')
+ saved_path = deck.save(output)
+
+ expect(saved_path).to eq(output)
+ expect(File.exist?(output)).to be(true)
+
+ entries = pptx_entries(output)
+ expect(entries).to include('[Content_Types].xml')
+ expect(entries).to include('ppt/presentation.xml')
+ expect(entries).to include('ppt/slides/slide1.xml', 'ppt/slides/slide2.xml', 'ppt/slides/slide3.xml', 'ppt/slides/slide4.xml', 'ppt/slides/slide5.xml')
+ expect(entries).to include('ppt/media/sample_png.png', 'ppt/media/sample_jpg.jpg', 'ppt/media/sample_gif.gif')
+ expect(entries).not_to include(a_string_matching(/\.keep$/))
+
+ intro_slide = pptx_entry_contents(output, 'ppt/slides/slide1.xml')
+ expect(intro_slide).to include('Deck Title')
+ expect(intro_slide).to include('Deck Subtitle')
+ expect(intro_slide.scan('').size).to eq(2)
+
+ textual_slide = pptx_entry_contents(output, 'ppt/slides/slide2.xml')
+ expect(textual_slide).to include('Agenda')
+ expect(textual_slide).to include('Item 1')
+ expect(textual_slide).to include('Item 2')
+ expect(textual_slide.scan('').size).to eq(2)
+
+ pictorial_slide = pptx_entry_contents(output, 'ppt/slides/slide3.xml')
+ expect(pictorial_slide).to include('')
+ expect(pictorial_slide).to include('')
+ expect(pictorial_slide.scan('').size).to eq(1)
+
+ split_slide = pptx_entry_contents(output, 'ppt/slides/slide4.xml')
+ expect(split_slide).to include('Split')
+ expect(split_slide).to include('Left text')
+ expect(split_slide.scan('').size).to eq(2)
+
+ description_slide = pptx_entry_contents(output, 'ppt/slides/slide5.xml')
+ expect(description_slide).to include('Description')
+ expect(description_slide).to include('Caption')
+ expect(description_slide.scan('').size).to eq(2)
+
+ content_types = pptx_entry_contents(output, '[Content_Types].xml')
+ expect(content_types).to include('Extension="png"')
+ expect(content_types).to include('Extension="jpg"')
+ expect(content_types).to include('Extension="gif"')
+
+ presentation_xml = pptx_entry_contents(output, 'ppt/presentation.xml')
+ expect(presentation_xml.scan('')
+
+ relationships_xml = pptx_entry_contents(output, 'ppt/_rels/presentation.xml.rels')
+ expect(relationships_xml).to include('Target="slides/slide1.xml"')
+ expect(relationships_xml).to include('Target="slides/slide5.xml"')
+ end
+
+ expect(saved_path).to end_with('.pptx')
+ end
+
+ it 'generates readable text on a widescreen slide and escapes XML content' do
+ deck = described_class.new
+ deck.add_textual_slide('Goals & ', ['Readable & visible'])
+
+ Dir.mktmpdir do |dir|
+ output = File.join(dir, 'widescreen.pptx')
+ deck.save(output)
+
+ presentation_xml = pptx_entry_contents(output, 'ppt/presentation.xml')
+ slide_xml = pptx_entry_contents(output, 'ppt/slides/slide1.xml')
+ app_xml = pptx_entry_contents(output, 'docProps/app.xml')
+
+ expect(presentation_xml).to include('')
+ expect(slide_xml).to include('')
+ expect(slide_xml).to include('Goals & <objectives>')
+ expect(slide_xml).to include('Readable & visible')
+ expect(app_xml).to include('On-screen Show (16:9)')
+ expect(app_xml).to include('Goals & <objectives>')
+ end
+ end
+
+ it 'adds transitions and durations to each slide type' do
+ deck = described_class.new
+ deck.add_intro('Opening', 'Subtitle', transition: { type: :fade, duration: 750 })
+ deck.add_textual_slide('Agenda', [], transition: { type: :wipe, duration: 1000 })
+ deck.add_pictorial_slide('Picture', png_image, {}, transition: { type: :dissolve, duration: 1250 })
+ deck.add_text_picture_slide('Split', png_image, [], transition: { type: :push, duration: 1500 })
+ deck.add_picture_description_slide('Description', png_image, [], transition: { type: :zoom, duration: 2000 })
+
+ Dir.mktmpdir do |dir|
+ output = File.join(dir, 'transitions.pptx')
+ deck.save(output)
+
+ expected_transitions = [
+ ['fade', 750],
+ ['wipe', 1000],
+ ['dissolve', 1250],
+ ['push', 1500],
+ ['zoom', 2000]
+ ]
+
+ expected_transitions.each_with_index do |(type, duration), index|
+ slide_xml = pptx_entry_contents(output, "ppt/slides/slide#{index + 1}.xml")
+ expect(slide_xml).to include(%(p14:dur="#{duration}"))
+ expect(slide_xml).to include("")
+ end
+ end
+ end
+
+ it 'rejects unsupported transitions and invalid durations' do
+ deck = described_class.new
+ deck.add_textual_slide('Invalid', [], transition: { type: 'unexpected' })
+
+ Dir.mktmpdir do |dir|
+ expect { deck.save(File.join(dir, 'invalid.pptx')) }.to raise_error(ArgumentError, /unsupported transition type/)
+ end
+
+ deck = described_class.new
+ deck.add_textual_slide('Invalid duration', [], transition: { duration: 'fast' })
+
+ Dir.mktmpdir do |dir|
+ expect { deck.save(File.join(dir, 'invalid-duration.pptx')) }.to raise_error(ArgumentError, /duration must be a positive integer/)
+ end
+ end
+
+ it 'supports omitted transitions, default effects, and string-keyed options' do
+ deck = described_class.new
+ deck.add_textual_slide('No transition')
+ deck.add_textual_slide('Default effect', [], transition: {})
+ deck.add_intro('String keys', transition: { 'type' => 'wipe', 'duration' => 500 })
+
+ no_transition = deck.slides.find { |slide| slide.title == 'No transition' }
+ default_effect = deck.slides.find { |slide| slide.title == 'Default effect' }
+ string_keys = deck.slides.find { |slide| slide.title == 'String keys' }
+
+ expect(no_transition.send(:transition_xml)).to eq('')
+ expect(default_effect.send(:transition_xml)).to include('')
+ expect(default_effect.send(:transition_xml)).not_to include('p14:dur')
+ expect(string_keys.send(:transition_xml)).to include('p14:dur="500"', '')
+ end
+
+ it 'rejects non-hash transitions and non-positive durations' do
+ [ 'fade', 1 ].each do |transition|
+ deck = described_class.new
+ deck.add_textual_slide('Invalid', [], transition: transition)
+
+ Dir.mktmpdir do |dir|
+ expect { deck.save(File.join(dir, 'invalid.pptx')) }.to raise_error(ArgumentError, /transition must be a Hash/)
+ end
+ end
+
+ [0, -1].each do |duration|
+ deck = described_class.new
+ deck.add_textual_slide('Invalid duration', [], transition: { duration: duration })
+
+ Dir.mktmpdir do |dir|
+ expect { deck.save(File.join(dir, 'invalid-duration.pptx')) }.to raise_error(ArgumentError, /duration must be a positive integer/)
+ end
+ end
+ end
+
+ it 'replaces an existing intro slide and keeps it at the beginning' do
+ deck = described_class.new
+ deck.add_textual_slide('Body', ['one'])
+ deck.add_intro('Original', 'Sub')
+ deck.add_intro('Updated', 'Sub 2')
+
+ expect(deck.slides.length).to eq(2)
+ expect(deck.slides.first).to be_a(Powerpoint::Slide::Intro)
+ expect(deck.slides.first.title).to eq('Updated')
+ expect(deck.slides[1]).to be_a(Powerpoint::Slide::Textual)
+ end
+
+ it 'returns unique non-nil media file types' do
+ deck = described_class.new
+ deck.add_intro('Only title')
+ deck.add_pictorial_slide('Png 1', png_image)
+ deck.add_pictorial_slide('Png 2', png_image)
+ deck.add_pictorial_slide('Jpg', jpg_image)
+
+ expect(deck.file_types).to eq(%w[png jpg])
+ end
+end
+
+RSpec.describe Powerpoint::Util do
+ let(:dummy_class) do
+ Class.new do
+ include Powerpoint::Util
+ end
+ end
+ let(:dummy) { dummy_class.new }
+
+ it 'converts pixels to points' do
+ expect(dummy.pixle_to_pt(2)).to eq(25_400)
+ end
+
+ it 'raises when required arguments are missing' do
+ expect { dummy.require_arguments([:a, :b], { a: 1 }) }.to raise_error(ArgumentError)
+ end
+
+ it 'does not raise when required arguments are present' do
+ expect { dummy.require_arguments([:a], { a: 1 }) }.not_to raise_error
+ end
+
+ it 'copies media only once when destination already exists' do
+ Dir.mktmpdir do |dir|
+ media_dir = File.join(dir, 'ppt', 'media')
+ FileUtils.mkdir_p(media_dir)
+ source = File.join(dir, 'source.png')
+ destination = File.join(media_dir, 'source.png')
+
+ File.write(source, 'first')
+ dummy.copy_media(dir, source)
+ expect(File.read(destination)).to eq('first')
+
+ File.write(source, 'second')
+ dummy.copy_media(dir, source)
+ expect(File.read(destination)).to eq('first')
+ end
+ end
+
+ it 'renders templates with variables' do
+ Dir.mktmpdir do |dir|
+ output = File.join(dir, 'rendered.xml')
+ dummy.render_view('textual_rel.xml.erb', output, index: 12)
+ expect(File.read(output)).to include('slideLayout12.xml')
+ end
+ end
+
+ it 'reads templates from the gem views path' do
+ template = dummy.read_template('app.xml.erb')
+ expect(template).to include('<%= slides.length %>')
+ end
+
+ it 'merges local variables into a binding' do
+ base_binding = binding
+ merged_binding = dummy.merge_variables(base_binding, answer: 42)
+
+ expect(merged_binding.local_variable_get(:answer)).to eq(42)
+ end
+end
+
+RSpec.describe 'slide helpers and compression' do
+ let(:png_image) { File.expand_path('../samples/images/sample_png.png', __dir__) }
+
+ it 'uses empty default coordinates when image dimensions are unavailable' do
+ slide = Powerpoint::Slide::Pictorial.new(
+ presentation: Powerpoint::Presentation.new,
+ title: 'Missing image',
+ image_path: '/does/not/exist.png',
+ coords: {}
+ )
+
+ expect(slide.coords).to eq({})
+ expect(slide.file_type).to eq('png')
+ end
+
+ it 'compresses pptx directories and excludes macOS metadata files' do
+ Dir.mktmpdir do |dir|
+ source_dir = File.join(dir, 'source')
+ output = File.join(dir, 'archive.pptx')
+
+ FileUtils.mkdir_p(File.join(source_dir, 'nested'))
+ File.write(File.join(source_dir, 'nested', 'file.txt'), 'hello')
+ File.write(File.join(source_dir, '.DS_Store'), 'ignored')
+
+ Powerpoint.compress_pptx(source_dir, output)
+ expect(File.exist?(output)).to be(true)
+
+ zip_entries = Zip::File.open(output) { |zip_file| zip_file.map(&:name) }
+ expect(zip_entries).to include('nested/file.txt')
+ expect(zip_entries).not_to include('.DS_Store')
+ end
+ end
+
+ it 'computes non-empty default coordinates for valid images' do
+ slide = Powerpoint::Slide::TextPicSplit.new(
+ presentation: Powerpoint::Presentation.new,
+ title: 'Split',
+ image_path: png_image,
+ content: ['A']
+ )
+
+ expect(slide.coords).to include(:x, :y, :cx, :cy)
+ expect(slide.coords[:x]).to eq(480 * 12_700)
+ expect(slide.coords[:cy]).to be <= 420 * 12_700
+ end
+
+ it 'centers default images within the widescreen canvas and available height' do
+ slides = [
+ [
+ Powerpoint::Slide::Pictorial.new(
+ presentation: Powerpoint::Presentation.new,
+ title: 'Picture',
+ image_path: png_image,
+ coords: {}
+ ),
+ 420
+ ],
+ [
+ Powerpoint::Slide::DescriptionPic.new(
+ presentation: Powerpoint::Presentation.new,
+ title: 'Description',
+ image_path: png_image,
+ content: []
+ ),
+ 300
+ ]
+ ]
+
+ slides.each do |slide, max_height|
+ expect(slide.coords[:x] + slide.coords[:cx] / 2).to eq(960 * 12_700 / 2)
+ expect(slide.coords[:cy]).to be <= max_height * 12_700
+ end
+ end
+
+ it 'constrains portrait images to the slide content height' do
+ allow(FastImage).to receive(:size).and_return([100, 1_000])
+
+ slides = [
+ [
+ Powerpoint::Slide::Pictorial.new(
+ presentation: Powerpoint::Presentation.new,
+ title: 'Picture',
+ image_path: 'portrait.png',
+ coords: {}
+ ),
+ 420
+ ],
+ [
+ Powerpoint::Slide::TextPicSplit.new(
+ presentation: Powerpoint::Presentation.new,
+ title: 'Split',
+ image_path: 'portrait.png',
+ content: []
+ ),
+ 420
+ ],
+ [
+ Powerpoint::Slide::DescriptionPic.new(
+ presentation: Powerpoint::Presentation.new,
+ title: 'Description',
+ image_path: 'portrait.png',
+ content: []
+ ),
+ 300
+ ]
+ ]
+
+ slides.each do |slide, max_height|
+ expect(slide.coords[:cy]).to eq(max_height * 12_700)
+ end
end
end
diff --git a/template/ppt/slideLayouts/slideLayout1.xml b/template/ppt/slideLayouts/slideLayout1.xml
index 0a295ae2..8e2c486b 100755
--- a/template/ppt/slideLayouts/slideLayout1.xml
+++ b/template/ppt/slideLayouts/slideLayout1.xml
@@ -27,8 +27,8 @@
-
-
+
+
@@ -55,8 +55,8 @@
-
-
+
+
diff --git a/template/ppt/slideLayouts/slideLayout11.xml b/template/ppt/slideLayouts/slideLayout11.xml
index 90973afc..6083284e 100755
--- a/template/ppt/slideLayouts/slideLayout11.xml
+++ b/template/ppt/slideLayouts/slideLayout11.xml
@@ -1,2 +1,2 @@
-Click to edit Master title styleClick to edit Master text stylesSecond levelThird levelFourth levelFifth level7/14/15‹#›
\ No newline at end of file
+Click to edit Master title styleClick to edit Master text stylesSecond levelThird levelFourth levelFifth level7/14/15‹#›
\ No newline at end of file
diff --git a/template/ppt/slideLayouts/slideLayout3.xml b/template/ppt/slideLayouts/slideLayout3.xml
index 0453d176..376af51c 100755
--- a/template/ppt/slideLayouts/slideLayout3.xml
+++ b/template/ppt/slideLayouts/slideLayout3.xml
@@ -1,2 +1,2 @@
-Click to edit Master title styleClick to edit Master text styles7/14/15‹#›
\ No newline at end of file
+Click to edit Master title styleClick to edit Master text styles7/14/15‹#›
\ No newline at end of file
diff --git a/template/ppt/slideLayouts/slideLayout4.xml b/template/ppt/slideLayouts/slideLayout4.xml
index a5528a3f..44b086c9 100755
--- a/template/ppt/slideLayouts/slideLayout4.xml
+++ b/template/ppt/slideLayouts/slideLayout4.xml
@@ -1,2 +1,2 @@
-Click to edit Master title styleClick to edit Master text stylesSecond levelThird levelFourth levelFifth levelClick to edit Master text stylesSecond levelThird levelFourth levelFifth level7/14/15‹#›
\ No newline at end of file
+Click to edit Master title styleClick to edit Master text stylesSecond levelThird levelFourth levelFifth levelClick to edit Master text stylesSecond levelThird levelFourth levelFifth level7/14/15‹#›
\ No newline at end of file
diff --git a/template/ppt/slideLayouts/slideLayout5.xml b/template/ppt/slideLayouts/slideLayout5.xml
index 97c0d08e..b350985c 100755
--- a/template/ppt/slideLayouts/slideLayout5.xml
+++ b/template/ppt/slideLayouts/slideLayout5.xml
@@ -1,2 +1,2 @@
-Click to edit Master title styleClick to edit Master text stylesClick to edit Master text stylesSecond levelThird levelFourth levelFifth levelClick to edit Master text stylesClick to edit Master text stylesSecond levelThird levelFourth levelFifth level7/14/15‹#›
\ No newline at end of file
+Click to edit Master title styleClick to edit Master text stylesClick to edit Master text stylesSecond levelThird levelFourth levelFifth levelClick to edit Master text stylesClick to edit Master text stylesSecond levelThird levelFourth levelFifth level7/14/15‹#›
\ No newline at end of file
diff --git a/template/ppt/slideLayouts/slideLayout8.xml b/template/ppt/slideLayouts/slideLayout8.xml
index 757a586d..4bc59ebc 100755
--- a/template/ppt/slideLayouts/slideLayout8.xml
+++ b/template/ppt/slideLayouts/slideLayout8.xml
@@ -1,2 +1,2 @@
-Click to edit Master title styleClick to edit Master text stylesSecond levelThird levelFourth levelFifth levelClick to edit Master text styles7/14/15‹#›
\ No newline at end of file
+Click to edit Master title styleClick to edit Master text stylesSecond levelThird levelFourth levelFifth levelClick to edit Master text styles7/14/15‹#›
\ No newline at end of file
diff --git a/template/ppt/slideLayouts/slideLayout9.xml b/template/ppt/slideLayouts/slideLayout9.xml
index 76039f15..e3098151 100755
--- a/template/ppt/slideLayouts/slideLayout9.xml
+++ b/template/ppt/slideLayouts/slideLayout9.xml
@@ -1,2 +1,2 @@
-Click to edit Master title styleClick to edit Master text styles7/14/15‹#›
\ No newline at end of file
+Click to edit Master title styleClick to edit Master text styles7/14/15‹#›
\ No newline at end of file
diff --git a/template/ppt/slideMasters/slideMaster1.xml b/template/ppt/slideMasters/slideMaster1.xml
index d25bed4d..018533f1 100755
--- a/template/ppt/slideMasters/slideMaster1.xml
+++ b/template/ppt/slideMasters/slideMaster1.xml
@@ -32,8 +32,8 @@
-
-
+
+
@@ -65,8 +65,8 @@
-
-
+
+
@@ -127,8 +127,8 @@
-
-
+
+
@@ -169,8 +169,8 @@
-
-
+
+
@@ -206,8 +206,8 @@
-
-
+
+