Current File : //usr/share/texlive/texmf-dist/tex/luatex/luaotfload/fontloader-font-one.lua |
if not modules then modules = { } end modules ['font-one'] = {
version = 1.001,
optimize = true,
comment = "companion to font-ini.mkiv",
author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
copyright = "PRAGMA ADE / ConTeXt Development Team",
license = "see context related readme files"
}
--[[ldx--
<p>Some code may look a bit obscure but this has to do with the fact that we also use
this code for testing and much code evolved in the transition from <l n='tfm'/> to
<l n='afm'/> to <l n='otf'/>.</p>
<p>The following code still has traces of intermediate font support where we handles
font encodings. Eventually font encoding went away but we kept some code around in
other modules.</p>
<p>This version implements a node mode approach so that users can also more easily
add features.</p>
--ldx]]--
local fonts, logs, trackers, containers, resolvers = fonts, logs, trackers, containers, resolvers
local next, type, tonumber, rawget = next, type, tonumber, rawget
local match, gsub = string.match, string.gsub
local abs = math.abs
local P, S, R, Cmt, C, Ct, Cs, Carg = lpeg.P, lpeg.S, lpeg.R, lpeg.Cmt, lpeg.C, lpeg.Ct, lpeg.Cs, lpeg.Carg
local lpegmatch, patterns = lpeg.match, lpeg.patterns
local sortedhash = table.sortedhash
local trace_features = false trackers.register("afm.features", function(v) trace_features = v end)
local trace_indexing = false trackers.register("afm.indexing", function(v) trace_indexing = v end)
local trace_loading = false trackers.register("afm.loading", function(v) trace_loading = v end)
local trace_defining = false trackers.register("fonts.defining", function(v) trace_defining = v end)
local report_afm = logs.reporter("fonts","afm loading")
local setmetatableindex = table.setmetatableindex
local derivetable = table.derive
local findbinfile = resolvers.findbinfile
local privateoffset = fonts.constructors and fonts.constructors.privateoffset or 0xF0000 -- 0x10FFFF
local definers = fonts.definers
local readers = fonts.readers
local constructors = fonts.constructors
local afm = constructors.handlers.afm
local pfb = constructors.handlers.pfb
local otf = fonts.handlers.otf
local otfreaders = otf.readers
local otfenhancers = otf.enhancers
local afmfeatures = constructors.features.afm
local registerafmfeature = afmfeatures.register
local afmenhancers = constructors.enhancers.afm
local registerafmenhancer = afmenhancers.register
afm.version = 1.513 -- incrementing this number one up will force a re-cache
afm.cache = containers.define("fonts", "one", afm.version, true)
afm.autoprefixed = true -- this will become false some day (catches texnansi-blabla.*)
afm.helpdata = { } -- set later on so no local for this
afm.syncspace = true -- when true, nicer stretch values
local overloads = fonts.mappings.overloads
local applyruntimefixes = fonts.treatments and fonts.treatments.applyfixes
--[[ldx--
<p>We cache files. Caching is taken care of in the loader. We cheat a bit by adding
ligatures and kern information to the afm derived data. That way we can set them faster
when defining a font.</p>
<p>We still keep the loading two phased: first we load the data in a traditional
fashion and later we transform it to sequences. Then we apply some methods also
used in opentype fonts (like <t>tlig</t>).</p>
--ldx]]--
function afm.load(filename)
filename = resolvers.findfile(filename,'afm') or ""
if filename ~= "" and not fonts.names.ignoredfile(filename) then
local name = file.removesuffix(file.basename(filename))
local data = containers.read(afm.cache,name)
local attr = lfs.attributes(filename)
local size = attr and attr.size or 0
local time = attr and attr.modification or 0
--
local pfbfile = file.replacesuffix(name,"pfb")
local pfbname = resolvers.findfile(pfbfile,"pfb") or ""
if pfbname == "" then
pfbname = resolvers.findfile(file.basename(pfbfile),"pfb") or ""
end
local pfbsize = 0
local pfbtime = 0
if pfbname ~= "" then
local attr = lfs.attributes(pfbname)
pfbsize = attr.size or 0
pfbtime = attr.modification or 0
end
if not data or data.size ~= size or data.time ~= time or data.pfbsize ~= pfbsize or data.pfbtime ~= pfbtime then
report_afm("reading %a",filename)
data = afm.readers.loadfont(filename,pfbname)
if data then
afmenhancers.apply(data,filename)
-- otfreaders.addunicodetable(data) -- only when not done yet
fonts.mappings.addtounicode(data,filename)
otfreaders.stripredundant(data)
-- otfreaders.extend(data)
otfreaders.pack(data)
data.size = size
data.time = time
data.pfbsize = pfbsize
data.pfbtime = pfbtime
report_afm("saving %a in cache",name)
-- data.resources.unicodes = nil -- consistent with otf but here we save not much
data = containers.write(afm.cache, name, data)
data = containers.read(afm.cache,name)
end
end
if data then
-- constructors.addcoreunicodes(unicodes)
otfreaders.unpack(data)
otfreaders.expand(data) -- inline tables
otfreaders.addunicodetable(data) -- only when not done yet
otfenhancers.apply(data,filename,data)
if applyruntimefixes then
applyruntimefixes(filename,data)
end
end
return data
end
end
-- we run a more advanced analyzer later on anyway
local uparser = fonts.mappings.makenameparser() -- each time
local function enhance_unify_names(data, filename)
local unicodevector = fonts.encodings.agl.unicodes -- loaded runtime in context
local unicodes = { }
local names = { }
local private = data.private or privateoffset
local descriptions = data.descriptions
for name, blob in sortedhash(data.characters) do -- sorting is nicer for privates
local code = unicodevector[name] -- or characters.name_to_unicode[name]
if not code then
code = lpegmatch(uparser,name)
if type(code) ~= "number" then
code = private
private = private + 1
report_afm("assigning private slot %U for unknown glyph name %a",code,name)
end
end
local index = blob.index
unicodes[name] = code
names[name] = index
blob.name = name
descriptions[code] = {
boundingbox = blob.boundingbox,
width = blob.width,
kerns = blob.kerns,
index = index,
name = name,
}
end
for unicode, description in next, descriptions do
local kerns = description.kerns
if kerns then
local krn = { }
for name, kern in next, kerns do
local unicode = unicodes[name]
if unicode then
krn[unicode] = kern
else
-- print(unicode,name)
end
end
description.kerns = krn
end
end
data.characters = nil
data.private = private
local resources = data.resources
local filename = resources.filename or file.removesuffix(file.basename(filename))
resources.filename = resolvers.unresolve(filename) -- no shortcut
resources.unicodes = unicodes -- name to unicode
resources.marks = { } -- todo
-- resources.names = names -- name to index
end
local everywhere = { ["*"] = { ["*"] = true } } -- or: { ["*"] = { "*" } }
local noflags = { false, false, false, false }
local function enhance_normalize_features(data)
local ligatures = setmetatableindex("table")
local kerns = setmetatableindex("table")
local extrakerns = setmetatableindex("table")
for u, c in next, data.descriptions do
local l = c.ligatures
local k = c.kerns
local e = c.extrakerns
if l then
ligatures[u] = l
for u, v in next, l do
l[u] = { ligature = v }
end
c.ligatures = nil
end
if k then
kerns[u] = k
for u, v in next, k do
k[u] = v -- { v, 0 }
end
c.kerns = nil
end
if e then
extrakerns[u] = e
for u, v in next, e do
e[u] = v -- { v, 0 }
end
c.extrakerns = nil
end
end
local features = {
gpos = { },
gsub = { },
}
local sequences = {
-- only filled ones
}
if next(ligatures) then
features.gsub.liga = everywhere
data.properties.hasligatures = true
sequences[#sequences+1] = {
features = {
liga = everywhere,
},
flags = noflags,
name = "s_s_0",
nofsteps = 1,
order = { "liga" },
type = "gsub_ligature",
steps = {
{
coverage = ligatures,
},
},
}
end
if next(kerns) then
features.gpos.kern = everywhere
data.properties.haskerns = true
sequences[#sequences+1] = {
features = {
kern = everywhere,
},
flags = noflags,
name = "p_s_0",
nofsteps = 1,
order = { "kern" },
type = "gpos_pair",
steps = {
{
format = "kern",
coverage = kerns,
},
},
}
end
if next(extrakerns) then
features.gpos.extrakerns = everywhere
data.properties.haskerns = true
sequences[#sequences+1] = {
features = {
extrakerns = everywhere,
},
flags = noflags,
name = "p_s_1",
nofsteps = 1,
order = { "extrakerns" },
type = "gpos_pair",
steps = {
{
format = "kern",
coverage = extrakerns,
},
},
}
end
-- todo: compress kerns
data.resources.features = features
data.resources.sequences = sequences
end
local function enhance_fix_names(data)
for k, v in next, data.descriptions do
local n = v.name
local r = overloads[n]
if r then
local name = r.name
if trace_indexing then
report_afm("renaming characters %a to %a",n,name)
end
v.name = name
v.unicode = r.unicode
end
end
end
--[[ldx--
<p>These helpers extend the basic table with extra ligatures, texligatures
and extra kerns. This saves quite some lookups later.</p>
--ldx]]--
local addthem = function(rawdata,ligatures)
if ligatures then
local descriptions = rawdata.descriptions
local resources = rawdata.resources
local unicodes = resources.unicodes
-- local names = resources.names
for ligname, ligdata in next, ligatures do
local one = descriptions[unicodes[ligname]]
if one then
for _, pair in next, ligdata do
local two = unicodes[pair[1]]
local three = unicodes[pair[2]]
if two and three then
local ol = one.ligatures
if ol then
if not ol[two] then
ol[two] = three
end
else
one.ligatures = { [two] = three }
end
end
end
end
end
end
end
local function enhance_add_ligatures(rawdata)
addthem(rawdata,afm.helpdata.ligatures)
end
--[[ldx--
<p>We keep the extra kerns in separate kerning tables so that we can use
them selectively.</p>
--ldx]]--
-- This is rather old code (from the beginning when we had only tfm). If
-- we unify the afm data (now we have names all over the place) then
-- we can use shcodes but there will be many more looping then. But we
-- could get rid of the tables in char-cmp then. Als, in the generic version
-- we don't use the character database. (Ok, we can have a context specific
-- variant).
local function enhance_add_extra_kerns(rawdata) -- using shcodes is not robust here
local descriptions = rawdata.descriptions
local resources = rawdata.resources
local unicodes = resources.unicodes
local function do_it_left(what)
if what then
for unicode, description in next, descriptions do
local kerns = description.kerns
if kerns then
local extrakerns
for complex, simple in next, what do
complex = unicodes[complex]
simple = unicodes[simple]
if complex and simple then
local ks = kerns[simple]
if ks and not kerns[complex] then
if extrakerns then
extrakerns[complex] = ks
else
extrakerns = { [complex] = ks }
end
end
end
end
if extrakerns then
description.extrakerns = extrakerns
end
end
end
end
end
local function do_it_copy(what)
if what then
for complex, simple in next, what do
complex = unicodes[complex]
simple = unicodes[simple]
if complex and simple then
local complexdescription = descriptions[complex]
if complexdescription then -- optional
local simpledescription = descriptions[complex]
if simpledescription then
local extrakerns
local kerns = simpledescription.kerns
if kerns then
for unicode, kern in next, kerns do
if extrakerns then
extrakerns[unicode] = kern
else
extrakerns = { [unicode] = kern }
end
end
end
local extrakerns = simpledescription.extrakerns
if extrakerns then
for unicode, kern in next, extrakerns do
if extrakerns then
extrakerns[unicode] = kern
else
extrakerns = { [unicode] = kern }
end
end
end
if extrakerns then
complexdescription.extrakerns = extrakerns
end
end
end
end
end
end
end
-- add complex with values of simplified when present
do_it_left(afm.helpdata.leftkerned)
do_it_left(afm.helpdata.bothkerned)
-- copy kerns from simple char to complex char unless set
do_it_copy(afm.helpdata.bothkerned)
do_it_copy(afm.helpdata.rightkerned)
end
--[[ldx--
<p>The copying routine looks messy (and is indeed a bit messy).</p>
--ldx]]--
local function adddimensions(data) -- we need to normalize afm to otf i.e. indexed table instead of name
if data then
for unicode, description in next, data.descriptions do
local bb = description.boundingbox
if bb then
local ht = bb[4]
local dp = -bb[2]
if ht == 0 or ht < 0 then
-- no need to set it and no negative heights, nil == 0
else
description.height = ht
end
if dp == 0 or dp < 0 then
-- no negative depths and no negative depths, nil == 0
else
description.depth = dp
end
end
end
end
end
local function copytotfm(data)
if data and data.descriptions then
local metadata = data.metadata
local resources = data.resources
local properties = derivetable(data.properties)
local descriptions = derivetable(data.descriptions)
local goodies = derivetable(data.goodies)
local characters = { }
local parameters = { }
local unicodes = resources.unicodes
--
for unicode, description in next, data.descriptions do -- use parent table
characters[unicode] = { }
end
--
local filename = constructors.checkedfilename(resources)
local fontname = metadata.fontname or metadata.fullname
local fullname = metadata.fullname or metadata.fontname
local endash = 0x2013
local emdash = 0x2014
local space = 0x0020 -- space
local spacer = "space"
local spaceunits = 500
--
local monospaced = metadata.monospaced
local charwidth = metadata.charwidth
local italicangle = metadata.italicangle
local charxheight = metadata.xheight and metadata.xheight > 0 and metadata.xheight
properties.monospaced = monospaced
parameters.italicangle = italicangle
parameters.charwidth = charwidth
parameters.charxheight = charxheight
-- nearly the same as otf, catches
local d_endash = descriptions[endash]
local d_emdash = descriptions[emdash]
local d_space = descriptions[space]
if not d_space or d_space == 0 then
d_space = d_endash
end
if d_space then
spaceunits, spacer = d_space.width or 0, "space"
end
if properties.monospaced then
if spaceunits == 0 and d_emdash then
spaceunits, spacer = d_emdash.width or 0, "emdash"
end
else
if spaceunits == 0 and d_endash then
spaceunits, spacer = d_emdash.width or 0, "endash"
end
end
if spaceunits == 0 and charwidth then
spaceunits, spacer = charwidth or 0, "charwidth"
end
if spaceunits == 0 then
spaceunits = tonumber(spaceunits) or 500
end
if spaceunits == 0 then
spaceunits = 500
end
--
parameters.slant = 0
parameters.space = spaceunits
parameters.space_stretch = 500
parameters.space_shrink = 333
parameters.x_height = 400
parameters.quad = 1000
--
if italicangle and italicangle ~= 0 then
parameters.italicangle = italicangle
parameters.italicfactor = math.cos(math.rad(90+italicangle))
parameters.slant = - math.tan(italicangle*math.pi/180)
end
if monospaced then
parameters.space_stretch = 0
parameters.space_shrink = 0
elseif afm.syncspace then
parameters.space_stretch = spaceunits/2
parameters.space_shrink = spaceunits/3
end
parameters.extra_space = parameters.space_shrink
if charxheight then
parameters.x_height = charxheight
else
-- same as otf
local x = 0x0078 -- x
if x then
local x = descriptions[x]
if x then
parameters.x_height = x.height
end
end
--
end
--
if metadata.sup then
local dummy = { 0, 0, 0 }
parameters[ 1] = metadata.designsize or 0
parameters[ 2] = metadata.checksum or 0
parameters[ 3],
parameters[ 4],
parameters[ 5] = unpack(metadata.space or dummy)
parameters[ 6] = metadata.quad or 0
parameters[ 7] = metadata.extraspace or 0
parameters[ 8],
parameters[ 9],
parameters[10] = unpack(metadata.num or dummy)
parameters[11],
parameters[12] = unpack(metadata.denom or dummy)
parameters[13],
parameters[14],
parameters[15] = unpack(metadata.sup or dummy)
parameters[16],
parameters[17] = unpack(metadata.sub or dummy)
parameters[18] = metadata.supdrop or 0
parameters[19] = metadata.subdrop or 0
parameters[20],
parameters[21] = unpack(metadata.delim or dummy)
parameters[22] = metadata.axisheight or 0
end
--
parameters.designsize = (metadata.designsize or 10)*65536
parameters.ascender = abs(metadata.ascender or 0)
parameters.descender = abs(metadata.descender or 0)
parameters.units = 1000
--
properties.spacer = spacer
properties.format = fonts.formats[filename] or "type1"
properties.filename = filename
properties.fontname = fontname
properties.fullname = fullname
properties.psname = fullname
properties.name = filename or fullname or fontname
properties.private = properties.private or data.private or privateoffset
--
if not CONTEXTLMTXMODE or CONTEXTLMTXMODE == 0 then
properties.encodingbytes = 2
end
--
if next(characters) then
return {
characters = characters,
descriptions = descriptions,
parameters = parameters,
resources = resources,
properties = properties,
goodies = goodies,
}
end
end
return nil
end
--[[ldx--
<p>Originally we had features kind of hard coded for <l n='afm'/> files but since I
expect to support more font formats, I decided to treat this fontformat like any
other and handle features in a more configurable way.</p>
--ldx]]--
function afm.setfeatures(tfmdata,features)
local okay = constructors.initializefeatures("afm",tfmdata,features,trace_features,report_afm)
if okay then
return constructors.collectprocessors("afm",tfmdata,features,trace_features,report_afm)
else
return { } -- will become false
end
end
local function addtables(data)
local resources = data.resources
local lookuptags = resources.lookuptags
local unicodes = resources.unicodes
if not lookuptags then
lookuptags = { }
resources.lookuptags = lookuptags
end
setmetatableindex(lookuptags,function(t,k)
local v = type(k) == "number" and ("lookup " .. k) or k
t[k] = v
return v
end)
if not unicodes then
unicodes = { }
resources.unicodes = unicodes
setmetatableindex(unicodes,function(t,k)
setmetatableindex(unicodes,nil)
for u, d in next, data.descriptions do
local n = d.name
if n then
t[n] = u
end
end
return rawget(t,k)
end)
end
constructors.addcoreunicodes(unicodes) -- do we really need this?
end
local function afmtotfm(specification)
local afmname = specification.filename or specification.name
if specification.forced == "afm" or specification.format == "afm" then -- move this one up
if trace_loading then
report_afm("forcing afm format for %a",afmname)
end
else
local tfmname = findbinfile(afmname,"ofm") or ""
if tfmname ~= "" then
if trace_loading then
report_afm("fallback from afm to tfm for %a",afmname)
end
return -- just that
end
end
if afmname ~= "" then
-- weird, isn't this already done then?
local features = constructors.checkedfeatures("afm",specification.features.normal)
specification.features.normal = features
constructors.hashinstance(specification,true) -- also weird here
--
specification = definers.resolve(specification) -- new, was forgotten
local cache_id = specification.hash
local tfmdata = containers.read(constructors.cache, cache_id) -- cache with features applied
if not tfmdata then
local rawdata = afm.load(afmname)
if rawdata and next(rawdata) then
addtables(rawdata)
adddimensions(rawdata)
tfmdata = copytotfm(rawdata)
if tfmdata and next(tfmdata) then
local shared = tfmdata.shared
if not shared then
shared = { }
tfmdata.shared = shared
end
shared.rawdata = rawdata
shared.dynamics = { }
tfmdata.changed = { }
shared.features = features
shared.processes = afm.setfeatures(tfmdata,features)
end
elseif trace_loading then
report_afm("no (valid) afm file found with name %a",afmname)
end
tfmdata = containers.write(constructors.cache,cache_id,tfmdata)
end
return tfmdata
end
end
--[[ldx--
<p>As soon as we could intercept the <l n='tfm'/> reader, I implemented an
<l n='afm'/> reader. Since traditional <l n='pdftex'/> could use <l n='opentype'/>
fonts with <l n='afm'/> companions, the following method also could handle
those cases, but now that we can handle <l n='opentype'/> directly we no longer
need this features.</p>
--ldx]]--
local function read_from_afm(specification)
local tfmdata = afmtotfm(specification)
if tfmdata then
tfmdata.properties.name = specification.name
tfmdata = constructors.scale(tfmdata, specification)
local allfeatures = tfmdata.shared.features or specification.features.normal
constructors.applymanipulators("afm",tfmdata,allfeatures,trace_features,report_afm)
fonts.loggers.register(tfmdata,'afm',specification)
end
return tfmdata
end
--[[ldx--
<p>We have the usual two modes and related features initializers and processors.</p>
--ldx]]--
registerafmfeature {
name = "mode",
description = "mode",
initializers = {
base = otf.modeinitializer,
node = otf.modeinitializer,
}
}
registerafmfeature {
name = "features",
description = "features",
default = true,
initializers = {
node = otf.nodemodeinitializer,
base = otf.basemodeinitializer,
},
processors = {
node = otf.featuresprocessor,
}
}
-- readers
fonts.formats.afm = "type1"
fonts.formats.pfb = "type1"
local function check_afm(specification,fullname)
local foundname = findbinfile(fullname, 'afm') or "" -- just to be sure
if foundname == "" then
foundname = fonts.names.getfilename(fullname,"afm") or ""
end
if fullname and foundname == "" and afm.autoprefixed then
local encoding, shortname = match(fullname,"^(.-)%-(.*)$") -- context: encoding-name.*
if encoding and shortname and fonts.encodings.known[encoding] then
shortname = findbinfile(shortname,'afm') or "" -- just to be sure
if shortname ~= "" then
foundname = shortname
if trace_defining then
report_afm("stripping encoding prefix from filename %a",afmname)
end
end
end
end
if foundname ~= "" then
specification.filename = foundname
specification.format = "afm"
return read_from_afm(specification)
end
end
function readers.afm(specification,method)
local fullname = specification.filename or ""
local tfmdata = nil
if fullname == "" then
local forced = specification.forced or ""
if forced ~= "" then
tfmdata = check_afm(specification,specification.name .. "." .. forced)
end
if not tfmdata then
local check_tfm = readers.check_tfm
method = (check_tfm and (method or definers.method or "afm or tfm")) or "afm"
if method == "tfm" then
tfmdata = check_tfm(specification,specification.name)
elseif method == "afm" then
tfmdata = check_afm(specification,specification.name)
elseif method == "tfm or afm" then
tfmdata = check_tfm(specification,specification.name) or check_afm(specification,specification.name)
else -- method == "afm or tfm" or method == "" then
tfmdata = check_afm(specification,specification.name) or check_tfm(specification,specification.name)
end
end
else
tfmdata = check_afm(specification,fullname)
end
return tfmdata
end
function readers.pfb(specification,method) -- only called when forced
local original = specification.specification
if trace_defining then
report_afm("using afm reader for %a",original)
end
specification.forced = "afm"
local function swap(name)
local value = specification[swap]
if value then
specification[swap] = gsub("%.pfb",".afm",1)
end
end
swap("filename")
swap("fullname")
swap("forcedname")
swap("specification")
return readers.afm(specification,method)
end
-- now we register them
registerafmenhancer("unify names", enhance_unify_names)
registerafmenhancer("add ligatures", enhance_add_ligatures)
registerafmenhancer("add extra kerns", enhance_add_extra_kerns)
registerafmenhancer("normalize features", enhance_normalize_features)
registerafmenhancer("check extra features", otfenhancers.enhance)
registerafmenhancer("fix names", enhance_fix_names)