public
Description: Fast, Nimble PDF Writer for Ruby
Homepage: http://prawn.majesticseacreature.com
Clone URL: git://github.com/sandal/prawn.git
prawn / lib / prawn / document / text.rb
100644 293 lines (251 sloc) 10.265 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
# text.rb : Implements PDF text primitives
#
# Copyright May 2008, Gregory Brown. All Rights Reserved.
#
# This is free software. Please see the LICENSE and COPYING files for details.
require "zlib"
 
module Prawn
  class Document
    module Text
      DEFAULT_FONT_SIZE = 12
      
      # The built in fonts specified by the Adobe PDF spec.
      BUILT_INS = %w[ Courier Courier-Bold Courier-Oblique Courier-BoldOblique
Helvetica Helvetica-Bold Helvetica-Oblique
Helvetica-BoldOblique Times-Roman Times-Bold Times-Italic
Times-BoldItalic Symbol ZapfDingbats ]
 
      # Draws text on the page. If a point is specified via the <tt>:at</tt>
      # option the text will begin exactly at that point, and the string is
      # assumed to be pre-formatted to properly fit the page.
      #
      # When <tt>:at</tt> is not specified, Prawn attempts to wrap the text to
      # fit within your current bounding box (or margin box if no bounding box
      # is being used ). Text will flow onto the next page when it reaches
      # the bottom of the margin_box. Text wrap in Prawn does not re-flow
      # linebreaks, so if you want fully automated text wrapping, be sure to
      # remove newlines before attempting to draw your string.
      #
      # pdf.text "Hello World", :at => [100,100]
      # pdf.text "Goodbye World", :at => [50,50], :size => 16
      # pdf.text "Will be wrapped when it hits the edge of your bounding box"
      #
      # All strings passed to this function should be encoded as UTF-8.
      # If you gets unexpected characters appearing in your rendered
      # document, check this.
      #
      # If an empty box is rendered to your PDF instead of the character you
      # wanted it usually means the current font doesn't include that character.
      #
      def text(text,options={})
        # TODO: if the current font is a built in one, we can't use the utf-8
        # string provided by the user. We should convert it to WinAnsi or
        # MacRoman or some such.
 
        # ensure a valid font is selected
        font "Helvetica" unless fonts[@font]
 
        return wrapped_text(text,options) unless options[:at]
        x,y = translate(options[:at])
        font_size(options[:size] || current_font_size) do
          font_name = font_registry[fonts[@font]]
 
          # replace the users string with a string composed of glyph codes
          # TODO: hackish
          if fonts[@font].data[:Subtype] == :Type0
            unicode_codepoints = text.unpack("U*")
            glyph_codes = unicode_codepoints.map { |u|
              enctables[@font].get_glyph_id_for_unicode(u)
            }
            text = glyph_codes.pack("n*")
          end
 
          add_content %Q{
BT
/#{font_name} #{current_font_size} Tf
#{x} #{y} Td
#{Prawn::PdfObject(text)} Tj
ET
}
        end
      end
 
      def font_metrics #:nodoc:
        @font_metrics ||= Prawn::Font::Metrics["Helvetica"]
      end
 
      # Sets the current font.
      #
      # The single parameter must be a string. It can be one of the 14 built-in
      # fonts supported by PDF, or the location of a TTF file. The BUILT_INS
      # array specifies the valid built in font values.
      #
      # pdf.font "Times-Roman"
      # pdf.font "Chalkboard.ttf"
      #
      # If a ttf font is specified, the full file will be embedded in the
      # rendered PDF. This should be your preferred option in most cases.
      # It will increase the size of the resulting file, but also make it
      # more portable.
      #
      def font(name)
        @font_metrics = Prawn::Font::Metrics[name]
        case(name)
        when /\.ttf$/
          @font = embed_ttf_font(name)
        else
          @font = register_builtin_font(name)
        end
        set_current_font
      end
      
      # Sets the default font size for use within a block. Individual overrides
      # can be used as desired. The previous font size will be stored after the
      # block.
      #
      # Prawn::Document.generate("font_size.pdf") do
      # font_size!(16)
      # text "At size 16"
      #
      # font_size(10) do
      # text "At size 10"
      # text "At size 6", :size => 6
      # text "At size 10"
      # end
      #
      # text "At size 16"
      # end
      #
      def font_size(size)
        font_size_before_block = @font_size || DEFAULT_FONT_SIZE
        font_size!(size)
        yield
        font_size!(font_size_before_block)
      end
      
      # Sets the default font size. See example in font_size
      #
      def font_size!(size)
        @font_size = size unless size == nil
      end
     
      # The current font_size being used in the document.
      #
      def current_font_size
        @font_size || DEFAULT_FONT_SIZE
      end
 
      private
 
      def move_text_position(dy)
         if (y - dy) < @margin_box.absolute_bottom
           return start_new_page
         end
         self.y -= dy
      end
 
      def text_width(text,size)
        @font_metrics.string_width(text,size)
      end
 
      def wrapped_text(text,options)
        font_size(options[:size] || current_font_size) do
          font_name = font_registry[fonts[@font]]
 
          text = @font_metrics.naive_wrap(text, bounds.right, current_font_size)
 
          # THIS CODE JUST DID THE NASTY. FIXME!
          lines = text.lines
 
          if fonts[@font].data[:Subtype] == :Type0
            lines = lines.map do |line|
              unicode_codepoints = line.chomp.unpack("U*")
              glyph_codes = unicode_codepoints.map { |u|
                enctables[@font].get_glyph_id_for_unicode(u)
              }
              glyph_codes.pack("n*")
            end
          end
          
          lines.each do |e|
            move_text_position(@font_metrics.font_height(current_font_size) +
                               @font_metrics.descender / 1000.0 * current_font_size)
            add_content %Q{
BT
/#{font_name} #{current_font_size} Tf
#{@bounding_box.absolute_left} #{y} Td
#{Prawn::PdfObject(e.to_s.chomp)} Tj
ET
}
 
            move_text_position(-@font_metrics.descender / 1000.0 * current_font_size)
          end
        end
      end
 
      def embed_ttf_font(file) #:nodoc:
 
        ttf_metrics = Prawn::Font::Metrics::TTF.new(file)
 
        unless File.file?(file)
          raise ArgumentError, "file #{file} does not exist"
        end
 
        basename = @font_metrics.basename
 
        raise "Can't detect a postscript name for #{file}" if basename.nil?
 
        enctables[basename] = @font_metrics.enc_table
 
        if enctables[basename].nil?
          raise "#{file} missing the required encoding table"
        end
 
        font_content = File.read(file)
        compressed_font = Zlib::Deflate.deflate(font_content)
 
        fontfile = ref(:Length => compressed_font.size,
                       :Length1 => font_content.size,
                       :Filter => :FlateDecode )
        fontfile << compressed_font
 
        # TODO: Not sure what to do about CapHeight, as ttf2afm doesn't
        # pick it up. Missing proper StemV and flags
        #
        descriptor = ref(:Type => :FontDescriptor,
                         :FontName => basename,
                         :FontFile2 => fontfile,
                         :FontBBox => @font_metrics.bbox,
                         :Flags => 32, # FIXME: additional flags
                         :StemV => 0,
                         :ItalicAngle => 0,
                         :Ascent => @font_metrics.ascender,
                         :Descent => @font_metrics.descender
                         )
 
        descendant = ref(:Type => :Font,
                         :Subtype => :CIDFontType2, # CID, TTF
                         :BaseFont => basename,
                         :CIDSystemInfo => { :Registry => "Adobe",
                                              :Ordering => "Identity",
                                              :Supplement => 0 },
                         :FontDescriptor => descriptor,
                         :W => @font_metrics.glyph_widths,
                         :CIDToGIDMap => :Identity
                        )
 
        to_unicode_content = @font_metrics.to_unicode_cmap.to_s
        compressed_to_unicode = Zlib::Deflate.deflate(to_unicode_content)
        to_unicode = ref(:Length => compressed_to_unicode.size,
                         :Length1 => to_unicode_content.size,
                         :Filter => :FlateDecode )
        to_unicode << compressed_to_unicode
 
        # TODO: Needs ToUnicode (at least)
        fonts[basename] ||= ref(:Type => :Font,
                                :Subtype => :Type0,
                                :BaseFont => basename,
                                :DescendantFonts => [descendant],
                                :Encoding => :"Identity-H",
                                :ToUnicode => to_unicode)
        return basename
      end
 
      def register_builtin_font(name) #:nodoc:
        unless BUILT_INS.include?(name)
          raise Prawn::Errors::UnknownFont, "#{name} is not a known font."
        end
        fonts[name] ||= ref(:Type => :Font,
                            :Subtype => :Type1,
                            :BaseFont => name.to_sym,
                            :Encoding => :MacRomanEncoding)
        return name
      end
 
      def set_current_font #:nodoc:
        return if @font.nil?
        font_registry[fonts[@font]] ||= :"F#{font_registry.size + 1}"
 
        @current_page.data[:Resources][:Font].merge!(
          font_registry[fonts[@font]] => fonts[@font]
        )
      end
 
      def enctables #:nodoc
        @enctables ||= {}
      end
      def font_registry #:nodoc:
        @font_registry ||= {}
      end
 
      def font_proc #:nodoc:
        @font_proc ||= ref [:PDF, :Text]
      end
 
      def fonts #:nodoc:
        @fonts ||= {}
      end
 
    end
  end
end