Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved gradient support #175

Merged
merged 7 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
- `<marker>`

- `<linearGradient>` and `<radialGradient>` are implemented on Prawn 2.2.0+ with attributes `gradientUnits` and
`gradientTransform` (`spreadMethod` and `stop-opacity` are unimplemented.)
`gradientTransform`

- `<switch>` and `<foreignObject>`, although prawn-svg cannot handle any data that is not SVG so `<foreignObject>`
tags are always ignored.
Expand Down
5 changes: 2 additions & 3 deletions lib/prawn-svg.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require 'prawn/svg/calculators/aspect_ratio'
require 'prawn/svg/calculators/document_sizing'
require 'prawn/svg/calculators/pixels'
require 'prawn/svg/transform_utils'
require 'prawn/svg/transform_parser'
require 'prawn/svg/url_loader'
require 'prawn/svg/loaders/data'
Expand All @@ -30,12 +31,10 @@
require 'prawn/svg/ttf'
require 'prawn/svg/font'
require 'prawn/svg/gradients'
require 'prawn/svg/gradient_renderer'
require 'prawn/svg/document'
require 'prawn/svg/state'

require 'prawn/svg/extensions/additional_gradient_transforms'
Prawn::Document.prepend Prawn::SVG::Extensions::AdditionalGradientTransforms

module Prawn
Svg = SVG # backwards compatibility
end
Expand Down
2 changes: 1 addition & 1 deletion lib/prawn/svg/attributes/transform.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module Prawn::SVG::Attributes::Transform
def parse_transform_attribute_and_call
return unless (transform = attributes['transform'])

matrix = parse_transform_attribute(transform)
matrix = matrix_for_pdf(parse_transform_attribute(transform))
add_call_and_enter 'transformation_matrix', *matrix unless matrix == [1, 0, 0, 1, 0, 0]
end
end
7 changes: 2 additions & 5 deletions lib/prawn/svg/elements/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,8 @@ def apply_colors
add_call "#{type}_color", result.value
true
when Prawn::SVG::Elements::Gradient
arguments = result.gradient_arguments(self)
if arguments
add_call "#{type}_gradient", **arguments
true
end
add_call 'svg:render_gradient', type.to_sym, **result.gradient_arguments(self)
true
end
end

Expand Down
163 changes: 96 additions & 67 deletions lib/prawn/svg/elements/gradient.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
attr_reader :parent_gradient
attr_reader :x1, :y1, :x2, :y2, :cx, :cy, :fx, :fy, :radius, :units, :stops, :transform_matrix
attr_reader :x1, :y1, :x2, :y2, :cx, :cy, :r, :fx, :fy, :fr, :units, :stops, :transform_matrix, :wrap

TAG_NAME_TO_TYPE = {
'linearGradient' => :linear,
Expand All @@ -12,6 +12,9 @@ def parse
raise SkipElementQuietly if attributes['id'].nil?

@parent_gradient = document.gradients[href_attribute[1..]] if href_attribute && href_attribute[0] == '#'
@transform_matrix = Matrix.identity(3)
@wrap = :pad

assert_compatible_prawn_version
load_gradient_configuration
load_coordinates
Expand All @@ -23,14 +26,29 @@ def parse
end

def gradient_arguments(element)
# Passing in a transformation matrix to the apply_transformations option is supported
# by a monkey patch installed by prawn-svg. Prawn only sees this as a truthy variable.
#
# See Prawn::SVG::Extensions::AdditionalGradientTransforms for details.
base_arguments = { stops: stops, apply_transformations: transform_matrix || true }

arguments = specific_gradient_arguments(element)
arguments&.merge(base_arguments)
bbox = element.bounding_box

if type == :radial
{
from: [fx, fy],
r1: fr,
to: [cx, cy],
r2: r,
stops: stops,
matrix: matrix_for_bounding_box(*bbox),
wrap: wrap,
bounding_box: bbox
}
else
{
from: [x1, y1],
to: [x2, y2],
stops: stops,
matrix: matrix_for_bounding_box(*bbox),
wrap: wrap,
bounding_box: bbox
}
end
end

def derive_attribute(name)
Expand All @@ -39,45 +57,27 @@ def derive_attribute(name)

private

def specific_gradient_arguments(element)
def matrix_for_bounding_box(bounding_x1, bounding_y1, bounding_x2, bounding_y2)
if units == :bounding_box
bounding_x1, bounding_y1, bounding_x2, bounding_y2 = element.bounding_box
return if bounding_y2.nil?

width = bounding_x2 - bounding_x1
height = bounding_y1 - bounding_y2
end

case [type, units]
when [:linear, :bounding_box]
from = [bounding_x1 + (width * x1), bounding_y1 - (height * y1)]
to = [bounding_x1 + (width * x2), bounding_y1 - (height * y2)]

{ from: from, to: to }

when [:linear, :user_space]
{ from: [x1, y1], to: [x2, y2] }

when [:radial, :bounding_box]
center = [bounding_x1 + (width * cx), bounding_y1 - (height * cy)]
focus = [bounding_x1 + (width * fx), bounding_y1 - (height * fy)]

# NOTE: Chrome, at least, implements radial bounding box radiuses as
# having separate X and Y components, so in bounding box mode their
# gradients come out as ovals instead of circles. PDF radial shading
# doesn't have the option to do this, and it's confusing why the
# Chrome user space gradients don't apply the same logic anyway.
hypot = Math.sqrt((width * width) + (height * height))
{ from: focus, r1: 0, to: center, r2: radius * hypot }

when [:radial, :user_space]
{ from: [fx, fy], r1: 0, to: [cx, cy], r2: radius }
bounding_box_to_user_space_matrix = Matrix[
[width, 0.0, bounding_x1],
[0.0, height, document.sizing.output_height - bounding_y1],
[0.0, 0.0, 1.0]
]

svg_to_pdf_matrix * bounding_box_to_user_space_matrix * transform_matrix
else
raise 'unexpected type/unit system'
svg_to_pdf_matrix * transform_matrix
end
end

def svg_to_pdf_matrix
@svg_to_pdf_matrix ||= Matrix[[1.0, 0.0, 0.0], [0.0, -1.0, document.sizing.output_height], [0.0, 0.0, 1.0]]
end

def type
TAG_NAME_TO_TYPE.fetch(name)
end
Expand All @@ -92,41 +92,44 @@ def load_gradient_configuration
@units = derive_attribute('gradientUnits') == 'userSpaceOnUse' ? :user_space : :bounding_box

if (transform = derive_attribute('gradientTransform'))
@transform_matrix = parse_transform_attribute(transform)
@transform_matrix = parse_transform_attribute(transform, space: :svg)
end

if (spread_method = derive_attribute('spreadMethod')) && spread_method != 'pad'
warnings << "prawn-svg only currently supports the 'pad' spreadMethod attribute value"
if (spread_method = derive_attribute('spreadMethod'))
spread_method = spread_method.to_sym
@wrap = [:pad, :reflect, :repeat].include?(spread_method) ? spread_method : :pad
end
end

def load_coordinates
case [type, units]
when [:linear, :bounding_box]
@x1 = parse_zero_to_one(derive_attribute('x1'), 0)
@y1 = parse_zero_to_one(derive_attribute('y1'), 0)
@x2 = parse_zero_to_one(derive_attribute('x2'), 1)
@y2 = parse_zero_to_one(derive_attribute('y2'), 0)
@x1 = percentage_or_proportion(derive_attribute('x1'), 0.0)
@y1 = percentage_or_proportion(derive_attribute('y1'), 0.0)
@x2 = percentage_or_proportion(derive_attribute('x2'), 1.0)
@y2 = percentage_or_proportion(derive_attribute('y2'), 0.0)

when [:linear, :user_space]
@x1 = x(derive_attribute('x1'))
@y1 = y(derive_attribute('y1'))
@y1 = y_pixels(derive_attribute('y1'))
@x2 = x(derive_attribute('x2'))
@y2 = y(derive_attribute('y2'))
@y2 = y_pixels(derive_attribute('y2'))

when [:radial, :bounding_box]
@cx = parse_zero_to_one(derive_attribute('cx'), 0.5)
@cy = parse_zero_to_one(derive_attribute('cy'), 0.5)
@fx = parse_zero_to_one(derive_attribute('fx'), cx)
@fy = parse_zero_to_one(derive_attribute('fy'), cy)
@radius = parse_zero_to_one(derive_attribute('r'), 0.5)
@cx = percentage_or_proportion(derive_attribute('cx'), 0.5)
@cy = percentage_or_proportion(derive_attribute('cy'), 0.5)
@r = percentage_or_proportion(derive_attribute('r'), 0.5)
@fx = percentage_or_proportion(derive_attribute('fx'), cx)
@fy = percentage_or_proportion(derive_attribute('fy'), cy)
@fr = percentage_or_proportion(derive_attribute('fr'), 0.0)

when [:radial, :user_space]
@cx = x(derive_attribute('cx') || '50%')
@cy = y(derive_attribute('cy') || '50%')
@cy = y_pixels(derive_attribute('cy') || '50%')
@r = pixels(derive_attribute('r') || '50%')
@fx = x(derive_attribute('fx') || derive_attribute('cx'))
@fy = y(derive_attribute('fy') || derive_attribute('cy'))
@radius = pixels(derive_attribute('r') || '50%')
@fy = y_pixels(derive_attribute('fy') || derive_attribute('cy'))
@fr = pixels(derive_attribute('fr') || '0%')

else
raise 'unexpected type/unit system'
Expand All @@ -142,14 +145,14 @@ def load_stops
element.name == 'stop' && element.attributes['offset']
end

@stops = stop_elements.each.with_object([]) do |child, result|
offset = parse_zero_to_one(child.attributes['offset'])
@stops = stop_elements.each_with_object([]) do |child, result|
offset = percentage_or_proportion(child.attributes['offset']).clamp(0.0, 1.0)

# Offsets must be strictly increasing (SVG 13.2.4)
offset = result.last.first if result.last && result.last.first > offset
offset = result.last[:offset] if result.last && result.last[:offset] > offset

if (color = Prawn::SVG::Color.css_color_to_prawn_color(child.properties.stop_color))
result << [offset, color]
result << { offset: offset, color: color, opacity: parse_opacity(child.properties.stop_opacity) }
end
end

Expand All @@ -160,17 +163,43 @@ def load_stops

@stops = parent_gradient.stops
else
stops.unshift([0, stops.first.last]) if stops.first.first.positive?
stops.push([1, stops.last.last]) if stops.last.first < 1
if stops.first[:offset].positive?
start_stop = stops.first.dup
start_stop[:offset] = 0
stops.unshift(start_stop)
end

if stops.last[:offset] < 1
end_stop = stops.last.dup
end_stop[:offset] = 1
stops.push(end_stop)
end
end
end

def parse_zero_to_one(string, default = 0)
def percentage_or_proportion(string, default = 0)
string = string.to_s.strip
return default if string == ''
percentage = false

if string[-1] == '%'
percentage = true
string = string[0..-2]
end

value = Float(string, exception: false)
return default unless value

if percentage
value / 100.0
else
value
end
end

def parse_opacity(string)
value = Float(string, exception: false)
return 1.0 unless value

value = string.to_f
value /= 100.0 if string[-1..] == '%'
[0.0, value, 1.0].sort[1]
value.clamp(0.0, 1.0)
end
end
23 changes: 0 additions & 23 deletions lib/prawn/svg/extensions/additional_gradient_transforms.rb

This file was deleted.

Loading
Loading