prawn-git 2.0.1
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.
- checksums.yaml +7 -0
- data/.yardopts +10 -0
- data/COPYING +2 -0
- data/GPLv2 +340 -0
- data/GPLv3 +674 -0
- data/Gemfile +11 -0
- data/LICENSE +56 -0
- data/Rakefile +55 -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/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/images/16bit.alpha +0 -0
- data/data/images/16bit.color +0 -0
- data/data/images/16bit.png +0 -0
- data/data/images/arrow.png +0 -0
- data/data/images/arrow2.png +0 -0
- data/data/images/dice.alpha +0 -0
- data/data/images/dice.color +0 -0
- data/data/images/dice.png +0 -0
- data/data/images/dice_interlaced.png +0 -0
- data/data/images/fractal.jpg +0 -0
- data/data/images/indexed_color.dat +0 -0
- data/data/images/indexed_color.png +0 -0
- data/data/images/letterhead.jpg +0 -0
- data/data/images/license.md +8 -0
- data/data/images/page_white_text.alpha +0 -0
- data/data/images/page_white_text.color +0 -0
- data/data/images/page_white_text.png +0 -0
- data/data/images/pal_bk.png +0 -0
- data/data/images/pigs.jpg +0 -0
- data/data/images/prawn.png +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/ruport_type0.png +0 -0
- data/data/images/stef.jpg +0 -0
- data/data/images/tru256.bmp +0 -0
- data/data/images/web-links.dat +1 -0
- data/data/images/web-links.png +0 -0
- data/data/pdfs/complex_template.pdf +0 -0
- data/data/pdfs/contains_ttf_font.pdf +0 -0
- data/data/pdfs/encrypted.pdf +0 -0
- data/data/pdfs/form.pdf +820 -0
- data/data/pdfs/hexagon.pdf +61 -0
- data/data/pdfs/indirect_reference.pdf +86 -0
- data/data/pdfs/multipage_template.pdf +127 -0
- data/data/pdfs/nested_pages.pdf +118 -0
- data/data/pdfs/page_without_mediabox.pdf +193 -0
- data/data/pdfs/resources_as_indirect_object.pdf +83 -0
- data/data/pdfs/two_hexagons.pdf +90 -0
- data/data/pdfs/version_1_6.pdf +61 -0
- data/data/shift_jis_text.txt +1 -0
- data/lib/prawn.rb +89 -0
- data/lib/prawn/document.rb +706 -0
- data/lib/prawn/document/bounding_box.rb +539 -0
- data/lib/prawn/document/column_box.rb +144 -0
- data/lib/prawn/document/internals.rb +58 -0
- data/lib/prawn/document/span.rb +57 -0
- data/lib/prawn/encoding.rb +87 -0
- data/lib/prawn/errors.rb +80 -0
- data/lib/prawn/font.rb +413 -0
- data/lib/prawn/font/afm.rb +256 -0
- data/lib/prawn/font/dfont.rb +43 -0
- data/lib/prawn/font/ttf.rb +355 -0
- data/lib/prawn/font_metric_cache.rb +46 -0
- data/lib/prawn/graphics.rb +646 -0
- data/lib/prawn/graphics/cap_style.rb +47 -0
- data/lib/prawn/graphics/color.rb +232 -0
- data/lib/prawn/graphics/dash.rb +109 -0
- data/lib/prawn/graphics/join_style.rb +49 -0
- data/lib/prawn/graphics/patterns.rb +126 -0
- data/lib/prawn/graphics/transformation.rb +157 -0
- data/lib/prawn/graphics/transparency.rb +101 -0
- data/lib/prawn/grid.rb +279 -0
- data/lib/prawn/image_handler.rb +44 -0
- data/lib/prawn/images.rb +199 -0
- data/lib/prawn/images/image.rb +49 -0
- data/lib/prawn/images/jpg.rb +91 -0
- data/lib/prawn/images/png.rb +290 -0
- data/lib/prawn/measurement_extensions.rb +50 -0
- data/lib/prawn/measurements.rb +77 -0
- data/lib/prawn/outline.rb +289 -0
- data/lib/prawn/repeater.rb +124 -0
- data/lib/prawn/security.rb +288 -0
- data/lib/prawn/security/arcfour.rb +54 -0
- data/lib/prawn/soft_mask.rb +94 -0
- data/lib/prawn/stamp.rb +136 -0
- data/lib/prawn/text.rb +437 -0
- data/lib/prawn/text/box.rb +141 -0
- data/lib/prawn/text/formatted.rb +7 -0
- data/lib/prawn/text/formatted/arranger.rb +290 -0
- data/lib/prawn/text/formatted/box.rb +614 -0
- data/lib/prawn/text/formatted/fragment.rb +264 -0
- data/lib/prawn/text/formatted/line_wrap.rb +277 -0
- data/lib/prawn/text/formatted/parser.rb +224 -0
- data/lib/prawn/text/formatted/wrap.rb +160 -0
- data/lib/prawn/utilities.rb +46 -0
- data/lib/prawn/version.rb +5 -0
- data/lib/prawn/view.rb +91 -0
- data/manual/absolute_position.pdf +0 -0
- data/manual/basic_concepts/adding_pages.rb +27 -0
- data/manual/basic_concepts/basic_concepts.rb +36 -0
- data/manual/basic_concepts/creation.rb +39 -0
- data/manual/basic_concepts/cursor.rb +33 -0
- data/manual/basic_concepts/measurement.rb +25 -0
- data/manual/basic_concepts/origin.rb +38 -0
- data/manual/basic_concepts/other_cursor_helpers.rb +40 -0
- data/manual/basic_concepts/view.rb +42 -0
- data/manual/bounding_box/bounding_box.rb +39 -0
- data/manual/bounding_box/bounds.rb +49 -0
- data/manual/bounding_box/canvas.rb +24 -0
- data/manual/bounding_box/creation.rb +23 -0
- data/manual/bounding_box/indentation.rb +46 -0
- data/manual/bounding_box/nesting.rb +45 -0
- data/manual/bounding_box/russian_boxes.rb +40 -0
- data/manual/bounding_box/stretchy.rb +31 -0
- data/manual/contents.rb +29 -0
- data/manual/cover.rb +39 -0
- data/manual/document_and_page_options/background.rb +27 -0
- data/manual/document_and_page_options/document_and_page_options.rb +32 -0
- data/manual/document_and_page_options/metadata.rb +23 -0
- data/manual/document_and_page_options/page_margins.rb +38 -0
- data/manual/document_and_page_options/page_size.rb +34 -0
- data/manual/document_and_page_options/print_scaling.rb +20 -0
- data/manual/example_helper.rb +7 -0
- data/manual/graphics/circle_and_ellipse.rb +22 -0
- data/manual/graphics/color.rb +24 -0
- data/manual/graphics/common_lines.rb +30 -0
- data/manual/graphics/fill_and_stroke.rb +42 -0
- data/manual/graphics/fill_rules.rb +37 -0
- data/manual/graphics/gradients.rb +37 -0
- data/manual/graphics/graphics.rb +58 -0
- data/manual/graphics/helper.rb +24 -0
- data/manual/graphics/line_width.rb +35 -0
- data/manual/graphics/lines_and_curves.rb +41 -0
- data/manual/graphics/polygon.rb +29 -0
- data/manual/graphics/rectangle.rb +21 -0
- data/manual/graphics/rotate.rb +28 -0
- data/manual/graphics/scale.rb +41 -0
- data/manual/graphics/soft_masks.rb +46 -0
- data/manual/graphics/stroke_cap.rb +31 -0
- data/manual/graphics/stroke_dash.rb +48 -0
- data/manual/graphics/stroke_join.rb +30 -0
- data/manual/graphics/translate.rb +29 -0
- data/manual/graphics/transparency.rb +35 -0
- data/manual/how_to_read_this_manual.rb +40 -0
- data/manual/images/absolute_position.rb +23 -0
- data/manual/images/fit.rb +21 -0
- data/manual/images/horizontal.rb +25 -0
- data/manual/images/images.rb +40 -0
- data/manual/images/plain_image.rb +18 -0
- data/manual/images/scale.rb +22 -0
- data/manual/images/vertical.rb +28 -0
- data/manual/images/width_and_height.rb +25 -0
- data/manual/layout/boxes.rb +27 -0
- data/manual/layout/content.rb +25 -0
- data/manual/layout/layout.rb +28 -0
- data/manual/layout/simple_grid.rb +23 -0
- data/manual/outline/add_subsection_to.rb +61 -0
- data/manual/outline/insert_section_after.rb +47 -0
- data/manual/outline/outline.rb +32 -0
- data/manual/outline/sections_and_pages.rb +67 -0
- data/manual/repeatable_content/alternate_page_numbering.rb +32 -0
- data/manual/repeatable_content/page_numbering.rb +54 -0
- data/manual/repeatable_content/repeatable_content.rb +32 -0
- data/manual/repeatable_content/repeater.rb +55 -0
- data/manual/repeatable_content/stamp.rb +41 -0
- data/manual/security/encryption.rb +31 -0
- data/manual/security/permissions.rb +38 -0
- data/manual/security/security.rb +28 -0
- data/manual/table.rb +16 -0
- data/manual/text/alignment.rb +44 -0
- data/manual/text/color.rb +24 -0
- data/manual/text/column_box.rb +32 -0
- data/manual/text/fallback_fonts.rb +37 -0
- data/manual/text/font.rb +41 -0
- data/manual/text/font_size.rb +45 -0
- data/manual/text/font_style.rb +23 -0
- data/manual/text/formatted_callbacks.rb +60 -0
- data/manual/text/formatted_text.rb +50 -0
- data/manual/text/free_flowing_text.rb +51 -0
- data/manual/text/inline.rb +41 -0
- data/manual/text/kerning_and_character_spacing.rb +39 -0
- data/manual/text/leading.rb +25 -0
- data/manual/text/line_wrapping.rb +41 -0
- data/manual/text/paragraph_indentation.rb +34 -0
- data/manual/text/positioned_text.rb +38 -0
- data/manual/text/registering_families.rb +48 -0
- data/manual/text/rendering_and_color.rb +37 -0
- data/manual/text/right_to_left_text.rb +47 -0
- data/manual/text/rotation.rb +43 -0
- data/manual/text/single_usage.rb +37 -0
- data/manual/text/text.rb +73 -0
- data/manual/text/text_box_excess.rb +32 -0
- data/manual/text/text_box_extensions.rb +45 -0
- data/manual/text/text_box_overflow.rb +48 -0
- data/manual/text/utf8.rb +28 -0
- data/manual/text/win_ansi_charset.rb +60 -0
- data/prawn.gemspec +45 -0
- data/spec/acceptance/png.rb +25 -0
- data/spec/annotations_spec.rb +74 -0
- data/spec/bounding_box_spec.rb +510 -0
- data/spec/column_box_spec.rb +65 -0
- data/spec/data/curves.pdf +66 -0
- data/spec/destinations_spec.rb +15 -0
- data/spec/document_spec.rb +748 -0
- data/spec/extensions/encoding_helpers.rb +11 -0
- data/spec/extensions/mocha.rb +46 -0
- data/spec/font_metric_cache_spec.rb +52 -0
- data/spec/font_spec.rb +474 -0
- data/spec/formatted_text_arranger_spec.rb +421 -0
- data/spec/formatted_text_box_spec.rb +705 -0
- data/spec/formatted_text_fragment_spec.rb +298 -0
- data/spec/graphics_spec.rb +683 -0
- data/spec/grid_spec.rb +96 -0
- data/spec/image_handler_spec.rb +54 -0
- data/spec/images_spec.rb +153 -0
- data/spec/inline_formatted_text_parser_spec.rb +564 -0
- data/spec/jpg_spec.rb +25 -0
- data/spec/line_wrap_spec.rb +367 -0
- data/spec/measurement_units_spec.rb +25 -0
- data/spec/outline_spec.rb +430 -0
- data/spec/png_spec.rb +245 -0
- data/spec/reference_spec.rb +25 -0
- data/spec/repeater_spec.rb +160 -0
- data/spec/security_spec.rb +158 -0
- data/spec/soft_mask_spec.rb +79 -0
- data/spec/span_spec.rb +44 -0
- data/spec/spec_helper.rb +54 -0
- data/spec/stamp_spec.rb +160 -0
- data/spec/stroke_styles_spec.rb +211 -0
- data/spec/text_at_spec.rb +143 -0
- data/spec/text_box_spec.rb +1043 -0
- data/spec/text_rendering_mode_spec.rb +45 -0
- data/spec/text_spacing_spec.rb +93 -0
- data/spec/text_spec.rb +557 -0
- data/spec/text_with_inline_formatting_spec.rb +35 -0
- data/spec/transparency_spec.rb +91 -0
- data/spec/view_spec.rb +43 -0
- metadata +509 -0
@@ -0,0 +1,256 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# prawn/font/afm.rb : Implements AFM font support for Prawn
|
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
|
+
require_relative "../encoding"
|
10
|
+
|
11
|
+
module Prawn
|
12
|
+
class Font
|
13
|
+
|
14
|
+
# @private
|
15
|
+
|
16
|
+
class AFM < Font
|
17
|
+
class << self
|
18
|
+
attr_accessor :hide_m17n_warning
|
19
|
+
end
|
20
|
+
|
21
|
+
self.hide_m17n_warning = false
|
22
|
+
|
23
|
+
BUILT_INS = %w[ Courier Helvetica Times-Roman Symbol ZapfDingbats
|
24
|
+
Courier-Bold Courier-Oblique Courier-BoldOblique
|
25
|
+
Times-Bold Times-Italic Times-BoldItalic
|
26
|
+
Helvetica-Bold Helvetica-Oblique Helvetica-BoldOblique ]
|
27
|
+
|
28
|
+
def unicode?
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.metrics_path
|
33
|
+
if m = ENV['METRICS']
|
34
|
+
@metrics_path ||= m.split(':')
|
35
|
+
else
|
36
|
+
@metrics_path ||= [
|
37
|
+
".", "/usr/lib/afm",
|
38
|
+
"/usr/local/lib/afm",
|
39
|
+
"/usr/openwin/lib/fonts/afm",
|
40
|
+
Prawn::DATADIR+'/fonts']
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :attributes #:nodoc:
|
45
|
+
|
46
|
+
def initialize(document, name, options={}) #:nodoc:
|
47
|
+
unless BUILT_INS.include?(name)
|
48
|
+
raise Prawn::Errors::UnknownFont, "#{name} is not a known font."
|
49
|
+
end
|
50
|
+
|
51
|
+
super
|
52
|
+
|
53
|
+
@@font_data ||= SynchronizedCache.new # parse each ATM font file once only
|
54
|
+
|
55
|
+
file_name = @name.dup
|
56
|
+
file_name << ".afm" unless file_name =~ /\.afm$/
|
57
|
+
file_name = file_name[0] == ?/ ? file_name : find_font(file_name)
|
58
|
+
|
59
|
+
font_data = @@font_data[file_name] ||= parse_afm(file_name)
|
60
|
+
@glyph_widths = font_data[:glyph_widths]
|
61
|
+
@glyph_table = font_data[:glyph_table]
|
62
|
+
@bounding_boxes = font_data[:bounding_boxes]
|
63
|
+
@kern_pairs = font_data[:kern_pairs]
|
64
|
+
@kern_pair_table = font_data[:kern_pair_table]
|
65
|
+
@attributes = font_data[:attributes]
|
66
|
+
|
67
|
+
@ascender = @attributes["ascender"].to_i
|
68
|
+
@descender = @attributes["descender"].to_i
|
69
|
+
@line_gap = Float(bbox[3] - bbox[1]) - (@ascender - @descender)
|
70
|
+
end
|
71
|
+
|
72
|
+
# The font bbox, as an array of integers
|
73
|
+
#
|
74
|
+
def bbox
|
75
|
+
@bbox ||= @attributes['fontbbox'].split(/\s+/).map { |e| Integer(e) }
|
76
|
+
end
|
77
|
+
|
78
|
+
# NOTE: String *must* be encoded as WinAnsi
|
79
|
+
def compute_width_of(string, options={}) #:nodoc:
|
80
|
+
scale = (options[:size] || size) / 1000.0
|
81
|
+
|
82
|
+
if options[:kerning]
|
83
|
+
strings, numbers = kern(string).partition { |e| e.is_a?(String) }
|
84
|
+
total_kerning_offset = numbers.inject(0.0) { |s,r| s + r }
|
85
|
+
(unscaled_width_of(strings.join) - total_kerning_offset) * scale
|
86
|
+
else
|
87
|
+
unscaled_width_of(string) * scale
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns true if the font has kerning data, false otherwise
|
92
|
+
#
|
93
|
+
def has_kerning_data?
|
94
|
+
@kern_pairs.any?
|
95
|
+
end
|
96
|
+
|
97
|
+
# built-in fonts only work with winansi encoding, so translate the
|
98
|
+
# string. Changes the encoding in-place, so the argument itself
|
99
|
+
# is replaced with a string in WinAnsi encoding.
|
100
|
+
#
|
101
|
+
def normalize_encoding(text)
|
102
|
+
text.encode("windows-1252")
|
103
|
+
rescue ::Encoding::InvalidByteSequenceError,
|
104
|
+
::Encoding::UndefinedConversionError
|
105
|
+
|
106
|
+
raise Prawn::Errors::IncompatibleStringEncoding,
|
107
|
+
"Your document includes text that's not compatible with the Windows-1252 character set.\n"+
|
108
|
+
"If you need full UTF-8 support, use TTF fonts instead of PDF's built-in fonts\n."
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_utf8(text)
|
112
|
+
text.encode("UTF-8")
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns the number of characters in +str+ (a WinAnsi-encoded string).
|
116
|
+
#
|
117
|
+
def character_count(str)
|
118
|
+
str.length
|
119
|
+
end
|
120
|
+
|
121
|
+
# Perform any changes to the string that need to happen
|
122
|
+
# before it is rendered to the canvas. Returns an array of
|
123
|
+
# subset "chunks", where each chunk is an array of two elements.
|
124
|
+
# The first element is the font subset number, and the second
|
125
|
+
# is either a string or an array (for kerned text).
|
126
|
+
#
|
127
|
+
# For Adobe fonts, there is only ever a single subset, so
|
128
|
+
# the first element of the array is "0", and the second is
|
129
|
+
# the string itself (or an array, if kerning is performed).
|
130
|
+
#
|
131
|
+
# The +text+ parameter must be in WinAnsi encoding (cp1252).
|
132
|
+
#
|
133
|
+
def encode_text(text, options={})
|
134
|
+
[[0, options[:kerning] ? kern(text) : text]]
|
135
|
+
end
|
136
|
+
|
137
|
+
def glyph_present?(char)
|
138
|
+
!!normalize_encoding(char)
|
139
|
+
rescue Prawn::Errors::IncompatibleStringEncoding
|
140
|
+
false
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def register(subset)
|
146
|
+
font_dict = {:Type => :Font,
|
147
|
+
:Subtype => :Type1,
|
148
|
+
:BaseFont => name.to_sym}
|
149
|
+
|
150
|
+
# Symbolic AFM fonts (Symbol, ZapfDingbats) have their own encodings
|
151
|
+
font_dict.merge!(:Encoding => :WinAnsiEncoding) unless symbolic?
|
152
|
+
|
153
|
+
@document.ref!(font_dict)
|
154
|
+
end
|
155
|
+
|
156
|
+
def symbolic?
|
157
|
+
attributes["characterset"] == "Special"
|
158
|
+
end
|
159
|
+
|
160
|
+
def find_font(file)
|
161
|
+
self.class.metrics_path.find { |f| File.exist? "#{f}/#{file}" } + "/#{file}"
|
162
|
+
rescue NoMethodError
|
163
|
+
raise Prawn::Errors::UnknownFont,
|
164
|
+
"Couldn't find the font: #{file} in any of:\n" +
|
165
|
+
self.class.metrics_path.join("\n")
|
166
|
+
end
|
167
|
+
|
168
|
+
def parse_afm(file_name)
|
169
|
+
data = {:glyph_widths => {}, :bounding_boxes => {}, :kern_pairs => {}, :attributes => {}}
|
170
|
+
section = []
|
171
|
+
|
172
|
+
File.foreach(file_name) do |line|
|
173
|
+
case line
|
174
|
+
when /^Start(\w+)/
|
175
|
+
section.push $1
|
176
|
+
next
|
177
|
+
when /^End(\w+)/
|
178
|
+
section.pop
|
179
|
+
next
|
180
|
+
end
|
181
|
+
|
182
|
+
case section
|
183
|
+
when ["FontMetrics", "CharMetrics"]
|
184
|
+
next unless line =~ /^CH?\s/
|
185
|
+
|
186
|
+
name = line[/\bN\s+(\.?\w+)\s*;/, 1]
|
187
|
+
data[:glyph_widths][name] = line[/\bWX\s+(\d+)\s*;/, 1].to_i
|
188
|
+
data[:bounding_boxes][name] = line[/\bB\s+([^;]+);/, 1].to_s.rstrip
|
189
|
+
when ["FontMetrics", "KernData", "KernPairs"]
|
190
|
+
next unless line =~ /^KPX\s+(\.?\w+)\s+(\.?\w+)\s+(-?\d+)/
|
191
|
+
data[:kern_pairs][[$1, $2]] = $3.to_i
|
192
|
+
when ["FontMetrics", "KernData", "TrackKern"],
|
193
|
+
["FontMetrics", "Composites"]
|
194
|
+
next
|
195
|
+
else
|
196
|
+
parse_generic_afm_attribute(line, data)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# process data parsed from AFM file to build tables which
|
201
|
+
# will be used when measuring and kerning text
|
202
|
+
data[:glyph_table] = (0..255).map do |i|
|
203
|
+
data[:glyph_widths][Encoding::WinAnsi::CHARACTERS[i]].to_i
|
204
|
+
end
|
205
|
+
|
206
|
+
character_hash = Hash[Encoding::WinAnsi::CHARACTERS.zip((0..Encoding::WinAnsi::CHARACTERS.size).to_a)]
|
207
|
+
data[:kern_pair_table] = data[:kern_pairs].inject({}) do |h,p|
|
208
|
+
h[p[0].map { |n| character_hash[n] }] = p[1]
|
209
|
+
h
|
210
|
+
end
|
211
|
+
|
212
|
+
data.each_value { |hash| hash.freeze }
|
213
|
+
data.freeze
|
214
|
+
end
|
215
|
+
|
216
|
+
def parse_generic_afm_attribute(line, hash)
|
217
|
+
line =~ /(^\w+)\s+(.*)/
|
218
|
+
key, value = $1.to_s.downcase, $2
|
219
|
+
|
220
|
+
hash[:attributes][key] = hash[:attributes][key] ? Array(hash[:attributes][key]) << value : value
|
221
|
+
end
|
222
|
+
|
223
|
+
# converts a string into an array with spacing offsets
|
224
|
+
# bewteen characters that need to be kerned
|
225
|
+
#
|
226
|
+
# String *must* be encoded as WinAnsi
|
227
|
+
#
|
228
|
+
def kern(string)
|
229
|
+
kerned = [[]]
|
230
|
+
last_byte = nil
|
231
|
+
|
232
|
+
string.each_byte do |byte|
|
233
|
+
if k = last_byte && @kern_pair_table[[last_byte, byte]]
|
234
|
+
kerned << -k << [byte]
|
235
|
+
else
|
236
|
+
kerned.last << byte
|
237
|
+
end
|
238
|
+
last_byte = byte
|
239
|
+
end
|
240
|
+
|
241
|
+
kerned.map { |e|
|
242
|
+
e = (Array === e ? e.pack("C*") : e)
|
243
|
+
e.respond_to?(:force_encoding) ? e.force_encoding(::Encoding::Windows_1252) : e
|
244
|
+
}
|
245
|
+
end
|
246
|
+
|
247
|
+
private
|
248
|
+
|
249
|
+
def unscaled_width_of(string)
|
250
|
+
string.bytes.inject(0) do |s,r|
|
251
|
+
s + @glyph_table[r]
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# font.rb : The Prawn font class
|
4
|
+
#
|
5
|
+
# Copyright November 2008, Jamis Buck. All Rights Reserved.
|
6
|
+
#
|
7
|
+
# This is free software. Please see the LICENSE and COPYING files for details.
|
8
|
+
#
|
9
|
+
require_relative 'ttf'
|
10
|
+
|
11
|
+
module Prawn
|
12
|
+
class Font
|
13
|
+
# @private
|
14
|
+
class DFont < TTF
|
15
|
+
|
16
|
+
# Returns a list of the names of all named fonts in the given dfont file.
|
17
|
+
# Note that fonts are not required to be named in a dfont file, so the
|
18
|
+
# list may be empty even if the file does contain fonts. Also, note that
|
19
|
+
# the list is returned in no particular order, so the first font in the
|
20
|
+
# list is not necessarily the font at index 0 in the file.
|
21
|
+
#
|
22
|
+
def self.named_fonts(file)
|
23
|
+
TTFunk::ResourceFile.open(file) do |f|
|
24
|
+
return f.resources_for("sfnt")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the number of fonts contained in the dfont file.
|
29
|
+
#
|
30
|
+
def self.font_count(file)
|
31
|
+
TTFunk::ResourceFile.open(file) do |f|
|
32
|
+
return f.map["sfnt"][:list].length
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def read_ttf_file
|
39
|
+
TTFunk::File.from_dfont(@name, @options[:font] || 0)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,355 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# prawn/font/ttf.rb : Implements AFM font support for Prawn
|
4
|
+
#
|
5
|
+
# Copyright May 2008, Gregory Brown / James Healy / Jamis Buck
|
6
|
+
# All Rights Reserved.
|
7
|
+
#
|
8
|
+
# This is free software. Please see the LICENSE and COPYING files for details.
|
9
|
+
|
10
|
+
require 'ttfunk'
|
11
|
+
require 'ttfunk/subset_collection'
|
12
|
+
|
13
|
+
module Prawn
|
14
|
+
class Font
|
15
|
+
|
16
|
+
# @private
|
17
|
+
class TTF < Font
|
18
|
+
attr_reader :ttf, :subsets
|
19
|
+
|
20
|
+
def unicode?
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(document, name, options={})
|
25
|
+
super
|
26
|
+
|
27
|
+
@ttf = read_ttf_file
|
28
|
+
@subsets = TTFunk::SubsetCollection.new(@ttf)
|
29
|
+
|
30
|
+
@attributes = {}
|
31
|
+
@bounding_boxes = {}
|
32
|
+
@char_widths = {}
|
33
|
+
@has_kerning_data = @ttf.kerning.exists? && @ttf.kerning.tables.any?
|
34
|
+
|
35
|
+
@ascender = Integer(@ttf.ascent * scale_factor)
|
36
|
+
@descender = Integer(@ttf.descent * scale_factor)
|
37
|
+
@line_gap = Integer(@ttf.line_gap * scale_factor)
|
38
|
+
end
|
39
|
+
|
40
|
+
# NOTE: +string+ must be UTF8-encoded.
|
41
|
+
def compute_width_of(string, options={}) #:nodoc:
|
42
|
+
scale = (options[:size] || size) / 1000.0
|
43
|
+
if options[:kerning]
|
44
|
+
kern(string).inject(0) do |s,r|
|
45
|
+
if r.is_a?(Numeric)
|
46
|
+
s - r
|
47
|
+
else
|
48
|
+
r.inject(s) { |s2, u| s2 + character_width_by_code(u) }
|
49
|
+
end
|
50
|
+
end * scale
|
51
|
+
else
|
52
|
+
string.codepoints.inject(0) do |s,r|
|
53
|
+
s + character_width_by_code(r)
|
54
|
+
end * scale
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# The font bbox, as an array of integers
|
59
|
+
#
|
60
|
+
def bbox
|
61
|
+
@bbox ||= @ttf.bbox.map { |i| Integer(i * scale_factor) }
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns true if the font has kerning data, false otherwise
|
65
|
+
def has_kerning_data?
|
66
|
+
@has_kerning_data
|
67
|
+
end
|
68
|
+
|
69
|
+
# Perform any changes to the string that need to happen
|
70
|
+
# before it is rendered to the canvas. Returns an array of
|
71
|
+
# subset "chunks", where the even-numbered indices are the
|
72
|
+
# font subset number, and the following entry element is
|
73
|
+
# either a string or an array (for kerned text).
|
74
|
+
#
|
75
|
+
# The +text+ parameter must be UTF8-encoded.
|
76
|
+
#
|
77
|
+
def encode_text(text,options={})
|
78
|
+
text = text.chomp
|
79
|
+
|
80
|
+
if options[:kerning]
|
81
|
+
last_subset = nil
|
82
|
+
kern(text).inject([]) do |result, element|
|
83
|
+
if element.is_a?(Numeric)
|
84
|
+
result.last[1] = [result.last[1]] unless result.last[1].is_a?(Array)
|
85
|
+
result.last[1] << element
|
86
|
+
result
|
87
|
+
else
|
88
|
+
encoded = @subsets.encode(element)
|
89
|
+
|
90
|
+
if encoded.first[0] == last_subset
|
91
|
+
result.last[1] << encoded.first[1]
|
92
|
+
encoded.shift
|
93
|
+
end
|
94
|
+
|
95
|
+
if encoded.any?
|
96
|
+
last_subset = encoded.last[0]
|
97
|
+
result + encoded
|
98
|
+
else
|
99
|
+
result
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
else
|
104
|
+
@subsets.encode(text.unpack("U*"))
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def basename
|
109
|
+
@basename ||= @ttf.name.postscript_name
|
110
|
+
end
|
111
|
+
|
112
|
+
# not sure how to compute this for true-type fonts...
|
113
|
+
def stemV
|
114
|
+
0
|
115
|
+
end
|
116
|
+
|
117
|
+
def italic_angle
|
118
|
+
@italic_angle ||= if @ttf.postscript.exists?
|
119
|
+
raw = @ttf.postscript.italic_angle
|
120
|
+
hi, low = raw >> 16, raw & 0xFF
|
121
|
+
hi = -((hi ^ 0xFFFF) + 1) if hi & 0x8000 != 0
|
122
|
+
"#{hi}.#{low}".to_f
|
123
|
+
else
|
124
|
+
0
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def cap_height
|
129
|
+
@cap_height ||= begin
|
130
|
+
height = @ttf.os2.exists? && @ttf.os2.cap_height || 0
|
131
|
+
height == 0 ? @ascender : height
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def x_height
|
136
|
+
# FIXME: seems like if os2 table doesn't exist, we could
|
137
|
+
# just find the height of the lower-case 'x' glyph?
|
138
|
+
@ttf.os2.exists? && @ttf.os2.x_height || 0
|
139
|
+
end
|
140
|
+
|
141
|
+
def family_class
|
142
|
+
@family_class ||= (@ttf.os2.exists? && @ttf.os2.family_class || 0) >> 8
|
143
|
+
end
|
144
|
+
|
145
|
+
def serif?
|
146
|
+
@serif ||= [1,2,3,4,5,7].include?(family_class)
|
147
|
+
end
|
148
|
+
|
149
|
+
def script?
|
150
|
+
@script ||= family_class == 10
|
151
|
+
end
|
152
|
+
|
153
|
+
def pdf_flags
|
154
|
+
@flags ||= begin
|
155
|
+
flags = 0
|
156
|
+
flags |= 0x0001 if @ttf.postscript.fixed_pitch?
|
157
|
+
flags |= 0x0002 if serif?
|
158
|
+
flags |= 0x0008 if script?
|
159
|
+
flags |= 0x0040 if italic_angle != 0
|
160
|
+
flags |= 0x0004 # assume the font contains at least some non-latin characters
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def normalize_encoding(text)
|
165
|
+
begin
|
166
|
+
text.encode(::Encoding::UTF_8)
|
167
|
+
rescue => e
|
168
|
+
puts e
|
169
|
+
raise Prawn::Errors::IncompatibleStringEncoding, "Encoding " +
|
170
|
+
"#{text.encoding} can not be transparently converted to UTF-8. " +
|
171
|
+
"Please ensure the encoding of the string you are attempting " +
|
172
|
+
"to use is set correctly"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def to_utf8(text)
|
177
|
+
text.encode("UTF-8")
|
178
|
+
end
|
179
|
+
|
180
|
+
def glyph_present?(char)
|
181
|
+
code = char.codepoints.first
|
182
|
+
cmap[code] > 0
|
183
|
+
end
|
184
|
+
|
185
|
+
# Returns the number of characters in +str+ (a UTF-8-encoded string).
|
186
|
+
#
|
187
|
+
def character_count(str)
|
188
|
+
str.length
|
189
|
+
end
|
190
|
+
|
191
|
+
private
|
192
|
+
|
193
|
+
def cmap
|
194
|
+
@cmap ||= @ttf.cmap.unicode.first or raise("no unicode cmap for font")
|
195
|
+
end
|
196
|
+
|
197
|
+
# +string+ must be UTF8-encoded.
|
198
|
+
#
|
199
|
+
# Returns an array. If an element is a numeric, it represents the
|
200
|
+
# kern amount to inject at that position. Otherwise, the element
|
201
|
+
# is an array of UTF-16 characters.
|
202
|
+
def kern(string)
|
203
|
+
a = []
|
204
|
+
|
205
|
+
string.each_codepoint do |r|
|
206
|
+
if a.empty?
|
207
|
+
a << [r]
|
208
|
+
elsif (kern = kern_pairs_table[[cmap[a.last.last], cmap[r]]])
|
209
|
+
kern *= scale_factor
|
210
|
+
a << -kern << [r]
|
211
|
+
else
|
212
|
+
a.last << r
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
a
|
217
|
+
end
|
218
|
+
|
219
|
+
def kern_pairs_table
|
220
|
+
@kerning_data ||= has_kerning_data? ? @ttf.kerning.tables.first.pairs : {}
|
221
|
+
end
|
222
|
+
|
223
|
+
def cid_to_gid_map
|
224
|
+
max = cmap.code_map.keys.max
|
225
|
+
(0..max).map { |cid| cmap[cid] }.pack("n*")
|
226
|
+
end
|
227
|
+
|
228
|
+
def hmtx
|
229
|
+
@hmtx ||= @ttf.horizontal_metrics
|
230
|
+
end
|
231
|
+
|
232
|
+
def character_width_by_code(code)
|
233
|
+
return 0 unless cmap[code]
|
234
|
+
|
235
|
+
# Some TTF fonts have nonzero widths for \n (UTF-8 / ASCII code: 10).
|
236
|
+
# Patch around this as we'll never be drawing a newline with a width.
|
237
|
+
return 0.0 if code == 10
|
238
|
+
|
239
|
+
@char_widths[code] ||= Integer(hmtx.widths[cmap[code]] * scale_factor)
|
240
|
+
end
|
241
|
+
|
242
|
+
def scale_factor
|
243
|
+
@scale ||= 1000.0 / @ttf.header.units_per_em
|
244
|
+
end
|
245
|
+
|
246
|
+
def register(subset)
|
247
|
+
temp_name = @ttf.name.postscript_name.gsub("\0","").to_sym
|
248
|
+
ref = @document.ref!(:Type => :Font, :BaseFont => temp_name)
|
249
|
+
|
250
|
+
# Embed the font metrics in the document after everything has been
|
251
|
+
# drawn, just before the document is emitted.
|
252
|
+
@document.renderer.before_render { |doc| embed(ref, subset) }
|
253
|
+
|
254
|
+
ref
|
255
|
+
end
|
256
|
+
|
257
|
+
def embed(reference, subset)
|
258
|
+
font_content = @subsets[subset].encode
|
259
|
+
|
260
|
+
# FIXME: we need postscript_name and glyph widths from the font
|
261
|
+
# subset. Perhaps this could be done by querying the subset,
|
262
|
+
# rather than by parsing the font that the subset produces?
|
263
|
+
font = TTFunk::File.new(font_content)
|
264
|
+
|
265
|
+
# empirically, it looks like Adobe Reader will not display fonts
|
266
|
+
# if their font name is more than 33 bytes long. Strange. But true.
|
267
|
+
basename = font.name.postscript_name[0, 33].gsub("\0","")
|
268
|
+
|
269
|
+
raise "Can't detect a postscript name for #{file}" if basename.nil?
|
270
|
+
|
271
|
+
fontfile = @document.ref!(:Length1 => font_content.size)
|
272
|
+
fontfile.stream << font_content
|
273
|
+
fontfile.stream.compress!
|
274
|
+
|
275
|
+
descriptor = @document.ref!(:Type => :FontDescriptor,
|
276
|
+
:FontName => basename.to_sym,
|
277
|
+
:FontFile2 => fontfile,
|
278
|
+
:FontBBox => bbox,
|
279
|
+
:Flags => pdf_flags,
|
280
|
+
:StemV => stemV,
|
281
|
+
:ItalicAngle => italic_angle,
|
282
|
+
:Ascent => @ascender,
|
283
|
+
:Descent => @descender,
|
284
|
+
:CapHeight => cap_height,
|
285
|
+
:XHeight => x_height)
|
286
|
+
|
287
|
+
hmtx = font.horizontal_metrics
|
288
|
+
widths = font.cmap.tables.first.code_map.map { |gid|
|
289
|
+
Integer(hmtx.widths[gid] * scale_factor) }[32..-1]
|
290
|
+
|
291
|
+
# It would be nice to have Encoding set for the macroman subsets,
|
292
|
+
# and only do a ToUnicode cmap for non-encoded unicode subsets.
|
293
|
+
# However, apparently Adobe Reader won't render MacRoman encoded
|
294
|
+
# subsets if original font contains unicode characters. (It has to
|
295
|
+
# be some flag or something that ttfunk is simply copying over...
|
296
|
+
# but I can't figure out which flag that is.)
|
297
|
+
#
|
298
|
+
# For now, it's simplest to just create a unicode cmap for every font.
|
299
|
+
# It offends my inner purist, but it'll do.
|
300
|
+
|
301
|
+
map = @subsets[subset].to_unicode_map
|
302
|
+
|
303
|
+
ranges = [[]]
|
304
|
+
map.keys.sort.inject("") do |s, code|
|
305
|
+
ranges << [] if ranges.last.length >= 100
|
306
|
+
unicode = map[code]
|
307
|
+
ranges.last << "<%02x><%04x>" % [code, unicode]
|
308
|
+
end
|
309
|
+
|
310
|
+
range_blocks = ranges.inject("") do |s, list|
|
311
|
+
s << "%d beginbfchar\n%s\nendbfchar\n" % [list.length, list.join("\n")]
|
312
|
+
end
|
313
|
+
|
314
|
+
to_unicode_cmap = UNICODE_CMAP_TEMPLATE % range_blocks.strip
|
315
|
+
|
316
|
+
cmap = @document.ref!({})
|
317
|
+
cmap << to_unicode_cmap
|
318
|
+
cmap.stream.compress!
|
319
|
+
|
320
|
+
reference.data.update(:Subtype => :TrueType,
|
321
|
+
:BaseFont => basename.to_sym,
|
322
|
+
:FontDescriptor => descriptor,
|
323
|
+
:FirstChar => 32,
|
324
|
+
:LastChar => 255,
|
325
|
+
:Widths => @document.ref!(widths),
|
326
|
+
:ToUnicode => cmap)
|
327
|
+
end
|
328
|
+
|
329
|
+
UNICODE_CMAP_TEMPLATE = <<-STR.strip.gsub(/^\s*/, "")
|
330
|
+
/CIDInit /ProcSet findresource begin
|
331
|
+
12 dict begin
|
332
|
+
begincmap
|
333
|
+
/CIDSystemInfo <<
|
334
|
+
/Registry (Adobe)
|
335
|
+
/Ordering (UCS)
|
336
|
+
/Supplement 0
|
337
|
+
>> def
|
338
|
+
/CMapName /Adobe-Identity-UCS def
|
339
|
+
/CMapType 2 def
|
340
|
+
1 begincodespacerange
|
341
|
+
<00><ff>
|
342
|
+
endcodespacerange
|
343
|
+
%s
|
344
|
+
endcmap
|
345
|
+
CMapName currentdict /CMap defineresource pop
|
346
|
+
end
|
347
|
+
end
|
348
|
+
STR
|
349
|
+
|
350
|
+
def read_ttf_file
|
351
|
+
TTFunk::File.open(@name)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|