File: images.rb

package info (click to toggle)
ruby-prawn 2.3.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 4,380 kB
  • sloc: ruby: 15,820; sh: 43; makefile: 20
file content (206 lines) | stat: -rw-r--r-- 6,615 bytes parent folder | download
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
# encoding: ASCII-8BIT

# frozen_string_literal: true

# 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'
require 'pathname'

module Prawn
  module Images
    # @group Stable API

    # Add the image at filename to the current page. Currently only
    # JPG and PNG files are supported. (Note that processing PNG
    # images with alpha channels can be processor and memory intensive.)
    #
    # Arguments:
    # <tt>file</tt>:: path to file or an object that responds to #read and
    #   #rewind
    #
    # 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, :bottom) 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::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
    # 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.
    #
    #
    # 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://prawnpdf.org/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 %i[
        at position vposition height
        width scale fit
      ], options

      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].
    #
    # @private
    def build_image_object(file)
      image_content = verify_and_read_image(file)
      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
        info = Prawn.image_handler.find(image_content).new(image_content)

        # Bump PDF version if the image requires it
        if info.respond_to?(:min_pdf_version)
          renderer.min_version(info.min_pdf_version)
        end

        # 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

      [image_obj, info]
    end

    # 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.
    #
    # @private
    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[label] = pdf_obj

      cm_params = PDF::Core.real_params([w, 0, 0, h, x, y - h])
      renderer.add_content("\nq\n#{cm_params} cm\n/#{label} Do\nQ")
    end

    private

    def verify_and_read_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.read
      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_or_path.binread
    end

    def image_position(width, height, options)
      options[:position] ||= :left

      y = case options[:vposition]
          when :top
            bounds.absolute_top
          when :center
            bounds.absolute_top - (bounds.height - height) / 2.0
          when :bottom
            bounds.absolute_bottom + height
          when Numeric
            bounds.absolute_top - options[:vposition]
          else
            determine_y_with_page_flow(height)
          end

      x = case options[:position]
          when :left
            bounds.left_side
          when :center
            bounds.left_side + (bounds.width - width) / 2.0
          when :right
            bounds.right_side - width
          when Numeric
            options[:position] + bounds.left_side
          end

      [x, y]
    end

    def determine_y_with_page_flow(height)
      if overruns_page?(height)
        bounds.move_past_bottom
      end
      y
    end

    def overruns_page?(height)
      (y - height) < reference_bounds.absolute_bottom
    end

    def image_registry
      @image_registry ||= {}
    end

    def next_image_id
      @image_counter ||= 0
      @image_counter += 1
    end
  end
end