# 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