Skip to content

Commit

Permalink
Track transformations and apply to gradients' matrices
Browse files Browse the repository at this point in the history
PDF gradients/patterns take coordinates in the coordinate space of the
document, not the "user space", so if you perform a scale/rotate/translate
and then paint a gradient inside, it doesn't render correctly.

This commit tracks transformations applied to the document, and multiplies
the gradient matrix with this tracked transformation matrix so that the
gradient appears in the correct place in the document.
  • Loading branch information
Roger Nesbitt committed Jul 19, 2015
1 parent afc263b commit c255966
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 10 deletions.
1 change: 1 addition & 0 deletions lib/prawn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def configuration(*args)
require_relative "prawn/stamp"
require_relative "prawn/soft_mask"
require_relative "prawn/security"
require_relative "prawn/transformation_stack"
require_relative "prawn/document"
require_relative "prawn/font"
require_relative "prawn/measurements"
Expand Down
1 change: 1 addition & 0 deletions lib/prawn/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Document
include Prawn::Images
include Prawn::Stamp
include Prawn::SoftMask
include Prawn::TransformationStack

# @group Extension API

Expand Down
12 changes: 10 additions & 2 deletions lib/prawn/document/internals.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,18 @@ module Internals
# Perhaps they will become part of the extension API?
# Anyway, for now it's not clear what we should do w. them.
delegate [ :graphic_state,
:save_graphics_state,
:restore_graphics_state,
:on_page_create ] => :renderer

def save_graphics_state(state = nil, &block)
save_transformation_stack
renderer.save_graphics_state(state, &block)
end

def restore_graphics_state
restore_transformation_stack
renderer.restore_graphics_state
end

# FIXME: This is a circular reference, because in theory Prawn should
# be passing instances of renderer to PDF::Core::Page, but it's
# passing Prawn::Document objects instead.
Expand Down
29 changes: 21 additions & 8 deletions lib/prawn/graphics/patterns.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,20 @@ def set_gradient(type, *grad)
end

def gradient_registry_key(gradient)
x1, y1, x2, y2 = gradient_coordinates(gradient)
transformation = current_transformation_matrix_with_translation(x1, y1)

if gradient[1].is_a?(Array) # axial
[
map_to_absolute(gradient[0]),
map_to_absolute(gradient[1]),
transformation,
x2, y2,
gradient[2], gradient[3]
]
else # radial
[
map_to_absolute(gradient[0]),
transformation,
x2, y2,
gradient[1],
map_to_absolute(gradient[2]),
gradient[3],
gradient[4], gradient[5]
]
Expand Down Expand Up @@ -104,12 +107,16 @@ def gradient(*args)
:N => 1.0
)

x1, y1, x2, y2 = gradient_coordinates(args)

if args.length == 4
coords = [0, 0, args[1].first - args[0].first, args[1].last - args[0].last]
coords = [0, 0, x2 - x1, y2 - y1]
else
coords = [0, 0, args[1], args[2].first - args[0].first, args[2].last - args[0].last, args[3]]
coords = [0, 0, args[1], x2 - x1, y2 - y1, args[3]]
end

transformation = current_transformation_matrix_with_translation(x1, y1)

shading = ref!(
:ShadingType => args.length == 4 ? 2 : 3, # axial : radial shading
:ColorSpace => color_space(color1),
Expand All @@ -121,10 +128,16 @@ def gradient(*args)
ref!(
:PatternType => 2, # shading pattern
:Shading => shading,
:Matrix => [1, 0,
0, 1] + map_to_absolute(args[0])
:Matrix => transformation
)
end

def gradient_coordinates(args)
x1, y1 = map_to_absolute(args[0])
x2, y2 = map_to_absolute(args[args.length == 4 ? 1 : 2])

[x1, y1, x2, y2]
end
end
end
end
3 changes: 3 additions & 0 deletions lib/prawn/graphics/transformation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ def scale(factor, options = {}, &block)
def transformation_matrix(a, b, c, d, e, f)
values = [a, b, c, d, e, f].map { |x| "%.5f" % x }.join(" ")
save_graphics_state if block_given?

add_to_transformation_stack(a, b, c, d, e, f)

renderer.add_content "#{values} cm"
if block_given?
yield
Expand Down
42 changes: 42 additions & 0 deletions lib/prawn/transformation_stack.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# encoding: utf-8
#
# transformation_stack.rb : Stores the transformations that have been applied to the document
#
# Copyright 2015, Roger Nesbitt. All Rights Reserved.
#
# This is free software. Please see the LICENSE and COPYING files for details.
#

require 'matrix'

module Prawn
module TransformationStack
def add_to_transformation_stack(a, b, c, d, e, f)
@transformation_stack ||= [[]]
@transformation_stack.last.push([a, b, c, d, e, f].map { |x| x.to_f })
end

def save_transformation_stack
@transformation_stack ||= [[]]
@transformation_stack.push(@transformation_stack.last.dup)
end

def restore_transformation_stack
@transformation_stack.pop if @transformation_stack
end

def current_transformation_matrix_with_translation(x = 0, y = 0)
transformations = (@transformation_stack || [[]]).last

matrix = Matrix.identity(3)

transformations.each do |a, b, c, d, e, f|
matrix *= Matrix[[a, c, e], [b, d, f], [0, 0, 1]]
end

matrix *= Matrix[[1, 0, x], [0, 1, y], [0, 0, 1]]

matrix.to_a[0..1].transpose.flatten
end
end
end
24 changes: 24 additions & 0 deletions spec/graphics_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,18 @@
str = @pdf.render
expect(str).to match(%r{/Pattern\s+CS\s*/SP-?\d+\s+SCN})
end

it "uses the transformation stack to translate user co-ordinates to document co-ordinates required by /Pattern" do
@pdf.scale 2 do
@pdf.translate 40, 40 do
@pdf.fill_gradient [0, 10], [15, 15], 'FF0000', '0000FF'
end
end

grad = PDF::Inspector::Graphics::Pattern.analyze(@pdf.render)
pattern = grad.patterns.values.first
expect(pattern[:Matrix]).to eq([2, 0, 0, 2, 80, 100])
end
end

describe 'radial gradients' do
Expand Down Expand Up @@ -342,6 +354,18 @@
str = @pdf.render
expect(str).to match(%r{/Pattern\s+CS\s*/SP-?\d+\s+SCN})
end

it "uses the transformation stack to translate user co-ordinates to document co-ordinates required by /Pattern" do
@pdf.scale 2 do
@pdf.translate 40, 40 do
@pdf.fill_gradient [0, 10], 15, [15, 15], 25, 'FF0000', '0000FF'
end
end

grad = PDF::Inspector::Graphics::Pattern.analyze(@pdf.render)
pattern = grad.patterns.values.first
expect(pattern[:Matrix]).to eq([2, 0, 0, 2, 80, 100])
end
end
end

Expand Down
63 changes: 63 additions & 0 deletions spec/transformation_stack_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# encoding: utf-8

require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")

describe Prawn::TransformationStack do
before { create_pdf }
before { pdf.add_to_transformation_stack(2, 0, 0, 2, 100, 100) }

let(:pdf) { @pdf }
let(:stack) { @pdf.instance_variable_get(:@transformation_stack) }

describe "#add_to_transformation_stack" do
it "creates and adds to the stack" do
pdf.add_to_transformation_stack(1, 0, 0, 1, 20, 20)

expect(stack).to eq [[[2, 0, 0, 2, 100, 100], [1, 0, 0, 1, 20, 20]]]
end

it "adds to the last stack" do
pdf.save_transformation_stack
pdf.add_to_transformation_stack(1, 0, 0, 1, 20, 20)

expect(stack).to eq [
[[2, 0, 0, 2, 100, 100]],
[[2, 0, 0, 2, 100, 100], [1, 0, 0, 1, 20, 20]]
]
end
end

describe "#save_transformation_stack" do
it "clones the last stack" do
pdf.save_transformation_stack

expect(stack.length).to eq 2
expect(stack.first).to eq stack.last
expect(stack.first).to_not be stack.last
end
end

describe "#restore_transformation_stack" do
it "pops off the last stack" do
pdf.save_transformation_stack
pdf.add_to_transformation_stack(1, 0, 0, 1, 20, 20)
pdf.restore_transformation_stack

expect(stack).to eq [[[2, 0, 0, 2, 100, 100]]]
end
end

describe "current_transformation_matrix_with_translation" do
before do
pdf.add_to_transformation_stack(1, 0, 0, 1, 20, 20)
end

it "calculates the last transformation" do
expect(pdf.current_transformation_matrix_with_translation).to eq [2, 0, 0, 2, 140, 140]
end

it "adds the supplied x and y coordinates to the transformation stack" do
expect(pdf.current_transformation_matrix_with_translation(15, 15)).to eq [2, 0, 0, 2, 170, 170]
end
end
end

0 comments on commit c255966

Please sign in to comment.