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
Choosing a font by name and specififying its size is only part of the
game. In order to prevent complex commands,
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)
We can prefix a font specification by
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
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
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
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 aWe overload both the