if not modules then modules = { } end modules ['font-def'] = { version = 1.001, comment = "companion to font-ini.tex", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } -- check reuse of lmroman1o-regular vs lmr10 --[[ldx--

Here we deal with defining fonts. We do so by intercepting the default loader that only handles .

--ldx]]-- fonts = fonts or { } fonts.define = fonts.define or { } fonts.tfm = fonts.tfm or { } fonts.vf = fonts.vf or { } fonts.used = fonts.used or { } fonts.tfm.version = 1.01 fonts.tfm.cache = containers.define("fonts", "tfm", fonts.tfm.version, false) -- better in font-tfm --[[ldx--

Choosing a font by name and specififying its size is only part of the game. In order to prevent complex commands, introduced a method to pass feature information as part of the font name. At the risk of introducing nasty parsing and compatinility problems, this syntax was expanded over time.

For the sake of users who have defined fonts using that syntax, we will support it, but we will provide additional methods as well. Normally users will not use this direct way, but use a more abstract interface.

--ldx]]-- --~ name, kind, features = fonts.features.split_xetex("blabla / B : + lnum ; foo = bar ; - whatever ; whow ; + hans ; test = yes") fonts.define.method = 3 -- 1: tfm 2: tfm and if not then afm 3: afm and if not then tfm fonts.define.auto_afm = true fonts.define.auto_otf = true fonts.define.specify = fonts.define.specify or { } fonts.define.splitsymbols = "" fonts.define.methods = fonts.define.methods or { } fonts.tfm.fonts = fonts.tfm.fonts or { } fonts.tfm.readers = fonts.tfm.readers or { } fonts.tfm.internalized = fonts.tfm.internalized or { } -- internal tex numbers fonts.tfm.id = fonts.tfm.id or { } -- font data, maybe use just fonts.ids (faster lookup) fonts.tfm.readers.sequence = { 'otf', 'ttf', 'afm', 'tfm' } --[[ldx--

We hardly gain anything when we cache the final (pre scaled) table. But it can be handy for debugging.

--ldx]]-- fonts.version = 1.05 fonts.cache = containers.define("fonts", "def", fonts.version, false) --[[ldx--

We can prefix a font specification by name: or file:. The first case will result in a lookup in the synonym table.

[ name: | file: ] identifier [ separator [ specification ] ]

The following function split the font specification into components and prepares a table that will move along as we proceed.

--ldx]]-- function fonts.define.analyze(name, size, id) name = name or 'unknown' local specification = name local lookup, rest = specification:match("^(.-):(.+)$") local sub = "" if lookup == 'file' or lookup == 'name' then name = rest else lookup = 'file' end local font, method, detail = name:match("^(.-)(["..fonts.define.splitsymbols.."])(.+)$") if method and detail then name = font else method, detail = "", "" end local mainfont, subfont = name:match("^(.*-)(%(.*-)(%)$") if mainfont and subfont then name, sub = mainfont, subfont end size = size or (65536*10) return { 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 id = id, -- font id features = { }, -- preprocessed features -- hash = nil -- filename = nil, -- encoding = nil, -- format = nil, } end function fonts.define.register_split(symbol,action) fonts.define.splitsymbols = fonts.define.splitsymbols .. "%" .. symbol fonts.define.specify[symbol] = action end --[[ldx--

A unique hash value is generated by:

--ldx]]-- function fonts.tfm.hash_features(specification) if specification.features then local t = { } local normal = specification.features.normal if not table.is_empty(normal) then for _, v in pairs(table.sortedkeys(normal)) do if v ~= "number" then t[#t+1] = v .. '=' .. tostring(normal[v]) end end end local vtf = specification.features.vtf if not table.is_empty(vtf) then for _, v in pairs(table.sortedkeys(vtf)) do t[#t+1] = v .. '=' .. tostring(vtf[v]) end end if next(t) then return table.concat(t,"+") end end return "unknown" end --~ function fonts.tfm.hash_instance(specification) --~ if not specification.hash then --~ specification.hash = fonts.tfm.hash_features(specification) --~ end --~ return specification.hash .. ' @ ' .. tostring(specification.size) --~ end fonts.designsizes = { } --[[ldx--

In principle we can share tfm tables when we are in node for a font, but then we need to define a font switch as an id/attr switch which is no fun, so in that case users can best use dynamic features ... so, we will not use that speedup. Okay, when we get rid of base mode we can optimize even further by sharing, but then we loose our testcases for .

--ldx]]-- function fonts.tfm.hash_instance(specification) local hash, size = specification.hash, specification.size if not hash then hash = fonts.tfm.hash_features(specification) specification.hash = hash end if size < 1000 and fonts.designsizes[hash] then size = fonts.tfm.scaled(size, fonts.designsizes[hash]) specification.size = size end return hash .. ' @ ' .. tostring(size) end --[[ldx--

We can resolve the filename using the next function:

--ldx]]-- function fonts.define.resolve(specification) if specification.lookup == 'name' then specification.resolved, specification.sub = fonts.names.resolve(specification.name,specification.sub) if specification.resolved then specification.forced = file.extname(specification.resolved) specification.name = file.removesuffix(specification.resolved) end elseif specification.lookup == 'file' then specification.forced = file.extname(specification.name) specification.name = file.removesuffix(specification.name) end if specification.forced == "" then specification.forced = nil end specification.hash = specification.name .. ' @ ' .. fonts.tfm.hash_features(specification) if specification.sub and specification.sub ~= "" then specification.hash = specification.sub .. ' @ ' .. specification.hash end return specification end --[[ldx--

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.

We need to cache when possible. We do cache raw tfm data (from , or ). After that we can cache based on specificstion (name) and size, that is, only needs a number for an already loaded fonts. However, it may make sense to cache fonts before they're scaled as well (store '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.

Watch out, here we do load a font, but we don't prepare the specification yet.

--ldx]]-- function fonts.tfm.read(specification) garbagecollector.push() input.starttiming(fonts) local hash = fonts.tfm.hash_instance(specification) local tfmtable = fonts.tfm.fonts[hash] -- hashes by size ! if not tfmtable then if specification.forced and specification.forced ~= "" then tfmtable = fonts.tfm.readers[specification.forced](specification) if not tfmtable then logs.error("define font",string.format("forced type %s of %s not found",specification.forced,specification.name)) end else for _, reader in ipairs(fonts.tfm.readers.sequence) do if fonts.tfm.readers[reader] then -- not really needed if fonts.trace then logs.report("define font",string.format("trying type %s for %s with file %s",reader,specification.name,specification.filename or "unknown")) end tfmtable = fonts.tfm.readers[reader](specification) if tfmtable then break end end end end if tfmtable then if tfmtable.filename and fonts.dontembed[tfmtable.filename] then tfmtable.embedding = "no" else tfmtable.embedding = "subset" end end fonts.tfm.fonts[hash] = tfmtable fonts.designsizes[specification.hash] = tfmtable.designsize -- we only know this for sure after loading once --~ tfmtable.mode = specification.features.normal.mode or "base" end input.stoptiming(fonts) garbagecollector.pop() if not tfmtable then logs.error("define font",string.format("font with name %s is not found",specification.name)) end return tfmtable end --[[ldx--

For virtual fonts we need a slightly different approach:

--ldx]]-- function fonts.tfm.read_and_define(name,size) -- no id local specification = fonts.define.analyze(name,size,nil) if specification.method and fonts.define.specify[specification.method] then specification = fonts.define.specify[specification.method](specification) end specification = fonts.define.resolve(specification) local hash = fonts.tfm.hash_instance(specification) local id = fonts.tfm.internalized[hash] if not id then local fontdata = fonts.tfm.read(specification) if fontdata then if not fonts.tfm.internalized[hash] then id = font.define(fontdata) fonts.tfm.id[id] = fontdata fonts.tfm.internalized[hash] = id if fonts.trace then logs.report("define font", string.format("at 1 id %s, hash: %s",id,hash)) end else id = fonts.tfm.internalized[hash] end else id = 0 -- signal end end return fonts.tfm.id[id], id end --[[ldx--

A naive callback could be the following:

callback.register('define_font', function(name,size,id) return fonts.define.read(fonts.define.resolve(fonts.define.analyze(name,size,id))) end) --ldx]]-- --[[ldx--

Next follow the readers. This code was written while evolved. Each one has its own way of dealing with its format.

--ldx]]-- function fonts.tfm.readers.opentype(specification,suffix,what) if fonts.define.auto_otf then local fullname, tfmtable = nil, nil fullname = input.findbinfile(texmf.instance,specification.name,suffix) if fullname and fullname ~= "" then specification.filename, specification.format = fullname, what -- hm, so we do set the filename, then tfmtable = fonts.tfm.read_from_open_type(specification) -- we need to do it for all matches / todo end return tfmtable else return nil end end function fonts.tfm.readers.otf(specification) return fonts.tfm.readers.opentype(specification,"otf","opentype") end function fonts.tfm.readers.ttf(specification) return fonts.tfm.readers.opentype(specification,"ttf","truetype") end function fonts.tfm.readers.ttc(specification) return fonts.tfm.readers.opentype(specification,"ttf","truetype") end -- !! function fonts.tfm.readers.afm(specification,method) local fullname, tfmtable = nil, nil method = method or fonts.define.method if method == 2 then fullname = input.findbinfile(texmf.instance,specification.name,"ofm") or "" if fullname == "" then tfmtable = fonts.tfm.read_from_afm(specification) else -- redundant specification.filename = fullname tfmtable = fonts.tfm.read_from_tfm(specification) end elseif method == 3 then -- maybe also findbinfile here if fonts.define.auto_afm then tfmtable = fonts.tfm.read_from_afm(specification) end elseif method == 4 then -- maybe also findbinfile here tfmtable = fonts.tfm.read_from_afm(specification) end return tfmtable end function fonts.tfm.readers.tfm(specification) local fullname, tfmtable = nil, nil tfmtable = fonts.tfm.read_from_tfm(specification) return tfmtable end --[[ldx--

So far we haven't really dealt with features (or whatever we want to pass along with the font definition. We distinguish the following situations:

situations:

name:xetex like specs name@virtual font spec name*context specification

Of course one can always define more.

--ldx]]-- function fonts.define.specify.predefined(specification) if specification.detail ~= "" and fonts.define.methods[specification.detail] then specification.features.vtf = { preset = specification.detail } end return specification end fonts.define.register_split("@", fonts.define.specify.predefined) function fonts.define.specify.colonized(specification) -- xetex mode local list = { } if specification.detail and specification.detail ~= "" then local expanded_features = { } local function expand(features) for _,v in pairs(features:split(";")) do expanded_features[#expanded_features+1] = v end end expand(specification.detail) for _,v in pairs(expanded_features) do local a, b = v:match("^%s*(%S+)%s*=%s*(%S+)%s*$") if a and b then list[a] = b:is_boolean() if type(list[a]) == "nil" then list[a] = b end else local a, b = v:match("^%s*([%+%-]?)%s*(%S+)%s*$") if a and b then list[b] = a ~= "-" end end end end specification.features.normal = list return specification end function fonts.tfm.make(specification) local fvm = fonts.define.methods[specification.features.vtf.preset] if fvm then return fvm(specification) else return nil end end fonts.define.register_split(":", fonts.define.specify.colonized) fonts.define.specify.context_setups = fonts.define.specify.context_setups or { } fonts.define.specify.context_numbers = fonts.define.specify.context_numbers or { } fonts.define.specify.synonyms = fonts.define.specify.synonyms or { } input.storage.register(false,"fonts/setups" , fonts.define.specify.context_setups , "fonts.define.specify.context_setups" ) input.storage.register(false,"fonts/numbers", fonts.define.specify.context_numbers, "fonts.define.specify.context_numbers") function fonts.define.specify.preset_context(name,features) local fds = fonts.define.specify local setups, numbers, synonyms = fds.context_setups, fds.context_numbers, fds.synonyms local number = (setups[name] and setups[name].number) or 0 --~ local t = aux.settings_to_hash(features) --~ for k,v in pairs(t) do --~ k = synonyms[k] or k --~ t[k] = v:is_boolean() --~ if type(t[k]) == "nil" then --~ t[k] = v --~ end --~ end local t = fonts.otf.meanings.resolve(aux.settings_to_hash(features)) -- todo: synonyms if number == 0 then numbers[#numbers+1] = name t.number = #numbers else t.number = number end setups[name] = t end --~ function fonts.define.specify.context_number(name) --~ local s = fonts.define.specify.context_setups[name] --~ return (s and s.number) or -1 --~ end do -- here we clone features according to languages local default = 0 local setups = fonts.define.specify.context_setups local numbers = fonts.define.specify.context_numbers function fonts.define.specify.context_number(name) local t = setups[name] if not t then return default elseif t.auto then local lng = tonumber(tex.language) local tag = name .. ":" .. lng local s = setups[tag] if s then return s.number or default else local script, language = languages.association(lng) if t.script ~= script or t.language ~= language then local s = table.fastcopy(t) local n = #numbers + 1 setups[tag] = s numbers[n] = tag s.number = n s.script = script s.language = language return n else setups[tag] = t return t.number or default end end else return t.number or default end end end function fonts.define.specify.context_tostring(name,kind,separator,yes,no,strict,omit) return aux.hash_to_string(table.merged(fonts[kind].features.default or {},fonts.define.specify.context_setups[name] or {}),separator,yes,no,strict,omit) end function fonts.define.specify.split_context(features) if fonts.define.specify.context_setups[features] then return fonts.define.specify.context_setups[features] else -- ? ? ? return fonts.define.specify.preset_context("***",features) end end function fonts.define.specify.starred(features) if features.detail and features.detail ~= "" then features.features.normal = fonts.define.specify.split_context(features.detail) else features.features.normal = { } end return features end fonts.define.register_split('*',fonts.define.specify.starred) --[[ldx--

We need to check for default features. For this we provide a helper function.

--ldx]]-- function fonts.define.check(features,defaults) if table.is_empty(features) then features = table.fastcopy(defaults) -- we could do without copy else for k,v in pairs(defaults) do if features[k] == nil then features[k] = v end end end return features end --[[ldx--

So far the specifyers. 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).

In the previously defined reader (the one resulting in a 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.

--ldx]]-- function fonts.define.read(name,size,id) local specification = fonts.define.analyze(name,size,id) if specification.method and fonts.define.specify[specification.method] then specification = fonts.define.specify[specification.method](specification) end specification = fonts.define.resolve(specification) local hash = fonts.tfm.hash_instance(specification) if true then --~ local fontdata = containers.read(fonts.cache,hash) -- for tracing purposes end local fontdata = fonts.tfm.internalized[hash] -- id if not fontdata then if specification.features.vtf and specification.features.vtf.preset then fontdata = fonts.tfm.make(specification) else fontdata = fonts.tfm.read(specification) fonts.tfm.check_virtual_id(fontdata) end if true then --~ fontdata = containers.write(fonts.cache,hash,fontdata) -- for tracing purposes end if not fonts.tfm.internalized[hash] then fonts.tfm.id[id] = fontdata fonts.tfm.internalized[hash] = id if fonts.trace then logs.report("define font", string.format("at 2 id %s, hash: %s",id,hash)) end else fontdata = fonts.tfm.internalized[hash] end end if not fontdata then logs.error("define font", string.format("name: %s, loading aborted",specification.name)) elseif fonts.trace and type(fontdata) == "table" then logs.report("use font",string.format("%s font n:%s s:%s b:%s e:%s p:%s f:%s", fontdata.type or "unknown", fontdata.name or "?", fontdata.size or "default", fontdata.encodingbytes or "?", fontdata.encodingname or "unicode", fontdata.fullname or "?", file.basename(fontdata.filename or "?"))) end return fontdata end --~ table.insert(fonts.tfm.readers.sequence,1,'vtf') --~ function fonts.tfm.readers.vtf(specification) --~ if specification.features.vtf and specification.features.vtf.preset then --~ return fonts.tfm.make(specification) --~ else --~ return nil --~ end --~ end function fonts.vf.find(name) name = file.removesuffix(file.basename(name)) if fonts.tfm.resolve_vf then local format = fonts.logger.format(name) if format == 'tfm' or format == 'ofm' then if fonts.trace then logs.report("define font",string.format("locating vf for %s",name)) end return input.findbinfile(texmf.instance,name,"ovf") else if fonts.trace then logs.report("define font",string.format("vf for %s is already taken care of",name)) end return nil -- "" end else if fonts.trace then logs.report("define font",string.format("locating vf for %s",name)) end return input.findbinfile(texmf.instance,name,"ovf") end end --[[ldx--

We overload both the and readers.

--ldx]]-- callback.register('define_font' , fonts.define.read) callback.register('find_vf_file', fonts.vf.find ) -- not that relevant any more