# frozen_string_literal: true require 'digest/sha1' # patterns.rb : Implements axial & radial gradients # # Originally implemented by Wojciech Piekutowski. November, 2009 # Copyright September 2012, Alexander Mankuta. All Rights Reserved. # # This is free software. Please see the LICENSE and COPYING files for details. # module Prawn module Graphics module Patterns GradientStop = Struct.new(:position, :color) Gradient = Struct.new( :type, :apply_transformations, :stops, :from, :to, :r1, :r2 ) # @group Stable API # Sets the fill gradient. # old arguments: # from, to, color1, color2 # or # from, r1, to, r2, color1, color2 # new arguments: # from: [x, y] # to: [x, y] # r1: radius # r2: radius # stops: [color, color, ...] or # { position => color, position => color, ... } # apply_transformations: true # # Examples: # # # draws a horizontal axial gradient that starts at red on the left # # and ends at blue on the right # fill_gradient from: [0, 0], to: [100, 0], stops: ['red', 'blue'] # # # draws a horizontal radial gradient that starts at red, is green # # 80% of the way through, and finishes blue # fill_gradient from: [0, 0], r1: 0, to: [100, 0], r2: 180, # stops: { 0 => 'red', 0.8 => 'green', 1 => 'blue' } # # from and to specify the axis of where the gradient # should be painted. # # r1 and r2, if specified, make a radial gradient with # the starting circle of radius r1 centered at from # and ending at a circle of radius r2 centered at to. # If r1 is not specified, a axial gradient will be drawn. # # stops is an array or hash of stops. Each stop is either just a # string indicating the color, in which case the stops will be evenly # distributed across the gradient, or a hash where the key is # a position between 0 and 1 indicating what distance through the # gradient the color should change, and the value is a color string. # # Option apply_transformations, if set true, will transform the # gradient's co-ordinate space so it matches the current co-ordinate # space of the document. This option will be the default from Prawn v3, # and is default true if you use the new arguments format. # The default for the old arguments format, false, will mean if you # (for example) scale your document by 2 and put a gradient inside, you # will have to manually multiply your co-ordinates by 2 so the gradient # is correctly positioned. def fill_gradient(*args, **kwargs) set_gradient(:fill, *args, **kwargs) end # Sets the stroke gradient. # See fill_gradient for a description of the arguments to this method. def stroke_gradient(*args, **kwargs) set_gradient(:stroke, *args, **kwargs) end private def set_gradient(type, *grad, **kwargs) gradient = parse_gradient_arguments(*grad, **kwargs) patterns = page.resources[:Pattern] ||= {} registry_key = gradient_registry_key gradient unless patterns.key? "SP#{registry_key}" shading = gradient_registry[registry_key] unless shading shading = create_gradient_pattern(gradient) gradient_registry[registry_key] = shading end patterns["SP#{registry_key}"] = shading end operator = case type when :fill 'scn' when :stroke 'SCN' else raise ArgumentError, "unknown type '#{type}'" end set_color_space type, :Pattern renderer.add_content "/SP#{registry_key} #{operator}" end # rubocop: disable Metrics/ParameterLists def parse_gradient_arguments( *arguments, from: nil, to: nil, r1: nil, r2: nil, stops: nil, apply_transformations: nil ) case arguments.length when 0 apply_transformations = true if apply_transformations.nil? when 4 from, to, *stops = arguments when 6 from, r1, to, r2, *stops = arguments else raise ArgumentError, "Unknown type of gradient: #{arguments.inspect}" end if stops.length < 2 raise ArgumentError, 'At least two stops must be specified' end stops = stops.map.with_index do |stop, index| case stop when Array, Hash position, color = stop else position = index / (stops.length.to_f - 1) color = stop end unless (0..1).cover?(position) raise ArgumentError, 'position must be between 0 and 1' end GradientStop.new(position, normalize_color(color)) end if stops.first.position != 0 raise ArgumentError, 'The first stop must have a position of 0' end if stops.last.position != 1 raise ArgumentError, 'The last stop must have a position of 1' end if stops.map { |stop| color_type(stop.color) }.uniq.length > 1 raise ArgumentError, 'All colors must be of the same color space' end Gradient.new( r1 ? :radial : :axial, apply_transformations, stops, from, to, r1, r2 ) end # rubocop: enable Metrics/ParameterLists def gradient_registry_key(gradient) _x1, _y1, x2, y2, transformation = gradient_coordinates(gradient) key = [ gradient.type.to_s, transformation, x2, y2, gradient.r1 || -1, gradient.r2 || -1, gradient.stops.length, gradient.stops.map { |s| [s.position, s.color] } ].flatten Digest::SHA1.hexdigest(key.join(',')) end def gradient_registry @gradient_registry ||= {} end def create_gradient_pattern(gradient) if gradient.apply_transformations.nil? && current_transformation_matrix_with_translation(0, 0) != [1, 0, 0, 1, 0, 0] warn 'Gradients in Prawn 2.x and lower are not correctly positioned '\ 'when a transformation has been made to the document. ' \ "Pass 'apply_transformations: true' to correctly transform the " \ 'gradient, or see ' \ 'https://github.com/prawnpdf/prawn/wiki/Gradient-Transformations ' \ 'for more information.' end shader_stops = gradient.stops.each_cons(2).map do |first, second| ref!( FunctionType: 2, Domain: [0.0, 1.0], C0: first.color, C1: second.color, N: 1.0 ) end # If there's only two stops, we can use the single shader. # Otherwise we stitch the multiple shaders together. shader = if shader_stops.length == 1 shader_stops.first else ref!( FunctionType: 3, # stitching function Domain: [0.0, 1.0], Functions: shader_stops, Bounds: gradient.stops[1..-2].map(&:position), Encode: [0.0, 1.0] * shader_stops.length ) end x1, y1, x2, y2, transformation = gradient_coordinates(gradient) coords = if gradient.type == :axial [0, 0, x2 - x1, y2 - y1] else [0, 0, gradient.r1, x2 - x1, y2 - y1, gradient.r2] end shading = ref!( ShadingType: gradient.type == :axial ? 2 : 3, ColorSpace: color_space(gradient.stops.first.color), Coords: coords, Function: shader, Extend: [true, true] ) ref!( PatternType: 2, # shading pattern Shading: shading, Matrix: transformation ) end def gradient_coordinates(gradient) x1, y1 = map_to_absolute(gradient.from) x2, y2 = map_to_absolute(gradient.to) transformation = if gradient.apply_transformations current_transformation_matrix_with_translation(x1, y1) else [1, 0, 0, 1, x1, y1] end [x1, y1, x2, y2, transformation] end end end end