pdf-wrapper 0.0.7 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +11 -0
- data/Rakefile +4 -4
- data/examples/cell.rb +1 -1
- data/examples/image.rb +3 -3
- data/examples/markup.rb +12 -0
- data/examples/repeating.rb +1 -1
- data/examples/scaled.rb +38 -0
- data/examples/scaled_cells.rb +30 -0
- data/examples/scaled_image.rb +14 -0
- data/examples/shapes.rb +1 -1
- data/examples/table.rb +25 -4
- data/examples/translate.rb +21 -0
- data/examples/utf8-long.rb +1 -2
- data/examples/utf8.rb +1 -2
- data/lib/pdf/core.rb +23 -0
- data/lib/pdf/wrapper.rb +136 -760
- data/lib/pdf/wrapper/graphics.rb +116 -0
- data/lib/pdf/wrapper/images.rb +206 -0
- data/lib/pdf/wrapper/loading.rb +52 -0
- data/lib/pdf/wrapper/table.rb +473 -0
- data/lib/pdf/wrapper/text.rb +337 -0
- data/specs/data/windmill.jpg +0 -0
- data/specs/{shapes_spec.rb → graphics_spec.rb} +3 -3
- data/specs/spec_helper.rb +6 -1
- data/specs/tables_spec.rb +111 -0
- data/specs/text_spec.rb +63 -2
- data/specs/wrapper_spec.rb +145 -72
- metadata +20 -9
- data/examples/padded_image.rb +0 -12
- data/examples/template.rb +0 -13
data/CHANGELOG
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
v0.1.0 (28th May 2008)
|
2
|
+
- added PDF::Wrapper#translate and PDF::Wrapper#scale
|
3
|
+
- fixed a bug that caused some text to be rendered off the page when wrapping onto 3rd and
|
4
|
+
subsequent pages
|
5
|
+
- Improved table support using a new class PDF::Wrapper::Table
|
6
|
+
- Added a :center option to Wrapper#image
|
7
|
+
- Added a :markup option to text related functions. Pango markup can be used to tweak
|
8
|
+
text style (bold, italics, super script, etc)
|
9
|
+
- Replaced PDF::Wrapper#rounded_rectangle with the :radius option to PDF::Wrapper#rectangle
|
10
|
+
- Allow cells to have a border with rounded corners
|
11
|
+
|
1
12
|
v0.0.7 (30th April 2008)
|
2
13
|
- Added support for creating new pages from a template file (pdf/png/jpg/gif/svg/probably more)
|
3
14
|
- Convert the params to start_new_page to be an options hash
|
data/Rakefile
CHANGED
@@ -6,7 +6,7 @@ require 'rake/testtask'
|
|
6
6
|
require "rake/gempackagetask"
|
7
7
|
require 'spec/rake/spectask'
|
8
8
|
|
9
|
-
PKG_VERSION = "0.0
|
9
|
+
PKG_VERSION = "0.1.0"
|
10
10
|
PKG_NAME = "pdf-wrapper"
|
11
11
|
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
12
12
|
|
@@ -16,11 +16,11 @@ task :default => [ :spec ]
|
|
16
16
|
# run all rspecs
|
17
17
|
desc "Run all rspec files"
|
18
18
|
Spec::Rake::SpecTask.new("spec") do |t|
|
19
|
-
#
|
20
|
-
t.spec_files = ['specs/load_spec.rb','specs/image_spec.rb','specs/
|
19
|
+
# spec files listed explicitly so that load_spec is the first one run
|
20
|
+
t.spec_files = ['specs/load_spec.rb','specs/image_spec.rb','specs/graphics_spec.rb','specs/tables_spec.rb','specs/text_spec.rb','specs/wrapper_spec.rb']
|
21
21
|
t.rcov = true
|
22
22
|
t.rcov_dir = (ENV['CC_BUILD_ARTIFACTS'] || 'doc') + "/rcov"
|
23
|
-
t.rcov_opts = ["--exclude","spec.*\.rb","--exclude",".*cairo.*","--exclude",".*rcov.*","--exclude",".*rspec.*","--exclude",".*pdf-reader.*"]
|
23
|
+
t.rcov_opts = ["--exclude","spec.*\.rb","--exclude",".*cairo.*","--exclude",".*rcov.*","--exclude",".*rspec.*","--exclude",".*pdf-reader.*", "--exclude",".*gems.*"]
|
24
24
|
end
|
25
25
|
|
26
26
|
# generate specdocs
|
data/examples/cell.rb
CHANGED
@@ -6,5 +6,5 @@ $:.unshift(File.dirname(__FILE__) + "/../lib")
|
|
6
6
|
require 'pdf/wrapper'
|
7
7
|
|
8
8
|
pdf = PDF::Wrapper.new(:paper => :A4)
|
9
|
-
pdf.cell("Given an index within a layout, determines the positions that of the strong and weak cursors if the insertion point is at that index. The position of each cursor is stored as a zero-width rectangle. The strong cursor location is the location where characters of the directionality equal to the base direction of the layout are inserted. The weak cursor location is the location where characters of the directionality opposite to the base direction of the layout are inserted.", 100, 100, 100, 200, {:border => "", :color => :
|
9
|
+
pdf.cell("Given an index within a layout, determines the positions that of the strong and weak cursors if the insertion point is at that index. The position of each cursor is stored as a zero-width rectangle. The strong cursor location is the location where characters of the directionality equal to the base direction of the layout are inserted. The weak cursor location is the location where characters of the directionality opposite to the base direction of the layout are inserted.", 100, 100, 100, 200, {:border => "", :color => :black, :radius => 10})
|
10
10
|
pdf.render_to_file("wrapper-cell.pdf")
|
data/examples/image.rb
CHANGED
@@ -6,8 +6,8 @@ $:.unshift(File.dirname(__FILE__) + "/../lib")
|
|
6
6
|
require 'pdf/wrapper'
|
7
7
|
|
8
8
|
pdf = PDF::Wrapper.new(:paper => :A4)
|
9
|
-
pdf.
|
10
|
-
pdf.
|
9
|
+
pdf.font("Sans Serif")
|
10
|
+
pdf.color(:black)
|
11
11
|
pdf.text("PDF::Wrapper Supports Images", :alignment => :center)
|
12
12
|
pdf.image(File.dirname(__FILE__) + "/../specs/data/zits.gif")
|
13
13
|
pdf.image(File.dirname(__FILE__) + "/../specs/data/google.png", :left => 100, :top => 350)
|
@@ -16,6 +16,6 @@ pdf.start_new_page
|
|
16
16
|
pdf.image(File.dirname(__FILE__) + "/../specs/data/orc.svg", :left => pdf.margin_left, :top => pdf.margin_top, :width => pdf.body_width, :height => pdf.body_height)
|
17
17
|
pdf.start_new_page
|
18
18
|
pdf.image(File.dirname(__FILE__) + "/../specs/data/utf8-long.pdf", :left => pdf.margin_left, :top => pdf.margin_top, :width => pdf.body_width/2, :height => pdf.body_height/2)
|
19
|
-
pdf.
|
19
|
+
pdf.color(:red)
|
20
20
|
|
21
21
|
pdf.render_to_file("image.pdf")
|
data/examples/markup.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
5
|
+
|
6
|
+
require 'pdf/wrapper'
|
7
|
+
|
8
|
+
pdf = PDF::Wrapper.new(:paper => :A4)
|
9
|
+
pdf.font("Sans Serif")
|
10
|
+
pdf.color(:black)
|
11
|
+
pdf.text "<i>James Healy</i>", :font => "Monospace", :font_size => 16, :alignment => :center, :markup => :pango
|
12
|
+
pdf.render_to_file("markup.pdf")
|
data/examples/repeating.rb
CHANGED
data/examples/scaled.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
5
|
+
|
6
|
+
require 'pdf/wrapper'
|
7
|
+
|
8
|
+
pdf = PDF::Wrapper.new(:paper => :A4)
|
9
|
+
pdf.line_width = 2
|
10
|
+
|
11
|
+
# grid lines
|
12
|
+
pdf.line(50, 0, 50, pdf.page_height)
|
13
|
+
pdf.line(100, 0, 100, pdf.page_height)
|
14
|
+
pdf.line(150, 0, 150, pdf.page_height)
|
15
|
+
pdf.line(200, 0, 200, pdf.page_height)
|
16
|
+
pdf.line(0, 50, pdf.page_width, 50)
|
17
|
+
pdf.line(0, 100, pdf.page_width, 100)
|
18
|
+
pdf.line(0, 150, pdf.page_width, 150)
|
19
|
+
pdf.line(0, 200, pdf.page_width, 200)
|
20
|
+
|
21
|
+
# non scaled
|
22
|
+
pdf.rectangle(100,100,100,100, :fill_color => :green)
|
23
|
+
|
24
|
+
# scaled
|
25
|
+
pdf.scale(pdf.page_width.to_f, pdf.page_height.to_f) do
|
26
|
+
# top left corner 10% of the page width from the left and top of the page.
|
27
|
+
# width 10% of the page width
|
28
|
+
# height 10% of the page height
|
29
|
+
# - obviously will not be square on a A4 page
|
30
|
+
pdf.rectangle(0.1,0.1,0.1,0.1, :fill_color => :red)
|
31
|
+
pdf.text("boo!", :top => 0.5, :left => 0.2, :width => 0.6)
|
32
|
+
end
|
33
|
+
|
34
|
+
#pdf.text("boo2!", :top => 500, :left => 100)
|
35
|
+
|
36
|
+
|
37
|
+
# show results
|
38
|
+
pdf.render_to_file("scaled.pdf")
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
5
|
+
|
6
|
+
require 'pdf/wrapper'
|
7
|
+
|
8
|
+
pdf = PDF::Wrapper.new(:paper => :A4)
|
9
|
+
pdf.font("Sans Serif")
|
10
|
+
pdf.line_width(0.1)
|
11
|
+
# naglowek
|
12
|
+
pdf.translate(pdf.absolute_left_margin, pdf.absolute_top_margin) do
|
13
|
+
pdf.scale(pdf.body_width, pdf.body_width) do
|
14
|
+
pdf.cell("Exorigo", 0, 0, 0.2, 0.05, :alignment => :center, :font_size => 15)
|
15
|
+
pdf.cell("Zamówienie nr PO/0000/01/01/2008", 0.2, 0, 0.8, 0.05, :fill_color => :gray, :alignment => :center)
|
16
|
+
pdf.cell("imię i naziwsko osoby zamawiającej. NALEŻY PODAĆ NA FAKTURZE", 0, 0.05, 0.55, 0.025, :font_size => 6)
|
17
|
+
pdf.cell("data zamówienia", 0.55, 0.05, 0.15, 0.025, :font_size => 6)
|
18
|
+
pdf.cell("POWYŻSZY NUMER ZAMÓWIENIA NALEŻY PODAĆ NA FAKTURZE", 0.7, 0.05, 0.3, 0.05, :font_size => 6, :alignment => :center, :spacing => 4)
|
19
|
+
pdf.cell("Jan Kowalski", 0, 0.075, 0.55, 0.025, :font => "Sans Serif bold", :font_size => 8)
|
20
|
+
pdf.cell("22.01.2008", 0.55, 0.075, 0.15, 0.025, :font => "Sans Serif bold", :font_size => 8)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
# dane dostawcy
|
24
|
+
|
25
|
+
# szczegoly zamowienia
|
26
|
+
|
27
|
+
# autoryzacja
|
28
|
+
|
29
|
+
# adres
|
30
|
+
pdf.render_to_file("scaled-cells.pdf")
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
5
|
+
|
6
|
+
require 'pdf/wrapper'
|
7
|
+
|
8
|
+
pdf = PDF::Wrapper.new(:paper => :A4)
|
9
|
+
pdf.image(File.dirname(__FILE__) + "/../specs/data/zits.gif", :top => 100, :height => 200, :width => 200, :proportional => true, :center => true)
|
10
|
+
pdf.rectangle(pdf.margin_left, 100, 200, 200)
|
11
|
+
pdf.image(File.dirname(__FILE__) + "/../specs/data/windmill.jpg", :top => 400, :height => 200, :width => 200, :proportional => true, :center => true)
|
12
|
+
pdf.rectangle(pdf.margin_left, 400, 200, 200)
|
13
|
+
|
14
|
+
pdf.render_to_file("scaled_image.pdf")
|
data/examples/shapes.rb
CHANGED
@@ -9,5 +9,5 @@ pdf = PDF::Wrapper.new(:paper => :A4)
|
|
9
9
|
pdf.rectangle(30,30,100,100, :fill_color => :red)
|
10
10
|
pdf.circle(100,300,30)
|
11
11
|
pdf.line(100, 350, 400, 150)
|
12
|
-
pdf.
|
12
|
+
pdf.rectangle(300,300, 200, 200, :fill_color => :green, :radius => 10)
|
13
13
|
pdf.render_to_file("shapes.pdf")
|
data/examples/table.rb
CHANGED
@@ -6,13 +6,34 @@ $:.unshift(File.dirname(__FILE__) + "/../lib")
|
|
6
6
|
require 'pdf/wrapper'
|
7
7
|
|
8
8
|
pdf = PDF::Wrapper.new(:paper => :A4)
|
9
|
-
pdf.text "
|
10
|
-
|
9
|
+
pdf.text File.read(File.dirname(__FILE__) + "/../specs/data/utf8.txt").strip, :alignment => :centre
|
10
|
+
pdf.pad 5
|
11
|
+
headers = %w{one two three four}
|
11
12
|
|
12
|
-
data
|
13
|
+
data = []
|
14
|
+
data << ["This is some longer text to ensure that the cell wraps","oh noes! the cols can't get the width they desire",3,4]
|
15
|
+
data << ["This is some longer text to ensure that the cell wraps","oh noes! the cols can't get the width they desire",3,4]
|
16
|
+
|
17
|
+
data << [[], "j", "a", "m"]
|
13
18
|
|
14
19
|
(1..100).each do
|
15
20
|
data << %w{1 2 3 4}
|
16
21
|
end
|
17
|
-
|
22
|
+
|
23
|
+
table = PDF::Wrapper::Table.new do |t|
|
24
|
+
t.data = data
|
25
|
+
t.headers = headers
|
26
|
+
t.table_options :font_size => 10
|
27
|
+
t.header_options :color => :white, :fill_color => :black
|
28
|
+
t.row_options 6, {:border => "t"}
|
29
|
+
t.row_options :even, {:fill_color => :gray}
|
30
|
+
t.col_options 0, {:border => "tb"}
|
31
|
+
t.col_options 1, {:alignment => :centre}
|
32
|
+
t.col_options 2, {:alignment => :centre}
|
33
|
+
t.col_options 3, {:alignment => :centre, :border => "tb"}
|
34
|
+
t.col_options :even, {:fill_color => :blue}
|
35
|
+
t.cell_options 3, 3, {:fill_color => :green}
|
36
|
+
end
|
37
|
+
|
38
|
+
pdf.table(table)
|
18
39
|
pdf.render_to_file("table.pdf")
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
5
|
+
|
6
|
+
require 'pdf/wrapper'
|
7
|
+
|
8
|
+
@pdf = PDF::Wrapper.new(:paper => :A4)
|
9
|
+
|
10
|
+
def captioned_image(filename, caption, x, y)
|
11
|
+
@pdf.translate(x, y) do
|
12
|
+
@pdf.image(filename, :top => 0, :left => 0, :height => 100, :width => 100, :proportional => true)
|
13
|
+
@pdf.text("Image Caption", :top => 110, :left => 0)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
captioned_image(File.dirname(__FILE__) + "/../specs/data/orc.svg", "One", 100, 100)
|
18
|
+
captioned_image(File.dirname(__FILE__) + "/../specs/data/orc.svg", "Two", 250, 300)
|
19
|
+
captioned_image(File.dirname(__FILE__) + "/../specs/data/orc.svg", "Three", 400, 500)
|
20
|
+
|
21
|
+
@pdf.render_to_file("translate.pdf")
|
data/examples/utf8-long.rb
CHANGED
@@ -6,7 +6,6 @@ $:.unshift(File.dirname(__FILE__) + "/../lib")
|
|
6
6
|
require 'pdf/wrapper'
|
7
7
|
|
8
8
|
pdf = PDF::Wrapper.new(:paper => :A4)
|
9
|
-
pdf.
|
10
|
-
pdf.default_color(:black)
|
9
|
+
pdf.font("Sans Serif")
|
11
10
|
pdf.text File.read(File.dirname(__FILE__) + "/../specs/data/utf8-long.txt"), :font => "Monospace", :font_size => 8
|
12
11
|
pdf.render_to_file("utf8-long.pdf")
|
data/examples/utf8.rb
CHANGED
@@ -6,7 +6,6 @@ $:.unshift(File.dirname(__FILE__) + "/../lib")
|
|
6
6
|
require 'pdf/wrapper'
|
7
7
|
|
8
8
|
pdf = PDF::Wrapper.new(:paper => :A4)
|
9
|
-
pdf.
|
10
|
-
pdf.default_color(:black)
|
9
|
+
pdf.font("Sans Serif")
|
11
10
|
pdf.text File.read(File.dirname(__FILE__) + "/../specs/data/utf8.txt"), :font => "Monospace", :font_size => 8, :alignment => :center
|
12
11
|
pdf.render_to_file("wrapper.pdf")
|
data/lib/pdf/core.rb
CHANGED
@@ -7,4 +7,27 @@ class Hash
|
|
7
7
|
unknown_keys = keys - [valid_keys].flatten
|
8
8
|
raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
|
9
9
|
end
|
10
|
+
|
11
|
+
def only(*keys)
|
12
|
+
keys.flatten!
|
13
|
+
self.dup.reject { |k,v|
|
14
|
+
!keys.include? k.to_sym
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
unless [].respond_to?(:sum)
|
20
|
+
|
21
|
+
module Enumerable
|
22
|
+
# borrowed from active support. No need to pull that entire beast in as a dependency
|
23
|
+
def sum(identity = 0, &block)
|
24
|
+
return identity unless size > 0
|
25
|
+
|
26
|
+
if block_given?
|
27
|
+
map(&block).sum
|
28
|
+
else
|
29
|
+
inject { |sum, element| sum + element }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
10
33
|
end
|
data/lib/pdf/wrapper.rb
CHANGED
@@ -3,6 +3,12 @@
|
|
3
3
|
require 'stringio'
|
4
4
|
require 'pdf/core'
|
5
5
|
|
6
|
+
require File.dirname(__FILE__) + "/wrapper/graphics"
|
7
|
+
require File.dirname(__FILE__) + "/wrapper/images"
|
8
|
+
require File.dirname(__FILE__) + "/wrapper/loading"
|
9
|
+
require File.dirname(__FILE__) + "/wrapper/table"
|
10
|
+
require File.dirname(__FILE__) + "/wrapper/text"
|
11
|
+
|
6
12
|
# try to load cairo from the standard places, but don't worry if it fails,
|
7
13
|
# we'll try to find it via rubygems
|
8
14
|
begin
|
@@ -40,14 +46,13 @@ module PDF
|
|
40
46
|
#
|
41
47
|
# require 'pdf/wrapper'
|
42
48
|
# pdf = PDF::Wrapper.new(:paper => :A4)
|
43
|
-
# pdf.
|
49
|
+
# pdf.font("Monospace")
|
44
50
|
# pdf.text "Hello World", :font => "Sans Serif", :font_size => 18
|
45
51
|
# pdf.text "Pretend this is a code sample"
|
46
52
|
# puts pdf.render
|
47
53
|
class Wrapper
|
48
54
|
|
49
|
-
attr_reader :
|
50
|
-
attr_reader :page_width, :page_height, :page
|
55
|
+
attr_reader :page
|
51
56
|
|
52
57
|
# borrowed from PDF::Writer
|
53
58
|
PAGE_SIZES = { # :value {...}:
|
@@ -87,10 +92,20 @@ module PDF
|
|
87
92
|
# <tt>:margin_bottom</tt>:: The size of the default bottom margin (default 5% of page)
|
88
93
|
# <tt>:margin_left</tt>:: The size of the default left margin (default 5% of page)
|
89
94
|
# <tt>:margin_right</tt>:: The size of the default right margin (default 5% of page)
|
90
|
-
# <tt>:template</tt>:: The path to an image file. If specified, the first page of the document will use the specified image as a template.
|
95
|
+
# <tt>:template</tt>:: The path to an image file. If specified, the first page of the document will use the specified image as a template.
|
91
96
|
# The page will be sized to match the template size. The use templates on subsequent pages, see the options for
|
92
97
|
# start_new_page.
|
93
98
|
def initialize(opts={})
|
99
|
+
# TODO: Investigate ways of using the cairo transform/translate/scale functionality to
|
100
|
+
# reduce the amount of irritating co-ordinate maths the user of PDF::Wrapper (ie. me!)
|
101
|
+
# is required to do.
|
102
|
+
# - translate the pdf body width so that it's 1.0 wide and 1.0 high?
|
103
|
+
# TODO: find a way to add metadata (title, author, subject, etc) to the output file
|
104
|
+
# currently no way to specify this in cairo.
|
105
|
+
# tentatively scheduled for cairo 1.8 - see:
|
106
|
+
# - http://cairographics.org/roadmap/
|
107
|
+
# - http://lists.cairographics.org/archives/cairo/2007-September/011441.html
|
108
|
+
# - http://lists.freedesktop.org/archives/cairo/2006-April/006809.html
|
94
109
|
|
95
110
|
# ensure we have recentish cairo bindings
|
96
111
|
raise "Ruby Cairo bindings version #{Cairo::BINDINGS_VERSION.join(".")} is too low. At least 1.5 is required" if Cairo::BINDINGS_VERSION.to_s < "150"
|
@@ -129,14 +144,14 @@ module PDF
|
|
129
144
|
@context = Cairo::Context.new(@surface)
|
130
145
|
|
131
146
|
# set the background colour
|
132
|
-
|
147
|
+
color(options[:background_color])
|
133
148
|
@context.paint
|
134
149
|
|
135
150
|
# set a default drawing colour and font style
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
151
|
+
color(:black)
|
152
|
+
line_width(0.5)
|
153
|
+
font("Sans Serif")
|
154
|
+
font_size(16)
|
140
155
|
|
141
156
|
# maintain a count of pages and array of repeating elements to add to each page
|
142
157
|
@page = 1
|
@@ -160,499 +175,165 @@ module PDF
|
|
160
175
|
# Returns the x value of the left margin
|
161
176
|
# The top left corner of the page is (0,0)
|
162
177
|
def absolute_left_margin
|
163
|
-
|
178
|
+
margin_left
|
164
179
|
end
|
165
180
|
|
166
181
|
# Returns the x value of the right margin
|
167
182
|
# The top left corner of the page is (0,0)
|
168
183
|
def absolute_right_margin
|
169
|
-
|
184
|
+
page_width - margin_right
|
170
185
|
end
|
171
186
|
|
172
187
|
# Returns the y value of the top margin
|
173
188
|
# The top left corner of the page is (0,0)
|
174
189
|
def absolute_top_margin
|
175
|
-
|
190
|
+
margin_top
|
176
191
|
end
|
177
192
|
|
178
193
|
# Returns the y value of the bottom margin
|
179
194
|
# The top left corner of the page is (0,0)
|
180
195
|
def absolute_bottom_margin
|
181
|
-
|
196
|
+
page_height - margin_bottom
|
182
197
|
end
|
183
198
|
|
184
199
|
# Returns the x at the middle of the page
|
185
200
|
def absolute_x_middle
|
186
|
-
|
201
|
+
page_width / 2
|
187
202
|
end
|
188
203
|
|
189
204
|
# Returns the y at the middle of the page
|
190
205
|
def absolute_y_middle
|
191
|
-
|
206
|
+
page_height / 2
|
192
207
|
end
|
193
208
|
|
194
209
|
# Returns the width of the usable part of the page (between the side margins)
|
195
210
|
def body_width
|
196
|
-
@page_width - @margin_left - @margin_right
|
211
|
+
device_x_to_user_x(@page_width - @margin_left - @margin_right)
|
197
212
|
end
|
198
213
|
|
199
214
|
# Returns the height of the usable part of the page (between the top and bottom margins)
|
200
215
|
def body_height
|
201
|
-
@page_height - @margin_top - @margin_bottom
|
216
|
+
#@context.device_to_user(@page_width - @margin_left - @margin_right, @page_height - @margin_top - @margin_bottom).last
|
217
|
+
device_y_to_user_y(@page_height - @margin_top - @margin_bottom)
|
202
218
|
end
|
203
219
|
|
204
220
|
# Returns the x coordinate of the middle part of the usable space between the margins
|
205
|
-
def
|
206
|
-
|
221
|
+
def body_x_middle
|
222
|
+
margin_left + (body_width / 2)
|
207
223
|
end
|
208
224
|
|
209
225
|
# Returns the y coordinate of the middle part of the usable space between the margins
|
210
|
-
def
|
211
|
-
|
226
|
+
def body_y_middle
|
227
|
+
margin_top + (body_height / 2)
|
228
|
+
end
|
229
|
+
|
230
|
+
def page_height
|
231
|
+
device_y_to_user_y(@page_height)
|
232
|
+
end
|
233
|
+
|
234
|
+
def page_width
|
235
|
+
device_x_to_user_x(@page_width)
|
212
236
|
end
|
213
237
|
|
214
238
|
# return the current position of the cursor
|
215
239
|
# returns 2 values - x,y
|
216
240
|
def current_point
|
217
|
-
|
241
|
+
@context.current_point
|
218
242
|
end
|
219
243
|
|
220
|
-
|
221
|
-
|
222
|
-
absolute_bottom_margin - starty
|
244
|
+
def margin_bottom
|
245
|
+
device_y_to_user_y(@margin_bottom).to_i
|
223
246
|
end
|
224
247
|
|
225
|
-
|
226
|
-
|
227
|
-
absolute_right_margin - startx
|
248
|
+
def margin_left
|
249
|
+
device_x_to_user_x(@margin_left).to_i
|
228
250
|
end
|
229
251
|
|
230
|
-
|
231
|
-
|
232
|
-
#####################################################
|
233
|
-
|
234
|
-
# change the default font size
|
235
|
-
def default_font_size(size)
|
236
|
-
#@context.set_font_size(size.to_i)
|
237
|
-
@default_font_size = size.to_i unless size.nil?
|
252
|
+
def margin_right
|
253
|
+
device_x_to_user_x(@margin_right).to_i
|
238
254
|
end
|
239
|
-
alias default_font_size= default_font_size
|
240
|
-
alias font_size default_font_size # PDF::Writer compatibility
|
241
|
-
|
242
|
-
# change the default font to write with
|
243
|
-
def default_font(fontname, style = nil, weight = nil)
|
244
|
-
#@context.select_font_face(fontname, slant, bold)
|
245
|
-
@default_font = fontname
|
246
|
-
@default_font_style = style unless style.nil?
|
247
|
-
@default_font_weight = weight unless weight.nil?
|
248
|
-
end
|
249
|
-
alias default_font= default_font
|
250
|
-
alias select_font default_font # PDF::Writer compatibility
|
251
255
|
|
252
|
-
|
253
|
-
|
254
|
-
# Parameters:
|
255
|
-
# <tt>c</tt>:: either a colour symbol recognised by rcairo (:red, :blue, :black, etc) or
|
256
|
-
# an array with 3-4 integer elements. The first 3 numbers are red, green and
|
257
|
-
# blue (0-255). The optional 4th number is the alpha channel and should be
|
258
|
-
# between 0 and 1. See the API docs at http://cairo.rubyforge.org/ for a list
|
259
|
-
# of predefined colours
|
260
|
-
def default_color(c)
|
261
|
-
c = translate_color(c)
|
262
|
-
validate_color(c)
|
263
|
-
@default_color = c
|
256
|
+
def margin_top
|
257
|
+
device_y_to_user_y(@margin_top).to_i
|
264
258
|
end
|
265
|
-
alias default_color= default_color
|
266
|
-
alias stroke_color default_color # PDF::Writer compatibility
|
267
259
|
|
268
|
-
#
|
269
|
-
|
270
|
-
|
271
|
-
# <tt>f</tt>:: float value of stroke width from 0.01 to 255
|
272
|
-
def default_line_width(f)
|
273
|
-
@default_line_width = f
|
260
|
+
# return the number of points from starty to the bottom border
|
261
|
+
def points_to_bottom_margin(starty)
|
262
|
+
absolute_bottom_margin - starty
|
274
263
|
end
|
275
|
-
alias default_line_width= default_line_width
|
276
264
|
|
277
|
-
#
|
278
|
-
|
279
|
-
|
280
|
-
# In addition to the standard text style options (see the documentation for text()), cell() supports
|
281
|
-
# the following options:
|
282
|
-
#
|
283
|
-
# <tt>:border</tt>:: Which sides of the cell should have a border? A string with any combination the letters tblr (top, bottom, left, right). Nil for no border, defaults to all sides.
|
284
|
-
# <tt>:border_width</tt>:: How wide should the border be?
|
285
|
-
# <tt>:border_color</tt>:: What color should the border be?
|
286
|
-
# <tt>:fill_color</tt>:: A background color for the cell. Defaults to none.
|
287
|
-
# <tt>:padding</tt>:: The number of points to leave between the inside of the border and text. Defaults to 3.
|
288
|
-
def cell(str, x, y, w, h, opts={})
|
289
|
-
# TODO: add support for pango markup (see http://ruby-gnome2.sourceforge.jp/hiki.cgi?pango-markup)
|
290
|
-
# TODO: add a wrap option so wrapping can be disabled
|
291
|
-
# TODO: handle a single word that is too long for the width
|
292
|
-
# TODO: add an option to draw a border with rounded corners
|
293
|
-
|
294
|
-
options = default_text_options
|
295
|
-
options.merge!({:border => "tblr", :border_width => @default_line_width, :border_color => :black, :fill_color => nil, :padding => 3})
|
296
|
-
options.merge!(opts)
|
297
|
-
options.assert_valid_keys(default_text_options.keys + [:width, :border, :border_width, :border_color, :fill_color, :padding])
|
298
|
-
|
299
|
-
# apply padding
|
300
|
-
textw = w - (options[:padding] * 2)
|
301
|
-
texth = h - (options[:padding] * 2)
|
302
|
-
textx = x + options[:padding]
|
303
|
-
texty = y + options[:padding]
|
304
|
-
|
305
|
-
options[:border] = "" unless options[:border]
|
306
|
-
options[:border].downcase!
|
307
|
-
|
308
|
-
# save the cursor position so we can restore it at the end
|
309
|
-
origx, origy = current_point
|
310
|
-
|
311
|
-
# TODO: raise an exception if the box coords or dimensions will place it off the canvas
|
312
|
-
rectangle(x,y,w,h, :color => options[:fill_color], :fill_color => options[:fill_color]) if options[:fill_color]
|
313
|
-
layout = build_pango_layout(str.to_s, textw, options)
|
314
|
-
|
315
|
-
set_color(options[:color])
|
316
|
-
|
317
|
-
# draw the context on our cairo layout
|
318
|
-
render_layout(layout, textx, texty, texth, :auto_new_page => false)
|
319
|
-
|
320
|
-
# draw a border around the cell
|
321
|
-
line(x,y,x+w,y, :color => options[:border_color], :line_width => options[:border_width]) if options[:border].include?("t")
|
322
|
-
line(x,y+h,x+w,y+h, :color => options[:border_color], :line_width => options[:border_width]) if options[:border].include?("b")
|
323
|
-
line(x,y,x,y+h, :color => options[:border_color], :line_width => options[:border_width]) if options[:border].include?("l")
|
324
|
-
line(x+w,y,x+w,y+h, :color => options[:border_color], :line_width => options[:border_width]) if options[:border].include?("r")
|
325
|
-
|
326
|
-
# restore the cursor position
|
327
|
-
move_to(origx, origy)
|
265
|
+
# return the number of points from startx to the right border
|
266
|
+
def points_to_right_margin(startx)
|
267
|
+
absolute_right_margin - startx
|
328
268
|
end
|
329
269
|
|
330
|
-
#
|
270
|
+
# Set a new location to be the origin (0,0). This is useful for repetitive tasks
|
271
|
+
# where objects need to be added to the canvas at regular offsets, and can save
|
272
|
+
# a significant amount of irritating co-ordinate maths.
|
331
273
|
#
|
332
|
-
#
|
274
|
+
# As an example, consider the following code fragment. If you have a series of images
|
275
|
+
# to arrange on a page with identical sizes, translate can help keep the code clean
|
276
|
+
# and readable by reducing (or removing completely) the need to perform a series of
|
277
|
+
# basic sums to calculate the correct offsets, etc.
|
333
278
|
#
|
334
|
-
#
|
335
|
-
#
|
279
|
+
# def captioned_image(filename, caption, x, y)
|
280
|
+
# @pdf.translate(x, y) do
|
281
|
+
# @pdf.image(filename, :top => 0, :left => 0, :height => 100, :width => 100, :proportional => true)
|
282
|
+
# @pdf.text(caption, :top => 110, :left => 0, :width => 100)
|
283
|
+
# end
|
284
|
+
# end
|
336
285
|
#
|
337
|
-
#
|
338
|
-
#
|
339
|
-
#
|
340
|
-
def
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
x, y = current_point
|
346
|
-
options = default_text_options.merge!({:left => x,
|
347
|
-
:top => y
|
348
|
-
})
|
349
|
-
options.merge!(opts)
|
350
|
-
options.assert_valid_keys(default_text_options.keys + default_positioning_options.keys)
|
351
|
-
options[:width] = body_width - options[:left] unless options[:width]
|
352
|
-
|
353
|
-
# move to the start of our table (the top left)
|
354
|
-
x = options[:left]
|
355
|
-
y = options[:top]
|
356
|
-
move_to(x,y)
|
357
|
-
|
358
|
-
# all columns will have the same width at this stage
|
359
|
-
cell_width = options[:width] / data.first.size
|
360
|
-
|
361
|
-
# draw the header cells
|
362
|
-
y = draw_table_row(data.shift, cell_width, options)
|
363
|
-
x = options[:left]
|
364
|
-
move_to(x,y)
|
365
|
-
|
366
|
-
# draw the data cells
|
367
|
-
data.each do |row|
|
368
|
-
y = draw_table_row(row, cell_width, options)
|
369
|
-
x = options[:left]
|
370
|
-
move_to(x,y)
|
286
|
+
# captioned_image("orc.svg", "Orc", 100, 100)
|
287
|
+
# captioned_image("hobbit.svg", "Hobbit", 100, 400)
|
288
|
+
# captioned_image("elf.svg", "Elf", 100, 400)
|
289
|
+
def translate(x, y, &block)
|
290
|
+
@context.save do
|
291
|
+
@context.translate(x, y)
|
292
|
+
yield
|
371
293
|
end
|
372
294
|
end
|
373
295
|
|
374
|
-
#
|
375
|
-
#
|
376
|
-
# By default the text will be rendered using all the space within the margins and using
|
377
|
-
# the default font styling set by default_font(), default_font_size, etc
|
378
|
-
#
|
379
|
-
# There is no way to place a bottom bound (or height) onto the text. Text will wrap as
|
380
|
-
# necessary and take all the room it needs. For finer grained control of text boxes, see the
|
381
|
-
# cell method.
|
382
|
-
#
|
383
|
-
# To override all these defaults, use the options hash
|
296
|
+
# all code wrapped in the block passed to this function will have co-ordinates
|
297
|
+
# and distances (width/height) multiplied by these values before being used
|
384
298
|
#
|
385
|
-
#
|
299
|
+
# Divide everything by 2
|
386
300
|
#
|
387
|
-
#
|
388
|
-
#
|
389
|
-
#
|
301
|
+
# pdf.scale(0.5, 0.5) do
|
302
|
+
# ...
|
303
|
+
# end
|
390
304
|
#
|
391
|
-
#
|
305
|
+
# Make the page 1.0 wide and 1.0 tall, so co-ordinates and distances
|
306
|
+
# can be specified as percentages (0.5 == 50%, etc)
|
392
307
|
#
|
393
|
-
#
|
394
|
-
#
|
395
|
-
#
|
396
|
-
# <tt>:justify</tt>:: Justify the text so it exapnds to fill the entire width of each line. Note that this only works in pango >= 1.17
|
397
|
-
# <tt>:spacing</tt>:: Space between lines in PDF points
|
398
|
-
def text(str, opts={})
|
399
|
-
# TODO: add support for pango markup (see http://ruby-gnome2.sourceforge.jp/hiki.cgi?pango-markup)
|
400
|
-
# TODO: add converters from various markup languages to pango markup. (bluecloth, redcloth, markdown, textile, etc)
|
401
|
-
# TODO: add a wrap option so wrapping can be disabled
|
402
|
-
#
|
403
|
-
# the non pango way to add text to the cairo context, not particularly useful for
|
404
|
-
# PDF generation as it doesn't support wrapping text or other advanced layout features
|
405
|
-
# and I really don't feel like re-implementing all that
|
406
|
-
# @context.show_text(str)
|
407
|
-
|
408
|
-
# the "pango way"
|
409
|
-
x, y = current_point
|
410
|
-
options = default_text_options.merge!({:left => x, :top => y})
|
411
|
-
options.merge!(opts)
|
412
|
-
options.assert_valid_keys(default_text_options.keys + default_positioning_options.keys)
|
413
|
-
|
414
|
-
# if the user hasn't specified a width, make the text wrap on the right margin
|
415
|
-
options[:width] = absolute_right_margin - options[:left] if options[:width].nil?
|
416
|
-
|
417
|
-
layout = build_pango_layout(str.to_s, options[:width], options)
|
418
|
-
|
419
|
-
set_color(options[:color])
|
420
|
-
|
421
|
-
# draw the context on our cairo layout
|
422
|
-
y = render_layout(layout, options[:left], options[:top], points_to_bottom_margin(options[:top]), :auto_new_page => true)
|
423
|
-
|
424
|
-
move_to(options[:left], y + 5)
|
425
|
-
end
|
426
|
-
|
427
|
-
# Returns the amount of vertical space needed to display the supplied text at the requested width
|
428
|
-
# opts is an options hash that specifies various attributes of the text. See the text function for more information.
|
429
|
-
def text_height(str, width, opts = {})
|
430
|
-
options = default_text_options.merge!(opts)
|
431
|
-
options[:width] = width || body_width
|
432
|
-
options.assert_valid_keys(default_text_options.keys + default_positioning_options.keys)
|
433
|
-
|
434
|
-
layout = build_pango_layout(str.to_s, options[:width], options)
|
435
|
-
width, height = layout.size
|
436
|
-
|
437
|
-
return height / Pango::SCALE
|
438
|
-
end
|
439
|
-
|
440
|
-
# Returns the amount of horizontal space needed to display the supplied text with the requested options
|
441
|
-
# opts is an options hash that specifies various attributes of the text. See the text function for more information.
|
442
|
-
# The text is assumed to not wrap.
|
443
|
-
def text_width(str, opts = {})
|
444
|
-
options = default_text_options.merge!(opts)
|
445
|
-
options.assert_valid_keys(default_text_options.keys)
|
446
|
-
|
447
|
-
layout = build_pango_layout(str.to_s, -1, options)
|
448
|
-
width, height = layout.size
|
449
|
-
|
450
|
-
return width / Pango::SCALE
|
451
|
-
end
|
452
|
-
|
453
|
-
#####################################################
|
454
|
-
# Functions relating to working with graphics
|
455
|
-
#####################################################
|
456
|
-
|
457
|
-
# draw a circle with radius r and a centre point at (x,y).
|
458
|
-
# Parameters:
|
459
|
-
# <tt>:x</tt>:: The x co-ordinate of the circle centre.
|
460
|
-
# <tt>:y</tt>:: The y co-ordinate of the circle centre.
|
461
|
-
# <tt>:r</tt>:: The radius of the circle
|
308
|
+
# pdf.scale(pdf.page_width.to_f, pdf.page_height.to_f) do
|
309
|
+
# ...
|
310
|
+
# end
|
462
311
|
#
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
# <tt>:fill_color</tt>:: The colour to fill the circle with. Defaults to nil (no fill)
|
467
|
-
def circle(x, y, r, opts = {})
|
468
|
-
options = {:color => @default_color,
|
469
|
-
:line_width => @default_line_width,
|
470
|
-
:fill_color => nil
|
471
|
-
}
|
472
|
-
options.merge!(opts)
|
473
|
-
options.assert_valid_keys(:color, :line_width, :fill_color)
|
474
|
-
|
475
|
-
# save the cursor position so we can restore it at the end
|
476
|
-
origx, origy = current_point
|
477
|
-
|
478
|
-
move_to(x + r, y)
|
312
|
+
def scale(w, h, &block)
|
313
|
+
@context.save do
|
314
|
+
@context.scale(w, h)
|
479
315
|
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
316
|
+
# set the line width again so that it's set relative to the current
|
317
|
+
# scale factor
|
318
|
+
line_width @line_width
|
319
|
+
yield
|
484
320
|
end
|
485
|
-
|
486
|
-
set_color(options[:color])
|
487
|
-
@context.set_line_width(options[:line_width])
|
488
|
-
@context.circle(x, y, r).stroke
|
489
|
-
|
490
|
-
# restore the cursor position
|
491
|
-
move_to(origx, origy)
|
492
321
|
end
|
493
322
|
|
494
|
-
#
|
495
|
-
#
|
496
|
-
# Options:
|
497
|
-
# <tt>:color</tt>:: The colour of the line
|
498
|
-
# <tt>:line_width</tt>:: The width of line. Defaults its 2.0
|
499
|
-
def line(x0, y0, x1, y1, opts = {})
|
500
|
-
options = {:color => @default_color, :line_width => @default_line_width }
|
501
|
-
options.merge!(opts)
|
502
|
-
options.assert_valid_keys(:color, :line_width)
|
503
|
-
|
504
|
-
# save the cursor position so we can restore it at the end
|
505
|
-
origx, origy = current_point
|
506
|
-
|
507
|
-
set_color(options[:color])
|
508
|
-
@context.set_line_width(options[:line_width])
|
509
|
-
move_to(x0,y0)
|
510
|
-
@context.line_to(x1,y1).stroke
|
511
|
-
|
512
|
-
# restore the cursor position
|
513
|
-
move_to(origx, origy)
|
514
|
-
end
|
515
|
-
|
516
|
-
# Adds a cubic Bezier spline to the path from the (x0, y0) to position (x3, y3)
|
517
|
-
# in user-space coordinates, using (x1, y1) and (x2, y2) as the control points.
|
518
|
-
# Options:
|
519
|
-
# <tt>:color</tt>:: The colour of the line
|
520
|
-
# <tt>:line_width</tt>:: The width of line. Defaults to 2.0
|
521
|
-
def curve(x0, y0, x1, y1, x2, y2, x3, y3, opts = {})
|
522
|
-
options = {:color => @default_color, :line_width => @default_line_width }
|
523
|
-
options.merge!(opts)
|
524
|
-
options.assert_valid_keys(:color, :line_width)
|
525
|
-
origx, origy = current_point
|
526
|
-
|
527
|
-
set_color(options[:color])
|
528
|
-
@context.set_line_width(options[:line_width])
|
529
|
-
move_to(x0,y0)
|
530
|
-
@context.curve_to(x1, y1, x2, y2, x3, y3).stroke
|
531
|
-
|
532
|
-
# restore the cursor position
|
533
|
-
move_to(origx, origy)
|
534
|
-
end
|
535
|
-
|
536
|
-
|
537
|
-
# draw a rectangle starting at x,y with w,h dimensions.
|
538
|
-
# Parameters:
|
539
|
-
# <tt>:x</tt>:: The x co-ordinate of the top left of the rectangle.
|
540
|
-
# <tt>:y</tt>:: The y co-ordinate of the top left of the rectangle.
|
541
|
-
# <tt>:w</tt>:: The width of the rectangle
|
542
|
-
# <tt>:h</tt>:: The height of the rectangle
|
323
|
+
# change the default colour used to draw on the canvas
|
543
324
|
#
|
544
|
-
# Options:
|
545
|
-
# <tt>:color</tt>:: The colour of the rectangle outline
|
546
|
-
# <tt>:line_width</tt>:: The width of outline. Defaults to 2.0
|
547
|
-
# <tt>:fill_color</tt>:: The colour to fill the rectangle with. Defaults to nil (no fill)
|
548
|
-
def rectangle(x, y, w, h, opts = {})
|
549
|
-
options = {:color => @default_color,
|
550
|
-
:line_width => @default_line_width,
|
551
|
-
:fill_color => nil
|
552
|
-
}
|
553
|
-
options.merge!(opts)
|
554
|
-
options.assert_valid_keys(:color, :line_width, :fill_color)
|
555
|
-
|
556
|
-
# save the cursor position so we can restore it at the end
|
557
|
-
origx, origy = current_point
|
558
|
-
|
559
|
-
# if the rectangle should be filled in
|
560
|
-
if options[:fill_color]
|
561
|
-
set_color(options[:fill_color])
|
562
|
-
@context.rectangle(x, y, w, h).fill
|
563
|
-
end
|
564
|
-
|
565
|
-
set_color(options[:color])
|
566
|
-
@context.set_line_width(options[:line_width])
|
567
|
-
@context.rectangle(x, y, w, h).stroke
|
568
|
-
|
569
|
-
# restore the cursor position
|
570
|
-
move_to(origx, origy)
|
571
|
-
end
|
572
|
-
|
573
|
-
# draw a rounded rectangle starting at x,y with w,h dimensions.
|
574
325
|
# Parameters:
|
575
|
-
# <tt
|
576
|
-
#
|
577
|
-
#
|
578
|
-
#
|
579
|
-
#
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
# <tt>:fill_color</tt>:: The colour to fill the rectangle with. Defaults to nil (no fill)
|
585
|
-
def rounded_rectangle(x, y, w, h, r, opts = {})
|
586
|
-
options = {:color => @default_color,
|
587
|
-
:line_width => @default_line_width,
|
588
|
-
:fill_color => nil
|
589
|
-
}
|
590
|
-
options.merge!(opts)
|
591
|
-
options.assert_valid_keys(:color, :fill_color, :line_width)
|
592
|
-
|
593
|
-
raise ArgumentError, "Argument r must be less than both w and h arguments" if r >= w || r >= h
|
594
|
-
|
595
|
-
# save the cursor position so we can restore it at the end
|
596
|
-
origx, origy = current_point
|
597
|
-
|
598
|
-
# if the rectangle should be filled in
|
599
|
-
if options[:fill_color]
|
600
|
-
set_color(options[:fill_color])
|
601
|
-
@context.rounded_rectangle(x, y, w, h, r).fill
|
602
|
-
end
|
603
|
-
|
604
|
-
set_color(options[:color])
|
605
|
-
@context.set_line_width(options[:line_width])
|
606
|
-
@context.rounded_rectangle(x, y, w, h, r).stroke
|
607
|
-
|
608
|
-
# restore the cursor position
|
609
|
-
move_to(origx, origy)
|
610
|
-
end
|
611
|
-
|
612
|
-
#####################################################
|
613
|
-
# Functions relating to working with images
|
614
|
-
#####################################################
|
615
|
-
|
616
|
-
# add an image to the page - a wide range of image formats are supported,
|
617
|
-
# including svg, jpg, png and gif. PDF images are also supported - an attempt
|
618
|
-
# to add a multipage PDF will result in only the first page appearing in the
|
619
|
-
# new document.
|
620
|
-
#
|
621
|
-
# supported options:
|
622
|
-
# <tt>:left</tt>:: The x co-ordinate of the left-hand side of the image.
|
623
|
-
# <tt>:top</tt>:: The y co-ordinate of the top of the image.
|
624
|
-
# <tt>:height</tt>:: The height of the image
|
625
|
-
# <tt>:width</tt>:: The width of the image
|
626
|
-
# <tt>:proportional</tt>:: Boolean. Maintain image proportions when scaling. Defaults to false.
|
627
|
-
# <tt>:padding</tt>:: Add some padding between the image and the specified box.
|
628
|
-
#
|
629
|
-
# left and top default to the current cursor location
|
630
|
-
# width and height default to the size of the imported image
|
631
|
-
# padding defaults to 0
|
632
|
-
def image(filename, opts = {})
|
633
|
-
# TODO: add some options for justification and padding
|
634
|
-
raise ArgumentError, "file #{filename} not found" unless File.file?(filename)
|
635
|
-
opts.assert_valid_keys(default_positioning_options.keys + [:padding, :proportional])
|
636
|
-
|
637
|
-
if opts[:padding]
|
638
|
-
opts[:left] += opts[:padding].to_i if opts[:left]
|
639
|
-
opts[:top] += opts[:padding].to_i if opts[:top]
|
640
|
-
opts[:width] -= opts[:padding].to_i * 2 if opts[:width]
|
641
|
-
opts[:height] -= opts[:padding].to_i * 2 if opts[:height]
|
642
|
-
end
|
643
|
-
|
644
|
-
case detect_image_type(filename)
|
645
|
-
when :pdf then draw_pdf filename, opts
|
646
|
-
when :png then draw_png filename, opts
|
647
|
-
when :svg then draw_svg filename, opts
|
648
|
-
else
|
649
|
-
begin
|
650
|
-
draw_pixbuf filename, opts
|
651
|
-
rescue Gdk::PixbufError
|
652
|
-
raise ArgumentError, "Unrecognised image format (#{filename})"
|
653
|
-
end
|
654
|
-
end
|
326
|
+
# <tt>c</tt>:: either a colour symbol recognised by rcairo (:red, :blue, :black, etc) or
|
327
|
+
# an array with 3-4 integer elements. The first 3 numbers are red, green and
|
328
|
+
# blue (0-255). The optional 4th number is the alpha channel and should be
|
329
|
+
# between 0 and 1. See the API docs at http://cairo.rubyforge.org/ for a list
|
330
|
+
# of predefined colours
|
331
|
+
def color(c)
|
332
|
+
c = translate_color(c)
|
333
|
+
validate_color(c)
|
334
|
+
@context.set_source_rgba(*c)
|
655
335
|
end
|
336
|
+
alias color= color
|
656
337
|
|
657
338
|
#####################################################
|
658
339
|
# Functions relating to generating the final document
|
@@ -693,8 +374,6 @@ module PDF
|
|
693
374
|
|
694
375
|
# move the cursor to an arbitary position on the current page
|
695
376
|
def move_to(x,y)
|
696
|
-
raise ArgumentError, 'x cannot be larger than the width of the page' if x > page_width
|
697
|
-
raise ArgumentError, 'y cannot be larger than the height of the page' if y > page_height
|
698
377
|
@context.move_to(x,y)
|
699
378
|
end
|
700
379
|
|
@@ -761,61 +440,6 @@ module PDF
|
|
761
440
|
|
762
441
|
private
|
763
442
|
|
764
|
-
def build_pango_layout(str, w, opts = {})
|
765
|
-
options = default_text_options.merge!(opts)
|
766
|
-
|
767
|
-
# if the user hasn't specified a width, make the layout as wide as the page body
|
768
|
-
w = body_width if w.nil?
|
769
|
-
|
770
|
-
# even though this is a private function, raise this error to force calling functions
|
771
|
-
# to decide how they want to handle converting non-strings into strings for rendering
|
772
|
-
raise ArgumentError, 'build_pango_layout must be passed a string' unless str.kind_of?(String)
|
773
|
-
|
774
|
-
# if we're running under a M17n aware VM, ensure the string provided is UTF-8 or can be
|
775
|
-
# converted to UTF-8
|
776
|
-
if RUBY_VERSION >= "1.9"
|
777
|
-
begin
|
778
|
-
str = str.encode("UTF-8")
|
779
|
-
rescue
|
780
|
-
raise ArgumentError, 'Strings must be supplied with a UTF-8 encoding, or an encoding that can be converted to UTF-8'
|
781
|
-
end
|
782
|
-
end
|
783
|
-
|
784
|
-
# The pango way:
|
785
|
-
load_libpango
|
786
|
-
|
787
|
-
# create a new Pango layout that our text will be added to
|
788
|
-
layout = @context.create_pango_layout
|
789
|
-
layout.text = str.to_s
|
790
|
-
if w == -1
|
791
|
-
layout.width = -1
|
792
|
-
else
|
793
|
-
layout.width = w * Pango::SCALE
|
794
|
-
end
|
795
|
-
layout.spacing = options[:spacing] * Pango::SCALE
|
796
|
-
|
797
|
-
# set the alignment of the text in the layout
|
798
|
-
if options[:alignment].eql?(:left)
|
799
|
-
layout.alignment = Pango::Layout::ALIGN_LEFT
|
800
|
-
elsif options[:alignment].eql?(:right)
|
801
|
-
layout.alignment = Pango::Layout::ALIGN_RIGHT
|
802
|
-
elsif options[:alignment].eql?(:center) || options[:alignment].eql?(:centre)
|
803
|
-
layout.alignment = Pango::Layout::ALIGN_CENTER
|
804
|
-
else
|
805
|
-
raise ArgumentError, "Invalid alignment requested"
|
806
|
-
end
|
807
|
-
|
808
|
-
# justify the text if need be - only works in pango >= 1.17
|
809
|
-
layout.justify = true if options[:justify]
|
810
|
-
|
811
|
-
# setup the font that will be used to render the text
|
812
|
-
fdesc = Pango::FontDescription.new(options[:font])
|
813
|
-
fdesc.set_size(options[:font_size] * Pango::SCALE)
|
814
|
-
layout.font_description = fdesc
|
815
|
-
@context.update_pango_layout(layout)
|
816
|
-
return layout
|
817
|
-
end
|
818
|
-
|
819
443
|
# runs the code in block, passing it a hash of options that might be
|
820
444
|
# required
|
821
445
|
def call_repeating_element(spec, block)
|
@@ -844,299 +468,51 @@ module PDF
|
|
844
468
|
}
|
845
469
|
end
|
846
470
|
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
:justify => false,
|
853
|
-
:spacing => 0
|
854
|
-
}
|
855
|
-
end
|
856
|
-
|
857
|
-
def detect_image_type(filename)
|
858
|
-
# read the first Kb from the file to attempt file type detection
|
859
|
-
f = File.new(filename)
|
860
|
-
bytes = f.read(1024)
|
861
|
-
|
862
|
-
# if the file is a PNG
|
863
|
-
if bytes[1,3].eql?("PNG")
|
864
|
-
return :png
|
865
|
-
elsif bytes[0,3].eql?("GIF")
|
866
|
-
return :gif
|
867
|
-
elsif bytes[0,4].eql?("%PDF")
|
868
|
-
return :pdf
|
869
|
-
elsif bytes.include?("<svg")
|
870
|
-
return :svg
|
871
|
-
elsif bytes.include?("Exif") || bytes.include?("JFIF")
|
872
|
-
return :jpg
|
873
|
-
else
|
874
|
-
return nil
|
875
|
-
end
|
876
|
-
end
|
877
|
-
|
878
|
-
def calc_image_dimensions(desired_w, desired_h, actual_w, actual_h, scale = false)
|
879
|
-
if scale
|
880
|
-
wp = desired_w / actual_w.to_f
|
881
|
-
hp = desired_h / actual_h.to_f
|
882
|
-
|
883
|
-
if wp < hp
|
884
|
-
width = actual_w * wp
|
885
|
-
height = actual_h * wp
|
886
|
-
else
|
887
|
-
width = actual_w * hp
|
888
|
-
height = actual_h * hp
|
889
|
-
end
|
890
|
-
else
|
891
|
-
width = desired_w || actual_w
|
892
|
-
height = desired_h || actual_h
|
893
|
-
end
|
894
|
-
return width.to_f, height.to_f
|
895
|
-
end
|
896
|
-
|
897
|
-
def draw_pdf(filename, opts = {})
|
898
|
-
# based on a similar function in rabbit. Thanks Kou.
|
899
|
-
load_libpoppler
|
900
|
-
x, y = current_point
|
901
|
-
page = Poppler::Document.new(filename).get_page(1)
|
902
|
-
w, h = page.size
|
903
|
-
width, height = calc_image_dimensions(opts[:width], opts[:height], w, h, opts[:proportional])
|
904
|
-
@context.save do
|
905
|
-
@context.translate(opts[:left] || x, opts[:top] || y)
|
906
|
-
@context.scale(width / w, height / h)
|
907
|
-
@context.render_poppler_page(page)
|
908
|
-
end
|
909
|
-
move_to(opts[:left] || x, (opts[:top] || y) + height)
|
910
|
-
end
|
911
|
-
|
912
|
-
def draw_pixbuf(filename, opts = {})
|
913
|
-
# based on a similar function in rabbit. Thanks Kou.
|
914
|
-
load_libpixbuf
|
915
|
-
x, y = current_point
|
916
|
-
pixbuf = Gdk::Pixbuf.new(filename)
|
917
|
-
width, height = calc_image_dimensions(opts[:width], opts[:height], pixbuf.width, pixbuf.height, opts[:proportional])
|
918
|
-
@context.save do
|
919
|
-
@context.translate(opts[:left] || x, opts[:top] || y)
|
920
|
-
@context.scale(width / pixbuf.width, height / pixbuf.height)
|
921
|
-
@context.set_source_pixbuf(pixbuf, 0, 0)
|
922
|
-
@context.paint
|
923
|
-
end
|
924
|
-
move_to(opts[:left] || x, (opts[:top] || y) + height)
|
925
|
-
end
|
926
|
-
|
927
|
-
def draw_png(filename, opts = {})
|
928
|
-
# based on a similar function in rabbit. Thanks Kou.
|
929
|
-
x, y = current_point
|
930
|
-
img_surface = Cairo::ImageSurface.from_png(filename)
|
931
|
-
width, height = calc_image_dimensions(opts[:width], opts[:height], img_surface.width, img_surface.height, opts[:proportional])
|
932
|
-
@context.save do
|
933
|
-
@context.translate(opts[:left] || x, opts[:top] || y)
|
934
|
-
@context.scale(width / img_surface.width, height / img_surface.height)
|
935
|
-
@context.set_source(img_surface, 0, 0)
|
936
|
-
@context.paint
|
937
|
-
end
|
938
|
-
move_to(opts[:left] || x, (opts[:top] || y) + height)
|
471
|
+
# save and restore the cursor position around a block
|
472
|
+
def save_coords(&block)
|
473
|
+
origx, origy = current_point
|
474
|
+
yield
|
475
|
+
move_to(origx, origy)
|
939
476
|
end
|
940
477
|
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
x, y = current_point
|
945
|
-
handle = RSVG::Handle.new_from_file(filename)
|
946
|
-
width, height = calc_image_dimensions(opts[:width], opts[:height], handle.width, handle.height, opts[:proportional])
|
478
|
+
# save and restore the cursor position and graphics state around a block
|
479
|
+
def save_coords_and_state(&block)
|
480
|
+
origx, origy = current_point
|
947
481
|
@context.save do
|
948
|
-
|
949
|
-
@context.scale(width / handle.width, height / handle.height)
|
950
|
-
@context.render_rsvg_handle(handle)
|
951
|
-
#@context.paint
|
952
|
-
end
|
953
|
-
move_to(opts[:left] || x, (opts[:top] || y) + height)
|
954
|
-
end
|
955
|
-
|
956
|
-
# adds a single table row to the canvas. Top left of the row will be at the current x,y
|
957
|
-
# co-ordinates, so make sure they're set correctly before calling this function
|
958
|
-
#
|
959
|
-
# strings - array of strings. Each element of the array is a cell
|
960
|
-
# column_widths - the width of each column. At this stage it should be an int. All columns are the same width
|
961
|
-
# options - any options relating to text style to use. font, font_size, alignment, etc. See text() for more info.
|
962
|
-
#
|
963
|
-
# Returns the y co-ordinates of the bottom edge of the row, ready for the next row
|
964
|
-
def draw_table_row(strings, column_widths, options)
|
965
|
-
row_height = 0
|
966
|
-
x, y = current_point
|
967
|
-
|
968
|
-
# we run all this code twice. The first time is a dry run to calculate the
|
969
|
-
# height of the largest cell, which determines the overall height of the row.
|
970
|
-
# The second run through we actually draw each cell onto the canvas
|
971
|
-
[:dry, :paint].each do |action|
|
972
|
-
|
973
|
-
strings.each do |head|
|
974
|
-
# TODO: provide a way for these to be overridden on a per cell basis
|
975
|
-
opts = {
|
976
|
-
:font => options[:font],
|
977
|
-
:font_size => options[:font_size],
|
978
|
-
:color => options[:color],
|
979
|
-
:alignment => options[:alignment],
|
980
|
-
:justify => options[:justify],
|
981
|
-
:spacing => options[:spacing]
|
982
|
-
}
|
983
|
-
|
984
|
-
if action == :dry
|
985
|
-
# calc the cell height, and set row_height if this cell is the biggest in the row
|
986
|
-
cell_height = text_height(head, column_widths, opts)
|
987
|
-
row_height = cell_height if cell_height > row_height
|
988
|
-
else
|
989
|
-
# start a new page if necesary
|
990
|
-
if row_height > (absolute_bottom_margin - y)
|
991
|
-
start_new_page
|
992
|
-
y = margin_top
|
993
|
-
end
|
994
|
-
|
995
|
-
# add our cell, then advance x to the left edge of the next cell
|
996
|
-
self.cell(head, x, y, column_widths, row_height, opts)
|
997
|
-
x += column_widths
|
998
|
-
end
|
999
|
-
|
1000
|
-
end
|
1001
|
-
end
|
1002
|
-
|
1003
|
-
return y + row_height
|
1004
|
-
end
|
1005
|
-
|
1006
|
-
def image_dimensions(filename)
|
1007
|
-
raise ArgumentError, "file #{filename} not found" unless File.file?(filename)
|
1008
|
-
|
1009
|
-
case detect_image_type(filename)
|
1010
|
-
when :pdf then
|
1011
|
-
load_libpoppler
|
1012
|
-
page = Poppler::Document.new(filename).get_page(1)
|
1013
|
-
return page.size
|
1014
|
-
when :png then
|
1015
|
-
img_surface = Cairo::ImageSurface.from_png(filename)
|
1016
|
-
return img_surface.width, img_surface.height
|
1017
|
-
when :svg then
|
1018
|
-
load_librsvg
|
1019
|
-
handle = RSVG::Handle.new_from_file(filename)
|
1020
|
-
return handle.width, handle.height
|
1021
|
-
else
|
1022
|
-
load_libpixbuf
|
1023
|
-
begin
|
1024
|
-
pixbuf = Gdk::Pixbuf.new(filename)
|
1025
|
-
return pixbuf.width, pixbuf.height
|
1026
|
-
rescue Gdk::PixbufError
|
1027
|
-
raise ArgumentError, "Unrecognised image format (#{filename})"
|
1028
|
-
end
|
482
|
+
yield
|
1029
483
|
end
|
484
|
+
move_to(origx, origy)
|
1030
485
|
end
|
1031
486
|
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
begin
|
1038
|
-
require 'pango' unless @context.respond_to? :create_pango_layout
|
1039
|
-
rescue LoadError
|
1040
|
-
raise LoadError, 'Ruby/Pango library not found. Visit http://ruby-gnome2.sourceforge.jp/'
|
1041
|
-
end
|
487
|
+
def translate_color(c)
|
488
|
+
# the follow line converts a color definition from various formats (hex, symbol, etc)
|
489
|
+
# into a 4 item array. This is normally handled within cairo itself, however when
|
490
|
+
# Cairo and Poppler are both loaded, it breaks.
|
491
|
+
Cairo::Color.parse(c).to_rgb.to_a
|
1042
492
|
end
|
1043
493
|
|
1044
|
-
|
1045
|
-
|
1046
|
-
# its own classes and constants.
|
1047
|
-
def load_libpixbuf
|
1048
|
-
begin
|
1049
|
-
require 'gdk_pixbuf2' unless @context.respond_to? :set_source_pixbuf
|
1050
|
-
rescue LoadError
|
1051
|
-
raise LoadError, 'Ruby/GdkPixbuf library not found. Visit http://ruby-gnome2.sourceforge.jp/'
|
1052
|
-
end
|
494
|
+
def user_to_device_dist(x,y)
|
495
|
+
@context.user_to_device_distance(x, y)
|
1053
496
|
end
|
1054
497
|
|
1055
|
-
|
1056
|
-
|
1057
|
-
# its own classes and constants.
|
1058
|
-
def load_libpoppler
|
1059
|
-
begin
|
1060
|
-
require 'poppler' unless @context.respond_to? :render_poppler_page
|
1061
|
-
rescue LoadError
|
1062
|
-
raise LoadError, 'Ruby/Poppler library not found. Visit http://ruby-gnome2.sourceforge.jp/'
|
1063
|
-
end
|
498
|
+
def user_x_to_device_x(x)
|
499
|
+
@context.user_to_device(x, 0).first.abs
|
1064
500
|
end
|
1065
501
|
|
1066
|
-
|
1067
|
-
|
1068
|
-
# that allows an existing SVG to be drawn directly onto it
|
1069
|
-
# There's a *little* bit of documentation at:
|
1070
|
-
# http://ruby-gnome2.sourceforge.jp/fr/hiki.cgi?Cairo%3A%3AContext#render_rsvg_handle
|
1071
|
-
def load_librsvg
|
1072
|
-
begin
|
1073
|
-
require 'rsvg2' unless @context.respond_to? :render_svg_handle
|
1074
|
-
rescue LoadError
|
1075
|
-
raise LoadError, 'Ruby/RSVG library not found. Visit http://ruby-gnome2.sourceforge.jp/'
|
1076
|
-
end
|
502
|
+
def user_y_to_device_y(y)
|
503
|
+
@context.user_to_device(0, y).last.abs
|
1077
504
|
end
|
1078
505
|
|
1079
|
-
|
1080
|
-
|
1081
|
-
# distributed with rcairo - it's still black magic to me and has a few edge
|
1082
|
-
# cases where it doesn't work too well. Needs to be improved.
|
1083
|
-
def render_layout(layout, x, y, h, opts = {})
|
1084
|
-
# we can't use context.show_pango_layout, as that won't start
|
1085
|
-
# a new page if the layout hits the bottom margin. Instead,
|
1086
|
-
# we iterate over each line of text in the layout and add it to
|
1087
|
-
# the canvas, page breaking as necessary
|
1088
|
-
options = {:auto_new_page => true }
|
1089
|
-
options.merge!(opts)
|
1090
|
-
|
1091
|
-
offset = 0
|
1092
|
-
baseline = 0
|
1093
|
-
|
1094
|
-
iter = layout.iter
|
1095
|
-
loop do
|
1096
|
-
line = iter.line
|
1097
|
-
ink_rect, logical_rect = iter.line_extents
|
1098
|
-
if y + (baseline - offset) >= (y + h)
|
1099
|
-
# our text is using the maximum amount of vertical space we want it to
|
1100
|
-
if options[:auto_new_page]
|
1101
|
-
# create a new page and we can continue adding text
|
1102
|
-
offset += baseline
|
1103
|
-
start_new_page
|
1104
|
-
else
|
1105
|
-
# the user doesn't want us to continue on the next page, so
|
1106
|
-
# stop adding lines to the canvas
|
1107
|
-
break
|
1108
|
-
end
|
1109
|
-
end
|
1110
|
-
|
1111
|
-
# move to the start of the next line
|
1112
|
-
#move_to(x, y)
|
1113
|
-
baseline = iter.baseline / Pango::SCALE
|
1114
|
-
@context.move_to(x + logical_rect.x / Pango::SCALE, y + baseline - offset)
|
1115
|
-
|
1116
|
-
# draw the line on the canvas
|
1117
|
-
@context.show_pango_layout_line(line)
|
1118
|
-
|
1119
|
-
break unless iter.next_line!
|
1120
|
-
end
|
1121
|
-
|
1122
|
-
# return the y co-ord we finished on
|
1123
|
-
return y + baseline - offset
|
506
|
+
def device_to_user_dist(x, y)
|
507
|
+
@context.device_to_user_distance(x, y)
|
1124
508
|
end
|
1125
509
|
|
1126
|
-
def
|
1127
|
-
|
1128
|
-
# into a 4 item array. This is normally handled within cairo itself, however when
|
1129
|
-
# Cairo and Poppler are both loaded, it breaks.
|
1130
|
-
Cairo::Color.parse(c).to_rgb.to_a
|
510
|
+
def device_x_to_user_x(x)
|
511
|
+
@context.device_to_user(x, 0).first.abs
|
1131
512
|
end
|
1132
513
|
|
1133
|
-
|
1134
|
-
|
1135
|
-
# for info on what is valid, see the comments for default_color
|
1136
|
-
def set_color(c)
|
1137
|
-
c = translate_color(c)
|
1138
|
-
validate_color(c)
|
1139
|
-
@context.set_source_rgba(*c)
|
514
|
+
def device_y_to_user_y(y)
|
515
|
+
@context.device_to_user(0, y).last.abs
|
1140
516
|
end
|
1141
517
|
|
1142
518
|
# test to see if the specified colour is a a valid cairo color
|