# encoding: ASCII-8BIT # images.rb : Implements PDF image embedding # # 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' module Prawn module Images # add the image at filename to the current page. Currently only # JPG and PNG files are supported. # # Arguments: # filename:: the path to the file to be embedded # # Options: # :at:: the location of the top left corner of the image. # :position:: One of (:left, :center, :right) or an x-offset # :height:: the height of the image [actual height of the image] # :width:: the width of the image [actual width of the image] # :scale:: scale the dimensions of the image proportionally # # 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 # # If only one of :width / :height are provided, the image will be scaled # proportionally. When both are provided, the image will be stretched to # fit the dimensions without maintaining the aspect ratio. # def image(filename, options={}) Prawn.verify_options [:at,:position, :height, :width, :scale], options raise ArgumentError, "#{filename} not found" unless File.file?(filename) image_content = File.read_binary(filename) image_sha1 = Digest::SHA1.hexdigest(image_content) # register the fact that the current page uses images proc_set :ImageC # if this image has already been embedded, just reuse it image_obj = image_registry[image_sha1] if image_registry[image_sha1] info = image_registry[image_sha1][:info] image_obj = image_registry[image_sha1][:obj] else # build the image object and embed the raw data image_obj = case detect_image_format(image_content) when :jpg then info = Prawn::Images::JPG.new(image_content) build_jpg_object(image_content, info) when :png then info = Prawn::Images::PNG.new(image_content) build_png_object(image_content, info) end 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) if options[:at] x,y = translate(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}" page_xobjects.merge!( label => image_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 ] end private 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 return [x,y] end def build_jpg_object(data, jpg) color_space = case jpg.channels when 1 :DeviceGray when 4 :DeviceCMYK else :DeviceRGB end obj = ref(:Type => :XObject, :Subtype => :Image, :Filter => :DCTDecode, :ColorSpace => color_space, :BitsPerComponent => jpg.bits, :Width => jpg.width, :Height => jpg.height, :Length => data.size ) obj << data return obj end def build_png_object(data, png) if png.compression_method != 0 raise ArgumentError, 'PNG uses an unsupported compression method' end if png.filter_method != 0 raise ArgumentError, 'PNG uses an unsupported filter method' end if png.interlace_method != 0 raise ArgumentError, 'PNG uses unsupported interlace method' end if png.bits > 8 raise ArgumentError, 'PNG uses more than 8 bits' end case png.pixel_bytes when 1 color = :DeviceGray when 3 color = :DeviceRGB end # build the image dict obj = ref(:Type => :XObject, :Subtype => :Image, :Height => png.height, :Width => png.width, :BitsPerComponent => png.bits, :Length => png.img_data.size, :Filter => :FlateDecode ) unless png.alpha_channel obj.data[:DecodeParms] = {:Predictor => 15, :Colors => png.pixel_bytes, :Columns => png.width} end # append the actual image data to the object as a stream obj << png.img_data # sort out the colours of the image if png.palette.empty? obj.data[:ColorSpace] = color else # embed the colour palette in the PDF as a object stream palette_obj = ref(:Length => png.palette.size) palette_obj << png.palette # build the color space array for the image obj.data[:ColorSpace] = [:Indexed, :DeviceRGB, (png.palette.size / 3) -1, palette_obj] end # ************************************* # add transparency data if necessary # ************************************* # For PNG color types 0, 2 and 3, the transparency data is stored in # a dedicated PNG chunk, and is exposed via the transparency attribute # of the PNG class. if png.transparency[:grayscale] # Use Color Key Masking (spec section 4.8.5) # - An array with N elements, where N is two times the number of color # components. val = png.transparency[:grayscale] obj.data[:Mask] = [val, val] elsif png.transparency[:rgb] # Use Color Key Masking (spec section 4.8.5) # - An array with N elements, where N is two times the number of color # components. rgb = png.transparency[:rgb] obj.data[:Mask] = rgb.collect { |val| [val,val] }.flatten elsif png.transparency[:indexed] # TODO: broken. I was attempting to us Color Key Masking, but I think # we need to construct an SMask i think. Maybe do it inside # the PNG class, and store it in alpha_channel #obj.data[:Mask] = png.transparency[:indexed] end # For PNG color types 4 and 6, the transparency data is stored as a alpha # channel mixed in with the main image data. The PNG class seperates # it out for us and makes it available via the alpha_channel attribute if png.alpha_channel smask_obj = ref(:Type => :XObject, :Subtype => :Image, :Height => png.height, :Width => png.width, :BitsPerComponent => 8, :Length => png.alpha_channel.size, :Filter => :FlateDecode, :ColorSpace => :DeviceGray, :Decode => [0, 1] ) smask_obj << png.alpha_channel obj.data[:SMask] = smask_obj end return obj end def calc_image_dimensions(info, options) # TODO: allow the image to be aligned in a box 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] end [w,h] end def detect_image_format(content) top = content[0,128] if top[0, 3] == "\xff\xd8\xff" return :jpg elsif top[0, 8] == "\x89PNG\x0d\x0a\x1a\x0a" return :png else raise ArgumentError, "Unsupported Image Type" end end def image_registry @image_registry ||= {} end def next_image_id @image_counter ||= 0 @image_counter += 1 end end end