lib/pdf/wrapper.rb in pdf-wrapper-0.1.3 vs lib/pdf/wrapper.rb in pdf-wrapper-0.2.0

- old
+ new

@@ -1,10 +1,12 @@ # coding: utf-8 require 'stringio' require 'pdf/core' require 'pdf/errors' +require 'tempfile' +require 'fileutils' require File.dirname(__FILE__) + "/wrapper/graphics" require File.dirname(__FILE__) + "/wrapper/images" require File.dirname(__FILE__) + "/wrapper/loading" require File.dirname(__FILE__) + "/wrapper/table" @@ -16,32 +18,53 @@ require 'cairo' module PDF # Create PDF files by using the cairo and pango libraries. # - # Rendering to a file: + # == Rendering to a file # # require 'pdf/wrapper' - # pdf = PDF::Wrapper.new(:paper => :A4) + # pdf = PDF::Wrapper.new("somefile.pdf", :paper => :A4) # pdf.text "Hello World" - # pdf.render_to_file("wrapper.pdf") + # pdf.finish # - # Rendering to a string: + # == Rendering to a file (alternative) # # require 'pdf/wrapper' - # pdf = PDF::Wrapper.new(:paper => :A4) + # File.open("somefile.pdf", "w") do |output| + # pdf = PDF::Wrapper.new(output, :paper => :A4) + # pdf.text "Hello World" + # pdf.finish + # end + # + # == Rendering to a string + # + # require 'pdf/wrapper' + # output = StringIO.new + # pdf = PDF::Wrapper.new(output, :paper => :A4) # pdf.text "Hello World", :font_size => 16 - # puts pdf.render + # pdf.finish + # puts output.string # - # Changing the default font: + # == Block format # + # Avoid the need to call finish() + # # require 'pdf/wrapper' - # pdf = PDF::Wrapper.new(:paper => :A4) + # PDF::Wrapper.open("somefile.pdf", :paper => :A4) + # pdf.text "Hello World", :font_size => 16 + # end + # + # == Changing the default font + # + # require 'pdf/wrapper' + # pdf = PDF::Wrapper.new("file.pdf", :paper => :A4) # pdf.font("Monospace") # pdf.text "Hello World", :font => "Sans Serif", :font_size => 18 # pdf.text "Pretend this is a code sample" - # puts pdf.render + # pdf.finish + # class Wrapper attr_reader :page # borrowed from PDF::Writer @@ -72,10 +95,13 @@ :LEGAL => [612.00, 1008.00], :FOLIO => [612.00, 936.00], :EXECUTIVE => [521.86, 756.00] } # create a new PDF::Wrapper class to compose a PDF document + # Params: + # <tt>output</tt>:: Where to render the PDF to. Can be a string containing a filename, + # or an IO object (File, StringIO, etc) # Options: # <tt>:paper</tt>:: The paper size to use (default :A4) # <tt>:orientation</tt>:: :portrait (default) or :landscape # <tt>:background_color</tt>:: The background colour to use (default :white) # <tt>:margin_top</tt>:: The size of the default top margin (default 5% of page) @@ -83,55 +109,64 @@ # <tt>:margin_left</tt>:: The size of the default left margin (default 5% of page) # <tt>:margin_right</tt>:: The size of the default right margin (default 5% of page) # <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. # The page will be sized to match the template size. The use templates on subsequent pages, see the options for # start_new_page. - def initialize(opts={}) + def initialize(*args) # TODO: Investigate ways of using the cairo transform/translate/scale functionality to # reduce the amount of irritating co-ordinate maths the user of PDF::Wrapper (ie. me!) # is required to do. # - translate the pdf body width so that it's 1.0 wide and 1.0 high? # TODO: find a way to add metadata (title, author, subject, etc) to the output file # currently no way to specify this in cairo. - # tentatively scheduled for cairo 1.8 - see: + # tentatively scheduled for cairo 1.10 - see: # - http://cairographics.org/roadmap/ # - http://lists.cairographics.org/archives/cairo/2007-September/011441.html # - http://lists.freedesktop.org/archives/cairo/2006-April/006809.html # ensure we have recentish cairo bindings 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" + if args.size == 0 + opts = {} + output = StringIO.new + warn "WARNING: deprecated call to PDF::Wrapper constructor. Check API documentation on new compulsory parameter" + elsif args.size == 1 + if args.first.kind_of?(Hash) + opts = *args + output = StringIO.new + warn "WARNING: deprecated call to PDF::Wrapper constructor. Check API documentation on new compulsory parameter" + else + output = args.first + opts = {} + end + elsif args.size == 2 + output, opts = *args + else + raise ArgumentError, 'Invalid parameters passed to constructor' + end + options = {:paper => :A4, :orientation => :portrait, :background_color => :white } options.merge!(opts) # test for invalid options - options.assert_valid_keys(:paper, :orientation, :background_color, :margin_left, :margin_right, :margin_top, :margin_bottom, :template) - options[:paper] = options[:paper].to_sym - raise ArgumentError, "Invalid paper option" unless PAGE_SIZES.include?(options[:paper]) + options.assert_valid_keys(:paper, :orientation, :background_color, :margin_left, :margin_right, + :margin_top, :margin_bottom, :io, :template) - # set page dimensions - if options[:orientation].eql?(:portrait) - @page_width = PAGE_SIZES[options[:paper]][0] - @page_height = PAGE_SIZES[options[:paper]][1] - elsif options[:orientation].eql?(:landscape) - @page_width = PAGE_SIZES[options[:paper]][1] - @page_height = PAGE_SIZES[options[:paper]][0] - else - raise ArgumentError, "Invalid orientation" - end + set_dimensions(options[:orientation], options[:paper]) # set page margins and dimensions of usable canvas @margin_left = options[:margin_left] || (@page_width * 0.05).ceil @margin_right = options[:margin_right] || (@page_width * 0.05).ceil @margin_top = options[:margin_top] || (@page_height * 0.05).ceil @margin_bottom = options[:margin_bottom] || (@page_height * 0.05).ceil # initialize some cairo objects to draw on - @output = StringIO.new + @output = output @surface = Cairo::PDFSurface.new(@output, @page_width, @page_height) @context = Cairo::Context.new(@surface) # set the background colour color(options[:background_color]) @@ -156,10 +191,25 @@ # move the cursor to the top left of the usable canvas reset_cursor end + # convenience method, takes the same arguments as the constructor along with a block, + # and automatically finishes the PDF for you. + # + #= Usage + # + # PDF::Wrapper.open("somefile.pdf") do |pdf| + # pdf.text "hi!" + # end + # + def self.open(output, options = {}, &block) + pdf = PDF::Wrapper.new(output, options) + yield pdf + pdf.finish + end + ##################################################### # Functions relating to calculating various page dimensions ##################################################### # Returns the x value of the left margin @@ -229,10 +279,18 @@ # returns 2 values - x,y def current_point @context.current_point end + def x + @context.current_point.first + end + + def y + @context.current_point.last + end + def margin_bottom device_y_to_user_y(@margin_bottom).to_i end def margin_left @@ -303,31 +361,34 @@ # Functions relating to generating the final document ##################################################### # render the PDF and return it as a string def render - # finalise the document, then convert the StringIO object it was rendered to - # into a string + # TODO: remove this method at some point. Deprecation started on 15th September 2008 + warn "WARNING: render() is deprecated. See documentation for PDF::Wrapper#initialize for more information" finish - return @output.string + case @output + when StringIO then return @output.string + when File then return File.read(@output.path) + else + return File.read(@output) + end end - def render_to_file(filename) #nodoc - # TODO: remove this at some point - warn "WARNING: render_to_file() is deprecated, please use render_file()" - render_file filename - end - # save the rendered PDF to a file def render_file(filename) + # TODO: remove this method at some point. Deprecation started on 15th September 2008 + warn "WARNING: render_file() is deprecated. See documentation for PDF::Wrapper#initialize for more information" finish - - # write each line from the StringIO object it was rendered to into the - # requested file - File.open(filename, "w") do |of| - @output.rewind - @output.each_line { |line| of.write(line) } + case @output + when StringIO then + File.open(filename, "w") do |of| + of.write(@output.string) + end + when File then return FileUtils.cp(@output.path, filename) + else + return FileUtils.cp(@output, filename) end end ##################################################### # Misc Functions @@ -447,17 +508,34 @@ # reset the cursor by moving it to the top left of the useable section of the page def reset_cursor @context.move_to(margin_left,margin_top) end + def finish + # finalise the document + @context.show_page + @context.target.finish + #@output.close if io_output? + @surface.finish + #@surface.destroy + #@context.destroy + self + rescue Cairo::SurfaceFinishedError + # do nothing, we're happy that the surfaced has been finished + end + # returns true if the PDF has already been rendered, false if it hasn't. # Due to limitations of the underlying libraries, content cannot be # added to a PDF once it has been rendered. # def finished? - @output.seek(@output.size - 6) - bytes = @output.read(6) + if io_output? + @output.seek(@output.size - 6) + bytes = @output.read(6) + else + bytes = @output[-6,6] + end bytes == "%%EOF\n" ? true : false end # add the same elements to multiple pages. Useful for adding items like headers, footers and # watermarks. @@ -486,16 +564,20 @@ end # move to the next page # # options: + # <tt>:paper</tt>:: The paper size to use (default: same as the previous page) + # <tt>:orientation</tt>:: :portrait or :landscape (default: same as the previous page) # <tt>:pageno</tt>:: If specified, the current page number will be set to that. By default, the page number will just increment. # <tt>:template</tt>:: The path to an image file. If specified, the new page will use the specified image as a template. The page will be sized to match the template size # def start_new_page(opts = {}) - opts.assert_valid_keys(:pageno, :template) + opts.assert_valid_keys(:paper, :orientation, :pageno, :template) + set_dimensions(opts[:orientation], opts[:paper]) + @context.show_page if opts[:template] w, h = image_dimensions(opts[:template]) @surface.set_size(w, h) @@ -520,16 +602,35 @@ end end private - def finish - # finalise the document - @context.show_page - @context.target.finish - rescue Cairo::SurfaceFinishedError - # do nothing, we're happy that the surfaced has been finished + def set_dimensions(orientation, paper) + # use the defaults if none were provided + orientation ||= @orientation + paper ||= @paper + + # safety check + orientation = orientation.to_sym + paper = paper.to_sym + + raise ArgumentError, "Unrecognised paper size (#{paper})" if PAGE_SIZES[paper].nil? + + # set page dimensions + if orientation.eql?(:portrait) + @page_width = PAGE_SIZES[paper][0] + @page_height = PAGE_SIZES[paper][1] + elsif orientation.eql?(:landscape) + @page_width = PAGE_SIZES[paper][1] + @page_height = PAGE_SIZES[paper][0] + else + raise ArgumentError, "Invalid orientation" + end + + # make the new values the defaults + @orientation = orientation + @paper = paper end # runs the code in block, passing it a hash of options that might be # required def call_repeating_element(spec, block) @@ -553,9 +654,17 @@ { :left => x, :top => y, :width => points_to_right_margin(x), :height => points_to_bottom_margin(y) } + end + + def io_output? + if @output.respond_to?(:write) && @output.respond_to?(:read) + true + else + false + end end # save and restore the cursor position around a block def save_coords(&block) origx, origy = current_point