# encoding: utf-8
# metrics.rb : Font metrics parsers for AFM and TTF.
#
# Font::Metrics::Adobe is mainly a port of CPAN's Font::AFM
# http://search.cpan.org/~gaas/Font-AFM-1.19/AFM.pm
#
# Copyright May 2008, Gregory Brown / James Edward Gray II. All Rights Reserved.
#
# This is free software. Please see the LICENSE and COPYING files for details.
module Prawn
module Font #:nodoc:
class Metrics #:nodoc:
include Prawn::Font::Wrapping
def self.[](font)
data[font] ||= case(font)
when /\.ttf$/
TTF.new(font)
else
Adobe.new(font)
end
end
def self.data
@data ||= {}
end
def string_height(string,options={})
string = naive_wrap(string, options[:line_width], options[:font_size])
string.lines.to_a.length * font_height(options[:font_size])
end
class Adobe < Metrics #:nodoc:
ISOLatin1Encoding = %w[
.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef
.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef
.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef
.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef space
exclam quotedbl numbersign dollar percent ampersand quoteright
parenleft parenright asterisk plus comma minus period slash zero one
two three four five six seven eight nine colon semicolon less equal
greater question at A B C D E F G H I J K L M N O P Q R S
T U V W X Y Z bracketleft backslash bracketright asciicircum
underscore quoteleft a b c d e f g h i j k l m n o p q r s
t u v w x y z braceleft bar braceright asciitilde .notdef .notdef
.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef
.notdef .notdef .notdef .notdef .notdef .notdef .notdef dotlessi grave
acute circumflex tilde macron breve dotaccent dieresis .notdef ring
cedilla .notdef hungarumlaut ogonek caron space exclamdown cent
sterling currency yen brokenbar section dieresis copyright ordfeminine
guillemotleft logicalnot hyphen registered macron degree plusminus
twosuperior threesuperior acute mu paragraph periodcentered cedilla
onesuperior ordmasculine guillemotright onequarter onehalf threequarters
questiondown Agrave Aacute Acircumflex Atilde Adieresis Aring AE
Ccedilla Egrave Eacute Ecircumflex Edieresis Igrave Iacute Icircumflex
Idieresis Eth Ntilde Ograve Oacute Ocircumflex Otilde Odieresis
multiply Oslash Ugrave Uacute Ucircumflex Udieresis Yacute Thorn
germandbls agrave aacute acircumflex atilde adieresis aring ae
ccedilla egrave eacute ecircumflex edieresis igrave iacute icircumflex
idieresis eth ntilde ograve oacute ocircumflex otilde odieresis divide
oslash ugrave uacute ucircumflex udieresis yacute thorn ydieresis
]
attr_reader :attributes
def initialize(font_name)
@attributes = {}
@glyph_widths = {}
@bounding_boxes = {}
@kern_pairs = {}
file = font_name.sub(/\.afm$/,'') + '.afm'
unless file[0..0] == "/"
file = find_font(file)
end
parse_afm(file)
end
def bbox
fontbbox.split(/\s+/).map { |e| Integer(e) }
end
def font_height(font_size)
Float(bbox[3] - bbox[1]) * font_size / 1000.0
end
def string_width(string, font_size, options = {})
scale = font_size / 1000.0
if options[:kerning]
kern(string).inject(0) do |s,r|
if r.is_a? String
s + string_width(r, font_size, :kerning => false)
else
s - (r * scale)
end
end
else
string.unpack("U*").inject(0) do |s,r|
s + latin_glyphs_table[r].to_i
end * scale
end
end
def kern(string)
kerned = string.unpack("U*").inject([]) do |a,r|
if a.last.is_a? Array
if kern = latin_kern_pairs_table[[a.last.last, r]]
a << kern << [r]
else
a.last << r
end
else
a << [r]
end
a
end
kerned.map { |r|
i = r.is_a?(Array) ? r.pack("U*") : r
i.is_a?(Numeric) ? -i : i
}
end
def latin_kern_pairs_table
@kern_pairs_table ||= @kern_pairs.inject({}) do |h,p|
h[p[0].map { |n| ISOLatin1Encoding.index(n) }] = p[1]
h
end
end
def latin_glyphs_table
@glyphs_table ||= (0..255).map do |i|
@glyph_widths[ISOLatin1Encoding[i]].to_i
end
end
def ascender
@attributes["ascender"].to_i
end
def descender
@attributes["descender"].to_i
end
# Hackish, but does the trick for now.
def method_missing(method, *args, &block)
name = method.to_s.delete("_")
if @attributes.include? name
@attributes[name]
else
super
end
end
def metrics_path
if m = ENV['METRICS']
@metrics_path ||= m.split(':')
else
@metrics_path ||= [
"/usr/lib/afm",
"/usr/local/lib/afm",
"/usr/openwin/lib/fonts/afm/",
Prawn::BASEDIR+'/data/fonts/','.']
end
end
def has_kerning_data?
true
end
def type0?
false
end
def convert_text(text, options={})
options[:kerning] ? kern(text) : text
end
private
def find_font(file)
metrics_path.find { |f| File.exist? "#{f}/#{file}" } + "/#{file}"
rescue NoMethodError
raise Prawn::Errors::UnknownFont,
"Couldn't find the font: #{file} in any of:\n" +
@metrics_path.join("\n")
end
def parse_afm(file)
section = []
File.open(file,"rb") do |file|
file.each do |line|
if line =~ /^Start(\w+)/
section.push $1
next
elsif line =~ /^End(\w+)/
section.pop
next
end
if section == ["FontMetrics", "CharMetrics"]
next unless line =~ /^CH?\s/
name = line[/\bN\s+(\.?\w+)\s*;/, 1]
@glyph_widths[name] = line[/\bWX\s+(\d+)\s*;/, 1].to_i
@bounding_boxes[name] = line[/\bB\s+([^;]+);/, 1].to_s.rstrip
elsif section == ["FontMetrics", "KernData", "KernPairs"]
next unless line =~ /^KPX\s+(\.?\w+)\s+(\.?\w+)\s+(-?\d+)/
@kern_pairs[[$1, $2]] = $3.to_i
elsif section == ["FontMetrics", "KernData", "TrackKern"]
next
elsif section == ["FontMetrics", "Composites"]
next
elsif line =~ /(^\w+)\s+(.*)/
key, value = $1.to_s.downcase, $2
@attributes[key] = @attributes[key] ?
Array(@attributes[key]) << value : value
else
warn "Can't parse: #{line}"
end
end
end
end
end
class TTF < Metrics #:nodoc:
def initialize(font)
@ttf = ::Font::TTF::File.open(font,"rb")
@attributes = {}
@glyph_widths = {}
@bounding_boxes = {}
end
def cmap
@cmap ||= enc_table.charmaps
end
def string_width(string, font_size, options = {})
scale = font_size / 1000.0
if options[:kerning]
kern(string,:skip_conversion => true).inject(0) do |s,r|
if r.is_a? String
s + string_width(r, font_size, :kerning => false)
else
s + r * scale
end
end
else
string.unpack("U*").inject(0) do |s,r|
s + character_width_by_code(r)
end * scale
end
end
# TODO: NASTY.
def kern(string,options={})
string.unpack("U*").inject([]) do |a,r|
if a.last.is_a? Array
if kern = kern_pairs_table[[cmap[a.last.last], cmap[r]]]
kern *= scale_factor
a << kern << [r]
else
a.last << r
end
else
a << [r]
end
a
end.map { |r|
if options[:skip_conversion]
r.is_a?(Array) ? r.pack("U*") : r
else
i = r.is_a?(Array) ? r.pack("U*") : r
x = if i.is_a?(String)
unicode_codepoints = i.unpack("U*")
glyph_codes = unicode_codepoints.map { |u|
enc_table.get_glyph_id_for_unicode(u)
}
glyph_codes.pack("n*")
else
i
end
x.is_a?(Numeric) ? -x : x
end
}
end
def glyph_widths
glyphs = cmap.values.uniq.sort
first_glyph = glyphs.shift
widths = [first_glyph, [Integer(hmtx[first_glyph][0] * scale_factor)]]
prev_glyph = first_glyph
glyphs.each do |glyph|
unless glyph == prev_glyph + 1
widths << glyph
widths << []
end
widths.last << Integer(hmtx[glyph][0] * scale_factor )
prev_glyph = glyph
end
widths
end
def bbox
head = @ttf.get_table(:head)
[:x_min, :y_min, :x_max, :y_max].map do |atr|
Integer(head.send(atr)) * scale_factor
end
end
def ascender
Integer(@ttf.get_table(:hhea).ascender * scale_factor)
end
def descender
Integer(@ttf.get_table(:hhea).descender * scale_factor)
end
def font_height(size)
(ascender - descender) * size / 1000.0
end
def basename
return @basename if @basename
ps_name = ::Font::TTF::Table::Name::NameRecord::POSTSCRIPT_NAME
@ttf.get_table(:name).name_records.each do |rec|
@basename = rec.utf8_str.to_sym if rec.name_id == ps_name
end
@basename
end
def enc_table
@enc_table ||= @ttf.get_table(:cmap).encoding_tables.find do |t|
t.class == ::Font::TTF::Table::Cmap::EncodingTable4
end
end
# TODO: instead of creating a map that contains every glyph in the font,
# only include the glyphs that were used
def to_unicode_cmap
return @to_unicode if @to_unicode
@to_unicode = Prawn::Font::CMap.new
unicode_for_glyph = cmap.invert
glyphs = unicode_for_glyph.keys.uniq.sort
glyphs.each do |glyph|
@to_unicode[unicode_for_glyph[glyph]] = glyph
end
@to_unicode
end
def kern_pairs_table
return @kern_pairs_table if @kern_pairs_table
table = @ttf.get_table(:kern).subtables.find { |s|
s.is_a? ::Font::TTF::Table::Kern::KerningSubtable0 }
if table
@kern_pairs_table ||= table.kerning_pairs.inject({}) do |h,p|
h[[p.left, p.right]] = p.value
h
end
else
@kern_pairs_table = {}
end
end
def has_kerning_data?
!kern_pairs_table.empty?
rescue ::Font::TTF::TableMissing
false
end
def type0?
true
end
def convert_text(text,options)
text = text.chomp
if options[:kerning]
kern(text)
else
unicode_codepoints = text.unpack("U*")
glyph_codes = unicode_codepoints.map { |u|
enc_table.get_glyph_id_for_unicode(u)
}
text = glyph_codes.pack("n*")
end
end
private
def hmtx
@hmtx ||= @ttf.get_table(:hmtx).metrics
end
def character_width_by_code(code)
return 0 unless cmap[code]
Integer(hmtx[cmap[code]][0] * scale_factor)
end
def scale_factor
@scale ||= 1 / Float(@ttf.get_table(:head).units_per_em / 1000.0)
end
end
end
end
end