lib/prawn/images.rb in prawn-0.12.0 vs lib/prawn/images.rb in prawn-0.13.0

- old
+ new

@@ -4,45 +4,46 @@ # Copyright April 2008, James Healy, Gregory Brown. All Rights Reserved. # # This is free software. Please see the LICENSE and COPYING files for details. require 'digest/sha1' +require 'pathname' module Prawn module Images - # Add the image at filename to the current page. Currently only # JPG and PNG files are supported. # - # NOTE: Prawn is very slow at rendering PNGs with alpha channels. The - # workaround for those who don't mind installing RMagick is to use: + # NOTE: Prawn is very slow at rendering PNGs with alpha channels, and this + # uses a lot of RAM. The workaround for those who don't mind installing + # RMagick is to use: # # http://github.com/amberbit/prawn-fast-png # # Arguments: # <tt>file</tt>:: path to file or an object that responds to #read # # Options: # <tt>:at</tt>:: an array [x,y] with the location of the top left corner of the image. # <tt>:position</tt>:: One of (:left, :center, :right) or an x-offset - # <tt>:vposition</tt>:: One of (:top, :center, :center) or an y-offset + # <tt>:vposition</tt>:: One of (:top, :center, :center) or an y-offset # <tt>:height</tt>:: the height of the image [actual height of the image] # <tt>:width</tt>:: the width of the image [actual width of the image] # <tt>:scale</tt>:: scale the dimensions of the image proportionally # <tt>:fit</tt>:: scale the dimensions of the image proportionally to fit inside [width,height] - # - # Prawn::Document.generate("image2.pdf", :page_layout => :landscape) do - # pigs = "#{Prawn::BASEDIR}/data/images/pigs.jpg" - # image pigs, :at => [50,450], :width => 450 # - # dice = "#{Prawn::BASEDIR}/data/images/dice.png" - # image dice, :at => [50, 450], :scale => 0.75 - # end + # Prawn::Document.generate("image2.pdf", :page_layout => :landscape) do + # pigs = "#{Prawn::DATADIR}/images/pigs.jpg" + # image pigs, :at => [50,450], :width => 450 # + # dice = "#{Prawn::DATADIR}/images/dice.png" + # image dice, :at => [50, 450], :scale => 0.75 + # end + # # If only one of :width / :height are provided, the image will be scaled - # proportionally. When both are provided, the image will be stretched to + # proportionally. When both are provided, the image will be stretched to # fit the dimensions without maintaining the aspect ratio. # # # If :at is provided, the image will be place in the current page but # the text position will not be changed. @@ -52,88 +53,104 @@ # passed as +file+, you can embed images from IO objects and things # that act like them (including Tempfiles and open-uri objects). # # require "open-uri" # - # Prawn::Document.generate("remote_images.pdf") do + # Prawn::Document.generate("remote_images.pdf") do # image open("http://prawn.majesticseacreature.com/media/prawn_logo.png") # end # # This method returns an image info object which can be used to check the - # dimensions of an image object if needed. + # dimensions of an image object if needed. # (See also: Prawn::Images::PNG , Prawn::Images::JPG) - # + # def image(file, options={}) - Prawn.verify_options [:at, :position, :vposition, :height, + Prawn.verify_options [:at, :position, :vposition, :height, :width, :scale, :fit], options - if file.respond_to?(:read) - image_content = file.read - else - raise ArgumentError, "#{file} not found" unless File.file?(file) - image_content = File.binread(file) - end - + pdf_obj, info = build_image_object(file) + embed_image(pdf_obj, info, options) + + info + end + + # Builds an info object (Prawn::Images::*) and a PDF reference representing + # the given image. Return a pair: [pdf_obj, info]. + # + def build_image_object(file) + io = verify_and_open_image(file) + image_content = io.read image_sha1 = Digest::SHA1.hexdigest(image_content) # if this image has already been embedded, just reuse it if image_registry[image_sha1] info = image_registry[image_sha1][:info] image_obj = image_registry[image_sha1][:obj] else # Build the image object - klass = case detect_image_format(image_content) - when :jpg then Prawn::Images::JPG - when :png then Prawn::Images::PNG - end - info = klass.new(image_content) + info = Prawn.image_handler.find(image_content).new(image_content) # Bump PDF version if the image requires it min_version(info.min_pdf_version) if info.respond_to?(:min_pdf_version) # Add the image to the PDF and register it in case we see it again. image_obj = info.build_pdf_object(self) image_registry[image_sha1] = {:obj => image_obj, :info => info} end - # find where the image will be placed and how big it will be - w,h = calc_image_dimensions(info, options) + [image_obj, info] + end - if options[:at] - x,y = map_to_absolute(options[:at]) - else - x,y = image_position(w,h,options) - move_text_position h + # Given a PDF image resource <tt>pdf_obj</tt> that has been added to the + # page's resources and an <tt>info</tt> object (the pair returned from + # build_image_object), embed the image according to the <tt>options</tt> + # given. + # + def embed_image(pdf_obj, info, options) + # find where the image will be placed and how big it will be + w,h = info.calc_image_dimensions(options) + + if options[:at] + x,y = map_to_absolute(options[:at]) + else + x,y = image_position(w,h,options) + move_text_position h end # add a reference to the image object to the current page # resource list and give it a label label = "I#{next_image_id}" - state.page.xobjects.merge!( label => image_obj ) + state.page.xobjects.merge!(label => pdf_obj) # add the image to the current page instruct = "\nq\n%.3f 0 0 %.3f %.3f %.3f cm\n/%s Do\nQ" add_content instruct % [ w, h, x, y - h, label ] - - return info end - private - + private + + def verify_and_open_image(io_or_path) + # File or IO + if io_or_path.respond_to?(:rewind) + io = io_or_path + # Rewind if the object we're passed is an IO, so that multiple embeds of + # the same IO object will work + io.rewind + # read the file as binary so the size is calculated correctly + # guard binmode because some objects acting io-like don't implement it + io.binmode if io.respond_to?(:binmode) + return io + end + # String or Pathname + io_or_path = Pathname.new(io_or_path) + raise ArgumentError, "#{io_or_path} not found" unless io_or_path.file? + io = io_or_path.open('rb') + io + end + def image_position(w,h,options) options[:position] ||= :left - - x = case options[:position] - when :left - bounds.absolute_left - when :center - bounds.absolute_left + (bounds.width - w) / 2.0 - when :right - bounds.absolute_right - w - when Numeric - options[:position] + bounds.absolute_left - end y = case options[:vposition] when :top bounds.absolute_top when :center @@ -143,69 +160,33 @@ when Numeric bounds.absolute_top - options[:vposition] else determine_y_with_page_flow(h) end + + x = case options[:position] + when :left + bounds.left_side + when :center + bounds.left_side + (bounds.width - w) / 2.0 + when :right + bounds.right_side - w + when Numeric + options[:position] + bounds.left_side + end + return [x,y] - end - + end + def determine_y_with_page_flow(h) if overruns_page?(h) - start_new_page - bounds.absolute_top - else - self.y + bounds.move_past_bottom end - end - - def overruns_page?(h) - (self.y - h) < reference_bounds.absolute_bottom + self.y end - def calc_image_dimensions(info, options) - w = options[:width] || info.width - h = options[:height] || info.height - - if options[:width] && !options[:height] - wp = w / info.width.to_f - w = info.width * wp - h = info.height * wp - elsif options[:height] && !options[:width] - hp = h / info.height.to_f - w = info.width * hp - h = info.height * hp - elsif options[:scale] - w = info.width * options[:scale] - h = info.height * options[:scale] - elsif options[:fit] - bw, bh = options[:fit] - bp = bw / bh.to_f - ip = info.width / info.height.to_f - if ip > bp - w = bw - h = bw / ip - else - h = bh - w = bh * ip - end - end - info.scaled_width = w - info.scaled_height = h - [w,h] - end - - def detect_image_format(content) - top = content[0,128] - - # Unpack before comparing for JPG header, so as to avoid having to worry - # about the source string encoding. We just want a byte-by-byte compare. - if top[0, 3].unpack("C*") == [255, 216, 255] - return :jpg - elsif top[0, 8].unpack("C*") == [137, 80, 78, 71, 13, 10, 26, 10] - return :png - else - raise Errors::UnsupportedImageType, "image file is an unrecognised format" - end + def overruns_page?(h) + (self.y - h) < reference_bounds.absolute_bottom end def image_registry @image_registry ||= {} end