pdf-wrapper 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +2 -0
- data/DESIGN +29 -0
- data/README +73 -0
- data/Rakefile +76 -0
- data/examples/cell.rb +9 -0
- data/examples/google.png +0 -0
- data/examples/image.pdf +0 -0
- data/examples/image.rb +12 -0
- data/examples/shapes.pdf +66 -0
- data/examples/shapes.rb +12 -0
- data/examples/table.rb +17 -0
- data/examples/utf8.rb +11 -0
- data/lib/pdf/core.rb +8 -0
- data/lib/pdf/wrapper.rb +839 -0
- metadata +73 -0
data/CHANGELOG
ADDED
data/DESIGN
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
*************************************
|
2
|
+
* Overarching Design Principles
|
3
|
+
*************************************
|
4
|
+
- low level canvas drawing API
|
5
|
+
- add text
|
6
|
+
- add shapes / images
|
7
|
+
|
8
|
+
- higher level "widget" API
|
9
|
+
- text boxes
|
10
|
+
- form helpers ( check boxes, etc)
|
11
|
+
- water mark
|
12
|
+
- repeating elements (page numbers, headers/footers, etc)
|
13
|
+
- image boxes
|
14
|
+
- lists
|
15
|
+
- tables (port simple table?)
|
16
|
+
|
17
|
+
*************************************
|
18
|
+
* Thoughts on the table API
|
19
|
+
*************************************
|
20
|
+
|
21
|
+
FPDF::Table
|
22
|
+
- .table(data = [], columns = [])
|
23
|
+
- http://source.mihelac.org/2006/6/19/creating-pdf-documents-with-tables-in-ruby-rails#comments
|
24
|
+
|
25
|
+
PDF::SimpleTable
|
26
|
+
- .table(data = [{}], columns = []
|
27
|
+
|
28
|
+
XHTML?
|
29
|
+
- .table(data = String, opts = {})
|
data/README
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
= Overview
|
2
|
+
|
3
|
+
PDF::Wrapper is a PDF generation library that uses the cairo and pango
|
4
|
+
libraries to do the heavy lifting. I've essentially just wrapped these general
|
5
|
+
purpose graphics libraries with some sugar that makes them a little easier to
|
6
|
+
use for making PDFs. The idea is to lever the low level tools in those libraries
|
7
|
+
(drawing shapes, laying out text, importing raster images, etc) to build some
|
8
|
+
higher level tools - tables, text boxes, borders, lists, repeating elements
|
9
|
+
(headers/footers), etc.
|
10
|
+
|
11
|
+
At this stage the API is *roughly* following that of PDF::Writer, but i've made
|
12
|
+
tweaks in some places and added some new methods. This is a work in progress so
|
13
|
+
many features of PDF::Writer aren't available yet.
|
14
|
+
|
15
|
+
A key motivation for writing this library is cairo's support for Unicode in PDFs.
|
16
|
+
All text functions in this library support UTF8 input, although as a native
|
17
|
+
English speaker I've only tested this a little, so any feedback is welcome.
|
18
|
+
|
19
|
+
There also seems to be a lack of English documentation available for the ruby
|
20
|
+
bindings to cairo/pango, so I'm aiming to document the code as much as possible
|
21
|
+
to provide worked examples for others. I'm learning as I go though, so if regular
|
22
|
+
users of either library spot techniques that fail best practice, please let me know.
|
23
|
+
|
24
|
+
It's early days, so the API is far from stable and I'm hesitant to write extensive
|
25
|
+
documentation just yet. It's the price you pay for being an early adopter. The
|
26
|
+
examples/ dir should have a range of sample code, and I'll try to keep it up to
|
27
|
+
date.
|
28
|
+
|
29
|
+
I welcome all feedback, feature requests, patches and suggestions. In
|
30
|
+
particular, what high level widgets would you like to see? What do you use when
|
31
|
+
building reports and documents in GUI programs?
|
32
|
+
|
33
|
+
= Installation
|
34
|
+
|
35
|
+
The recommended installation method is via Rubygems.
|
36
|
+
|
37
|
+
gem install pdf-wrapper
|
38
|
+
|
39
|
+
= Author
|
40
|
+
|
41
|
+
James Healy <jimmy@deefa.com>
|
42
|
+
|
43
|
+
= License
|
44
|
+
|
45
|
+
* GPL version 2 or the Ruby License
|
46
|
+
* Ruby: http://www.ruby-lang.org/en/LICENSE.txt
|
47
|
+
|
48
|
+
= Dependencies
|
49
|
+
|
50
|
+
* ruby/cairo
|
51
|
+
* ruby/pango (optional, required to add text)
|
52
|
+
* ruby/rsvg2 (optional, required for SVG support)
|
53
|
+
|
54
|
+
These are all ruby bindings to C libraries. On Debian/Ubuntu based systems
|
55
|
+
(which I develop on) you can get them by running:
|
56
|
+
|
57
|
+
aptitude install libcairo-ruby libpango1-ruby librsvg2-ruby
|
58
|
+
|
59
|
+
For users of other systems, I'd love to receive info on how you set these bindings up.
|
60
|
+
|
61
|
+
ruby/cairo is also available as a gem (cairo), which may be installable if you have a copy
|
62
|
+
of the cairo source available on your system.
|
63
|
+
|
64
|
+
= Compatibility
|
65
|
+
|
66
|
+
JRuby users, you're probably out of luck.
|
67
|
+
|
68
|
+
Rubinius users, I have no idea.
|
69
|
+
|
70
|
+
Ruby1.9 users, the current release of ruby/cairo (1.5.0) doesn't work with 1.9,
|
71
|
+
but the version in SVN does. Hopefully it will be released soon. The version in
|
72
|
+
Debian has been patched to work with 1.9 already. PDF::Wrapper itself is 1.9
|
73
|
+
compatible.
|
data/Rakefile
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/clean'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/testtask'
|
6
|
+
require "rake/gempackagetask"
|
7
|
+
require 'spec/rake/spectask'
|
8
|
+
|
9
|
+
PKG_VERSION = "0.0.1"
|
10
|
+
PKG_NAME = "pdf-wrapper"
|
11
|
+
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
12
|
+
|
13
|
+
desc "Default Task"
|
14
|
+
task :default => [ :spec ]
|
15
|
+
|
16
|
+
# run all rspecs
|
17
|
+
desc "Run all rspec files"
|
18
|
+
Spec::Rake::SpecTask.new("spec") do |t|
|
19
|
+
t.spec_files = FileList['specs/**/*.rb']
|
20
|
+
t.rcov = true
|
21
|
+
t.rcov_dir = (ENV['CC_BUILD_ARTIFACTS'] || 'doc') + "/rcov"
|
22
|
+
t.rcov_opts = ["--exclude","spec.*\.rb","--exclude",".*cairo.*","--exclude",".*rcov.*","--exclude",".*rspec.*","--exclude",".*df-reader.*"]
|
23
|
+
end
|
24
|
+
|
25
|
+
# generate specdocs
|
26
|
+
desc "Generate Specdocs"
|
27
|
+
Spec::Rake::SpecTask.new("specdocs") do |t|
|
28
|
+
t.spec_files = FileList['specs/**/*.rb']
|
29
|
+
t.spec_opts = ["--format", "rdoc"]
|
30
|
+
t.out = (ENV['CC_BUILD_ARTIFACTS'] || 'doc') + '/specdoc.rd'
|
31
|
+
end
|
32
|
+
|
33
|
+
# generate failing spec report
|
34
|
+
desc "Generate failing spec report"
|
35
|
+
Spec::Rake::SpecTask.new("spec_report") do |t|
|
36
|
+
t.spec_files = FileList['specs/**/*.rb']
|
37
|
+
t.spec_opts = ["--format", "html", "--diff"]
|
38
|
+
t.out = (ENV['CC_BUILD_ARTIFACTS'] || 'doc') + '/spec_report.html'
|
39
|
+
t.fail_on_error = false
|
40
|
+
end
|
41
|
+
|
42
|
+
# Genereate the RDoc documentation
|
43
|
+
desc "Create documentation"
|
44
|
+
Rake::RDocTask.new("doc") do |rdoc|
|
45
|
+
rdoc.title = "pdf-wrapper"
|
46
|
+
rdoc.rdoc_dir = (ENV['CC_BUILD_ARTIFACTS'] || 'doc') + '/rdoc'
|
47
|
+
rdoc.rdoc_files.include('README')
|
48
|
+
rdoc.rdoc_files.include('CHANGELOG')
|
49
|
+
rdoc.rdoc_files.include('DESIGN')
|
50
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
51
|
+
rdoc.options << "--inline-source"
|
52
|
+
end
|
53
|
+
|
54
|
+
# a gemspec for packaging this library
|
55
|
+
spec = Gem::Specification.new do |spec|
|
56
|
+
spec.name = PKG_NAME
|
57
|
+
spec.version = PKG_VERSION
|
58
|
+
spec.platform = Gem::Platform::RUBY
|
59
|
+
spec.summary = "A PDF generating library built on top of cairo"
|
60
|
+
spec.files = Dir.glob("{examples,lib}/**/**/*") + ["Rakefile"]
|
61
|
+
spec.require_path = "lib"
|
62
|
+
spec.has_rdoc = true
|
63
|
+
spec.extra_rdoc_files = %w{README DESIGN CHANGELOG}
|
64
|
+
spec.rdoc_options << '--title' << 'PDF::Wrapper Documentation' << '--main' << 'README' << '-q'
|
65
|
+
spec.author = "James Healy"
|
66
|
+
spec.email = "jimmy@deefa.com"
|
67
|
+
spec.rubyforge_project = "pdf-wrapper"
|
68
|
+
spec.description = "A PDF writing library that uses the cairo and pango libraries to do the heavy lifting."
|
69
|
+
end
|
70
|
+
|
71
|
+
# package the library into a gem
|
72
|
+
desc "Generate a gem for pdf-wrapper"
|
73
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
74
|
+
pkg.need_zip = true
|
75
|
+
pkg.need_tar = true
|
76
|
+
end
|
data/examples/cell.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
4
|
+
|
5
|
+
require 'pdf/wrapper'
|
6
|
+
|
7
|
+
pdf = PDF::Wrapper.new(:paper => :A4)
|
8
|
+
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 => :white, :bgcolor => :black})
|
9
|
+
pdf.render_to_file("wrapper-cell.pdf")
|
data/examples/google.png
ADDED
Binary file
|
data/examples/image.pdf
ADDED
Binary file
|
data/examples/image.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
4
|
+
|
5
|
+
require 'pdf/wrapper'
|
6
|
+
|
7
|
+
pdf = PDF::Wrapper.new(:paper => :A4)
|
8
|
+
pdf.default_font("Sans Serif")
|
9
|
+
pdf.default_color(:black)
|
10
|
+
pdf.text("PDF::Wrapper Supports Images", :alignment => :center)
|
11
|
+
pdf.image(File.dirname(__FILE__) + "/google.png", :left => 100, :top => 250)
|
12
|
+
pdf.render_to_file("image.pdf")
|
data/examples/shapes.pdf
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
%PDF-1.4
|
2
|
+
%����
|
3
|
+
2 0 obj
|
4
|
+
<< /Length 3 0 R
|
5
|
+
/Filter /FlateDecode
|
6
|
+
/Type /XObject
|
7
|
+
/Subtype /Form
|
8
|
+
/BBox [ 0 0 595.28 841.89 ]
|
9
|
+
>>
|
10
|
+
stream
|
11
|
+
x��R�N�0�W�?�(v�� !qX8"(�V��a��O�<�yQ��ӎ�c� ��G�G8~�/<����-p����;��%0�s�6��u�`S0g���>)+�^�Ey�1�(�S߽�}GO���ɑ|��J�;z w�A�`��8ܶ>�(Wق�i�ɱ�jhēd�Ӄ3�5d�)�wIs���X�q�Rr���~0��l��S�,��K�m��>A�mZ���pPb3�0VS`8�X%I���"�BR�ز��dj���^A*�%�H�Z���^���z�)��v�����3];�'a��q�hz����-b���͚C^��N
|
12
|
+
endstream
|
13
|
+
endobj
|
14
|
+
3 0 obj
|
15
|
+
342
|
16
|
+
endobj
|
17
|
+
4 0 obj
|
18
|
+
<< /Type /Page
|
19
|
+
/Parent 1 0 R
|
20
|
+
/MediaBox [ 0 0 595.28 841.89 ]
|
21
|
+
/Contents [ 2 0 R ]
|
22
|
+
/Group <<
|
23
|
+
/Type /Group
|
24
|
+
/S /Transparency
|
25
|
+
/CS /DeviceRGB
|
26
|
+
>>
|
27
|
+
>>
|
28
|
+
endobj
|
29
|
+
1 0 obj
|
30
|
+
<< /Type /Pages
|
31
|
+
/Kids [ 4 0 R ]
|
32
|
+
/Count 1
|
33
|
+
/Resources <<
|
34
|
+
/ExtGState <<
|
35
|
+
/a0 << /CA 1 /ca 1 >>
|
36
|
+
>>
|
37
|
+
>>
|
38
|
+
>>
|
39
|
+
endobj
|
40
|
+
5 0 obj
|
41
|
+
<< /Creator (cairo 1.4.12 (http://cairographics.org))
|
42
|
+
/Producer (cairo 1.4.12 (http://cairographics.org))
|
43
|
+
>>
|
44
|
+
endobj
|
45
|
+
6 0 obj
|
46
|
+
<< /Type /Catalog
|
47
|
+
/Pages 1 0 R
|
48
|
+
>>
|
49
|
+
endobj
|
50
|
+
xref
|
51
|
+
0 7
|
52
|
+
0000000000 65535 f
|
53
|
+
0000000739 00000 n
|
54
|
+
0000000017 00000 n
|
55
|
+
0000000512 00000 n
|
56
|
+
0000000537 00000 n
|
57
|
+
0000000898 00000 n
|
58
|
+
0000001030 00000 n
|
59
|
+
trailer
|
60
|
+
<< /Size 7
|
61
|
+
/Root 6 0 R
|
62
|
+
/Info 5 0 R
|
63
|
+
>>
|
64
|
+
startxref
|
65
|
+
1087
|
66
|
+
%%EOF
|
data/examples/shapes.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
4
|
+
|
5
|
+
require 'pdf/wrapper'
|
6
|
+
|
7
|
+
pdf = PDF::Wrapper.new(:paper => :A4)
|
8
|
+
pdf.rectangle(30,30,100,100, :fill_color => :red)
|
9
|
+
pdf.circle(100,300,30)
|
10
|
+
pdf.line(100, 350, 400, 150)
|
11
|
+
pdf.rounded_rectangle(300,300, 200, 200, 10, :fill_color => :green)
|
12
|
+
pdf.render_to_file("shapes.pdf")
|
data/examples/table.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
4
|
+
|
5
|
+
require 'pdf/wrapper'
|
6
|
+
|
7
|
+
pdf = PDF::Wrapper.new(:paper => :A4)
|
8
|
+
pdf.text "Chunky Bacon!!"
|
9
|
+
data = [%w{one two three four}]
|
10
|
+
|
11
|
+
data << ["This is some longer text to ensure that the cell wraps",2,3,4]
|
12
|
+
|
13
|
+
(1..100).each do
|
14
|
+
data << %w{1 2 3 4}
|
15
|
+
end
|
16
|
+
pdf.table(data, :font_size => 10)
|
17
|
+
pdf.render_to_file("table.pdf")
|
data/examples/utf8.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
4
|
+
|
5
|
+
require 'pdf/wrapper'
|
6
|
+
|
7
|
+
pdf = PDF::Wrapper.new(:paper => :A4)
|
8
|
+
pdf.default_font("Sans Serif")
|
9
|
+
pdf.default_color(:black)
|
10
|
+
pdf.text File.read(File.dirname(__FILE__) + "/../specs/data/utf8.txt"), :font => "Monospace", :font_size => 8
|
11
|
+
pdf.render_to_file("wrapper.pdf")
|
data/lib/pdf/core.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
class Hash
|
2
|
+
# raise an error if this hash has any keys that aren't in the supplied list
|
3
|
+
# - borrowed from activesupport
|
4
|
+
def assert_valid_keys(*valid_keys)
|
5
|
+
unknown_keys = keys - [valid_keys].flatten
|
6
|
+
raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
|
7
|
+
end
|
8
|
+
end
|
data/lib/pdf/wrapper.rb
ADDED
@@ -0,0 +1,839 @@
|
|
1
|
+
# -* coding: UTF-8 -*-
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
require 'pdf/core'
|
5
|
+
|
6
|
+
# try to load cairo from the standard places, but don't worry if it fails,
|
7
|
+
# we'll try to find it via rubygems
|
8
|
+
begin
|
9
|
+
require 'cairo'
|
10
|
+
rescue LoadError
|
11
|
+
begin
|
12
|
+
require 'rubygems'
|
13
|
+
gem 'cairo', '>=1.5'
|
14
|
+
require 'cairo'
|
15
|
+
rescue Gem::LoadError
|
16
|
+
raise LoadError, "Could not find the ruby cairo bindings in the standard locations or via rubygems. Check to ensure they're installed correctly"
|
17
|
+
rescue LoadError
|
18
|
+
raise LoadError, "Could not load rubygems"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module PDF
|
23
|
+
# Create PDF files by using the cairo and pango libraries.
|
24
|
+
#
|
25
|
+
# Rendering to a file:
|
26
|
+
#
|
27
|
+
# require 'pdfwrapper'
|
28
|
+
# pdf = PDF::Wrapper.new(:paper => :A4)
|
29
|
+
# pdf.text "Hello World"
|
30
|
+
# pdf.render_to_file("wrapper.pdf")
|
31
|
+
#
|
32
|
+
# Rendering to a string:
|
33
|
+
#
|
34
|
+
# require 'pdfwrapper'
|
35
|
+
# pdf = PDF::Wrapper.new(:paper => :A4)
|
36
|
+
# pdf.text "Hello World", :font_size => 16
|
37
|
+
# puts pdf.render
|
38
|
+
#
|
39
|
+
# Changing the default font:
|
40
|
+
#
|
41
|
+
# require 'pdfwrapper'
|
42
|
+
# pdf = PDF::Wrapper.new(:paper => :A4)
|
43
|
+
# pdf.default_font("Monospace")
|
44
|
+
# pdf.text "Hello World", :font => "Sans Serif", :font_size => 18
|
45
|
+
# pdf.text "Pretend this is a code sample"
|
46
|
+
# puts pdf.render
|
47
|
+
class Wrapper
|
48
|
+
|
49
|
+
attr_reader :margin_left, :margin_right, :margin_top, :margin_bottom
|
50
|
+
attr_reader :page_width, :page_height
|
51
|
+
|
52
|
+
# borrowed from PDF::Writer
|
53
|
+
PAGE_SIZES = { # :value {...}:
|
54
|
+
#:4A0 => [4767.87, 6740.79], :2A0 => [3370.39, 4767.87],
|
55
|
+
:A0 => [2383.94, 3370.39], :A1 => [1683.78, 2383.94],
|
56
|
+
:A2 => [1190.55, 1683.78], :A3 => [841.89, 1190.55],
|
57
|
+
:A4 => [595.28, 841.89], :A5 => [419.53, 595.28],
|
58
|
+
:A6 => [297.64, 419.53], :A7 => [209.76, 297.64],
|
59
|
+
:A8 => [147.40, 209.76], :A9 => [104.88, 147.40],
|
60
|
+
:A10 => [73.70, 104.88], :B0 => [2834.65, 4008.19],
|
61
|
+
:B1 => [2004.09, 2834.65], :B2 => [1417.32, 2004.09],
|
62
|
+
:B3 => [1000.63, 1417.32], :B4 => [708.66, 1000.63],
|
63
|
+
:B5 => [498.90, 708.66], :B6 => [354.33, 498.90],
|
64
|
+
:B7 => [249.45, 354.33], :B8 => [175.75, 249.45],
|
65
|
+
:B9 => [124.72, 175.75], :B10 => [87.87, 124.72],
|
66
|
+
:C0 => [2599.37, 3676.54], :C1 => [1836.85, 2599.37],
|
67
|
+
:C2 => [1298.27, 1836.85], :C3 => [918.43, 1298.27],
|
68
|
+
:C4 => [649.13, 918.43], :C5 => [459.21, 649.13],
|
69
|
+
:C6 => [323.15, 459.21], :C7 => [229.61, 323.15],
|
70
|
+
:C8 => [161.57, 229.61], :C9 => [113.39, 161.57],
|
71
|
+
:C10 => [79.37, 113.39], :RA0 => [2437.80, 3458.27],
|
72
|
+
:RA1 => [1729.13, 2437.80], :RA2 => [1218.90, 1729.13],
|
73
|
+
:RA3 => [864.57, 1218.90], :RA4 => [609.45, 864.57],
|
74
|
+
:SRA0 => [2551.18, 3628.35], :SRA1 => [1814.17, 2551.18],
|
75
|
+
:SRA2 => [1275.59, 1814.17], :SRA3 => [907.09, 1275.59],
|
76
|
+
:SRA4 => [637.80, 907.09], :LETTER => [612.00, 792.00],
|
77
|
+
:LEGAL => [612.00, 1008.00], :FOLIO => [612.00, 936.00],
|
78
|
+
:EXECUTIVE => [521.86, 756.00]
|
79
|
+
}
|
80
|
+
|
81
|
+
# create a new PDF::Wrapper class to compose a PDF document
|
82
|
+
# Options:
|
83
|
+
# <tt>:paper</tt>:: The paper size to use (default :A4)
|
84
|
+
# <tt>:orientation</tt>:: :portrait (default) or :landscape
|
85
|
+
# <tt>:background_colour</tt>:: The background colour to use (default :white)
|
86
|
+
def initialize(opts={})
|
87
|
+
options = {:paper => :A4,
|
88
|
+
:orientation => :portrait,
|
89
|
+
:background_colour => :white
|
90
|
+
}
|
91
|
+
options.merge!(opts)
|
92
|
+
|
93
|
+
# test for invalid options
|
94
|
+
raise ArgumentError, "Invalid paper option" unless PAGE_SIZES.include?(options[:paper])
|
95
|
+
|
96
|
+
# set page dimensions
|
97
|
+
if options[:orientation].eql?(:portrait)
|
98
|
+
@page_width = PAGE_SIZES[options[:paper]][0]
|
99
|
+
@page_height = PAGE_SIZES[options[:paper]][1]
|
100
|
+
elsif options[:orientation].eql?(:landscape)
|
101
|
+
@page_width = PAGE_SIZES[options[:paper]][1]
|
102
|
+
@page_height = PAGE_SIZES[options[:paper]][0]
|
103
|
+
else
|
104
|
+
raise ArgumentError, "Invalid orientation"
|
105
|
+
end
|
106
|
+
|
107
|
+
# set page margins and dimensions of usable canvas
|
108
|
+
# TODO: add options for customising the margins. ATM they're always 5% of the page dimensions
|
109
|
+
@margin_left = (@page_width * 0.05).ceil
|
110
|
+
@margin_right = (@page_width * 0.05).ceil
|
111
|
+
@margin_top = (@page_height * 0.05).ceil
|
112
|
+
@margin_bottom = (@page_height * 0.05).ceil
|
113
|
+
|
114
|
+
# initialize some cairo objects to draw on
|
115
|
+
@output = StringIO.new
|
116
|
+
@surface = Cairo::PDFSurface.new(@output, @page_width, @page_height)
|
117
|
+
@context = Cairo::Context.new(@surface)
|
118
|
+
|
119
|
+
# set the background colour
|
120
|
+
set_color(options[:background_colour])
|
121
|
+
@context.paint
|
122
|
+
|
123
|
+
# set a default drawing colour and font style
|
124
|
+
default_color(:black)
|
125
|
+
default_font("Sans Serif")
|
126
|
+
default_font_size(16)
|
127
|
+
|
128
|
+
# move the cursor to the top left of the usable canvas
|
129
|
+
reset_cursor
|
130
|
+
end
|
131
|
+
|
132
|
+
#####################################################
|
133
|
+
# Functions relating to calculating various page dimensions
|
134
|
+
#####################################################
|
135
|
+
|
136
|
+
# Returns the x value of the left margin
|
137
|
+
# The top left corner of the page is (0,0)
|
138
|
+
def absolute_left_margin
|
139
|
+
@margin_left
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns the x value of the right margin
|
143
|
+
# The top left corner of the page is (0,0)
|
144
|
+
def absolute_right_margin
|
145
|
+
@page_width - @margin_right
|
146
|
+
end
|
147
|
+
|
148
|
+
# Returns the y value of the top margin
|
149
|
+
# The top left corner of the page is (0,0)
|
150
|
+
def absolute_top_margin
|
151
|
+
@margin_top
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns the y value of the bottom margin
|
155
|
+
# The top left corner of the page is (0,0)
|
156
|
+
def absolute_bottom_margin
|
157
|
+
@page_height - @margin_bottom
|
158
|
+
end
|
159
|
+
|
160
|
+
# Returns the x at the middle of the page
|
161
|
+
def absolute_x_middle
|
162
|
+
@page_width / 2
|
163
|
+
end
|
164
|
+
|
165
|
+
# Returns the y at the middle of the page
|
166
|
+
def absolute_y_middle
|
167
|
+
@page_height / 2
|
168
|
+
end
|
169
|
+
|
170
|
+
# Returns the width of the usable part of the page (between the side margins)
|
171
|
+
def body_width
|
172
|
+
@page_width - @margin_left - @margin_right
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns the height of the usable part of the page (between the top and bottom margins)
|
176
|
+
def body_height
|
177
|
+
@page_height - @margin_top - @margin_bottom
|
178
|
+
end
|
179
|
+
|
180
|
+
# Returns the x coordinate of the middle part of the usable space between the margins
|
181
|
+
def margin_x_middle
|
182
|
+
@margin_left + (body_width / 2)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Returns the y coordinate of the middle part of the usable space between the margins
|
186
|
+
def margin_y_middle
|
187
|
+
@margin_top + (body_height / 2)
|
188
|
+
end
|
189
|
+
|
190
|
+
# return the current position of the cursor
|
191
|
+
# returns 2 values - x,y
|
192
|
+
def current_point
|
193
|
+
return @context.current_point
|
194
|
+
end
|
195
|
+
|
196
|
+
# return the number of points from starty to the bottom border
|
197
|
+
def points_to_bottom_margin(starty)
|
198
|
+
absolute_bottom_margin - starty
|
199
|
+
end
|
200
|
+
|
201
|
+
# return the number of points from startx to the right border
|
202
|
+
def points_to_right_margin(startx)
|
203
|
+
absolute_right_margin - startx
|
204
|
+
end
|
205
|
+
|
206
|
+
#####################################################
|
207
|
+
# Functions relating to working with text
|
208
|
+
#####################################################
|
209
|
+
|
210
|
+
# change the default font size
|
211
|
+
def default_font_size(size)
|
212
|
+
#@context.set_font_size(size.to_i)
|
213
|
+
@default_font_size = size.to_i unless size.nil?
|
214
|
+
end
|
215
|
+
alias default_font_size= default_font_size
|
216
|
+
alias font_size default_font_size # PDF::Writer compatibility
|
217
|
+
|
218
|
+
# change the default font to write with
|
219
|
+
def default_font(fontname, style = nil, weight = nil)
|
220
|
+
#@context.select_font_face(fontname, slant, bold)
|
221
|
+
@default_font = fontname
|
222
|
+
@default_font_style = style unless style.nil?
|
223
|
+
@default_font_weight = weight unless weight.nil?
|
224
|
+
end
|
225
|
+
alias default_font= default_font
|
226
|
+
alias select_font default_font # PDF::Writer compatibility
|
227
|
+
|
228
|
+
# change the default colour used to draw on the canvas
|
229
|
+
#
|
230
|
+
# Parameters:
|
231
|
+
# <tt>c</tt>:: either a colour symbol recognised by rcairo (:red, :blue, :black, etc) or
|
232
|
+
# an array with 3-4 integer elements. The first 3 numbers are red, green and
|
233
|
+
# blue (0-255). The optional 4th number is the alpha channel and should be
|
234
|
+
# between 0 and 1. See the API docs at http://cairo.rubyforge.org/ for a list
|
235
|
+
# of predefined colours
|
236
|
+
def default_color(c)
|
237
|
+
validate_color(c)
|
238
|
+
@default_color = c
|
239
|
+
end
|
240
|
+
alias default_color= default_color
|
241
|
+
alias stroke_color default_color # PDF::Writer compatibility
|
242
|
+
|
243
|
+
# add text to the page, bounded by a box with dimensions HxW, with it's top left corner
|
244
|
+
# at x,y. Any text that doesn't fit it the box will be silently dropped.
|
245
|
+
#
|
246
|
+
# In addition to the standard text style options (see the documentation for text()), cell() supports
|
247
|
+
# the following options:
|
248
|
+
#
|
249
|
+
# <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.
|
250
|
+
# <tt>:border_width</tt>:: How wide should the border be?
|
251
|
+
# <tt>:border_color</tt>:: What color should the border be?
|
252
|
+
# <tt>:bgcolor</tt>:: A background color for the cell. Defaults to none.
|
253
|
+
def cell(str, x, y, w, h, opts={})
|
254
|
+
# TODO: add support for pango markup (see http://ruby-gnome2.sourceforge.jp/hiki.cgi?pango-markup)
|
255
|
+
# TODO: add a wrap option so wrapping can be disabled
|
256
|
+
# TODO: raise an error if any unrecognised options were supplied
|
257
|
+
# TODO: add padding between border and text
|
258
|
+
# TODO: how do we handle a single word that is too long for the width?
|
259
|
+
|
260
|
+
options = default_text_options
|
261
|
+
options.merge!({:border => "tblr", :border_width => 1, :border_color => :black, :bgcolor => nil})
|
262
|
+
options.merge!(opts)
|
263
|
+
|
264
|
+
options[:width] = w
|
265
|
+
options[:border] = "" unless options[:border]
|
266
|
+
options[:border].downcase!
|
267
|
+
|
268
|
+
# TODO: raise an exception if the box coords or dimensions will place it off the canvas
|
269
|
+
rectangle(x,y,w,h, :color => options[:bgcolor], :fill_color => options[:bgcolor]) if options[:bgcolor]
|
270
|
+
|
271
|
+
layout = build_pango_layout(str.to_s, options)
|
272
|
+
|
273
|
+
set_color(options[:color])
|
274
|
+
|
275
|
+
# draw the context on our cairo layout
|
276
|
+
render_layout(layout, x, y, h, :auto_new_page => false)
|
277
|
+
|
278
|
+
# draw a border around the cell
|
279
|
+
# TODO: obey options[:border_width]
|
280
|
+
# TODO: obey options[:border_color]
|
281
|
+
line(x,y,x+w,y) if options[:border].include?("t")
|
282
|
+
line(x,y+h,x+w,y+h) if options[:border].include?("b")
|
283
|
+
line(x,y,x,y+h) if options[:border].include?("l")
|
284
|
+
line(x+w,y,x+w,y+h) if options[:border].include?("r")
|
285
|
+
end
|
286
|
+
|
287
|
+
# draws a basic table onto the page
|
288
|
+
# data - a 2d array with the data for the columns. The first row will be treated as the headings
|
289
|
+
#
|
290
|
+
# In addition to the standard text style options (see the documentation for text()), cell() supports
|
291
|
+
# the following options:
|
292
|
+
#
|
293
|
+
# <tt>:left</tt>:: The x co-ordinate of the left-hand side of the table. Defaults to the left margin
|
294
|
+
# <tt>:top</tt>:: The y co-ordinate of the top of the text. Defaults to the top margin
|
295
|
+
# <tt>:width</tt>:: The width of the table. Defaults to the width of the page body
|
296
|
+
def table(data, opts = {})
|
297
|
+
# TODO: instead of accepting the data, use a XHTML table string?
|
298
|
+
# TODO: handle overflowing to a new page
|
299
|
+
# TODO: add a way to display borders
|
300
|
+
# TODO: raise an error if any unrecognised options were supplied
|
301
|
+
|
302
|
+
x, y = current_point
|
303
|
+
options = default_text_options.merge!({:left => x,
|
304
|
+
:top => y
|
305
|
+
})
|
306
|
+
options.merge!(opts)
|
307
|
+
options[:width] = body_width - options[:left] unless options[:width]
|
308
|
+
|
309
|
+
# move to the start of our table (the top left)
|
310
|
+
x = options[:left]
|
311
|
+
y = options[:top]
|
312
|
+
move_to(x,y)
|
313
|
+
|
314
|
+
# all columns will have the same width at this stage
|
315
|
+
cell_width = options[:width] / data.first.size
|
316
|
+
|
317
|
+
# draw the header cells
|
318
|
+
y = draw_table_row(data.shift, cell_width, options)
|
319
|
+
x = options[:left]
|
320
|
+
move_to(x,y)
|
321
|
+
|
322
|
+
# draw the data cells
|
323
|
+
data.each do |row|
|
324
|
+
y = draw_table_row(row, cell_width, options)
|
325
|
+
x = options[:left]
|
326
|
+
move_to(x,y)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# Write text to the page
|
331
|
+
#
|
332
|
+
# By default the text will be rendered using all the space within the margins and using
|
333
|
+
# the default font styling set by default_font(), default_font_size, etc
|
334
|
+
#
|
335
|
+
# There is no way to place a bottom bound (or height) onto the text. Text will wrap as
|
336
|
+
# necessary and take all the room it needs. For finer grained control of text boxes, see the
|
337
|
+
# cell method.
|
338
|
+
#
|
339
|
+
# To override all these defaults, use the options hash
|
340
|
+
#
|
341
|
+
# Positioning Options:
|
342
|
+
#
|
343
|
+
# <tt>:left</tt>:: The x co-ordinate of the left-hand side of the text.
|
344
|
+
# <tt>:top</tt>:: The y co-ordinate of the top of the text.
|
345
|
+
# <tt>:width</tt>:: The width of the text to wrap at
|
346
|
+
#
|
347
|
+
# Text Style Options:
|
348
|
+
#
|
349
|
+
# <tt>:font</tt>:: The font family to use as a string
|
350
|
+
# <tt>:font_size</tt>:: The size of the font in points
|
351
|
+
# <tt>:alignment</tt>:: Align the text along the left, right or centre. Use :left, :right, :center
|
352
|
+
# <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
|
353
|
+
# <tt>:spacing</tt>:: Space between lines in PDF points
|
354
|
+
def text(str, opts={})
|
355
|
+
# TODO: add support for pango markup (see http://ruby-gnome2.sourceforge.jp/hiki.cgi?pango-markup)
|
356
|
+
# TODO: add a wrap option so wrapping can be disabled
|
357
|
+
# TODO: raise an error if any unrecognised options were supplied
|
358
|
+
#
|
359
|
+
# the non pango way to add text to the cairo context, not particularly useful for
|
360
|
+
# PDF generation as it doesn't support wrapping text or other advanced layout features
|
361
|
+
# and I really don't feel like re-implementing all that
|
362
|
+
# @context.show_text(str)
|
363
|
+
|
364
|
+
# the "pango way"
|
365
|
+
x, y = current_point
|
366
|
+
options = default_text_options.merge!({:left => x, :top => y})
|
367
|
+
options.merge!(opts)
|
368
|
+
|
369
|
+
# if the user hasn't specified a width, make the text wrap on the right margin
|
370
|
+
options[:width] = absolute_right_margin - options[:left] if options[:width].nil?
|
371
|
+
|
372
|
+
layout = build_pango_layout(str.to_s, options)
|
373
|
+
|
374
|
+
set_color(options[:color])
|
375
|
+
|
376
|
+
# draw the context on our cairo layout
|
377
|
+
y = render_layout(layout, options[:left], options[:top], points_to_bottom_margin(options[:top]), :auto_new_page => true)
|
378
|
+
|
379
|
+
move_to(options[:left], y + 5)
|
380
|
+
end
|
381
|
+
|
382
|
+
# Returns the amount of vertical space needed to display the supplied text at the requested width
|
383
|
+
# opts is an options hash that specifies various attributes of the text. See the text function for more information.
|
384
|
+
# TODO: raise an error if any unrecognised options were supplied
|
385
|
+
def text_height(str, width, opts = {})
|
386
|
+
options = default_text_options.merge!(opts)
|
387
|
+
options[:width] = width || body_width
|
388
|
+
|
389
|
+
layout = build_pango_layout(str.to_s, options)
|
390
|
+
width, height = layout.size
|
391
|
+
|
392
|
+
return height / Pango::SCALE
|
393
|
+
end
|
394
|
+
|
395
|
+
#####################################################
|
396
|
+
# Functions relating to working with graphics
|
397
|
+
#####################################################
|
398
|
+
|
399
|
+
# draw a circle with radius r and a centre point at (x,y).
|
400
|
+
# Parameters:
|
401
|
+
# <tt>:x</tt>:: The x co-ordinate of the circle centre.
|
402
|
+
# <tt>:y</tt>:: The y co-ordinate of the circle centre.
|
403
|
+
# <tt>:r</tt>:: The radius of the circle
|
404
|
+
#
|
405
|
+
# Options:
|
406
|
+
# <tt>:color</tt>:: The colour of the circle outline
|
407
|
+
# <tt>:fill_color</tt>:: The colour to fill the circle with. Defaults to nil (no fill)
|
408
|
+
def circle(x, y, r, opts = {})
|
409
|
+
# TODO: raise an error if any unrecognised options were supplied
|
410
|
+
options = {:color => @default_color,
|
411
|
+
:fill_color => nil
|
412
|
+
}
|
413
|
+
options.merge!(opts)
|
414
|
+
|
415
|
+
move_to(x + r, y)
|
416
|
+
|
417
|
+
# if the rectangle should be filled in
|
418
|
+
if options[:fill_color]
|
419
|
+
set_color(options[:fill_color])
|
420
|
+
@context.circle(x, y, r).fill
|
421
|
+
end
|
422
|
+
|
423
|
+
set_color(options[:color])
|
424
|
+
@context.circle(x, y, r).stroke
|
425
|
+
move_to(x + r, y + r)
|
426
|
+
end
|
427
|
+
|
428
|
+
# draw a line from x1,y1 to x2,y2
|
429
|
+
#
|
430
|
+
# Options:
|
431
|
+
# <tt>:color</tt>:: The colour of the line
|
432
|
+
def line(x0, y0, x1, y1, opts = {})
|
433
|
+
# TODO: raise an error if any unrecognised options were supplied
|
434
|
+
options = {:color => @default_color }
|
435
|
+
options.merge!(opts)
|
436
|
+
|
437
|
+
set_color(options[:color])
|
438
|
+
|
439
|
+
move_to(x0,y0)
|
440
|
+
@context.line_to(x1,y1).stroke
|
441
|
+
move_to(x1,y1)
|
442
|
+
end
|
443
|
+
|
444
|
+
# draw a rectangle starting at x,y with w,h dimensions.
|
445
|
+
# Parameters:
|
446
|
+
# <tt>:x</tt>:: The x co-ordinate of the top left of the rectangle.
|
447
|
+
# <tt>:y</tt>:: The y co-ordinate of the top left of the rectangle.
|
448
|
+
# <tt>:w</tt>:: The width of the rectangle
|
449
|
+
# <tt>:h</tt>:: The height of the rectangle
|
450
|
+
#
|
451
|
+
# Options:
|
452
|
+
# <tt>:color</tt>:: The colour of the rectangle outline
|
453
|
+
# <tt>:fill_color</tt>:: The colour to fill the rectangle with. Defaults to nil (no fill)
|
454
|
+
def rectangle(x, y, w, h, opts = {})
|
455
|
+
# TODO: raise an error if any unrecognised options were supplied
|
456
|
+
options = {:color => @default_color,
|
457
|
+
:fill_color => nil
|
458
|
+
}
|
459
|
+
options.merge!(opts)
|
460
|
+
|
461
|
+
# if the rectangle should be filled in
|
462
|
+
if options[:fill_color]
|
463
|
+
set_color(options[:fill_color])
|
464
|
+
@context.rectangle(x, y, w, h).fill
|
465
|
+
end
|
466
|
+
|
467
|
+
set_color(options[:color])
|
468
|
+
@context.rectangle(x, y, w, h).stroke
|
469
|
+
|
470
|
+
move_to(x+w, y+h)
|
471
|
+
end
|
472
|
+
|
473
|
+
# draw a rounded rectangle starting at x,y with w,h dimensions.
|
474
|
+
# Parameters:
|
475
|
+
# <tt>:x</tt>:: The x co-ordinate of the top left of the rectangle.
|
476
|
+
# <tt>:y</tt>:: The y co-ordinate of the top left of the rectangle.
|
477
|
+
# <tt>:w</tt>:: The width of the rectangle
|
478
|
+
# <tt>:h</tt>:: The height of the rectangle
|
479
|
+
# <tt>:r</tt>:: The size of the rounded corners
|
480
|
+
#
|
481
|
+
# Options:
|
482
|
+
# <tt>:color</tt>:: The colour of the rectangle outline
|
483
|
+
# <tt>:fill_color</tt>:: The colour to fill the rectangle with. Defaults to nil (no fill)
|
484
|
+
def rounded_rectangle(x, y, w, h, r, opts = {})
|
485
|
+
# TODO: raise an error if any unrecognised options were supplied
|
486
|
+
options = {:color => @default_color,
|
487
|
+
:fill_color => nil
|
488
|
+
}
|
489
|
+
options.merge!(opts)
|
490
|
+
|
491
|
+
raise ArgumentError, "Argument r must be less than both w and h arguments" if r >= w || r >= h
|
492
|
+
|
493
|
+
# if the rectangle should be filled in
|
494
|
+
if options[:fill_color]
|
495
|
+
set_color(options[:fill_color])
|
496
|
+
@context.rounded_rectangle(x, y, w, h, r).fill
|
497
|
+
end
|
498
|
+
|
499
|
+
set_color(options[:color])
|
500
|
+
@context.rounded_rectangle(x, y, w, h, r).stroke
|
501
|
+
|
502
|
+
move_to(x+w, y+h)
|
503
|
+
end
|
504
|
+
|
505
|
+
#####################################################
|
506
|
+
# Functions relating to working with images
|
507
|
+
#####################################################
|
508
|
+
|
509
|
+
# add an image to the page
|
510
|
+
# at this stage the file must be a PNG or SVG
|
511
|
+
# supported options:
|
512
|
+
# <tt>:left</tt>:: The x co-ordinate of the left-hand side of the image.
|
513
|
+
# <tt>:top</tt>:: The y co-ordinate of the top of the image.
|
514
|
+
# <tt>:height</tt>:: The height of the image
|
515
|
+
# <tt>:width</tt>:: The width of the image
|
516
|
+
#
|
517
|
+
# left and top default to the current cursor location
|
518
|
+
# width and height default to the size of the imported image
|
519
|
+
def image(filename, opts = {})
|
520
|
+
# TODO: maybe split this up into separate functions for each image type
|
521
|
+
# TODO: add some options for things like justification, scaling and padding
|
522
|
+
# TODO: png images currently can't be resized
|
523
|
+
# TODO: raise an error if any unrecognised options were supplied
|
524
|
+
raise ArgumentError, "file #{filename} not found" unless File.file?(filename)
|
525
|
+
|
526
|
+
filetype = detect_image_type(filename)
|
527
|
+
|
528
|
+
if filetype.eql?(:png)
|
529
|
+
img_surface = Cairo::ImageSurface.from_png(filename)
|
530
|
+
x, y = current_point
|
531
|
+
@context.set_source(img_surface, opts[:left] || x, opts[:top] || y)
|
532
|
+
@context.paint
|
533
|
+
elsif filetype.eql?(:svg)
|
534
|
+
# thanks to Nathan Stitt for help with this section
|
535
|
+
load_librsvg
|
536
|
+
@context.save
|
537
|
+
|
538
|
+
# import it
|
539
|
+
handle = RSVG::Handle.new_from_file(filename)
|
540
|
+
|
541
|
+
# size the SVG
|
542
|
+
if opts[:height] && opts[:width]
|
543
|
+
handle.set_size_callback do |h,w|
|
544
|
+
[ opts[:width], opts[:height] ]
|
545
|
+
end
|
546
|
+
end
|
547
|
+
|
548
|
+
# place the image on our main context
|
549
|
+
x, y = current_point
|
550
|
+
@context.translate( opts[:left] || x, opts[:top] || y )
|
551
|
+
@context.render_rsvg_handle(handle)
|
552
|
+
@context.restore
|
553
|
+
else
|
554
|
+
raise ArgumentError, "Unrecognised image format"
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
558
|
+
#####################################################
|
559
|
+
# Functions relating to generating the final document
|
560
|
+
#####################################################
|
561
|
+
|
562
|
+
# render the PDF and return it as a string
|
563
|
+
def render
|
564
|
+
# finalise the document, then convert the StringIO object it was rendered to
|
565
|
+
# into a string
|
566
|
+
@context.show_page
|
567
|
+
@context.target.finish
|
568
|
+
return @output.string
|
569
|
+
end
|
570
|
+
|
571
|
+
# save the rendered PDF to a file
|
572
|
+
def render_to_file(filename)
|
573
|
+
# finalise the document
|
574
|
+
@context.show_page
|
575
|
+
@context.target.finish
|
576
|
+
|
577
|
+
# write each line from the StringIO object it was rendered to into the
|
578
|
+
# requested file
|
579
|
+
File.open(filename, "w") do |of|
|
580
|
+
@output.rewind
|
581
|
+
@output.each_line { |line| of.write(line) }
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
#####################################################
|
586
|
+
# Misc Functions
|
587
|
+
#####################################################
|
588
|
+
|
589
|
+
# move the cursor to an arbitary position on the current page
|
590
|
+
def move_to(x,y)
|
591
|
+
raise ArgumentError, 'x cannot be larger than the width of the page' if x > page_width
|
592
|
+
raise ArgumentError, 'y cannot be larger than the height of the page' if y > page_height
|
593
|
+
@context.move_to(x,y)
|
594
|
+
end
|
595
|
+
|
596
|
+
# reset the cursor by moving it to the top left of the useable section of the page
|
597
|
+
def reset_cursor
|
598
|
+
@context.move_to(margin_left,margin_top)
|
599
|
+
end
|
600
|
+
|
601
|
+
# move to the next page
|
602
|
+
def start_new_page
|
603
|
+
@context.show_page
|
604
|
+
reset_cursor
|
605
|
+
end
|
606
|
+
|
607
|
+
private
|
608
|
+
|
609
|
+
def build_pango_layout(str, opts = {})
|
610
|
+
options = {:left => @margin_left,
|
611
|
+
:top => @margin_top,
|
612
|
+
:font => @default_font,
|
613
|
+
:font_size => @default_font_size,
|
614
|
+
:color => @default_color,
|
615
|
+
:alignment => :left,
|
616
|
+
:justify => false,
|
617
|
+
:spacing => 0
|
618
|
+
}
|
619
|
+
options.merge!(opts)
|
620
|
+
|
621
|
+
# if the user hasn't specified a width, make the text wrap on the right margin
|
622
|
+
options[:width] = absolute_right_margin - options[:left] if options[:width].nil?
|
623
|
+
|
624
|
+
# even though this is a private function, raise this error to force calling functions
|
625
|
+
# to decide how they want to handle converting non-strings into strings for rendering
|
626
|
+
raise ArgumentError, 'build_pango_layout must be passed a string' unless str.kind_of?(String)
|
627
|
+
|
628
|
+
# if we're running under a M17n aware VM, ensure the string provided is UTF-8
|
629
|
+
if RUBY_VERSION >= "1.9"
|
630
|
+
begin
|
631
|
+
str = str.encode("UTF-8")
|
632
|
+
rescue
|
633
|
+
raise ArgumentError, 'Strings must be supplied with a UTF-8 encoding, or an encoding that can be converted to UTF-8'
|
634
|
+
end
|
635
|
+
end
|
636
|
+
|
637
|
+
# The pango way:
|
638
|
+
load_libpango
|
639
|
+
|
640
|
+
# create a new Pango layout that our text will be added to
|
641
|
+
layout = @context.create_pango_layout
|
642
|
+
layout.text = str.to_s
|
643
|
+
layout.width = options[:width] * Pango::SCALE
|
644
|
+
layout.spacing = options[:spacing] * Pango::SCALE
|
645
|
+
|
646
|
+
# set the alignment of the text in the layout
|
647
|
+
if options[:alignment].eql?(:left)
|
648
|
+
layout.alignment = Pango::Layout::ALIGN_LEFT
|
649
|
+
elsif options[:alignment].eql?(:right)
|
650
|
+
layout.alignment = Pango::Layout::ALIGN_RIGHT
|
651
|
+
elsif options[:alignment].eql?(:center) || options[:alignment].eql?(:centre)
|
652
|
+
layout.alignment = Pango::Layout::ALIGN_CENTER
|
653
|
+
else
|
654
|
+
raise ArgumentError, "Invalid alignment requested"
|
655
|
+
end
|
656
|
+
|
657
|
+
# justify the text if need be - only works in pango >= 1.17
|
658
|
+
layout.justify = true if options[:justify]
|
659
|
+
|
660
|
+
# setup the font that will be used to render the text
|
661
|
+
fdesc = Pango::FontDescription.new(options[:font])
|
662
|
+
fdesc.set_size(options[:font_size] * Pango::SCALE)
|
663
|
+
layout.font_description = fdesc
|
664
|
+
@context.update_pango_layout(layout)
|
665
|
+
return layout
|
666
|
+
end
|
667
|
+
|
668
|
+
def default_text_options
|
669
|
+
{ :font => @default_font,
|
670
|
+
:font_size => @default_font_size,
|
671
|
+
:color => @default_color,
|
672
|
+
:alignment => :left,
|
673
|
+
:justify => false,
|
674
|
+
:spacing => 0
|
675
|
+
}
|
676
|
+
end
|
677
|
+
|
678
|
+
def detect_image_type(filename)
|
679
|
+
|
680
|
+
# read the first Kb from the file to attempt file type detection
|
681
|
+
f = File.new(filename)
|
682
|
+
bytes = f.read(1024)
|
683
|
+
|
684
|
+
# if the file is a PNG
|
685
|
+
if bytes[1,3].eql?("PNG")
|
686
|
+
return :png
|
687
|
+
elsif bytes.include?("<svg")
|
688
|
+
return :svg
|
689
|
+
else
|
690
|
+
return nil
|
691
|
+
end
|
692
|
+
end
|
693
|
+
|
694
|
+
# adds a single table row to the canvas. Top left of the row will be at the current x,y
|
695
|
+
# co-ordinates, so make sure they're set correctly before calling this function
|
696
|
+
#
|
697
|
+
# strings - array of strings. Each element of the array is a cell
|
698
|
+
# column_widths - the width of each column. At this stage it should be an int. All columns are the same width
|
699
|
+
# options - any options relating to text style to use. font, font_size, alignment, etc. See text() for more info.
|
700
|
+
#
|
701
|
+
# Returns the y co-ordinates of the bottom edge of the row, ready for the next row
|
702
|
+
def draw_table_row(strings, column_widths, options)
|
703
|
+
row_height = 0
|
704
|
+
x, y = current_point
|
705
|
+
|
706
|
+
# we run all this code twice. The first time is a dry run to calculate the
|
707
|
+
# height of the largest cell, which determines the overall height of the row.
|
708
|
+
# The second run through we actually draw each cell onto the canvas
|
709
|
+
[:dry, :paint].each do |action|
|
710
|
+
|
711
|
+
strings.each do |head|
|
712
|
+
# TODO: provide a way for these to be overridden on a per cell basis
|
713
|
+
opts = {
|
714
|
+
:font => options[:font],
|
715
|
+
:font_size => options[:font_size],
|
716
|
+
:color => options[:color],
|
717
|
+
:alignment => options[:alignment],
|
718
|
+
:justify => options[:justify],
|
719
|
+
:spacing => options[:spacing]
|
720
|
+
}
|
721
|
+
|
722
|
+
if action == :dry
|
723
|
+
# calc the cell height, and set row_height if this cell is the biggest in the row
|
724
|
+
cell_height = text_height(head, column_widths, opts)
|
725
|
+
row_height = cell_height if cell_height > row_height
|
726
|
+
else
|
727
|
+
# start a new page if necesary
|
728
|
+
if row_height > (absolute_bottom_margin - y)
|
729
|
+
start_new_page
|
730
|
+
y = margin_top
|
731
|
+
end
|
732
|
+
|
733
|
+
# add our cell, then advance x to the left edge of the next cell
|
734
|
+
self.cell(head, x, y, column_widths, row_height, opts)
|
735
|
+
x += column_widths
|
736
|
+
end
|
737
|
+
|
738
|
+
end
|
739
|
+
end
|
740
|
+
|
741
|
+
return y + row_height
|
742
|
+
end
|
743
|
+
|
744
|
+
# load libpango if it isn't already loaded.
|
745
|
+
# This will add some methods to the cairo Context class in addition to providing
|
746
|
+
# its own classes and constants. A small amount of documentation is available at
|
747
|
+
# http://ruby-gnome2.sourceforge.jp/fr/hiki.cgi?Cairo%3A%3AContext#Pango+related+APIs
|
748
|
+
def load_libpango
|
749
|
+
begin
|
750
|
+
require 'pango' unless ::Object.const_defined?(:Pango)
|
751
|
+
rescue LoadError
|
752
|
+
raise LoadError, 'Ruby/Pango library not found. Visit http://ruby-gnome2.sourceforge.jp/'
|
753
|
+
end
|
754
|
+
end
|
755
|
+
|
756
|
+
# load librsvg if it isn't already loaded
|
757
|
+
# This will add an additional method to the Cairo::Context class
|
758
|
+
# that allows an existing SVG to be drawn directly onto it
|
759
|
+
# There's a *little* bit of documentation at:
|
760
|
+
# http://ruby-gnome2.sourceforge.jp/fr/hiki.cgi?Cairo%3A%3AContext#render_rsvg_handle
|
761
|
+
def load_librsvg
|
762
|
+
begin
|
763
|
+
require 'rsvg2' unless ::Object.const_defined?(:RSVG)
|
764
|
+
rescue LoadError
|
765
|
+
raise LoadError, 'Ruby/RSVG library not found. Visit http://ruby-gnome2.sourceforge.jp/'
|
766
|
+
end
|
767
|
+
end
|
768
|
+
|
769
|
+
# renders a pango layout onto our main context
|
770
|
+
# based on a function of the same name found in the text2.rb sample file
|
771
|
+
# distributed with rcairo - it's still black magic to me and has a few edge
|
772
|
+
# cases where it doesn't work too well. Needs to be improved.
|
773
|
+
def render_layout(layout, x, y, h, opts = {})
|
774
|
+
options = {:auto_new_page => true }
|
775
|
+
options.merge!(opts)
|
776
|
+
|
777
|
+
limit_y = y + h
|
778
|
+
|
779
|
+
iter = layout.iter
|
780
|
+
prev_baseline = iter.baseline / Pango::SCALE
|
781
|
+
begin
|
782
|
+
line = iter.line
|
783
|
+
ink_rect, logical_rect = iter.line_extents
|
784
|
+
y_begin, y_end = iter.line_yrange
|
785
|
+
if limit_y < (y + y_end / Pango::SCALE)
|
786
|
+
if options[:auto_new_page]
|
787
|
+
start_new_page
|
788
|
+
y = margin_top - prev_baseline
|
789
|
+
else
|
790
|
+
break
|
791
|
+
end
|
792
|
+
end
|
793
|
+
width, height = layout.size
|
794
|
+
baseline = iter.baseline / Pango::SCALE
|
795
|
+
move_to(x + logical_rect.x / Pango::SCALE, y + baseline)
|
796
|
+
@context.show_pango_layout_line(line)
|
797
|
+
prev_baseline = baseline
|
798
|
+
end while iter.next_line!
|
799
|
+
return y + baseline
|
800
|
+
end
|
801
|
+
|
802
|
+
# set the current drawing colour
|
803
|
+
#
|
804
|
+
# for info on what is valid, see the comments for default_color
|
805
|
+
def set_color(c)
|
806
|
+
# catch and reraise an exception to keep stack traces readable and clear
|
807
|
+
validate_color(c)
|
808
|
+
|
809
|
+
if c.kind_of?(Array)
|
810
|
+
@context.set_source_color(*c)
|
811
|
+
else
|
812
|
+
@context.set_source_color(c)
|
813
|
+
end
|
814
|
+
end
|
815
|
+
|
816
|
+
# test to see if the specified colour is a a valid cairo color
|
817
|
+
#
|
818
|
+
# for info on what is valid, see the comments for default_color
|
819
|
+
def validate_color(c)
|
820
|
+
@context.save
|
821
|
+
begin
|
822
|
+
if c.kind_of?(Array)
|
823
|
+
# if the colour is being specified manually, there must be 3 or 4 elements
|
824
|
+
raise ArgumentError if c.size != 3 && c.size != 4
|
825
|
+
@context.set_source_color(c)
|
826
|
+
else
|
827
|
+
@context.set_source_color(c)
|
828
|
+
end
|
829
|
+
@default_color = c
|
830
|
+
rescue ArgumentError
|
831
|
+
c.kind_of?(Array) ? str = "[#{c.join(",")}]" : str = c.to_s
|
832
|
+
raise ArgumentError, "#{str} is not a valid color definition"
|
833
|
+
ensure
|
834
|
+
@context.restore
|
835
|
+
end
|
836
|
+
return true
|
837
|
+
end
|
838
|
+
end
|
839
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pdf-wrapper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James Healy
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-01-09 00:00:00 +11:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A PDF writing library that uses the cairo and pango libraries to do the heavy lifting.
|
17
|
+
email: jimmy@deefa.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README
|
24
|
+
- DESIGN
|
25
|
+
- CHANGELOG
|
26
|
+
files:
|
27
|
+
- examples/google.png
|
28
|
+
- examples/cell.rb
|
29
|
+
- examples/image.rb
|
30
|
+
- examples/table.rb
|
31
|
+
- examples/shapes.rb
|
32
|
+
- examples/shapes.pdf
|
33
|
+
- examples/image.pdf
|
34
|
+
- examples/utf8.rb
|
35
|
+
- lib/pdf
|
36
|
+
- lib/pdf/wrapper.rb
|
37
|
+
- lib/pdf/core.rb
|
38
|
+
- Rakefile
|
39
|
+
- README
|
40
|
+
- DESIGN
|
41
|
+
- CHANGELOG
|
42
|
+
has_rdoc: true
|
43
|
+
homepage:
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options:
|
46
|
+
- --title
|
47
|
+
- PDF::Wrapper Documentation
|
48
|
+
- --main
|
49
|
+
- README
|
50
|
+
- -q
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
version:
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
version:
|
65
|
+
requirements: []
|
66
|
+
|
67
|
+
rubyforge_project: pdf-wrapper
|
68
|
+
rubygems_version: 1.0.1
|
69
|
+
signing_key:
|
70
|
+
specification_version: 2
|
71
|
+
summary: A PDF generating library built on top of cairo
|
72
|
+
test_files: []
|
73
|
+
|