From e0c6872cf40896c7be36b11dcc744620f10adf1d Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Mon, 2 Sep 2019 13:46:59 +0900 Subject: Initial commit --- support/rfil/lib/rfil/font.rb | 720 ++++++++++++++++ support/rfil/lib/rfil/font/afm.rb | 402 +++++++++ support/rfil/lib/rfil/font/glyph.rb | 198 +++++ support/rfil/lib/rfil/font/metric.rb | 129 +++ support/rfil/lib/rfil/font/truetype.rb | 29 + support/rfil/lib/rfil/fontcollection.rb | 182 ++++ support/rfil/lib/rfil/helper.rb | 155 ++++ support/rfil/lib/rfil/rfi.rb | 472 +++++++++++ support/rfil/lib/rfil/rfi_plugin_context.rb | 90 ++ support/rfil/lib/rfil/rfi_plugin_latex.rb | 95 +++ support/rfil/lib/rfil/tex/enc.rb | 225 +++++ support/rfil/lib/rfil/tex/kpathsea.rb | 65 ++ support/rfil/lib/rfil/tex/tfm.rb | 1200 +++++++++++++++++++++++++++ support/rfil/lib/rfil/tex/vf.rb | 848 +++++++++++++++++++ 14 files changed, 4810 insertions(+) create mode 100644 support/rfil/lib/rfil/font.rb create mode 100644 support/rfil/lib/rfil/font/afm.rb create mode 100644 support/rfil/lib/rfil/font/glyph.rb create mode 100644 support/rfil/lib/rfil/font/metric.rb create mode 100644 support/rfil/lib/rfil/font/truetype.rb create mode 100644 support/rfil/lib/rfil/fontcollection.rb create mode 100644 support/rfil/lib/rfil/helper.rb create mode 100644 support/rfil/lib/rfil/rfi.rb create mode 100644 support/rfil/lib/rfil/rfi_plugin_context.rb create mode 100644 support/rfil/lib/rfil/rfi_plugin_latex.rb create mode 100644 support/rfil/lib/rfil/tex/enc.rb create mode 100644 support/rfil/lib/rfil/tex/kpathsea.rb create mode 100644 support/rfil/lib/rfil/tex/tfm.rb create mode 100644 support/rfil/lib/rfil/tex/vf.rb (limited to 'support/rfil/lib') diff --git a/support/rfil/lib/rfil/font.rb b/support/rfil/lib/rfil/font.rb new file mode 100644 index 0000000000..349021146a --- /dev/null +++ b/support/rfil/lib/rfil/font.rb @@ -0,0 +1,720 @@ +# 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 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 :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 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 include?. + 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 diff --git a/support/rfil/lib/rfil/fontcollection.rb b/support/rfil/lib/rfil/fontcollection.rb new file mode 100644 index 0000000000..e206653d65 --- /dev/null +++ b/support/rfil/lib/rfil/fontcollection.rb @@ -0,0 +1,182 @@ +#-- +# fontcollection.rb +# Last Change: Tue May 16 19:02:11 2006 +#++ + +require 'rfil/rfi' +require 'rfil/font' +require 'rfil/helper' + +module RFIL # :nodoc: + class RFI + # A set of fonts (regular,bold,italic). Used to write an fd-file for + # LaTeX or a typescript for ConTeXt. Register different fonts and set + # encodings, so you dont't have to specify them in each font. + + class FontCollection + include Helper + def self.documented_as_accessor(*args) # :nodoc: + end + def self.documented_as_reader(*args) # :nodoc: + end + + attr_accessor :vendor + + # attr_accessor :fontname + + # Name of the font (collection) + attr_accessor :name + + # One object or one or more objects in an array that describe the + # encoding of the postscript font. Object can either be a string + # that represents the filename of the encoding ("8r.enc", "8r") or + # an ENC object that already contains the encoding + documented_as_accessor :mapenc + + # One object or one ore more objects in an array that describe the + # encoding of the font TeX expects. Object can either be a string + # that represents the filename of the encoding ("8r.enc", "8r") or + # an ENC object that already contains the encoding + documented_as_accessor :texenc + + # hash of directories for writing files. Default to current working + # directory. The setting in the Font object overrides the setting here. + attr_accessor :dirs + + attr_accessor :fonts + + # sans, roman, typewriter + attr_accessor :style + + attr_accessor :write_vf + + attr_accessor :options + + # list of temps + documented_as_reader :plugins + + def initialize() + @kpse=Kpathsea.new + @basedir=nil + @name=nil + @texenc=nil + @mapenc=nil + @write_vf=true + @fonts=[] + @options={:verbose=>false,:dryrun=>false} + @style=nil + @dirs={} + @vendor=nil + @fontname=nil + set_dirs(Dir.getwd) + @plugins={} + # find temps-plugins + $:.each{ |d| + a=Dir.glob(d+"/rfil/rfi_plugin_*.rb") + a.each{ |f| + require f + } + } + ObjectSpace.each_object(Class){ |x| + if Plugin > x + t = x.new(self) + n = t.name + if @plugins.has_key?(n) + raise "Name already registered" + end + @plugins[n]=t + end + } + # done initializing plugins + end + + # Add a font to the collection. Return the number of the font. + def register_font (font) + unless font.respond_to?(:maplines) + raise ArgumentError, "parameter does not look like a font" + end + fontnumber=@fonts.size + @fonts << font + return fontnumber + end + + def run_plugin(name) + + raise "don't know plugin #{name}" unless @plugins.has_key?(name) + + # doc for run_plugin + # Return the contents of the file that should be used by the TeX + # macro package, i.e a typescript for ConTeXt or an fd-file for + # Context. Return value is an Array of Hashes. The Hash has three + # different keys: + # [:type] The type of the file, should be either :fd or :typescript. + # [:filename] the filename (without a path) of the file + # [:contents] the contents of the file. + + files=@plugins[name].run_plugin + if files.respond_to?(:each) + files.each { |fh| + dir=get_dir(fh[:type]) + filename=File.join(dir,fh[:filename]) + puts "writing file #{filename}" if @options[:verbose] + + unless @options[:dryrun] + ensure_dir(dir) + File.open(filename,"w") { |f| f << fh[:contents] } + end + } + end + end + def plugins #:nodoc: + @plugins.keys + end + def mapfile + mapfile=[] + @fonts.each {|font| + mapfile << font.maplines + } + mapfile.flatten + end + def mapenc # :nodoc: + return nil if @mapenc==:none + @mapenc + end + def mapenc= (enc) # :nodoc: + set_mapenc(enc) + end + def texenc # :nodoc: + if @texenc + @texenc + else + # @texenc not set + ret=nil + @kpse.open_file("8a.enc","enc") { |f| + ret = [ENC.new(f)] + } + return ret + end + end + def texenc= (enc) # :nodoc: + @texenc=[] + set_encarray(enc,@texenc) + end + def get_dir(type) + @dirs[type] + end + def write_files(options={}) + mapdir=get_dir(:map); ensure_dir(mapdir) + + @fonts.each {|font| + font.write_files(:mapfile => false) + mapfile << font.maplines + } + mapfilename=File.join(mapdir,@name+".map") + unless options[:dryrun]==true + File.open(mapfilename, "w") {|file| + file << mapfile.to_s + } + end + end + end + end # class RFI +end # module RFIL diff --git a/support/rfil/lib/rfil/helper.rb b/support/rfil/lib/rfil/helper.rb new file mode 100644 index 0000000000..fc8fb807a0 --- /dev/null +++ b/support/rfil/lib/rfil/helper.rb @@ -0,0 +1,155 @@ +#-- +# helper.rb Last Change: Fri Jul 1 14:29:09 2005 +#++ +# Helper module for Font and FontCollection. + +# Here we define methods that are used in Font and FontCollection. + +require 'fileutils' +require 'rfil/tex/kpathsea' + +module RFIL # :nodoc: + class RFI + module Helper + include TeX + def set_encarray(enc,where) #:nodoc: + if enc.instance_of?(ENC) + where.push(enc) + else + enc.each { |e| + if e.instance_of?(String) + e = e.chomp(".enc") + ".enc" + f=@kpse.open_file(e,"enc") + where.push(ENC.new(f)) + f.close + elsif e.instance_of?(ENC) + where.push(e) + end + } + end + end + def set_mapenc(enc) # :nodoc: + @mapenc=enc + + # nil/:none is perfectly valid + return if enc==nil or enc==:none + + if enc.instance_of?(ENC) + @mapenc = enc + else + enc.find { |e| + if e.instance_of?(String) + e = e.chomp(".enc") + ".enc" + @kpse.open_file(e,"enc") { |f| + @mapenc = ENC.new(f) + } + elsif e.instance_of?(ENC) + @mapenc = e + end + } + end + end + # call-seq: + # set_dirs(string) + # set_dirs(hash) + # + # Set the base dir of all font related files. Acts only as a storage + # for the information. The automatic font installation method in + # Font#write_files uses this information. When a _string_ is passed, + # use this as the base dir for all files, when a hash is given, the + # keys must be one of :afm, :tfm, + # :vf,:map, :pfb, :tt, :tds. + def set_dirs(arg) + # tds needs testing! set vendor/fonname before/after set_dirs + types=[:afm, :tfm, :vpl, :vf, :pl, :map, :type1,:truetype, :fd, :typescript] + if arg.instance_of? String + @basedir=arg + types.each { |sym| + @dirs[sym]=arg + } + elsif arg.instance_of? Hash + if arg[:base] + @basedir=arg[:base] + end + if arg[:tds]==true + suffix = if @vendor and @name + File.join(@vendor,@name) + else + "" + end + types.each { |t| + subdir= case t + when :afm + File.join("fonts/afm",suffix) + when :tfm + File.join("fonts/tfm",suffix) + when :vpl + File.join("fonts/source/vpl",suffix) + when :vf + File.join("fonts/vf",suffix) + when :pl + File.join("fonts/source/pl",suffix) + when :map + "fonts/map/dvips" + when :type1 + File.join("fonts/type1",suffix) + when :truetype + File.join("fonts/truetype",suffix) + when :fd + File.join("tex/latex",suffix) + when :typescript + File.join("tex/context",suffix) + else + raise "unknown type" + end + @dirs[t] = File.join(@basedir,subdir) + } + else + arg.each { |key,value| + @dirs[key] = value + } + end + end + end + def ensure_dir(dirname) # :nodoc: + if File.exists?(dirname) + if File.directory?(dirname) + # nothing to do + else + # exists, but not dir + raise "File exists, but is not a directory: #{dirname}" + end + else + # file does not exist, we can create a directory (hopefully) + + puts "Creating directory hierarchy #{dirname}" if @options[:verbose] + + unless @options[:dryrun] + FileUtils.mkdir_p(dirname) + end + end + end #ensure_dir + end # helper + # options is a hash, but with lookup to a fontcollection + class Options # :nodoc: + def initialize(fontcollection) + @fc=fontcollection + @options={} + end + def [](idx) + if @options[idx] + return @options[idx] + end + if @fc + @fc.options[idx] + else + nil + end + end + def []=(idx,obj) + @options[idx]=obj + end + + end + end # RFI +end diff --git a/support/rfil/lib/rfil/rfi.rb b/support/rfil/lib/rfil/rfi.rb new file mode 100644 index 0000000000..2ea1f50bd1 --- /dev/null +++ b/support/rfil/lib/rfil/rfi.rb @@ -0,0 +1,472 @@ +# rfi.rb -- general use classes +#-- +# Last Change: Tue May 16 19:21:51 2006 +#++ + +require 'rfil/font/glyph' + +module RFIL # :nodoc: + + # = RFI + # Everything that does not fit somewhere else gets included in the + # wrapper class RFI. + + # This class contains methods and other classes that are pretty much + # useless of their own or are accessed in different classes. + + class RFI # :nodoc: + + # Super class for plugins. Just subclass this Plugin, set the name + # when calling Plugin#new and implement run_plugin. + class Plugin + # Name of the plugin. A Symbol or a String. + attr_reader :name + + attr_reader :filetypes + + # Create a new plugin. _name_ is the name of the plugin (it must + # be a Symbol or a String). _filetypes_ is a list of symbols, of + # what files the plugin is capable of writing. + def initialize(name,*filetypes) + @name=name + @filetypes=filetypes + end + + # Return an Array of files that should be written on the user's + # harddrive. The Hash entries are + # [:type] Type of the file (:fd, :typescript etc.) + # [:filename] The filename (without a path) of the file. + # [:contents] The contents of the file. + def run_plugin + #dummy + end + end + + # Some instructions to remove kerning information from digits and + # other things. -> sort this out + STDLIGKERN = ["space l =: lslash", + "space L =: Lslash", "question quoteleft =: questiondown", + "exclam quoteleft =: exclamdown", "hyphen hyphen =: endash", + "endash hyphen =: emdash", "quoteleft quoteleft =: quotedblleft", + "quoteright quoteright =: quotedblright", "space {} *", "* {} space", + "zero {} *", "* {} zero", + "one {} *", "* {} one", + "two {} *", "* {} two", + "three {} *","* {} three", + "four {} *", "* {} four", + "five {} *", "* {} five", + "six {} *", "* {} six", + "seven {} *", "* {} seven", + "eight {} *", "* {} eight", + "nine {} *", "* {} nine", + "comma comma =: quotedblbase", + "less less =: guillemotleft", + "greater greater =: guillemotright"] + + # Metric information about a glyph. Does not contain the glyph + # (outlines) itself. + class Char < Font::Glyph + + # fontnumber is used in Font class + attr_accessor :fontnumber + + # If not nil, _mapto_ is the glyphname that should be used instead + # of the current one. + attr_accessor :mapto + + # Sets the extension factor. This is used by calculations of _wx_, + # _llx_ and _urx_. + attr_accessor :efactor + + # Sets the slant factor. This is used by calculations of _wx_, + # _llx_ and _urx_. + attr_accessor :slant + + def wx # :nodoc: + transform(@wx,0) + end + def wx=(obj) # :nodoc: + @wx=obj + end + # Lower left x position of glyph. + def llx # :nodoc: + transform(@b[0],b[1]) + end + + # Upper right x position of glyph. + def urx # :nodoc: + transform(@b[2],ury) + end + + private + + def transform (x,y) + (@efactor * x + @slant * y) + end + + end # class Char + + + # Represent the different ligatures possible in tfm. + class LIG + + @@encligops = ["=:", "|=:", "|=:>", "=:|", "=:|>", "|=:|", "|=:|>", "|=:|>>"] + @@vpligops = ["LIG", "/LIG", "/LIG>", "LIG/", "LIG/>", "/LIG/", + "/LIG/>", "/LIG/>>"] + @@symligops = [:lig, :"lig/", :"/lig", :"/lig/", :"lig/>", :"/lig>", :"/lig/>", :"/lig/>>"] + + # First glyph of a two glyph sequence before it is turned into a + # ligature. + attr_accessor :left + + # Second glyph of a two glyph sequence before it is turned into a + # ligature. + attr_accessor :right + + # The ligature that gets inserterd instead of the left and right glyph. + attr_accessor :result + + # [0, 1, 2, 3, 4, 5, 6, 7 ] + # + # [=: , |=: , |=:> , =:| , =:|>, |=:|, |=:|>, |=:|>> ] + # + # [LIG, /LIG, /LIG>, LIG/, LIG/>, /LIG/, /LIG/>, /LIG/>>] + attr_accessor :type + + + + # call-seq: + # new + # new(left,[right,[result,[type]]]) + # new(hash) + # new(otherlig) + # + # When called with left, right, result or type parameters, take + # these settings for the LIG object. When called with a hash as an + # argument, the keys should look like: :left,:right,:result,:type. + # When called with an existing LIG object, the values are taken + # from the old object. + def initialize(left=nil,right=nil,result=nil,type=nil) + case left + when Hash + [:left,:right,:result,:type].each { |sym| + if left.has_key?(sym) + self.send((sym.to_s+"=").to_sym,left[sym]) + end + } + when LIG + [:left,:right,:result,:type].each { |sym| + self.send((sym.to_s+"=").to_sym,left.send(sym)) + } + # warning!!!!! LIG accepts a String as well as Fixnum as + # parameters, this might have side effects!? + when Fixnum,nil,String + @left=left + @right=right + @result=result + @type=type + else + raise "unknown argument for new() in LIG: #{left}" + end + # test! + #unless @type.instance_of?(Fixnum) + # raise "type must be a fixnum" + #end + end + def ==(lig) + @left=lig.left and + @right=lig.right and + @result=lig.result and + @type=lig.type + end + + def to_pl(encoding) + encoding.glyph_index[@right].sort.collect { |rightslot| + left=encoding.glyph_index[@left].min + # right=encoding.glyph_index[@right].min + result=encoding.glyph_index[@result].min + type=@@vpligops[@type] + LIG.new(:left=>left, :right=>rightslot, :result=>result, :type=>type) + } + end + # Return an array that is suitable for tfm + def to_tfminstr(encoding) + encoding.glyph_index[@right].sort.collect { |rightslot| + left=encoding.glyph_index[@left].min + # right=encoding.glyph_index[@right].min + result=encoding.glyph_index[@result].min + type=@@symligops[@type] + [type,rightslot,result] + } + end + def inspect + "[#{@type.to_s.upcase} #@left + #@right => #@result]" + end + end + + require 'forwardable' + + # Stores information about kerning and ligature information. Allows + # deep copy of ligature and kerning information. Obsolete. Don't use. + class LigKern + extend Forwardable + # Optional parameter initializes the new LigKern object. + def initialize(h={}) + @h=h + end + + def_delegators(:@h, :each, :[], :[]=,:each_key,:has_key?) + + def initialize_copy(obj) # :nodoc: + tmp={} + if obj[:lig] + tmp[:lig]=Array.new + obj[:lig].each { |elt| + tmp[:lig].push(elt.dup) + } + end + if obj[:krn] + tmp[:krn]=Array.new + obj[:krn].each { |elt| + tmp[:krn].push(elt.dup) + } + end + if obj[:alias] + tmp[:alias]=obj[:alias].dup + end + @h=tmp + end + # Compare this object to another object of the same class. + def ==(obj) + return false unless obj.respond_to?(:each) + # the krn needs to be compared one by one, because they are floats + if obj.has_key?(:krn) + obj[:krn].each { |destchar,value| + return false unless @h[:krn].assoc(destchar) + return false if (value - @h[:krn].assoc(destchar)[1]).abs > 0.01 + } + end + obj.each { |key,value| + next if key==:krn + return false unless @h[key]==value + } + true + end + end + + + # The Glyphlist is a actually a Hash with some special methods + # attached. + class Glyphlist < Hash + @@encligops = ["=:", "|=:", "|=:>", "=:|", "=:|>", "|=:|", "|=:|>", "|=:|>>"] + @@vpligops = ["LIG", "/LIG", "/LIG>", "LIG/", "LIG/>", "/LIG/", + "/LIG/>", "/LIG/>>"] + + # Return an array with name of glyphs that are represented by the + # symbol _glyphlist_. + # These symbols are defined: :lowercase, :uppercase, :digits + def get_glyphlist(glyphlist) + ret=[] + unless glyphlist.instance_of? Symbol + raise ArgumentError, "glyphlist must be a symbol" + end + case glyphlist + when :lowercase + update_uc_lc_list + + self.each { |glyphname,char| + if char.uc != nil + ret.push glyphname + end + } + when :uppercase + update_uc_lc_list + + self.each { |glyphname,char| + if char.lc != nil + ret.push glyphname + end + } + when :digits + ret=%w(one two three four five six seven eight nine zero) + end + ret + end + + # instructions.each must yield string objects (i.e. an array of + # strings, an IO object, a single string, ...). Instruction is like: + # "space l =: lslash" or "two {} *" + def apply_ligkern_instructions (instructions) + instructions.each { |instr| + s = instr.split(' ') + + if @@encligops.member?(s[2]) # one of =:, |=: |=:> ... + if self[s[0]] + self[s[0]].lig_data[s[1]]=LIG.new(s[0],s[1],s[3],@@encligops.index(s[2])) + else + # puts "glyphlist#apply_ligkern_instructions: char not found: #{s[0]}" + end + elsif s[1] == "{}" + remove_kern(s[0],s[2]) + end + } + end + # _left_ and _right_ must be either a glyphname or a '*' + # (asterisk) which acts like a wildcard. So ('one','*') would + # remove all kerns of glyph 'one' where 'one' is the left glyph in + # a kerning pair. + def remove_kern(left,right) + raise ArgumentError, "Only one operand may be '*'" if left=='*' and right=='*' + if right == "*" + self[left].kern_data={} if self[left] + elsif left == "*" + if self[right] + self.each { |name,chardata| + chardata.kern_data.delete(right) + } + end + else + if self[right] and self[left] + self[left].kern_data.delete(right) + end + end + end + + # Update all glyph entries to see what the uppercase or the + # lowercase variants are. Warning!! Tcaron <-> tquoteright in + # non-unicode fonts. + def update_uc_lc_list + # we need this list only when faking small caps (which will, of + # course, never happen!) + + # make a list of all uppercase and lowercase glyphs. Be aware of + # ae<->AE, oe<->OE, germandbls<-->SS, dotlessi->I, dotlessj->J + # do the + # @upper_lower={} + # @lower_upper={} + self.each_key {|glyphname| + thischar=self[glyphname] + if glyphname =~ /^[a-z]/ + if glyphname =~ /^(a|o)e$/ and self[glyphname.upcase] + thischar.uc = glyphname.upcase + elsif glyphname =~ /^dotless(i|j)$/ + thischar.uc = glyphname[-1].chr.upcase + elsif self[glyphname.capitalize] + thischar.uc = glyphname.capitalize + end + else + if glyphname =~ /^(A|O)e$/ and self[glyphname.dowcase] + thischar.lc = glyphname.downcase + elsif self[glyphname.downcase] + thischar.lc = glyphname.downcase + end + end + } + if self['germandbls'] + self['germandbls'].uc='S' + end + end + + # Modify the charmetrics and the kerning/ligatures so that the + # lowercase chars are made from scaling uppercase chars. + def fake_caps (factor) + update_uc_lc_list + # we need to do the following + # 1. adapt kerning pairs + # 2. change font metrics (wd) + # 3. remove ligatures from sc + @fake_caps=true + @capheight=factor + self.each { |glyphname,char| + if char.is_lowercase? + # remove ligatures from sc + char.lig_data={} + char.kern_data={} + char.mapto=char.capitalize + self[char.uc].kern_data.each { |destglyph,kerndata| + unless self[destglyph].is_lowercase? + char.kern_data[destglyph.downcase]=[kerndata[0] * factor,0] + end + } + char.b = self[char.capitalize].b.clone + char.wx = self[char.capitalize].wx * @capheight + char.lly *= @capheight + char.urx *= @capheight + char.ury *= @capheight + else # char is something like Aring, semicolon, ... + # if destchar is uppercase letter (A, Aring, ...) + # 1. delete all kerns to lowercase letters (not e.g. semicolon) + # 2. duplicate all uc kerns, multiply by factor and insert this + # as lc kern + char.kern_data.delete_if { |destglyph,kerndata| + self[destglyph].is_lowercase? + } + + new_kern_data={} + char.kern_data.each { |destglyph,kerndata| + if self[destglyph].is_uppercase? + new_kern_data[self[destglyph].downcase]=[kerndata[0]*factor,kerndata[1]] + end + new_kern_data[destglyph]=kerndata + } + char.kern_data=new_kern_data + end + # 2. + } + if self['germandbls'] + s=self['S'] + d=self['germandbls'] + + d.b = s.b.dup + d.wx = s.wx * 2 * @capheight + d.urx += s.wx + + + d.kern_data={} + s.kern_data.each { |destglyph,kerndata| + unless self[destglyph].is_lowercase? + # we are looking at non-lowercase chars. These might be + # ones that are uppercase or are 'something else', e.g. + # hyphen... + # since we only replace the lc variants, keep the uc and + # others intact. + if self[destglyph].is_uppercase? + d.kern_data[self[destglyph].downcase]=[kerndata[0] * @capheight,0] + + else + d.kern_data[destglyph]=[kerndata[0] * @capheight,0] + + end + end + } + + d.pcc_data=[['S',0,0],['S',s.wx,0]] + d.lly *= @capheight + d.urx *= @capheight + + end + end # fake_caps + + def fix_height(xheight) + # this is what afm2tfm does. I am not sure if it is clever. + self.each { |name,data| + + # xheight <= 50 -> @chars[char].ury + # char.size > 1 -> @chars[char].ury + # char+accentname (ntilde, udieresis,...) exists? + # then calculate else @chars[char].ury + # calculate := ntilde.ury - tilde.ury + xheight + # same as texheight in afm2tfm source + unless name.size>1 or xheight < 50 + %w(acute tilde caron dieresis).each {|accent| + naccent=name + accent + next unless self[naccent] + data.ury = self[naccent].ury - self[accent].ury + xheight + break + } + end + } + end # fix_height + end # class Glyphlist + end # class RFI +end diff --git a/support/rfil/lib/rfil/rfi_plugin_context.rb b/support/rfil/lib/rfil/rfi_plugin_context.rb new file mode 100644 index 0000000000..08a904b3ef --- /dev/null +++ b/support/rfil/lib/rfil/rfi_plugin_context.rb @@ -0,0 +1,90 @@ +=begin rdoc +Plugin for RFIL to create a typescript usable for ConTeXt. +=end + +# :enddoc: + +class TypescriptWriterConTeXt < RFIL::RFI::Plugin + + def initialize(fontcollection) + @fc=fontcollection + super(:context,:typescript) + end + + STOPTYPESCRIPT="\\stoptypescript\n\n" + + def run_plugin + ret=[] + str="" + puts "running context plugin" if @fc.options[:verbose] + @fc.texenc.each { |e| + str << typescript(e) + str << "\n" + } + h={} + h[:type]=:typescript + h[:filename],h[:contents]=["type-#{@fc.name}.tex",str] + ret.push(h) + ret + end + + # Returns hash: Style, font + def find_fonts + ret={} + @fc.fonts.each { |font| + ret[""]=font if font.variant==:regular and font.weight==:regular +# ret[""]=font if font.variant==:regular and font.weight==:regular and font.style!=:sans + ret["Bold"]=font if font.variant==:regular and font.weight==:bold + ret["Italic"]=font if font.variant==:italic and font.weight==:regular + ret["Caps"]=font if font.variant==:smallcaps and font.weight==:regular + } + ret + end + def typescript(e) + contextenc=case e.encname + when "ECEncoding" + "ec" + when "TS1Encoding" + "ts1" + when "T1Encoding" + "tex256" + when "TeXBase1Encoding" + "8r" + else + raise "unknown context encoding: #{e.encname}" + end + # i know that this is crap, it's just a start + contextstyle=case @fc.style + when :sans + "Sans" + when :roman, :serif + "Serif" + when :typewriter + "Typewriter" + else + raise "unknown style found: #{@fc.style}" + end + tmp = "" + fontname=@fc.name + tmp << "\\starttypescript[#{@fc.style}][#{fontname}][name]\n" + find_fonts.sort{ |a,b| a[0] <=> b[0]}.each { |style,font| + tmp << "\\definefontsynonym [#{contextstyle}" + p style + tmp << "#{style}" if style.length > 0 + tmp << "] [#{fontname}" + tmp << "-#{style}" if style.length > 0 + tmp << "]\n" + } + tmp << STOPTYPESCRIPT + + tmp << "\\starttypescript[#{@fc.style}][#{fontname}][#{contextenc}]\n" + find_fonts.sort{ |a,b| a[0] <=> b[0]}.each { |style,font| + tmp << "\\definefontsynonym [#{fontname}" + tmp << "-#{style}" if style.length > 0 + tmp << "][#{font.tex_fontname(e)}]\n" + } + tmp << STOPTYPESCRIPT + + return tmp + end +end diff --git a/support/rfil/lib/rfil/rfi_plugin_latex.rb b/support/rfil/lib/rfil/rfi_plugin_latex.rb new file mode 100644 index 0000000000..86f2e9a1ad --- /dev/null +++ b/support/rfil/lib/rfil/rfi_plugin_latex.rb @@ -0,0 +1,95 @@ +=begin rdoc +Plugin for RFIL to create a fontdefinition file (.fd) for LaTeX +=end + +# :enddoc: + +class FDWriterLaTeX < RFIL::RFI::Plugin + + def initialize(fontcollection) + @fc=fontcollection + super(:latex,:sty) + end + + def run_plugin + ret=[] + @fc.texenc.each { |e| + h={} + h[:type]=:fd + h[:filename],h[:contents]=latex_fd(e) + ret.push(h) + } + ret + end + + + # example, should be an extra plugin + def latex_fd(e) + latexenc=case e.encname + when "ECEncoding","T1Encoding" + "T1" + when "TeXBase1Encoding" + "8r" + when "TS1Encoding" + "TS1" + when "OT2AdobeEncoding" + "OT2" + else + raise "unknown latex encoding: #{e.encname}" + end + filename="#{latexenc}#{@fc.name}.fd" + + fd="\\ProvidesFile{#{filename}} +\\DeclareFontFamily{#{latexenc}}{#{@fc.name}}{} +" + weight=[:m,:b,:bx] + variant=[:n,:sc,:sl,:it] + for i in 0..11 + w=weight[i/4] + v=variant[i % 4] + f=find_font(w,v) + if f + name = f.tex_fontname(e) + else + if i < 4 + name = "ssub * #{@fc.name}/m/n" + else + name = "ssub * #{@fc.name}/#{weight[i/4 - 1]}/#{v}" + end + end + +# [[:m,:n],[:m,:sc],[:m,:sl],[:m,:it], +# [:b,:n],[:b,:sc],[:b,:sl],[:b,:it], +# [:bx,:n],[:bx,:sc],[:bx,:sl],[:bx,:it]].each{ |w,v| +# f=find_font(w,v) + +# name = f ? f.tex_fontname(e) : "<->ssub * #{@fc.name}/m/n" + fd << "\\DeclareFontShape{#{latexenc}}{#{@fc.name}}{#{w}}{#{v}}{ + <-> #{name} +}{} +" + end + return [filename,fd] +end + def find_font(w,v) + weight={} + variant={} + weight[:m]=:regular + weight[:b]=:bold + variant[:n]=:regular + variant[:it]=:italic + variant[:sl]=:slanted + variant[:sc]=:smallcaps + + # w is one of :m, :b, :bx + # v is one of :n, :sc, :sl, :it + @fc.fonts.each { |font| + #p b[:weight]==weight[w] + if font.variant ==variant[v] and font.weight==weight[w] + return font + end + } + return nil + end + +end diff --git a/support/rfil/lib/rfil/tex/enc.rb b/support/rfil/lib/rfil/tex/enc.rb new file mode 100644 index 0000000000..d2bb21a620 --- /dev/null +++ b/support/rfil/lib/rfil/tex/enc.rb @@ -0,0 +1,225 @@ +#-- +# enc.rb - read and parse TeX's encoding files +# Last Change: Tue May 16 17:24:31 2006 +#++ +# See the class ENC for the api description. + +require 'strscan' +require 'set' +require 'forwardable' + +module RFIL + module TeX + + # = ENC -- Access encoding files + # + # == General information + # + # Read a TeX encoding vector (.enc-file) and associated + # ligkern instructions. The encoding slot are accessible via [] + # and []=, just like an Array. + # + # == Example usage + # + # === Read an encoding file + # filename = "/opt/tetex/3.0/texmf/fonts/enc/dvips/base/EC.enc" + # File.open(filename) { |encfile| + # enc=ENC.new(encfile) + # enc.encname # => "ECEncoding" + # enc # => ['grave','acute',...] + # enc.filename # => "EC.enc" + # enc.ligkern_instructions # => ["space l =: lslash","space L =: Lslash",... ] + # } + # === Create an encoding + # enc=ENC.new + # enc.encname="Exampleenc" + # enc[0]="grave" + # # all undefined slots are ".notdef" + # .... + # + # # write encoding to new.enc + # File.open("new.enc") do |f| + # f << enc.to_s + # end + # --- + # Remark: This interface is pretty much fixed. + #-- + # dont't subclass Array directly, it might be a bad idea. See for + # example [ruby-talk:147327] + #++ + + class ENC # < DelegateClass(Array) + def self.documented_as_accessor(*args) # :nodoc: + end + + extend Forwardable + def_delegators(:@encvector, :size, :[],:each, :each_with_index) + + # _encname_ is the PostScript name of the encoding vector. + attr_accessor :encname + + # ligkern_instructions is an array of strings (instructions) as + # found in the encoding file, such as: + # "quoteright quoteright =: quotedblright" + # "* {} space" + attr_accessor :ligkern_instructions + + # Hash: key is glyph name, value is a Set of indexes. + # Example: glyph_index['hyphen']=# in + # ec.enc. Automatically updated when changing the encoding + # vector via []=. + attr_reader :glyph_index + + # Filename of the encoding vector. Used for creating mapfile + # entries. Always ends with ".enc" if read (unless it is unset). + documented_as_accessor :filename + + # Optional enc is either a File object or a string with the contents + # of a file. If set, the object is initialized with the given + # encoding vector. + def initialize (enc=nil) + @glyph_index={} + @ligkern_instructions=[] + # File, Tempfile, IO respond to read + if enc + @encvector=[] + string = enc.respond_to?(:read) ? enc.read : enc + if enc.respond_to?(:path) + self.filename= enc.path + end + parse(string) + else + @encvector=Array.new(256,".notdef") + end + end + + def filename # :nodoc: + @filename + end + + def filename=(fn) # :nodoc: + @filename=File.basename(fn.chomp(".enc")+".enc") + end + + # Return true if the encoding name and the encoding Array are the + # same. If _obj_ is an Array, only compare the Array elements. + def ==(obj) + return false if obj==nil + if obj.instance_of?(ENC) + return false unless @encname==obj.encname + end + + return false unless obj.respond_to?(:[]) + 0.upto(255) { |i| + return false if @encvector[i]!=obj[i] + } + true + end + + # todo: document and test + def -(obj) + tmp=[] + for i in 0..255 + tmp[i]=obj[i] + end + @encvector - tmp + end + + # also updates the glyph_index + def []=(i,obj) # :nodoc: + if obj==nil and @encvector[i] != nil + @glyph_index.delete(@encvector[i]) + return obj + end + + @encvector[i]=obj + addtoindex(obj,i) + return obj + end + + # Return a string representation of the encoding that is compatible + # with dvips and alike. + def to_s + str = "" + @ligkern_instructions.each { |instr| + str << "% LIGKERN #{instr} ;\n" + } + str << "%\n" + str << "/#@encname [\n" + @encvector.each_with_index { |glyphname,i| + str << "% #{i}\n" if (i % 16 == 0) + str << " " unless (i % 8 == 0) + str << "/#{glyphname}" + str << "\n" if (i % 8 == 7) + } + str << "] def\n" + str + end + + ####### + private + ####### + + # creates the glyph_index from the encvector. Use this method after + # you made changes to the encvector. + def update_glyph_index + @encvector.each_with_index { |name,i| + next if name==".notdef" + addtoindex(name,i) + } + end + + # Adds position i to glyph_index for glyph _glyph_. + def addtoindex(glyph,i) + return if glyph==".notdef" + if @glyph_index[glyph] + @glyph_index[glyph].add i + else + @glyph_index[glyph]=Set.new().add(i) + end + end + + # return the next postscript element (e.g. /name or [ ) + def tok(s) + unless s.peek(1) == "/" + s.skip_until(/[^\/\[\]]+/) # not '/' '[' or ']' + end + s.scan(/(?:\/\.?\w+|\[|\])/) + end + + # fill Array with contents of string. + def parse(str) + count=0 + s=StringScanner.new(str) + ligkern="" + while s.skip_until(/^%\s+LIGKERN\s+/) + ligkern << s.scan_until(/$/) + end + ligkern.split(';').each { |instruction| + @ligkern_instructions.push instruction.strip + } + s.string=(str.gsub(/%.*/,'')) + t=tok(s) + @encname=t[1,t.length-1] + loop do + t = tok(s) + case t + when "[" + # ignore + when "]" + unless @encvector.size == 256 + raise "Unexpected size of encoding. It should contain 256 entries, but has #{@encvector.size} entries." + end + update_glyph_index + return + else + name = t[1,t.length-1] + @encvector.push(name) + end + end + # never reached + raise "Internal ENC error" + end + end #class Enc + end +end diff --git a/support/rfil/lib/rfil/tex/kpathsea.rb b/support/rfil/lib/rfil/tex/kpathsea.rb new file mode 100644 index 0000000000..d8bc3d4cb7 --- /dev/null +++ b/support/rfil/lib/rfil/tex/kpathsea.rb @@ -0,0 +1,65 @@ +#-- +# kpathsea.rb - libkpathsea access for ruby +# Last Change: Tue May 16 17:23:14 2006 +#++ + + +module RFIL + module TeX + + # Find TeX related files with help of the 'kpsewhich' program. + class Kpathsea + # _progname_ defaults to the name of the main Ruby script. + # _progname_ is used to find program specific files as in + # TEXINPUT.progname in the texmf.cnf. + def initialize (progname=File.basename($0)) + raise ArgumentError if progname.match(/('|")/) + @progname=progname + end + + def reset_program_name(suffix) + @progname=suffix + end + + # Return the complete path of the file _name_. _name_ must not + # contain single or double quotes. + def find_file(name,fmt="tex",mustexist=false) + raise ArgumentError if name.match(/('|")/) + raise ArgumentError if fmt.match(/('|")/) + runkpsewhich(name,fmt,mustexist) + end + + # Return a File object. Raise Errno::ENOENT if file is not found. If + # block is given, a File object is passed into the block and the + # file gets closed when leaving the block. It behaves exactly as + # the File.open method. + def open_file(name,fmt="tex") + loc=self.find_file(name,fmt) + raise Errno::ENOENT, "#{name}" unless loc + if block_given? + File.open(loc) { |file| + yield file + } + else + File.open(loc) + end + end + + private + + def runkpsewhich(name,fmt,mustexist) + fmt.untaint + name.untaint + @progname.untaint + # path or amok XXX + cmdline= "kpsewhich -progname=\"#{@progname}\" -format=\"#{fmt}\" #{name}" + # puts cmdline + lines = "" + IO.popen(cmdline) do |io| + lines = io.readlines + end + return $? == 0 ? lines.to_s.chomp.untaint : nil + end + end + end +end diff --git a/support/rfil/lib/rfil/tex/tfm.rb b/support/rfil/lib/rfil/tex/tfm.rb new file mode 100644 index 0000000000..22465fe92b --- /dev/null +++ b/support/rfil/lib/rfil/tex/tfm.rb @@ -0,0 +1,1200 @@ +# tfm.rb - Access information of a TeX font metric file. +#-- +# Last Change: Tue May 16 19:12:26 2006 +#++ + +module RFIL # :nodoc: + module TeX # :nodoc: + + # TFM (TeX font metric) reader/writer class + class TFM + class TFMReader + # reading a tfm file is about 10 times faster than doing + # `tftop xyz.pl` and using PL#parse. And only a bit slower than + # `tftop xyz.pl > /dev/null` alone. (1.3 secs. vs. 0.9 secs. - 10 times) + + # Output more information + attr_accessor :verbose + + LIGTAG=1 + STOPFLAG=128 + KERNFLAG=128 + LIGSIZE=5000 + class TFMError < Exception + end + + def initialize(tfmobject=nil) + # type of font: textfont (:vanilla), math symbols (:mathsy), math + # extension (:mathex) + @font_type=nil + + @perfect=true + + # this is where we store all our data + @tfm=if tfmobject + tfmobject + else + TFM.new + end + end # initialize + + # _tfmdata_ is a string with the contents of the tfm (binary) file. + def parse(tfmdata) + @tfmdata=tfmdata.unpack("C*") + @index=0 + + @lf=get_dbyte + @lh=get_dbyte + @bc=get_dbyte + @ec=get_dbyte + @nw=get_dbyte + @nh=get_dbyte + @nd=get_dbyte + @ni=get_dbyte + @nl=get_dbyte + @nk=get_dbyte + @ne=get_dbyte + @np=get_dbyte + + if @verbose + puts "lf=#{@lf}" + puts "lh=#{@lh}" + puts "bc=#{@bc}" + puts "ec=#{@ec}" + puts "nw=#{@nw}" + puts "nh=#{@nh}" + puts "nd=#{@nd}" + puts "ni=#{@ni}" + puts "nl=#{@nl}" + puts "nk=#{@nk}" + puts "ne=#{@ne}" + puts "np=#{@np}" + end + raise TFMError, "The following condition is not true: bc-1 <= ec and ec <= 255" unless @bc-1 <= @ec and @ec <= 255 + raise TFMError, "The following condition is not true: ne <= 256" unless @ne <= 256 + raise TFMError, "The following condition is not true: lf == 6+lh+(ec-bc+1)+nw+nh+nd+ni+nl+nk+ne+np" unless @lf == 6+@lh+(@ec-@bc+1)+@nw+@nh+@nd+@ni+@nl+@nk+@ne+@np + + # § 23 + @header_base = 6 + @char_base = @header_base + @lh + @width_base = @char_base + (@ec - @bc) + 1 + @height_base = @width_base + @nw + @depth_base = @height_base + @nh + @italic_base = @depth_base + @nd + @lig_kern_base = @italic_base + @ni + @kern_base = @lig_kern_base + @nl + @exten_base = @kern_base + @nk + @param_base = @exten_base + @ne + + parse_header + parse_params + parse_char_info + parse_lig_kern + # exten? + + return @tfm + end # parse + + ####### + private + ####### + + def parse_header + @index = @header_base * 4 + @tfm.checksum=get_qbyte + @tfm.designsize=get_fix_word + if @lh >= 3 + count = get_byte + @tfm.codingscheme=get_chars(count) + @font_type= case @tfm.codingscheme + when "TeX math symbols" + :mathsy + when "TeX math extension" + :mathex + else + :vanilla + end + end + @index = (@header_base + 12) * 4 + if @lh > 12 + count = get_byte + @tfm.fontfamily=get_chars(count) + end + @index = (@header_base + 17 ) * 4 + if @lh >= 17 + @tfm.sevenbitsafeflag=get_byte > 127 + # two bytes ignored + get_byte ; get_byte + @tfm.face=get_byte + end + # let us ignore the rest of the header (TeX ignores it, so we may + # do the same) + end # parse_header + + def parse_params + @index=@param_base * 4 + @tfm.params << nil + @np.times { + @tfm.params << get_fix_word + } + end # parse_params + + # §78 TFtoPL + def parse_char_info + @index=@char_base *4 + (@bc..@ec).each { |n| + tmp=if @tfm.chars[n] + @tfm.chars[n] + else + Hash.new + end + index=get_byte + tmp[:charwd]=get_fix_word((@width_base + index)*4) + b=get_byte + tmp[:charht]=get_fix_word((@height_base + (b >> 4))*4) + tmp[:chardp]=get_fix_word((@depth_base + (b % 16))*4) + tmp[:charic]=get_fix_word((@italic_base + (get_byte >> 2))*4) + # we ignore the remainder and look it up on demand + get_byte + if index == 0 + @tfm.chars[n]=nil + else + @tfm.chars[n]=tmp + end + } + end + + # now for the ugly part in the tfm, §63 pp + # Hey, we do a more clever implementation: we do not check for any + # errors. So coding is only a few lines instead of a few sections. + # this one took me so much time (the original, not this + # implementation), I am really frustrated. + def parse_lig_kern + # array that stores 'instruction that starts at x can be found in + # @tfm.lig_kern at position y' + start_instr=[] + @bc.upto(@ec) { |c| + next unless @tfm.chars[c] + if char_tag(c) == LIGTAG + start=get_lig_starting_point(c) + if start_instr[start] != nil + # we have already stored this ligkern + @tfm.chars[c][:lig_kern]=start_instr[start] + next + end + tmp=[] + + start_instr[start]=@tfm.lig_kern.size + @tfm.lig_kern.push tmp + @tfm.chars[c][:lig_kern]=start_instr[start] + + begin + s=get_byte(lig_step(start)) + puts "warning: skip > 128 (#{s}) I don't know what to do." if s > 128 + n,op,rem=get_byte(lig_step(start)+1),get_byte(lig_step(start)+2),get_byte(lig_step(start)+3) + + if op >= 128 + # kern! + kernamount=get_fix_word((@kern_base + (256 * (op-128) +rem)) *4) + tmp.push [:krn, n, kernamount] + else + tmp.push [TFM::LIGOPS[op], n, rem ] + end + tmp.push [:skip, s] if s > 0 and s < 128 + start += 1 + end until s >= 128 + end + } + end + + + # -------------------------------------------------- + def char_tag(c) + @tfmdata[((@char_base + c - @bc ) *4 + 2)] % 2 + end + def char_remainder(c) + @tfmdata[((@char_base + c - @bc ) *4 + 3)] + end + def get_lig_starting_point(char) + # warning: had some wine + return nil unless char_tag(char) == LIGTAG + r = char_remainder(char) + s=get_byte(lig_step(r)) + if s > 128 + # it does not start here, it starts somewhere else + n,op,rem=get_byte(lig_step(r)+1),get_byte(lig_step(r)+2),get_byte(lig_step(r)+3) + + 256*op+rem + else + r + end + end + + def get_byte(i=nil) + global = i == nil + i = @index if global + r=@tfmdata[i] + @index += 1 if global + r + end + # 16 bit integer + def get_dbyte + r = (@tfmdata[@index] << 8) + @tfmdata[@index + 1] + @index += 2 + r + end + # 32 bit integer + def get_qbyte + r = (@tfmdata[@index] << 24) + (@tfmdata[@index+1] << 16) + (@tfmdata[@index+2] << 8) + @tfmdata[@index+3] + @index += 4 + r + end + def get_chars(count) + ret="" + count.times { |count| + c=@tfmdata[@index + count] + ret << c.chr if c > 0 + } + @index += count + ret + end + def get_fix_word(i=nil) + global = i==nil + i = @index if global + b=@tfmdata[(i..i+3)] + @index += 4 if global + a= (b[0] * 16) + (b[1].div 16) + f= ((b[1] % 16) * 256 + b[2] ) * 256 + b[3] + + str = "" + if a > 03777 + str << "-" + a = 010000 - a + if f > 0 + f = 04000000 - f + a -= 1 + end + end + # Knuth, TFtoPL §42 + + delta = 10 + f=10*f+5 + + str << a.to_s + "." + begin + if delta > 04000000 + f = f + 02000000 - ( delta / 2 ) + end + str << (f / 04000000).to_s + f = 10 * ( f % 04000000) + delta *= 10 + end until f <= delta + str.to_f + end + def lig_step(num) + (@lig_kern_base + num )*4 + end + end + + + + class TFMWriter + # More output to stdout + attr_accessor :verbose + + WIDTH=1 + HEIGHT=2 + DEPTH=3 + ITALIC=4 + + def initialize(tfmobject) + @tfm=tfmobject + @chars=[] + @lig_kern=nil + # for the sorting + @memsize=1028 + 4 + # @memory=Array.new(@memsize) + @memory=[] + @whdi_index=[] + @mem_ptr=nil + @link=Array.new(@memsize) + @index=[] + @memory[0]=017777777777 + @memory[WIDTH]=0 + @memory[HEIGHT]=0 + @memory[DEPTH]=0 + @memory[ITALIC]=0 + @link[WIDTH]=0 + @link[HEIGHT]=0 + @link[DEPTH]=0 + @link[ITALIC]=0 + @mem_ptr = ITALIC + @next_d=nil + + + @bchar_label=077777 + + @data=[] + @lf = 0 + @lh = 0 # ok + @bc = 0 # ok + @ec = 0 # ok + @nw = 0 # ok + @nh = 0 # ok + @nd = 0 # ok + @ni = 0 # ok + @nl = 0 # ok + @nk = 0 # ok + @ne = 0 # ingore + @np = 0 # ok + end + def to_data + update_bc_ec + calculate_header + # width,heigt,dp,ic index + update_whdi_index + # @widths, @heights, @depths, @italics finished + update_lig_kern + # @kerns finished + update_parameters + # @parameters finished + @lf = 6 + @lh + (@ec - @bc + 1) + @nw + @nh + @nd + @ni + @nl + @nk + @ne + @np + @data += out_dbyte(@lf) + @data += out_dbyte(@lh) + @data += out_dbyte(@bc) + @data += out_dbyte(@ec) + @data += out_dbyte(@nw) + @data += out_dbyte(@nh) + @data += out_dbyte(@nd) + @data += out_dbyte(@ni) + @data += out_dbyte(@nl) + @data += out_dbyte(@nk) + @data += out_dbyte(@ne) + @data += out_dbyte(@np) + @data += @header + calculate_chars + @data += @chars + @data += @widths + @data += @heights + @data += @depths + @data += @italics + @data += @lig_kern + @data += @kerns + @data += @parameters + + @data.pack("C*") + end + + def calculate_chars + (@bc..@ec).each { |n| + if @tfm.chars[n] + wd_idx=@index[@widths_orig[n]] + ht_idx=@index[@heights_orig[n]] ? @index[@heights_orig[n]] << 4 : 0 + dp_idx=@index[@depths_orig[n]] ? @index[@depths_orig[n]] : 0 + ic_idx= @index[@italics_orig[n]] ? (@index[@italics_orig[n]] << 2) : 0 + tag = @tfm.chars[n][:lig_kern] ? 1 : 0 + remainder= @tfm.chars[n][:lig_kern] ? @instr_index[@tfm.chars[n][:lig_kern]] : 0 + @chars += [wd_idx,ht_idx + dp_idx, ic_idx + tag, remainder] + else + @chars += [0,0,0,0] + end + } + end + + def update_parameters + @parameters=[] + + @tfm.params.each_with_index { |p,i| + next if i==0 + @parameters += out_fix_word(p) + } + @np=@parameters.size / 4 + end + + def update_whdi_index + @widths_orig=[] + @heights_orig=[] + @depths_orig=[] + @italics_orig=[] + + (@bc..@ec).each { |c| + if @tfm.chars[c] + @widths_orig[c]= sort_in(WIDTH,@tfm.chars[c][:charwd]) + @heights_orig[c] = sort_in(HEIGHT,@tfm.chars[c][:charht] || 0) + @depths_orig[c] = sort_in(DEPTH,@tfm.chars[c][:chardp] || 0 ) + @italics_orig[c] = sort_in(ITALIC,@tfm.chars[c][:charic] || 0 ) + else + @widths_orig[c] = 0 + @depths_orig[c] = 0 + @heights_orig[c] = 0 + @italics_orig[c] = 0 + end + + } + delta=shorten(WIDTH,200) + set_indices(WIDTH,delta) + delta=shorten(HEIGHT,15) + set_indices(HEIGHT,delta) + delta=shorten(DEPTH,15) + set_indices(DEPTH,delta) + delta=shorten(ITALIC,63) + set_indices(ITALIC,delta) + + + @widths = fill_index(WIDTH) + @heights = fill_index(HEIGHT) + @depths = fill_index(DEPTH) + @italics = fill_index(ITALIC) + @nw= @widths.size/4 + @nh= @heights.size/4 + @nd= @depths.size/4 + @ni= @italics.size/4 + end + + def update_lig_kern + kerns=[] + instructions=[] + (@bc..@ec).each { |n| + next unless @tfm.chars[n] + next unless @tfm.chars[n][:lig_kern] + # we can skip aliases + next if instructions[@tfm.chars[n][:lig_kern]] + newinstr=[] + @tfm.lig_kern[@tfm.chars[n][:lig_kern]].each { |instr,*rest| + skip=nextchar=op=remainder=0 + case instr + when :krn + i=nil + unless i = kerns.index(rest[1]) + kerns << rest[1] + i=kerns.size - 1 + end + skip=0 + nextchar=rest[0] + remainder=i % 256 + op = remainder / 256 + 128 + # :stopdoc: + when :lig, :"lig/", :"/lig", :"/lig/", :"lig/>", :"/lig>", :"/lig/>", :"/lig/>>" + # :startdoc: + skip=0 + nextchar,remainder=rest + op=TFM::LIGOPS.index(instr) + when :skip + # todo: test for incorrect situations + newinstr[-4] = rest[0] + next + else + raise "don't know instruction #{instr}" + end + newinstr += [skip,nextchar,op,remainder] + } + newinstr[-4] = 128 + instructions[@tfm.chars[n][:lig_kern]] = newinstr + } + + # we have all instructions collected in an array. The problem now + # is to fill the @lig_kern array so that all start of instruction + # programs are within the first 256 words of @lig_kern. So we keep + # filling the @lig_kern array until there would not be enough room + # left for the indirect nodes for the remaining count of + # instructions. Say, we have 50 instructions left to go and there + # are 60 words free in the first 256 words of @lig_kern, but the + # current instruction would take more then 10 words, we need to + # stop and fill the @lig_kern array with the indirect nodes and + # then continue with the instructions. The following + # implementation seems to work, but I refuse to prove it and it is + # definitely not the most beautiful piece of code I have written. + + @instr_index=[] + @lig_kern=[] + + total_instr=instructions.size + if total_instr > 0 + instr_left=total_instr + thisinstr=instructions.shift + + while (256 - @lig_kern.size / 4) - instr_left - thisinstr.size / 4 > 0 + @instr_index[total_instr-instr_left]=@lig_kern.size / 4 + @lig_kern += thisinstr + thisinstr=instructions.shift + instr_left -= 1 + break if instr_left.zero? + end + + unless instr_left.zero? + # undo last changes, since these don't fit into the @lig_kern + # array (first 256 elements) (yes, this is ugly) + instructions.unshift thisinstr + + + + pos=@lig_kern.size / 4 + instr_left + count=@instr_index.size + + # now fill the indirect nodes, calculate the starting points of + # the instructions + instructions.each { |i| + @instr_index[count]=@lig_kern.size / 4 + count += 1 + @lig_kern += [ 129, 0, (pos / 256) , (pos % 256) ] + pos += i.size / 4 + } + + # now we continue with the instructions + instructions.each { |i| + @lig_kern += i + } + end + end + @nl = @lig_kern.size / 4 + + @kerns=[] + kerns.each { |k| + @kerns += out_fix_word(k) + } + @nk=@kerns.size / 4 + end + + + def fill_index(start) + i=start + what=[0,0,0,0] + while (i=@link[i]) > 0 + what += out_fix_word(@memory[i]) + end + return what + end + + def calculate_header + @header=[] + # checksum + @header += checksum + # dsize + @header += out_fix_word(@tfm.designsize) + # 2..11 coding scheme, bcpl + out_bcpl(@tfm.codingscheme,40) + # 12..16 font identifier + out_bcpl(@tfm.fontfamily,20) + # calculate 7bitflag! + # 7bitflag, byte, byte, face + if @tfm.sevenbitsafeflag + @header << 128 + else + @header << 0 + end + @header << 0 + @header << 0 + @header << @tfm.face + @lh = @header.size / 4 + end + def update_bc_ec + @bc=nil + @tfm.chars.each_with_index{ |elt,i| + @bc=i if @bc==nil and elt!=nil + @ec=i if elt + } + end + def checksum + return out_qbyte(@tfm.checksum) + end + + def out_bcpl(string,len) + str=string + l = str.length + if l > 39 + str=string[0..38] + end + l = str.length + @header << l + count=1 + str.each_byte { |x| + count += 1 + @header << x + } + while len - count > 0 + @header << 0 + count += 1 + end + end + def out_dbyte(int) + a1=int % 256 + a0=int / 256 + return [a0,a1] + end + def out_qbyte(int) + a3=int % 256 + int = int / 256 + a2 = int % 256 + int = int / 256 + a1=int % 256 + a0=int / 256 + return [a0,a1,a2,a3] + end + + # looks ok + def out_fix_word(b) + # a=int part, f=after dec point + a=b.truncate + f=b-a + if b < 0 + f = 1 - f.abs + a = a -1 + end + x=(2**20.0*f).round + a3=x.modulo(256) + # x >>= 8 + x=x/256 + a2=x % 256 + # x >>= 8 + x = x >> 8 + a1=x % 16 + a1 += (a % 16) << 4 + a0=b < 0 ? 256 + a / 16 : a / 16 + [a0,a1, a2, a3] + end + + def sort_in(h,d) + if d==0 and h!=WIDTH + return 0 + end + p=h + while d >= @memory[@link[p]] + p=@link[p] + end + if d==@memory[p] and p!=h + return p + end + raise "Memory overflow: more than 1028 widths etc." if @mem_ptr==@memsize + @mem_ptr += 1 + @memory[@mem_ptr]=d + @link[@mem_ptr]=@link[p] + @link[p]=@mem_ptr + @memory[h]+=1 + return @mem_ptr + end + + # see PLtoTF, §75pp + def min_cover(h,d) + m=0 + p=@link[h] + @next_d=@memory[0] # large value + while p!=0 + m += 1 + l = @memory[p] + while @memory[@link[p]]<=l+d + p=@link[p] + end + p=@link[p] + if @memory[p]-l < @next_d + @next_d=@memory[p]-l + end + end + return m + end + + def shorten(h,m) + if @memory[h] <= m + return 0 + end + @excess=@memory[h]-m + if @excess > 0 and @verbose + puts "We need to shorten the list by #@excess" + end + k=min_cover(h,0) + d=@next_d + begin + d=d+d + k=min_cover(h,d) + end until k <= m + d = d / 2 + k=min_cover(h,d) + while k > m + d=@next_d + k=min_cover(h,d) + end + return d + end + + def set_indices(h,d) + q=h + p=@link[q] + m=0 + while p!=0 + m+=1 + l=@memory[p] + @index[p]=m + while @memory[@link[p]] <= l+d + p=@link[p] + @index[p]=m + @excess -= 1 + if @excess == 0 + d=0 + end + end + @link[q]=p + @memory[p] = l+(@memory[p]-l) / 2 + q=p + p=@link[p] + end + @memory[h]=m + end + + end + + + + # Parse a pl (property list) file. + class PLParser + require 'strscan' + + # _tfmobj_ is an Object of the TFM class. + def initialize(tfmobj) + @tfm=tfmobj + @s=nil + @syntax={ + "COMMENT" => :get_balanced, + "FAMILY" => :get_family, + "FACE" => :get_face, + "CODINGSCHEME" => :get_codingscheme, + "DESIGNSIZE" => :get_designsize, + "CHECKSUM" => :get_checksum, + "FONTDIMEN" => :get_fontdimen, + "LIGTABLE" => :get_ligtable, + "CHARACTER" => :get_character, + } + end + + # Parse the given pl file. _obj_ should be a string. + def parse (obj) + @s=StringScanner.new(obj) + @level=0 + while k=keyword + if m=@syntax[k] + r=self.send(m) + else + raise "unknown property #{k}" + end + end + end + + ####### + private + ####### + + def get_character + thischar = @tfm.chars[get_num] ||= {} + # [:charwd, :charht, :chardp, :charic].each do |s| + # thischar[s]=0.0 + # end + thislevel=@level + while @level >= thislevel + case k=keyword + when "COMMENT" + get_balanced + eat_closing_paren + when "CHARWD","CHARHT","CHARDP","CHARIC" + thischar[k.downcase.to_sym]=get_num + else + raise "Unknown property in pl file/character section: #{k}" + end + end + end + def get_ligtable + thislevel=@level + @tfm.lig_kern = [] + instruction=[] + instrnum=[] + while @level==thislevel + case kw=keyword + when "LABEL" + instrnum.push get_num + when /LIG/ + instruction << [kw.downcase.to_sym, get_num, get_num] + when "KRN" + instruction << [:krn, get_num,get_num] + when "STOP" + n=@tfm.lig_kern.size + instrnum.each { |x| + t = @tfm.chars[x] ||= {} + t[:lig_kern] = n + } + instrnum=[] + @tfm.lig_kern.push instruction + instruction=[] + eat_closing_paren + else + puts "unknown element in ligtable #{kw}, stop" + exit + end + end + end + + def get_fontdimen + thislevel=@level + while @level==thislevel + n=case keyword + when "SLANT" then 1 + when "SPACE" then 2 + when "STRETCH" then 3 + when "SHRINK" then 4 + when "XHEIGHT" then 5 + when "QUAD" then 6 + when "EXTRASPACE" then 7 + when "NUM1", "DEFAULT_RULE_THICKNESS" then 8 + when "NUM2", "BIG_OP_SPACING1" then 9 + when "NUM3", "BIG_OP_SPACING2" then 10 + when "DENOM1", "BIG_OP_SPACING3" then 11 + when "DENOM2", "BIG_OP_SPACING4" then 12 + when "SUP1", "BIG_OP_SPACING5" then 13 + when "SUP2" then 14 + when "SUP3" then 15 + when "SUB1" then 16 + when "SUB2" then 17 + when "SUPDROP" then 18 + when "PARAMETER" + get_num + else + raise "unknown instruction in fontdimen" + end + @tfm.params[n]=get_num + end + end + def get_checksum + @tfm.checksum=get_num + end + def get_designsize + @tfm.designsize=get_num + end + def get_family + @tfm.fontfamily=get_string + end + def get_face + @tfm.face=get_num + end + def get_codingscheme + @tfm.codingscheme=get_balanced + eat_closing_paren + end + def eat_closing_paren + while @s.scan(/\s*\n?\)\n?/) + @level -= 1 + end + end + # we are just before an open paren + def keyword + @s.skip_until(/\(/) + @level += 1 + @s.skip(/\s+/) + ret= @s.scan(/[A-Za-z\/>]+/) + @s.skip(/\s+/) + return ret + end + + def get_balanced + str="" + startlevel=@level + while @level >= startlevel + str << @s.scan(/[^\(\)]*/) + if (tmp = @s.scan(/(\(|\))/)) == "(" + @level += 1 + else + @level -= 1 + end + str << tmp if @level >= startlevel + end + @s.skip(/\n/) + str + end + def get_string + @s.skip(/\s/) + s= @s.scan(/[[:alnum:]`'_\- :]+/) + @s.scan(/\)\s*\n/) + @level -= 1 + return s + end + def get_num + @s.skip(/\s+/) + s=@s.scan(/(R|C|D|O|F|H)/) + @s.skip(/\s+/) + value=case s + when "R" + @s.scan(/-?\d+(\.\d+)?/).to_f + when "C" + @s.scan(/[[:alnum:]]/)[0] + when "D" + @s.scan(/\d+/).to_i + when "O" + @s.scan(/\d+/).to_i(8) + when "F" + t=@s.scan(/(M|B|L)(R|I)(R|C|E)/) + ['MRR','MIR','BRR','BIR','LRR','LIR','MRC','MIC','BRC','BIC', + 'LRC','LIC','MRE','MIE','BRE','BIE','LRE','LIE'].index(t) + else + raise "not implemented yet" + end + eat_closing_paren + value + end + end #class pl parser + + + + # :stopdoc: + LIGOPS= [ :lig, :"lig/", :"/lig", :"/lig/", + nil, :"lig/>", :"/lig>", :"/lig/>", + nil, nil, nil, :"/lig/>>" ] + + FACE = ['MRR','MIR','BRR','BIR','LRR','LIR','MRC','MIC','BRC','BIC', + 'LRC','LIC','MRE','MIE','BRE','BIE','LRE','LIE'] + + NOTAG=0 + LIGTAG=1 + LISTTAG=2 + EXTTAG=3 + + def self.documented_as_accessor(*args) #:nodoc: + end + def self.documented_as_reader(*args) #:nodoc: + end + # :startdoc: + + # Print diagnostics + attr_accessor :verbose + + # Filename sans path of the tfm file. To change this attribute, set + # pathname. + documented_as_reader :tfmfilename + + # Path to the tfm file. + attr_accessor :tfmpathname + + # Checksum of the tfm file. + attr_accessor :checksum + + # The designsize (Float). Must be >= 1.0. + attr_accessor :designsize + + # Coding scheme of the font. One of "TeX math symbols", "TeX math + # extension" or anything else. The two have special meaning (more + # parameters). Maximum length is 40 + attr_accessor :codingscheme + + # Font family is an arbitrary String. Default is "UNSPECIFIED". + # Maximum length is 20. + attr_accessor :fontfamily + + # This boolean flag denotes if the font has chars with index > 127. + attr_accessor :sevenbitsafeflag + + # Face code. 0 <= 17. + attr_accessor :face + + # Array of chars. Each entry is a Hash with the following keys: + # :charwd :charht, :chardp, + # :charic and :lig_kern. The first four are in + # designsize units. The :lig_kern key is the instruction + # number pointing to the entry in the lig_kern attribute of the TFM + # class. + attr_accessor :chars + + # Array of ligkern instructions. Each instruction is an Array of + # Arrays where the first element is either :krn or one of + # :lig, :lig/, :/lig, :/lig/, + # :lig/>, :/lig>, :/lig/> or + # :/lig/>>. If it is :krn, then the second + # element is the next char and the third element must be the amount + # of kerning in multiples of the designsize. If it is a + # :lig (or similar), then the second element is the + # nextchar. The third element is the resulting char. + + # Example for an instruction: + # + # + # [[:"lig/", 39, 148], + # [:krn, 121, -0.029993], + # [:krn, 39, -0.159998], + # [:krn, 148, -0.13999], + # [:krn, 89, -0.13999]] + # + # The complete lig_kern would be an Array of such instructions. + attr_accessor :lig_kern + + # The fontdimensions, index starts at 1. + attr_accessor :params + + def initialize + @chars=[] + @lig_kern=[] + @params=[] + @face=0 + @designsize=10.0 + @checksum=0 + @fontfamily="UNSPECIFIED" + @verbose=false + end + def tfmfilename # :nodoc: + File.basename(@tfmpathname) + end + + + # _plfile_ is a filename (String). (Future: File and String (pathname)) + def read_pl(plfilename) + File.open(plfilename) { |f| + parse_pl(f.read) + } + return self + end + def parse_pl(plstring) + p=PLParser.new(self) + p.parse(plstring) + return self + end + + # _file_ is either a File object (or something similar, it must + # respond to :read) or a string containing the full pathname to the + # tfm file. Returns the TFM object. + def read_tfm(file) + p=TFMReader.new(self) + p.verbose=@verbose + if file.respond_to? :read + if file.respond_to? :path + @tfmpathname=file.path + end + p.parse(file.read) + else + # we assume it is a string + @tfmpathname=file + case file + when /\.tfm$/ + File.open(file) { |f| + p.parse(f.read) + } + else + raise ArgumentError, "unknown Filetype: #{file}" + end + end + return self + end # read_file + + # If _overwrite_ is true, we will replace existing files without + # raising Errno::EEXIST. + def save(overwrite=false) + raise Errno::EEXIST if File.exists?(@tfmpathname) and not overwrite + puts "saving #{@tfmpathname}..." if @verbose + File.open(@tfmpathname,"wb") { |f| + write_file(f) + } + puts "saving #{@tfmpathname}...done" if @verbose + end + + # _file_ is a File object (or something similar, it must + # respond to <<). + def write_file(file) + tfmwriter=TFMWriter.new(self) + tfmwriter.verbose=@verbose + file << tfmwriter.to_data + end + + # Return pltotf compatible output. + def to_s + indent=" " + str="" + str << out_head(indent) + str << out_parameters(indent) + str << out_ligtable(indent) + str << out_chars(indent) + str + end + + ####### + private + ####### + + def out_head(indent) + str ="(FAMILY #{fontfamily.upcase})\n" + str << "(FACE F #{FACE[face]})\n" + str << "(CODINGSCHEME #{codingscheme.upcase})\n" + str << "(DESIGNSIZE R #{designsize})\n" + str << "(CHECKSUM O #{sprintf("%o",checksum)})\n" + end + def out_chars(indent) + str = "" + chars.each_with_index { |c,i| + next unless c + # str << "(CHARACTER O #{sprintf("%o",i)}\n" + str << "(CHARACTER D %d\n" % i + [:charwd,:charht,:chardp,:charic].each { |dim| + str << indent + "(#{dim.to_s.upcase} R #{c[dim]})\n" if c[dim]!=0.0 + } + str << indent + ")\n" + } + str + end + def out_parameters(indent) + paramname=%w( X SLANT SPACE STRETCH SHRINK XHEIGHT QUAD EXTRASPACE ) + if codingscheme=="TeX math symbols" + paramname += %w(NUM1 NUM2 NUM3 DENOM1 DENOM2 SUP1 SUP2 SUP3 + SUB1 SUB2 SUPDROP) + elsif codingscheme=="TeX math extension" + paramname += %w(DEFAULT_RULE_THICKNESS BIG_OP_SPACING1 + BIG_OP_SPACING2 BIG_OP_SPACING3 BIG_OP_SPACING4 BIG_OP_SPACING5) + end + + str = "(FONTDIMEN\n" + @params.each_with_index { |p,i| + next if i==0 + if paramname[i] + str << indent + "(#{paramname[i]} R #{p})\n" + else + str << indent + "(PARAMETER D #{i} R #{p})\n" + end + } + str << indent + ")\n" + str + end + def out_ligtable(indent) + return "" if @lig_kern.size==0 + str = "(LIGTABLE\n" + lk_char=[] + # first appearance of a char is the index, all chars for the same + # instructions is the value + # e.g. firstchar_chars[8]=[8,9] if chars 8 and 9 point to the same instr. + firstchar_chars=[] + @chars.each_with_index {|c,i| + next unless c + next unless instr=c[:lig_kern] + # we need to find duplicates + # some chars point to the same instruction + if lk_char[instr] + lk_char[instr].push i + else + lk_char[instr] = [i] + end + } + + lk_char.each{ |a| + firstchar_chars[a[0]]=a + } + firstchar_chars.each { |a| + next unless a + a.each { |l| + str << indent + "(LABEL D #{l})\n" + } + @lig_kern[@chars[a[0]][:lig_kern]].each {|la| + case la[0] + when :skip + str << indent + "(SKIP D #{la[1]})\n" + when :krn + str << indent + "(KRN D #{la[1]} R #{la[2]})\n" + when :lig, :"lig/", :"/lig", :"/lig/", :"lig/>", :"/lig>", :"/lig/>", :"/lig/>>" + str << indent + "(#{la[0].to_s.upcase} O #{sprintf("%o",la[1])} O #{sprintf("%o",la[2])})\n" + end + } + str << indent + "(STOP)\n" + } + str << indent + ")\n" + str + end + end # class TFM + end # module TeX + end diff --git a/support/rfil/lib/rfil/tex/vf.rb b/support/rfil/lib/rfil/tex/vf.rb new file mode 100644 index 0000000000..8116be52b0 --- /dev/null +++ b/support/rfil/lib/rfil/tex/vf.rb @@ -0,0 +1,848 @@ +# vf.rb -- Class that models TeX's virtual fonts. +#-- +# Last Change: Tue May 16 17:32:53 2006 +#++ + +require 'rfil/tex/tfm' +require 'rfil/tex/kpathsea' + +module RFIL + module TeX + + # The vf (virtual font) files are described in vftovp and vptovf. They + # are always connected with a tfm file that hold the font metric. The + # vf contain some redundant information copied from the tfm file. + # Since the VF class is derived from the TFM class, there is no need + # to duplicate these pieces of information. + + class VF < TFM + # This class is not meant to be used directly by the programmer. It + # is used in the VF class to read a virtual font from a file. + + class VFReader + def initialize(vfobj) + @vfobj= vfobj || VF.new + @stack=[[0,0,0,0]] + push + @index=0 + @dviindex=nil + end + + # _vfdata_ is a string with the contents of the vf (binary) file. + # Return a VF object filled with the information of the virtual + # font. Does not read the tfm data. It is safe to parse tfm data + # after parsing the virtual font. + def parse(vfdata) + raise ArgumentError, "I expect a string" unless vfdata.respond_to?(:unpack) + @index=0 + @kpse=Kpathsea.new + @data=vfdata.unpack("C*") + + raise VFError, "This does not look like a vf to me" if 247 != get_byte + raise VFError, "Unknown VF version" unless 202 == get_byte + + @vfobj.vtitle=get_chars(get_byte) + + tfmcksum = get_qbyte + tfmdsize = get_fix_word + + while b=get_byte + case b + when 0..241 + @index -= 1 + parse_char(:short) + when 242 + parse_char(:long) + when 243,244,245,246 + # jippie, a (map)font + fontnumber=get_bytes(243-b+1,false) + # perhaps we should actually load the tfm instead of saving + # the metadata? + @vfobj.fontlist[fontnumber]={} + checksum=get_qbyte + # @vfobj.fontlist[fontnumber][:checksum]=checksum + scale=get_fix_word + @vfobj.fontlist[fontnumber][:scale]=scale + dsize = get_fix_word + # @vfobj.fontlist[fontnumber][:designsize]=dsize + a = get_byte # length of area (directory?) + l = get_byte # length of fontname + area=get_chars(a) + name=get_chars(l) + # @vfobj.fontlist[fontnumber][:name]=name + @kpse.open_file(name,'tfm') { |file| + @vfobj.fontlist[fontnumber][:tfm]=TFM.new.read_tfm(file) + } + when 248 + parse_postamble + else + raise VFError, "unknown instruction number #{b.inspect}" + end + end + return @vfobj + end # parse + + + ####### + private + ####### + + def parse_postamble + while get_byte + end + end + # type: :long, :short + def parse_char(type) + instructions=[] + case type + when :long + pl=get_qbyte + cc=get_qbyte + tfm=out_as_fix(get_bytes(4,true,@index)) + @index+=4 + when :short + pl=get_byte + cc=get_byte + tfm=out_as_fix(get_bytes(3,true,@index)) + @index+=3 + else + raise ArgumentError,"unknown type: #{type}" + end + dvi=@data[(@index..@index+pl-1)] + @dviindex=@index + @index += pl + while i = get_byte(@dviindex) and @dviindex < @index + case i + when 0 + # setchar 0 + raise "not implementd" + when 1..127 + instructions << [:setchar, i] + when 128..131 + c=4-(131-i) + instructions << [:setchar, get_bytes(c,false,@dviindex+1)] + @dviindex += c + when 132,137 + x=out_as_fix(get_bytes(4,true,@dviindex+1)) + y=out_as_fix(get_bytes(4,true,@dviindex+5)) + instructions << [:setrule,x,y] + @dviindex += 8 + when 133..136 + # are these ever used? + c=4-(136-i) + instructions << [:setchar, get_bytes(c,false,@dviindex+1)] + @dviindex += c + when 138 + # nop + when 139,140 + raise VFError, "illegal instruction in VF: #{i}" + when 141 + instructions << [:push] + push + when 142 + instructions << [:pop] + pop + when 143..146 + c=4-(146-i) + b=out_as_fix(get_bytes(c,true,@dviindex+1)) + instructions << [:moveright, b] + @dviindex += c + when 147 + instructions << [:moveright, _w] + when 148..151 + c=4-(151-i) + self._w=out_as_fix(get_bytes(c,true,@dviindex+1)) + instructions << [:moveright,_w] + @dviindex += c + when 152 + instructions << [:moveright, _x] + when 153..156 + c=4-(156-i) + x=out_as_fix(get_bytes(c,true,@dviindex+1)) + self._x=x + instructions << [:moveright,x] + @dviindex += c + when 157..160 + # are these really used? + c=i-157+1 + v=out_as_fix(get_bytes(c,true,@dviindex+1)) + instructions << [:movedown,v] + @dviindex += c + when 161 + instructions << [:movedown, _y] + when 162..165 + c=i-162+1 + self._y = out_as_fix(get_bytes(c,true,@dviindex+1)) + instructions << [:movedown,_y] + @dviindex += c + # puts "#{i} movedown y #{_y}" + when 166 + instructions << [:movedown, _z] + when 167..170 + c=i-167+1 + self._z = out_as_fix(get_bytes(c,true,@dviindex+1)) + instructions << [:movedown,_z] + @dviindex += c + # puts "#{i} movedown z #{_z}" + when 171..234 + instructions << [:selectfont, 63-234+i] + when 235..238 + c=i-235+1 + instructions << [:selectfont, get_bytes(c,true,@dviindex+1)] + @dviindex += c + when 239..242 + c=i-239+1 + k=get_bytes(c,true,@dviindex+1) + if k < 0 + raise VFError, "length of special is negative" + end + instructions << [:special, get_chars(k,@dviindex+2)] + @dviindex += 1+k + when 243..255 + raise VFError, "illegal instruction in VF: #{i}" + else + raise "not implemented: #{i}" + end + @dviindex += 1 + end + # puts "charcode=#{cc} (octal #{sprintf("%o",cc)})" + tmp=if @vfobj.chars[cc] + @vfobj.chars[cc] + else + Hash.new + end + @vfobj.chars[cc]=tmp + tmp[:dvi]=instructions + end + def push + top=@stack[-1] + @stack.push top.dup + end + def pop + if @stack.size < 2 + raise VFError, "more pop then push on stack" + end + return @stack.pop + end + def _w=(value) + @stack[-1][0]=value + end + def _w + @stack[-1][0] + end + def _x=(value) + @stack[-1][1]=value + end + def _x + @stack[-1][1] + end + + def _y=(value) + @stack[-1][2]=value + end + def _y + @stack[-1][2] + end + + def _z=(value) + @stack[-1][3]=value + end + def _z + @stack[-1][3] + end + + def get_byte(i=nil) + global = i==nil + i = @index if global + r=@data[i] + @index += 1 if global + r + end + # 16 bit integer + def get_dbyte(i=nil) + global = i == nil + i = @index if global + r = (@data[i] << 8) + @data[i + 1] + @index += 2 if global + r + end + # 24 bit int + def get_tbyte(i=nil) + global = i == nil + i = @index if global + r = (@data[i] << 16) + (@data[i] << 8) + @data[i + 1] + @index += 3 if global + r + end + # signed 24 bit int + def get_stbyte(i=nil) + global = i == nil + i = @index if global + r = if @data[i] < 128 + (@data[i] << 16) + (@data[i] << 8) + @data[i + 1] + else + ((256 - @data[i]) << 16) + (@data[i] << 8) + @data[i + 1] + end + @index += 3 if global + r + end + + # 32 bit integer + def get_qbyte + r = (@data[@index] << 24) + (@data[@index+1] << 16) + (@data[@index+2] << 8) + @data[@index+3] + @index += 4 + r + end + # Read a string with at most count bytes. Does not add \0 to the string. + def get_chars(count,i=nil) + ret="" + global = i==nil + i = @index if global + + count.times { |coumt| + c=@data[i + coumt] + ret << c.chr if c > 0 + } + @index += count if global + return ret.size==0 ? nil : ret + end + def get_bytes(count,signed,i=nil) + global = i==nil + i=@index if global + a=@data[i] + if (count==4) or signed + if a >= 128 + a -= 256 + end + end + i +=1 + while count > 1 + a = a * 256 + @data[i] + i +=1 + count -=1 + end + @index += count if global + return a + end + def out_as_fix(x) + raise VFError if x.abs >= 0100000000 + # let's misuse @data -> change + if x>=0 then @data[0]=0 + else + @data[0]=255 + x += 0100000000 + end + 3.downto(1) { |k| + @data[k]=x % 256 + x = x.div(256) + } + get_fix_word(0) + end + def get_fix_word(i=nil) + global = i==nil + i = @index if global + b=@data[(i..i+3)] + @index += 4 if global + a= (b[0] * 16) + (b[1].div 16) + f= ((b[1] % 16) * 0400 + b[2] ) * 0400 + b[3] + + str = "" + if a > 03777 + str << "-" + a = 010000 - a + if f > 0 + f = 04000000 - f + a -= 1 + end + end + # Knuth, TFtoPL §42 + + delta = 10 + f=10*f+5 + + str << a.to_s + "." + begin + if delta > 04000000 + f = f + 02000000 - ( delta / 2 ) + end + str << (f / 04000000).to_s + f = 10 * ( f % 04000000) + delta *= 10 + end until f <= delta + str.to_f + end + + end + + + class VFWriter + attr_accessor :verbose + def initialize(vfobject) + @vf=vfobject + end + + def to_data + # preamble + @data=[247,202] + @data += out_string(@vf.vtitle) + @data += out_qbyte(@vf.checksum) + @data += out_fix_word(@vf.designsize) + + # fonts + @vf.fontlist.each_with_index { |f,i| + count,*bytes=out_n_bytes(i) + @data += [242+count] + @data += bytes + + @data+=out_qbyte(f[:tfm].checksum) + @data+=out_fix_word(f[:scale]) + @data+=out_fix_word(f[:tfm].designsize) + @data+=[0] + @data += out_string(f[:tfm].tfmfilename.chomp('.tfm')) + } + + # now for the chars + @vf.chars.each_with_index { |c,i| + next unless c + dvi=out_instructions(c[:dvi]) + pl=dvi.length + tfm=c[:charwd] + if pl < 242 and tfm < 16.0 and tfm > 0 and i < 256 + @data << pl + @data << i + @data += out_fix_word(tfm,3) + else + @data << 242 + @data += out_qbyte(pl) + @data += out_qbyte(i) + @data += out_fix_word(tfm) + end + @data += dvi + } + @data << 248 + while @data.size % 4 != 0 + @data << 248 + end + return @data.pack("C*") + end + + ####### + private + ####### + + def out_instructions(instructionlist) + ret=[] + instructionlist.each { |i| + case i[0] + when :setchar + charnum=i[1] + if charnum < 128 + ret << charnum + elsif charnum > 255 + raise VFError, "TeX does not know about chars > 8bit" + else + ret << 128 + ret << charnum + end + when :setrule + ret << 132 + ret += out_fix_word(i[1]) + ret += out_fix_word(i[2]) + when :noop + ret << 138 + when :push + ret << 141 + when :pop + ret << 142 + when :moveright + # should we choose another moveright? --pg + ret << 156 + ret += out_fix_word(i[1]) + when :movedown + ret << 165 + ret += out_fix_word(i[1]) + when :special + len,*data=out_string(i[1]) + blen,bytes = out_n_bytes(len) + ret << blen+238 + ret << bytes + ret += data + else + raise VFError, "not implemented" + end + } + ret + end + + def out_n_bytes(int) + case + when (int < 0), (int >= 0100000000) + [4] + out_sqbyte(int) + when int >= 0200000 + [3] + out_tbyte(int) + when int >= 0400 + [2] + out_dbyte(int) + else + [1,int] + end + end + def out_dbyte(int) + a1=int % 256 + a0=int / 256 + return [a0,a1] + end + def out_tbyte(int) + a2 = int % 256 + int = int / 256 + a1=int % 256 + a0=int / 256 + return [a0,a1,a2] + end + def out_qbyte(int) + a3=int % 256 + int = int / 256 + a2 = int % 256 + int = int / 256 + a1=int % 256 + a0=int / 256 + return [a0,a1,a2,a3] + end + # signed four bytes + def out_sqbyte(int) + a3=int % 256 + int = int / 256 + a2 = int % 256 + int = int / 256 + a1=int % 256 + a0=int / 256 + if int < 0 + a0 = 256 + a0 + end + return [a0,a1,a2,a3] + end + def out_fix_word(b,bytes=4) + # a=int part, f=after dec point + a=b.truncate + f=b-a + if b < 0 + f = 1 - f.abs + a = a -1 + end + x=(2**20.0*f).round + a3=x.modulo(256) + # x >>= 8 + x=x/256 + a2=x % 256 + # x >>= 8 + x = x >> 8 + a1=x % 16 + a1 += (a % 16) << 4 + a0=b < 0 ? 256 + a / 16 : a / 16 + if bytes == 3 + [a1, a2, a3] + else + [a0,a1, a2, a3] + end + end + + def out_string(string) + unless string + return [0] + end + ret=[string.length] + string.each_byte { |s| + ret << s + } + return ret + end + end + + + # Parse a vpl (virtual property list) file. See also TFM::PLParser. + class VPLParser < PLParser + # _vfobj_ is an initialized object of the VF class. Call + # parse(fileobj) to fill the VF object. + def initialize(vfobj) + @vf=vfobj + super + @syntax["VTITLE"] =:get_vtitle + @syntax["MAPFONT"]=:get_mapfont + end + + ####### + private + ####### + + def get_vtitle + @vf.vtitle=get_string + end + def get_mapfont + @vf.fontlist=[] + t = @vf.fontlist[get_num] = {} + t[:tfm]=TFM.new + thislevel=@level + while @level >= thislevel + case k=keyword + when "FONTNAME" + t[:tfm].tfmpathname=get_string + when "FONTCHECKSUM" + t[:tfm].checksum=get_num + when "FONTAT" + t[:scale]=get_num + when "FONTDSIZE" + t[:tfm].designsize=get_num + else + raise "Unknown property in MAPFONT section: #{k}" + end + end + end # get_mapfont + + # we copy this from tfm.rb, because now MAP is also allowed + def get_character + thischar = @tfm.chars[get_num] ||= {} + thislevel=@level + while @level >= thislevel + case k=keyword + when "COMMENT" + get_balanced + eat_closing_paren + when "CHARWD","CHARHT","CHARDP","CHARIC" + thischar[k.downcase.to_sym]=get_num + when "MAP" + instr=thischar[:dvi]=[] + maplevel=@level + while @level >= maplevel + case ik=keyword + when "SELECTFONT" + instr << [:selectfont, get_num] + when "SETCHAR" + instr << [:setchar, get_num] + when "SETRULE" + instr << [:setrule, get_num, get_num] + when "MOVEDOWN" + instr << [:movedown, get_num] + when "MOVERIGHT" + instr << [:moveright, get_num] + when "PUSH" + instr << [:push] + eat_closing_paren + when "POP" + instr << [:pop] + eat_closing_paren + when "SPECIAL" + instr << [:special, get_balanced] + # puts "special, #{get_balanced}" + eat_closing_paren + else + raise "Unknown instruction in character/map section: #{ik}" + end + end + else + raise "Unknown property in pl file/character section: #{k}" + end + end + end # get_character + end + + + + # VF class + + # Raise this exception if an error related to the virtual font is + # encountered. Don't expect this library to be too clever at the beginning. + class VFError < Exception + end + + def self.documented_as_accessor(*args) #:nodoc: + end + def self.documented_as_reader(*args) #:nodoc: + end + + # Filename sans path of the vf file. To change this attribute, set + # vfpathname. + documented_as_reader :vffilename + + # Path to the vf file. + attr_accessor :vfpathname + + + # fontlist is an array of Hashes with the following keys: + # [:scale] Relative size of the font + # [:tfm] TFM object. + attr_accessor :fontlist + + # This is the same Array as in TFM. Besides the keys :charwd, + # :charht, :chardp and :charic, we now have a key + # :dvi that holds all vf instructions. + documented_as_accessor :chars + + # Comment at the beginning of the vf file. Must be < 256 chars. + attr_accessor :vtitle + + # Return an empty VF object + def initialize + super + @vtitle="" + @fontlist=[] + end + + def vffilename # :nodoc: + File.basename(@vfpathname) + end + + # _vplfile_ is a filename (String). (Future: File and String (pathname)) + def read_vpl(vplfilename) + File.open(vplfilename) { |f| + parse_vpl(f.read) + } + return self + end + def parse_vpl(vplstring) + v=VPLParser.new(self) + v.parse(vplstring) + return self + end + + # _file_ is either a string (pathname) of a File object (must + # respond to read) + def read_vf(file) + p=VFReader.new(self) + if file.respond_to? :read + if file.respond_to? :path + @vfpathname=file.path + end + p.parse(file.read) + else + # we assume it is a string + @vfpathname=file + case file + when /\.vf$/ + File.open(file) { |f| + p.parse(f.read) + } + else + raise ArgumentError, "unknown Filetype: #{file}" + end + end + t=TFMReader.new(self) + @tfmpathname=@vfpathname.chomp(".vf")+".tfm" + File.open(@tfmpathname){ |f| + t.parse(f.read) + } + return self + end #read_vf + + # If _overwrite_ is true, we will replace existing files without + # raising Errno::EEXIST. + def save(overwrite=false) + # tfmpathname=@vfpathname.chomp(".vf")+".tfm" + raise "tfmpathname not set" unless @tfmpathname + raise Errno::EEXIST if File.exists?(@vfpathname) and not overwrite + raise Errno::EEXIST if File.exists?(@tfmpathname) and not overwrite + puts "saving #{@vfpathname}..." if @verbose + File.open(@vfpathname,"wb") { |f| + write_vf_file(f) + } + puts "saving #{@vfpathname}...done" if @verbose + puts "saving #{@tfmpathname}..." if @verbose + File.open(@tfmpathname,"wb") { |f| + write_tfm_file(f) + } + puts "saving #{@tfmpathname}...done" if @verbose + + end + + + # _file_ is a File object (or something similar, it must + # respond to <<). Will be moved. + def write_vf_file(file) + vfwriter=VFWriter.new(self) + vfwriter.verbose=@verbose + file << vfwriter.to_data + end + + # _file_ is a File object (or something similar, it must + # respond to <<). Will be moved. + def write_tfm_file(file) + tfmwriter=TFMWriter.new(self) + tfmwriter.verbose=@verbose + file << tfmwriter.to_data + end + # Return vptovf compatible output + def to_s + indent=" " + str="" + str << out_head(indent) + str << "(VTITLE #{vtitle})\n" + str << out_parameters(indent) + str << out_mapfont(indent) + str << out_ligtable(indent) + str << out_chars(indent) + str + end + + ####### + private + ####### + + def out_chars(indent) + str = "" + chars.each_with_index { |c,i| + next unless c + # str << "(CHARACTER O #{sprintf("%o",i)}\n" + str << "(CHARACTER D %d\n" % i + [:charwd,:charht,:chardp,:charic].each { |dim| + str << indent + "(#{dim.to_s.upcase} R #{c[dim]})\n" if c[dim]!=0.0 + } + if c[:dvi] + str << indent + "(MAP\n" + c[:dvi].each { |instr,*rest| + + case instr + when :setchar + str << indent*2 + "(SETCHAR D %d)\n" % rest[0].to_i + when :setrule + str << indent*2 + "(SETRULE R #{rest[0]} R #{rest[1]})\n" + when :noop + # ignore + when :push + str << indent*2 + "(PUSH)\n" + when :pop + str << indent*2 + "(POP)\n" + when :moveright + str << indent*2 + "(MOVERIGHT R #{rest[0]})\n" + when :movedown + str << indent*2 + "(MOVEDOWN R #{rest[0]})\n" + when :selectfont + str << indent*2 + "(SELECTFONT D #{rest[0]})\n" + when :special + str << indent*2 + "(SPECIAL #{rest[0]})\n" + else + raise "unknown dvi instruction #{instr}" + end + } + str << indent*2 + ")\n" + end + str << indent + ")\n" + } + str + end + + def out_mapfont(indent) + return "" if fontlist.size == 0 + str="" + + fontlist.each_with_index { |f,i| + str << "(MAPFONT D %d\n" % i + str << indent + "(FONTNAME %s)\n" % f[:tfm].tfmfilename + str << indent + "(FONTCHECKSUM O %o)\n" % f[:tfm].checksum + str << indent + "(FONTAT R %f)\n" % (f[:scale] ? f[:scale].to_f : 1.0) + str << indent + "(FONTDSIZE R %f)\n" % f[:tfm].designsize + str << indent + ")\n" + } + str + end + end #class VF + + end #module TeX +end -- cgit v1.2.3