1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
|
# 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' }
#
# <tt>from</tt> and <tt>to</tt> specify the axis of where the gradient
# should be painted.
#
# <tt>r1</tt> and <tt>r2</tt>, if specified, make a radial gradient with
# the starting circle of radius <tt>r1</tt> centered at <tt>from</tt>
# and ending at a circle of radius <tt>r2</tt> centered at <tt>to</tt>.
# If <tt>r1</tt> is not specified, a axial gradient will be drawn.
#
# <tt>stops</tt> 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 <tt>apply_transformations</tt>, 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
|