# Last Change: Tue May 16 19:11:20 2006 # require 'rfi' require 'strscan' require 'pathname' require 'rfil/font/metric' module RFIL # :nodoc: module Font # :nodoc: # = AFM -- Access type1 font metric files # # == General information # # Read and parse a (type1) afm file. The afm file must be compliant to # the afm specification as described in 'Adobe Font Metrics File # Format Specification' Version 4.1, dated October 7 1998. # # == Example usage # # === Read an afm file # filename = "/opt/tetex/3.0/texmf/fonts/afm/urw/palatino/uplb8a.afm" # afm=AFM.new # afm.read(filename) # afm.filename # => "/opt/..../uplb8a.afm" # afm.count_charmetrics # => 316 # afm.encodingscheme # => "AdobeStandardEncoding" # # .... # class AFM < Metric # This is set to true if there is something wrong in the afm file. # Diagnostics can be turned on with :verbose set to true # when creating the object. attr_reader :something_strange # Number of characters found in the afm file. attr_accessor :count_charmetrics # Number of encoded character found in the afm file. attr_accessor :count_charmetrics_encoded # Number of unencoded character found in the afm file. attr_accessor :count_charmetrics_unencoded # The default encoding of the font. attr_accessor :encodingscheme # Boundingbox of the font. Array of for elements. attr_accessor :fontbbox # Underline position of the font. attr_accessor :underlineposition # Underline thickness. attr_accessor :underlinethickness # Height of caps. attr_accessor :capheight # Height of ascender. attr_accessor :ascender # Height of descender. attr_accessor :descender # Create an empty afm file. If _afm_ is set, use this to initialize # the object. _afm_ is either a string with the contents of an afm # file or a File object that points to the afm file. _options_ # currently only accepts :verbose (true/false), that prints # out some diagnostic information on STDERR. def initialize(options={}) @something_strange = false super() @outlinetype=:type1 @comment = "" @verbose=options[:verbose]==true end # Read the afm file given with _filename_. _filename_ must be full # path to the afm file, it does not perform any lookups. Returns self. def read (filename) @filename=File.basename(filename) @name=@filename.chomp(".afm") self.pathname=Pathname.new(filename).realpath.to_s parse(File.read(filename)) end # Return a string representation of the afm file that is compliant # with the afm spec. def to_s s ="StartFontMetrics 2.0\n" s << "Comment Generated using the RFI Library\n" %w( FontName FullName FamilyName Weight Notice ItalicAngle IsFixedPitch UnderlinePosition UnderlineTickness Version EncodingScheme CapHeight XHeight Descender Ascender ).each {|kw| meth=kw.downcase.to_sym value=self.send(meth) if self.respond_to?(meth) if value s << kw << " " << value.to_s << "\n" end } s << "FontBBox " << @fontbbox.join(" ") << "\n" s << "StartCharMetrics #@count_charmetrics\n" @chars.sort{ |a,b| # puts "a=#{a[1].c}, b=#{b[1].c}" if a[1].c == -1 b[1].c == -1 ? 0 : 1 else b[1].c == -1 ? -1 : a[1].c <=> b[1].c end }.each { |a,b| s << "C #{b.c} ; WX #{b.wx} ; N #{a} ; B #{b.b.join(" ")}\n" } s << "EndCharMetrics\nStartKernData\nStartKernPairs" count=0 @chars.each_value { |c| count += c.kern_data.size } s << " #{count}\n" @chars.sort{ |a,b| a[0] <=> b[0] }.each { |name,char| char.kern_data.each { |destname, value| s << "KPX #{name} #{destname} #{value[0]}\n" } } s << "EndKernPairs\nEndKernData\nEndFontMetrics\n" s end # Parse the contents of the String _txt_. Returns self. def parse(txt) @chars ||= Hash.new @s=StringScanner.new(txt.gsub(/\r\n/,"\n")) @s.scan(/StartFontMetrics/) get_fontmetrics self end ####### private ####### def get_keyword @s.skip_until(/\s+/) @s.scan(/[A-Z][A-Za-z0-9]+/) end def get_integer @s.skip(/\s+/) @s.scan(/-?\d+/).to_i end def get_number @s.skip(/\s+/) @s.scan(/-?\d+(?:\.\d+)?/).to_f end def get_boolean @s.skip(/\s+/) @s.scan(/(true|false)/) == 'true' end def get_name @s.skip(/\s+/) @s.scan(/[^\s]+/) end def get_string @s.skip(/\s+/) @s.scan(/.*/) end def get_fontmetrics @version = get_number loop do kw=get_keyword STDERR.puts "KW: " + kw if @verbose case kw when "FontName" @fontname=get_string when "FamilyName" @familyname = get_string when "FullName" @fullname = get_string when "EncodingScheme" @encodingscheme = get_string when "ItalicAngle" @italicangle = get_number when "IsFixedPitch" @isfixedpitch = get_boolean when "Weight" @weight = get_string when "XHeight" @xheight= get_number when "Comment" @comment << get_string << "\n" when "FontBBox" @fontbbox = [get_number,get_number, get_number, get_number] when "Version" @version = get_string when "Notice" @notice = get_string when "MappingScheme" @mappingscheme = get_integer when "EscChar" @escchar = get_integer when "CharacterSet" @characterset = get_string when "Characters" @characters = get_integer when "IsBaseFont" @isbasefont = get_boolean when "VVector" @vvector = [get_number,get_number] when "IsFixedV" @isfixedv = get_boolean when "CapHeight" @capheight = get_number when "Ascender" @ascender = get_number when "Descender" @descender = get_number when "UnderlinePosition" @underlineposition = get_number when "UnderlineThickness" @underlinethickness = get_number when "StartDirection" get_direction when "StartCharMetrics" get_charmetrics when "StartKernData" get_kerndata when "StartComposites" get_composites when "EndFontMetrics" break end end end def get_direction # ignored end def get_charmetrics @count_charmetrics = get_integer @count_charmetrics_encoded = 0 @count_charmetrics_unencoded = 0 loop do @s.skip_until(/\n/) nextstring = @s.scan_until(/(?:StopCharMetrics|.*)/) return if nextstring=="EndCharMetrics" a=nextstring.split(';') # ["C 32 ", " WX 250 ", " N space ", " B 125 0 125 0 "] a.collect! { |elt| elt.strip.split(/ /,2) } # [["C", "32"], ["WX", "250"], ["N", "space"], ["B", "125 0 125 0"]] char=new_glyph a.each { |elt| key,value = elt case key when "N" char.name=value when "B" #special treatment for bounding box char.b = value.split.collect { |e| e.to_i } char.llx = char.llx char.urx = char.urx # We need to avoid negative heights or depths. They break # accents in math mode, among other things. char.lly = 0 if char.lly > 0 char.ury = 0 if char.ury < 0 when "C" char.c = value.to_i when "CH" # hex: '<20>' -> '0x20' -> .to_i -> 32 char.c = value.sub(//,'').to_i(16) when "WX" char.wx = value.to_i # for "L", check /var/www/mirror/system/tex/texmf-local/fonts/afm/jmn/hans/hans.afm when "L", nil #ignore else char.send((key.downcase + "=").to_sym,value.to_i) end } @chars[char.name]=char # update information about encoded/unencoded if char.c > -1 @count_charmetrics_encoded += 1 else @count_charmetrics_unencoded += 1 end end raise "never reached" end def get_kerndata loop do kw = get_keyword STDERR.puts "kw=" + kw if @verbose case kw when "EndKernData" return when "StartKernPairs" get_kernpairs when "StartTrackKern" # TrackKern get_trackkern else # KernPairs0 # KernPairs1 raise "not implemented" end end raise "never reached" end def get_composites count = get_integer loop do kw = get_keyword STDERR.puts "get_composites keyword = '" + kw + "'" if @verbose case kw when "CC" get_composite when "EndComposites" return else STDERR.puts "next to read = " + @s.string[@s.pos,40] raise "AFM error" end end raise "never reached" end def get_composite glyphname = get_name count = get_integer @s.skip_until(/;\s+/) count.times do nextstring = get_name raise "AFM Error" unless nextstring == "PCC" [get_number,get_number] @s.skip_until(/;/) end end def get_trackkern count = get_integer loop do case get_keyword when "EndTrackKern" return when "TrackKern" # TrackKern degree min-ptsize min-kern max-ptsize max-kern [get_integer,get_number,get_number,get_number,get_number] else raise "afm error" end end raise "never reached" end def get_kernpairs count = get_integer loop do case get_keyword when "KPX" # y is 0 name=get_name # if @info['chars'][name] if @chars[name] # array is [x,y] kerning destname,num=get_name,get_number # somethimes something stupid like # KPX .notdef y -26 # KPX A .notdef -43 # is in the afm data... :-( -> reject those entries # if @info['chars'][destname] if @chars[destname] @chars[name].kern_data[destname] = [num,0] else STDERR.puts "info: unused kern data for " + name if @verbose end else # ignore this entry, print a message STDERR.puts "info: unused kern data for " + name if @verbose @something_strange=true [get_name,get_number] # ignored end when "EndKernPairs" return else STDERR.puts @s.pos raise "not implmented" end end raise "never reached" end end end end