From 07a9612e7a9cb4f2ef253748ef5801142240d296 Mon Sep 17 00:00:00 2001 From: Micah Gideon Modell Date: Wed, 9 Sep 2026 20:20:01 -0400 Subject: [PATCH 01/12] Updating to latest RubyZip --- .gitignore | 4 +++- lib/powerpoint/compression.rb | 2 +- mise.toml | 2 ++ powerpoint.gemspec | 8 ++++---- samples/pptx/sample.pptx | Bin 1500269 -> 1499001 bytes 5 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 mise.toml 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/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/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..86d08aed 100644 --- a/powerpoint.gemspec +++ b/powerpoint.gemspec @@ -20,12 +20,12 @@ 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 'pry' - 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 1221afc4ec6600227620ee82b39695566f3e4b56..3da567329657f7a03caae3417f64d87da68c7c11 100644 GIT binary patch delta 4123 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 From 0cb5dfb1c71ef11f750aba044e255f475265b607 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Sep 2026 00:09:40 +0000 Subject: [PATCH 02/12] Initial plan From 5f2ed0639f7d32cd249baca3ad241e003b3520e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Sep 2026 00:13:44 +0000 Subject: [PATCH 03/12] Add comprehensive RSpec coverage and CI workflow Co-authored-by: mgmodell <7279993+mgmodell@users.noreply.github.com> --- .github/workflows/test.yml | 21 ++++ powerpoint.gemspec | 1 + spec/spec_helper.rb | 11 ++ spec/test_spec.rb | 218 +++++++++++++++++++++++++++++++++---- 4 files changed, 231 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 spec/spec_helper.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..6c337759 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,21 @@ +name: Test + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + rspec: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - name: Run test suite + run: bundle exec rspec diff --git a/powerpoint.gemspec b/powerpoint.gemspec index 86d08aed..095d977c 100644 --- a/powerpoint.gemspec +++ b/powerpoint.gemspec @@ -24,6 +24,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rake" spec.add_development_dependency 'rspec', '~> 3.13.2' spec.add_development_dependency 'pry' + spec.add_development_dependency 'simplecov', '~> 0.22' spec.add_dependency 'rubyzip', '~> 3' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..f7cec8d1 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,11 @@ +require 'simplecov' + +SimpleCov.start 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..3022c07c 100644 --- a/spec/test_spec.rb +++ b/spec/test_spec.rb @@ -1,22 +1,200 @@ -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') + + 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') + + pictorial_slide = pptx_entry_contents(output, 'ppt/slides/slide3.xml') + expect(pictorial_slide).to include('') + expect(pictorial_slide).to include('') + + split_slide = pptx_entry_contents(output, 'ppt/slides/slide4.xml') + expect(split_slide).to include('Split') + expect(split_slide).to include('Left text') + + description_slide = pptx_entry_contents(output, 'ppt/slides/slide5.xml') + expect(description_slide).to include('Description') + expect(description_slide).to include('Caption') + + 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('') + 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) end end From 14c076c975bd14c1fbf2dade330ae8a878bc798e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Sep 2026 00:14:36 +0000 Subject: [PATCH 04/12] Harden test workflow token permissions Co-authored-by: mgmodell <7279993+mgmodell@users.noreply.github.com> --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6c337759..5806ea43 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,8 @@ on: jobs: rspec: runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v4 From 52e22b827ed9d0ccdbf08c266f2a09934f5bb630 Mon Sep 17 00:00:00 2001 From: Micah Gideon Modell Date: Mon, 28 Sep 2026 20:58:59 -0400 Subject: [PATCH 05/12] adding coveralls support --- .github/workflows/test.yml | 3 +++ powerpoint.gemspec | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5806ea43..6dfb35e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,7 @@ jobs: permissions: contents: read + environment: test steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 @@ -21,3 +22,5 @@ jobs: bundler-cache: true - name: Run test suite run: bundle exec rspec + env: + COVERALLS_REPO_TOKEN: ${{secrets.COVERALLS_REPO_TOKEN}} diff --git a/powerpoint.gemspec b/powerpoint.gemspec index 095d977c..dfc5dbfa 100644 --- a/powerpoint.gemspec +++ b/powerpoint.gemspec @@ -23,6 +23,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "bundler", "~> 4" spec.add_development_dependency "rake" 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' From f8f6792e4409e7cafd2069640b7e961d847470d8 Mon Sep 17 00:00:00 2001 From: Micah Gideon Modell Date: Mon, 28 Sep 2026 21:04:04 -0400 Subject: [PATCH 06/12] Add coverage badge and remove downloads badge Updated README to include coverage badge and remove downloads badge. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d33fe79f..4f402943 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![version](https://badge.fury.io/rb/powerpoint.svg)](https://badge.fury.io/rb/powerpointk) -[![downloads](https://ruby-gem-downloads-badge.herokuapp.com/powerpoint?type=total&total_label=downloads)](https://ruby-gem-downloads-badge.herokuapp.com/powerpoint?type=total&total_label=downloads) +[![Coverage # Status](https://coveralls.io/repos/mgmodell/powerpoint/badge.svg?branch=master&service=github)](https://coveralls.io/github/mgmodell/powerpoint?branch=master) # 'powerpoint' gem -- for creating PowerPoint Slides in Ruby. From ab27a080ecd4f0d8f148ae00f0be2c5b73fc8342 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Sep 2026 01:45:24 +0000 Subject: [PATCH 07/12] Configure Coveralls during tests Co-authored-by: mgmodell <7279993+mgmodell@users.noreply.github.com> --- .github/workflows/test.yml | 6 ++++++ spec/spec_helper.rb | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6dfb35e3..b05485a6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,4 +23,10 @@ jobs: - 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/spec/spec_helper.rb b/spec/spec_helper.rb index f7cec8d1..693f2887 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,6 @@ -require 'simplecov' +require 'coveralls' -SimpleCov.start do +Coveralls.wear! do add_filter '/spec/' minimum_coverage 90 end From e6765db32973478d8855ed463d42a2c1772acf9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:26:26 +0000 Subject: [PATCH 08/12] Initial plan From 7eb0a3613dde3454aeaa27d0e9e36f25dde34b5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:31:58 +0000 Subject: [PATCH 09/12] Generate readable widescreen PowerPoint slides Co-authored-by: mgmodell <7279993+mgmodell@users.noreply.github.com> --- README.md | 2 + lib/powerpoint/slide/pictorial.rb | 11 ++- lib/powerpoint/slide/picture_description.rb | 2 +- lib/powerpoint/slide/text_picture_split.rb | 11 ++- lib/powerpoint/views/app.xml.erb | 8 +- lib/powerpoint/views/intro_slide.xml.erb | 8 +- lib/powerpoint/views/pictorial_slide.xml.erb | 4 +- .../views/picture_description_slide.xml.erb | 8 +- lib/powerpoint/views/presentation.xml.erb | 2 +- .../views/text_picture_split_slide.xml.erb | 8 +- lib/powerpoint/views/textual_slide.xml.erb | 8 +- spec/test_spec.rb | 95 +++++++++++++++++++ template/ppt/slideLayouts/slideLayout1.xml | 8 +- template/ppt/slideLayouts/slideLayout11.xml | 2 +- template/ppt/slideLayouts/slideLayout3.xml | 2 +- template/ppt/slideLayouts/slideLayout4.xml | 2 +- template/ppt/slideLayouts/slideLayout5.xml | 2 +- template/ppt/slideLayouts/slideLayout8.xml | 2 +- template/ppt/slideLayouts/slideLayout9.xml | 2 +- template/ppt/slideMasters/slideMaster1.xml | 20 ++-- 20 files changed, 157 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 4f402943..c41c1614 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ coords = {x: 124200, y: 3356451, cx: 2895600, cy: 1013460} ## 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/slide/pictorial.rb b/lib/powerpoint/slide/pictorial.rb index 4b8f98bd..3ff73697 100644 --- a/lib/powerpoint/slide/pictorial.rb +++ b/lib/powerpoint/slide/pictorial.rb @@ -28,13 +28,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 diff --git a/lib/powerpoint/slide/picture_description.rb b/lib/powerpoint/slide/picture_description.rb index e53e0dd7..e1c09162 100644 --- a/lib/powerpoint/slide/picture_description.rb +++ b/lib/powerpoint/slide/picture_description.rb @@ -28,7 +28,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) diff --git a/lib/powerpoint/slide/text_picture_split.rb b/lib/powerpoint/slide/text_picture_split.rb index 425fddbd..6b00e346 100644 --- a/lib/powerpoint/slide/text_picture_split.rb +++ b/lib/powerpoint/slide/text_picture_split.rb @@ -28,13 +28,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 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..7eeca45b 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) %> diff --git a/lib/powerpoint/views/pictorial_slide.xml.erb b/lib/powerpoint/views/pictorial_slide.xml.erb index 38c5ad82..a4bfb46f 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) %> diff --git a/lib/powerpoint/views/picture_description_slide.xml.erb b/lib/powerpoint/views/picture_description_slide.xml.erb index f1721803..afd9625c 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) %> 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..d58b59c2 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) %> diff --git a/lib/powerpoint/views/textual_slide.xml.erb b/lib/powerpoint/views/textual_slide.xml.erb index 5bea44e7..679ab4c2 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) %> diff --git a/spec/test_spec.rb b/spec/test_spec.rb index 3022c07c..9d789f22 100644 --- a/spec/test_spec.rb +++ b/spec/test_spec.rb @@ -39,23 +39,28 @@ def pptx_entries(pptx_path) 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"') @@ -64,6 +69,7 @@ def pptx_entries(pptx_path) 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"') @@ -73,6 +79,27 @@ def pptx_entries(pptx_path) 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 'replaces an existing intro slide and keeps it at the beginning' do deck = described_class.new deck.add_textual_slide('Body', ['one']) @@ -196,5 +223,73 @@ def pptx_entries(pptx_path) ) 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 @@ - - + + From 74ed8436c579af92ab73dd6c6c1b8da8a3f2f624 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:57:59 +0000 Subject: [PATCH 10/12] Initial plan From 2c8dfe584e4960305f7f81759477ac328a172314 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:59:58 +0000 Subject: [PATCH 11/12] Add slide transition configuration Co-authored-by: mgmodell <7279993+mgmodell@users.noreply.github.com> --- README.md | 14 ++++++ lib/powerpoint.rb | 1 + lib/powerpoint/presentation.rb | 24 +++++----- lib/powerpoint/slide/intro.rb | 3 +- lib/powerpoint/slide/pictorial.rb | 3 +- lib/powerpoint/slide/picture_description.rb | 3 +- lib/powerpoint/slide/text_picture_split.rb | 3 +- lib/powerpoint/slide/textual.rb | 3 +- lib/powerpoint/slide/transition.rb | 26 +++++++++++ lib/powerpoint/views/intro_slide.xml.erb | 1 + lib/powerpoint/views/pictorial_slide.xml.erb | 1 + .../views/picture_description_slide.xml.erb | 1 + .../views/text_picture_split_slide.xml.erb | 1 + lib/powerpoint/views/textual_slide.xml.erb | 1 + spec/test_spec.rb | 44 +++++++++++++++++++ 15 files changed, 114 insertions(+), 15 deletions(-) create mode 100644 lib/powerpoint/slide/transition.rb diff --git a/README.md b/README.md index c41c1614..f029bc67 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,20 @@ 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 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/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 3ff73697..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 @@ -51,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 e1c09162..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 @@ -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 6b00e346..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 @@ -51,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/intro_slide.xml.erb b/lib/powerpoint/views/intro_slide.xml.erb index 7eeca45b..95d42e86 100644 --- a/lib/powerpoint/views/intro_slide.xml.erb +++ b/lib/powerpoint/views/intro_slide.xml.erb @@ -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 a4bfb46f..403a72a8 100644 --- a/lib/powerpoint/views/pictorial_slide.xml.erb +++ b/lib/powerpoint/views/pictorial_slide.xml.erb @@ -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 afd9625c..f5ed0f11 100644 --- a/lib/powerpoint/views/picture_description_slide.xml.erb +++ b/lib/powerpoint/views/picture_description_slide.xml.erb @@ -112,4 +112,5 @@ + <%= transition_xml %> \ No newline at end of file diff --git a/lib/powerpoint/views/text_picture_split_slide.xml.erb b/lib/powerpoint/views/text_picture_split_slide.xml.erb index d58b59c2..148816c7 100644 --- a/lib/powerpoint/views/text_picture_split_slide.xml.erb +++ b/lib/powerpoint/views/text_picture_split_slide.xml.erb @@ -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 679ab4c2..768c78e0 100644 --- a/lib/powerpoint/views/textual_slide.xml.erb +++ b/lib/powerpoint/views/textual_slide.xml.erb @@ -75,4 +75,5 @@ + <%= transition_xml %> \ No newline at end of file diff --git a/spec/test_spec.rb b/spec/test_spec.rb index 9d789f22..540e1f26 100644 --- a/spec/test_spec.rb +++ b/spec/test_spec.rb @@ -100,6 +100,50 @@ def pptx_entries(pptx_path) 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 'replaces an existing intro slide and keeps it at the beginning' do deck = described_class.new deck.add_textual_slide('Body', ['one']) From 5b0e39e07baf07d3b08bfb51d431ca9ad30be4ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:32:58 +0000 Subject: [PATCH 12/12] Increase transition test coverage Co-authored-by: mgmodell <7279993+mgmodell@users.noreply.github.com> --- spec/test_spec.rb | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/spec/test_spec.rb b/spec/test_spec.rb index 540e1f26..006f3c77 100644 --- a/spec/test_spec.rb +++ b/spec/test_spec.rb @@ -144,6 +144,42 @@ def pptx_entries(pptx_path) 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'])