prawn 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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