prawn 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +16 -2
- data/Rakefile +3 -3
- data/data/images/arrow.png +0 -0
- data/data/images/arrow2.png +0 -0
- data/data/images/barcode_issue.png +0 -0
- data/data/images/ruport_type0.png +0 -0
- data/examples/cell.rb +14 -3
- data/examples/chinese_text_wrapping.rb +17 -0
- data/examples/family_based_styling.rb +21 -0
- data/examples/fancy_table.rb +4 -4
- data/examples/flowing_text_with_header_and_footer.rb +72 -0
- data/examples/font_size.rb +2 -2
- data/examples/lazy_bounding_boxes.rb +19 -0
- data/examples/table.rb +13 -11
- data/examples/text_flow.rb +1 -1
- data/lib/prawn.rb +44 -15
- data/lib/prawn/compatibility.rb +20 -7
- data/lib/prawn/document.rb +72 -122
- data/lib/prawn/document/bounding_box.rb +124 -24
- data/lib/prawn/document/internals.rb +107 -0
- data/lib/prawn/document/table.rb +99 -70
- data/lib/prawn/document/text.rb +92 -314
- data/lib/prawn/errors.rb +13 -2
- data/lib/prawn/font.rb +312 -1
- data/lib/prawn/font/cmap.rb +1 -1
- data/lib/prawn/font/metrics.rb +52 -49
- data/lib/prawn/font/wrapping.rb +14 -12
- data/lib/prawn/graphics.rb +23 -74
- data/lib/prawn/graphics/cell.rb +30 -25
- data/lib/prawn/graphics/color.rb +132 -0
- data/lib/prawn/images.rb +37 -16
- data/lib/prawn/images/png.rb +29 -24
- data/lib/prawn/pdf_object.rb +3 -1
- data/spec/bounding_box_spec.rb +12 -3
- data/spec/document_spec.rb +40 -72
- data/spec/font_spec.rb +97 -0
- data/spec/graphics_spec.rb +46 -99
- data/spec/images_spec.rb +4 -21
- data/spec/pdf_object_spec.rb +8 -8
- data/spec/png_spec.rb +47 -12
- data/spec/spec_helper.rb +5 -24
- data/spec/table_spec.rb +53 -59
- data/spec/text_spec.rb +28 -93
- data/vendor/pdf-inspector/README +18 -0
- data/vendor/pdf-inspector/lib/pdf/inspector.rb +25 -0
- data/vendor/pdf-inspector/lib/pdf/inspector/graphics.rb +80 -0
- data/vendor/pdf-inspector/lib/pdf/inspector/page.rb +16 -0
- data/vendor/pdf-inspector/lib/pdf/inspector/text.rb +31 -0
- data/vendor/pdf-inspector/lib/pdf/inspector/xobject.rb +19 -0
- metadata +63 -38
- data/examples/on_page_start.rb +0 -17
- data/examples/table_bench.rb +0 -92
- data/spec/box_calculation_spec.rb +0 -17
@@ -0,0 +1,107 @@
|
|
1
|
+
module Prawn
|
2
|
+
class Document
|
3
|
+
|
4
|
+
# This module exposes a few low-level PDF features for those who want
|
5
|
+
# to extend Prawn's core functionality. If you are not comfortable with
|
6
|
+
# low level PDF functionality as defined by Adobe's specification, chances
|
7
|
+
# are you won't need anything you find here.
|
8
|
+
#
|
9
|
+
module Internals
|
10
|
+
|
11
|
+
# Creates a new Prawn::Reference and adds it to the Document's object
|
12
|
+
# list. The +data+ argument is anything that Prawn::PdfObject() can convert.
|
13
|
+
def ref(data)
|
14
|
+
@objects.push(Prawn::Reference.new(@objects.size + 1, data)).last
|
15
|
+
end
|
16
|
+
|
17
|
+
# Appends a raw string to the current page content.
|
18
|
+
#
|
19
|
+
# # Raw line drawing example:
|
20
|
+
# x1,y1,x2,y2 = 100,500,300,550
|
21
|
+
# pdf.add_content("%.3f %.3f m" % [ x1, y1 ]) # move
|
22
|
+
# pdf.add_content("%.3f %.3f l" % [ x2, y2 ]) # draw path
|
23
|
+
# pdf.add_content("S") # stroke
|
24
|
+
#
|
25
|
+
def add_content(str)
|
26
|
+
@page_content << str << "\n"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Add a new type to the current pages ProcSet
|
30
|
+
#
|
31
|
+
def proc_set(*types)
|
32
|
+
@current_page.data[:ProcSet] ||= ref([])
|
33
|
+
@current_page.data[:ProcSet].data |= types
|
34
|
+
end
|
35
|
+
|
36
|
+
# The Resources dictionary for the current page
|
37
|
+
#
|
38
|
+
def page_resources
|
39
|
+
@current_page.data[:Resources] ||= {}
|
40
|
+
end
|
41
|
+
|
42
|
+
# The Font dictionary for the current page
|
43
|
+
#
|
44
|
+
def page_fonts
|
45
|
+
page_resources[:Font] ||= {}
|
46
|
+
end
|
47
|
+
|
48
|
+
# The XObject dictionary for the current page
|
49
|
+
def page_xobjects
|
50
|
+
page_resources[:XObject] ||= {}
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def finish_page_content
|
56
|
+
@header.draw if @header
|
57
|
+
@footer.draw if @footer
|
58
|
+
add_content "Q"
|
59
|
+
@page_content.compress_stream if compression_enabled?
|
60
|
+
@page_content.data[:Length] = @page_content.stream.size
|
61
|
+
end
|
62
|
+
|
63
|
+
# Write out the PDF Header, as per spec 3.4.1
|
64
|
+
def render_header(output)
|
65
|
+
# pdf version
|
66
|
+
output << "%PDF-1.3\n"
|
67
|
+
|
68
|
+
# 4 binary chars, as recommended by the spec
|
69
|
+
output << "\xFF\xFF\xFF\xFF\n"
|
70
|
+
end
|
71
|
+
|
72
|
+
# Write out the PDF Body, as per spec 3.4.2
|
73
|
+
def render_body(output)
|
74
|
+
@objects.each do |ref|
|
75
|
+
ref.offset = output.size
|
76
|
+
output << ref.object
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Write out the PDF Cross Reference Table, as per spec 3.4.3
|
81
|
+
def render_xref(output)
|
82
|
+
@xref_offset = output.size
|
83
|
+
output << "xref\n"
|
84
|
+
output << "0 #{@objects.size + 1}\n"
|
85
|
+
output << "0000000000 65535 f \n"
|
86
|
+
@objects.each do |ref|
|
87
|
+
output.printf("%010d", ref.offset)
|
88
|
+
output << " 00000 n \n"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Write out the PDF Body, as per spec 3.4.4
|
93
|
+
def render_trailer(output)
|
94
|
+
trailer_hash = {:Size => @objects.size + 1,
|
95
|
+
:Root => @root,
|
96
|
+
:Info => @info}
|
97
|
+
|
98
|
+
output << "trailer\n"
|
99
|
+
output << Prawn::PdfObject(trailer_hash) << "\n"
|
100
|
+
output << "startxref\n"
|
101
|
+
output << @xref_offset << "\n"
|
102
|
+
output << "%%EOF"
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/prawn/document/table.rb
CHANGED
@@ -7,8 +7,8 @@
|
|
7
7
|
# This is free software. Please see the LICENSE and COPYING files for details.
|
8
8
|
|
9
9
|
module Prawn
|
10
|
-
class Document
|
11
|
-
|
10
|
+
class Document
|
11
|
+
|
12
12
|
# Builds and renders a Document::Table object from raw data.
|
13
13
|
# For details on the options that can be passed, see
|
14
14
|
# Document::Table.new
|
@@ -37,7 +37,7 @@ module Prawn
|
|
37
37
|
#
|
38
38
|
# end
|
39
39
|
#
|
40
|
-
def table(data,options={})
|
40
|
+
def table(data,options={})
|
41
41
|
Prawn::Document::Table.new(data,self,options).draw
|
42
42
|
end
|
43
43
|
|
@@ -53,6 +53,7 @@ module Prawn
|
|
53
53
|
# * Automated page-breaking as needed
|
54
54
|
# * Column widths can be calculated automatically or defined explictly on a
|
55
55
|
# column by column basis
|
56
|
+
# * Text alignment can be set for the whole table or by column
|
56
57
|
#
|
57
58
|
# The current implementation is a bit barebones, but covers most of the
|
58
59
|
# basic needs for PDF table generation. If you have feature requests,
|
@@ -61,9 +62,13 @@ module Prawn
|
|
61
62
|
# Tables will be revisited before the end of the Ruby Mendicant project and
|
62
63
|
# the most commonly needed functionality will likely be added.
|
63
64
|
#
|
64
|
-
class Table
|
65
|
+
class Table
|
66
|
+
|
67
|
+
include Prawn::Configurable
|
65
68
|
|
66
|
-
attr_reader :col_widths # :nodoc:
|
69
|
+
attr_reader :col_widths # :nodoc:
|
70
|
+
|
71
|
+
NUMBER_PATTERN = /^-?(?:0|[1-9]\d*)(?:\.\d+(?:[eE][+-]?\d+)?)?$/ #:nodoc:
|
67
72
|
|
68
73
|
# Creates a new Document::Table object. This is generally called
|
69
74
|
# indirectly through Document#table but can also be used explictly.
|
@@ -79,12 +84,13 @@ module Prawn
|
|
79
84
|
# <tt>:horizontal_padding</tt>:: The horizontal cell padding in PDF points [5]
|
80
85
|
# <tt>:vertical_padding</tt>:: The vertical cell padding in PDF points [5]
|
81
86
|
# <tt>:padding</tt>:: Horizontal and vertical cell padding (overrides both)
|
82
|
-
# <tt>:
|
87
|
+
# <tt>:border_width</tt>:: With of border lines in PDF points [1]
|
83
88
|
# <tt>:border_style</tt>:: If set to :grid, fills in all borders. Otherwise, borders are drawn on columns only, not rows
|
84
89
|
# <tt>:position</tt>:: One of <tt>:left</tt>, <tt>:center</tt> or <tt>n</tt>, where <tt>n</tt> is an x-offset from the left edge of the current bounding box
|
85
90
|
# <tt>:widths:</tt> A hash of indices and widths in PDF points. E.g. <tt>{ 0 => 50, 1 => 100 }</tt>
|
86
91
|
# <tt>:row_colors</tt>:: An array of row background colors which are used cyclicly.
|
87
|
-
# <tt>:align</tt>:: Alignment of text in columns
|
92
|
+
# <tt>:align</tt>:: Alignment of text in columns, for entire table (<tt>:center</tt>) or by column (<tt>{ 0 => :left, 1 => :center}</tt>)
|
93
|
+
# <tt>:align_headers</tt>:: Alignment of header text.
|
88
94
|
#
|
89
95
|
# Row colors are specified as html encoded values, e.g.
|
90
96
|
# ["ffffff","aaaaaa","ccaaff"]. You can also specify
|
@@ -94,31 +100,37 @@ module Prawn
|
|
94
100
|
# See Document#table for typical usage, as directly using this class is
|
95
101
|
# not recommended unless you know why you want to do it.
|
96
102
|
#
|
97
|
-
def initialize(data, document,options={})
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
@border_style = options[:border_style]
|
102
|
-
@border = options[:border] || 1
|
103
|
-
@position = options[:position] || :left
|
104
|
-
@headers = options[:headers]
|
105
|
-
@row_colors = options[:row_colors]
|
106
|
-
@align = options[:align]
|
107
|
-
|
108
|
-
@horizontal_padding = options[:horizontal_padding] || 5
|
109
|
-
@vertical_padding = options[:vertical_padding] || 5
|
110
|
-
|
111
|
-
if options[:padding]
|
112
|
-
@horizontal_padding = @vertical_padding = options[:padding]
|
103
|
+
def initialize(data, document,options={})
|
104
|
+
unless data.all? { |e| Array === e }
|
105
|
+
raise Prawn::Errors::InvalidTableData,
|
106
|
+
"data must be a two dimensional array of Prawn::Cells or strings"
|
113
107
|
end
|
114
|
-
|
115
108
|
|
116
|
-
@
|
109
|
+
@data = data
|
110
|
+
@document = document
|
111
|
+
|
112
|
+
Prawn.verify_options [:font_size,:border_style, :border_width,
|
113
|
+
:position, :headers, :row_colors, :align, :align_headers,
|
114
|
+
:horizontal_padding, :vertical_padding, :padding, :widths ], options
|
115
|
+
|
116
|
+
configuration.update(options)
|
117
117
|
|
118
|
-
|
118
|
+
if padding = options[:padding]
|
119
|
+
C(:horizontal_padding => padding, :vertical_padding => padding)
|
120
|
+
end
|
121
|
+
|
122
|
+
if options[:row_colors] == :pdf_writer
|
123
|
+
C(:row_colors => ["ffffff","cccccc"])
|
124
|
+
end
|
119
125
|
|
126
|
+
if options[:row_colors]
|
127
|
+
C(:original_row_colors => C(:row_colors))
|
128
|
+
end
|
129
|
+
|
120
130
|
calculate_column_widths(options[:widths])
|
121
|
-
end
|
131
|
+
end
|
132
|
+
|
133
|
+
attr_reader :col_widths #:nodoc:
|
122
134
|
|
123
135
|
# Width of the table in PDF points
|
124
136
|
#
|
@@ -128,85 +140,98 @@ module Prawn
|
|
128
140
|
|
129
141
|
# Draws the table onto the PDF document
|
130
142
|
#
|
131
|
-
def draw
|
132
|
-
|
143
|
+
def draw
|
144
|
+
@parent_bounds = @document.bounds
|
145
|
+
case C(:position)
|
133
146
|
when :center
|
134
147
|
x = (@document.bounds.width - width) / 2.0
|
135
|
-
|
136
|
-
@document.bounding_box [x,
|
148
|
+
dy = @document.bounds.absolute_top - @document.y
|
149
|
+
@document.bounding_box [x, @parent_bounds.top], :width => width do
|
150
|
+
@document.move_down(dy)
|
137
151
|
generate_table
|
138
152
|
end
|
139
153
|
when Numeric
|
140
|
-
x = @
|
141
|
-
|
142
|
-
@document.bounding_box [x,y], :width => width do
|
143
|
-
generate_table
|
144
|
-
end
|
154
|
+
x, y = C(:position), @document.y - @document.bounds.absolute_bottom
|
155
|
+
@document.bounding_box([x,y], :width => width) { generate_table }
|
145
156
|
else
|
146
157
|
generate_table
|
147
158
|
end
|
148
159
|
end
|
149
160
|
|
150
161
|
private
|
162
|
+
|
163
|
+
def default_configuration
|
164
|
+
{ :font_size => 12,
|
165
|
+
:border_width => 1,
|
166
|
+
:position => :left,
|
167
|
+
:horizontal_padding => 5,
|
168
|
+
:vertical_padding => 5 }
|
169
|
+
end
|
151
170
|
|
152
171
|
def calculate_column_widths(manual_widths=nil)
|
153
172
|
@col_widths = [0] * @data[0].length
|
154
173
|
renderable_data.each do |row|
|
155
174
|
row.each_with_index do |cell,i|
|
156
175
|
length = cell.to_s.lines.map { |e|
|
157
|
-
@document.
|
158
|
-
2
|
159
|
-
@col_widths[i] = length if length > @col_widths[i]
|
176
|
+
@document.font.metrics.string_width(e,C(:font_size)) }.max.to_f +
|
177
|
+
2*C(:horizontal_padding)
|
178
|
+
@col_widths[i] = length.ceil if length > @col_widths[i]
|
160
179
|
end
|
161
180
|
end
|
162
181
|
|
163
|
-
# TODO: Could optimize here
|
164
182
|
manual_widths.each { |k,v| @col_widths[k] = v } if manual_widths
|
165
183
|
end
|
166
184
|
|
167
185
|
def renderable_data
|
168
|
-
|
169
|
-
[@headers] + @data
|
170
|
-
else
|
171
|
-
@data
|
172
|
-
end
|
186
|
+
C(:headers) ? [C(:headers)] + @data : @data
|
173
187
|
end
|
174
188
|
|
175
|
-
def generate_table
|
189
|
+
def generate_table
|
176
190
|
page_contents = []
|
177
|
-
y_pos = @document.y
|
191
|
+
y_pos = @document.y
|
178
192
|
|
179
|
-
@document.
|
193
|
+
@document.font.size C(:font_size) do
|
180
194
|
renderable_data.each_with_index do |row,index|
|
181
195
|
c = Prawn::Graphics::CellBlock.new(@document)
|
182
|
-
row.each_with_index do |e,i|
|
183
|
-
case(
|
196
|
+
row.each_with_index do |e,i|
|
197
|
+
case C(:align)
|
198
|
+
when Hash
|
199
|
+
align = C(:align)[i]
|
200
|
+
else
|
201
|
+
align = C(:align)
|
202
|
+
end
|
203
|
+
|
204
|
+
|
205
|
+
align ||= e.to_s =~ NUMBER_PATTERN ? :right : :left
|
206
|
+
|
207
|
+
case e
|
184
208
|
when Prawn::Graphics::Cell
|
185
209
|
e.document = @document
|
186
210
|
e.width = @col_widths[i]
|
187
|
-
e.horizontal_padding =
|
188
|
-
e.vertical_padding =
|
189
|
-
e.
|
211
|
+
e.horizontal_padding = C(:horizontal_padding)
|
212
|
+
e.vertical_padding = C(:vertical_padding)
|
213
|
+
e.border_width = C(:border_width)
|
190
214
|
e.border_style = :sides
|
191
|
-
e.align =
|
215
|
+
e.align = align
|
192
216
|
c << e
|
193
217
|
else
|
194
218
|
c << Prawn::Graphics::Cell.new(
|
195
219
|
:document => @document,
|
196
220
|
:text => e.to_s,
|
197
221
|
:width => @col_widths[i],
|
198
|
-
:horizontal_padding =>
|
199
|
-
:vertical_padding
|
200
|
-
:
|
201
|
-
:border_style
|
202
|
-
:align
|
222
|
+
:horizontal_padding => C(:horizontal_padding),
|
223
|
+
:vertical_padding => C(:vertical_padding),
|
224
|
+
:border_width => C(:border_width),
|
225
|
+
:border_style => :sides,
|
226
|
+
:align => align )
|
203
227
|
end
|
204
228
|
end
|
205
|
-
|
206
|
-
|
229
|
+
|
230
|
+
bbox = @parent_bounds.stretchy? ? @document.margin_box : @parent_bounds
|
231
|
+
if c.height > y_pos - bbox.absolute_bottom
|
207
232
|
draw_page(page_contents)
|
208
233
|
@document.start_new_page
|
209
|
-
if
|
234
|
+
if C(:headers)
|
210
235
|
page_contents = [page_contents[0]]
|
211
236
|
y_pos = @document.y - page_contents[0].height
|
212
237
|
else
|
@@ -224,22 +249,26 @@ module Prawn
|
|
224
249
|
end
|
225
250
|
|
226
251
|
end
|
227
|
-
@document.y -= @vertical_padding
|
228
252
|
end
|
229
253
|
end
|
230
254
|
|
231
255
|
def draw_page(contents)
|
232
256
|
return if contents.empty?
|
233
257
|
|
234
|
-
if
|
258
|
+
if C(:border_style) == :grid || contents.length == 1
|
235
259
|
contents.each { |e| e.border_style = :all }
|
236
|
-
else
|
237
|
-
|
260
|
+
else
|
261
|
+
if C(:headers)
|
262
|
+
contents.first.border_style = :all
|
263
|
+
contents.first.align = C(:align_headers) || :left
|
264
|
+
else
|
265
|
+
contents.first.border_style = :no_bottom
|
266
|
+
end
|
238
267
|
contents.last.border_style = :no_top
|
239
268
|
end
|
240
269
|
|
241
270
|
contents.each do |x|
|
242
|
-
x.background_color = next_row_color if
|
271
|
+
x.background_color = next_row_color if C(:row_colors)
|
243
272
|
x.draw
|
244
273
|
end
|
245
274
|
|
@@ -247,11 +276,11 @@ module Prawn
|
|
247
276
|
end
|
248
277
|
|
249
278
|
def next_row_color
|
250
|
-
|
279
|
+
C(:row_colors).unshift(C(:row_colors).pop).last
|
251
280
|
end
|
252
281
|
|
253
|
-
def reset_row_colors
|
254
|
-
|
282
|
+
def reset_row_colors
|
283
|
+
C(:row_colors => C(:original_row_colors).dup) if C(:row_colors)
|
255
284
|
end
|
256
285
|
|
257
286
|
end
|
data/lib/prawn/document/text.rb
CHANGED
@@ -10,34 +10,34 @@ require "zlib"
|
|
10
10
|
module Prawn
|
11
11
|
class Document
|
12
12
|
module Text
|
13
|
-
|
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>
|
13
|
+
# Draws text on the page. If a point is specified via the +:at+
|
22
14
|
# option the text will begin exactly at that point, and the string is
|
23
15
|
# assumed to be pre-formatted to properly fit the page.
|
16
|
+
#
|
17
|
+
# pdf.text "Hello World", :at => [100,100]
|
18
|
+
# pdf.text "Goodbye World", :at => [50,50], :size => 16
|
24
19
|
#
|
25
|
-
# When
|
26
|
-
# fit within your current bounding box (or
|
20
|
+
# When +:at+ is not specified, Prawn attempts to wrap the text to
|
21
|
+
# fit within your current bounding box (or margin_box if no bounding box
|
27
22
|
# is being used ). Text will flow onto the next page when it reaches
|
28
|
-
# the bottom of the
|
23
|
+
# the bottom of the bounding box. Text wrap in Prawn does not re-flow
|
29
24
|
# linebreaks, so if you want fully automated text wrapping, be sure to
|
30
25
|
# remove newlines before attempting to draw your string.
|
31
26
|
#
|
32
|
-
# pdf.text "Hello World", :at => [100,100]
|
33
|
-
# pdf.text "Goodbye World", :at => [50,50], :size => 16
|
34
27
|
# pdf.text "Will be wrapped when it hits the edge of your bounding box"
|
28
|
+
# pdf.text "This will be centered", :align => :center
|
29
|
+
# pdf.text "This will be right aligned", :align => :right
|
30
|
+
#
|
31
|
+
# Wrapping is done by splitting words by spaces by default. If your text
|
32
|
+
# does not contain spaces, you can wrap based on characters instead:
|
33
|
+
#
|
34
|
+
# pdf.text "This will be wrapped by character", :wrap => :character
|
35
35
|
#
|
36
36
|
# If your font contains kerning pairs data that Prawn can parse, the
|
37
37
|
# text will be kerned by default. You can disable this feature by passing
|
38
38
|
# <tt>:kerning => false</tt>.
|
39
39
|
#
|
40
|
-
#
|
40
|
+
# === Character Encoding Details:
|
41
41
|
#
|
42
42
|
# Note that strings passed to this function should be encoded as UTF-8.
|
43
43
|
# If you get unexpected characters appearing in your rendered document,
|
@@ -45,162 +45,82 @@ module Prawn
|
|
45
45
|
#
|
46
46
|
# If the current font is a built-in one, although the string must be
|
47
47
|
# encoded as UTF-8, only characters that are available in ISO-8859-1
|
48
|
-
# are allowed.
|
48
|
+
# are allowed (transliteration will be attempted).
|
49
49
|
#
|
50
50
|
# If an empty box is rendered to your PDF instead of the character you
|
51
51
|
# wanted it usually means the current font doesn't include that character.
|
52
52
|
#
|
53
|
-
def text(text,options={})
|
54
|
-
# ensure a valid font is selected
|
55
|
-
font "Helvetica" unless fonts[@font]
|
56
|
-
|
53
|
+
def text(text,options={})
|
57
54
|
# we'll be messing with the strings encoding, don't change the users
|
58
55
|
# original string
|
59
|
-
text = text.dup
|
60
|
-
|
61
|
-
# check the string is encoded sanely
|
62
|
-
# - UTF-8 for TTF fonts
|
63
|
-
# - ISO-8859-1 for Built-In fonts
|
64
|
-
if using_builtin_font?
|
65
|
-
normalize_builtin_encoding(text)
|
66
|
-
else
|
67
|
-
normalize_ttf_encoding(text)
|
68
|
-
end
|
69
|
-
|
70
|
-
if options.key?(:kerning)
|
71
|
-
options[:kerning] = false unless font_metrics.has_kerning_data?
|
72
|
-
else
|
73
|
-
options[:kerning] = true if font_metrics.has_kerning_data?
|
74
|
-
end
|
75
|
-
|
76
|
-
return wrapped_text(text,options) unless options[:at]
|
56
|
+
text = text.to_s.dup
|
77
57
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
58
|
+
# we might also mess with the font
|
59
|
+
original_font = font.name
|
60
|
+
|
61
|
+
options = text_options.merge(options)
|
62
|
+
process_text_options(options)
|
63
|
+
|
64
|
+
font.normalize_encoding(text) unless @skip_encoding
|
65
|
+
|
66
|
+
if options[:at]
|
67
|
+
x,y = translate(options[:at])
|
68
|
+
font.size(options[:size]) { add_text_content(text,x,y,options) }
|
69
|
+
else
|
70
|
+
wrapped_text(text,options)
|
71
|
+
end
|
83
72
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
}
|
96
|
-
end
|
97
|
-
end
|
73
|
+
font(original_font)
|
74
|
+
end
|
75
|
+
|
76
|
+
# A hash of configuration options, to be used globally by text().
|
77
|
+
#
|
78
|
+
# pdf.text_options.update(:size => 16, :align => :right)
|
79
|
+
# pdf.text "Hello World" #=> Size 16 w. right alignment
|
80
|
+
#
|
81
|
+
def text_options
|
82
|
+
@text_options ||= {}
|
83
|
+
end
|
98
84
|
|
99
|
-
|
100
|
-
# who require direct access to font attributes, and can be safely ignored
|
101
|
-
# otherwise.
|
102
|
-
#
|
103
|
-
def font_metrics
|
104
|
-
@font_metrics ||= Prawn::Font::Metrics["Helvetica"]
|
105
|
-
end
|
106
|
-
|
107
|
-
# Sets the current font.
|
108
|
-
#
|
109
|
-
# The single parameter must be a string. It can be one of the 14 built-in
|
110
|
-
# fonts supported by PDF, or the location of a TTF file. The BUILT_INS
|
111
|
-
# array specifies the valid built in font values.
|
112
|
-
#
|
113
|
-
# pdf.font "Times-Roman"
|
114
|
-
# pdf.font "Chalkboard.ttf"
|
115
|
-
#
|
116
|
-
# If a ttf font is specified, the full file will be embedded in the
|
117
|
-
# rendered PDF. This should be your preferred option in most cases.
|
118
|
-
# It will increase the size of the resulting file, but also make it
|
119
|
-
# more portable.
|
120
|
-
#
|
121
|
-
def font(name)
|
122
|
-
proc_set :PDF, :Text
|
123
|
-
@font_metrics = Prawn::Font::Metrics[name]
|
124
|
-
case(name)
|
125
|
-
when /\.ttf$/
|
126
|
-
@font = embed_ttf_font(name)
|
127
|
-
else
|
128
|
-
@font = register_builtin_font(name)
|
129
|
-
end
|
130
|
-
set_current_font
|
131
|
-
end
|
132
|
-
|
133
|
-
# Sets the default font size for use within a block. Individual overrides
|
134
|
-
# can be used as desired. The previous font size will be restored after the
|
135
|
-
# block.
|
136
|
-
#
|
137
|
-
# Prawn::Document.generate("font_size.pdf") do
|
138
|
-
# font_size!(16)
|
139
|
-
# text "At size 16"
|
140
|
-
#
|
141
|
-
# font_size(10) do
|
142
|
-
# text "At size 10"
|
143
|
-
# text "At size 6", :size => 6
|
144
|
-
# text "At size 10"
|
145
|
-
# end
|
146
|
-
#
|
147
|
-
# text "At size 16"
|
148
|
-
# end
|
149
|
-
#
|
150
|
-
# When called without an argument, this method returns the current font
|
151
|
-
# size.
|
152
|
-
#
|
153
|
-
def font_size(size=nil)
|
154
|
-
return current_font_size unless size
|
155
|
-
font_size_before_block = @font_size || DEFAULT_FONT_SIZE
|
156
|
-
font_size!(size)
|
157
|
-
yield
|
158
|
-
font_size!(font_size_before_block)
|
159
|
-
end
|
160
|
-
|
161
|
-
# Sets the default font size. See example in font_size
|
162
|
-
#
|
163
|
-
def font_size!(size)
|
164
|
-
@font_size = size unless size == nil
|
165
|
-
end
|
85
|
+
private
|
166
86
|
|
167
|
-
|
87
|
+
def process_text_options(options)
|
88
|
+
Prawn.verify_options [:style, :kerning, :size, :at, :wrap,
|
89
|
+
:spacing, :align ], options
|
90
|
+
|
91
|
+
if options[:style]
|
92
|
+
raise "Bad font family" unless font.family
|
93
|
+
font(font.family,:style => options[:style])
|
94
|
+
end
|
168
95
|
|
169
|
-
|
96
|
+
unless options.key?(:kerning)
|
97
|
+
options[:kerning] = font.metrics.has_kerning_data?
|
98
|
+
end
|
170
99
|
|
100
|
+
options[:size] ||= font.size
|
101
|
+
end
|
171
102
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
103
|
+
def move_text_position(dy)
|
104
|
+
bottom = @bounding_box.stretchy? ? @margin_box.absolute_bottom :
|
105
|
+
@bounding_box.absolute_bottom
|
106
|
+
start_new_page if (y - dy) < bottom
|
107
|
+
|
108
|
+
self.y -= dy
|
176
109
|
end
|
177
110
|
|
178
|
-
def move_text_position(dy)
|
179
|
-
(y - dy) < @margin_box.absolute_bottom ? start_new_page : self.y -= dy
|
180
|
-
end
|
181
|
-
|
182
|
-
def text_width(text,size)
|
183
|
-
@font_metrics.string_width(text,size)
|
184
|
-
end
|
185
|
-
|
186
|
-
# TODO: Get kerning working with wrapped text
|
187
111
|
def wrapped_text(text,options)
|
188
|
-
options[:align] ||= :left
|
189
|
-
font_size(options[:size] || current_font_size) do
|
190
|
-
font_name = font_registry[fonts[@font]]
|
112
|
+
options[:align] ||= :left
|
191
113
|
|
192
|
-
|
193
|
-
|
114
|
+
font.size(options[:size]) do
|
115
|
+
text = font.metrics.naive_wrap(text, bounds.right, font.size,
|
116
|
+
:kerning => options[:kerning], :mode => options[:wrap])
|
194
117
|
|
195
118
|
lines = text.lines
|
196
|
-
|
197
|
-
lines.each do |e|
|
198
|
-
|
199
|
-
move_text_position(@font_metrics.font_height(current_font_size) +
|
200
|
-
@font_metrics.descender / 1000.0 * current_font_size)
|
201
|
-
|
119
|
+
|
120
|
+
lines.each do |e|
|
121
|
+
move_text_position( font.height + font.descender )
|
202
122
|
|
203
|
-
line_width =
|
123
|
+
line_width = font.width_of(e)
|
204
124
|
case(options[:align])
|
205
125
|
when :left
|
206
126
|
x = @bounding_box.absolute_left
|
@@ -208,173 +128,31 @@ module Prawn
|
|
208
128
|
x = @bounding_box.absolute_left +
|
209
129
|
(@bounding_box.width - line_width) / 2.0
|
210
130
|
when :right
|
211
|
-
x = @bounding_box.absolute_right - line_width
|
131
|
+
x = @bounding_box.absolute_right - line_width
|
212
132
|
end
|
213
133
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
#{x} #{y} Td
|
218
|
-
}
|
219
|
-
|
220
|
-
add_content Prawn::PdfObject(@font_metrics.convert_text(e,options), true) <<
|
221
|
-
" #{options[:kerning] ? 'TJ' : 'Tj'}\n"
|
222
|
-
|
223
|
-
add_content %Q{
|
224
|
-
ET
|
225
|
-
}
|
226
|
-
|
227
|
-
ds = -@font_metrics.descender / 1000.0 * current_font_size
|
228
|
-
move_text_position(options[:spacing] || ds )
|
229
|
-
end
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
def embed_ttf_font(file) #:nodoc:
|
234
|
-
|
235
|
-
ttf_metrics = Prawn::Font::Metrics::TTF.new(file)
|
236
|
-
|
237
|
-
unless File.file?(file)
|
238
|
-
raise ArgumentError, "file #{file} does not exist"
|
239
|
-
end
|
240
|
-
|
241
|
-
basename = @font_metrics.basename
|
242
|
-
|
243
|
-
raise "Can't detect a postscript name for #{file}" if basename.nil?
|
244
|
-
|
245
|
-
enctables[basename] = @font_metrics.enc_table
|
246
|
-
|
247
|
-
if enctables[basename].nil?
|
248
|
-
raise "#{file} missing the required encoding table"
|
249
|
-
end
|
250
|
-
|
251
|
-
font_content = File.open(file,"rb") { |f| f.read }
|
252
|
-
compressed_font = Zlib::Deflate.deflate(font_content)
|
253
|
-
|
254
|
-
fontfile = ref(:Length => compressed_font.size,
|
255
|
-
:Length1 => font_content.size,
|
256
|
-
:Filter => :FlateDecode )
|
257
|
-
fontfile << compressed_font
|
258
|
-
|
259
|
-
# TODO: Not sure what to do about CapHeight, as ttf2afm doesn't
|
260
|
-
# pick it up. Missing proper StemV and flags
|
261
|
-
#
|
262
|
-
descriptor = ref(:Type => :FontDescriptor,
|
263
|
-
:FontName => basename,
|
264
|
-
:FontFile2 => fontfile,
|
265
|
-
:FontBBox => @font_metrics.bbox,
|
266
|
-
:Flags => 32, # FIXME: additional flags
|
267
|
-
:StemV => 0,
|
268
|
-
:ItalicAngle => 0,
|
269
|
-
:Ascent => @font_metrics.ascender,
|
270
|
-
:Descent => @font_metrics.descender
|
271
|
-
)
|
272
|
-
|
273
|
-
descendant = ref(:Type => :Font,
|
274
|
-
:Subtype => :CIDFontType2, # CID, TTF
|
275
|
-
:BaseFont => basename,
|
276
|
-
:CIDSystemInfo => { :Registry => "Adobe",
|
277
|
-
:Ordering => "Identity",
|
278
|
-
:Supplement => 0 },
|
279
|
-
:FontDescriptor => descriptor,
|
280
|
-
:W => @font_metrics.glyph_widths,
|
281
|
-
:CIDToGIDMap => :Identity
|
282
|
-
)
|
283
|
-
|
284
|
-
to_unicode_content = @font_metrics.to_unicode_cmap.to_s
|
285
|
-
compressed_to_unicode = Zlib::Deflate.deflate(to_unicode_content)
|
286
|
-
to_unicode = ref(:Length => compressed_to_unicode.size,
|
287
|
-
:Length1 => to_unicode_content.size,
|
288
|
-
:Filter => :FlateDecode )
|
289
|
-
to_unicode << compressed_to_unicode
|
290
|
-
|
291
|
-
# TODO: Needs ToUnicode (at least)
|
292
|
-
fonts[basename] ||= ref(:Type => :Font,
|
293
|
-
:Subtype => :Type0,
|
294
|
-
:BaseFont => basename,
|
295
|
-
:DescendantFonts => [descendant],
|
296
|
-
:Encoding => :"Identity-H",
|
297
|
-
:ToUnicode => to_unicode)
|
298
|
-
return basename
|
299
|
-
end
|
300
|
-
|
301
|
-
# built-in fonts only work with latin encoding, so translate the string
|
302
|
-
def normalize_builtin_encoding(text)
|
303
|
-
if text.respond_to?(:encode!)
|
304
|
-
text.encode!("ISO-8859-1")
|
305
|
-
else
|
306
|
-
require 'iconv'
|
307
|
-
text.replace Iconv.conv('ISO-8859-1', 'utf-8', text)
|
308
|
-
end
|
309
|
-
rescue
|
310
|
-
raise Prawn::Errors::IncompatibleStringEncoding, "When using a " +
|
311
|
-
"builtin font, only characters that exist in " +
|
312
|
-
"WinAnsi/ISO-8859-1 are allowed."
|
313
|
-
end
|
314
|
-
|
315
|
-
def normalize_ttf_encoding(text)
|
316
|
-
# TODO: if the current font is a built in one, we can't use the utf-8
|
317
|
-
# string provided by the user. We should convert it to WinAnsi or
|
318
|
-
# MacRoman or some such.
|
319
|
-
if text.respond_to?(:encode!)
|
320
|
-
# if we're running under a M17n aware VM, ensure the string provided is
|
321
|
-
# UTF-8 (by converting it if necessary)
|
322
|
-
begin
|
323
|
-
text.encode!("UTF-8")
|
324
|
-
rescue
|
325
|
-
raise Prawn::Errors::IncompatibleStringEncoding, "Encoding " +
|
326
|
-
"#{text.encoding} can not be transparently converted to UTF-8. " +
|
327
|
-
"Please ensure the encoding of the string you are attempting " +
|
328
|
-
"to use is set correctly"
|
329
|
-
end
|
330
|
-
else
|
331
|
-
# on a non M17N aware VM, use unpack as a hackish way to verify the
|
332
|
-
# string is valid utf-8. I thought it was better than loading iconv
|
333
|
-
# though.
|
334
|
-
begin
|
335
|
-
text.unpack("U*")
|
336
|
-
rescue
|
337
|
-
raise Prawn::Errors::IncompatibleStringEncoding, "The string you " +
|
338
|
-
"are attempting to render is not encoded in valid UTF-8."
|
339
|
-
end
|
340
|
-
end
|
341
|
-
end
|
342
|
-
|
343
|
-
def register_builtin_font(name) #:nodoc:
|
344
|
-
unless BUILT_INS.include?(name)
|
345
|
-
raise Prawn::Errors::UnknownFont, "#{name} is not a known font."
|
134
|
+
add_text_content(e,x,y,options)
|
135
|
+
move_text_position(options[:spacing] || -font.descender )
|
136
|
+
end
|
346
137
|
end
|
347
|
-
|
348
|
-
:Subtype => :Type1,
|
349
|
-
:BaseFont => name.to_sym,
|
350
|
-
:Encoding => :WinAnsiEncoding)
|
351
|
-
return name
|
352
|
-
end
|
353
|
-
|
354
|
-
def set_current_font #:nodoc:
|
355
|
-
return if @font.nil?
|
356
|
-
font_registry[fonts[@font]] ||= :"F#{font_registry.size + 1}"
|
357
|
-
|
358
|
-
page_fonts.merge!(
|
359
|
-
font_registry[fonts[@font]] => fonts[@font]
|
360
|
-
)
|
361
|
-
end
|
362
|
-
|
363
|
-
def enctables #:nodoc
|
364
|
-
@enctables ||= {}
|
365
|
-
end
|
138
|
+
end
|
366
139
|
|
367
|
-
def
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
140
|
+
def add_text_content(text, x, y, options)
|
141
|
+
text = font.metrics.convert_text(text,options)
|
142
|
+
|
143
|
+
add_content %Q{
|
144
|
+
BT
|
145
|
+
/#{font.identifier} #{font.size} Tf
|
146
|
+
#{x} #{y} Td
|
147
|
+
}
|
148
|
+
|
149
|
+
add_content Prawn::PdfObject(text, true) <<
|
150
|
+
" #{options[:kerning] ? 'TJ' : 'Tj'}\n"
|
151
|
+
|
152
|
+
add_content %Q{
|
153
|
+
ET
|
154
|
+
}
|
155
|
+
end
|
378
156
|
end
|
379
157
|
end
|
380
158
|
end
|