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