Current File : //usr/share/texlive/texmf-dist/tex/luatex/luaotfload/fontloader-font-otc.lua |
if not modules then modules = { } end modules ['font-otc'] = {
version = 1.001,
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"
}
local insert, sortedkeys, sortedhash, tohash = table.insert, table.sortedkeys, table.sortedhash, table.tohash
local type, next, tonumber = type, next, tonumber
local lpegmatch = lpeg.match
local utfbyte, utflen = utf.byte, utf.len
local sortedhash = table.sortedhash
-- we assume that the other otf stuff is loaded already
local trace_loading = false trackers.register("otf.loading", function(v) trace_loading = v end)
local report_otf = logs.reporter("fonts","otf loading")
local fonts = fonts
local otf = fonts.handlers.otf
local registerotffeature = otf.features.register
local setmetatableindex = table.setmetatableindex
local fonthelpers = fonts.helpers
local checkmerge = fonthelpers.checkmerge
local checkflags = fonthelpers.checkflags
local checksteps = fonthelpers.checksteps
local normalized = {
substitution = "substitution",
single = "substitution",
ligature = "ligature",
alternate = "alternate",
multiple = "multiple",
kern = "kern",
pair = "pair",
single = "single",
chainsubstitution = "chainsubstitution",
chainposition = "chainposition",
}
local types = {
substitution = "gsub_single",
ligature = "gsub_ligature",
alternate = "gsub_alternate",
multiple = "gsub_multiple",
kern = "gpos_pair",
pair = "gpos_pair",
single = "gpos_single",
chainsubstitution = "gsub_contextchain",
chainposition = "gpos_contextchain",
}
local names = {
gsub_single = "gsub",
gsub_multiple = "gsub",
gsub_alternate = "gsub",
gsub_ligature = "gsub",
gsub_context = "gsub",
gsub_contextchain = "gsub",
gsub_reversecontextchain = "gsub",
gpos_single = "gpos",
gpos_pair = "gpos",
gpos_cursive = "gpos",
gpos_mark2base = "gpos",
gpos_mark2ligature = "gpos",
gpos_mark2mark = "gpos",
gpos_context = "gpos",
gpos_contextchain = "gpos",
}
setmetatableindex(types, function(t,k) t[k] = k return k end) -- "key"
local everywhere = { ["*"] = { ["*"] = true } } -- or: { ["*"] = { "*" } }
local noflags = { false, false, false, false }
-- beware: shared, maybe we should copy the sequence
local function getrange(sequences,category)
local count = #sequences
local first = nil
local last = nil
for i=1,count do
local t = sequences[i].type
if t and names[t] == category then
if not first then
first = i
end
last = i
end
end
return first or 1, last or count
end
local function validspecification(specification,name)
local dataset = specification.dataset
if dataset then
-- okay
elseif specification[1] then
dataset = specification
specification = { dataset = dataset }
else
dataset = { { data = specification.data } }
specification.data = nil
specification.dataset = dataset
end
local first = dataset[1]
if first then
first = first.data
end
if not first then
report_otf("invalid feature specification, no dataset")
return
end
if type(name) ~= "string" then
name = specification.name or first.name
end
if type(name) ~= "string" then
report_otf("invalid feature specification, no name")
return
end
local n = #dataset
if n > 0 then
for i=1,n do
setmetatableindex(dataset[i],specification)
end
return specification, name
end
end
local function addfeature(data,feature,specifications)
-- todo: add some validator / check code so that we're more tolerant to
-- user errors
if not specifications then
report_otf("missing specification")
return
end
local descriptions = data.descriptions
local resources = data.resources
local features = resources.features
local sequences = resources.sequences
if not features or not sequences then
report_otf("missing specification")
return
end
local alreadydone = resources.alreadydone
if not alreadydone then
alreadydone = { }
resources.alreadydone = alreadydone
end
if alreadydone[specifications] then
return
else
alreadydone[specifications] = true
end
-- feature has to be unique but the name entry wins eventually
local fontfeatures = resources.features or everywhere
local unicodes = resources.unicodes
local splitter = lpeg.splitter(" ",unicodes)
local done = 0
local skip = 0
local aglunicodes = false
local privateslot = fonthelpers.privateslot
local specifications = validspecification(specifications,feature)
if not specifications then
-- report_otf("invalid specification")
return
end
local p = lpeg.P("P")
* (lpeg.patterns.hexdigit^1/function(s) return tonumber(s,16) end)
* lpeg.P(-1)
local function tounicode(code)
if not code then
return
end
if type(code) == "number" then
return code
end
local u = unicodes[code]
if u then
-- unicodes[code] = u
return u
end
if utflen(code) == 1 then
u = utfbyte(code)
if u then
return u
end
end
if privateslot then
u = privateslot(code) -- no creation !
if u then
-- unicodes[code] = u
return u
end
end
local u = lpegmatch(p,code)
if u then
-- unicodes[code] = u
return u
end
if not aglunicodes then
aglunicodes = fonts.encodings.agl.unicodes -- delayed
end
local u = aglunicodes[code]
if u then
-- unicodes[code] = u
return u
end
end
local coverup = otf.coverup
local coveractions = coverup.actions
local stepkey = coverup.stepkey
local register = coverup.register
local function prepare_substitution(list,featuretype,nocheck)
local coverage = { }
local cover = coveractions[featuretype]
for code, replacement in next, list do
local unicode = tounicode(code)
local description = descriptions[unicode]
if not nocheck and not description then
skip = skip + 1
else
if type(replacement) == "table" then
replacement = replacement[1]
end
replacement = tounicode(replacement)
if replacement and (nocheck or descriptions[replacement]) then
cover(coverage,unicode,replacement)
done = done + 1
else
skip = skip + 1
end
end
end
return coverage
end
local function prepare_alternate(list,featuretype,nocheck)
local coverage = { }
local cover = coveractions[featuretype]
for code, replacement in next, list do
local unicode = tounicode(code)
local description = descriptions[unicode]
if not nocheck and not description then
skip = skip + 1
elseif type(replacement) == "table" then
local r = { }
for i=1,#replacement do
local u = tounicode(replacement[i])
r[i] = (nocheck or descriptions[u]) and u or unicode
end
cover(coverage,unicode,r)
done = done + 1
else
local u = tounicode(replacement)
if u then
cover(coverage,unicode,{ u })
done = done + 1
else
skip = skip + 1
end
end
end
return coverage
end
local function prepare_multiple(list,featuretype,nocheck)
local coverage = { }
local cover = coveractions[featuretype]
for code, replacement in next, list do
local unicode = tounicode(code)
local description = descriptions[unicode]
if not nocheck and not description then
skip = skip + 1
elseif type(replacement) == "table" then
local r = { }
local n = 0
for i=1,#replacement do
local u = tounicode(replacement[i])
if nocheck or descriptions[u] then
n = n + 1
r[n] = u
end
end
if n > 0 then
cover(coverage,unicode,r)
done = done + 1
else
skip = skip + 1
end
else
local u = tounicode(replacement)
if u then
cover(coverage,unicode,{ u })
done = done + 1
else
skip = skip + 1
end
end
end
return coverage
end
local function prepare_ligature(list,featuretype,nocheck)
local coverage = { }
local cover = coveractions[featuretype]
for code, ligature in next, list do
local unicode = tounicode(code)
local description = descriptions[unicode]
if not nocheck and not description then
skip = skip + 1
else
if type(ligature) == "string" then
ligature = { lpegmatch(splitter,ligature) }
end
local present = true
for i=1,#ligature do
local l = ligature[i]
local u = tounicode(l)
if nocheck or descriptions[u] then
ligature[i] = u
else
present = false
break
end
end
if present then
cover(coverage,unicode,ligature)
done = done + 1
else
skip = skip + 1
end
end
end
return coverage
end
local function resetspacekerns()
-- a bit of a hack, this nil setting but it forces a
-- rehash of the resources needed .. the feature itself
-- should be a kern (at least for now)
data.properties.hasspacekerns = true
data.resources .spacekerns = nil
end
local function prepare_kern(list,featuretype)
local coverage = { }
local cover = coveractions[featuretype]
local isspace = false
for code, replacement in next, list do
local unicode = tounicode(code)
local description = descriptions[unicode]
if description and type(replacement) == "table" then
local r = { }
for k, v in next, replacement do
local u = tounicode(k)
if u then
r[u] = v
if u == 32 then
isspace = true
end
end
end
if next(r) then
cover(coverage,unicode,r)
done = done + 1
if unicode == 32 then
isspace = true
end
else
skip = skip + 1
end
else
skip = skip + 1
end
end
if isspace then
resetspacekerns()
end
return coverage
end
local function prepare_pair(list,featuretype)
local coverage = { }
local cover = coveractions[featuretype]
if cover then
for code, replacement in next, list do
local unicode = tounicode(code)
local description = descriptions[unicode]
if description and type(replacement) == "table" then
local r = { }
for k, v in next, replacement do
local u = tounicode(k)
if u then
r[u] = v
if u == 32 then
isspace = true
end
end
end
if next(r) then
cover(coverage,unicode,r)
done = done + 1
if unicode == 32 then
isspace = true
end
else
skip = skip + 1
end
else
skip = skip + 1
end
end
if isspace then
resetspacekerns()
end
else
report_otf("unknown cover type %a",featuretype)
end
return coverage
end
local prepare_single = prepare_pair -- we could have a better test on the spec
local function prepare_chain(list,featuretype,sublookups)
-- todo: coveractions
local rules = list.rules
local coverage = { }
if rules then
local rulehash = { }
local rulesize = 0
local lookuptype = types[featuretype]
for nofrules=1,#rules do
local rule = rules[nofrules]
local current = rule.current
local before = rule.before
local after = rule.after
local replacements = rule.replacements or false
local sequence = { }
local nofsequences = 0
if before then
for n=1,#before do
nofsequences = nofsequences + 1
sequence[nofsequences] = before[n]
end
end
local start = nofsequences + 1
for n=1,#current do
nofsequences = nofsequences + 1
sequence[nofsequences] = current[n]
end
local stop = nofsequences
if after then
for n=1,#after do
nofsequences = nofsequences + 1
sequence[nofsequences] = after[n]
end
end
local lookups = rule.lookups or false
local subtype = nil
if lookups and sublookups then
for k, v in sortedhash(lookups) do
local t = type(v)
if t == "table" then
-- already ok
for i=1,#v do
local vi = v[i]
if type(vi) ~= "table" then
v[i] = { vi }
end
end
elseif t == "number" then
local lookup = sublookups[v]
if lookup then
lookups[k] = { lookup }
if not subtype then
subtype = lookup.type
end
elseif v == 0 then
lookups[k] = { { type = "gsub_remove" } }
else
lookups[k] = false -- { false } -- new
end
else
lookups[k] = false -- { false } -- new
end
end
end
if nofsequences > 0 then -- we merge coverage into one
-- we copy as we can have different fonts
local hashed = { }
for i=1,nofsequences do
local t = { }
local s = sequence[i]
for i=1,#s do
local u = tounicode(s[i])
if u then
t[u] = true
end
end
hashed[i] = t
end
sequence = hashed
-- now we create the rule
rulesize = rulesize + 1
rulehash[rulesize] = {
nofrules, -- 1
lookuptype, -- 2
sequence, -- 3
start, -- 4
stop, -- 5
lookups, -- 6 (6/7 also signal of what to do)
replacements, -- 7
subtype, -- 8
}
-- for unic in next, sequence[start] do
for unic in sortedhash(sequence[start]) do
local cu = coverage[unic]
if not cu then
coverage[unic] = rulehash -- can now be done cleaner i think
end
end
sequence.n = nofsequences
end
end
rulehash.n = rulesize
end
return coverage
end
local dataset = specifications.dataset
local function report(name,category,position,first,last,sequences)
report_otf("injecting name %a of category %a at position %i in [%i,%i] of [%i,%i]",
name,category,position,first,last,1,#sequences)
end
local function inject(specification,sequences,sequence,first,last,category,name)
local position = specification.position or false
if not position then
position = specification.prepend
if position == true then
if trace_loading then
report(name,category,first,first,last,sequences)
end
insert(sequences,first,sequence)
return
end
end
if not position then
position = specification.append
if position == true then
if trace_loading then
report(name,category,last+1,first,last,sequences)
end
insert(sequences,last+1,sequence)
return
end
end
local kind = type(position)
if kind == "string" then
local index = false
for i=first,last do
local s = sequences[i]
local f = s.features
if f then
for k in sortedhash(f) do -- next, f do
if k == position then
index = i
break
end
end
if index then
break
end
end
end
if index then
position = index
else
position = last + 1
end
elseif kind == "number" then
if position < 0 then
position = last - position + 1
end
if position > last then
position = last + 1
elseif position < first then
position = first
end
else
position = last + 1
end
if trace_loading then
report(name,category,position,first,last,sequences)
end
insert(sequences,position,sequence)
end
for s=1,#dataset do
local specification = dataset[s]
local valid = specification.valid -- nowhere used
local feature = specification.name or feature
if not feature or feature == "" then
report_otf("no valid name given for extra feature")
elseif not valid or valid(data,specification,feature) then -- anum uses this
local initialize = specification.initialize
if initialize then
-- when false is returned we initialize only once
specification.initialize = initialize(specification,data) and initialize or nil
end
local askedfeatures = specification.features or everywhere
local askedsteps = specification.steps or specification.subtables or { specification.data } or { }
local featuretype = normalized[specification.type or "substitution"] or "substitution"
local featureflags = specification.flags or noflags
local nocheck = specification.nocheck
local featureorder = specification.order or { feature }
local featurechain = (featuretype == "chainsubstitution" or featuretype == "chainposition") and 1 or 0
local nofsteps = 0
local steps = { }
local sublookups = specification.lookups
local category = nil
--
checkflags(specification,resources)
--
if sublookups then
local s = { }
for i=1,#sublookups do
local specification = sublookups[i]
local askedsteps = specification.steps or specification.subtables or { specification.data } or { }
local featuretype = normalized[specification.type or "substitution"] or "substitution"
local featureflags = specification.flags or noflags
local nofsteps = 0
local steps = { }
for i=1,#askedsteps do
local list = askedsteps[i]
local coverage = nil
local format = nil
if featuretype == "substitution" then
coverage = prepare_substitution(list,featuretype,nocheck)
elseif featuretype == "ligature" then
coverage = prepare_ligature(list,featuretype,nocheck)
elseif featuretype == "alternate" then
coverage = prepare_alternate(list,featuretype,nocheck)
elseif featuretype == "multiple" then
coverage = prepare_multiple(list,featuretype,nocheck)
elseif featuretype == "kern" or featuretype == "move" then
format = featuretype
coverage = prepare_kern(list,featuretype)
elseif featuretype == "pair" then
format = "pair"
coverage = prepare_pair(list,featuretype)
elseif featuretype == "single" then
format = "single"
coverage = prepare_single(list,featuretype)
end
if coverage and next(coverage) then
nofsteps = nofsteps + 1
steps[nofsteps] = register(coverage,featuretype,format,feature,nofsteps,descriptions,resources)
end
end
--
checkmerge(specification)
checksteps(specification)
--
s[i] = {
[stepkey] = steps,
nofsteps = nofsteps,
flags = featureflags,
type = types[featuretype],
}
end
sublookups = s
end
for i=1,#askedsteps do
local list = askedsteps[i]
local coverage = nil
local format = nil
if featuretype == "substitution" then
category = "gsub"
coverage = prepare_substitution(list,featuretype,nocheck)
elseif featuretype == "ligature" then
category = "gsub"
coverage = prepare_ligature(list,featuretype,nocheck)
elseif featuretype == "alternate" then
category = "gsub"
coverage = prepare_alternate(list,featuretype,nocheck)
elseif featuretype == "multiple" then
category = "gsub"
coverage = prepare_multiple(list,featuretype,nocheck)
elseif featuretype == "kern" or featuretype == "move" then
category = "gpos"
format = featuretype
coverage = prepare_kern(list,featuretype)
elseif featuretype == "pair" then
category = "gpos"
format = "pair"
coverage = prepare_pair(list,featuretype)
elseif featuretype == "single" then
category = "gpos"
format = "single"
coverage = prepare_single(list,featuretype)
elseif featuretype == "chainsubstitution" then
category = "gsub"
coverage = prepare_chain(list,featuretype,sublookups)
elseif featuretype == "chainposition" then
category = "gpos"
coverage = prepare_chain(list,featuretype,sublookups)
else
report_otf("not registering feature %a, unknown category",feature)
return
end
if coverage and next(coverage) then
nofsteps = nofsteps + 1
steps[nofsteps] = register(coverage,featuretype,format,feature,nofsteps,descriptions,resources)
end
end
if nofsteps > 0 then
-- script = { lang1, lang2, lang3 } or script = { lang1 = true, ... }
for k, v in next, askedfeatures do
if v[1] then
askedfeatures[k] = tohash(v)
end
end
--
if featureflags[1] then featureflags[1] = "mark" end
if featureflags[2] then featureflags[2] = "ligature" end
if featureflags[3] then featureflags[3] = "base" end
local steptype = types[featuretype]
local sequence = {
chain = featurechain,
features = { [feature] = askedfeatures },
flags = featureflags,
name = feature, -- redundant
order = featureorder,
[stepkey] = steps,
nofsteps = nofsteps,
type = steptype,
}
--
checkflags(sequence,resources)
checkmerge(sequence)
checksteps(sequence)
-- position | prepend | append
local first, last = getrange(sequences,category)
inject(specification,sequences,sequence,first,last,category,feature)
-- register in metadata (merge as there can be a few)
local features = fontfeatures[category]
if not features then
features = { }
fontfeatures[category] = features
end
local k = features[feature]
if not k then
k = { }
features[feature] = k
end
--
for script, languages in next, askedfeatures do
local kk = k[script]
if not kk then
kk = { }
k[script] = kk
end
for language, value in next, languages do
kk[language] = value
end
end
end
end
end
if trace_loading then
report_otf("registering feature %a, affected glyphs %a, skipped glyphs %a",feature,done,skip)
end
end
otf.enhancers.addfeature = addfeature
local extrafeatures = { }
local knownfeatures = { }
function otf.addfeature(name,specification)
if type(name) == "table" then
specification = name
end
if type(specification) ~= "table" then
report_otf("invalid feature specification, no valid table")
return
end
specification, name = validspecification(specification,name)
if name and specification then
local slot = knownfeatures[name]
if not slot then
-- we have a new one
slot = #extrafeatures + 1
knownfeatures[name] = slot
elseif specification.overload == false then
-- we add an extre one
slot = #extrafeatures + 1
knownfeatures[name] = slot
else
-- we overload a previous one
end
specification.name = name -- to be sure
extrafeatures[slot] = specification
-- report_otf("adding feature %a @ %i",name,slot)
end
end
-- for feature, specification in next, extrafeatures do
-- addfeature(data,feature,specification)
-- end
local function enhance(data,filename,raw)
for slot=1,#extrafeatures do
local specification = extrafeatures[slot]
addfeature(data,specification.name,specification)
end
end
otf.enhancers.enhance = enhance
otf.enhancers.register("check extra features",enhance)
-- fonts.handlers.otf.features.register {
-- name = 'hangulfix',
-- description = 'fixes for hangul',
-- }
-- fonts.handlers.otf.addfeature {
-- name = "stest",
-- type = "substitution",
-- data = {
-- a = "X",
-- b = "P",
-- }
-- }
-- fonts.handlers.otf.addfeature {
-- name = "atest",
-- type = "alternate",
-- data = {
-- a = { "X", "Y" },
-- b = { "P", "Q" },
-- }
-- }
-- fonts.handlers.otf.addfeature {
-- name = "mtest",
-- type = "multiple",
-- data = {
-- a = { "X", "Y" },
-- b = { "P", "Q" },
-- }
-- }
-- fonts.handlers.otf.addfeature {
-- name = "ltest",
-- type = "ligature",
-- data = {
-- X = { "a", "b" },
-- Y = { "d", "a" },
-- }
-- }
-- fonts.handlers.otf.addfeature {
-- name = "ktest",
-- type = "kern",
-- data = {
-- a = { b = -500 },
-- }
-- }