From d4f0105785f54af1af65101d2981acf426d5eaa4 Mon Sep 17 00:00:00 2001 From: Andy Meneely Date: Mon, 3 Nov 2014 22:47:06 -0500 Subject: [PATCH] Image rotation support for png and svg commands With unit tests and other refactorings --- lib/squib/api/image.rb | 11 ++-- lib/squib/graphics/image.rb | 13 ++++- samples/load_images.rb | 5 ++ spec/graphics/graphics_images_spec.rb | 79 +++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 spec/graphics/graphics_images_spec.rb diff --git a/lib/squib/api/image.rb b/lib/squib/api/image.rb index f2c073e..52d1533 100644 --- a/lib/squib/api/image.rb +++ b/lib/squib/api/image.rb @@ -15,14 +15,16 @@ module Squib # @option opts layout [String, Symbol] (nil) entry in the layout to use as defaults for this command. See {file:README.md#Custom_Layouts Custom Layouts}. Supports Arrays, see {file:README.md#Arrays_and_Singleton_Expansion Arrays and Singleon Expansion} # @option opts alpha [Decimal] (1.0) the alpha-transparency percentage used to blend this image. Supports Arrays, see {file:README.md#Arrays_and_Singleton_Expansion Arrays and Singleon Expansion} # @option opts blend [:none, :multiply, :screen, :overlay, :darken, :lighten, :color_dodge, :color_burn, :hard_light, :soft_light, :difference, :exclusion, :hsl_hue, :hsl_saturation, :hsl_color, :hsl_luminosity] (:none) the composite blend operator used when applying this image. See Blend Modes at http://cairographics.org/operators. Supports Arrays, see {file:README.md#Arrays_and_Singleton_Expansion Arrays and Singleon Expansion} + # @option opts angle [FixNum] (0) Rotation of the in radians. Note that this rotates around the upper-left corner, making the placement of x-y coordinates slightly tricky. Supports Arrays, see {file:README.md#Arrays_and_Singleton_Expansion Arrays and Singleon Expansion} # @return [nil] Returns nil # @api public def png(opts = {}) - opts = needs(opts, [:range, :files, :x, :y, :alpha, :layout, :blend]) + opts = needs(opts, [:range, :files, :x, :y, :alpha, :layout, :blend, :angle]) Dir.chdir(@img_dir) do @progress_bar.start('Loading PNG(s)', opts[:range].size) do |bar| opts[:range].each do |i| - @cards[i].png(opts[:file][i], opts[:x][i], opts[:y][i], opts[:alpha][i], opts[:blend][i]) + @cards[i].png(opts[:file][i], opts[:x][i], opts[:y][i], + opts[:alpha][i], opts[:blend][i], opts[:angle][i]) bar.increment end end @@ -46,16 +48,17 @@ module Squib # @option opts layout [String, Symbol] (nil) entry in the layout to use as defaults for this command. See {file:README.md#Custom_Layouts Custom Layouts}. Supports Arrays, see {file:README.md#Arrays_and_Singleton_Expansion Arrays and Singleon Expansion} # @option opts alpha [Decimal] (1.0) the alpha-transparency percentage used to blend this image. Supports Arrays, see {file:README.md#Arrays_and_Singleton_Expansion Arrays and Singleon Expansion} # @option opts blend [:none, :multiply, :screen, :overlay, :darken, :lighten, :color_dodge, :color_burn, :hard_light, :soft_light, :difference, :exclusion, :hsl_hue, :hsl_saturation, :hsl_color, :hsl_luminosity] (:none) the composite blend operator used when applying this image. See Blend Modes at http://cairographics.org/operators. Supports Arrays, see {file:README.md#Arrays_and_Singleton_Expansion Arrays and Singleon Expansion} + # @option opts angle [FixNum] (0) Rotation of the in radians. Note that this rotates around the upper-left corner, making the placement of x-y coordinates slightly tricky. Supports Arrays, see {file:README.md#Arrays_and_Singleton_Expansion Arrays and Singleon Expansion} # @return [nil] Returns nil # @api public def svg(opts = {}) - p = needs(opts,[:range, :files, :svgid, :force_svgid, :x, :y, :width, :height, :layout, :alpha, :blend]) + p = needs(opts,[:range, :files, :svgid, :force_svgid, :x, :y, :width, :height, :layout, :alpha, :blend, :angle]) Dir.chdir(@img_dir) do @progress_bar.start('Loading SVG(s)', p[:range].size) do |bar| p[:range].each do |i| unless p[:force_id][i] && p[:id][i].to_s.empty? @cards[i].svg(p[:file][i], p[:id][i], p[:x][i], p[:y][i], - p[:width][i], p[:height][i], p[:alpha][i], p[:blend][i]) + p[:width][i], p[:height][i], p[:alpha][i], p[:blend][i], p[:angle][i]) end bar.increment end diff --git a/lib/squib/graphics/image.rb b/lib/squib/graphics/image.rb index ef96aae..9f9a813 100644 --- a/lib/squib/graphics/image.rb +++ b/lib/squib/graphics/image.rb @@ -14,10 +14,14 @@ module Squib # :nodoc: # @api private - def png(file, x, y, alpha, blend) + def png(file, x, y, alpha, blend, angle) + Squib.logger.debug {"Rendering: #{file} @#{x},#{y} #{width}x#{height}, alpha: #{alpha}, blend: #{blend}, angle: #{angle}"} return if file.nil? or file.eql? '' png = Squib.cache_load_image(file) use_cairo do |cc| + cc.translate(x, y) + cc.rotate(angle) + cc.translate(-1 * x, -1 * y) cc.set_source(png, x, y) cc.operator = blend unless blend == :none cc.paint(alpha) @@ -26,8 +30,8 @@ module Squib # :nodoc: # @api private - def svg(file, id, x, y, width, height, alpha, blend) - Squib.logger.debug {"Rendering: #{file}, #{id} #{x}, #{y}, #{width}, #{height}, #{alpha}, #{blend}"} + def svg(file, id, x, y, width, height, alpha, blend, angle) + Squib.logger.debug {"Rendering: #{file}, id: #{id} @#{x},#{y} #{width}x#{height}, alpha: #{alpha}, blend: #{blend}, angle: #{angle}"} return if file.nil? or file.eql? '' svg = RSVG::Handle.new_from_file(file) width = svg.width if width == :native @@ -37,6 +41,9 @@ module Squib tmp_cc.scale(width.to_f / svg.width.to_f, height.to_f / svg.height.to_f) tmp_cc.render_rsvg_handle(svg, id) use_cairo do |cc| + cc.translate(x, y) + cc.rotate(angle) + cc.translate(-1 * x, -1 * y) cc.set_source(tmp, x, y) cc.operator = blend unless blend == :none cc.paint(alpha) diff --git a/samples/load_images.rb b/samples/load_images.rb index 12add04..630b065 100644 --- a/samples/load_images.rb +++ b/samples/load_images.rb @@ -25,5 +25,10 @@ Squib::Deck.new(width: 825, height: 1125, cards: 1) do png file: 'ball.png', x: 50, y: 700 png file: 'grit.png', x: 70, y: 750, blend: :color_burn, alpha: 0.75 + # Images can be rotated around their upper-left corner + png file: 'shiny-purse.png', x: 300, y: 700, angle: 0.0 # default (no rotate) + png file: 'shiny-purse.png', x: 300, y: 800, angle: Math::PI / 4 + svg file: 'spanner.svg', x: 300, y: 900, angle: Math::PI / 2 - 0.1 + save prefix: 'load_images_', format: :png end diff --git a/spec/graphics/graphics_images_spec.rb b/spec/graphics/graphics_images_spec.rb new file mode 100644 index 0000000..867e25f --- /dev/null +++ b/spec/graphics/graphics_images_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' +require 'squib' + +describe Squib::Card do + + before(:each) do + @deck = double(Squib::Deck) + @context = double(Cairo::Context) + @svg = double(RSVG::Handle) + allow(Cairo::Context).to receive(:new).and_return(@context) + allow(Cairo::ImageSurface).to receive(:from_png).and_return(nil) + allow(Cairo::ImageSurface).to receive(:new).and_return(nil) + allow(RSVG::Handle).to receive(:new_from_file).and_return(@svg) + end + + context '#png' do + it 'makes all the expected calls on a smoke test' do + expect(@context).to receive(:save).once + expect(@context).to receive(:translate).with(-37, -38).once + expect(@context).to receive(:rotate).with(0.0).once + expect(@context).to receive(:translate).with(37, 38).once + expect(@context).to receive(:set_source).with(nil, 37, 38).once + expect(@context).to receive(:paint).with(0.9).once + expect(@context).to receive(:restore).once + + card = Squib::Card.new(@deck, 100, 150) + # png(file, x, y, alpha, blend, angle) + card.png('foo.png', 37, 38, 0.9, :none, 0.0) + end + + it 'sets blend when needed' do + @context.as_null_object + expect(@context).to receive(:operator=).with(:overlay).once + + card = Squib::Card.new(@deck, 100, 150) + card.png('foo.png', 37, 38, 0.9, :overlay, 0.0) + end + end + + context '#svg' do + it 'makes all the expected calls on a smoke test' do + expect(@svg).to receive(:width).and_return(100).twice + expect(@svg).to receive(:height).and_return(100).twice + expect(@context).to receive(:save).once + expect(@context).to receive(:translate).with(-37, -38).once + expect(@context).to receive(:rotate).with(0.0).once + expect(@context).to receive(:translate).with(37, 38).once + expect(@context).to receive(:scale).with(1.0, 1.0).once + expect(@context).to receive(:render_rsvg_handle).with(@svg, 'id').once + expect(@context).to receive(:set_source).with(nil, 37, 38).once + expect(@context).to receive(:paint).with(0.9).once + expect(@context).to receive(:restore).once + + card = Squib::Card.new(@deck, 100, 150) + # svg(file, id, x, y, width, height, alpha, blend, angle) + card.svg('foo.png', 'id', 37, 38, :native, :native, 0.9, :none, 0.0) + end + + it 'sets blend when needed' do + @context.as_null_object + @svg.as_null_object + expect(@context).to receive(:operator=).with(:overlay).once + + card = Squib::Card.new(@deck, 100, 150) + card.svg('foo.png', nil, 37, 38, :native, :native, 0.9, :overlay, 0.0) + end + + it 'sets width & height when needed' do + @context.as_null_object + expect(@svg).to receive(:width).and_return(100).once + expect(@svg).to receive(:height).and_return(100).once + expect(@context).to receive(:scale).with(2.0, 3.0).once + + card = Squib::Card.new(@deck, 100, 150) + card.svg('foo.png', nil, 37, 38, 200, 300, 0.9, :none, 0.0) + end + end + +end