# font.rb - Implements Font. See that class for documentaton. #-- # Last Change: Tue May 16 19:21:33 2006 #++ require 'set' require 'rfil/helper' require 'rfil/font/afm' require 'rfil/font/truetype' require 'rfil/tex/enc' require 'rfil/tex/kpathsea' require 'rfil/tex/tfm' require 'rfil/tex/vf' require 'rfil/rfi' module RFIL # :nodoc: class RFI # :nodoc: # Main class to manipulate and combine font metrics. This is mostly a # convenience class, if you don't want to do the boring stuff # yourself. You can 'load' a font, manipulate the data and create a tfm # and vf file. It is used in conjunction with FontCollection, a class # that contains several Font objects (perhaps a font family). # The Font class relys on TFM/VF to write out the tfm and vf files, on the # subclasses of RFI, especially on RFI::Glyphlist that knows about a # lot of things about the char metrics, ENC for handling the encoding # information and, of course, FontMetric and its subclasses to read a # font. class Font include TeX def self.documented_as_accessor(*args) # :nodoc: end # lookup_meth def self.lookup_meth(*args) args.each do |arg| define_method(arg) do if @fontcollection @fontcollection.instance_variable_get("@#{arg}") else instance_variable_get("@#{arg}") end end define_method("#{arg}=") do |v| instance_variable_set("@#{arg}", v) end end end lookup_meth :style, :write_vf include Helper RULE=[:setrule, 0.4, 0.4] # The encoding that the PDF/PS expects (what is put before # "ReEncodeFont" in the mapfile). If not set, use the setting from # the fontcollection. You can specify at most one encoding. If you # set it to an array of encodings, only the first item in the array # will be used. The assignment to _mapenc_ can be an Enc object or a # string that is a filename of the encoding. If unset, use all the # encoding mentioned in #texenc. In this case, a one to one mapping # will be done: 8r -> 8r, t1 -> t1 etc. (like the -T switch in # afm2tfm). documented_as_accessor :mapenc # Array of encodings that TeX spits out. If it is not set, take # the settings from the fontcollection. documented_as_accessor :texenc # The fontmetric of the default font attr_accessor :defaultfm # extend font with this factor attr_accessor :efactor # slantfactor attr_accessor :slant # Don't write out virtual fonts if write_vf is set to false here or # in the fontcollection. documented_as_accessor :write_vf # sans, roman, typewriter documented_as_accessor :style # :regular, :bold, :black, :light attr_accessor :weight # :regular, :italic, :slanted, :smallcaps attr_accessor :variant # :dryrun, :verbose, see also fontcollection attr_accessor :options # all the loaded attr_accessor :variants # If fontcollection is supplied, we are now part as the # fontcollection. You can set mapenc and texenc in the fontcollection # and don't bother about it here. Settings in a Font object will # override settings in the fontcollection. def initialize (fontcollection=nil) # we are part of a fontcollection @fontcollection=fontcollection # @defaultfm=FontMetric.new @weight=:regular @variant=:regular @defaultfm=nil @efactor=1.0 @slant=0.0 @capheight=nil @write_vf=true @texenc=nil @mapenc=nil @variants=[] @style=nil @dirs={} @origsuffix="-orig" @kpse=Kpathsea.new if fontcollection unless @fontcollection.respond_to?(:register_font) raise ArgumentError, "parameter does not look like a fontcollection" end @colnum=@fontcollection.register_font(self) else # the default dirs set_dirs(Dir.getwd) end @options=Options.new(fontcollection) end # hook run after font has been loaded by load_variant def after_load_hook end # Read a font(metric file). Return a number that identifies the font. # The first font read is the default font. def load_variant(fontname) fm=nil if fontname.instance_of? String if File.exists?(fontname) case File.extname(fontname) when ".afm" fm=RFIL::Font::AFM.new when ".ttf" fm=RFIL::Font::TrueType.new else raise ArgumentError, "Unknown filetype: #{File.basename(fontname)}" end else # let us guess the inputfile %w( .afm .ttf ).each { |ext| if File.exists?(fontname+ext) fontname += ext case ext when ".afm" fm=RFIL::Font::AFM.new when ".ttf" fm=RFIL::Font::TrueType.new end break end } end raise Errno::ENOENT,"Font not found: #{fontname}" unless fm # We need more TeX-specific classes: fm.glyph_class=RFIL::RFI::Char # fm.glyph_class=::Font::Glyph fm.chars=RFIL::RFI::Glyphlist.new fm.read(fontname) raise ScriptError, "Fontname is not set" unless fm.name elsif fontname.respond_to? :charwd # some kind of font metric fm=fontname end class << fm # scalefactor of font (1=don't scale) attr_accessor :fontat # auxiliary attribute to store the name of the 'original' font attr_accessor :mapto end @variants.push(fm) fontnumber=@variants.size - 1 # the first font loaded is the default font if fontnumber == 0 @defaultfm = fm end fm.chars.each { |name,chardata| chardata.fontnumber=fontnumber } fm.chars.fix_height(fm.xheight) fm.fontat=1 # default scale factor after_load_hook fontnumber end # load_variant # change the metrics (and glyphs) of the default font so that # uppercase variants are mapped onto the lowercase variants. def fake_caps(fontnumber,capheight) raise ScriptError, "no font loaded" unless @defaultfm # first, make a list of uppercase and lowercase glyphs @defaultfm.chars.update_uc_lc_list @capheight=capheight v=@variants[fontnumber] v.fontat=capheight v.chars.fake_caps(capheight) end # Return tfm object for that font. _enc_ is the encoding of the # tfm file, which must be an ENC object. Ligature and kerning # information is put into the tfm file unless :noligs is # set to true in the opts. def to_tfm(enc,opts={}) tfm=TFM.new tfm.fontfamily=@defaultfm.familyname tfm.codingscheme=enc.encname tfm.designsize=10.0 tfm.params[1]=(@slant - @efactor * Math::tan(@defaultfm.italicangle * Math::PI / 180.0)) / 1000.0 tfm.params[2]=(transform(@defaultfm.space,0)) / 1000.0 tfm.params[3]=(@defaultfm.isfixedpitch ? 0 : transform(0.3,0)) tfm.params[4]=(@defaultfm.isfixedpitch ? 0 : transform(0.1,0)) tfm.params[5]=@defaultfm.xheight / 1000.0 tfm.params[6]=transform(1.0,0) charhash=enc.glyph_index.dup enc.each_with_index{ |char,i| next if char==".notdef" thischar=@defaultfm.chars[char] next unless thischar # ignore those chars we have already encountered next unless charhash.has_key?(char) thischar.efactor=@efactor thischar.slant=@slant c={} charhash[char].each { |slot| tfm.chars[slot]=c } charhash.delete(char) [:charwd, :charht, :chardp, :charic].each { |sym| c[sym]=thischar.send(sym) / 1000.0 } } if opts[:noligs] != true tfm_lig(tfm,enc) end return tfm end # Return vf object for that font. _mapenc_l_ and _texenc_ must be an # ENC object. _mapenc_l_ is the destination encoding (of the fonts # in the mapfile) and _texenc_ is is the encoding of the resulting # tfm file. They may be the same. def to_vf(mapenc_l,texenc) raise ArgumentError, "mapenc must be an ENC object" unless mapenc_l.respond_to? :encname raise ArgumentError, "texenc must be an ENC object" unless texenc.respond_to? :encname vf=VF.new vf.vtitle="Installed with rfi library" vf.fontfamily=@defaultfm.familyname vf.codingscheme= if mapenc_l.encname != texenc.encname mapenc_l.encname + " + " + texenc.encname else mapenc_l.encname end vf.designsize=10.0 fm=@defaultfm vf.params[1]=(@slant - @efactor * Math::tan(@defaultfm.italicangle * Math::PI / 180.0)) / 1000.0 vf.params[2]=(transform(@defaultfm.space,0)) / 1000.0 vf.params[3]=(@defaultfm.isfixedpitch ? 0 : transform(0.3,0)) vf.params[4]=(@defaultfm.isfixedpitch ? 0 : transform(0.1,0)) vf.params[5]=@defaultfm.xheight / 1000.0 vf.params[6]=transform(1,0) vf.params[7]==fm.isfixedpitch ? fm.space : transform(0.111,0) # mapfont find_used_fonts.each_with_index { |fontnumber,i| fl=vf.fontlist[fontnumber]={} tfm=fl[:tfm]=TFM.new tfm.tfmpathname=map_fontname(mapenc_l,fontnumber) fl[:scale]=@variants[fontnumber].fontat } charhash=texenc.glyph_index.dup texenc.each_with_index { |char,i| next if char==".notdef" thischar=@defaultfm.chars[char] next unless thischar # ignore those chars we have already encountered next unless charhash.has_key?(char) thischar.efactor=@efactor thischar.slant=@slant c={} charhash[char].each { |slot| vf.chars[slot]=c } charhash.delete(char) c[:dvi]=dvi=[] if thischar.fontnumber > 0 dvi << [:selectfont,thischar.fontnumber] end if thischar.pcc_data thischar.pcc_data.each { |pcc| if mapenc_l.glyph_index[pcc[0]] dvi << [:setchar,mapenc_l.glyph_index[pcc[0]].min] else dvi << RULE end } elsif thischar.mapto if mapenc_l.glyph_index[thischar.mapto] if mapenc_l.glyph_index[thischar.mapto] dvi << [:setchar, mapenc_l.glyph_index[thischar.mapto].min] else dvi << RULE end else dvi << [:special, "unencoded glyph '#{char}'"] dvi << RULE end elsif mapenc_l.glyph_index[char] dvi << [:setchar, mapenc_l.glyph_index[char].min] else dvi << RULE end [:charwd, :charht, :chardp, :charic].each { |sym| c[sym]=thischar.send(sym) / 1000.0 } } tfm_lig(vf,texenc) return vf end # Todo: document and test! def apply_ligkern_instructions(what) @defaultfm.chars.apply_ligkern_instructions(what) end # Return a string or an array of strings that should be put in a mapfile. def maplines() # "normally" (afm2tfm) # savorg__ Savoy-Regular " mapenc ReEncodeFont " 0 unless te.filename == "8a.enc" str << " <#{te.filename}" end str << " <#{@variants[f].fontfilename}" str << "\n" ret.push str } } # FIXME: remove duplicate lines in a more sensible way # no fontname (first entry) should appear twice! ret.uniq end # Creates all the necessary files to use the font. This is mainly a # shortcut if you are too lazy to program. _opts_: # [:dryrun] true/false # [:verbose] true/false # [:mapfile] true/false def write_files(opts={}) tfmdir=get_dir(:tfm); ensure_dir(tfmdir) vfdir= get_dir(:vf) ; ensure_dir(vfdir) unless opts[:mapfile]==false mapdir=get_dir(:map); ensure_dir(mapdir) end encodings=Set.new texenc.each { |te| encodings.add mapenc ? mapenc : te } encodings.each { |enc| find_used_fonts.each { |var| tfmfilename=File.join(tfmdir,map_fontname(enc,var) + ".tfm") if options[:verbose]==true puts "tfm: writing tfm: #{tfmfilename}" end unless options[:dryrun]==true tfm=to_tfm(enc) tfm.tfmpathname=tfmfilename tfm.save(true) end } } if write_vf encodings=Set.new texenc.each { |te| encodings.add mapenc ? mapenc : te } texenc.each { |te| outenc = mapenc ? mapenc : te vffilename= File.join(vfdir, tex_fontname(te) + ".vf") tfmfilename=File.join(tfmdir,tex_fontname(te) + ".tfm") if options[:verbose]==true puts "vf: writing tfm: #{tfmfilename}" puts "vf: writing vf: #{vffilename}" end unless options[:dryrun]==true vf=to_vf(outenc,te) vf.tfmpathname=tfmfilename vf.vfpathname=vffilename vf.save(true) end } end unless opts[:mapfile]==false # mapfile if options[:verbose]==true puts "writing #{mapfilename}" end unless options[:dryrun]==true File.open(mapfilename,"w") { |f| f << maplines } end end end # Return a directory where files of type _type_ will be placed in. # Default to current working directory. def get_dir(type) if @dirs.has_key?(type) @dirs[type] elsif @fontcollection and @fontcollection.dirs.has_key?(type) @fontcollection.dirs[type] else Dir.getwd end end def mapenc # :nodoc: return nil if @mapenc == :none if @mapenc==nil and @fontcollection @fontcollection.mapenc else @mapenc end end def mapenc=(enc) # :nodoc: set_mapenc(enc) end def texenc # :nodoc: if @texenc @texenc else # @texenc not set if @fontcollection @fontcollection.texenc else ret=nil @kpse.open_file("8a.enc","enc") { |f| ret = [ENC.new(f)] } # puts "returning #{ret}" return ret end end end def texenc=(enc) # :nodoc: @texenc=[] if enc set_encarray(enc,@texenc) end end # Return the full path to the mapfile. def mapfilename File.join(get_dir(:map),@defaultfm.name + ".map") end # Copy glyphs from one font to the default font. _fontnumber_ is the # number that is returned from load_variant, _glyphlist_ is whatever # you want to copy. Overwrites existing chars. _opts_ is one of: # [:ligkern] copy the ligature and kerning information with the glyphs stated in glyphlist. This will remove all related existing ligature and kerning information the default font. # *needs testing* def copy(fontnumber,glyphlist,opts={}) tocopy=[] case glyphlist when Symbol tocopy=@defaultfm.chars.get_glyphlist(glyphlist) when Array tocopy=glyphlist end tocopy.each { |glyphname| @defaultfm.chars[glyphname]=@variants[fontnumber].chars[glyphname] @defaultfm.chars[glyphname].fontnumber=fontnumber } if opts[:ligkern]==true # assume: copying lowercase letters. # we need to copy *all* lowercase -> * data and replace all # we need to remove all uppercase -> lowercase data first # we need to copy all uppercase -> lowercase data @variants[fontnumber].chars.each { |glyphname,data| if tocopy.member?(glyphname) #puts "font#copy: using kern_data for #{glyphname}" @defaultfm.chars[glyphname].kern_data=data.kern_data.dup else # delete all references to the 'tocopy' @defaultfm.chars[glyphname].kern_data.each { |destchar,kern| if tocopy.member?(destchar) #puts "font#copy: removing kern_data for #{glyphname}->#{destchar}" @defaultfm.chars[glyphname].kern_data.delete(destchar) end } data.kern_data.each { |destchar,kern| if tocopy.member?(destchar) @defaultfm.chars[glyphname].kern_data[destchar]=kern end } end } end end # copy # Return an array with all used fontnumbers loaded with # load_variant. If, for example, fontnubmer 0 and 3 are used, # find_used_fonts would return [0,3]. def find_used_fonts fonts=Set.new @defaultfm.chars.each{ |glyph,data| fonts.add(data.fontnumber) } fonts.to_a.sort end # Return the name of the font in the mapline. If we don't write # virtual fonts, this is the name of the tfm file written. If we # write vf's, than this is the name used in the mapfont section of # the virtual font as well as the name of the tfm file, but both # with some marker that this font 'should' not be used directly. def map_fontname (texenc,varnumber=0,opts={}) mapenc_loc=mapenc suffix="" suffix << @origsuffix if write_vf if mapenc_loc # use the one in mapenc_loc construct_fontname(mapenc,varnumber) + suffix else construct_fontname(texenc,varnumber) + suffix end end # Return the name def tex_fontname (encoding) tf=construct_fontname(encoding) tf << "-capitalized-#{(@capheight*1000).round}" if @capheight tf end def guess_weight_variant fm=@defaultfm # fm[:smallcaps] = false # fm[:expert] = false [fm.fontname,fm.familyname,fm.weight].each { |fontinfo| case fontinfo when /italic/i @variant=:italic when /bold/i @weight=:bold when /smcaps/i @variant=:smallcaps # when /expert/i # f[:expert] = true # puts "expert" end } end ####### private ####### def tfm_lig(tfm,enc) charhash=enc.glyph_index.dup enc.each_with_index { |char,i| next if char==".notdef" thischar=@defaultfm.chars[char] next unless thischar # ignore those chars we have already encountered next unless charhash.has_key?(char) lk=[] thischar.lig_data.each_value { |lig| if (enc.glyph_index.has_key? lig.right) and (enc.glyph_index.has_key? lig.result) # lig is like "hyphen ..." but needs to be in a format like # "45 .." lk += lig.to_tfminstr(enc) end } thischar.kern_data.each { |dest,kern| if (enc.glyph_index.has_key? dest) enc.glyph_index[dest].each { |slot| lk << [:krn, slot,(kern[0]*@efactor)/1000.0] } end } next if lk.size==0 instrnum = tfm.lig_kern.size tfm.lig_kern << lk charhash[char].each { |slot| c=tfm.chars[slot] ||= {} c[:lig_kern]=instrnum } charhash.delete(char) } return tfm end def construct_fontname(encoding,varnumber=0) encodingname=if String === encoding encoding else if encoding.filename encoding.filename.chomp(".enc").downcase else encoding.encname end end fontname=@variants[varnumber].name # default tf=if encodingname == "8a" "#{fontname}" else "#{encodingname}-#{fontname}" end tf << "-slanted-#{(@slant*100).round}" if @slant != 0.0 tf << "-extended-#{(@efactor*100).round}" if @efactor != 1.0 tf end def transform (x,y) @efactor * x + @slant * y end end # class Font end # class RFI end #module RFIL