# frozen_string_literal: true require 'digest/sha1' module Prawn module Graphics # Implements axial & radial gradients. module Patterns # Gradient color stop. # @private GradientStop = Struct.new(:position, :color) # @private Gradient = Struct.new( :type, :apply_transformations, :stops, :from, :to, :r1, :r2, ) # @group Stable API # Sets the fill gradient. # # @overload fill_gradient(from, to, color1, color2, apply_margin_options: false) # Set an axial (linear) fill gradient. # # @param from [Array(Number, Number)] # Starting point of the gradient. # @param to [Array(Number, Number)] ending point of the gradient. # @param color1 [Color] starting color of the gradient. # @param color2 [Color] ending color of the gradient. # @param apply_transformations [Boolean] (false) # 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 all-keyword version of this method. The default for the # positional arguments version (this one), `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. # @return [void] # # @overload fill_gradient(from, r1, to, r2, color1, color2, apply_margin_options: false) # Set a radial fill gradient. # # @param from [Array(Number, Number)] # Starting point of the gradient. # @param r1 [Number] # Radius of the starting circle of a radial gradient. The circle is # centered at `from`. # @param to [Array(Number, Number)] # Ending point of the gradient. # @param r2 [Number] # Radius of the ending circle of a radial gradient. The circle is # centered at `to`. # @param color1 [Color] # Starting color. # @param color2 [Color] # Ending color. # @param apply_transformations [Boolean] (false) # 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 all-keyword version of this method. The default for the # positional arguments version (this one), `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. # @return [void] # # @overload fill_gradient(from:, to:, r1: nil, r2: nil, stops:, apply_margin_options: true) # Set the fill gradient. # # @example Draw 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: ['ff0000', '0000ff'] # # @example Draw a horizontal radial gradient that starts at red, is green 80% through, and finishes blue # fill_gradient from: [0, 0], r1: 0, to: [100, 0], r2: 180, # stops: { 0 => 'ff0000', 0.8 => '00ff00', 1 => '0000ff' } # # @param from [Array(Number, Number)] # Starting point of the gradient. # @param r1 [Number, nil] # Radius of the starting circle of a radial gradient. The circle is # centered at `from`. If omitted a linear gradient will be produced. # @param to [Array(Number, Number)] # Ending point of the gradient. # @param r2 [Number, nil] # Radius of the ending circle of a radial gradient. The circle is # centered at `to`. # @param stops [Array, Hash{Number => Color}] # Color stops. Each stop is either just a 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. # @param apply_transformations [Boolean] (true) # 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 all-keyword version of this method (this one). 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. # @return [void] def fill_gradient(*args, **kwargs) set_gradient(:fill, *args, **kwargs) end # Sets the stroke gradient. # # @overload fill_gradient(from, to, color1, color2, apply_margin_options: false) # Set an axial (linear) stroke gradient. # # @param from [Array(Number, Number)] # Starting point of the gradient. # @param to [Array(Number, Number)] ending point of the gradient. # @param color1 [Color] starting color of the gradient. # @param color2 [Color] ending color of the gradient. # @param apply_transformations [Boolean] (false) # 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 all-keyword version of this method. The default for the # positional arguments version (this one), `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. # @return [void] # # @overload fill_gradient(from, r1, to, r2, color1, color2, apply_margin_options: false) # Set a radial stroke gradient. # # @param from [Array(Number, Number)] # Starting point of the gradient. # @param r1 [Number] # Radius of the starting circle of a radial gradient. The circle is # centered at `from`. # @param to [Array(Number, Number)] # Ending point of the gradient. # @param r2 [Number] # Radius of the ending circle of a radial gradient. The circle is # centered at `to`. # @param color1 [Color] # Starting color. # @param color2 [Color] # Ending color. # @param apply_transformations [Boolean] (false) # 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 all-keyword version of this method. The default for the # positional arguments version (this one), `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. # @return [void] # # @overload fill_gradient(from:, to:, r1: nil, r2: nil, stops:, apply_margin_options: true) # Set the stroke gradient. # # @example Draw a horizontal axial gradient that starts at red on the left and ends at blue on the right # stroke_gradient from: [0, 0], to: [100, 0], stops: ['ff0000', '0000ff'] # # @example Draw a horizontal radial gradient that starts at red, is green 80% through, and finishes blue # stroke_gradient from: [0, 0], r1: 0, to: [100, 0], r2: 180, # stops: { 0 => 'ff0000', 0.8 => '00ff00', 1 => '0000ff' } # # @param from [Array(Number, Number)] # Starting point of the gradient. # @param r1 [Number, nil] # Radius of the starting circle of a radial gradient. The circle is # centered at `from`. If omitted a linear gradient will be produced. # @param to [Array(Number, Number)] # Ending point of the gradient. # @param r2 [Number, nil] # Radius of the ending circle of a radial gradient. The circle is # centered at `to`. # @param stops [Array, Hash{Number => Color}] # Color stops. Each stop is either just a 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. # @param apply_transformations [Boolean] (true) # 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 all-keyword version of this method (this one). 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. # @return [void] 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 { |stop, index| case stop when Array, Hash position, color = stop else position = index / (Float(stops.length) - 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)) } 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 { |first, second| ref!( FunctionType: 2, Domain: [0.0, 1.0], C0: first.color, C1: second.color, N: 1.0, ) } # 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