prawn 0.1.2 → 0.2.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 (53) hide show
  1. data/README +16 -2
  2. data/Rakefile +3 -3
  3. data/data/images/arrow.png +0 -0
  4. data/data/images/arrow2.png +0 -0
  5. data/data/images/barcode_issue.png +0 -0
  6. data/data/images/ruport_type0.png +0 -0
  7. data/examples/cell.rb +14 -3
  8. data/examples/chinese_text_wrapping.rb +17 -0
  9. data/examples/family_based_styling.rb +21 -0
  10. data/examples/fancy_table.rb +4 -4
  11. data/examples/flowing_text_with_header_and_footer.rb +72 -0
  12. data/examples/font_size.rb +2 -2
  13. data/examples/lazy_bounding_boxes.rb +19 -0
  14. data/examples/table.rb +13 -11
  15. data/examples/text_flow.rb +1 -1
  16. data/lib/prawn.rb +44 -15
  17. data/lib/prawn/compatibility.rb +20 -7
  18. data/lib/prawn/document.rb +72 -122
  19. data/lib/prawn/document/bounding_box.rb +124 -24
  20. data/lib/prawn/document/internals.rb +107 -0
  21. data/lib/prawn/document/table.rb +99 -70
  22. data/lib/prawn/document/text.rb +92 -314
  23. data/lib/prawn/errors.rb +13 -2
  24. data/lib/prawn/font.rb +312 -1
  25. data/lib/prawn/font/cmap.rb +1 -1
  26. data/lib/prawn/font/metrics.rb +52 -49
  27. data/lib/prawn/font/wrapping.rb +14 -12
  28. data/lib/prawn/graphics.rb +23 -74
  29. data/lib/prawn/graphics/cell.rb +30 -25
  30. data/lib/prawn/graphics/color.rb +132 -0
  31. data/lib/prawn/images.rb +37 -16
  32. data/lib/prawn/images/png.rb +29 -24
  33. data/lib/prawn/pdf_object.rb +3 -1
  34. data/spec/bounding_box_spec.rb +12 -3
  35. data/spec/document_spec.rb +40 -72
  36. data/spec/font_spec.rb +97 -0
  37. data/spec/graphics_spec.rb +46 -99
  38. data/spec/images_spec.rb +4 -21
  39. data/spec/pdf_object_spec.rb +8 -8
  40. data/spec/png_spec.rb +47 -12
  41. data/spec/spec_helper.rb +5 -24
  42. data/spec/table_spec.rb +53 -59
  43. data/spec/text_spec.rb +28 -93
  44. data/vendor/pdf-inspector/README +18 -0
  45. data/vendor/pdf-inspector/lib/pdf/inspector.rb +25 -0
  46. data/vendor/pdf-inspector/lib/pdf/inspector/graphics.rb +80 -0
  47. data/vendor/pdf-inspector/lib/pdf/inspector/page.rb +16 -0
  48. data/vendor/pdf-inspector/lib/pdf/inspector/text.rb +31 -0
  49. data/vendor/pdf-inspector/lib/pdf/inspector/xobject.rb +19 -0
  50. metadata +63 -38
  51. data/examples/on_page_start.rb +0 -17
  52. data/examples/table_bench.rb +0 -92
  53. data/spec/box_calculation_spec.rb +0 -17
data/lib/prawn/errors.rb CHANGED
@@ -23,11 +23,22 @@ module Prawn
23
23
  #
24
24
  class UnknownFont < StandardError; end
25
25
 
26
- # This error is raised when prawn is being used on a M17N aware VM,
26
+ # This error is raised when Prawn is being used on a M17N aware VM,
27
27
  # and the user attempts to add text that isn't compatible with UTF-8
28
28
  # to their document
29
29
  #
30
- class IncompatibleStringEncoding < StandardError; end
30
+ class IncompatibleStringEncoding < StandardError; end
31
+
32
+ # This error is raised when Prawn encounters an unknown key in functions
33
+ # that accept an options hash. This usually means there is a typo in your
34
+ # code or that the option you are trying to use has a different name than
35
+ # what you have specified.
36
+ #
37
+ class UnknownOption < StandardError; end
38
+
39
+ # This error is raised when table data is malformed
40
+ #
41
+ class InvalidTableData < StandardError; end
31
42
 
32
43
  end
33
44
  end
data/lib/prawn/font.rb CHANGED
@@ -2,4 +2,315 @@
2
2
 
3
3
  require "prawn/font/wrapping"
4
4
  require "prawn/font/metrics"
5
- require "prawn/font/cmap"
5
+ require "prawn/font/cmap"
6
+
7
+ module Prawn
8
+
9
+ class Document
10
+ # Sets the current font.
11
+ #
12
+ # The single parameter must be a string. It can be one of the 14 built-in
13
+ # fonts supported by PDF, or the location of a TTF file. The BUILT_INS
14
+ # array specifies the valid built in font values.
15
+ #
16
+ # pdf.font "Times-Roman"
17
+ # pdf.font "Chalkboard.ttf"
18
+ #
19
+ # If a ttf font is specified, the full file will be embedded in the
20
+ # rendered PDF. This should be your preferred option in most cases.
21
+ # It will increase the size of the resulting file, but also make it
22
+ # more portable.
23
+ #
24
+ def font(name=nil, options={})
25
+ if name
26
+ if font_families.key?(name)
27
+ ff = name
28
+ name = font_families[name][options[:style] || :normal]
29
+ end
30
+ Prawn::Font.register(name,:for => self, :family => ff) unless font_registry[name]
31
+ font_registry[name].add_to_current_page
32
+ @font_name = name
33
+ elsif @font_name.nil?
34
+ Prawn::Font.register("Helvetica", :for => self, :family => "Helvetica")
35
+ @font_name = "Helvetica"
36
+ end
37
+ font_registry[@font_name]
38
+ end
39
+
40
+ # Hash of Font objects keyed by names
41
+ #
42
+ def font_registry
43
+ @font_registry ||= {}
44
+ end
45
+
46
+ # Hash that maps font family names to their styled individual font names
47
+ #
48
+ # To add support for another font family, append to this hash, e.g:
49
+ #
50
+ # pdf.font_families.update(
51
+ # "MyTrueTypeFamily" => { :bold => "foo-bold.ttf",
52
+ # :italic => "foo-italic.ttf",
53
+ # :bold_italic => "foo-bold-italic.ttf",
54
+ # :normal => "foo.ttf" })
55
+ #
56
+ # This will then allow you to use the fonts like so:
57
+ #
58
+ # pdf.font("MyTrueTypeFamily", :style => :bold)
59
+ # pdf.text "Some bold text"
60
+ # pdf.font("MyTrueTypeFamily")
61
+ # pdf.text "Some normal text"
62
+ #
63
+ # This assumes that you have appropriate TTF fonts for each style you
64
+ # wish to support.
65
+ #
66
+ def font_families
67
+ @font_families ||= Hash.new { |h,k| h[k] = {} }.merge!(
68
+ { "Courier" => { :bold => "Courier-Bold",
69
+ :italic => "Courier-Oblique",
70
+ :bold_italic => "Courier-BoldOblique",
71
+ :normal => "Courier" },
72
+
73
+ "Times-Roman" => { :bold => "Times-Bold",
74
+ :italic => "Times-Italic",
75
+ :bold_italic => "Times-BoldItalic",
76
+ :normal => "Times-Roman" },
77
+
78
+ "Helvetica" => { :bold => "Helvetica-Bold",
79
+ :italic => "Helvetica-Oblique",
80
+ :bold_italic => "Helvetica-BoldOblique",
81
+ :normal => "Helvetica" }
82
+ })
83
+ end
84
+ end
85
+
86
+ # Provides font information and helper functions.
87
+ #
88
+ class Font
89
+
90
+ BUILT_INS = %w[ Courier Helvetica Times-Roman Symbol ZapfDingbats
91
+ Courier-Bold Courier-Oblique Courier-BoldOblique
92
+ Times-Bold Times-Italic Times-BoldItalic
93
+ Helvetica-Bold Helvetica-Oblique Helvetica-BoldOblique ]
94
+
95
+ DEFAULT_SIZE = 12
96
+
97
+ def self.register(name,options={})
98
+ options[:for].font_registry[name] = Font.new(name,options)
99
+ end
100
+
101
+ attr_reader :metrics, :identifier, :reference, :name, :family
102
+ attr_writer :size
103
+
104
+ def initialize(name,options={})
105
+ @name = name
106
+ @family = options[:family]
107
+
108
+ @metrics = Prawn::Font::Metrics[name]
109
+ @document = options[:for]
110
+
111
+ @document.proc_set :PDF, :Text
112
+ @size = DEFAULT_SIZE
113
+ @identifier = :"F#{@document.font_registry.size + 1}"
114
+
115
+ case(name)
116
+ when /\.ttf$/
117
+ embed_ttf(name)
118
+ else
119
+ register_builtin(name)
120
+ end
121
+
122
+ add_to_current_page
123
+ end
124
+
125
+ # Sets the default font size for use within a block. Individual overrides
126
+ # can be used as desired. The previous font size will be restored after the
127
+ # block.
128
+ #
129
+ # Prawn::Document.generate("font_size.pdf") do
130
+ # font.size = 16
131
+ # text "At size 16"
132
+ #
133
+ # font.size(10) do
134
+ # text "At size 10"
135
+ # text "At size 6", :size => 6
136
+ # text "At size 10"
137
+ # end
138
+ #
139
+ # text "At size 16"
140
+ # end
141
+ #
142
+ # When called without an argument, this method returns the current font
143
+ # size.
144
+ #
145
+ def size(points=nil)
146
+ return @size unless points
147
+ size_before_yield = @size
148
+ @size = points
149
+ yield
150
+ @size = size_before_yield
151
+ end
152
+
153
+ # Gets width of string in PDF points at current font size
154
+ #
155
+ def width_of(string)
156
+ @metrics.string_width(string,@size)
157
+ end
158
+
159
+ # Gets height of text in PDF points at current font size.
160
+ # Text +:line_width+ must be specified in PDF points.
161
+ #
162
+ def height_of(text,options={})
163
+ @metrics.string_height( text, :font_size => @size,
164
+ :line_width => options[:line_width] )
165
+ end
166
+
167
+ # Gets height of current font in PDF points at current font size
168
+ #
169
+ def height
170
+ @metrics.font_height(@size)
171
+ end
172
+
173
+ def ascender # :nodoc:
174
+ @metrics.ascender / 1000.0 * @size
175
+ end
176
+
177
+ def descender # :nodoc:
178
+ @metrics.descender / 1000.0 * @size
179
+ end
180
+
181
+ def normalize_encoding(text) # :nodoc:
182
+ # check the string is encoded sanely
183
+ # - UTF-8 for TTF fonts
184
+ # - ISO-8859-1 for Built-In fonts
185
+ if @metrics.type0?
186
+ normalize_ttf_encoding(text)
187
+ else
188
+ normalize_builtin_encoding(text)
189
+ end
190
+ end
191
+
192
+ def add_to_current_page #:nodoc:
193
+ @document.page_fonts.merge!(@identifier => @reference)
194
+ end
195
+
196
+ private
197
+
198
+ # built-in fonts only work with latin encoding, so translate the string
199
+ def normalize_builtin_encoding(text)
200
+ if text.respond_to?(:encode!)
201
+ text.encode!("ISO-8859-1")
202
+ else
203
+ require 'iconv'
204
+ text.replace Iconv.conv('ISO-8859-1//TRANSLIT', 'utf-8', text)
205
+ end
206
+ rescue
207
+ raise Prawn::Errors::IncompatibleStringEncoding, "When using a " +
208
+ "builtin font, only characters that exist in " +
209
+ "WinAnsi/ISO-8859-1 are allowed."
210
+ end
211
+
212
+ def normalize_ttf_encoding(text)
213
+ # TODO: if the current font is a built in one, we can't use the utf-8
214
+ # string provided by the user. We should convert it to WinAnsi or
215
+ # MacRoman or some such.
216
+ if text.respond_to?(:encode!)
217
+ # if we're running under a M17n aware VM, ensure the string provided is
218
+ # UTF-8 (by converting it if necessary)
219
+ begin
220
+ text.encode!("UTF-8")
221
+ rescue
222
+ raise Prawn::Errors::IncompatibleStringEncoding, "Encoding " +
223
+ "#{text.encoding} can not be transparently converted to UTF-8. " +
224
+ "Please ensure the encoding of the string you are attempting " +
225
+ "to use is set correctly"
226
+ end
227
+ else
228
+ # on a non M17N aware VM, use unpack as a hackish way to verify the
229
+ # string is valid utf-8. I thought it was better than loading iconv
230
+ # though.
231
+ begin
232
+ text.unpack("U*")
233
+ rescue
234
+ raise Prawn::Errors::IncompatibleStringEncoding, "The string you " +
235
+ "are attempting to render is not encoded in valid UTF-8."
236
+ end
237
+ end
238
+ end
239
+
240
+ def register_builtin(name)
241
+ unless BUILT_INS.include?(name)
242
+ raise Prawn::Errors::UnknownFont, "#{name} is not a known font."
243
+ end
244
+
245
+ @reference = @document.ref( :Type => :Font,
246
+ :Subtype => :Type1,
247
+ :BaseFont => name.to_sym,
248
+ :Encoding => :WinAnsiEncoding)
249
+ end
250
+
251
+ def embed_ttf(file)
252
+ unless File.file?(file)
253
+ raise ArgumentError, "file #{file} does not exist"
254
+ end
255
+
256
+ basename = @metrics.basename
257
+
258
+ raise "Can't detect a postscript name for #{file}" if basename.nil?
259
+
260
+ @encodings = @metrics.enc_table
261
+
262
+ if @encodings.nil?
263
+ raise "#{file} missing the required encoding table"
264
+ end
265
+
266
+ font_content = File.open(file,"rb") { |f| f.read }
267
+ compressed_font = Zlib::Deflate.deflate(font_content)
268
+
269
+ fontfile = @document.ref(:Length => compressed_font.size,
270
+ :Length1 => font_content.size,
271
+ :Filter => :FlateDecode )
272
+ fontfile << compressed_font
273
+
274
+ # TODO: Not sure what to do about CapHeight, as ttf2afm doesn't
275
+ # pick it up. Missing proper StemV and flags
276
+ #
277
+ descriptor = @document.ref(:Type => :FontDescriptor,
278
+ :FontName => basename,
279
+ :FontFile2 => fontfile,
280
+ :FontBBox => @metrics.bbox,
281
+ :Flags => 32, # FIXME: additional flags
282
+ :StemV => 0,
283
+ :ItalicAngle => 0,
284
+ :Ascent => @metrics.ascender,
285
+ :Descent => @metrics.descender )
286
+
287
+ descendant = @document.ref(:Type => :Font,
288
+ :Subtype => :CIDFontType2, # CID, TTF
289
+ :BaseFont => basename,
290
+ :CIDSystemInfo => { :Registry => "Adobe",
291
+ :Ordering => "Identity",
292
+ :Supplement => 0 },
293
+ :FontDescriptor => descriptor,
294
+ :W => @metrics.glyph_widths,
295
+ :CIDToGIDMap => :Identity )
296
+
297
+ to_unicode_content = @metrics.to_unicode_cmap.to_s
298
+ compressed_to_unicode = Zlib::Deflate.deflate(to_unicode_content)
299
+
300
+ to_unicode = @document.ref(:Length => compressed_to_unicode.size,
301
+ :Length1 => to_unicode_content.size,
302
+ :Filter => :FlateDecode )
303
+ to_unicode << compressed_to_unicode
304
+
305
+ @reference = @document.ref(:Type => :Font,
306
+ :Subtype => :Type0,
307
+ :BaseFont => basename,
308
+ :DescendantFonts => [descendant],
309
+ :Encoding => :"Identity-H",
310
+ :ToUnicode => to_unicode)
311
+
312
+ end
313
+
314
+ end
315
+
316
+ end
@@ -7,7 +7,7 @@
7
7
  # This is free software. Please see the LICENSE and COPYING files for details.
8
8
 
9
9
  module Prawn
10
- module Font #:nodoc:
10
+ class Font #:nodoc:
11
11
  class CMap #:nodoc:
12
12
 
13
13
  def initialize
@@ -10,27 +10,26 @@
10
10
  # This is free software. Please see the LICENSE and COPYING files for details.
11
11
 
12
12
  module Prawn
13
- module Font #:nodoc:
13
+ class Font #:nodoc:
14
14
  class Metrics #:nodoc:
15
15
 
16
16
  include Prawn::Font::Wrapping
17
17
 
18
18
  def self.[](font)
19
- data[font] ||= case(font)
20
- when /\.ttf$/
21
- TTF.new(font)
22
- else
23
- Adobe.new(font)
24
- end
19
+ data[font] ||= (font.match(/\.ttf$/) ? TTF : Adobe).new(font)
25
20
  end
26
21
 
27
22
  def self.data
28
23
  @data ||= {}
29
24
  end
30
25
 
31
- def string_height(string,options={})
26
+ def string_height(string,options={})
32
27
  string = naive_wrap(string, options[:line_width], options[:font_size])
33
28
  string.lines.to_a.length * font_height(options[:font_size])
29
+ end
30
+
31
+ def font_height(size)
32
+ (ascender - descender + line_gap) * size / 1000.0
34
33
  end
35
34
 
36
35
  class Adobe < Metrics #:nodoc:
@@ -85,13 +84,9 @@ module Prawn
85
84
  fontbbox.split(/\s+/).map { |e| Integer(e) }
86
85
  end
87
86
 
88
- def font_height(font_size)
89
- Float(bbox[3] - bbox[1]) * font_size / 1000.0
90
- end
91
-
92
87
  # calculates the width of the supplied string.
93
88
  # String *must* be encoded as iso-8859-1
94
- def string_width(string, font_size, options = {})
89
+ def string_width(string, font_size, options = {})
95
90
  scale = font_size / 1000.0
96
91
 
97
92
  if options[:kerning]
@@ -116,8 +111,8 @@ module Prawn
116
111
  def kern(string)
117
112
  kerned = string.unpack("C*").inject([]) do |a,r|
118
113
  if a.last.is_a? Array
119
- if kern = latin_kern_pairs_table[[a.last.last, r]]
120
- a << kern << [r]
114
+ if k = latin_kern_pairs_table[[a.last.last, r]]
115
+ a << k << [r]
121
116
  else
122
117
  a.last << r
123
118
  end
@@ -134,7 +129,7 @@ module Prawn
134
129
  }
135
130
  end
136
131
 
137
- def latin_kern_pairs_table
132
+ def latin_kern_pairs_table
138
133
  @kern_pairs_table ||= @kern_pairs.inject({}) do |h,p|
139
134
  h[p[0].map { |n| ISOLatin1Encoding.index(n) }] = p[1]
140
135
  h
@@ -153,16 +148,16 @@ module Prawn
153
148
 
154
149
  def descender
155
150
  @attributes["descender"].to_i
151
+ end
152
+
153
+ def line_gap
154
+ Float(bbox[3] - bbox[1]) - (ascender - descender)
156
155
  end
157
156
 
158
157
  # Hackish, but does the trick for now.
159
158
  def method_missing(method, *args, &block)
160
159
  name = method.to_s.delete("_")
161
- if @attributes.include? name
162
- @attributes[name]
163
- else
164
- super
165
- end
160
+ @attributes.include?(name) ? @attributes[name] : super
166
161
  end
167
162
 
168
163
  def metrics_path
@@ -170,10 +165,10 @@ module Prawn
170
165
  @metrics_path ||= m.split(':')
171
166
  else
172
167
  @metrics_path ||= [
173
- "/usr/lib/afm",
168
+ ".", "/usr/lib/afm",
174
169
  "/usr/local/lib/afm",
175
170
  "/usr/openwin/lib/fonts/afm/",
176
- Prawn::BASEDIR+'/data/fonts/','.']
171
+ Prawn::BASEDIR+'/data/fonts/']
177
172
  end
178
173
  end
179
174
 
@@ -188,7 +183,8 @@ module Prawn
188
183
  # perform any changes to the string that need to happen
189
184
  # before it is rendered to the canvas
190
185
  #
191
- # String *must* be encoded as iso-8859-1
186
+ # String *must* be encoded as iso-8859-1
187
+ #
192
188
  def convert_text(text, options={})
193
189
  options[:kerning] ? kern(text) : text
194
190
  end
@@ -243,13 +239,17 @@ module Prawn
243
239
  end
244
240
  end
245
241
 
246
- class TTF < Metrics #:nodoc:
242
+ class TTF < Metrics #:nodoc:
243
+
244
+ attr_accessor :ttf
247
245
 
248
246
  def initialize(font)
249
247
  @ttf = ::Font::TTF::File.open(font,"rb")
250
- @attributes = {}
251
- @glyph_widths = {}
252
- @bounding_boxes = {}
248
+ @attributes = {}
249
+ @glyph_widths = {}
250
+ @bounding_boxes = {}
251
+ @char_widths = {}
252
+ @has_kerning_data = !kern_pairs_table.empty?
253
253
  end
254
254
 
255
255
  def cmap
@@ -275,7 +275,9 @@ module Prawn
275
275
 
276
276
  # TODO: NASTY.
277
277
  def kern(string,options={})
278
- string.unpack("U*").inject([]) do |a,r|
278
+ a = []
279
+
280
+ string.unpack("U*").each do |r|
279
281
  if a.last.is_a? Array
280
282
  if kern = kern_pairs_table[[cmap[a.last.last], cmap[r]]]
281
283
  kern *= scale_factor
@@ -287,7 +289,9 @@ module Prawn
287
289
  a << [r]
288
290
  end
289
291
  a
290
- end.map { |r|
292
+ end
293
+
294
+ a.map { |r|
291
295
  if options[:skip_conversion]
292
296
  r.is_a?(Array) ? r.pack("U*") : r
293
297
  else
@@ -335,10 +339,10 @@ module Prawn
335
339
 
336
340
  def descender
337
341
  Integer(@ttf.get_table(:hhea).descender * scale_factor)
338
- end
339
-
340
- def font_height(size)
341
- (ascender - descender) * size / 1000.0
342
+ end
343
+
344
+ def line_gap
345
+ Integer(@ttf.get_table(:hhea).line_gap * scale_factor)
342
346
  end
343
347
 
344
348
  def basename
@@ -377,19 +381,18 @@ module Prawn
377
381
  s.is_a? ::Font::TTF::Table::Kern::KerningSubtable0 }
378
382
 
379
383
  if table
380
- @kern_pairs_table ||= table.kerning_pairs.inject({}) do |h,p|
381
- h[[p.left, p.right]] = p.value
382
- h
384
+ @kern_pairs_table = table.kerning_pairs.inject({}) do |h,p|
385
+ h[[p.left, p.right]] = p.value; h
383
386
  end
384
387
  else
385
388
  @kern_pairs_table = {}
386
- end
389
+ end
390
+ rescue ::Font::TTF::TableMissing
391
+ @kern_pairs_table = {}
387
392
  end
388
393
 
389
394
  def has_kerning_data?
390
- !kern_pairs_table.empty?
391
- rescue ::Font::TTF::TableMissing
392
- false
395
+ @has_kerning_data
393
396
  end
394
397
 
395
398
  def type0?
@@ -398,9 +401,9 @@ module Prawn
398
401
 
399
402
  def convert_text(text,options)
400
403
  text = text.chomp
401
- if options[:kerning]
402
- kern(text)
403
- else
404
+ if options[:kerning]
405
+ kern(text)
406
+ else
404
407
  unicode_codepoints = text.unpack("U*")
405
408
  glyph_codes = unicode_codepoints.map { |u|
406
409
  enc_table.get_glyph_id_for_unicode(u)
@@ -413,15 +416,15 @@ module Prawn
413
416
 
414
417
  def hmtx
415
418
  @hmtx ||= @ttf.get_table(:hmtx).metrics
416
- end
417
-
418
- def character_width_by_code(code)
419
+ end
420
+
421
+ def character_width_by_code(code)
419
422
  return 0 unless cmap[code]
420
- Integer(hmtx[cmap[code]][0] * scale_factor)
423
+ @char_widths[code] ||= Integer(hmtx[cmap[code]][0] * scale_factor)
421
424
  end
422
425
 
423
426
  def scale_factor
424
- @scale ||= 1 / Float(@ttf.get_table(:head).units_per_em / 1000.0)
427
+ @scale ||= 1000 * Float(@ttf.get_table(:head).units_per_em)**-1
425
428
  end
426
429
 
427
430
  end