diff options
Diffstat (limited to 'support/rfil/lib/rfil/font')
-rw-r--r-- | support/rfil/lib/rfil/font/afm.rb | 402 | ||||
-rw-r--r-- | support/rfil/lib/rfil/font/glyph.rb | 198 | ||||
-rw-r--r-- | support/rfil/lib/rfil/font/metric.rb | 129 | ||||
-rw-r--r-- | support/rfil/lib/rfil/font/truetype.rb | 29 |
4 files changed, 758 insertions, 0 deletions
diff --git a/support/rfil/lib/rfil/font/afm.rb b/support/rfil/lib/rfil/font/afm.rb new file mode 100644 index 0000000000..3a087d36c8 --- /dev/null +++ b/support/rfil/lib/rfil/font/afm.rb @@ -0,0 +1,402 @@ +# 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 <tt>:verbose</tt> 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 <tt>:verbose</tt> (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(/</,'0x').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 diff --git a/support/rfil/lib/rfil/font/glyph.rb b/support/rfil/lib/rfil/font/glyph.rb new file mode 100644 index 0000000000..91b1d69422 --- /dev/null +++ b/support/rfil/lib/rfil/font/glyph.rb @@ -0,0 +1,198 @@ + +module RFIL + module Font + class Glyph + + # to make Rdoc and Ruby happy: [ruby-talk:147778] + def self.documented_as_accessor(*args) #:nodoc: + end + + # Glyphname + attr_accessor :name + + # Advance with + attr_accessor :wx + + # Standard code slot (0-255 or -1 for unencoded) + attr_accessor :c + + # bounding box (llx, lly, urx, ury). Array of size 4. + # You should use the methods llx, lly, urx, ury to access the + # bounding box. + attr_accessor :b + + # Kern_data (Hash). The key is the glyph name, the entries are + # _[x,y]_ arrays. For ltr and rtl typesetting only the x entry + # should be interesting. This is raw information from the font + # metric file. Does not change when efactor et al. are set in any + # way. + attr_accessor :kern_data + + # Information about ligatures - unknown datatype yet + attr_accessor :lig_data + + # Composite characters. Array [['glyph1',xshift,yshift],...] + attr_accessor :pcc_data + + # Upper right x value of glyph. + documented_as_accessor :urx + + # Upper right y value of glyph. + documented_as_accessor :ury + + # Lower left x value of glyph. + documented_as_accessor :llx + + # Lower left y value of glyph. + documented_as_accessor :lly + + # the name of the uppercase glyph (nil if there is no uppercase glyph) + attr_accessor :uc + + # the name of the lowercase glyph (nil if there is no lowercase glyph) + attr_accessor :lc + + # Optional argument sets the name of the glyph. + def initialize (glyphname=nil) + @name=glyphname + @lig_data={} + @kern_data={} + @wx=0 + @b=[0,0,0,0] + @efactor=1.0 + @slant=0.0 + end + + # Lower left x position of glyph. + def llx # :nodoc: + @b[0] + end + def llx=(value) # :nodoc: + @b[0]=value + end + + # Lower left y position of glyph. + def lly # :nodoc: + @b[1] + end + def lly=(value) # :nodoc: + @b[1]=value + end + # Upper right x position of glyph. + def urx # :nodoc: + @b[2] + end + def urx=(value) # :nodoc: + @b[2]=value + end + + # Upper right y position of glyph. + def ury # :nodoc: + @b[3] + end + def ury=(value) # :nodoc: + @b[3]=value + end + + # Return height of the char used for tfm file. + def charht + ury + end + + # Return width of the char used for tfm file. + def charwd + wx + end + + # Return depth of the char. + def chardp + lly >= 0 ? 0 : -lly + end + + # Return italic correction of the char. + def charic + (urx - wx) > 0 ? (urx - wx) : 0 + end + # Return an array with all kerning information (x-direction only) + # of this glyph. Kerning information is an Array where first + # element is the destchar, the second element is the kerning amount. + def kerns_x + ret=[] + @kern_data.each { |destchar,kern| + ret.push([destchar,kern[0]]) + } + ret + end + + # Return an array with all ligature information (LIG objects) of + # this glyph. + def ligs + ret=[] + @lig_data.each { |destchar,lig| + ret.push(lig) + } + ret + end + + # Return true if this char has ligature or kerning information. If + # glyphindex is supplied, only return true if relevant. This means + # that the second parameter of a kerning information or the second + # parameter and the result of a ligature information must be in + # the glyphindex. glyphindex must respond to <em>include?</em>. + def has_ligkern?(glyphindex=nil) + if glyphindex and not glyphindex.respond_to? :include? + raise ArgumentError, "glyphindex does not respod to include?" + end + return false if (lig_data == {} and kern_data=={}) + # this one is easy, just look at lig_data and kern_data + # more complicated, we have to take glyphindex into account + if glyphindex + return false unless glyphindex.include? self.name + # right kerningpair not in glyphindex? -> false + # right lig not in glyphindex? -> false + # result lig not in glyphindex? -> false + if lig_data + lig_data.each { |otherchar,lig| + if (glyphindex.include?(lig.right) and glyphindex.include?(lig.result)) + return true + end + } + end + if kern_data + kern_data.each { |otherchar,krn| + return true if glyphindex.include?(otherchar) + } + end + return false + else + # no glyphindex + return true + end + raise "never reached" + end # has_ligkern? + + # Return true if glyph is an uppercase char, such as AE. + def is_uppercase? + return @lc != nil + end + + # Return true if glyph is a lowercase char, such as germandbls, + # but not hyphen. + def is_lowercase? + return @uc != nil + end + + # Return the uppercase variant of the glyph. Undefined behaviour if + # glyph cannot be uppercased. + def capitalize + @uc + end + + # Return the lowercase variant of the glyph. Undefined behaviour if + # glyph cannot be lowercased. + def downcase + @lc + end + end + end +end diff --git a/support/rfil/lib/rfil/font/metric.rb b/support/rfil/lib/rfil/font/metric.rb new file mode 100644 index 0000000000..fb9afb66ab --- /dev/null +++ b/support/rfil/lib/rfil/font/metric.rb @@ -0,0 +1,129 @@ +# font/metric.rb - superclass for different font metric formats +# Last Change: Tue May 16 18:08:49 2006 + +require 'rfil/font/glyph' +#require 'font/afm' +#require 'font/truetype' + +module RFIL + module Font + # FontMetric is the superclass for font metrics. All information that + # is not specific to a certain kind of file format is accessible via + # this class. + + class Metric + # to make Rdoc and Ruby happy: [ruby-talk:147778] + def self.documented_as_accessor(*args) # :nodoc: + end + def self.documented_as_reader(*args) # :nodoc: + end + + # :type1, :truetype + attr_accessor :outlinetype + + # Hash of glyphs in the font. + attr_accessor :chars + + # The filename of the just read metric file. Does not contain the + # path. To set, change the pathname + documented_as_reader :filename + + # Absolute pathname of the metric file. Not checked when set. + attr_accessor :pathname + + # File name of the font containing the outlines (the file that needs + # to be put into the pdf-file). The .tt or the .pfb file. If unset + # use the value of filename, but changed to the correct extension in + # case of Type 1 fonts. + documented_as_accessor :fontfilename + + # Some unique name of the font. Use the filename of the font or a + # name after the KB naming schema. Do not add an extension such as + # afm or tt. + attr_accessor :name + + # family name of the font + attr_accessor :familyname + + # xheight in 1/1000 of an em + attr_accessor :xheight + + attr_accessor :weight + + # natural width of a space + documented_as_reader :space + + attr_accessor :italicangle + + # True if the font is a monospaced font (courier for example). + attr_accessor :isfixedpitch + + # The official name of the font as supplied by the vendor. Written + # as FontName in the afm file. + attr_accessor :fontname + + # The full name of the font, whatever this means. Written as + # FullName in the afm file. + attr_accessor :fullname + + # Class for new glyphs. Default is Glyph + attr_accessor :glyph_class + + def Metric.read(filename,options={}) + case File.extname(filename) + when ".afm" + require 'rfil/font/afm' + return AFM.new(options).read(filename) + when ".ttf" + require 'rfil/font/truetype' + return TrueType.new(options).read(filename) + else + raise ArgumentError, "Unknown filetype: #{File.basename(filename)}" + end + end + + def initialize + @chars=Hash.new + @xheight=nil + @glyph_class=Glyph + @outlinetype=nil + @info={} + @fontfilename=nil + @efactor=1.0 + @slantfactor=0.0 + @pathname=nil + end + + # Factory for new glyphs. Return new instance of glyph_class (see + # Attributes). + def new_glyph + @glyph_class.new + end + + + def space # :nodoc: + chars['space'].wx + end + + def filename # :nodoc: + File.basename(@pathname) + end + + def fontfilename= (obj) # :nodoc: + @fontfilename=obj + end + + # This one is documented in the 'attributes' section. If the global + # variable is unset, just use @filename, perhaps change afm to pfb + def fontfilename # :nodoc: + return @fontfilename if @fontfilename + case filename + when /\.afm$/ + return filename.chomp(".afm") + ".pfb" + when /\.tt$/ + return filename + end + end + end + end +end diff --git a/support/rfil/lib/rfil/font/truetype.rb b/support/rfil/lib/rfil/font/truetype.rb new file mode 100644 index 0000000000..6c26287eb4 --- /dev/null +++ b/support/rfil/lib/rfil/font/truetype.rb @@ -0,0 +1,29 @@ +# truetype.rb -- read truetype font metrics +#-- +# Last Change: Tue May 16 17:16:56 2006 +#++ + +require 'rfil/font/afm' + +module RFIL + module Font + # Read TrueType fonts. Use like the AFM class. + class TrueType < AFM + def initialize(options={}) + super + @outlinetype=:truetype + end + def read(filename) + @filename=File.basename(filename) + @fontfilename=filename + @name=@filename.chomp(".ttf") + self.pathname=Pathname.new(filename).realpath.to_s + a=`ttf2afm #{@fontfilename}` + parse(a) + # ttf2afm does not give an xheight!? + @xheight=@chars['x'].ury unless @xheight + self + end + end + end +end |