prawn 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. data/COPYING +340 -0
  2. data/LICENSE +56 -0
  3. data/README +30 -0
  4. data/Rakefile +83 -0
  5. data/data/fonts/Activa.ttf +0 -0
  6. data/data/fonts/Chalkboard.ttf +0 -0
  7. data/data/fonts/Courier-Bold.afm +342 -0
  8. data/data/fonts/Courier-BoldOblique.afm +342 -0
  9. data/data/fonts/Courier-Oblique.afm +342 -0
  10. data/data/fonts/Courier.afm +342 -0
  11. data/data/fonts/DejaVuSans.ttf +0 -0
  12. data/data/fonts/Dustismo_Roman.ttf +0 -0
  13. data/data/fonts/Helvetica-Bold.afm +2827 -0
  14. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  15. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  16. data/data/fonts/Helvetica.afm +3051 -0
  17. data/data/fonts/MustRead.html +19 -0
  18. data/data/fonts/Symbol.afm +213 -0
  19. data/data/fonts/Times-Bold.afm +2588 -0
  20. data/data/fonts/Times-BoldItalic.afm +2384 -0
  21. data/data/fonts/Times-Italic.afm +2667 -0
  22. data/data/fonts/Times-Roman.afm +2419 -0
  23. data/data/fonts/ZapfDingbats.afm +225 -0
  24. data/data/fonts/comicsans.ttf +0 -0
  25. data/data/fonts/gkai00mp.ttf +0 -0
  26. data/data/images/dice.png +0 -0
  27. data/data/images/pigs.jpg +0 -0
  28. data/data/images/ruport.png +0 -0
  29. data/data/images/ruport_data.dat +0 -0
  30. data/data/images/ruport_transparent.png +0 -0
  31. data/data/images/stef.jpg +0 -0
  32. data/data/shift_jis_text.txt +1 -0
  33. data/examples/addressbook.csv +6 -0
  34. data/examples/alignment.rb +16 -0
  35. data/examples/bounding_boxes.pdf +62 -0
  36. data/examples/bounding_boxes.rb +30 -0
  37. data/examples/canvas.pdf +81 -0
  38. data/examples/canvas.rb +12 -0
  39. data/examples/cell.rb +27 -0
  40. data/examples/currency.csv +1834 -0
  41. data/examples/curves.rb +10 -0
  42. data/examples/fancy_table.rb +48 -0
  43. data/examples/font_size.rb +19 -0
  44. data/examples/hexagon.rb +14 -0
  45. data/examples/image.pdf +0 -0
  46. data/examples/image.rb +23 -0
  47. data/examples/image2.rb +13 -0
  48. data/examples/inline_styles.pdf +117 -0
  49. data/examples/kerning.rb +27 -0
  50. data/examples/line.rb +31 -0
  51. data/examples/multi_page_layout.rb +14 -0
  52. data/examples/on_page_start.rb +17 -0
  53. data/examples/page_geometry.rb +28 -0
  54. data/examples/polygons.rb +16 -0
  55. data/examples/ruport_formatter.rb +47 -0
  56. data/examples/ruport_helpers.rb +17 -0
  57. data/examples/russian_boxes.rb +34 -0
  58. data/examples/simple_text.rb +15 -0
  59. data/examples/simple_text_ttf.rb +16 -0
  60. data/examples/sjis.rb +19 -0
  61. data/examples/table.rb +45 -0
  62. data/examples/table_bench.rb +92 -0
  63. data/examples/text_flow.rb +65 -0
  64. data/examples/utf8.rb +12 -0
  65. data/lib/prawn.rb +33 -0
  66. data/lib/prawn/compatibility.rb +33 -0
  67. data/lib/prawn/document.rb +334 -0
  68. data/lib/prawn/document/bounding_box.rb +253 -0
  69. data/lib/prawn/document/page_geometry.rb +78 -0
  70. data/lib/prawn/document/table.rb +253 -0
  71. data/lib/prawn/document/text.rb +346 -0
  72. data/lib/prawn/errors.rb +33 -0
  73. data/lib/prawn/font.rb +5 -0
  74. data/lib/prawn/font/cmap.rb +59 -0
  75. data/lib/prawn/font/metrics.rb +414 -0
  76. data/lib/prawn/font/wrapping.rb +45 -0
  77. data/lib/prawn/graphics.rb +285 -0
  78. data/lib/prawn/graphics/cell.rb +226 -0
  79. data/lib/prawn/images.rb +241 -0
  80. data/lib/prawn/images/jpg.rb +43 -0
  81. data/lib/prawn/images/png.rb +178 -0
  82. data/lib/prawn/pdf_object.rb +64 -0
  83. data/lib/prawn/reference.rb +47 -0
  84. data/spec/bounding_box_spec.rb +120 -0
  85. data/spec/box_calculation_spec.rb +17 -0
  86. data/spec/document_spec.rb +152 -0
  87. data/spec/graphics_spec.rb +250 -0
  88. data/spec/images_spec.rb +42 -0
  89. data/spec/jpg_spec.rb +25 -0
  90. data/spec/metrics_spec.rb +60 -0
  91. data/spec/pdf_object_spec.rb +102 -0
  92. data/spec/png_spec.rb +35 -0
  93. data/spec/reference_spec.rb +29 -0
  94. data/spec/spec_helper.rb +29 -0
  95. data/spec/table_spec.rb +145 -0
  96. data/spec/text_spec.rb +190 -0
  97. data/vendor/font_ttf/ttf.rb +20 -0
  98. data/vendor/font_ttf/ttf/datatypes.rb +189 -0
  99. data/vendor/font_ttf/ttf/encodings.rb +140 -0
  100. data/vendor/font_ttf/ttf/exceptions.rb +28 -0
  101. data/vendor/font_ttf/ttf/file.rb +290 -0
  102. data/vendor/font_ttf/ttf/fontchunk.rb +77 -0
  103. data/vendor/font_ttf/ttf/table/cmap.rb +408 -0
  104. data/vendor/font_ttf/ttf/table/cvt.rb +49 -0
  105. data/vendor/font_ttf/ttf/table/fpgm.rb +48 -0
  106. data/vendor/font_ttf/ttf/table/gasp.rb +88 -0
  107. data/vendor/font_ttf/ttf/table/glyf.rb +452 -0
  108. data/vendor/font_ttf/ttf/table/head.rb +86 -0
  109. data/vendor/font_ttf/ttf/table/hhea.rb +96 -0
  110. data/vendor/font_ttf/ttf/table/hmtx.rb +98 -0
  111. data/vendor/font_ttf/ttf/table/kern.rb +186 -0
  112. data/vendor/font_ttf/ttf/table/loca.rb +75 -0
  113. data/vendor/font_ttf/ttf/table/maxp.rb +81 -0
  114. data/vendor/font_ttf/ttf/table/name.rb +222 -0
  115. data/vendor/font_ttf/ttf/table/os2.rb +172 -0
  116. data/vendor/font_ttf/ttf/table/post.rb +120 -0
  117. data/vendor/font_ttf/ttf/table/prep.rb +27 -0
  118. data/vendor/font_ttf/ttf/table/vhea.rb +45 -0
  119. data/vendor/font_ttf/ttf/table/vmtx.rb +36 -0
  120. metadata +180 -0
@@ -0,0 +1,346 @@
1
+ # encoding: utf-8
2
+
3
+ # text.rb : Implements PDF text primitives
4
+ #
5
+ # Copyright May 2008, Gregory Brown. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+ require "zlib"
9
+
10
+ module Prawn
11
+ class Document
12
+ module Text
13
+ DEFAULT_FONT_SIZE = 12
14
+
15
+ # The built in fonts specified by the Adobe PDF spec.
16
+ BUILT_INS = %w[ Courier Courier-Bold Courier-Oblique Courier-BoldOblique
17
+ Helvetica Helvetica-Bold Helvetica-Oblique
18
+ Helvetica-BoldOblique Times-Roman Times-Bold Times-Italic
19
+ Times-BoldItalic Symbol ZapfDingbats ]
20
+
21
+ # Draws text on the page. If a point is specified via the <tt>:at</tt>
22
+ # option the text will begin exactly at that point, and the string is
23
+ # assumed to be pre-formatted to properly fit the page.
24
+ #
25
+ # When <tt>:at</tt> is not specified, Prawn attempts to wrap the text to
26
+ # fit within your current bounding box (or margin box if no bounding box
27
+ # is being used ). Text will flow onto the next page when it reaches
28
+ # the bottom of the margin_box. Text wrap in Prawn does not re-flow
29
+ # linebreaks, so if you want fully automated text wrapping, be sure to
30
+ # remove newlines before attempting to draw your string.
31
+ #
32
+ # pdf.text "Hello World", :at => [100,100]
33
+ # pdf.text "Goodbye World", :at => [50,50], :size => 16
34
+ # pdf.text "Will be wrapped when it hits the edge of your bounding box"
35
+ #
36
+ # If your font contains kerning pairs data that Prawn can parse, the
37
+ # text will be kerned by default. You can disable this feature by passing
38
+ # <tt>:kerning => false</tt>.
39
+ #
40
+ # Note that strings passed to this function should be encoded as UTF-8.
41
+ # If you get unexpected characters appearing in your rendered
42
+ # document, check this.
43
+ #
44
+ # If an empty box is rendered to your PDF instead of the character you
45
+ # wanted it usually means the current font doesn't include that character.
46
+ #
47
+ def text(text,options={})
48
+ # check the string is encoded sanely
49
+ normalize_encoding(text)
50
+
51
+ if options.key?(:kerning)
52
+ options[:kerning] = false unless font_metrics.has_kerning_data?
53
+ else
54
+ options[:kerning] = true if font_metrics.has_kerning_data?
55
+ end
56
+
57
+ # ensure a valid font is selected
58
+ font "Helvetica" unless fonts[@font]
59
+
60
+ return wrapped_text(text,options) unless options[:at]
61
+
62
+ x,y = translate(options[:at])
63
+ font_size(options[:size] || current_font_size) do
64
+ font_name = font_registry[fonts[@font]]
65
+
66
+ text = @font_metrics.convert_text(text,options)
67
+
68
+ add_content %Q{
69
+ BT
70
+ /#{font_name} #{current_font_size} Tf
71
+ #{x} #{y} Td
72
+ }
73
+
74
+ add_content Prawn::PdfObject(text, true) <<
75
+ " #{options[:kerning] ? 'TJ' : 'Tj'}\n"
76
+
77
+ add_content %Q{
78
+ ET
79
+ }
80
+ end
81
+ end
82
+
83
+ # Access to low-level font metrics data. This is only necessary for those
84
+ # who require direct access to font attributes, and can be safely ignored
85
+ # otherwise.
86
+ #
87
+ def font_metrics
88
+ @font_metrics ||= Prawn::Font::Metrics["Helvetica"]
89
+ end
90
+
91
+ # Sets the current font.
92
+ #
93
+ # The single parameter must be a string. It can be one of the 14 built-in
94
+ # fonts supported by PDF, or the location of a TTF file. The BUILT_INS
95
+ # array specifies the valid built in font values.
96
+ #
97
+ # pdf.font "Times-Roman"
98
+ # pdf.font "Chalkboard.ttf"
99
+ #
100
+ # If a ttf font is specified, the full file will be embedded in the
101
+ # rendered PDF. This should be your preferred option in most cases.
102
+ # It will increase the size of the resulting file, but also make it
103
+ # more portable.
104
+ #
105
+ def font(name)
106
+ proc_set :PDF, :Text
107
+ @font_metrics = Prawn::Font::Metrics[name]
108
+ case(name)
109
+ when /\.ttf$/
110
+ @font = embed_ttf_font(name)
111
+ else
112
+ @font = register_builtin_font(name)
113
+ end
114
+ set_current_font
115
+ end
116
+
117
+ # Sets the default font size for use within a block. Individual overrides
118
+ # can be used as desired. The previous font size will be restored after the
119
+ # block.
120
+ #
121
+ # Prawn::Document.generate("font_size.pdf") do
122
+ # font_size!(16)
123
+ # text "At size 16"
124
+ #
125
+ # font_size(10) do
126
+ # text "At size 10"
127
+ # text "At size 6", :size => 6
128
+ # text "At size 10"
129
+ # end
130
+ #
131
+ # text "At size 16"
132
+ # end
133
+ #
134
+ # When called without an argument, this method returns the current font
135
+ # size.
136
+ #
137
+ def font_size(size=nil)
138
+ return current_font_size unless size
139
+ font_size_before_block = @font_size || DEFAULT_FONT_SIZE
140
+ font_size!(size)
141
+ yield
142
+ font_size!(font_size_before_block)
143
+ end
144
+
145
+ # Sets the default font size. See example in font_size
146
+ #
147
+ def font_size!(size)
148
+ @font_size = size unless size == nil
149
+ end
150
+
151
+ alias_method :font_size=, :font_size!
152
+
153
+ private
154
+
155
+ # The current font_size being used in the document.
156
+ #
157
+ def current_font_size
158
+ @font_size || DEFAULT_FONT_SIZE
159
+ end
160
+
161
+ def move_text_position(dy)
162
+ (y - dy) < @margin_box.absolute_bottom ? start_new_page : self.y -= dy
163
+ end
164
+
165
+ def text_width(text,size)
166
+ @font_metrics.string_width(text,size)
167
+ end
168
+
169
+ # TODO: Get kerning working with wrapped text
170
+ def wrapped_text(text,options)
171
+ options[:align] ||= :left
172
+ font_size(options[:size] || current_font_size) do
173
+ font_name = font_registry[fonts[@font]]
174
+
175
+ text = @font_metrics.naive_wrap(text, bounds.right, current_font_size,
176
+ :kerning => options[:kerning])
177
+
178
+ lines = text.lines
179
+
180
+ lines.each do |e|
181
+
182
+ move_text_position(@font_metrics.font_height(current_font_size) +
183
+ @font_metrics.descender / 1000.0 * current_font_size)
184
+
185
+
186
+ line_width = text_width(e,font_size)
187
+ case(options[:align])
188
+ when :left
189
+ x = @bounding_box.absolute_left
190
+ when :center
191
+ x = @bounding_box.absolute_left +
192
+ (@bounding_box.width - line_width) / 2.0
193
+ when :right
194
+ x = @bounding_box.absolute_right - line_width
195
+ end
196
+
197
+ add_content %Q{
198
+ BT
199
+ /#{font_name} #{current_font_size} Tf
200
+ #{x} #{y} Td
201
+ }
202
+
203
+ add_content Prawn::PdfObject(@font_metrics.convert_text(e,options), true) <<
204
+ " #{options[:kerning] ? 'TJ' : 'Tj'}\n"
205
+
206
+ add_content %Q{
207
+ ET
208
+ }
209
+
210
+ ds = -@font_metrics.descender / 1000.0 * current_font_size
211
+ move_text_position(options[:spacing] || ds )
212
+ end
213
+ end
214
+ end
215
+
216
+ def embed_ttf_font(file) #:nodoc:
217
+
218
+ ttf_metrics = Prawn::Font::Metrics::TTF.new(file)
219
+
220
+ unless File.file?(file)
221
+ raise ArgumentError, "file #{file} does not exist"
222
+ end
223
+
224
+ basename = @font_metrics.basename
225
+
226
+ raise "Can't detect a postscript name for #{file}" if basename.nil?
227
+
228
+ enctables[basename] = @font_metrics.enc_table
229
+
230
+ if enctables[basename].nil?
231
+ raise "#{file} missing the required encoding table"
232
+ end
233
+
234
+ font_content = File.open(file,"rb") { |f| f.read }
235
+ compressed_font = Zlib::Deflate.deflate(font_content)
236
+
237
+ fontfile = ref(:Length => compressed_font.size,
238
+ :Length1 => font_content.size,
239
+ :Filter => :FlateDecode )
240
+ fontfile << compressed_font
241
+
242
+ # TODO: Not sure what to do about CapHeight, as ttf2afm doesn't
243
+ # pick it up. Missing proper StemV and flags
244
+ #
245
+ descriptor = ref(:Type => :FontDescriptor,
246
+ :FontName => basename,
247
+ :FontFile2 => fontfile,
248
+ :FontBBox => @font_metrics.bbox,
249
+ :Flags => 32, # FIXME: additional flags
250
+ :StemV => 0,
251
+ :ItalicAngle => 0,
252
+ :Ascent => @font_metrics.ascender,
253
+ :Descent => @font_metrics.descender
254
+ )
255
+
256
+ descendant = ref(:Type => :Font,
257
+ :Subtype => :CIDFontType2, # CID, TTF
258
+ :BaseFont => basename,
259
+ :CIDSystemInfo => { :Registry => "Adobe",
260
+ :Ordering => "Identity",
261
+ :Supplement => 0 },
262
+ :FontDescriptor => descriptor,
263
+ :W => @font_metrics.glyph_widths,
264
+ :CIDToGIDMap => :Identity
265
+ )
266
+
267
+ to_unicode_content = @font_metrics.to_unicode_cmap.to_s
268
+ compressed_to_unicode = Zlib::Deflate.deflate(to_unicode_content)
269
+ to_unicode = ref(:Length => compressed_to_unicode.size,
270
+ :Length1 => to_unicode_content.size,
271
+ :Filter => :FlateDecode )
272
+ to_unicode << compressed_to_unicode
273
+
274
+ # TODO: Needs ToUnicode (at least)
275
+ fonts[basename] ||= ref(:Type => :Font,
276
+ :Subtype => :Type0,
277
+ :BaseFont => basename,
278
+ :DescendantFonts => [descendant],
279
+ :Encoding => :"Identity-H",
280
+ :ToUnicode => to_unicode)
281
+ return basename
282
+ end
283
+
284
+ def normalize_encoding(text)
285
+ # TODO: if the current font is a built in one, we can't use the utf-8
286
+ # string provided by the user. We should convert it to WinAnsi or
287
+ # MacRoman or some such.
288
+ if text.respond_to?(:"encode!")
289
+ # if we're running under a M17n aware VM, ensure the string provided is
290
+ # UTF-8 (by converting it if necessary)
291
+ begin
292
+ text.encode!("UTF-8")
293
+ rescue
294
+ raise Prawn::Errors::IncompatibleStringEncoding, "Encoding " +
295
+ "#{text.encoding} can not be transparently converted to UTF-8. " +
296
+ "Please ensure the encoding of the string you are attempting " +
297
+ "to use is set correctly"
298
+ end
299
+ else
300
+ # on a non M17N aware VM, use unpack as a hackish way to verify the
301
+ # string is valid utf-8. I thought it was better than loading iconv
302
+ # though.
303
+ begin
304
+ text.unpack("U*")
305
+ rescue
306
+ raise Prawn::Errors::IncompatibleStringEncoding, "The string you " +
307
+ "are attempting to render is not encoded in valid UTF-8."
308
+ end
309
+ end
310
+ end
311
+
312
+ def register_builtin_font(name) #:nodoc:
313
+ unless BUILT_INS.include?(name)
314
+ raise Prawn::Errors::UnknownFont, "#{name} is not a known font."
315
+ end
316
+ fonts[name] ||= ref(:Type => :Font,
317
+ :Subtype => :Type1,
318
+ :BaseFont => name.to_sym,
319
+ :Encoding => :MacRomanEncoding)
320
+ return name
321
+ end
322
+
323
+ def set_current_font #:nodoc:
324
+ return if @font.nil?
325
+ font_registry[fonts[@font]] ||= :"F#{font_registry.size + 1}"
326
+
327
+ page_fonts.merge!(
328
+ font_registry[fonts[@font]] => fonts[@font]
329
+ )
330
+ end
331
+
332
+ def enctables #:nodoc
333
+ @enctables ||= {}
334
+ end
335
+
336
+ def font_registry #:nodoc:
337
+ @font_registry ||= {}
338
+ end
339
+
340
+ def fonts #:nodoc:
341
+ @fonts ||= {}
342
+ end
343
+
344
+ end
345
+ end
346
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+
3
+ # errors.rb : Implements custom error classes for Prawn
4
+ #
5
+ # Copyright April 2008, Gregory Brown. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+
9
+ module Prawn
10
+ module Errors
11
+
12
+ # This error is raised when Prawn::PdfObject() encounters a Ruby object it
13
+ # cannot convert to PDF
14
+ #
15
+ class FailedObjectConversion < StandardError; end
16
+
17
+ # This error is raised when Document#page_layout is set to anything
18
+ # other than :portrait or :landscape
19
+ #
20
+ class InvalidPageLayout < StandardError; end
21
+
22
+ # This error is raised when Prawn cannot find a specified font
23
+ #
24
+ class UnknownFont < StandardError; end
25
+
26
+ # This error is raised when prawn is being used on a M17N aware VM,
27
+ # and the user attempts to add text that isn't compatible with UTF-8
28
+ # to their document
29
+ #
30
+ class IncompatibleStringEncoding < StandardError; end
31
+
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ require "prawn/font/wrapping"
4
+ require "prawn/font/metrics"
5
+ require "prawn/font/cmap"
@@ -0,0 +1,59 @@
1
+ # encoding: utf-8
2
+
3
+ # cmap.rb : class for building ToUnicode CMaps for Type0 fonts
4
+ #
5
+ # Copyright May 2008, Gregory Brown / James Healy. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+
9
+ module Prawn
10
+ module Font #:nodoc:
11
+ class CMap #:nodoc:
12
+
13
+ def initialize
14
+ @codes = {}
15
+ end
16
+
17
+ def [](c)
18
+ @codes[c]
19
+ end
20
+
21
+ def []=(c, v)
22
+ @codes[c] = v
23
+ end
24
+
25
+ def to_s
26
+ # TODO: learn what all this means. I just copied the basic structure
27
+ # from an existing PDF
28
+ # TODO: make this more efficient. The mapping can be specified in
29
+ # ranges instead of one -> one
30
+ res = "12 dict begin\n"
31
+ res << "begincmap\n"
32
+ res << "/CIDSystemInfo\n"
33
+ res << "<< /Registry (Adobe)\n"
34
+ res << "/Ordering (UCS)\n"
35
+ res << "/Supplement 0\n"
36
+ res << ">> def\n"
37
+ res << "/CMapName /Adobe-Identity-UCS def\n"
38
+ res << "/CMapType 2 def\n"
39
+ res << "begincodespacerange\n"
40
+ res << "<0000> <ffff>\n"
41
+ res << "endcodespacerange\n"
42
+ res << "9 beginbfchar\n"
43
+ @codes.keys.sort.each do |key|
44
+ val = @codes[key]
45
+ ccode = val.to_s(16)
46
+ ccode = ("0" * (4 - ccode.size)) + ccode
47
+ unicode = key.to_s(16)
48
+ unicode = ("0" * (4 - unicode.size)) + unicode
49
+ res << "<#{ccode}> <#{unicode}>\n"
50
+ end
51
+ res << "endbfchar\n"
52
+ res << "endcmap\n"
53
+ res << "CMapName currentdict /CMap defineresource pop\n"
54
+ res << "end\n"
55
+ res << "end\n"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,414 @@
1
+ # encoding: utf-8
2
+
3
+ # metrics.rb : Font metrics parsers for AFM and TTF.
4
+ #
5
+ # Font::Metrics::Adobe is mainly a port of CPAN's Font::AFM
6
+ # http://search.cpan.org/~gaas/Font-AFM-1.19/AFM.pm
7
+ #
8
+ # Copyright May 2008, Gregory Brown / James Edward Gray II. All Rights Reserved.
9
+ #
10
+ # This is free software. Please see the LICENSE and COPYING files for details.
11
+
12
+ module Prawn
13
+ module Font #:nodoc:
14
+ class Metrics #:nodoc:
15
+
16
+ include Prawn::Font::Wrapping
17
+
18
+ def self.[](font)
19
+ data[font] ||= case(font)
20
+ when /\.ttf$/
21
+ TTF.new(font)
22
+ else
23
+ Adobe.new(font)
24
+ end
25
+ end
26
+
27
+ def self.data
28
+ @data ||= {}
29
+ end
30
+
31
+ def string_height(string,options={})
32
+ string = naive_wrap(string, options[:line_width], options[:font_size])
33
+ string.lines.to_a.length * font_height(options[:font_size])
34
+ end
35
+
36
+ class Adobe < Metrics #:nodoc:
37
+
38
+ ISOLatin1Encoding = %w[
39
+ .notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef
40
+ .notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef
41
+ .notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef
42
+ .notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef space
43
+ exclam quotedbl numbersign dollar percent ampersand quoteright
44
+ parenleft parenright asterisk plus comma minus period slash zero one
45
+ two three four five six seven eight nine colon semicolon less equal
46
+ greater question at A B C D E F G H I J K L M N O P Q R S
47
+ T U V W X Y Z bracketleft backslash bracketright asciicircum
48
+ underscore quoteleft a b c d e f g h i j k l m n o p q r s
49
+ t u v w x y z braceleft bar braceright asciitilde .notdef .notdef
50
+ .notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef
51
+ .notdef .notdef .notdef .notdef .notdef .notdef .notdef dotlessi grave
52
+ acute circumflex tilde macron breve dotaccent dieresis .notdef ring
53
+ cedilla .notdef hungarumlaut ogonek caron space exclamdown cent
54
+ sterling currency yen brokenbar section dieresis copyright ordfeminine
55
+ guillemotleft logicalnot hyphen registered macron degree plusminus
56
+ twosuperior threesuperior acute mu paragraph periodcentered cedilla
57
+ onesuperior ordmasculine guillemotright onequarter onehalf threequarters
58
+ questiondown Agrave Aacute Acircumflex Atilde Adieresis Aring AE
59
+ Ccedilla Egrave Eacute Ecircumflex Edieresis Igrave Iacute Icircumflex
60
+ Idieresis Eth Ntilde Ograve Oacute Ocircumflex Otilde Odieresis
61
+ multiply Oslash Ugrave Uacute Ucircumflex Udieresis Yacute Thorn
62
+ germandbls agrave aacute acircumflex atilde adieresis aring ae
63
+ ccedilla egrave eacute ecircumflex edieresis igrave iacute icircumflex
64
+ idieresis eth ntilde ograve oacute ocircumflex otilde odieresis divide
65
+ oslash ugrave uacute ucircumflex udieresis yacute thorn ydieresis
66
+ ]
67
+
68
+ attr_reader :attributes
69
+
70
+ def initialize(font_name)
71
+ @attributes = {}
72
+ @glyph_widths = {}
73
+ @bounding_boxes = {}
74
+ @kern_pairs = {}
75
+
76
+ file = font_name.sub(/\.afm$/,'') + '.afm'
77
+ unless file[0..0] == "/"
78
+ file = find_font(file)
79
+ end
80
+
81
+ parse_afm(file)
82
+ end
83
+
84
+ def bbox
85
+ fontbbox.split(/\s+/).map { |e| Integer(e) }
86
+ end
87
+
88
+ def font_height(font_size)
89
+ Float(bbox[3] - bbox[1]) * font_size / 1000.0
90
+ end
91
+
92
+ def string_width(string, font_size, options = {})
93
+ scale = font_size / 1000.0
94
+
95
+ if options[:kerning]
96
+ kern(string).inject(0) do |s,r|
97
+ if r.is_a? String
98
+ s + string_width(r, font_size, :kerning => false)
99
+ else
100
+ s - (r * scale)
101
+ end
102
+ end
103
+ else
104
+ string.unpack("U*").inject(0) do |s,r|
105
+ s + latin_glyphs_table[r]
106
+ end * scale
107
+ end
108
+ end
109
+
110
+ def kern(string)
111
+ kerned = string.unpack("U*").inject([]) do |a,r|
112
+ if a.last.is_a? Array
113
+ if kern = latin_kern_pairs_table[[a.last.last, r]]
114
+ a << kern << [r]
115
+ else
116
+ a.last << r
117
+ end
118
+ else
119
+ a << [r]
120
+ end
121
+ a
122
+ end
123
+
124
+ kerned.map { |r|
125
+ i = r.is_a?(Array) ? r.pack("U*") : r
126
+ i.is_a?(Numeric) ? -i : i
127
+ }
128
+ end
129
+
130
+ def latin_kern_pairs_table
131
+ @kern_pairs_table ||= @kern_pairs.inject({}) do |h,p|
132
+ h[p[0].map { |n| ISOLatin1Encoding.index(n) }] = p[1]
133
+ h
134
+ end
135
+ end
136
+
137
+ def latin_glyphs_table
138
+ @glyphs_table ||= (0..255).map do |i|
139
+ @glyph_widths[ISOLatin1Encoding[i]].to_i
140
+ end
141
+ end
142
+
143
+ def ascender
144
+ @attributes["ascender"].to_i
145
+ end
146
+
147
+ def descender
148
+ @attributes["descender"].to_i
149
+ end
150
+
151
+ # Hackish, but does the trick for now.
152
+ def method_missing(method, *args, &block)
153
+ name = method.to_s.delete("_")
154
+ if @attributes.include? name
155
+ @attributes[name]
156
+ else
157
+ super
158
+ end
159
+ end
160
+
161
+ def metrics_path
162
+ @metrics_path ||= (ENV['METRICS'] ||
163
+ "/usr/lib/afm:/usr/local/lib/afm:"+
164
+ "/usr/openwin/lib/fonts/afm/:"+
165
+ "#{Prawn::BASEDIR+'/data/fonts/'}:.").split(':')
166
+ end
167
+
168
+ def has_kerning_data?
169
+ true
170
+ end
171
+
172
+ def type0?
173
+ false
174
+ end
175
+
176
+ def convert_text(text, options={})
177
+ options[:kerning] ? kern(text) : text
178
+ end
179
+
180
+ private
181
+
182
+ def find_font(file)
183
+ metrics_path.find { |f| File.exist? "#{f}/#{file}" } + "/#{file}"
184
+ rescue NoMethodError
185
+ raise Prawn::Errors::UnknownFont,
186
+ "Couldn't find the font: #{file} in any of:\n" +
187
+ @metrics_path.join("\n")
188
+ end
189
+
190
+ def parse_afm(file)
191
+ section = []
192
+
193
+ File.open(file,"rb") do |file|
194
+
195
+ file.each do |line|
196
+ if line =~ /^Start(\w+)/
197
+ section.push $1
198
+ next
199
+ elsif line =~ /^End(\w+)/
200
+ section.pop
201
+ next
202
+ end
203
+
204
+ if section == ["FontMetrics", "CharMetrics"]
205
+ next unless line =~ /^CH?\s/
206
+
207
+ name = line[/\bN\s+(\.?\w+)\s*;/, 1]
208
+ @glyph_widths[name] = line[/\bWX\s+(\d+)\s*;/, 1].to_i
209
+ @bounding_boxes[name] = line[/\bB\s+([^;]+);/, 1].to_s.rstrip
210
+ elsif section == ["FontMetrics", "KernData", "KernPairs"]
211
+ next unless line =~ /^KPX\s+(\.?\w+)\s+(\.?\w+)\s+(-?\d+)/
212
+ @kern_pairs[[$1, $2]] = $3.to_i
213
+ elsif section == ["FontMetrics", "KernData", "TrackKern"]
214
+ next
215
+ elsif section == ["FontMetrics", "Composites"]
216
+ next
217
+ elsif line =~ /(^\w+)\s+(.*)/
218
+ key, value = $1.to_s.downcase, $2
219
+
220
+ @attributes[key] = @attributes[key] ?
221
+ Array(@attributes[key]) << value : value
222
+ else
223
+ warn "Can't parse: #{line}"
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ class TTF < Metrics #:nodoc:
231
+
232
+ def initialize(font)
233
+ @ttf = ::Font::TTF::File.open(font,"rb")
234
+ @attributes = {}
235
+ @glyph_widths = {}
236
+ @bounding_boxes = {}
237
+ end
238
+
239
+ def cmap
240
+ @cmap ||= enc_table.charmaps
241
+ end
242
+
243
+ def string_width(string, font_size, options = {})
244
+ scale = font_size / 1000.0
245
+ if options[:kerning]
246
+ kern(string,:skip_conversion => true).inject(0) do |s,r|
247
+ if r.is_a? String
248
+ s + string_width(r, font_size, :kerning => false)
249
+ else
250
+ s + r * scale
251
+ end
252
+ end
253
+ else
254
+ string.unpack("U*").inject(0) do |s,r|
255
+ s + character_width_by_code(r)
256
+ end * scale
257
+ end
258
+ end
259
+
260
+ # TODO: NASTY.
261
+ def kern(string,options={})
262
+ string.unpack("U*").inject([]) do |a,r|
263
+ if a.last.is_a? Array
264
+ if kern = kern_pairs_table[[cmap[a.last.last], cmap[r]]]
265
+ kern *= scale_factor
266
+ a << kern << [r]
267
+ else
268
+ a.last << r
269
+ end
270
+ else
271
+ a << [r]
272
+ end
273
+ a
274
+ end.map { |r|
275
+ if options[:skip_conversion]
276
+ r.is_a?(Array) ? r.pack("U*") : r
277
+ else
278
+ i = r.is_a?(Array) ? r.pack("U*") : r
279
+ x = if i.is_a?(String)
280
+ unicode_codepoints = i.unpack("U*")
281
+ glyph_codes = unicode_codepoints.map { |u|
282
+ enc_table.get_glyph_id_for_unicode(u)
283
+ }
284
+ glyph_codes.pack("n*")
285
+ else
286
+ i
287
+ end
288
+ x.is_a?(Numeric) ? -x : x
289
+ end
290
+ }
291
+ end
292
+
293
+ def glyph_widths
294
+ glyphs = cmap.values.uniq.sort
295
+ first_glyph = glyphs.shift
296
+ widths = [first_glyph, [Integer(hmtx[first_glyph][0] * scale_factor)]]
297
+ prev_glyph = first_glyph
298
+ glyphs.each do |glyph|
299
+ unless glyph == prev_glyph + 1
300
+ widths << glyph
301
+ widths << []
302
+ end
303
+ widths.last << Integer(hmtx[glyph][0] * scale_factor )
304
+ prev_glyph = glyph
305
+ end
306
+ widths
307
+ end
308
+
309
+ def bbox
310
+ head = @ttf.get_table(:head)
311
+ [:x_min, :y_min, :x_max, :y_max].map do |atr|
312
+ Integer(head.send(atr)) * scale_factor
313
+ end
314
+ end
315
+
316
+ def ascender
317
+ Integer(@ttf.get_table(:hhea).ascender * scale_factor)
318
+ end
319
+
320
+ def descender
321
+ Integer(@ttf.get_table(:hhea).descender * scale_factor)
322
+ end
323
+
324
+ def font_height(size)
325
+ (ascender - descender) * size / 1000.0
326
+ end
327
+
328
+ def basename
329
+ return @basename if @basename
330
+ ps_name = ::Font::TTF::Table::Name::NameRecord::POSTSCRIPT_NAME
331
+
332
+ @ttf.get_table(:name).name_records.each do |rec|
333
+ @basename = rec.utf8_str.to_sym if rec.name_id == ps_name
334
+ end
335
+ @basename
336
+ end
337
+
338
+ def enc_table
339
+ @enc_table ||= @ttf.get_table(:cmap).encoding_tables.find do |t|
340
+ t.class == ::Font::TTF::Table::Cmap::EncodingTable4
341
+ end
342
+ end
343
+
344
+ # TODO: instead of creating a map that contains every glyph in the font,
345
+ # only include the glyphs that were used
346
+ def to_unicode_cmap
347
+ return @to_unicode if @to_unicode
348
+ @to_unicode = Prawn::Font::CMap.new
349
+ unicode_for_glyph = cmap.invert
350
+ glyphs = unicode_for_glyph.keys.uniq.sort
351
+ glyphs.each do |glyph|
352
+ @to_unicode[unicode_for_glyph[glyph]] = glyph
353
+ end
354
+ @to_unicode
355
+ end
356
+
357
+ def kern_pairs_table
358
+ return @kern_pairs_table if @kern_pairs_table
359
+
360
+ table = @ttf.get_table(:kern).subtables.find { |s|
361
+ s.is_a? ::Font::TTF::Table::Kern::KerningSubtable0 }
362
+
363
+ if table
364
+ @kern_pairs_table ||= table.kerning_pairs.inject({}) do |h,p|
365
+ h[[p.left, p.right]] = p.value
366
+ h
367
+ end
368
+ else
369
+ @kern_pairs_table = {}
370
+ end
371
+ end
372
+
373
+ def has_kerning_data?
374
+ !kern_pairs_table.empty?
375
+ rescue ::Font::TTF::TableMissing
376
+ false
377
+ end
378
+
379
+ def type0?
380
+ true
381
+ end
382
+
383
+ def convert_text(text,options)
384
+ text = text.chomp
385
+ if options[:kerning]
386
+ kern(text)
387
+ else
388
+ unicode_codepoints = text.unpack("U*")
389
+ glyph_codes = unicode_codepoints.map { |u|
390
+ enc_table.get_glyph_id_for_unicode(u)
391
+ }
392
+ text = glyph_codes.pack("n*")
393
+ end
394
+ end
395
+
396
+ private
397
+
398
+ def hmtx
399
+ @hmtx ||= @ttf.get_table(:hmtx).metrics
400
+ end
401
+
402
+ def character_width_by_code(code)
403
+ return 0 unless cmap[code]
404
+ Integer(hmtx[cmap[code]][0] * scale_factor)
405
+ end
406
+
407
+ def scale_factor
408
+ @scale ||= 1 / Float(@ttf.get_table(:head).units_per_em / 1000.0)
409
+ end
410
+
411
+ end
412
+ end
413
+ end
414
+ end