You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
532 lines
20 KiB
532 lines
20 KiB
if not modules then modules = { } end modules ['font-def'] = {
|
|
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"
|
|
}
|
|
|
|
-- We can overload some of the definers.functions so we don't local them.
|
|
|
|
local lower, gsub = string.lower, string.gsub
|
|
local tostring, next = tostring, next
|
|
local lpegmatch = lpeg.match
|
|
local suffixonly, removesuffix, basename = file.suffix, file.removesuffix, file.basename
|
|
local formatters = string.formatters
|
|
local sortedhash, sortedkeys = table.sortedhash, table.sortedkeys
|
|
|
|
local allocate = utilities.storage.allocate
|
|
|
|
local trace_defining = false trackers .register("fonts.defining", function(v) trace_defining = v end)
|
|
local directive_embedall = false directives.register("fonts.embedall", function(v) directive_embedall = v end)
|
|
|
|
trackers.register("fonts.loading", "fonts.defining", "otf.loading", "afm.loading", "tfm.loading")
|
|
|
|
local report_defining = logs.reporter("fonts","defining")
|
|
|
|
--[[ldx--
|
|
<p>Here we deal with defining fonts. We do so by intercepting the
|
|
default loader that only handles <l n='tfm'/>.</p>
|
|
--ldx]]--
|
|
|
|
local fonts = fonts
|
|
local fontdata = fonts.hashes.identifiers
|
|
local readers = fonts.readers
|
|
local definers = fonts.definers
|
|
local specifiers = fonts.specifiers
|
|
local constructors = fonts.constructors
|
|
local fontgoodies = fonts.goodies
|
|
|
|
readers.sequence = allocate { 'otf', 'ttf', 'afm', 'tfm', 'lua' } -- dfont ttc
|
|
|
|
local variants = allocate()
|
|
specifiers.variants = variants
|
|
|
|
definers.methods = definers.methods or { }
|
|
|
|
local internalized = allocate() -- internal tex numbers (private)
|
|
|
|
local loadedfonts = constructors.loadedfonts
|
|
local designsizes = constructors.designsizes
|
|
|
|
-- not in generic (some day I'll make two defs, one for context, one for generic)
|
|
|
|
local resolvefile = fontgoodies and fontgoodies.filenames and fontgoodies.filenames.resolve or function(s) return s end
|
|
|
|
--[[ldx--
|
|
<p>We hardly gain anything when we cache the final (pre scaled)
|
|
<l n='tfm'/> table. But it can be handy for debugging, so we no
|
|
longer carry this code along. Also, we now have quite some reference
|
|
to other tables so we would end up with lots of catches.</p>
|
|
--ldx]]--
|
|
|
|
--[[ldx--
|
|
<p>We can prefix a font specification by <type>name:</type> or
|
|
<type>file:</type>. The first case will result in a lookup in the
|
|
synonym table.</p>
|
|
|
|
<typing>
|
|
[ name: | file: ] identifier [ separator [ specification ] ]
|
|
</typing>
|
|
|
|
<p>The following function split the font specification into components
|
|
and prepares a table that will move along as we proceed.</p>
|
|
--ldx]]--
|
|
|
|
-- beware, we discard additional specs
|
|
--
|
|
-- method:name method:name(sub) method:name(sub)*spec method:name*spec
|
|
-- name name(sub) name(sub)*spec name*spec
|
|
-- name@spec*oeps
|
|
|
|
local function makespecification(specification,lookup,name,sub,method,detail,size)
|
|
size = size or 655360
|
|
if not lookup or lookup == "" then
|
|
lookup = definers.defaultlookup
|
|
end
|
|
if trace_defining then
|
|
report_defining("specification %a, lookup %a, name %a, sub %a, method %a, detail %a",
|
|
specification, lookup, name, sub, method, detail)
|
|
end
|
|
local t = {
|
|
lookup = lookup, -- forced type
|
|
specification = specification, -- full specification
|
|
size = size, -- size in scaled points or -1000*n
|
|
name = name, -- font or filename
|
|
sub = sub, -- subfont (eg in ttc)
|
|
method = method, -- specification method
|
|
detail = detail, -- specification
|
|
resolved = "", -- resolved font name
|
|
forced = "", -- forced loader
|
|
features = { }, -- preprocessed features
|
|
}
|
|
return t
|
|
end
|
|
|
|
definers.makespecification = makespecification
|
|
|
|
if context then
|
|
|
|
local splitter, splitspecifiers = nil, "" -- not so nice
|
|
|
|
local P, C, S, Cc, Cs = lpeg.P, lpeg.C, lpeg.S, lpeg.Cc, lpeg.Cs
|
|
|
|
local left = P("(")
|
|
local right = P(")")
|
|
local colon = P(":")
|
|
local space = P(" ")
|
|
local lbrace = P("{")
|
|
local rbrace = P("}")
|
|
|
|
definers.defaultlookup = "file"
|
|
|
|
local prefixpattern = P(false)
|
|
|
|
local function addspecifier(symbol)
|
|
splitspecifiers = splitspecifiers .. symbol
|
|
local method = S(splitspecifiers)
|
|
local lookup = C(prefixpattern) * colon
|
|
local sub = left * C(P(1-left-right-method)^1) * right
|
|
local specification = C(method) * C(P(1)^1)
|
|
local name = Cs((lbrace/"") * (1-rbrace)^1 * (rbrace/"") + (1-sub-specification)^1)
|
|
splitter = P((lookup + Cc("")) * name * (sub + Cc("")) * (specification + Cc("")))
|
|
end
|
|
|
|
local function addlookup(str)
|
|
prefixpattern = prefixpattern + P(str)
|
|
end
|
|
|
|
definers.addlookup = addlookup
|
|
|
|
addlookup("file")
|
|
addlookup("name")
|
|
addlookup("spec")
|
|
|
|
local function getspecification(str)
|
|
return lpegmatch(splitter,str or "") -- weird catch
|
|
end
|
|
|
|
definers.getspecification = getspecification
|
|
|
|
function definers.registersplit(symbol,action,verbosename)
|
|
addspecifier(symbol)
|
|
variants[symbol] = action
|
|
if verbosename then
|
|
variants[verbosename] = action
|
|
end
|
|
end
|
|
|
|
function definers.analyze(specification, size)
|
|
-- can be optimized with locals
|
|
local lookup, name, sub, method, detail = getspecification(specification or "")
|
|
return makespecification(specification, lookup, name, sub, method, detail, size)
|
|
end
|
|
|
|
end
|
|
|
|
--[[ldx--
|
|
<p>We can resolve the filename using the next function:</p>
|
|
--ldx]]--
|
|
|
|
definers.resolvers = definers.resolvers or { }
|
|
local resolvers = definers.resolvers
|
|
|
|
-- todo: reporter
|
|
|
|
function resolvers.file(specification)
|
|
local name = resolvefile(specification.name) -- catch for renames
|
|
local suffix = lower(suffixonly(name))
|
|
if fonts.formats[suffix] then
|
|
specification.forced = suffix
|
|
specification.forcedname = name
|
|
specification.name = removesuffix(name)
|
|
else
|
|
specification.name = name -- can be resolved
|
|
end
|
|
end
|
|
|
|
function resolvers.name(specification)
|
|
local resolve = fonts.names.resolve
|
|
if resolve then
|
|
local resolved, sub, subindex, instance = resolve(specification.name,specification.sub,specification) -- we pass specification for overloaded versions
|
|
if resolved then
|
|
specification.resolved = resolved
|
|
specification.sub = sub
|
|
specification.subindex = subindex
|
|
-- new, needed for experiments
|
|
if instance then
|
|
specification.instance = instance
|
|
local features = specification.features
|
|
if not features then
|
|
features = { }
|
|
specification.features = features
|
|
end
|
|
local normal = features.normal
|
|
if not normal then
|
|
normal = { }
|
|
features.normal = normal
|
|
end
|
|
normal.instance = instance
|
|
-- if not callbacks.supported.glyph_stream_provider then
|
|
-- normal.variableshapes = true -- for the moment
|
|
-- end
|
|
end
|
|
--
|
|
local suffix = lower(suffixonly(resolved))
|
|
if fonts.formats[suffix] then
|
|
specification.forced = suffix
|
|
specification.forcedname = resolved
|
|
specification.name = removesuffix(resolved)
|
|
else
|
|
specification.name = resolved
|
|
end
|
|
end
|
|
else
|
|
resolvers.file(specification)
|
|
end
|
|
end
|
|
|
|
function resolvers.spec(specification)
|
|
local resolvespec = fonts.names.resolvespec
|
|
if resolvespec then
|
|
local resolved, sub, subindex = resolvespec(specification.name,specification.sub,specification) -- we pass specification for overloaded versions
|
|
if resolved then
|
|
specification.resolved = resolved
|
|
specification.sub = sub
|
|
specification.subindex = subindex
|
|
specification.forced = lower(suffixonly(resolved))
|
|
specification.forcedname = resolved
|
|
specification.name = removesuffix(resolved)
|
|
end
|
|
else
|
|
resolvers.name(specification)
|
|
end
|
|
end
|
|
|
|
function definers.resolve(specification)
|
|
if not specification.resolved or specification.resolved == "" then -- resolved itself not per se in mapping hash
|
|
local r = resolvers[specification.lookup]
|
|
if r then
|
|
r(specification)
|
|
end
|
|
end
|
|
if specification.forced == "" then
|
|
specification.forced = nil
|
|
specification.forcedname = nil
|
|
end
|
|
specification.hash = lower(specification.name .. ' @ ' .. constructors.hashfeatures(specification))
|
|
if specification.sub and specification.sub ~= "" then
|
|
specification.hash = specification.sub .. ' @ ' .. specification.hash
|
|
end
|
|
return specification
|
|
end
|
|
|
|
--[[ldx--
|
|
<p>The main read function either uses a forced reader (as determined by
|
|
a lookup) or tries to resolve the name using the list of readers.</p>
|
|
|
|
<p>We need to cache when possible. We do cache raw tfm data (from <l
|
|
n='tfm'/>, <l n='afm'/> or <l n='otf'/>). After that we can cache based
|
|
on specificstion (name) and size, that is, <l n='tex'/> only needs a number
|
|
for an already loaded fonts. However, it may make sense to cache fonts
|
|
before they're scaled as well (store <l n='tfm'/>'s with applied methods
|
|
and features). However, there may be a relation between the size and
|
|
features (esp in virtual fonts) so let's not do that now.</p>
|
|
|
|
<p>Watch out, here we do load a font, but we don't prepare the
|
|
specification yet.</p>
|
|
--ldx]]--
|
|
|
|
-- very experimental:
|
|
|
|
function definers.applypostprocessors(tfmdata)
|
|
local postprocessors = tfmdata.postprocessors
|
|
if postprocessors then
|
|
local properties = tfmdata.properties
|
|
for i=1,#postprocessors do
|
|
local extrahash = postprocessors[i](tfmdata) -- after scaling etc
|
|
if type(extrahash) == "string" and extrahash ~= "" then
|
|
-- e.g. a reencoding needs this
|
|
extrahash = gsub(lower(extrahash),"[^a-z]","-")
|
|
properties.fullname = formatters["%s-%s"](properties.fullname,extrahash)
|
|
end
|
|
end
|
|
end
|
|
return tfmdata
|
|
end
|
|
|
|
-- function definers.applypostprocessors(tfmdata)
|
|
-- return tfmdata
|
|
-- end
|
|
|
|
local function checkembedding(tfmdata)
|
|
local properties = tfmdata.properties
|
|
local embedding
|
|
if directive_embedall then
|
|
embedding = "full"
|
|
elseif properties and properties.filename and constructors.dontembed[properties.filename] then
|
|
embedding = "no"
|
|
else
|
|
embedding = "subset"
|
|
end
|
|
if properties then
|
|
properties.embedding = embedding
|
|
else
|
|
tfmdata.properties = { embedding = embedding }
|
|
end
|
|
tfmdata.embedding = embedding
|
|
end
|
|
|
|
local function checkfeatures(tfmdata)
|
|
local resources = tfmdata.resources
|
|
local shared = tfmdata.shared
|
|
if resources and shared then
|
|
local features = resources.features
|
|
local usedfeatures = shared.features
|
|
if features and usedfeatures then
|
|
local usedlanguage = usedfeatures.language or "dflt"
|
|
local usedscript = usedfeatures.script or "dflt"
|
|
local function check(what)
|
|
if what then
|
|
local foundlanguages = { }
|
|
for feature, scripts in next, what do
|
|
if usedscript == "auto" or scripts["*"] then
|
|
-- ok
|
|
elseif not scripts[usedscript] then
|
|
-- report_defining("font %!font:name!, feature %a, no script %a",
|
|
-- tfmdata,feature,usedscript)
|
|
else
|
|
for script, languages in next, scripts do
|
|
if languages["*"] then
|
|
-- ok
|
|
elseif context and not languages[usedlanguage] then
|
|
report_defining("font %!font:name!, feature %a, script %a, no language %a",
|
|
tfmdata,feature,script,usedlanguage)
|
|
end
|
|
end
|
|
end
|
|
for script, languages in next, scripts do
|
|
for language in next, languages do
|
|
foundlanguages[language] = true
|
|
end
|
|
end
|
|
end
|
|
if false then
|
|
foundlanguages["*"] = nil
|
|
foundlanguages = sortedkeys(foundlanguages)
|
|
for feature, scripts in sortedhash(what) do
|
|
for script, languages in next, scripts do
|
|
if not languages["*"] then
|
|
for i=1,#foundlanguages do
|
|
local language = foundlanguages[i]
|
|
if context and not languages[language] then
|
|
report_defining("font %!font:name!, feature %a, script %a, no language %a",
|
|
tfmdata,feature,script,language)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
check(features.gsub)
|
|
check(features.gpos)
|
|
end
|
|
end
|
|
end
|
|
|
|
function definers.loadfont(specification)
|
|
local hash = constructors.hashinstance(specification)
|
|
-- todo: also hash by instance / factors
|
|
local tfmdata = loadedfonts[hash] -- hashes by size !
|
|
if not tfmdata then
|
|
-- normally context will not end up here often (if so there is an issue somewhere)
|
|
local forced = specification.forced or ""
|
|
if forced ~= "" then
|
|
local reader = readers[lower(forced)] -- normally forced is already lowered
|
|
tfmdata = reader and reader(specification)
|
|
if not tfmdata then
|
|
report_defining("forced type %a of %a not found",forced,specification.name)
|
|
end
|
|
else
|
|
local sequence = readers.sequence -- can be overloaded so only a shortcut here
|
|
for s=1,#sequence do
|
|
local reader = sequence[s]
|
|
if readers[reader] then -- we skip not loaded readers
|
|
if trace_defining then
|
|
report_defining("trying (reader sequence driven) type %a for %a with file %a",reader,specification.name,specification.filename)
|
|
end
|
|
tfmdata = readers[reader](specification)
|
|
if tfmdata then
|
|
break
|
|
else
|
|
specification.filename = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if tfmdata then
|
|
tfmdata = definers.applypostprocessors(tfmdata)
|
|
checkembedding(tfmdata) -- todo: general postprocessor
|
|
loadedfonts[hash] = tfmdata
|
|
designsizes[specification.hash] = tfmdata.parameters.designsize
|
|
checkfeatures(tfmdata)
|
|
end
|
|
end
|
|
if not tfmdata then
|
|
report_defining("font with asked name %a is not found using lookup %a",specification.name,specification.lookup)
|
|
end
|
|
return tfmdata
|
|
end
|
|
|
|
function constructors.readanddefine(name,size) -- no id -- maybe a dummy first
|
|
local specification = definers.analyze(name,size)
|
|
local method = specification.method
|
|
if method and variants[method] then
|
|
specification = variants[method](specification)
|
|
end
|
|
specification = definers.resolve(specification)
|
|
local hash = constructors.hashinstance(specification)
|
|
local id = definers.registered(hash)
|
|
if not id then
|
|
local tfmdata = definers.loadfont(specification)
|
|
if tfmdata then
|
|
tfmdata.properties.hash = hash
|
|
id = font.define(tfmdata)
|
|
definers.register(tfmdata,id)
|
|
else
|
|
id = 0 -- signal
|
|
end
|
|
end
|
|
return fontdata[id], id
|
|
end
|
|
|
|
--[[ldx--
|
|
<p>So far the specifiers. Now comes the real definer. Here we cache
|
|
based on id's. Here we also intercept the virtual font handler. Since
|
|
it evolved stepwise I may rewrite this bit (combine code).</p>
|
|
|
|
In the previously defined reader (the one resulting in a <l n='tfm'/>
|
|
table) we cached the (scaled) instances. Here we cache them again, but
|
|
this time based on id. We could combine this in one cache but this does
|
|
not gain much. By the way, passing id's back to in the callback was
|
|
introduced later in the development.</p>
|
|
--ldx]]--
|
|
|
|
function definers.registered(hash)
|
|
local id = internalized[hash]
|
|
return id, id and fontdata[id]
|
|
end
|
|
|
|
function definers.register(tfmdata,id)
|
|
if tfmdata and id then
|
|
local hash = tfmdata.properties.hash
|
|
if not hash then
|
|
report_defining("registering font, id %a, name %a, invalid hash",id,tfmdata.properties.filename or "?")
|
|
elseif not internalized[hash] then
|
|
internalized[hash] = id
|
|
if trace_defining then
|
|
report_defining("registering font, id %s, hash %a",id,hash)
|
|
end
|
|
fontdata[id] = tfmdata
|
|
end
|
|
end
|
|
end
|
|
|
|
function definers.read(specification,size,id) -- id can be optional, name can already be table
|
|
statistics.starttiming(fonts)
|
|
if type(specification) == "string" then
|
|
specification = definers.analyze(specification,size)
|
|
end
|
|
local method = specification.method
|
|
if method and variants[method] then
|
|
specification = variants[method](specification)
|
|
end
|
|
specification = definers.resolve(specification)
|
|
local hash = constructors.hashinstance(specification)
|
|
local tfmdata = definers.registered(hash) -- id
|
|
if tfmdata then
|
|
if trace_defining then
|
|
report_defining("already hashed: %s",hash)
|
|
end
|
|
else
|
|
tfmdata = definers.loadfont(specification) -- can be overloaded
|
|
if tfmdata then
|
|
if trace_defining then
|
|
report_defining("loaded and hashed: %s",hash)
|
|
end
|
|
tfmdata.properties.hash = hash
|
|
if id then
|
|
definers.register(tfmdata,id)
|
|
end
|
|
else
|
|
if trace_defining then
|
|
report_defining("not loaded and hashed: %s",hash)
|
|
end
|
|
end
|
|
end
|
|
if not tfmdata then -- or id?
|
|
report_defining( "unknown font %a, loading aborted",specification.name)
|
|
elseif trace_defining and type(tfmdata) == "table" then
|
|
local properties = tfmdata.properties or { }
|
|
local parameters = tfmdata.parameters or { }
|
|
report_defining("using %a font with id %a, name %a, size %a, bytes %a, encoding %a, fullname %a, filename %a",
|
|
properties.format or "unknown", id or "-", properties.name, parameters.size, properties.encodingbytes,
|
|
properties.encodingname, properties.fullname, basename(properties.filename))
|
|
end
|
|
statistics.stoptiming(fonts)
|
|
return tfmdata
|
|
end
|
|
|
|
function font.getfont(id)
|
|
return fontdata[id] -- otherwise issues
|
|
end
|
|
|
|
--[[ldx--
|
|
<p>We overload the <l n='tfm'/> reader.</p>
|
|
--ldx]]--
|
|
|
|
if not context then
|
|
callbacks.register('define_font', definers.read, "definition of fonts (tfmdata preparation)")
|
|
end
|
|
|