lib/pdf/wrapper.rb in pdf-wrapper-0.0.1 vs lib/pdf/wrapper.rb in pdf-wrapper-0.0.2
- old
+ new
@@ -22,25 +22,25 @@
module PDF
# Create PDF files by using the cairo and pango libraries.
#
# Rendering to a file:
#
- # require 'pdfwrapper'
+ # require 'pdf/wrapper'
# pdf = PDF::Wrapper.new(:paper => :A4)
# pdf.text "Hello World"
# pdf.render_to_file("wrapper.pdf")
#
# Rendering to a string:
#
- # require 'pdfwrapper'
+ # require 'pdf/wrapper'
# pdf = PDF::Wrapper.new(:paper => :A4)
# pdf.text "Hello World", :font_size => 16
# puts pdf.render
#
# Changing the default font:
#
- # require 'pdfwrapper'
+ # require 'pdf/wrapper'
# pdf = PDF::Wrapper.new(:paper => :A4)
# pdf.default_font("Monospace")
# pdf.text "Hello World", :font => "Sans Serif", :font_size => 18
# pdf.text "Pretend this is a code sample"
# puts pdf.render
@@ -232,10 +232,11 @@
# an array with 3-4 integer elements. The first 3 numbers are red, green and
# blue (0-255). The optional 4th number is the alpha channel and should be
# between 0 and 1. See the API docs at http://cairo.rubyforge.org/ for a list
# of predefined colours
def default_color(c)
+ c = translate_color(c)
validate_color(c)
@default_color = c
end
alias default_color= default_color
alias stroke_color default_color # PDF::Writer compatibility
@@ -504,56 +505,41 @@
#####################################################
# Functions relating to working with images
#####################################################
- # add an image to the page
- # at this stage the file must be a PNG or SVG
+ # add an image to the page - a wide range of image formats are supported,
+ # including svg, jpg, png and gif. PDF images are also supported - an attempt
+ # to add a multipage PDF will result in only the first page appearing in the
+ # new document.
+ #
# supported options:
# <tt>:left</tt>:: The x co-ordinate of the left-hand side of the image.
# <tt>:top</tt>:: The y co-ordinate of the top of the image.
# <tt>:height</tt>:: The height of the image
# <tt>:width</tt>:: The width of the image
#
# left and top default to the current cursor location
# width and height default to the size of the imported image
+ #
+ # if width or height are specified, the image will *not* be scaled proportionally
def image(filename, opts = {})
- # TODO: maybe split this up into separate functions for each image type
# TODO: add some options for things like justification, scaling and padding
- # TODO: png images currently can't be resized
# TODO: raise an error if any unrecognised options were supplied
+ # TODO: add support for pdf/eps/ps images
raise ArgumentError, "file #{filename} not found" unless File.file?(filename)
- filetype = detect_image_type(filename)
-
- if filetype.eql?(:png)
- img_surface = Cairo::ImageSurface.from_png(filename)
- x, y = current_point
- @context.set_source(img_surface, opts[:left] || x, opts[:top] || y)
- @context.paint
- elsif filetype.eql?(:svg)
- # thanks to Nathan Stitt for help with this section
- load_librsvg
- @context.save
-
- # import it
- handle = RSVG::Handle.new_from_file(filename)
-
- # size the SVG
- if opts[:height] && opts[:width]
- handle.set_size_callback do |h,w|
- [ opts[:width], opts[:height] ]
- end
- end
-
- # place the image on our main context
- x, y = current_point
- @context.translate( opts[:left] || x, opts[:top] || y )
- @context.render_rsvg_handle(handle)
- @context.restore
+ case detect_image_type(filename)
+ when :pdf then draw_pdf filename, opts
+ when :png then draw_png filename, opts
+ when :svg then draw_svg filename, opts
else
- raise ArgumentError, "Unrecognised image format"
+ begin
+ draw_pixbuf filename, opts
+ rescue Gdk::PixbufError
+ raise ArgumentError, "Unrecognised image format (#{filename})"
+ end
end
end
#####################################################
# Functions relating to generating the final document
@@ -674,25 +660,89 @@
:spacing => 0
}
end
def detect_image_type(filename)
-
# read the first Kb from the file to attempt file type detection
f = File.new(filename)
bytes = f.read(1024)
# if the file is a PNG
if bytes[1,3].eql?("PNG")
return :png
+ elsif bytes[0,3].eql?("GIF")
+ return :gif
+ elsif bytes[0,4].eql?("%PDF")
+ return :pdf
elsif bytes.include?("<svg")
return :svg
+ elsif bytes.include?("Exif")
+ return :jpg
else
return nil
end
end
+ def draw_pdf(filename, opts = {})
+ # based on a similar function in rabbit. Thanks Kou.
+ load_libpoppler
+ x, y = current_point
+ page = Poppler::Document.new(filename).get_page(1)
+ w, h = page.size
+ width = (opts[:width] || w).to_f
+ height = (opts[:height] || h).to_f
+ @context.save do
+ @context.translate(opts[:left] || x, opts[:top] || y)
+ @context.scale(width / w, height / h)
+ @context.render_poppler_page(page)
+ end
+ end
+
+ def draw_pixbuf(filename, opts = {})
+ # based on a similar function in rabbit. Thanks Kou.
+ load_libpixbuf
+ x, y = current_point
+ pixbuf = Gdk::Pixbuf.new(filename)
+ width = (opts[:width] || pixbuf.width).to_f
+ height = (opts[:height] || pixbuf.height).to_f
+ @context.save do
+ @context.translate(opts[:left] || x, opts[:top] || y)
+ @context.scale(width / pixbuf.width, height / pixbuf.height)
+ @context.set_source_pixbuf(pixbuf, 0, 0)
+ @context.paint
+ end
+ end
+
+ def draw_png(filename, opts = {})
+ # based on a similar function in rabbit. Thanks Kou.
+ x, y = current_point
+ img_surface = Cairo::ImageSurface.from_png(filename)
+ width = (opts[:width] || img_surface.width).to_f
+ height = (opts[:height] || img_surface.height).to_f
+ @context.save do
+ @context.translate(opts[:left] || x, opts[:top] || y)
+ @context.scale(width / img_surface.width, height / img_surface.height)
+ @context.set_source(img_surface, 0, 0)
+ @context.paint
+ end
+ end
+
+ def draw_svg(filename, opts = {})
+ # based on a similar function in rabbit. Thanks Kou.
+ load_librsvg
+ x, y = current_point
+ handle = RSVG::Handle.new_from_file(filename)
+ width = (opts[:width] || handle.width).to_f
+ height = (opts[:height] || handle.height).to_f
+ @context.save do
+ @context.translate(opts[:left] || x, opts[:top] || y)
+ @context.scale(width / handle.width, height / handle.height)
+ @context.render_rsvg_handle(handle)
+ #@context.paint
+ end
+ end
+
# adds a single table row to the canvas. Top left of the row will be at the current x,y
# co-ordinates, so make sure they're set correctly before calling this function
#
# strings - array of strings. Each element of the array is a cell
# column_widths - the width of each column. At this stage it should be an int. All columns are the same width
@@ -745,89 +795,125 @@
# This will add some methods to the cairo Context class in addition to providing
# its own classes and constants. A small amount of documentation is available at
# http://ruby-gnome2.sourceforge.jp/fr/hiki.cgi?Cairo%3A%3AContext#Pango+related+APIs
def load_libpango
begin
- require 'pango' unless ::Object.const_defined?(:Pango)
+ require 'pango' unless @context.respond_to? :create_pango_layout
rescue LoadError
raise LoadError, 'Ruby/Pango library not found. Visit http://ruby-gnome2.sourceforge.jp/'
end
end
+ # load lib gdkpixbuf if it isn't already loaded.
+ # This will add some methods to the cairo Context class in addition to providing
+ # its own classes and constants.
+ def load_libpixbuf
+ begin
+ require 'gdk_pixbuf2' unless @context.respond_to? :set_source_pixbuf
+ rescue LoadError
+ raise LoadError, 'Ruby/GdkPixbuf library not found. Visit http://ruby-gnome2.sourceforge.jp/'
+ end
+ end
+
+ # load lib poppler if it isn't already loaded.
+ # This will add some methods to the cairo Context class in addition to providing
+ # its own classes and constants.
+ def load_libpoppler
+ begin
+ require 'poppler' unless @context.respond_to? :render_poppler_page
+ rescue LoadError
+ raise LoadError, 'Ruby/Poppler library not found. Visit http://ruby-gnome2.sourceforge.jp/'
+ end
+ end
+
# load librsvg if it isn't already loaded
# This will add an additional method to the Cairo::Context class
# that allows an existing SVG to be drawn directly onto it
# There's a *little* bit of documentation at:
# http://ruby-gnome2.sourceforge.jp/fr/hiki.cgi?Cairo%3A%3AContext#render_rsvg_handle
def load_librsvg
begin
- require 'rsvg2' unless ::Object.const_defined?(:RSVG)
+ require 'rsvg2' unless @context.respond_to? :render_svg_handle
rescue LoadError
raise LoadError, 'Ruby/RSVG library not found. Visit http://ruby-gnome2.sourceforge.jp/'
end
end
# renders a pango layout onto our main context
# based on a function of the same name found in the text2.rb sample file
# distributed with rcairo - it's still black magic to me and has a few edge
# cases where it doesn't work too well. Needs to be improved.
def render_layout(layout, x, y, h, opts = {})
+ # we can't use content.show_pango_layout, as that won't start
+ # a new page if the layout hits the bottom margin. Instead,
+ # we iterate over each line of text in the layout and add it to
+ # the canvas, page breaking as necessary
options = {:auto_new_page => true }
options.merge!(opts)
- limit_y = y + h
+ # store the starting x and y co-ords. If we start a new page, we'll continue
+ # adding text at the same co-ords
+ orig_x = x
+ orig_y = y
- iter = layout.iter
- prev_baseline = iter.baseline / Pango::SCALE
- begin
- line = iter.line
- ink_rect, logical_rect = iter.line_extents
- y_begin, y_end = iter.line_yrange
- if limit_y < (y + y_end / Pango::SCALE)
+ # for each line in the layout
+ layout.lines.each do |line|
+
+ # draw the line on the canvas
+ @context.show_pango_layout_line(line)
+
+ #calculate where the next line starts
+ ink_rect, logical_rect = line.extents
+ y = y + (logical_rect.height / Pango::SCALE * (3.0/4.0)) + 1
+
+ if y >= (orig_y + h)
+ # our text is using the maximum amount of vertical space we want it to
if options[:auto_new_page]
+ # create a new page and we can continue adding text
start_new_page
- y = margin_top - prev_baseline
+ x = orig_x
+ y = orig_y
else
+ # the user doesn't want us to continue on the next page, so
+ # stop adding lines to the canvas
break
end
end
- width, height = layout.size
- baseline = iter.baseline / Pango::SCALE
- move_to(x + logical_rect.x / Pango::SCALE, y + baseline)
- @context.show_pango_layout_line(line)
- prev_baseline = baseline
- end while iter.next_line!
- return y + baseline
+
+ # move to the start of the next line
+ move_to(x, y)
+ end
+
+ # return the y co-ord we finished on
+ return y
end
+ def translate_color(c)
+ # the follow line converts a color definition from various formats (hex, symbol, etc)
+ # into a 4 item array. This is normally handled within cairo itself, however when
+ # Cairo and Poppler are both loaded, it breaks.
+ Cairo::Color.parse(c).to_rgb.to_a
+ end
# set the current drawing colour
#
# for info on what is valid, see the comments for default_color
def set_color(c)
- # catch and reraise an exception to keep stack traces readable and clear
+ c = translate_color(c)
validate_color(c)
-
- if c.kind_of?(Array)
- @context.set_source_color(*c)
- else
- @context.set_source_color(c)
- end
+ @context.set_source_rgba(*c)
end
# test to see if the specified colour is a a valid cairo color
#
# for info on what is valid, see the comments for default_color
def validate_color(c)
+ c = translate_color(c)
@context.save
+ # catch and reraise an exception to keep stack traces readable and clear
begin
- if c.kind_of?(Array)
- # if the colour is being specified manually, there must be 3 or 4 elements
- raise ArgumentError if c.size != 3 && c.size != 4
- @context.set_source_color(c)
- else
- @context.set_source_color(c)
- end
- @default_color = c
+ raise ArgumentError unless c.kind_of?(Array)
+ raise ArgumentError if c.size != 3 && c.size != 4
+ @context.set_source_rgba(c)
rescue ArgumentError
c.kind_of?(Array) ? str = "[#{c.join(",")}]" : str = c.to_s
raise ArgumentError, "#{str} is not a valid color definition"
ensure
@context.restore