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.
- 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
|