From 7a53f68393f0b62bcf3c7fa77e35698eb77e3d40 Mon Sep 17 00:00:00 2001 From: Andy Meneely Date: Sun, 1 Feb 2015 23:04:39 -0500 Subject: [PATCH] Implementing mask feature for images Fixes #32. --- CHANGELOG.md | 4 ++- lib/squib/api/image.rb | 10 +++--- lib/squib/constants.rb | 2 ++ lib/squib/graphics/image.rb | 24 +++++++++---- samples/glass-heart.svg | 52 +++++++++++++++++++++++++++ samples/load_images.rb | 14 ++++++++ spec/api/api_image_spec.rb | 4 +-- spec/data/samples/load_images.rb.txt | 26 ++++++++++++++ spec/graphics/graphics_images_spec.rb | 11 +++--- 9 files changed, 127 insertions(+), 20 deletions(-) create mode 100644 samples/glass-heart.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index bda7923..e0ee6d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # Squib CHANGELOG ## v0.3.0a -* Gradients! Can now specify linear or radial gradients anywhere you specify colors. See README for more details. +* Masks! The `png` and `svg` commands can be used as if they are a mask, so you can color the icon with any color you like. Can be handy for switching to black-and-white, or for reusing the same image but different colors across cards. +* Gradients! Can now specify linear or radial gradients anywhere you specify colors. See README and `samples/gradients.rb` for more details. +* Special thanks to Shalom Craimer for the idea and proof-of-concept on the above two features! * Added new sample table for color constants in `samples/colors.rb` ## v0.2.0 diff --git a/lib/squib/api/image.rb b/lib/squib/api/image.rb index 63b223d..1b11ebb 100644 --- a/lib/squib/api/image.rb +++ b/lib/squib/api/image.rb @@ -17,16 +17,17 @@ module Squib # @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} + # @option opts mask [String] (nil) If specified, the image will be used as a mask for the given color/gradient. Transparent pixels are ignored, opaque pixels are the given color. # @return [nil] Returns nil # @api public def png(opts = {}) - opts = needs(opts, [:range, :files, :x, :y, :width, :height, :alpha, :layout, :blend, :angle]) + opts = needs(opts, [:range, :files, :x, :y, :width, :height, :alpha, :layout, :blend, :angle, :mask]) 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[:width][i], opts[:height][i], - opts[:alpha][i], opts[:blend][i], opts[:angle][i]) + opts[:alpha][i], opts[:blend][i], opts[:angle][i], opts[:mask][i]) bar.increment end end @@ -51,16 +52,17 @@ module Squib # @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} + # @option opts mask [String] (nil) If specified, the image will be used as a mask for the given color/gradient. Transparent pixels are ignored, opaque pixels are the given color. # @return [nil] Returns nil # @api public def svg(opts = {}) - p = needs(opts,[:range, :files, :svgid, :force_svgid, :x, :y, :width, :height, :layout, :alpha, :blend, :angle]) + p = needs(opts,[:range, :files, :svgid, :force_svgid, :x, :y, :width, :height, :layout, :alpha, :blend, :angle, :mask]) 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[:angle][i]) + p[:width][i], p[:height][i], p[:alpha][i], p[:blend][i], p[:angle][i],p[:mask][i]) end bar.increment end diff --git a/lib/squib/constants.rb b/lib/squib/constants.rb index 046b61d..72dcaab 100644 --- a/lib/squib/constants.rb +++ b/lib/squib/constants.rb @@ -25,6 +25,7 @@ module Squib :justify => false, :margin => 75, :markup => false, + :mask => nil, :offset => 1.1, :prefix => 'card_', :progress_bar => false, @@ -95,6 +96,7 @@ module Squib :justify => :justify, :layout => :layout, :markup => :markup, + :mask => :mask, :rect_radius => :radius, :spacing => :spacing, :str => :str, diff --git a/lib/squib/graphics/image.rb b/lib/squib/graphics/image.rb index 1c60553..a7c473e 100644 --- a/lib/squib/graphics/image.rb +++ b/lib/squib/graphics/image.rb @@ -14,8 +14,8 @@ module Squib # :nodoc: # @api private - def png(file, x, y, width, height, alpha, blend, angle) - Squib.logger.debug {"Rendering: #{file} @#{x},#{y} #{width}x#{height}, alpha: #{alpha}, blend: #{blend}, angle: #{angle}"} + def png(file, x, y, width, height, alpha, blend, angle, mask) + Squib.logger.debug {"Rendering: #{file} @#{x},#{y} #{width}x#{height}, alpha: #{alpha}, blend: #{blend}, angle: #{angle}, mask: #{mask}"} return if file.nil? or file.eql? '' png = Squib.cache_load_image(file) use_cairo do |cc| @@ -30,14 +30,19 @@ module Squib cc.translate(-1 * x, -1 * y) cc.set_source(png, x, y) cc.operator = blend unless blend == :none - cc.paint(alpha) + if mask.nil? + cc.paint(alpha) + else + cc.set_source_squibcolor(mask) + cc.mask(png, x, y) + end end end # :nodoc: # @api private - 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}"} + def svg(file, id, x, y, width, height, alpha, blend, angle, mask) + Squib.logger.debug {"Rendering: #{file}, id: #{id} @#{x},#{y} #{width}x#{height}, alpha: #{alpha}, blend: #{blend}, angle: #{angle}, mask: #{mask}"} return if file.nil? or file.eql? '' svg = RSVG::Handle.new_from_file(file) width = svg.width if width == :native @@ -50,9 +55,14 @@ module Squib 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) + if mask.nil? + cc.set_source(tmp, x, y) + cc.paint(alpha) + else + cc.set_source_squibcolor(mask) + cc.mask(tmp, x, y) + end end end diff --git a/samples/glass-heart.svg b/samples/glass-heart.svg new file mode 100644 index 0000000..9950ff3 --- /dev/null +++ b/samples/glass-heart.svg @@ -0,0 +1,52 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/samples/load_images.rb b/samples/load_images.rb index c2a4add..08aa9d2 100644 --- a/samples/load_images.rb +++ b/samples/load_images.rb @@ -32,5 +32,19 @@ Squib::Deck.new(width: 825, height: 1125, cards: 1) do 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 + # Images can also be used as masks instead of being directly painted. + # This is particularly useful for switching directly over to black-and-white for printing + # Or, if you want the same image to be used but with different colors/gradients + svg mask: '#00ff00', + file: 'glass-heart.svg', + x: 500, y: 600, width: 200, height: 200 + svg mask: '(500,1000)(500,800) #333@0.0 #ccc@1.0 ', + file: 'glass-heart.svg', + x: 500, y: 800, width: 200, height: 200 + + # Masks are based on transparency, so this is just a square + png mask: :magenta, file: 'shiny-purse.png', + x: 650, y: 950 + save prefix: 'load_images_', format: :png end diff --git a/spec/api/api_image_spec.rb b/spec/api/api_image_spec.rb index 979a87d..cefdaed 100644 --- a/spec/api/api_image_spec.rb +++ b/spec/api/api_image_spec.rb @@ -7,7 +7,7 @@ describe Squib::Deck, 'images' do it 'calls Card#png, Dir, and progress bar' do card = instance_double(Squib::Card) progress = double(Squib::Progress) - expect(card).to receive(:png).with('foo', 0, 1, :native, :native, 0.5, :overlay, 0.75).once + expect(card).to receive(:png).with('foo', 0, 1, :native, :native, 0.5, :overlay, 0.75, nil).once expect(Dir).to receive(:chdir).with('.').and_yield.once expect(progress).to receive(:start).and_yield(progress).once expect(progress).to receive(:increment).once @@ -23,7 +23,7 @@ describe Squib::Deck, 'images' do it 'calls Card#svg, Dir, and progress bar' do card = instance_double(Squib::Card) progress = double(Squib::Progress) - expect(card).to receive(:svg).with('foo', '#bar', 0, 1, 20, 30, 0.5, :overlay, 0.75).once + expect(card).to receive(:svg).with('foo', '#bar', 0, 1, 20, 30, 0.5, :overlay, 0.75, nil).once expect(Dir).to receive(:chdir).with('.').and_yield.once expect(progress).to receive(:start).and_yield(progress).once expect(progress).to receive(:increment).once diff --git a/spec/data/samples/load_images.rb.txt b/spec/data/samples/load_images.rb.txt index f57dfba..6002c38 100644 --- a/spec/data/samples/load_images.rb.txt +++ b/spec/data/samples/load_images.rb.txt @@ -109,4 +109,30 @@ cairo: translate([-300, -900]) cairo: set_source([MockDouble, 300, 900]) cairo: paint([1.0]) cairo: restore([]) +cairo: scale([0.390625, 0.390625]) +cairo: render_rsvg_handle([RSVG::Handle, nil]) +cairo: save([]) +cairo: translate([500, 600]) +cairo: rotate([0]) +cairo: translate([-500, -600]) +cairo: set_source_color(["#00ff00"]) +cairo: mask([MockDouble, 500, 600]) +cairo: restore([]) +cairo: scale([0.390625, 0.390625]) +cairo: render_rsvg_handle([RSVG::Handle, nil]) +cairo: save([]) +cairo: translate([500, 800]) +cairo: rotate([0]) +cairo: translate([-500, -800]) +cairo: set_source([LinearPattern]) +cairo: mask([MockDouble, 500, 800]) +cairo: restore([]) +cairo: save([]) +cairo: translate([650, 950]) +cairo: rotate([0]) +cairo: translate([-650, -950]) +cairo: set_source([ImageSurface, 650, 950]) +cairo: set_source_color([:magenta]) +cairo: mask([ImageSurface, 650, 950]) +cairo: restore([]) surface: write_to_png(["_output/load_images_0.png"]) diff --git a/spec/graphics/graphics_images_spec.rb b/spec/graphics/graphics_images_spec.rb index 5e04472..0d96969 100644 --- a/spec/graphics/graphics_images_spec.rb +++ b/spec/graphics/graphics_images_spec.rb @@ -26,7 +26,7 @@ describe Squib::Card do card = Squib::Card.new(@deck, 100, 150) # png(file, x, y, alpha, blend, angle) - card.png('foo.png', 37, 38, :native, :native, 0.9, :none, 0.0) + card.png('foo.png', 37, 38, :native, :native, 0.9, :none, 0.0, nil) end it 'sets blend when needed' do @@ -34,9 +34,8 @@ describe Squib::Card do expect(@context).to receive(:operator=).with(:overlay).once card = Squib::Card.new(@deck, 100, 150) - card.png('foo.png', 37, 38, :native, :native, 0.9, :overlay, 0.0) + card.png('foo.png', 37, 38, :native, :native, 0.9, :overlay, 0.0, nil) end - end context '#svg' do @@ -55,7 +54,7 @@ describe Squib::Card do 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) + card.svg('foo.png', 'id', 37, 38, :native, :native, 0.9, :none, 0.0, nil) end it 'sets blend when needed' do @@ -64,7 +63,7 @@ describe Squib::Card do 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) + card.svg('foo.png', nil, 37, 38, :native, :native, 0.9, :overlay, 0.0, nil) end it 'sets width & height when needed' do @@ -74,7 +73,7 @@ describe Squib::Card do 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) + card.svg('foo.png', nil, 37, 38, 200, 300, 0.9, :none, 0.0, nil) end end