# 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