if not modules then modules = { } end modules ['font-ctx'] = { 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" } -- At some point I will clean up the code here so that at the tex end -- the table interface is used. -- -- Todo: make a proper 'next id' mechanism (register etc) or wait till 'true' -- in virtual fonts indices is implemented. -- -- Olders code can be found in the mkiv lua variant. local tostring, next, type, rawget, tonumber = tostring, next, type, rawget, tonumber local format, gmatch, match, find, lower, upper, gsub, byte, topattern = string.format, string.gmatch, string.match, string.find, string.lower, string.upper, string.gsub, string.byte, string.topattern local concat, serialize, sort, fastcopy, mergedtable = table.concat, table.serialize, table.sort, table.fastcopy, table.merged local sortedhash, sortedkeys, sequenced = table.sortedhash, table.sortedkeys, table.sequenced local parsers = utilities.parsers local settings_to_hash, hash_to_string, settings_to_array = parsers.settings_to_hash, parsers.hash_to_string, parsers.settings_to_array local formatcolumns = utilities.formatters.formatcolumns local mergehashes = utilities.parsers.mergehashes local formatters = string.formatters local basename = file.basename local utfchar, utfbyte = utf.char, utf.byte local round = math.round local context, commands = context, commands local P, R, S, C, Cc, Ct, Cs, lpegmatch = lpeg.P, lpeg.R, lpeg.S, lpeg.C, lpeg.Cc, lpeg.Ct, lpeg.Cs, lpeg.match ----- Cf, Cg = lpeg.Cf, lpeg.Cg local trace_features = false trackers.register("fonts.features", function(v) trace_features = v end) local trace_defining = false trackers.register("fonts.defining", function(v) trace_defining = v end) local trace_designsize = false trackers.register("fonts.designsize", function(v) trace_designsize = v end) local trace_usage = false trackers.register("fonts.usage", function(v) trace_usage = v end) local trace_mapfiles = false trackers.register("fonts.mapfiles", function(v) trace_mapfiles = v end) local trace_automode = false trackers.register("fonts.automode", function(v) trace_automode = v end) local trace_merge = false trackers.register("fonts.merge", function(v) trace_merge = v end) local report = logs.reporter("fonts") local report_features = logs.reporter("fonts","features") local report_cummulative = logs.reporter("fonts","cummulative") local report_defining = logs.reporter("fonts","defining") local report_status = logs.reporter("fonts","status") local report_mapfiles = logs.reporter("fonts","mapfiles") local setmetatableindex = table.setmetatableindex local implement = interfaces.implement local chardata = characters.data local fonts = fonts local handlers = fonts.handlers local otf = handlers.otf -- brrr local afm = handlers.afm -- brrr local tfm = handlers.tfm -- brrr local names = fonts.names local definers = fonts.definers local specifiers = fonts.specifiers local constructors = fonts.constructors local loggers = fonts.loggers local fontgoodies = fonts.goodies local helpers = fonts.helpers local hashes = fonts.hashes local currentfont = font.current local definefont = font.define local getprivateslot = helpers.getprivateslot local cleanname = names.cleanname local encodings = fonts.encodings ----- aglunicodes = encodings.agl.unicodes local aglunicodes = nil -- delayed loading local nuts = nodes.nuts local tonut = nuts.tonut local nextchar = nuts.traversers.char local getattr = nuts.getattr local setattr = nuts.setattr local getstate = nuts.getstate local setsubtype = nuts.setsubtype local texgetdimen = tex.getdimen local texsetcount = tex.setcount local texiscount = tex.iscount local texget = tex.get local texdefinefont = tex.definefont local texsp = tex.sp local fontdata = hashes.identifiers local characters = hashes.characters local descriptions = hashes.descriptions local properties = hashes.properties local resources = hashes.resources local unicodes = hashes.unicodes local csnames = hashes.csnames local lastmathids = hashes.lastmathids local exheights = hashes.exheights local emwidths = hashes.emwidths local parameters = hashes.parameters local designsizefilename = fontgoodies.designsizes.filename local ctx_char = context.char local ctx_safechar = context.safechar local ctx_getvalue = context.getvalue local otffeatures = otf.features local otftables = otf.tables local registerotffeature = otffeatures.register local sequencers = utilities.sequencers local appendgroup = sequencers.appendgroup ----- appendaction = sequencers.appendaction specifiers.contextsetups = specifiers.contextsetups or { } specifiers.contextnumbers = specifiers.contextnumbers or { } specifiers.contextmerged = specifiers.contextmerged or { } specifiers.synonyms = specifiers.synonyms or { } local setups = specifiers.contextsetups local numbers = specifiers.contextnumbers local merged = specifiers.contextmerged local synonyms = specifiers.synonyms storage.register("fonts/setups" , setups , "fonts.specifiers.contextsetups" ) storage.register("fonts/numbers", numbers, "fonts.specifiers.contextnumbers") storage.register("fonts/merged", merged, "fonts.specifiers.contextmerged") storage.register("fonts/synonyms", synonyms, "fonts.specifiers.synonyms") -- inspect(setups) setmetatableindex(setups, environment.initex and function(t,k) return type(k) == "number" and rawget(t,numbers[k]) or nil end or function(t,k) local v = type(k) == "number" and rawget(t,numbers[k]) if v then t[k] = v return v end end ) -- this will move elsewhere ... local function getfontname(tfmdata) local p = type(tfmdata) == "number" and properties[tfmdata] or tfmdata.properties return basename((p and (p.name or p.fullname or p.fontname)) or "unknown") end helpers.name = getfontname local addformatter = utilities.strings.formatters.add addformatter(formatters,"font:name", [["'"..fontname(%s).."'"]], { fontname = helpers.name }) addformatter(formatters,"font:features",[["'"..sequenced(%s," ",true).."'"]],{ sequenced = table.sequenced }) -- ... like font-sfm or so constructors.resolvevirtualtoo = true -- context specific (due to resolver) constructors.sharefonts = true -- experimental constructors.nofsharedfonts = 0 constructors.nofsharedhashes = 0 constructors.nofsharedvectors = 0 constructors.noffontsloaded = 0 -- we can get rid of the tfm instance when we have fast access to the -- scaled character dimensions at the tex end, e.g. a fontobject.width -- actually we already have some of that now as virtual keys in glyphs -- -- flushing the kern and ligature tables from memory saves a lot (only -- base mode) but it complicates vf building where the new characters -- demand this data .. solution: functions that access them -- font.getcopy = font.getfont -- we always want the table that context uses local accuratefactors = false local accuratescale = false ----- compactfontmode = false experiments.register("fonts.accurate", function(v) accuratefactors = v end) experiments.register("fonts.compact", function() accuratefactors = true end) experiments.register("fonts.rescale", function() accuratescale = true end) do -- Does this still make sense? local shares = { } local hashes = { } local nofinstances = 0 local instances = setmetatableindex(function(t,k) nofinstances = nofinstances + 1 t[k] = nofinstances return nofinstances end) function constructors.trytosharefont(target,tfmdata) constructors.noffontsloaded = constructors.noffontsloaded + 1 if constructors.sharefonts then local fonthash = target.specification.hash if fonthash then local properties = target.properties local fullname = target.fullname local fontname = target.fontname local psname = target.psname -- for the moment here: local instance = properties.instance if instance then local format = tfmdata.properties.format if format == "opentype" then target.streamprovider = 1 elseif format == "truetype" then target.streamprovider = 2 else target.streamprovider = 0 end if target.streamprovider > 0 then if fullname then fullname = fullname .. ":" .. instances[instance] target.fullname = fullname end if fontname then fontname = fontname .. ":" .. instances[instance] target.fontname = fontname end if psname then -- this one is used for the funny prefix in font names in pdf -- so it has to be kind of unique in order to avoid subset prefix -- clashes being reported psname = psname .. ":" .. instances[instance] target.psname = psname end end end -- local sharedname = hashes[fonthash] if sharedname then -- this is ok for context as we know that only features can mess with font definitions -- so a similar hash means that the fonts are similar too if trace_defining then report_defining("font %a uses backend resources of font %a (%s)",target.fullname,sharedname,"common hash") end target.fullname = sharedname properties.sharedwith = sharedname constructors.nofsharedfonts = constructors.nofsharedfonts + 1 constructors.nofsharedhashes = constructors.nofsharedhashes + 1 else -- the one takes more time (in the worst case of many cjk fonts) but it also saves -- embedding time .. haha, this is interesting: when i got a clash on subset tag -- collision i saw in the source that these tags are also using a hash like below -- so maybe we should have an option to pass it from lua local characters = target.characters local n = 1 local t = { target.psname } -- for the moment here: if instance then n = n + 1 t[n] = instance end -- local u = sortedkeys(characters) for i=1,#u do local k = u[i] n = n + 1 ; t[n] = k n = n + 1 ; t[n] = characters[k].index or k end local checksum = md5.HEX(concat(t," ")) local sharedname = shares[checksum] local fullname = target.fullname if sharedname then if trace_defining then report_defining("font %a uses backend resources of font %a (%s)",fullname,sharedname,"common vector") end fullname = sharedname properties.sharedwith = sharedname constructors.nofsharedfonts = constructors.nofsharedfonts + 1 constructors.nofsharedvectors = constructors.nofsharedvectors + 1 else shares[checksum] = fullname end target.fullname = fullname hashes[fonthash] = fullname end end end end end directives.register("fonts.checksharing",function(v) if not v then report_defining("font sharing in backend is disabled") end constructors.sharefonts = v end) function definers.resetnullfont() -- resetting is needed because tikz misuses nullfont local parameters = fonts.nulldata.parameters -- parameters.slant = 0 -- 1 parameters.space = 0 -- 2 parameters.spacestretch = 0 -- 3 parameters.spaceshrink = 0 -- 4 parameters.xheight = 0 -- 5 parameters.quad = 0 -- 6 parameters.extraspace = 0 -- 7 parameters.designsize = 655360 -- constructors.enhanceparameters(parameters) -- official copies for us -- definers.resetnullfont = function() end end -- This is some initialization code and we don't want (tracing) clutter in lmtx -- which is why we follow a different route there. I admit that is sounds freaky. -- Watch out: in lmtx the font dimen array is no longer resized automatically. implement { name = "resetnullfont", onlyonce = true, permanent = false, actions = function() for i=1,7 do font.setfontdimen(0,i,0) end definers.resetnullfont() end } -- this cannot be a feature initializer as there is no auto namespace -- so we never enter the loop then; we can store the defaults in the tma -- file (features.gpos.mkmk = 1 etc) local needsnodemode = { -- we will have node mode by default anyway -- gsub_single = true, gsub_multiple = true, -- gsub_alternate = true, -- gsub_ligature = true, gsub_context = true, gsub_contextchain = true, gsub_reversecontextchain = true, -- chainsub = true, -- reversesub = true, gpos_mark2base = true, gpos_mark2ligature = true, gpos_mark2mark = true, gpos_cursive = true, -- gpos_single = true, -- gpos_pair = true, gpos_context = true, gpos_contextchain = true, } otftables.scripts.auto = "automatic fallback to latn when no dflt present" -- setmetatableindex(otffeatures.descriptions,otftables.features) local function checkedscript(tfmdata,resources,features) local latn = false local script = false if resources.features then for g, list in next, resources.features do for f, scripts in next, list do if scripts.dflt then script = "dflt" break elseif scripts.latn then latn = true end end end end if not script then script = latn and "latn" or "dflt" end if trace_automode then report_defining("auto script mode, using script %a in font %!font:name!",script,tfmdata) end features.script = script return script end -- basemode combined with dynamics is somewhat tricky local function checkedmode(tfmdata,resources,features) local sequences = resources.sequences if sequences and #sequences > 0 then local script = features.script or "dflt" local language = features.language or "dflt" for feature, value in next, features do if value then local found = false for i=1,#sequences do local sequence = sequences[i] local features = sequence.features if features then local scripts = features[feature] if scripts then local languages = scripts[script] if languages and languages[language] then if found then -- more than one lookup if trace_automode then report_defining("forcing mode %a, font %!font:name!, feature %a, script %a, language %a, %s", "node",tfmdata,feature,script,language,"multiple lookups") end features.mode = "node" return "node" elseif needsnodemode[sequence.type] then if trace_automode then report_defining("forcing mode %a, font %!font:name!, feature %a, script %a, language %a, %s", "node",tfmdata,feature,script,language,"no base support") end features.mode = "node" return "node" else -- at least one lookup found = true end end end end end end end end if trace_automode then report_defining("forcing mode base, font %!font:name!",tfmdata) end features.mode = "base" -- new, or is this wrong? return "base" end definers.checkedscript = checkedscript definers.checkedmode = checkedmode -- We only set the checker and leave other settings of the mode feature as -- they are. local function modechecker(tfmdata,features,mode) -- we cannot adapt features as they are shared! if trace_features then report_features("fontname %!font:name!, features %!font:features!",tfmdata,features) end local rawdata = tfmdata.shared.rawdata local resources = rawdata and rawdata.resources local script = features.script if resources then if script == "auto" then script = checkedscript(tfmdata,resources,features) end if mode == "auto" then mode = checkedmode(tfmdata,resources,features) end else report_features("missing resources for font %!font:name!",tfmdata) end return mode end registerotffeature { name = "mode", modechecker = modechecker, } do -- copying will end up here too local beforecopyingcharacters = sequencers.new { name = "beforecopyingcharacters", arguments = "target,original", } appendgroup(beforecopyingcharacters,"before") -- user appendgroup(beforecopyingcharacters,"system") -- private appendgroup(beforecopyingcharacters,"after" ) -- user function constructors.beforecopyingcharacters(original,target) local runner = beforecopyingcharacters.runner if runner then runner(original,target) end end local aftercopyingcharacters = sequencers.new { name = "aftercopyingcharacters", arguments = "target,original", } appendgroup(aftercopyingcharacters,"before") -- user appendgroup(aftercopyingcharacters,"system") -- private appendgroup(aftercopyingcharacters,"after" ) -- user function constructors.aftercopyingcharacters(original,target) local runner = aftercopyingcharacters.runner if runner then runner(original,target) end end local beforepassingfonttotex = sequencers.new { name = "beforepassingfonttotex", arguments = "tfmdata", } appendgroup(beforepassingfonttotex,"before") -- user appendgroup(beforepassingfonttotex,"system") -- private appendgroup(beforepassingfonttotex,"after" ) -- user function constructors.beforepassingfonttotex(tfmdata) local runner = beforepassingfonttotex.runner if runner then runner(tfmdata) end end end -- 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: -- -- name:xetex like specs -- name@virtual font spec -- name*context specification -- -- Currently fonts are scaled while constructing the font, so we have to do scaling -- of commands in the vf at that point using e.g. "local scale = g.parameters.factor -- or 1" after all, we need to work with copies anyway and scaling needs to be done -- at some point; however, when virtual tricks are used as feature (makes more -- sense) we scale the commands in fonts.constructors.scale (and set the factor -- there). local loadfont = definers.loadfont function definers.loadfont(specification,size,id) -- overloads the one in font-def local variants = definers.methods.variants local virtualfeatures = specification.features.virtual if virtualfeatures and virtualfeatures.preset then local variant = variants[virtualfeatures.preset] if variant then return variant(specification,size,id) end else return loadfont(specification,size,id) end end local function predefined(specification) local variants = definers.methods.variants local detail = specification.detail if detail ~= "" and variants[detail] then specification.features.virtual = { preset = detail } end return specification end definers.registersplit("@", predefined,"virtual") local normalize_features = otffeatures.normalize -- should be general local function definecontext(name,t) -- can be shared local number = setups[name] and setups[name].number or 0 -- hm, numbers[name] if number == 0 then number = #numbers + 1 numbers[number] = name end t.number = number setups[name] = t return number, t end -- {a,b,c} as table (so we don' need to parse again when it gets applied) -- we will update this ... when we have foo={a,b,c} then we can keep the table -- \definefontfeature[demo][a={b,c}] -- \definefontfeature[demo][a={b=12,c={34,35}}] local h = setmetatableindex(function(t,k) local v = "," .. k .. "," t[k] = v return v end) -- local function removefromhash(hash,key) -- local pattern = h[key] -- for k in next, hash do -- if k ~= key and find(h[k],pattern) then -- if find(k,",") and ... -- hash[k] = nil -- end -- end -- end local function presetcontext(name,parent,features) -- will go to con and shared if features == "" and find(parent,"=",1,true) then features = parent parent = "" end if not features or features == "" then features = { } elseif type(features) == "string" then features = normalize_features(settings_to_hash(features)) -- if type(value) == "string" and find(value,"[=:]") then -- local t = settings_to_hash_colon_too(value) -- clashes with foo=file:bar for key, value in next, features do if type(value) == "string" and find(value,"[=]") then local t = settings_to_hash(value) if next(t) then features[key] = sequenced(normalize_features(t,true),",") end end end else features = normalize_features(features) end -- todo: synonyms, and not otf bound if parent ~= "" then for p in gmatch(parent,"[^, ]+") do local s = setups[p] if s then for k, v in next, s do -- no, as then we cannot overload: e.g. math,mathextra -- reverted, so we only take from parent when not set if features[k] == nil then features[k] = v end end else -- just ignore an undefined one .. i.e. we can refer to not yet defined end end end -- these are auto set so in order to prevent redundant definitions -- we need to preset them (we hash the features and adding a default -- setting during initialization may result in a different hash) -- -- for k,v in next, triggers do -- if features[v] == nil then -- not false ! -- local vv = default_features[v] -- if vv then features[v] = vv end -- end -- end -- for feature,value in next, features do if value == nil then -- not false ! local default = default_features[feature] if default ~= nil then features[feature] = default end end end -- sparse 'm so that we get a better hash and less test (experimental -- optimization) local t = { } -- can we avoid t ? for k,v in next, features do -- if v then t[k] = v end t[k] = v end -- the number is needed for dynamic features; maybe number should always be -- renewed as we can redefine features ... i need a test local number = setups[name] and setups[name].number or 0 if number == 0 then number = #numbers + 1 numbers[number] = name end -- t.number = number -- there is the special case of combined features as we have in math but maybe -- this has to change some day ... otherwise we mess up dynamics (ok, we could -- impose a limit there: no combined features) -- -- done elsewhere (!) -- -- removefromhash(setups,name) -- can have changed (like foo,extramath) -- setups[name] = t return number, t end local function adaptcontext(pattern,features) local pattern = topattern(pattern,false,true) for name in next, setups do if find(name,pattern) then presetcontext(name,name,features) end end end -- See for old code in the lua file. We now share code. local function contextnumber(name) local t = setups[name] return t and t.number or 0 end local function mergecontext(currentnumber,extraname,option) -- number string number (used in scrp-ini local extra = setups[extraname] if extra then local current = setups[numbers[currentnumber]] local mergedfeatures = { } local mergedname = nil if option < 0 then if current then for k, v in next, current do if not extra[k] then mergedfeatures[k] = v end end end mergedname = currentnumber .. "-" .. extraname else if current then for k, v in next, current do mergedfeatures[k] = v end end for k, v in next, extra do mergedfeatures[k] = v end mergedname = currentnumber .. "+" .. extraname end local number = #numbers + 1 mergedfeatures.number = number numbers[number] = mergedname merged[number] = option setups[mergedname] = mergedfeatures return number else return currentnumber end end local extrasets = { } setmetatableindex(extrasets,function(t,k) local v = mergehashes(setups,k) t[k] = v return v end) local function mergecontextfeatures(currentname,extraname,how,mergedname) -- string string local extra = setups[extraname] or extrasets[extraname] if extra then local current = setups[currentname] local mergedfeatures = { } if how == "+" then if current then for k, v in next, current do mergedfeatures[k] = v end end for k, v in next, extra do mergedfeatures[k] = v end if trace_merge then report_features("merge %a, method %a, current %|T, extra %|T, result %|T",mergedname,"add",current or { },extra,mergedfeatures) end elseif how == "-" then if current then for k, v in next, current do mergedfeatures[k] = v end end for k, v in next, extra do -- only boolean features if v == true then mergedfeatures[k] = false end end if trace_merge then report_features("merge %a, method %a, current %|T, extra %|T, result %|T",mergedname,"subtract",current or { },extra,mergedfeatures) end else -- = for k, v in next, extra do mergedfeatures[k] = v end if trace_merge then report_features("merge %a, method %a, result %|T",mergedname,"replace",mergedfeatures) end end local number = #numbers + 1 mergedfeatures.number = number numbers[number] = mergedname merged[number] = option setups[mergedname] = mergedfeatures return number else return numbers[currentname] or 0 end end local function registercontext(fontnumber,extraname,option) local extra = setups[extraname] if extra then local mergedfeatures = { } local mergedname = nil if option < 0 then mergedname = fontnumber .. "-" .. extraname else mergedname = fontnumber .. "+" .. extraname end for k, v in next, extra do mergedfeatures[k] = v end local number = #numbers + 1 mergedfeatures.number = number numbers[number] = mergedname merged[number] = option setups[mergedname] = mergedfeatures return number else return 0 end end local function registercontextfeature(mergedname,extraname,how) local extra = setups[extraname] if extra then local mergedfeatures = { } for k, v in next, extra do mergedfeatures[k] = v end local number = #numbers + 1 -- we somehow end up with steps of 2 mergedfeatures.number = number numbers[number] = mergedname merged[number] = how == "=" and 1 or 2 -- 1=replace, 2=combine setups[mergedname] = mergedfeatures return number else report_features("unknown feature %a cannot be merged into %a using method %a",extraname,mergedname,how) return 0 end end specifiers.presetcontext = presetcontext specifiers.contextnumber = contextnumber specifiers.mergecontext = mergecontext specifiers.registercontext = registercontext specifiers.definecontext = definecontext constructors.hashmethods.normal = function(list) local s = { } local n = 0 for k, v in next, list do if not k then -- no need to add to hash elseif k == "number" or k == "features" then -- no need to add to hash (maybe we need a skip list) else n = n + 1 if type(v) == "table" then -- table.sequenced local t = { } local m = 0 for k, v in next, v do m = m + 1 t[m] = format("%q=%q",k,v) end sort(t) s[n] = format("%q={%s}",k,concat(t,",")) else s[n] = format("%q=%q",k,v) end end end if n > 0 then sort(s) return concat(s,"+") end end constructors.hashmethods.virtual = function(list) local s = { } local n = 0 for k, v in next, list do n = n + 1 s[n] = format("%q=%q",k,v) end if n > 0 then sort(s) return concat(s,"+") end end -- end of redefine -- local withcache = { } -- concat might be less efficient than nested tables -- -- local function withset(name,what) -- local zero = texgetattribute(0) -- local hash = zero .. "+" .. name .. "*" .. what -- local done = withcache[hash] -- if not done then -- done = mergecontext(zero,name,what) -- withcache[hash] = done -- end -- texsetattribute(0,done) -- end -- -- local function withfnt(name,what,font) -- local font = font or currentfont() -- local hash = font .. "*" .. name .. "*" .. what -- local done = withcache[hash] -- if not done then -- done = registercontext(font,name,what) -- withcache[hash] = done -- end -- texsetattribute(0,done) -- end function specifiers.showcontext(name) return setups[name] or setups[numbers[name]] or setups[numbers[tonumber(name)]] or { } end -- we need a copy as we will add (fontclass) goodies to the features and -- that is bad for a shared table -- local function splitcontext(features) -- presetcontext creates dummy here -- return fastcopy(setups[features] or (presetcontext(features,"","") and setups[features])) -- end -- local function splitcontext(features) -- presetcontext creates dummy here -- local sf = setups[features] -- if not sf then -- local n -- number -- if find(features,",") then -- -- let's assume a combination which is not yet defined but just specified (as in math) -- n, sf = presetcontext(features,features,"") -- else -- -- we've run into an unknown feature and or a direct spec so we create a dummy -- n, sf = presetcontext(features,"","") -- end -- end -- return fastcopy(sf) -- end local function splitcontext(features) -- presetcontext creates dummy here local n, sf -- We can have: "a=yes,b=yes" "a,b" "a" "a=yes" etc. if find(features,"[,=]") then -- -- from elsewhere (!) -- -- this will become: -- -- if find(features,"^reset," then setups[features] = nil -- end -- let's assume a combination which is not yet defined but just specified (as in math) n, sf = presetcontext(features,features,"") else sf = setups[features] if not sf then -- we've run into an unknown feature and or a direct spec so we create a dummy n, sf = presetcontext(features,"","") end end return fastcopy(sf) end -- local splitter = lpeg.splitat("=") -- -- local function splitcontext(features) -- local setup = setups[features] -- if setup then -- return setup -- elseif find(features,",",1,true) then -- -- This is not that efficient but handy anyway for quick and dirty tests -- -- beware, due to the way of caching setups you can get the wrong results -- -- when components change. A safeguard is to nil the cache. -- local merge = nil -- for feature in gmatch(features,"[^, ]+") do -- if find(feature,"=",1,true) then -- local k, v = lpegmatch(splitter,feature) -- if k and v then -- if not merge then -- merge = { k = v } -- else -- merge[k] = v -- end -- end -- else -- local s = setups[feature] -- if not s then -- -- skip -- elseif not merge then -- merge = s -- else -- for k, v in next, s do -- merge[k] = v -- end -- end -- end -- end -- setup = merge and presetcontext(features,"",merge) and setups[features] -- -- actually we have to nil setups[features] in order to permit redefinitions -- setups[features] = nil -- end -- return setup or (presetcontext(features,"","") and setups[features]) -- creates dummy -- end specifiers.splitcontext = splitcontext function specifiers.contexttostring(name,kind,separator,yes,no,strict,omit) -- not used return hash_to_string( mergedtable(handlers[kind].features.defaults or {},setups[name] or {}), separator, yes, no, strict, omit or { "number" } ) end local function starred(features) -- no longer fallbacks here local detail = features.detail if detail and detail ~= "" then features.features.normal = splitcontext(detail) else features.features.normal = { } end return features end definers.registersplit('*',starred,"featureset") -- sort of xetex mode, but without [] and / as we have file: and name: etc local space = P(" ") local spaces = space^0 local separator = S(";,") local equal = P("=") local sometext = C((1-equal-space-separator)^1) local truevalue = P("+") * spaces * sometext * Cc(true) local falsevalue = P("-") * spaces * sometext * Cc(false) local somevalue = sometext * spaces * Cc(true) local keyvalue = sometext * spaces * equal * spaces * sometext ----- pattern = Cf(Ct("") * (space + separator + Cg(falsevalue + truevalue + keyvalue + somevalue))^0, rawset) local pattern = Ct("") * (space + separator + ((falsevalue + truevalue + keyvalue + somevalue) % rawset))^0 local function colonized(specification) specification.features.normal = normalize_features(lpegmatch(pattern,specification.detail)) return specification end definers.registersplit(":",colonized,"direct") -- define (two steps) local sizepattern, splitpattern, specialscale do -- todo: use lpeg.patterns ----- space = P(" ") ----- spaces = space^0 local digit = R('09') local period = P(".") local leftparent = (P"(") local rightparent = (P")") local leftbrace = (P"{") local rightbrace = (P"}") local withinparents = leftparent * (1-rightparent)^0 * rightparent local withinbraces = leftbrace * (1-rightbrace )^0 * rightbrace local value = C((withinparents + withinbraces + (1-space))^1) local dimension = Cs((digit+period)^1 * (spaces/"") * (P(1)-digit-space)^0) -- local dimension_x = C((space/"" + P(1))^1) local dimension_x = C((1-space)^1) -- local scaler = C((digit+period)^1) local scaler = lpeg.patterns.unsigned/function(s) return round(tonumber(s)*1000) end + C(lpeg.patterns.cardinal) local rest = C(P(1)^0) local scale_none = Cc(0) local scale_at = (P("at") + P("@")) * Cc(1) * spaces * dimension -- dimension local scale_sa = P("sa") * Cc(2) * spaces * dimension_x -- number or string local scale_mo = P("mo") * Cc(3) * spaces * dimension_x -- number or string local scale_scaled = P("scaled") * Cc(4) * spaces * dimension -- number -- unrealiable and will be dropped -- local scale_scaled = P("scaled") * Cc(2) * spaces * (dimension / function(s) return s / 1000 end) -- cheat, not related to design size local scale_ht = P("ht") * Cc(5) * spaces * dimension -- dimension local scale_cp = P("cp") * Cc(6) * spaces * dimension -- dimension local scale_sx = P("sx") * spaces * scaler -- scale local scale_sy = P("sy") * spaces * scaler -- scale specialscale = { [5] = "ht", [6] = "cp" } -- sizepattern = spaces * ((scale_at + scale_sa + scale_mo + scale_ht + scale_cp + scale_scaled) * (spaces * scaler)^0 + scale_none) scaler = spaces * ( (scale_sx + Cc(0)) * spaces * (scale_sy + Cc(0)) )^-1 sizepattern = spaces * ((scale_at + scale_sa + scale_mo + scale_ht + scale_cp + scale_scaled) * scaler + scale_none) splitpattern = spaces * value * spaces * rest end function helpers.splitfontpattern(str) local name, size = lpegmatch(splitpattern,str) local kind, size = lpegmatch(sizepattern,size) -- sx, sy return name, kind, size -- sx, sy end function helpers.fontpatternhassize(str) local name, size = lpegmatch(splitpattern,str) local kind, size = lpegmatch(sizepattern,size) return size or false end local specification -- still needed as local ? local getspecification = definers.getspecification -- we can make helper macros which saves parsing (but normaly not -- that many calls, e.g. in mk a couple of 100 and in metafun 3500) local specifiers = { } do -- else too many locals local starttiming = statistics.starttiming local stoptiming = statistics.stoptiming local setmacro = tokens.setters.macro local ctxcatcodes = tex.ctxcatcodes local texconditionals = tex.conditionals local c_scaledfontmode = texiscount("scaledfontmode") local c_scaledfontsize = texiscount("scaledfontsize") local c_lastfontid = texiscount("lastfontid") local reported = setmetatableindex(function(t,k) local v = setmetatableindex(function(t,k) t[k] = true return false end) t[k] = v return v end) local obey_designsize = false experiments.register("fonts.compact.obeydesignsize",function(v) obey_designsize = v end) implement { name = "definefont_one", arguments = "string", actions = function(str) starttiming(fonts) if trace_defining then report_defining("memory usage before: %s",statistics.memused()) report_defining("start stage one: %s",str) end local fullname, size = lpegmatch(splitpattern,str) local lookup, name, sub, method, detail = getspecification(fullname) if not name then report_defining("strange definition %a",str) -- ctx_setdefaultfontname() elseif name == "unknown" then -- ctx_setdefaultfontname() else -- ctx_setsomefontname(name) setmacro("somefontname",name,"global") end -- we can also use a count for the size if size and size ~= "" then local mode, fontsize, sx, sy = lpegmatch(sizepattern,size) if mode and fontsize and fontsize ~= "" then texsetcount(c_scaledfontmode,mode) -- ctx_setsomefontsize(fontsize) -- We use a catcodetable, just in case it's 1.2\exheight (a corner case that showed -- up in the lmtx manual: marking that changed size in the mids of some verbatim). setmacro(ctxcatcodes,"somefontsize",fontsize) if sx then setmacro("somefontsizex",sx) end if sy then setmacro("somefontsizey",sy) end else texsetcount(c_scaledfontmode,0) -- ctx_setemptyfontsize() end elseif true then -- so we don't need to check in tex texsetcount(c_scaledfontmode,2) -- ctx_setemptyfontsize() else texsetcount(c_scaledfontmode,0) -- ctx_setemptyfontsize() end specification = definers.makespecification(str,lookup,name,sub,method,detail,size) -- specification.original = str if trace_defining then report_defining("stop stage one") end end } local function nice_cs(cs) return (gsub(cs,".->", "")) end local n = 0 local busy = false local combinefeatures = false directives.register("fonts.features.combine",function(v) combinefeatures = v end) function fonts.mode() return texconditionals["c_font_compact"] and "compact" or "normal" end implement { name = "definefont_two", arguments = { -- "boolean", "string", "string", "dimension", "integer", "string", -- "string", "string", "string", "integer", "dimension", "string", -- "string", "string", "string", "integer", "boolean", "argument", "argument", "dimension", "integer", "argument", "argument", "argument", "argument", "integer", "dimension", "argument", "argument", "argument", "argument", "integer", }, actions = function ( global, -- \ifx\fontclass\empty\s!false\else\s!true\fi cs, -- {#csname}% str, -- \somefontfile size, -- \d_font_scaled_font_size inheritancemode, -- \c_font_feature_inheritance_mode classfeatures, -- \m_font_class_features fontfeatures, -- \m_font_features classfallbacks, -- \m_font_class_fallbacks fontfallbacks, -- \m_font_fallbacks mathsize, -- \fontface textsize, -- \d_font_scaled_text_face classgoodies, -- \m_font_class_goodies goodies, -- \m_font_goodies classdesignsize, -- \m_font_class_designsize fontdesignsize, -- \m_font_designsize scaledfontmode -- \scaledfontmode ) if trace_defining then report_defining("start stage two: %s, size %s, features %a & %a, mode %a",str,size,classfeatures,fontfeatures,inheritancemode) end local compact = texconditionals["c_font_compact"] -- name is now resolved and size is scaled cf sa/mo local lookup, name, sub, method, detail = getspecification(str or "") -- new (todo: inheritancemode) local designsize = fontdesignsize ~= "" and fontdesignsize or classdesignsize or "" local designname = designsizefilename(name,designsize,size,obey_designsize) if designname and designname ~= "" then if trace_defining or trace_designsize then report_defining("remapping name %a, specification %a, size %a, designsize %a",name,designsize,size,designname) end -- we don't catch detail here local o_lookup, o_name, o_sub, o_method, o_detail = getspecification(designname) if o_lookup and o_lookup ~= "" then lookup = o_lookup end if o_method and o_method ~= "" then method = o_method end if o_detail and o_detail ~= "" then detail = o_detail end name = o_name sub = o_sub end if compact then size = 655360 end -- so far -- some settings can have been overloaded if lookup and lookup ~= "" then specification.lookup = lookup end if relativeid and relativeid ~= "" then -- experimental hook local id = tonumber(relativeid) or 0 specification.relativeid = id > 0 and id end -- specification.name = name specification.size = size specification.sub = (sub and sub ~= "" and sub) or specification.sub specification.mathsize = mathsize specification.textsize = textsize specification.goodies = goodies specification.cs = cs specification.global = global specification.scalemode = scaledfontmode -- context specific if detail and detail ~= "" then specification.method = method or "*" specification.detail = detail elseif specification.detail and specification.detail ~= "" then -- already set elseif inheritancemode == 0 then -- nothing elseif inheritancemode == 1 then -- fontonly if fontfeatures and fontfeatures ~= "" then specification.method = "*" specification.detail = fontfeatures end if fontfallbacks and fontfallbacks ~= "" then specification.fallbacks = fontfallbacks end elseif inheritancemode == 2 then -- classonly if classfeatures and classfeatures ~= "" then specification.method = "*" specification.detail = classfeatures end if classfallbacks and classfallbacks ~= "" then specification.fallbacks = classfallbacks end elseif inheritancemode == 3 then -- fontfirst if combinefeatures then if classfeatures and classfeatures ~= "" then specification.method = "*" if fontfeatures and fontfeatures ~= "" and fontfeatures ~= classfeatures then specification.detail = classfeatures .. "," .. fontfeatures else specification.detail = classfeatures end elseif fontfeatures and fontfeatures ~= "" then specification.method = "*" specification.detail = fontfeatures end else if fontfeatures and fontfeatures ~= "" then specification.method = "*" specification.detail = fontfeatures elseif classfeatures and classfeatures ~= "" then specification.method = "*" specification.detail = classfeatures end end if fontfallbacks and fontfallbacks ~= "" then specification.fallbacks = fontfallbacks elseif classfallbacks and classfallbacks ~= "" then specification.fallbacks = classfallbacks end elseif inheritancemode == 4 then -- classfirst if combinefeatures then if fontfeatures and fontfeatures ~= "" then specification.method = "*" if classfeatures and classfeatures ~= "" and classfeatures ~= fontfeatures then specification.detail = fontfeatures .. "," .. classfeatures else specification.detail = fontfeatures end elseif classfeatures and classfeatures ~= "" then specification.method = "*" specification.detail = classfeatures end else if classfeatures and classfeatures ~= "" then specification.method = "*" specification.detail = classfeatures elseif fontfeatures and fontfeatures ~= "" then specification.method = "*" specification.detail = fontfeatures end end if classfallbacks and classfallbacks ~= "" then specification.fallbacks = classfallbacks elseif fontfallbacks and fontfallbacks ~= "" then specification.fallbacks = fontfallbacks end end -- -- local tfmdata = definers.read(specification,size) -- id not yet known (size in spec?) local tfmdata, extend, squeeze, slant, weight = definers.read(specification,size,nil,compact) -- id not yet known (size in spec?) local lastfontid = 0 local tfmtype = type(tfmdata) if compact then setmacro("somefontextend", extend ~= 1 and round(extend *1000) or "") setmacro("somefontsqueeze",squeeze ~= 1 and round(squeeze*1000) or "") setmacro("somefontslant", slant ~= 0 and round(slant *1000) or "") setmacro("somefontweight", weight ~= 0 and round(weight *1000) or "") else -- actually we can omit this as it's not used there setmacro("somefontextend") setmacro("somefontsqueeze") setmacro("somefontslant") setmacro("somefontweight") end if tfmtype == "table" then -- setting the extra characters will move elsewhere local characters = tfmdata.characters local parameters = tfmdata.parameters local properties = tfmdata.properties if characters then -- we use char0 as signal; cf the spec pdf can handle this (no char in slot) characters[0] = nil tfmdata.original = specification.specification local id = definefont(tfmdata,properties.id) csnames[id] = specification.cs properties.id = id -- already set definers.register(tfmdata,id) -- to be sure, normally already done texdefinefont(global,cs,id) -- texdefinefont(cs,id,global and "global") constructors.finalize(tfmdata) if trace_defining then report_defining("defining %a, id %a, target %a, features %a / %a, fallbacks %a / %a, step %a", name,id,nice_cs(cs),classfeatures,fontfeatures,classfallbacks,fontfallbacks,"-") end -- resolved (when designsize is used): local size = round(tfmdata.parameters.size or 655360) if scaledfontmode ~= 4 then setmacro("somefontsize",size.."sp") end texsetcount(c_scaledfontsize,size) lastfontid = id else -- case 1: same as ** local nice = nice_cs(cs) if not reported[name][nice] then report_defining("unable to define %a as %a",name,nice) end lastfontid = -1 texsetcount(c_scaledfontsize,0) end elseif tfmtype == "number" then if trace_defining then report_defining("reusing %s, id %a, target %a, features %a / %a, fallbacks %a / %a, goodies %a / %a, designsize %a / %a", name,tfmdata,nice_cs(cs),classfeatures,fontfeatures,classfallbacks,fontfallbacks,classgoodies,goodies,classdesignsize,fontdesignsize) end csnames[tfmdata] = specification.cs -- \definedfont ends up here texdefinefont(global,cs,tfmdata) -- texdefinefont(cs,tfmdata,global and "global") -- resolved (when designsize is used): local size = round(fontdata[tfmdata].parameters.size or 0) if scaledfontmode ~= 4 then setmacro("somefontsize",size.."sp") end texsetcount(c_scaledfontsize,size) lastfontid = tfmdata else -- case 2: same as ** local nice = nice_cs(cs) if not reported[name][nice] then report_defining("unable to define %a as %a",name,nice) end lastfontid = -1 texsetcount(c_scaledfontsize,0) -- ctx_letvaluerelax(cs) -- otherwise the current definition takes the previous one end if trace_defining then report_defining("memory usage after: %s",statistics.memused()) report_defining("stop stage two") end -- texsetcount("global",c_lastfontid,lastfontid) specifiers[lastfontid] = { str, size } if not mathsize then -- forget about it elseif mathsize == 0 then -- can't happen (here) else -- maybe only 1 2 3 lastmathids[mathsize] = lastfontid end -- stoptiming(fonts) end } implement { name = "specifiedfontspec", arguments = "integer", actions = function(id) local f = specifiers[id] if f then context(f[1]) end end } implement { name = "specifiedfontsize", arguments = "integer", actions = function(id) local f = specifiers[id] if f then context(f[2]) end end } implement { name = "specifiedfont", arguments = { "integer", "number" }, actions = function(id,size) local f = specifiers[id] if f and size then context("%s at %0.2p",f[1],size * f[2]) -- we round to 2 decimals (as at the tex end) end end } -- local function define(specification) -- local name = specification.name if not name or name == "" then return -1 else starttiming(fonts) -- -- following calls expect a few properties to be set: -- local lookup, name, sub, method, detail = getspecification(name or "") -- specification.name = (name ~= "" and name) or specification.name -- specification.lookup = specification.lookup or (lookup ~= "" and lookup) or "file" specification.size = specification.size or 655260 specification.sub = specification.sub or (sub ~= "" and sub) or "" specification.method = specification.method or (method ~= "" and method) or "*" specification.detail = specification.detail or (detail ~= "" and detail) or "" -- if type(specification.size) == "string" then specification.size = texsp(specification.size) or 655260 end -- specification.specification = "" -- not used specification.resolved = "" specification.forced = "" specification.features = { } -- via detail, maybe some day -- -- we don't care about mathsize textsize goodies fallbacks -- local cs = specification.cs if cs == "" then cs = nil specification.cs = nil specification.global = false elseif specification.global == nil then specification.global = false end -- local tfmdata = definers.read(specification,specification.size) if not tfmdata then return -1, nil elseif type(tfmdata) == "number" then if cs then texdefinefont(specification.global,cs,tfmdata) -- texdefinefont(cs,tfmdata,specification.global and "global") csnames[tfmdata] = cs end stoptiming(fonts) return tfmdata, fontdata[tfmdata] else local id = definefont(tfmdata) tfmdata.properties.id = id definers.register(tfmdata,id) if cs then texdefinefont(specification.global,cs,id) -- texdefinefont(cs,id,specification.global and "global") csnames[id] = cs end constructors.finalize(tfmdata) stoptiming(fonts) return id, tfmdata end end end definers.define = define -- local id, cs = fonts.definers.internal { } -- local id, cs = fonts.definers.internal { number = 2 } -- local id, cs = fonts.definers.internal { name = "dejavusans" } local n = 0 local d_bodyfontsize = tex.isdimen("bodyfontsize") function definers.internal(specification,cs) specification = specification or { } local name = specification.name local size = tonumber(specification.size) local number = tonumber(specification.number) local id = nil if not size then size = texgetdimen(d_bodyfontsize) end if number then id = number elseif name and name ~= "" then local cs = cs or specification.cs if not cs then n = n + 1 -- beware ... there can be many and they are often used once -- cs = formatters["internal font %s"](n) cs = "internal font " .. n else specification.cs = cs end id = define { name = name, size = size, cs = cs, } end if not id then id = currentfont() end return id, csnames[id] end local function read(name,size) return (define { name = name, size = size } or 0) end callbacks.register("define_font",read,"define and/or load a font") -- here local infofont = table.setmetatableindex(function(t,k) local v = define { name = "dejavusansmono", size = texsp("6pt") } t[k] = v return v end) function fonts.infofont(small) return infofont[small == true and "3pt" or "6pt"] end -- abstract interfacing : we could actually do a runmacro or so implement { name = "tf", actions = function() setmacro("fontalternative","tf") end } implement { name = "bf", actions = function() setmacro("fontalternative","bf") end } implement { name = "it", actions = function() setmacro("fontalternative","it") end } implement { name = "sl", actions = function() setmacro("fontalternative","sl") end } implement { name = "bi", actions = function() setmacro("fontalternative","bi") end } implement { name = "bs", actions = function() setmacro("fontalternative","bs") end } end -- Not ok, we can best use a database for this. The problem is that we -- have delayed definitions and so we never know what style is taken -- as start. local c_font_scaled_points = texiscount("c_font_scaled_points") function constructors.calculatescale(tfmdata,scaledpoints,relativeid,specification) local parameters = tfmdata.parameters local units = parameters.units or 1000 if specification then local scalemode = specification.scalemode local special = scalemode and specialscale[scalemode] if special == "ht" then local height = parameters.ascender / units scaledpoints = scaledpoints / height elseif special == "cp" then local glyph = tfmdata.descriptions[utfbyte("X")] local height = (glyph and glyph.height or parameters.ascender) / units scaledpoints = scaledpoints / height end end local negative = scaledpoints < 0 if negative then scaledpoints = (- scaledpoints/1000) * (tfmdata.designsize or parameters.designsize) -- already in sp end -- a temp hack till we have upgraded all mechanisms local delta = scaledpoints/units local size = round(scaledpoints) if not accuratefactors then delta = round(delta) end texsetcount(c_font_scaled_points,size) if accuratescale and not negative then size = round(delta * 1000) end return size, delta end local designsizes = constructors.designsizes -- called quite often when in mp labels -- otf.normalizedaxis -- function constructors.hashinstance(specification,force) -- local hash = specification.hash -- local size = specification.size -- local fallbacks = specification.fallbacks -- if force or not hash then -- hash = constructors.hashfeatures(specification) -- specification.hash = hash -- end -- if size < 1000 and designsizes[hash] then -- size = round(constructors.scaled(size,designsizes[hash])) -- else -- size = round(size) -- end -- specification.size = size -- if fallbacks then -- hash = hash .. ' @ ' .. size .. ' @ ' .. fallbacks -- else -- local scalemode = specification.scalemode -- local special = scalemode and specialscale[scalemode] -- if special then -- hash = hash .. ' @ ' .. size .. ' @ ' .. special -- else -- hash = hash .. ' @ ' .. size -- end -- end -- return hash -- end function constructors.hashinstance(specification,force,compact) -- local normal = compact and specification.features.normal local extend, squeeze, slant, weight, auto if normal then local effect = normal.effect if effect then extend = effect.extend or 1 squeeze = effect.squeeze or 1 slant = effect.slant or 0 weight = effect.weight or 0 auto = effect.auto or false else extend = normal.extend or 1 squeeze = normal.squeeze or 1 slant = normal.slant or 0 weight = normal.weight or 0 auto = false end normal.extend = nil normal.squeeze = nil normal.slant = nil normal.weight = nil weight = weight / 2 else extend = 1 squeeze = 1 slant = 0 weight = 0 auto = false end -- local hash = specification.hash local size = specification.size local fallbacks = specification.fallbacks if force or not hash then hash = constructors.hashfeatures(specification) specification.hash = hash end if size < 1000 and designsizes[hash] then size = round(constructors.scaled(size,designsizes[hash])) else size = round(size) end specification.size = size if fallbacks then hash = hash .. ' @ ' .. size .. ' @ ' .. fallbacks else local scalemode = specification.scalemode local special = scalemode and specialscale[scalemode] if special then hash = hash .. ' @ ' .. size .. ' @ ' .. special else hash = hash .. ' @ ' .. size end end return hash, extend, squeeze, slant, weight, auto end -- We overload the (generic) resolver: local resolvers = definers.resolvers local hashfeatures = constructors.hashfeatures function definers.resolve(specification) -- overload function in font-con.lua 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 else specification.forced = specification.forced end -- goodies are a context specific thing and are not always defined -- as feature, so we need to make sure we add them here before -- hashing because otherwise we get funny goodies applied local goodies = specification.goodies if goodies and goodies ~= "" then -- this adapts the features table so it has best be a copy local normal = specification.features.normal if not normal then specification.features.normal = { goodies = goodies } elseif not normal.goodies then local g = normal.goodies if g and g ~= "" then normal.goodies = formatters["%s,%s"](g,goodies) else normal.goodies = goodies end end end -- so far for goodie hacks local hash = hashfeatures(specification) local name = specification.name or "badfont" local sub = specification.sub if sub and sub ~= "" then specification.hash = lower(name .. " @ " .. sub .. ' @ ' .. hash) else specification.hash = lower(name .. " @ " .. ' @ ' .. hash) end -- return specification end -- we need an 'do after the banner hook' -- => commands local pattern = P("P") * (lpeg.patterns.hexdigit^4 / function(s) return tonumber(s,16) end) * P(-1) local function nametoslot(name) -- also supports PXXXXX (4+ positions) local t = type(name) if t == "string" then local unic = unicodes[true] local slot = unic[name] if slot then return slot end -- local slot = unic[gsub(name,"_"," ")] or unic[gsub(name,"_","-")] or unic[gsub(name,"-"," ")] or unic[gsub(name,"-","_")] or unic[gsub(name," ","_")] or unic[gsub(name," ","-")] if slot then return slot end -- if not aglunicodes then aglunicodes = encodings.agl.unicodes end local char = characters[true] local slot = aglunicodes[name] if slot and char[slot] then return slot end local slot = lpegmatch(pattern,name) if slot and char[slot] then return slot end -- not in font elseif t == "number" then if characters[true][name] then return slot else -- not in font end end end local found = { } local function descriptiontoslot(name) local t = type(name) if t == "string" then -- slow local list = sortedkeys(chardata) -- can be a cache with weak tables local slot = found[name] local char = characters[true] if slot then return char[slot] and slot or nil end local NAME = upper(name) for i=1,#list do slot = list[i] local c = chardata[slot] local d = c.description if d == NAME then found[name] = slot return char[slot] and slot or nil end end for i=1,#list do slot = list[i] local c = chardata[slot] local s = c.synonyms if s then for i=1,#s do local si = s[i] if si == name then found[name] = si return char[slot] and slot or nil end end end end for i=1,#list do slot = list[i] local c = chardata[slot] local d = c.description if d and find(d,NAME) then found[name] = slot return char[slot] and slot or nil end end for i=1,#list do slot = list[i] local c = chardata[slot] local s = c.synonyms if s then for i=1,#s do local si = s[i] if find(s[i],name) then found[name] = si return char[slot] and slot or nil end end end end -- not in font elseif t == "number" then if characters[true][name] then return slot else -- not in font end end end local function indextoslot(font,index) if not index then index = font font = true end local r = resources[font] if r then local indices = r.indices if not indices then indices = { } local c = characters[font] for unicode, data in next, c do local di = data.index if di then indices[di] = unicode end end r.indices = indices end return indices[tonumber(index)] end end do -- else too many locals local entities = characters.entities local lowered = { } -- delayed initialization setmetatableindex(lowered,function(t,k) for k, v in next, entities do local l = lower(k) if not entities[l] then lowered[l] = v end end setmetatableindex(lowered,nil) return lowered[k] end) local methods = { -- entity e = function(name) return entities[name] or lowered[name] or name end, -- hexadecimal unicode x = function(name) local n = tonumber(name,16) return n and utfchar(n) or name end, -- decimal unicode d = function(name) local n = tonumber(name) return n and utfchar(n) or name end, -- hexadecimal index (slot) s = function(name) local n = tonumber(name,16) local n = n and indextoslot(n) return n and utfchar(n) or name end, -- decimal index i = function(name) local n = tonumber(name) local n = n and indextoslot(n) return n and utfchar(n) or name end, -- name n = function(name) local n = nametoslot(name) return n and utfchar(n) or name end, -- unicode description (synonym) u = function(name) local n = descriptiontoslot(name,false) return n and utfchar(n) or name end, -- all a = function(name) local n = nametoslot(name) or descriptiontoslot(name) return n and utfchar(n) or name end, -- char c = function(name) return name end, } -- -- nicer: -- -- setmetatableindex(methods,function(t,k) return methods.c end) -- -- local splitter = (C(1) * P(":") + Cc("c")) * C(P(1)^1) / function(method,name) -- return methods[method](name) -- end -- -- -- more efficient: local splitter = C(1) * P(":") * C(P(1)^1) / function(method,name) local action = methods[method] return action and action(name) or name end local function tochar(str) local t = type(str) if t == "number" then return utfchar(str) elseif t == "string" then return lpegmatch(splitter,str) or str else return str end end helpers.nametoslot = nametoslot helpers.descriptiontoslot = descriptiontoslot helpers.indextoslot = indextoslot helpers.tochar = tochar -- interfaces: implement { name = "fontchar", actions = { nametoslot, ctx_char }, arguments = "string", } implement { name = "fontcharbyindex", actions = { indextoslot, ctx_char }, arguments = "integer", } implement { name = "tochar", actions = { tochar, ctx_safechar }, arguments = "string", } end -- this will change ... function loggers.reportdefinedfonts() if trace_usage then local t, tn = { }, 0 for id, data in sortedhash(fontdata) do local properties = data.properties or { } local parameters = data.parameters or { } tn = tn + 1 t[tn] = { formatters["%03i"](id or 0), formatters["%p" ](parameters.size or 0), properties.format or "unknown", properties.name or "", properties.psname or "", properties.fullname or "", properties.sharedwith or "", } end formatcolumns(t," ") -- logs.startfilelogging(report,"defined fonts") for k=1,tn do report(t[k]) end logs.stopfilelogging() end end logs.registerfinalactions(loggers.reportdefinedfonts) function loggers.reportusedfeatures() -- numbers, setups, merged if trace_usage then local t, n = { }, #numbers for i=1,n do local name = numbers[i] local setup = setups[name] local n = setup.number setup.number = nil -- we have no reason to show this t[i] = { i, name, sequenced(setup,false,true) } -- simple mode setup.number = n -- restore it (normally not needed as we're done anyway) end formatcolumns(t," ") logs.startfilelogging(report,"defined featuresets") for k=1,n do report(t[k]) end logs.stopfilelogging() end end logs.registerfinalactions(loggers.reportusedfeatures) -- maybe move this to font-log.lua: statistics.register("font engine", function() local elapsed = statistics.elapsedseconds(fonts) local nofshared = constructors.nofsharedfonts or 0 local nofloaded = constructors.noffontsloaded or 0 if nofshared > 0 then return format("otf %0.3f, afm %0.3f, tfm %0.3f, %s instances, %s shared in backend, %s common vectors, %s common hashes, load time %s", otf.version,afm.version,tfm.version,nofloaded, nofshared,constructors.nofsharedvectors,constructors.nofsharedhashes, elapsed) elseif nofloaded > 0 and elapsed then return format("otf %0.3f, afm %0.3f, tfm %0.3f, %s instances, load time %s", otf.version,afm.version,tfm.version,nofloaded, elapsed) else return format("otf %0.3f, afm %0.3f, tfm %0.3f", otf.version,afm.version,tfm.version) end end) -- experimental mechanism for Mojca: -- -- fonts.definetypeface { -- name = "mainbodyfont-light", -- preset = "antykwapoltawskiego-light", -- } -- -- fonts.definetypeface { -- name = "mojcasfavourite", -- preset = "antykwapoltawskiego", -- normalweight = "light", -- boldweight = "bold", -- width = "condensed", -- } local Shapes = { serif = "Serif", sans = "Sans", mono = "Mono", } local ctx_startfontclass = context.startfontclass local ctx_stopfontclass = context.stopfontclass local ctx_definefontsynonym = context.definefontsynonym local ctx_dofastdefinetypeface = context.dofastdefinetypeface function fonts.definetypeface(name,t) if type(name) == "table" then -- {name=abc,k=v,...} t = name elseif t then if type(t) == "string" then -- "abc", "k=v,..." t = settings_to_hash(name) else -- "abc", {k=v,...} end t.name = t.name or name else -- "name=abc,k=v,..." t = settings_to_hash(name) end local p = t.preset and fonts.typefaces[t.preset] or { } local name = t.name or "unknowntypeface" local shortcut = t.shortcut or p.shortcut or "rm" local size = t.size or p.size or "default" local shape = t.shape or p.shape or "serif" local fontname = t.fontname or p.fontname or "unknown" local normalweight = t.normalweight or t.weight or p.normalweight or p.weight or "normal" local boldweight = t.boldweight or t.weight or p.boldweight or p.weight or "normal" local normalwidth = t.normalwidth or t.width or p.normalwidth or p.width or "normal" local boldwidth = t.boldwidth or t.width or p.boldwidth or p.width or "normal" Shape = Shapes[shape] or "Serif" ctx_startfontclass { name } ctx_definefontsynonym( { formatters["%s"] (Shape) }, { formatters["spec:%s-%s-regular-%s"] (fontname, normalweight, normalwidth) } ) ctx_definefontsynonym( { formatters["%sBold"] (Shape) }, { formatters["spec:%s-%s-regular-%s"] (fontname, boldweight, boldwidth ) } ) ctx_definefontsynonym( { formatters["%sBoldItalic"](Shape) }, { formatters["spec:%s-%s-italic-%s"] (fontname, boldweight, boldwidth ) } ) ctx_definefontsynonym( { formatters["%sItalic"] (Shape) }, { formatters["spec:%s-%s-italic-%s"] (fontname, normalweight, normalwidth) } ) ctx_stopfontclass() local settings = sequenced({ features = t.features },",") ctx_dofastdefinetypeface(name, shortcut, shape, size, settings) end implement { name = "definetypeface", actions = fonts.definetypeface, arguments = "2 strings" } function fonts.current() -- todo: also handle name return fontdata[currentfont()] or fontdata[0] end function fonts.currentid() return currentfont() or 0 end -- for the moment here, this will become a chain of extras that is -- hooked into the ctx registration (or scaler or ...) local dimenfactors = number.dimenfactors function helpers.dimenfactor(unit,id) if unit == "ex" then return id and exheights[id] or 282460 -- lm 10pt elseif unit == "em" then return id and emwidths [id] or 655360 -- lm 10pt else local du = dimenfactors[unit] return du and 1/du or tonumber(unit) or 1 end end local function digitwidth(font) -- max(quad/2,wd(0..9)) local tfmdata = fontdata[font] local parameters = tfmdata.parameters local width = parameters.digitwidth if not width then width = round(parameters.quad/2) -- maybe tex.scale local characters = tfmdata.characters for i=48,57 do local wd = round(characters[i].width) if wd > width then width = wd end end parameters.digitwidth = width end return width end helpers.getdigitwidth = digitwidth helpers.setdigitwidth = digitwidth -- function helpers.getparameters(tfmdata) local p = { } local m = p local parameters = tfmdata.parameters while true do for k, v in next, parameters do m[k] = v end parameters = getmetatable(parameters) parameters = parameters and parameters.__index if type(parameters) == "table" then m = { } p.metatable = m else break end end return p end if environment.initex then local function names(t) local nt = #t if nt > 0 then local n = { } for i=1,nt do n[i] = t[i].name end return concat(n," ") else return "-" end end statistics.register("font processing", function() local l = { } for what, handler in table.sortedpairs(handlers) do local features = handler and handler.features if features then l[#l+1] = format("[%s (base initializers: %s) (base processors: %s) (base manipulators: %s) (node initializers: %s) (node processors: %s) (node manipulators: %s)]", what, names(features.initializers.base), names(features.processors .base), names(features.manipulators.base), names(features.initializers.node), names(features.processors .node), names(features.manipulators.node) ) end end return concat(l, " | ") end) end -- redefinition -- local hashes = fonts.hashes -- local emwidths = hashes.emwidths -- local exheights = hashes.exheights setmetatableindex(dimenfactors, function(t,k) if k == "ex" then return 1/exheights[currentfont()] elseif k == "em" then return 1/emwidths[currentfont()] elseif k == "pct" or k == "%" then return 1/(texget("hsize")/100) else -- error("wrong dimension: " .. (s or "?")) -- better a message return false end end) dimenfactors.ex = nil dimenfactors.em = nil dimenfactors["%"] = nil dimenfactors.pct = nil -- Before a font is passed to TeX we scale it. Here we also need to scale virtual -- characters. do -- can become luat-tex.lua local texsetglyphdata = tex.setglyphdata local texgetglyphdata = tex.getglyphdata if not texsetglyphdata then local texsetattribute = tex.setattribute local texgetattribute = tex.getattribute texsetglyphdata = function(n) return texsetattribute(0,n) end texgetglyphdata = function() return texgetattribute(0) end tex.setglyphdata = texsetglyphdata tex.getglyphdata = texgetglyphdata end -- till here local setmacro = tokens.setters.macro function constructors.currentfonthasfeature(n) local f = fontdata[currentfont()] if not f then return end f = f.shared if not f then return end f = f.rawdata if not f then return end f = f.resources if not f then return end f = f.features return f and (f.gpos[n] or f.gsub[n]) end local ctx_doifelse = commands.doifelse local ctx_doif = commands.doif implement { name = "doifelsecurrentfonthasfeature", actions = { constructors.currentfonthasfeature, ctx_doifelse }, arguments = "string" } local f_strip = formatters["%0.2fpt"] -- normally this value is changed only once local stripper = lpeg.patterns.stripzeros local cache = { } local hows = { ["+"] = "add", ["-"] = "subtract", ["="] = "replace", } local function setfeature(how,parent,name,font) -- 0/1 test temporary for testing if not how or how == 0 then if trace_features and texgetglyphdata() ~= 0 then report_cummulative("font %!font:name!, reset",fontdata[font or true]) end texsetglyphdata(0) elseif how == true or how == 1 then local hash = "feature > " .. parent local done = cache[hash] if trace_features and done then report_cummulative("font %!font:name!, revive %a : %!font:features!",fontdata[font or true],parent,setups[numbers[done]]) end texsetglyphdata(done or 0) else local full = parent .. how .. name local hash = "feature > " .. full local done = cache[hash] if not done then local n = setups[full] if n then -- already defined else n = mergecontextfeatures(parent,name,how,full) end done = registercontextfeature(hash,full,how) cache[hash] = done if trace_features then report_cummulative("font %!font:name!, %s %a : %!font:features!",fontdata[font or true],hows[how],full,setups[numbers[done]]) end end texsetglyphdata(done) end end local function resetfeature() if trace_features and texgetglyphdata() ~= 0 then report_cummulative("font %!font:name!, reset",fontdata[true]) end texsetglyphdata(0) end local function setfontfeature(tag) texsetglyphdata(contextnumber(tag)) end local function resetfontfeature() texsetglyphdata(0) end implement { name = "normalizedbodyfontsize", arguments = "dimen", actions = function(d) context(lpegmatch(stripper,f_strip(d/65536))) end } implement { name = "featureattribute", arguments = "string", actions = { contextnumber, context } } implement { name = "setfontfeature", arguments = "string", actions = setfontfeature, } implement { name = "resetfontfeature", -- arguments = { 0, 0 }, actions = resetfontfeature, } implement { name = "setfontofid", arguments = "integer", actions = function(id) ctx_getvalue(csnames[id]) end } implement { name = "definefontfeature", arguments = "3 strings", actions = presetcontext, } implement { name = "doifelsefontfeature", arguments = "string", actions = function(name) ctx_doifelse(contextnumber(name) > 1) end, } implement { name = "doifunknownfontfeature", arguments = "string", actions = function(name) ctx_doif(contextnumber(name) == 0) end, } implement { name = "adaptfontfeature", arguments = "2 strings", actions = adaptcontext } local function registerlanguagefeatures() local specifications = languages.data.specifications for i=1,#specifications do local specification = specifications[i] local language = specification.opentype if language then local script = specification.opentypescript or specification.script if script then local context = specification.context if type(context) == "table" then for i=1,#context do definecontext(context[i], { language = language, script = script}) end elseif type(context) == "string" then definecontext(context, { language = language, script = script}) end end end end end constructors.setfeature = setfeature constructors.resetfeature = resetfeature implement { name = "resetfeature", actions = resetfeature } implement { name = "addfeature", actions = setfeature, arguments = { "'+'", "string", "string" } } implement { name = "subtractfeature", actions = setfeature, arguments = { "'-'", "string", "string" } } implement { name = "replacefeature", actions = setfeature, arguments = { "'='", "string", "string" } } implement { name = "revivefeature", actions = setfeature, arguments = { true, "string" } } implement { name = "featurelist", actions = { fonts.specifiers.contexttostring, context }, arguments = { "string", "'otf'", "string", "'yes'", "'no'", true } } implement { name = "registerlanguagefeatures", actions = registerlanguagefeatures, } end -- a fontkern plug: -- nodes.injections.installnewkern(nuts.pool.fontkern) do local report = logs.reporter("otf","variants") local function replace(tfmdata,feature,value) local characters = tfmdata.characters local variants = tfmdata.resources.variants if variants then local t = { } for k, v in sortedhash(variants) do t[#t+1] = formatters["0x%X (%i)"](k,k) end value = tonumber(value) or 0xFE00 -- 917762 report("fontname : %s",tfmdata.properties.fontname) report("available: % t",t) local v = variants[value] if v then report("using : %X (%i)",value,value) for k, v in next, v do local c = characters[v] if c then characters[k] = c end end else report("unknown : %X (%i)",value,value) end end end registerotffeature { name = 'variant', description = 'unicode variant', manipulators = { base = replace, node = replace, } } end -- here (todo: closure) -- make a closure (200 limit): do local trace_analyzing = false trackers.register("otf.analyzing", function(v) trace_analyzing = v end) local analyzers = fonts.analyzers local methods = analyzers.methods local unsetvalue = attributes.unsetvalue local a_color = attributes.private('color') local a_colormodel = attributes.private('colormodel') local m_color = attributes.list[a_color] or { } local glyph_code = nodes.nodecodes.glyph local states = analyzers.states local colornames = { [states.init] = "font:1", [states.medi] = "font:2", [states.fina] = "font:3", [states.isol] = "font:4", [states.mark] = "font:5", [states.rest] = "font:6", [states.rphf] = "font:1", [states.half] = "font:2", [states.pref] = "font:3", [states.blwf] = "font:4", [states.pstf] = "font:5", } -- todo: traversers -- todo: check attr_list so that we can use the same .. helper: setcolorattr local function markstates(head) if head then head = tonut(head) local model = getattr(head,a_colormodel) or 1 for glyph in nextchar, head do local a = getstate(glyph) if a then local name = colornames[a] if name then local color = m_color[name] if color then setattr(glyph,a_colormodel,model) setattr(glyph,a_color,color) end end end end end end local function analyzeprocessor(head,font,attr) local tfmdata = fontdata[font] local script, language = otf.scriptandlanguage(tfmdata,attr) local action = methods[script] if not action then return head, false end if type(action) == "function" then local head, done = action(head,font,attr) if done and trace_analyzing then markstates(head) end return head, done end action = action[language] if action then local head, done = action(head,font,attr) if done and trace_analyzing then markstates(head) end return head, done else return head, false end end registerotffeature { -- adapts name = "analyze", processors = { node = analyzeprocessor, } } function methods.nocolor(head,font,attr) for n, c, f in nextchar, head do if not font or f == font then setattr(n,a_color,unsetvalue) end end return head, true end end local function purefontname(name) if type(name) == "number" then name = getfontname(name) end if type(name) == "string" then return basename(name) end end implement { name = "purefontname", actions = { purefontname, context }, arguments = "string", } local sharedstorage = storage.shared local list = sharedstorage.bodyfontsizes or { } local unknown = sharedstorage.unknownbodyfontsizes or { } sharedstorage.bodyfontsizes = list sharedstorage.unknownbodyfontsizes = unknown implement { name = "registerbodyfontsize", arguments = "string", actions = function(size) list[size] = true end } interfaces.implement { name = "registerunknownbodysize", arguments = "string", actions = function(size) if not unknown[size] then interfaces.showmessage("fonts",14,size) end unknown[size] = true end, } implement { name = "getbodyfontsizes", arguments = "string", actions = function(separator) context(concat(sortedkeys(list),separator)) end } implement { name = "processbodyfontsizes", arguments = "string", actions = function(command) local keys = sortedkeys(list) if command then local action = context[command] for i=1,#keys do action(keys[i]) end else context(concat(keys,",")) end end } implement { name = "cleanfontname", actions = { cleanname, context }, arguments = "string" } implement { name = "fontlookupinitialize", actions = names.lookup, arguments = "string", } implement { name = "fontlookupnoffound", actions = { names.noflookups, context }, } implement { name = "fontlookupgetkeyofindex", actions = { names.getlookupkey, context }, arguments = { "string", "integer"} } implement { name = "fontlookupgetkey", actions = { names.getlookupkey, context }, arguments = "string" } -- this might move to a runtime module: function commands.showchardata(n) local tfmdata = fontdata[currentfont()] if tfmdata then if type(n) == "string" then n = utfbyte(n) end local chr = tfmdata.characters[n] if chr then report_status("%s @ %s => %U => %c => %s",tfmdata.properties.fullname,tfmdata.parameters.size,n,n,serialize(chr,false)) end end end function commands.showfontparameters(tfmdata) -- this will become more clever local tfmdata = tfmdata or fontdata[currentfont()] if tfmdata then local parameters = tfmdata.parameters local mathparameters = tfmdata.mathparameters local properties = tfmdata.properties local hasparameters = parameters and next(parameters) local hasmathparameters = mathparameters and next(mathparameters) if hasparameters then report_status("%s @ %s => text parameters => %s",properties.fullname,parameters.size,serialize(parameters,false)) end if hasmathparameters then report_status("%s @ %s => math parameters => %s",properties.fullname,parameters.size,serialize(mathparameters,false)) end if not hasparameters and not hasmathparameters then report_status("%s @ %s => no text parameters and/or math parameters",properties.fullname,parameters.size) end end end implement { name = "currentdesignsize", actions = function() context(parameters[currentfont()].designsize) end } implement { name = "doifelsefontpresent", actions = { names.exists, commands.doifelse }, arguments = "string" } -- we use 0xFE000+ and 0xFF000+ in math and for runtime (text) extensions we -- use 0xFD000+ constructors.privateslots = constructors.privateslots or { } storage.register("fonts/constructors/privateslots", constructors.privateslots, "fonts.constructors.privateslots") do local privateslots = constructors.privateslots local lastprivateslot = 0xFD000 constructors.privateslots = setmetatableindex(privateslots,function(t,k) local v = lastprivateslot lastprivateslot = lastprivateslot + 1 t[k] = v return v end) implement { name = "getprivateglyphslot", actions = function(name) context(privateslots[name]) end, arguments = "string", } end -- an extra helper function helpers.getcoloredglyphs(tfmdata) if type(tfmdata) == "number" then tfmdata = fontdata[tfmdata] end if not tfmdata then tfmdata = fontdata[true] end local characters = tfmdata.characters local descriptions = tfmdata.descriptions local collected = { } for unicode, character in next, characters do local description = descriptions[unicode] if description and (description.colors or character.svg) then collected[#collected+1] = unicode end end table.sort(collected) return collected end -- for the font manual statistics.register("body font sizes", function() if next(unknown) then return formatters["defined: % t, undefined: % t"](sortedkeys(list),sortedkeys(unknown)) end end) statistics.register("used fonts",function() if trace_usage then local filename = file.nameonly(environment.jobname) .. "-fonts-usage.lua" if next(fontdata) then local files = { } local list = { } for id, tfmdata in sortedhash(fontdata) do local filename = tfmdata.properties.filename if filename then local filedata = files[filename] if filedata then filedata.instances = filedata.instances + 1 else local rawdata = tfmdata.shared and tfmdata.shared.rawdata local metadata = rawdata and rawdata.metadata files[filename] = { instances = 1, filename = filename, version = metadata and metadata.version, size = rawdata and rawdata.size, } end else -- what to do end end for k, v in sortedhash(files) do list[#list+1] = v end table.save(filename,list) else os.remove(filename) end end end) -- new do local settings_to_array = utilities.parsers.settings_to_array implement { name = "definefontcolorpalette", arguments = "2 strings", actions = function(name,set) otf.registerpalette(name,settings_to_array(set)) end } end do local pattern = C((1-S("* "))^1) -- strips all after * or ' at' implement { name = "truefontname", arguments = "string", actions = function(s) -- context(match(s,"[^* ]+") or s) context(lpegmatch(pattern,s) or s) end } end do local function getinstancespec(id) local data = fontdata[id or true] local shared = data.shared local resources = shared and shared.rawdata.resources if resources then local instancespec = data.properties.instance if instancespec then local variabledata = resources.variabledata if variabledata then local instances = variabledata.instances if instances then for i=1,#instances do local instance = instances[i] if cleanname(instance.subfamily)== instancespec then local values = table.copy(instance.values) local axis = variabledata.axis for i=1,#values do for j=1,#axis do if values[i].axis == axis[j].tag then values[i].name = axis[j].name break end end end return values end end end end end end end helpers.getinstancespec = getinstancespec -- expandable implement { name = "currentfontinstancespec", public = true, actions = function() local t = getinstancespec() -- current font if t then for i=1,#t do if i > 1 then context.space() end local ti = t[i] context("%s=%s",ti.name,ti.value) end end end } end -- for the moment here (and not in font-con.lua): do local identical = table.identical local copy = table.copy local fontdata = fonts.hashes.identifiers local addcharacters = font.addcharacters -- This helper is mostly meant to add last-resort (virtual) characters -- or runtime generated fonts (so we forget about features and such). It -- will probably take a while before it get used. local trace_adding = false local report_adding = logs.reporter("fonts","add characters") trackers.register("fonts.addcharacters",function(v) trace_adding = v end) function constructors.addcharacters(id,list) local newchar = list.characters if newchar then local data = fontdata[id] local newfont = list.fonts local oldchar = data.characters local oldfont = data.fonts addcharacters(id, { characters = newchar, fonts = newfont, nomath = not data.properties.hasmath, }) -- this is just for tracing, as the assignment only uses the fonts list -- and doesn't store it otherwise if newfont then if oldfont then local oldn = #oldfont local newn = #newfont for n=1,newn do local ok = false local nf = newfont[n] for o=1,oldn do if identical(nf,oldfont[o]) then ok = true break end end if not ok then oldn = oldn + 1 oldfont[oldn] = newfont[i] end end else data.fonts = newfont end end -- this is because we need to know what goes on and also might -- want to access character data for u, c in next, newchar do if trace_adding then report_adding("adding character %U to font %!font:name!",u,id) end oldchar[u] = c end end end implement { name = "addfontpath", arguments = "string", actions = function(list) names.addruntimepath(settings_to_array(list)) end } end -- moved here do local getfontoffamily = tex.getfontoffamily local new_glyph = nodes.pool.glyph local fontproperties = fonts.hashes.properties local function getprivateslot(id,name) if not name then name = id id = currentfont() end local properties = fontproperties[id] local privates = properties and properties.privates return privates and privates[name] end local function getprivatenode(tfmdata,name) if type(tfmdata) == "number" then tfmdata = fontdata[tfmdata] end local properties = tfmdata.properties local font = properties.id local slot = getprivateslot(font,name) if slot then -- todo: set current attribibutes local char = tfmdata.characters[slot] local tonode = char.tonode if tonode then return tonode(font,char) else return new_glyph(font,slot) end end end local function getprivatecharornode(tfmdata,name) if type(tfmdata) == "number" then tfmdata = fontdata[tfmdata] end local properties = tfmdata.properties local font = properties.id local slot = getprivateslot(font,name) if slot then -- todo: set current attributes local char = tfmdata.characters[slot] local tonode = char.tonode if tonode then return "node", tonode(tfmdata,char) else return "char", slot end end end helpers.getprivateslot = getprivateslot helpers.getprivatenode = getprivatenode helpers.getprivatecharornode = getprivatecharornode implement { name = "getprivatechar", arguments = "string", actions = function(name) local p = getprivateslot(name) if p then context(utfchar(p)) end end } implement { name = "getprivatemathchar", arguments = "string", actions = function(name) local p = getprivateslot(getfontoffamily(0),name) if p then context(utfchar(p)) end end } implement { name = "getprivateslot", arguments = "string", actions = function(name) local p = getprivateslot(name) if p then context(p) end end } end -- handy, for now here: function fonts.helpers.collectanchors(tfmdata) local resources = tfmdata.resources -- todo: use shared if not resources or resources.anchors then return resources.anchors end local anchors = { } local function set(unicode,target,class,anchor) local a = anchors[unicode] if not a then anchors[unicode] = { [target] = { anchor } } return end local t = a[target] if not t then a[target] = { anchor } return end local x = anchor[1] local y = anchor[2] for k, v in next, t do if v[1] == x and v[2] == y then return end end t[#t+1] = anchor end local function getanchors(steps,target) for i=1,#steps do local step = steps[i] local coverage = step.coverage for unicode, data in next, coverage do local class = data[1] local anchor = data[2] if anchor[1] ~= 0 or anchor[2] ~= 0 then set(unicode,target,class,anchor) end end end end local function getcursives(steps) for i=1,#steps do local step = steps[i] local coverage = step.coverage for unicode, data in next, coverage do local class = data[1] local en = data[2] local ex = data[3] if en then set(unicode,"entry",class,en) end if ex then set(unicode,"exit", class,ex) end end end end local function collect(list) if list then for i=1,#list do local entry = list[i] local steps = entry.steps local kind = entry.type if kind == "gpos_mark2mark" then getanchors(steps,"mark") elseif kind == "gpos_mark2base" then getanchors(steps,"base") elseif kind == "gpos_mark2ligature" then getanchors(steps,"ligature") elseif kind == "gpos_cursive" then getcursives(steps) end end end end collect(resources.sequences) collect(resources.sublookups) local function sorter(a,b) if a[1] == b[1] then return a[2] < b[2] else return a[1] < b[1] end end for unicode, old in next, anchors do for target, list in next, old do sort(list,sorter) end end resources.anchors = anchors return anchors end do -- We can make these real font dimens instead: local dimen_code = tokens.values.dimension local yscaled = font.yscaled implement { name = "ascender", public = true, protected = true, usage = "value", actions = function(index) -- return dimen_code, yscaled(parameters[true].ascender or 0) -- unreliable -- return dimen_code, yscaled(parameters[true].ascent or 0) -- too much local c = characters[true][0x28] -- ( return dimen_code, yscaled(c and c.height or 0) end } implement { name = "descender", public = true, protected = true, usage = "value", actions = function(index) -- return dimen_code, yscaled(parameters[true].descender or 0) -- unreliable -- return dimen_code, yscaled(parameters[true].descent or 0) -- too much local c = characters[true][0x28] -- ( return dimen_code, yscaled(c and c.depth or 0) end } implement { name = "capheight", public = true, protected = true, usage = "value", actions = function(index) -- return dimen_code, yscaled(parameters[true].capheight or 0) -- unreliable local c = characters[true][0x58] -- X return dimen_code, yscaled(c and c.height or 0) end } -- This has similar performance but then we need to define at the tex end so ... -- -- local properties = { -- [130] = "ascender", -- [133] = "descender", -- [132] = "capheight", -- } -- -- implement { -- name = "fontproperty", -- public = true, -- protected = true, -- usage = "value", -- actions = function(index) -- return dimen_code, yscaled(parameters[true][properties[index]] or 0) -- end -- } end