public
Description: Fast, Nimble PDF Writer for Ruby
Homepage: http://prawn.majesticseacreature.com
Clone URL: git://github.com/sandal/prawn.git
prawn / lib / prawn / images.rb
100644 318 lines (278 sloc) 10.594 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# 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:
    # <tt>file</tt>:: path to file or an object that responds to #read
    #
    # Options:
    # <tt>:at</tt>:: the location of the top left corner of the image.
    # <tt>:position</tt>:: One of (:left, :center, :right) or an x-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 [with,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
    #
    # 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.
    #
    # If instead of an explicit filename, an object with a read method is
    # 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
    # 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.
    # (See also: Prawn::Images::PNG , Prawn::Images::JPG)
    #
    def image(file, options={})
      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.read_binary(file)
      end
      
      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 ]
      
      return info
    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
      options[:vposition] ||= :top
      y = case options[:vposition]
      when :top
        bounds.absolute_top
      when :center
        bounds.absolute_top - (bounds.height - h) / 2.0
      when :bottom
        bounds.absolute_bottom + h
      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)
      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]
 
      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