Skip to content

Commit

Permalink
Implement gradientTransform on <linearGradient>
Browse files Browse the repository at this point in the history
Also implements the missing skewX and skewY transforms to normal
transforms.

Requested on #112
  • Loading branch information
Roger Nesbitt committed Aug 24, 2019
1 parent 04ef35b commit c0420fa
Show file tree
Hide file tree
Showing 13 changed files with 302 additions and 90 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre

- `<marker>`

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

- `<switch>` and `<foreignObject>`, although prawn-svg cannot handle any data that is not SVG so `<foreignObject>`
tags are always ignored.
Expand All @@ -91,7 +91,7 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre

- the <tt>preserveAspectRatio</tt> attribute on <tt>&lt;svg&gt;</tt>, <tt>&lt;image&gt;</tt> and `<marker>` elements

- transform methods: <tt>translate()</tt>, <tt>rotate()</tt>, <tt>scale()</tt>, <tt>matrix()</tt>
- transform methods: `translate`, `translateX`, `translateY`, `rotate`, `scale`, `skewX`, `skewY`, `matrix`

- colors: HTML standard names, <tt>#xxx</tt>, <tt>#xxxxxx</tt>, <tt>rgb(1, 2, 3)</tt>, <tt>rgb(1%, 2%, 3%)</tt>

Expand Down
4 changes: 4 additions & 0 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_parser'
require 'prawn/svg/url_loader'
require 'prawn/svg/loaders/data'
require 'prawn/svg/loaders/file'
Expand All @@ -29,6 +30,9 @@
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
46 changes: 2 additions & 44 deletions lib/prawn/svg/attributes/transform.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,7 @@ module Prawn::SVG::Attributes::Transform
def parse_transform_attribute_and_call
return unless transform = attributes['transform']

parse_css_method_calls(transform).each do |name, arguments|
case name
when 'translate'
x, y = arguments
add_call_and_enter name, x_pixels(x.to_f), -y_pixels(y.to_f)

when 'rotate'
r, x, y = arguments.collect {|a| a.to_f}
case arguments.length
when 1
add_call_and_enter name, -r, :origin => [0, y('0')]
when 3
add_call_and_enter name, -r, :origin => [x(x), y(y)]
else
warnings << "transform 'rotate' must have either one or three arguments"
end

when 'scale'
x_scale = arguments[0].to_f
y_scale = (arguments[1] || x_scale).to_f
add_call_and_enter "transformation_matrix", x_scale, 0, 0, y_scale, 0, 0

when 'matrix'
if arguments.length != 6
warnings << "transform 'matrix' must have six arguments"
else
a, b, c, d, e, f = arguments.collect {|argument| argument.to_f}
add_call_and_enter "transformation_matrix", a, -b, -c, d, x_pixels(e), -y_pixels(f)
end

else
warnings << "Unknown transformation '#{name}'; ignoring"
end
end
end

private

def parse_css_method_calls(string)
string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
name, argument_string = call
arguments = argument_string.strip.split(/\s*[,\s]\s*/)
[name, arguments]
end
matrix = parse_transform_attribute(transform)
add_call_and_enter "transformation_matrix", *matrix unless matrix == [1, 0, 0, 1, 0, 0]
end
end
2 changes: 2 additions & 0 deletions lib/prawn/svg/elements/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class Prawn::SVG::Elements::Base
include Prawn::SVG::Attributes::Stroke
include Prawn::SVG::Attributes::Space

include Prawn::SVG::TransformParser

PAINT_TYPES = %w(fill stroke)
COMMA_WSP_REGEXP = Prawn::SVG::Elements::COMMA_WSP_REGEXP
SVG_NAMESPACE = "http://www.w3.org/2000/svg"
Expand Down
11 changes: 6 additions & 5 deletions lib/prawn/svg/elements/gradient.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ def gradient_arguments(element)
to = [@x2, @y2]
end

{from: from, to: to, stops: @stops}
# 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.
{from: from, to: to, stops: @stops, apply_transformations: @transform_matrix || true}
end

private
Expand All @@ -51,10 +55,7 @@ def load_gradient_configuration
@units = attributes["gradientUnits"] == 'userSpaceOnUse' ? :user_space : :bounding_box

if transform = attributes["gradientTransform"]
matrix = transform.split(COMMA_WSP_REGEXP).map(&:to_f)
if matrix != [1, 0, 0, 1, 0, 0]
raise SkipElementError, "prawn-svg does not yet support gradients with a non-identity gradientTransform attribute"
end
@transform_matrix = parse_transform_attribute(transform)
end

if (spread_method = attributes['spreadMethod']) && spread_method != "pad"
Expand Down
23 changes: 23 additions & 0 deletions lib/prawn/svg/extensions/additional_gradient_transforms.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module Prawn::SVG::Extensions
module AdditionalGradientTransforms
def gradient_coordinates(gradient)
# As of Prawn 2.2.0, apply_transformations is used as purely a boolean.
#
# Here we're using it to optionally pass in a 6-tuple transformation matrix that gets applied to the
# gradient. This should be added to Prawn properly, and then this monkey patch will not be necessary.

if gradient.apply_transformations.is_a?(Array)
x1, y1, x2, y2, transformation = super
a, b, c, d, e, f = transformation
na, nb, nc, nd, ne, nf = gradient.apply_transformations

matrix = Matrix[[a, c, e], [b, d, f], [0, 0, 1]] * Matrix[[na, nc, ne], [nb, nd, nf], [0, 0, 1]]
new_transformation = matrix.to_a[0..1].transpose.flatten

[x1, y1, x2, y2, new_transformation]
else
super
end
end
end
end
72 changes: 72 additions & 0 deletions lib/prawn/svg/transform_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
module Prawn::SVG::TransformParser
def parse_transform_attribute(transform)
matrix = Matrix.identity(3)

parse_css_method_calls(transform).each do |name, arguments|
case name
when 'translate'
x, y = arguments
matrix *= Matrix[[1, 0, x_pixels(x.to_f)], [0, 1, -y_pixels(y.to_f)], [0, 0, 1]]

when 'translateX'
x = arguments.first
matrix *= Matrix[[1, 0, x_pixels(x.to_f)], [0, 1, 0], [0, 0, 1]]

when 'translateY'
y = arguments.first
matrix *= Matrix[[1, 0, 0], [0, 1, -y_pixels(y.to_f)], [0, 0, 1]]

when 'rotate'
angle, x, y = arguments.collect { |a| a.to_f }
angle = angle * Math::PI / 180.0

case arguments.length
when 1
matrix *= Matrix[[Math.cos(angle), Math.sin(angle), 0], [-Math.sin(angle), Math.cos(angle), 0], [0, 0, 1]]
when 3
matrix *= Matrix[[1, 0, x_pixels(x.to_f)], [0, 1, -y_pixels(y.to_f)], [0, 0, 1]]
matrix *= Matrix[[Math.cos(angle), Math.sin(angle), 0], [-Math.sin(angle), Math.cos(angle), 0], [0, 0, 1]]
matrix *= Matrix[[1, 0, -x_pixels(x.to_f)], [0, 1, y_pixels(y.to_f)], [0, 0, 1]]
else
warnings << "transform 'rotate' must have either one or three arguments"
end

when 'scale'
x_scale = arguments[0].to_f
y_scale = (arguments[1] || x_scale).to_f
matrix *= Matrix[[x_scale, 0, 0], [0, y_scale, 0], [0, 0, 1]]

when 'skewX'
angle = arguments[0].to_f * Math::PI / 180.0
matrix *= Matrix[[1, -Math.tan(angle), 0], [0, 1, 0], [0, 0, 1]]

when 'skewY'
angle = arguments[0].to_f * Math::PI / 180.0
matrix *= Matrix[[1, 0, 0], [-Math.tan(angle), 1, 0], [0, 0, 1]]

when 'matrix'
if arguments.length != 6
warnings << "transform 'matrix' must have six arguments"
else
a, b, c, d, e, f = arguments.collect { |argument| argument.to_f }
matrix *= Matrix[[a, -c, e], [-b, d, -f], [0, 0, 1]]
end

else
warnings << "Unknown/unsupported transformation '#{name}'; ignoring"
end
end

matrix.to_a[0..1].transpose.flatten
end

private

def parse_css_method_calls(string)
string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
name, argument_string = call
arguments = argument_string.strip.split(/\s*[,\s]\s*/)
[name, arguments]
end
end
end
65 changes: 30 additions & 35 deletions spec/prawn/svg/attributes/transform_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,38 @@ def initialize

let(:element) { TransformTestElement.new }

describe "#parse_transform_attribute_and_call" do
subject { element.send :parse_transform_attribute_and_call }

describe "translate" do
it "handles a missing y argument" do
expect(element).to receive(:add_call_and_enter).with('translate', -5.5, 0)
expect(element).to receive(:x_pixels).with(-5.5).and_return(-5.5)
expect(element).to receive(:y_pixels).with(0.0).and_return(0.0)

element.attributes['transform'] = 'translate(-5.5)'
subject
end
subject { element.send :parse_transform_attribute_and_call }

context "when a non-identity matrix is requested" do
let(:transform) { 'translate(-5.5)' }

it "passes the transform and executes the returned matrix" do
expect(element).to receive(:parse_transform_attribute).with(transform).and_return([1, 2, 3, 4, 5, 6])
expect(element).to receive(:add_call_and_enter).with('transformation_matrix', 1, 2, 3, 4, 5, 6)

element.attributes['transform'] = transform
subject
end
end

context "when an identity matrix is requested" do
let(:transform) { 'translate(0)' }

it "does not execute any commands" do
expect(element).to receive(:parse_transform_attribute).with(transform).and_return([1, 0, 0, 1, 0, 0])
expect(element).not_to receive(:add_call_and_enter)

element.attributes['transform'] = transform
subject
end
end

context "when transform is blank" do
it "does nothing" do
expect(element).not_to receive(:parse_transform_attribute)
expect(element).not_to receive(:add_call_and_enter)

describe "rotate" do
it "handles a single angle argument" do
expect(element).to receive(:add_call_and_enter).with('rotate', -5.5, :origin => [0, 0])
expect(element).to receive(:y).with('0').and_return(0)

element.attributes['transform'] = 'rotate(5.5)'
subject
end

it "handles three arguments" do
expect(element).to receive(:add_call_and_enter).with('rotate', -5.5, :origin => [1.0, 2.0])
expect(element).to receive(:x).with(1.0).and_return(1.0)
expect(element).to receive(:y).with(2.0).and_return(2.0)

element.attributes['transform'] = 'rotate(5.5 1 2)'
subject
end

it "does nothing and warns if two arguments" do
expect(element).to receive(:warnings).and_return([])
element.attributes['transform'] = 'rotate(5.5 1)'
subject
end
subject
end
end
end
4 changes: 2 additions & 2 deletions spec/prawn/svg/elements/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@
describe "applying calls from the standard attributes" do
let(:svg) do
<<-SVG
<something transform="rotate(90)" fill-opacity="0.5" fill="red" stroke="blue" stroke-width="5"/>
<something transform="scale(2)" fill-opacity="0.5" fill="red" stroke="blue" stroke-width="5"/>
SVG
end

it "appends the relevant calls" do
element.process
expect(element.base_calls).to eq [
["rotate", [-90.0, {origin: [0, 600.0]}], [
["transformation_matrix", [2, 0, 0, 2, 0, 0], [
["transparent", [0.5, 1], [
["fill_color", ["ff0000"], []],
["stroke_color", ["0000ff"], []],
Expand Down
28 changes: 26 additions & 2 deletions spec/prawn/svg/elements/gradient_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
expect(arguments).to eq(
from: [100.0, 100.0],
to: [120.0, 0.0],
stops: [[0, "ff0000"], [0.25, "ff0000"], [0.5, "ffffff"], [0.75, "0000ff"], [1, "0000ff"]]
stops: [[0, "ff0000"], [0.25, "ff0000"], [0.5, "ffffff"], [0.75, "0000ff"], [1, "0000ff"]],
apply_transformations: true,
)
end

Expand All @@ -54,7 +55,30 @@
expect(arguments).to eq(
from: [100.0, 100.0],
to: [200.0, 0.0],
stops: [[0, "ff0000"], [1, "0000ff"]]
stops: [[0, "ff0000"], [1, "0000ff"]],
apply_transformations: true,
)
end
end

context "when gradientTransform is specified" do
let(:svg) do
<<-SVG
<linearGradient id="flag" gradientTransform="translateX(10) scale(2)" x1="0" y1="0" x2="10" y2="10">
<stop offset="0" stop-color="red"/>
<stop offset="1" stop-color="blue"/>
</linearGradient>
SVG
end

it "passes in the transform via the apply_transformations option" do
arguments = element.gradient_arguments(double(bounding_box: [0, 0, 10, 10]))

expect(arguments).to eq(
from: [0, 0],
to: [10, 10],
stops: [[0, "ff0000"], [1, "0000ff"]],
apply_transformations: [2, 0, 0, 2, 10, 0],
)
end
end
Expand Down
Loading

0 comments on commit c0420fa

Please sign in to comment.