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