if not modules then modules = { } end modules ['font-ext'] = {
version = 1.001,
comment = "companion to font-ini.mkiv and hand-ini.mkiv",
author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
copyright = "PRAGMA ADE / ConTeXt Development Team",
license = "see context related readme files"
}
local next, type, tonumber = next, type, tonumber
local byte, find, formatters = string.byte, string.find, string.formatters
local utfchar = utf.char
local sortedhash, sortedkeys, sort = table.sortedhash, table.sortedkeys, table.sort
local context = context
local fonts = fonts
local utilities = utilities
local trace_protrusion = false trackers.register("fonts.protrusion", function(v) trace_protrusion = v end)
local trace_expansion = false trackers.register("fonts.expansion", function(v) trace_expansion = v end)
local report_expansions = logs.reporter("fonts","expansions")
local report_protrusions = logs.reporter("fonts","protrusions")
--[[ldx--
When we implement functions that deal with features, most of them
will depend of the font format. Here we define the few that are kind
of neutral.
--ldx]]--
local handlers = fonts.handlers
local hashes = fonts.hashes
local otf = handlers.otf
local afm = handlers.afm
local registerotffeature = otf.features.register
local registerafmfeature = afm.features.register
local fontdata = hashes.identifiers
local fontproperties = hashes.properties
local constructors = fonts.constructors
local getprivate = constructors.getprivate
local allocate = utilities.storage.allocate
local settings_to_array = utilities.parsers.settings_to_array
local settings_to_hash = utilities.parsers.settings_to_hash
local getparameters = utilities.parsers.getparameters
local gettexdimen = tex.getdimen
local family_font = node.family_font
local setmetatableindex = table.setmetatableindex
local implement = interfaces.implement
local variables = interfaces.variables
local v_background = variables.background
local v_frame = variables.frame
local v_empty = variables.empty
local v_none = variables.none
-- -- -- -- -- --
-- shared
-- -- -- -- -- --
local function get_class_and_vector(tfmdata,value,where) -- "expansions"
local g_where = tfmdata.goodies and tfmdata.goodies[where]
local f_where = fonts[where]
local g_classes = g_where and g_where.classes
local f_classes = f_where and f_where.classes
local class = (g_classes and g_classes[value]) or (f_classes and f_classes[value])
if class then
local class_vector = class.vector
local g_vectors = g_where and g_where.vectors
local f_vectors = f_where and f_where.vectors
local vector = (g_vectors and g_vectors[class_vector]) or (f_vectors and f_vectors[class_vector])
return class, vector
end
end
-- -- -- -- -- --
-- expansion (hz)
-- -- -- -- -- --
local expansions = fonts.expansions or allocate()
fonts.expansions = expansions
local classes = expansions.classes or allocate()
local vectors = expansions.vectors or allocate()
expansions.classes = classes
expansions.vectors = vectors
-- beware, pdftex itself uses percentages * 10
--
-- todo: get rid of byte() here
classes.preset = { stretch = 2, shrink = 2, step = .5, factor = 1 }
classes['quality'] = {
stretch = 2, shrink = 2, step = .5, vector = 'default', factor = 1
}
vectors['default'] = {
[byte('A')] = 0.5, [byte('B')] = 0.7, [byte('C')] = 0.7, [byte('D')] = 0.5, [byte('E')] = 0.7,
[byte('F')] = 0.7, [byte('G')] = 0.5, [byte('H')] = 0.7, [byte('K')] = 0.7, [byte('M')] = 0.7,
[byte('N')] = 0.7, [byte('O')] = 0.5, [byte('P')] = 0.7, [byte('Q')] = 0.5, [byte('R')] = 0.7,
[byte('S')] = 0.7, [byte('U')] = 0.7, [byte('W')] = 0.7, [byte('Z')] = 0.7,
[byte('a')] = 0.7, [byte('b')] = 0.7, [byte('c')] = 0.7, [byte('d')] = 0.7, [byte('e')] = 0.7,
[byte('g')] = 0.7, [byte('h')] = 0.7, [byte('k')] = 0.7, [byte('m')] = 0.7, [byte('n')] = 0.7,
[byte('o')] = 0.7, [byte('p')] = 0.7, [byte('q')] = 0.7, [byte('s')] = 0.7, [byte('u')] = 0.7,
[byte('w')] = 0.7, [byte('z')] = 0.7,
[byte('2')] = 0.7, [byte('3')] = 0.7, [byte('6')] = 0.7, [byte('8')] = 0.7, [byte('9')] = 0.7,
}
vectors['quality'] = vectors['default'] -- metatable ?
local function initializeexpansion(tfmdata,value)
if value then
local class, vector = get_class_and_vector(tfmdata,value,"expansions")
if class then
if vector then
local stretch = class.stretch or 0
local shrink = class.shrink or 0
local step = class.step or 0
local factor = class.factor or 1
if trace_expansion then
report_expansions("setting class %a, vector %a, factor %a, stretch %a, shrink %a, step %a",
value,class.vector,factor,stretch,shrink,step)
end
tfmdata.parameters.expansion = {
stretch = 10 * stretch,
shrink = 10 * shrink,
step = 10 * step,
factor = factor,
}
local data = characters and characters.data
for i, chr in next, tfmdata.characters do
local v = vector[i]
if data and not v then -- we could move the data test outside (needed for plain)
local d = data[i]
if d then
local s = d.shcode
if not s then
-- sorry
elseif type(s) == "table" then
v = ((vector[s[1]] or 0) + (vector[s[#s]] or 0)) / 2
else
v = vector[s] or 0
end
end
end
if v and v ~= 0 then
chr.expansion_factor = v*factor
else -- can be option
chr.expansion_factor = factor
end
end
elseif trace_expansion then
report_expansions("unknown vector %a in class %a",class.vector,value)
end
elseif trace_expansion then
report_expansions("unknown class %a",value)
end
end
end
local expansion_specification = {
name = "expansion",
description = "apply hz optimization",
initializers = {
base = initializeexpansion,
node = initializeexpansion,
}
}
registerotffeature(expansion_specification)
registerafmfeature(expansion_specification)
fonts.goodies.register("expansions", function(...) return fonts.goodies.report("expansions", trace_expansion, ...) end)
implement {
name = "setupfontexpansion",
arguments = "2 strings",
actions = function(class,settings) getparameters(classes,class,'preset',settings) end
}
-- -- -- -- -- --
-- protrusion
-- -- -- -- -- --
fonts.protrusions = allocate()
local protrusions = fonts.protrusions
protrusions.classes = allocate()
protrusions.vectors = allocate()
local classes = protrusions.classes
local vectors = protrusions.vectors
-- the values need to be revisioned
classes.preset = { factor = 1, left = 1, right = 1 }
classes['pure'] = {
vector = 'pure', factor = 1
}
classes['punctuation'] = {
vector = 'punctuation', factor = 1
}
classes['alpha'] = {
vector = 'alpha', factor = 1
}
classes['quality'] = {
vector = 'quality', factor = 1
}
vectors['pure'] = {
[0x002C] = { 0, 1 }, -- comma
[0x002E] = { 0, 1 }, -- period
[0x003A] = { 0, 1 }, -- colon
[0x003B] = { 0, 1 }, -- semicolon
[0x002D] = { 0, 1 }, -- hyphen
[0x00AD] = { 0, 1 }, -- also hyphen
[0x2013] = { 0, 0.50 }, -- endash
[0x2014] = { 0, 0.33 }, -- emdash
[0x3001] = { 0, 1 }, -- ideographic comma 、
[0x3002] = { 0, 1 }, -- ideographic full stop 。
[0x060C] = { 0, 1 }, -- arabic comma ،
[0x061B] = { 0, 1 }, -- arabic semicolon ؛
[0x06D4] = { 0, 1 }, -- arabic full stop ۔
}
vectors['punctuation'] = {
[0x003F] = { 0, 0.20 }, -- ?
[0x00BF] = { 0, 0.20 }, -- ¿
[0x0021] = { 0, 0.20 }, -- !
[0x00A1] = { 0, 0.20 }, -- ¡
[0x0028] = { 0.05, 0 }, -- (
[0x0029] = { 0, 0.05 }, -- )
[0x005B] = { 0.05, 0 }, -- [
[0x005D] = { 0, 0.05 }, -- ]
[0x002C] = { 0, 0.70 }, -- comma
[0x002E] = { 0, 0.70 }, -- period
[0x003A] = { 0, 0.50 }, -- colon
[0x003B] = { 0, 0.50 }, -- semicolon
[0x002D] = { 0, 0.70 }, -- hyphen
[0x00AD] = { 0, 0.70 }, -- also hyphen
[0x2013] = { 0, 0.30 }, -- endash
[0x2014] = { 0, 0.20 }, -- emdash
[0x060C] = { 0, 0.70 }, -- arabic comma
[0x061B] = { 0, 0.50 }, -- arabic semicolon
[0x06D4] = { 0, 0.70 }, -- arabic full stop
[0x061F] = { 0, 0.20 }, -- ؟
-- todo: left and right quotes: .5 double, .7 single
[0x2039] = { 0.70, 0.70 }, -- left single guillemet ‹
[0x203A] = { 0.70, 0.70 }, -- right single guillemet ›
[0x00AB] = { 0.50, 0.50 }, -- left guillemet «
[0x00BB] = { 0.50, 0.50 }, -- right guillemet »
[0x2018] = { 0.70, 0.70 }, -- left single quotation mark ‘
[0x2019] = { 0, 0.70 }, -- right single quotation mark ’
[0x201A] = { 0.70, 0 }, -- single low-9 quotation mark ,
[0x201B] = { 0.70, 0 }, -- single high-reversed-9 quotation mark ‛
[0x201C] = { 0.50, 0.50 }, -- left double quotation mark “
[0x201D] = { 0, 0.50 }, -- right double quotation mark ”
[0x201E] = { 0.50, 0 }, -- double low-9 quotation mark „
[0x201F] = { 0.50, 0 }, -- double high-reversed-9 quotation mark ‟
}
vectors['alpha'] = {
[byte("A")] = { .05, .05 },
[byte("F")] = { 0, .05 },
[byte("J")] = { .05, 0 },
[byte("K")] = { 0, .05 },
[byte("L")] = { 0, .05 },
[byte("T")] = { .05, .05 },
[byte("V")] = { .05, .05 },
[byte("W")] = { .05, .05 },
[byte("X")] = { .05, .05 },
[byte("Y")] = { .05, .05 },
[byte("k")] = { 0, .05 },
[byte("r")] = { 0, .05 },
[byte("t")] = { 0, .05 },
[byte("v")] = { .05, .05 },
[byte("w")] = { .05, .05 },
[byte("x")] = { .05, .05 },
[byte("y")] = { .05, .05 },
}
vectors['quality'] = table.merged(
vectors['punctuation'],
vectors['alpha']
)
-- As this is experimental code, users should not depend on it. The implications are still
-- discussed on the ConTeXt Dev List and we're not sure yet what exactly the spec is (the
-- next code is tested with a gyre font patched by / fea file made by Khaled Hosny). The
-- double trick should not be needed it proper hanging punctuation is used in which case
-- values < 1 can be used.
--
-- preferred (in context, usine vectors):
--
-- \definefontfeature[whatever][default][mode=node,protrusion=quality]
--
-- using lfbd and rtbd, with possibibility to enable only one side :
--
-- \definefontfeature[whocares][default][mode=node,protrusion=yes, opbd=yes,script=latn]
-- \definefontfeature[whocares][default][mode=node,protrusion=right,opbd=yes,script=latn]
--
-- idem, using multiplier
--
-- \definefontfeature[whocares][default][mode=node,protrusion=2,opbd=yes,script=latn]
-- \definefontfeature[whocares][default][mode=node,protrusion=double,opbd=yes,script=latn]
--
-- idem, using named feature file (less frozen):
--
-- \definefontfeature[whocares][default][mode=node,protrusion=2,opbd=yes,script=latn,featurefile=texgyrepagella-regularxx.fea]
classes['double'] = { -- for testing opbd
factor = 2, left = 1, right = 1,
}
local function map_opbd_onto_protrusion(tfmdata,value,opbd)
local characters = tfmdata.characters
local descriptions = tfmdata.descriptions
local properties = tfmdata.properties
local resources = tfmdata.resources
local rawdata = tfmdata.shared.rawdata
local lookuphash = rawdata.lookuphash
local lookuptags = resources.lookuptags
local script = properties.script
local language = properties.language
local done, factor, left, right = false, 1, 1, 1
local class = classes[value]
if class then
factor = class.factor or 1
left = class.left or 1
right = class.right or 1
else
factor = tonumber(value) or 1
end
if opbd ~= "right" then
local validlookups, lookuplist = otf.collectlookups(rawdata,"lfbd",script,language)
if validlookups then
for i=1,#lookuplist do
local lookup = lookuplist[i]
local steps = lookup.steps
if steps then
if trace_protrusion then
report_protrusions("setting left using lfbd")
end
for i=1,#steps do
local step = steps[i]
local coverage = step.coverage
if coverage then
for k, v in next, coverage do
-- local p = - v[3] / descriptions[k].width-- or 1 ~= 0 too but the same
local p = - (v[1] / 1000) * factor * left
characters[k].left_protruding = p
if trace_protrusion then
report_protrusions("lfbd -> %C -> %p",k,p)
end
end
end
end
done = true
end
end
end
end
if opbd ~= "left" then
local validlookups, lookuplist = otf.collectlookups(rawdata,"rtbd",script,language)
if validlookups then
for i=1,#lookuplist do
local lookup = lookuplist[i]
local steps = lookup.steps
if steps then
if trace_protrusion then
report_protrusions("setting right using rtbd")
end
for i=1,#steps do
local step = steps[i]
local coverage = step.coverage
if coverage then
for k, v in next, coverage do
-- local p = v[3] / descriptions[k].width -- or 3
local p = (v[1] / 1000) * factor * right
characters[k].right_protruding = p
if trace_protrusion then
report_protrusions("rtbd -> %C -> %p",k,p)
end
end
end
end
end
done = true
end
end
end
end
-- The opbd test is just there because it was discussed on the context development list. However,
-- the mentioned fxlbi.otf font only has some kerns for digits. So, consider this feature not supported
-- till we have a proper test font.
local function initializeprotrusion(tfmdata,value)
if value then
local opbd = tfmdata.shared.features.opbd
if opbd then
-- possible values: left right both yes no (experimental)
map_opbd_onto_protrusion(tfmdata,value,opbd)
else
local class, vector = get_class_and_vector(tfmdata,value,"protrusions")
if class then
if vector then
local factor = class.factor or 1
local left = class.left or 1
local right = class.right or 1
if trace_protrusion then
report_protrusions("setting class %a, vector %a, factor %a, left %a, right %a",
value,class.vector,factor,left,right)
end
local data = characters.data
local emwidth = tfmdata.parameters.quad
tfmdata.parameters.protrusion = {
factor = factor,
left = left,
right = right,
}
for i, chr in next, tfmdata.characters do
local v, pl, pr = vector[i], nil, nil
if v then
pl, pr = v[1], v[2]
else
local d = data[i]
if d then
local s = d.shcode
if not s then
-- sorry
elseif type(s) == "table" then
local vl, vr = vector[s[1]], vector[s[#s]]
if vl then pl = vl[1] end
if vr then pr = vr[2] end
else
v = vector[s]
if v then
pl, pr = v[1], v[2]
end
end
end
end
if pl and pl ~= 0 then
chr.left_protruding = left *pl*factor
end
if pr and pr ~= 0 then
chr.right_protruding = right*pr*factor
end
end
elseif trace_protrusion then
report_protrusions("unknown vector %a in class %a",class.vector,value)
end
elseif trace_protrusion then
report_protrusions("unknown class %a",value)
end
end
end
end
local protrusion_specification = {
name = "protrusion",
description = "l/r margin character protrusion",
initializers = {
base = initializeprotrusion,
node = initializeprotrusion,
}
}
registerotffeature(protrusion_specification)
registerafmfeature(protrusion_specification)
fonts.goodies.register("protrusions", function(...) return fonts.goodies.report("protrusions", trace_protrusion, ...) end)
implement {
name = "setupfontprotrusion",
arguments = "2 strings",
actions = function(class,settings) getparameters(classes,class,'preset',settings) end
}
-- -- --
local function initializenostackmath(tfmdata,value)
tfmdata.properties.nostackmath = value and true
end
registerotffeature {
name = "nostackmath",
description = "disable math stacking mechanism",
initializers = {
base = initializenostackmath,
node = initializenostackmath,
}
}
local function initializerealdimensions(tfmdata,value)
tfmdata.properties.realdimensions = value and true
end
registerotffeature {
name = "realdimensions",
description = "accept negative dimenions",
initializers = {
base = initializerealdimensions,
node = initializerealdimensions,
}
}
local function initializeitlc(tfmdata,value) -- hm, always value
if value then
-- the magic 40 and it formula come from Dohyun Kim but we might need another guess
local parameters = tfmdata.parameters
local italicangle = parameters.italicangle
if italicangle and italicangle ~= 0 then
local properties = tfmdata.properties
local factor = tonumber(value) or 1
properties.hasitalics = true
properties.autoitalicamount = factor * (parameters.uwidth or 40)/2
end
end
end
local italic_specification = {
name = "itlc",
description = "italic correction",
initializers = {
base = initializeitlc,
node = initializeitlc,
}
}
registerotffeature(italic_specification)
registerafmfeature(italic_specification)
local function initializetextitalics(tfmdata,value) -- yes no delay
tfmdata.properties.textitalics = toboolean(value)
end
local textitalics_specification = {
name = "textitalics",
description = "use alternative text italic correction",
initializers = {
base = initializetextitalics,
node = initializetextitalics,
}
}
registerotffeature(textitalics_specification)
registerafmfeature(textitalics_specification)
-- local function initializemathitalics(tfmdata,value) -- yes no delay
-- tfmdata.properties.mathitalics = toboolean(value)
-- end
--
-- local mathitalics_specification = {
-- name = "mathitalics",
-- description = "use alternative math italic correction",
-- initializers = {
-- base = initializemathitalics,
-- node = initializemathitalics,
-- }
-- }
-- registerotffeature(mathitalics_specification)
-- registerafmfeature(mathitalics_specification)
-- slanting
local function initializeslant(tfmdata,value)
value = tonumber(value)
if not value then
value = 0
elseif value > 1 then
value = 1
elseif value < -1 then
value = -1
end
tfmdata.parameters.slantfactor = value
end
local slant_specification = {
name = "slant",
description = "slant glyphs",
initializers = {
base = initializeslant,
node = initializeslant,
}
}
registerotffeature(slant_specification)
registerafmfeature(slant_specification)
local function initializeextend(tfmdata,value)
value = tonumber(value)
if not value then
value = 0
elseif value > 10 then
value = 10
elseif value < -10 then
value = -10
end
tfmdata.parameters.extendfactor = value
end
local extend_specification = {
name = "extend",
description = "scale glyphs horizontally",
initializers = {
base = initializeextend,
node = initializeextend,
}
}
registerotffeature(extend_specification)
registerafmfeature(extend_specification)
-- For Wolfgang Schuster:
--
-- \definefontfeature[thisway][default][script=hang,language=zhs,dimensions={2,2,2}]
-- \definedfont[file:kozminpr6nregular*thisway]
--
-- For the moment we don't mess with the descriptions.
local function manipulatedimensions(tfmdata,key,value)
if type(value) == "string" and value ~= "" then
local characters = tfmdata.characters
local parameters = tfmdata.parameters
local emwidth = parameters.quad
local exheight = parameters.xheight
local newwidth = false
local newheight = false
local newdepth = false
if value == "strut" then
newheight = gettexdimen("strutht")
newdepth = gettexdimen("strutdp")
elseif value == "mono" then
newwidth = emwidth
else
local spec = settings_to_array(value)
newwidth = tonumber(spec[1])
newheight = tonumber(spec[2])
newdepth = tonumber(spec[3])
if newwidth then newwidth = newwidth * emwidth end
if newheight then newheight = newheight * exheight end
if newdepth then newdepth = newdepth * exheight end
end
if newwidth or newheight or newdepth then
local additions = { }
for unicode, old_c in next, characters do
local oldwidth = old_c.width
local oldheight = old_c.height
local olddepth = old_c.depth
local width = newwidth or oldwidth or 0
local height = newheight or oldheight or 0
local depth = newdepth or olddepth or 0
if oldwidth ~= width or oldheight ~= height or olddepth ~= depth then
local private = getprivate(tfmdata)
local newslot = { "slot", 1, private } -- { "slot", 0, private }
local new_c
local commands = oldwidth ~= width and {
{ "right", (width - oldwidth) / 2 },
newslot,
} or {
newslot,
}
if height > 0 then
if depth > 0 then
new_c = {
width = width,
height = height,
depth = depth,
commands = commands,
}
else
new_c = {
width = width,
height = height,
commands = commands,
}
end
else
if depth > 0 then
new_c = {
width = width,
depth = depth,
commands = commands,
}
else
new_c = {
width = width,
commands = commands,
}
end
end
setmetatableindex(new_c,old_c)
characters[unicode] = new_c
additions[private] = old_c
end
end
for k, v in next, additions do
characters[k] = v
end
-- elseif height > 0 and depth > 0 then
-- for unicode, old_c in next, characters do
-- old_c.height = height
-- old_c.depth = depth
-- end
-- elseif height > 0 then
-- for unicode, old_c in next, characters do
-- old_c.height = height
-- end
-- elseif depth > 0 then
-- for unicode, old_c in next, characters do
-- old_c.depth = depth
-- end
end
end
end
local dimensions_specification = {
name = "dimensions",
description = "force dimensions",
manipulators = {
base = manipulatedimensions,
node = manipulatedimensions,
}
}
registerotffeature(dimensions_specification)
registerafmfeature(dimensions_specification)
--------------------------------------------------------------------------------------------------------------
-- local function fakemonospace(tfmdata)
-- local resources = tfmdata.resources
-- local gposfeatures = resources.features.gpos
-- local characters = tfmdata.characters
-- local descriptions = tfmdata.descriptions
-- local sequences = resources.sequences
-- local coverage = { }
-- local units = tfmdata.shared.rawdata.metadata.units
-- for k, v in next, characters do
-- local w = descriptions[k].width
-- local d = units - w
-- coverage[k] = { -d/2, 0, units, 0 }
-- end
-- local f = { dflt = { dflt = true } }
-- local s = #sequences + 1
-- local t = {
-- features = { fakemono = f },
-- flags = { false, false, false, false },
-- index = s,
-- name = "p_s_" .. s,
-- nofsteps = 1,
-- order = { "fakemono" },
-- skiphash = false,
-- type = "gpos_single",
-- steps = {
-- {
-- format = "single",
-- coverage = coverage,
-- }
-- }
-- }
-- gposfeatures["fakemono"] = f
-- sequences[s] = t
-- end
--
-- fonts.constructors.features.otf.register {
-- name = "fakemono",
-- description = "fake monospaced",
-- initializers = {
-- node = fakemonospace,
-- },
-- }
--------------------------------------------------------------------------------------------------------------
-- for zhichu chen (see mailing list archive): we might add a few more variants
-- in due time
--
-- \definefontfeature[boxed][default][boundingbox=yes] % paleblue
--
-- maybe:
--
-- \definecolor[DummyColor][s=.75,t=.5,a=1] {\DummyColor test} \nopdfcompression
--
-- local gray = { "pdf", "origin", "/Tr1 gs .75 g" }
-- local black = { "pdf", "origin", "/Tr0 gs 0 g" }
-- boundingbox={yes|background|frame|empty|}
local push = { "push" }
local pop = { "pop" }
----- gray = { "pdf", "origin", ".75 g .75 G" }
----- black = { "pdf", "origin", "0 g 0 G" }
----- gray = { "pdf", ".75 g" }
----- black = { "pdf", "0 g" }
-- local bp = number.dimenfactors.bp
--
-- local downcache = setmetatableindex(function(t,d)
-- local v = { "down", d }
-- t[d] = v
-- return v
-- end)
--
-- local backcache = setmetatableindex(function(t,h)
-- local h = h * bp
-- local v = setmetatableindex(function(t,w)
-- -- local v = { "rule", h, w }
-- local v = { "pdf", "origin", formatters["0 0 %.6F %.6F re F"](w*bp,h) }
-- t[w] = v
-- return v
-- end)
-- t[h] = v
-- return v
-- end)
--
-- local forecache = setmetatableindex(function(t,h)
-- local h = h * bp
-- local v = setmetatableindex(function(t,w)
-- local v = { "pdf", "origin", formatters["%.6F w 0 0 %.6F %.6F re S"](0.25*65536*bp,w*bp,h) }
-- t[w] = v
-- return v
-- end)
-- t[h] = v
-- return v
-- end)
local bp = number.dimenfactors.bp
local r = 16384 * bp -- 65536 // 4
local backcache = setmetatableindex(function(t,h)
local h = h * bp
local v = setmetatableindex(function(t,d)
local d = d * bp
local v = setmetatableindex(function(t,w)
local v = { "pdf", "origin", formatters["%.6F w 0 %.6F %.6F %.6F re f"](r,-d,w*bp,h+d) }
t[w] = v
return v
end)
t[d] = v
return v
end)
t[h] = v
return v
end)
local forecache = setmetatableindex(function(t,h)
local h = h * bp
local v = setmetatableindex(function(t,d)
local d = d * bp
local v = setmetatableindex(function(t,w)
-- the frame goes through the boundingbox
-- local v = { "pdf", "origin", formatters["[] 0 d 0 J %.6F w %.6F %.6F %.6F re S"](r,-d,w*bp,h+d) }
local v = { "pdf", "origin", formatters["[] 0 d 0 J %.6F w %.6F %.6F %.6F %.6F re S"](r,r/2,-d+r/2,w*bp-r,h+d-r) }
t[w] = v
return v
end)
t[d] = v
return v
end)
t[h] = v
return v
end)
local startcolor = nil
local stopcolor = nil
local function showboundingbox(tfmdata,key,value)
if value then
if not backcolors then
local vfspecials = backends.pdf.tables.vfspecials
startcolor = vfspecials.startcolor
stopcolor = vfspecials.stopcolor
end
local characters = tfmdata.characters
local additions = { }
local rulecache = backcache
local showchar = true
local color = "palegray"
if type(value) == "string" then
value = settings_to_array(value)
for i=1,#value do
local v = value[i]
if v == v_frame then
rulecache = forecache
elseif v == v_background then
rulecache = backcache
elseif v == v_empty then
showchar = false
elseif v == v_none then
color = nil
else
color = v
end
end
end
local gray = color and startcolor(color) or nil
local black = gray and stopcolor or nil
for unicode, old_c in next, characters do
local private = getprivate(tfmdata)
local width = old_c.width or 0
local height = old_c.height or 0
local depth = old_c.depth or 0
local char = showchar and { "slot", 1, private } or nil -- { "slot", 0, private }
-- local new_c
-- if depth == 0 then
-- new_c = {
-- width = width,
-- height = height,
-- commands = {
-- push,
-- gray,
-- rulecache[height][width],
-- black,
-- pop,
-- char,
-- }
-- }
-- else
-- new_c = {
-- width = width,
-- height = height,
-- depth = depth,
-- commands = {
-- push,
-- downcache[depth],
-- gray,
-- rulecache[height+depth][width],
-- black,
-- pop,
-- char,
-- }
-- }
-- end
local rule = rulecache[height][depth][width]
local new_c = {
width = width,
height = height,
depth = depth,
commands = gray and {
-- push,
gray,
rule,
black,
-- pop,
char,
} or {
rule,
char,
}
}
setmetatableindex(new_c,old_c)
characters[unicode] = new_c
additions[private] = old_c
end
for k, v in next, additions do
characters[k] = v
end
end
end
registerotffeature {
name = "boundingbox",
description = "show boundingbox",
manipulators = {
base = showboundingbox,
node = showboundingbox,
}
}
-- -- for notosans but not general
--
-- do
--
-- local v_local = interfaces and interfaces.variables and interfaces.variables["local"] or "local"
--
-- local utfbyte = utf.byte
--
-- local function initialize(tfmdata,key,value)
-- local characters = tfmdata.characters
-- local parameters = tfmdata.parameters
-- local oldchar = 32
-- local newchar = 32
-- if value == "locl" or value == v_local then
-- newchar = fonts.handlers.otf.getsubstitution(tfmdata,oldchar,"locl",true) or oldchar
-- elseif value == true then
-- -- use normal space
-- elseif value then
-- newchar = utfbyte(value)
-- else
-- return
-- end
-- local newchar = newchar and characters[newchar]
-- local newspace = newchar and newchar.width
-- if newspace > 0 then
-- parameters.space = newspace
-- parameters.space_stretch = newspace/2
-- parameters.space_shrink = newspace/3
-- parameters.extra_space = parameters.space_shrink
-- end
-- end
--
-- registerotffeature {
-- name = 'space', -- true|false|locl|character
-- description = 'space settings',
-- manipulators = {
-- base = initialize,
-- node = initialize,
-- }
-- }
--
-- end
do
local P, lpegpatterns, lpegmatch = lpeg.P, lpeg.patterns, lpeg.match
local amount, stretch, shrink, extra
local factor = lpegpatterns.unsigned
local space = lpegpatterns.space
local pattern = (
(factor / function(n) amount = tonumber(n) or amount end)
+ (P("+") + P("plus" )) * space^0 * (factor / function(n) stretch = tonumber(n) or stretch end)
+ (P("-") + P("minus")) * space^0 * (factor / function(n) shrink = tonumber(n) or shrink end)
+ ( P("extra")) * space^0 * (factor / function(n) extra = tonumber(n) or extra end)
+ space^1
)^1
local function initialize(tfmdata,key,value)
local characters = tfmdata.characters
local parameters = tfmdata.parameters
if type(value) == "string" then
local emwidth = parameters.quad
amount, stretch, shrink, extra = 0, 0, 0, false
lpegmatch(pattern,value)
if not extra then
if shrink ~= 0 then
extra = shrink
elseif stretch ~= 0 then
extra = stretch
else
extra = amount
end
end
parameters.space = amount * emwidth
parameters.space_stretch = stretch * emwidth
parameters.space_shrink = shrink * emwidth
parameters.extra_space = extra * emwidth
end
end
-- 1.1 + 1.2 - 1.3 minus 1.4 plus 1.1 extra 1.4 -- last one wins
registerotffeature {
name = "spacing",
description = "space settings",
manipulators = {
base = initialize,
node = initialize,
}
}
end
-- -- historic stuff, move from font-ota (handled differently, typo-rep)
--
-- local delete_node = nodes.delete
-- local fontdata = fonts.hashes.identifiers
--
-- local nodecodes = nodes.nodecodes
-- local glyph_code = nodecodes.glyph
--
-- local strippables = allocate()
-- fonts.strippables = strippables
--
-- strippables.joiners = table.tohash {
-- 0x200C, -- zwnj
-- 0x200D, -- zwj
-- }
--
-- strippables.all = table.tohash {
-- 0x000AD, 0x017B4, 0x017B5, 0x0200B, 0x0200C, 0x0200D, 0x0200E, 0x0200F, 0x0202A, 0x0202B,
-- 0x0202C, 0x0202D, 0x0202E, 0x02060, 0x02061, 0x02062, 0x02063, 0x0206A, 0x0206B, 0x0206C,
-- 0x0206D, 0x0206E, 0x0206F, 0x0FEFF, 0x1D173, 0x1D174, 0x1D175, 0x1D176, 0x1D177, 0x1D178,
-- 0x1D179, 0x1D17A, 0xE0001, 0xE0020, 0xE0021, 0xE0022, 0xE0023, 0xE0024, 0xE0025, 0xE0026,
-- 0xE0027, 0xE0028, 0xE0029, 0xE002A, 0xE002B, 0xE002C, 0xE002D, 0xE002E, 0xE002F, 0xE0030,
-- 0xE0031, 0xE0032, 0xE0033, 0xE0034, 0xE0035, 0xE0036, 0xE0037, 0xE0038, 0xE0039, 0xE003A,
-- 0xE003B, 0xE003C, 0xE003D, 0xE003E, 0xE003F, 0xE0040, 0xE0041, 0xE0042, 0xE0043, 0xE0044,
-- 0xE0045, 0xE0046, 0xE0047, 0xE0048, 0xE0049, 0xE004A, 0xE004B, 0xE004C, 0xE004D, 0xE004E,
-- 0xE004F, 0xE0050, 0xE0051, 0xE0052, 0xE0053, 0xE0054, 0xE0055, 0xE0056, 0xE0057, 0xE0058,
-- 0xE0059, 0xE005A, 0xE005B, 0xE005C, 0xE005D, 0xE005E, 0xE005F, 0xE0060, 0xE0061, 0xE0062,
-- 0xE0063, 0xE0064, 0xE0065, 0xE0066, 0xE0067, 0xE0068, 0xE0069, 0xE006A, 0xE006B, 0xE006C,
-- 0xE006D, 0xE006E, 0xE006F, 0xE0070, 0xE0071, 0xE0072, 0xE0073, 0xE0074, 0xE0075, 0xE0076,
-- 0xE0077, 0xE0078, 0xE0079, 0xE007A, 0xE007B, 0xE007C, 0xE007D, 0xE007E, 0xE007F,
-- }
--
-- strippables[true] = strippables.joiners
--
-- local function processformatters(head,font)
-- local subset = fontdata[font].shared.features.formatters
-- local vector = subset and strippables[subset]
-- if vector then
-- local current, done = head, false
-- while current do
-- if current.id == glyph_code and current.subtype<256 and current.font == font then
-- local char = current.char
-- if vector[char] then
-- head, current = delete_node(head,current)
-- done = true
-- else
-- current = current.next
-- end
-- else
-- current = current.next
-- end
-- end
-- return head, done
-- else
-- return head, false
-- end
-- end
--
-- registerotffeature {
-- name = "formatters",
-- description = "hide formatting characters",
-- methods = {
-- base = processformatters,
-- node = processformatters,
-- }
-- }
-- not to be used! experimental code, only needed when testing
local is_letter = characters.is_letter
local always = true
local function collapseitalics(tfmdata,key,value)
local threshold = value == true and 100 or tonumber(value)
if threshold and threshold > 0 then
if threshold > 100 then
threshold = 100
end
for unicode, data in next, tfmdata.characters do
if always or is_letter[unicode] or is_letter[data.unicode] then
local italic = data.italic
if italic and italic ~= 0 then
local width = data.width
if width and width ~= 0 then
local delta = threshold * italic / 100
data.width = width + delta
data.italic = italic - delta
end
end
end
end
end
end
local dimensions_specification = {
name = "collapseitalics",
description = "collapse italics",
manipulators = {
base = collapseitalics,
node = collapseitalics,
}
}
registerotffeature(dimensions_specification)
registerafmfeature(dimensions_specification)
-- a handy helper (might change or be moved to another namespace)
local nodepool = nodes.pool
local new_glyph = nodepool.glyph
local helpers = fonts.helpers
local currentfont = font.current
local currentprivate = 0xE000
local maximumprivate = 0xEFFF
-- if we run out of space we can think of another range but by sharing we can
-- use these privates for mechanisms like alignments-on-character and such
local sharedprivates = setmetatableindex(function(t,k)
v = currentprivate
if currentprivate < maximumprivate then
currentprivate = currentprivate + 1
else
-- reuse last slot, todo: warning
end
t[k] = v
return v
end)
function helpers.addprivate(tfmdata,name,characterdata)
local properties = tfmdata.properties
local characters = tfmdata.characters
local privates = properties.privates
if not privates then
privates = { }
properties.privates = privates
end
if not name then
name = formatters["anonymous_private_0x%05X"](currentprivate)
end
local usedprivate = sharedprivates[name]
privates[name] = usedprivate
characters[usedprivate] = characterdata
return usedprivate
end
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
function helpers.getprivates(tfmdata)
if type(tfmdata) == "number" then
tfmdata = fontdata[tfmdata]
end
local properties = tfmdata.properties
return properties and properties.privates
end
function helpers.hasprivate(tfmdata,name)
if type(tfmdata) == "number" then
tfmdata = fontdata[tfmdata]
end
local properties = tfmdata.properties
local privates = properties and properties.privates
return privates and privates[name] or false
end
-- relatively new:
do
local extraprivates = { }
function fonts.helpers.addextraprivate(name,f)
extraprivates[#extraprivates+1] = { name, f }
end
local function addextraprivates(tfmdata)
for i=1,#extraprivates do
local e = extraprivates[i]
local c = e[2](tfmdata)
if c then
fonts.helpers.addprivate(tfmdata, e[1], c)
end
end
end
constructors.newfeatures.otf.register {
name = "extraprivates",
description = "extra privates",
default = true,
manipulators = {
base = addextraprivates,
node = addextraprivates,
}
}
end
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(family_font(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
}
-- requested for latex but not supported unless really needed in context:
--
-- registerotffeature {
-- name = "ignoremathconstants",
-- description = "ignore math constants table",
-- initializers = {
-- base = function(tfmdata,value)
-- if value then
-- tfmdata.mathparameters = nil
-- end
-- end
-- }
-- }
-- tfmdata.properties.mathnolimitsmode = tonumber(value) or 0
do
local splitter = lpeg.splitat(",",tonumber)
local lpegmatch = lpeg.match
local function initialize(tfmdata,value)
local mathparameters = tfmdata.mathparameters
if mathparameters then
local sup, sub
if type(value) == "string" then
sup, sub = lpegmatch(splitter,value)
if not sup then
sub, sup = 0, 0
elseif not sub then
sub, sup = sup, 0
end
elseif type(value) == "number" then
sup, sub = 0, value
end
mathparameters.NoLimitSupFactor = sup
mathparameters.NoLimitSubFactor = sub
end
end
registerotffeature {
name = "mathnolimitsmode",
description = "influence nolimits placement",
initializers = {
base = initialize,
node = initialize,
}
}
end
do
local function initialize(tfmdata,value)
local properties = tfmdata.properties
if properties then
properties.identity = value == "vertical" and "vertical" or "horizontal"
end
end
registerotffeature {
name = "identity",
description = "set font identity",
initializers = {
base = initialize,
node = initialize,
}
}
local function initialize(tfmdata,value)
local properties = tfmdata.properties
if properties then
properties.writingmode = value == "vertical" and "vertical" or "horizontal"
end
end
registerotffeature {
name = "writingmode",
description = "set font direction",
initializers = {
base = initialize,
node = initialize,
}
}
end
do -- another hack for a crappy font
local function additalictowidth(tfmdata,key,value)
local characters = tfmdata.characters
local additions = { }
for unicode, old_c in next, characters do
-- maybe check for math
local oldwidth = old_c.width
local olditalic = old_c.italic
if olditalic and olditalic ~= 0 then
local private = getprivate(tfmdata)
local new_c = {
width = oldwidth + olditalic,
height = old_c.height,
depth = old_c.depth,
commands = {
-- { "slot", 1, private },
-- { "slot", 0, private },
{ "char", private },
{ "right", olditalic },
},
}
setmetatableindex(new_c,old_c)
characters[unicode] = new_c
additions[private] = old_c
end
end
for k, v in next, additions do
characters[k] = v
end
end
registerotffeature {
name = "italicwidths",
description = "add italic to width",
manipulators = {
base = additalictowidth,
-- node = additalictowidth, -- only makes sense for math
}
}
end
do
local tounicode = fonts.mappings.tounicode
local function check(tfmdata,key,value)
if value == "ligatures" then
local private = fonts.constructors and fonts.constructors.privateoffset or 0xF0000
local collected = fonts.handlers.otf.readers.getcomponents(tfmdata.shared.rawdata)
if collected and next(collected)then
for unicode, char in next, tfmdata.characters do
if true then -- if unicode >= private or (unicode >= 0xE000 and unicode <= 0xF8FF) then
local u = collected[unicode]
if u then
local n = #u
for i=1,n do
if u[i] > private then
n = 0
break
end
end
if n > 0 then
if n == 1 then
u = u[1]
end
char.unicode = u
char.tounicode = tounicode(u)
end
end
end
end
end
end
end
-- forceunicodes=ligatures : aggressive lig resolving (e.g. for emoji)
--
-- kind of like: \enabletrackers[fonts.mapping.forceligatures]
registerotffeature {
name = "forceunicodes",
description = "forceunicodes",
manipulators = {
base = check,
node = check,
}
}
end
do
-- This is a rather special test-only feature that I added for the sake of testing
-- Idris's husayni. We wanted to know if uniscribe obeys the order of lookups in a
-- font, in spite of what the description of handling arabic suggests. And indeed,
-- mixed-in lookups of other features (like all these ss* in husayni) are handled
-- the same in context as in uniscribe. If one sets reorderlookups=arab then we sort
-- according to the "assumed" order so e.g. the ss* move to after the standard
-- features. The observed difference in rendering is an indication that uniscribe is
-- quite faithful to the font (while e.g. tests with the hb plugin demonstrate some
-- interference, apart from some hard coded init etc expectations). Anyway, it means
-- that we're okay with the (generic) node processor. A pitfall is that in context
-- we can actually control more, so we can trigger an analyze pass with e.g.
-- dflt/dflt while the libraries depend on the script settings for that. Uniscribe
-- probably also parses the string and when seeing arabic will follow a different
-- code path, although it seems to treat all features equal.
local trace_reorder = trackers.register("fonts.reorderlookups",function(v) trace_reorder = v end)
local report_reorder = logs.reporter("fonts","reorder")
local vectors = { }
vectors.arab = {
gsub = {
ccmp = 1,
isol = 2,
fina = 3,
medi = 4,
init = 5,
rlig = 6,
rclt = 7,
calt = 8,
liga = 9,
dlig = 10,
cswh = 11,
mset = 12,
},
gpos = {
curs = 1,
kern = 2,
mark = 3,
mkmk = 4,
},
}
function otf.reorderlookups(tfmdata,vector)
local order = vectors[vector]
if not order then
return
end
local oldsequences = tfmdata.resources.sequences
if oldsequences then
local sequences = { }
for i=1,#oldsequences do
sequences[i] = oldsequences[i]
end
for i=1,#sequences do
local s = sequences[i]
local features = s.features
local kind = s.type
local index = s.index
if features then
local when
local what
for feature in sortedhash(features) do
if not what then
what = find(kind,"^gsub") and "gsub" or "gpos"
end
local newwhen = order[what][feature]
if not newwhen then
-- skip
elseif not when then
when = newwhen
elseif newwhen < when then
when = newwhen
end
end
s.ondex = s.index
s.index = i
s.what = what == "gsub" and 1 or 2
s.when = when or 99
else
s.ondex = s.index
s.index = i
s.what = 1
s.when = 99
end
end
sort(sequences,function(a,b)
local what_a = a.what
local what_b = b.what
if what_a ~= what_b then
return a.index < b.index
end
local when_a = a.when
local when_b = b.when
if when_a == when_b then
return a.index < b.index
else
return when_a < when_b
end
end)
local swapped = 0
for i=1,#sequences do
local sequence = sequences[i]
local features = sequence.features
if features then
local index = sequence.index
if index ~= i then
swapped = swapped + 1
end
if trace_reorder then
if swapped == 1 then
report_reorder()
report_reorder("start swapping lookups in font %!font:name!",tfmdata)
report_reorder()
report_reorder("gsub order: % t",table.swapped(order.gsub))
report_reorder("gpos order: % t",table.swapped(order.gpos))
report_reorder()
end
report_reorder("%03i : lookup %03i, type %s, sorted %2i, moved %s, % t",
i,index,sequence.what == 1 and "gsub" or "gpos",sequence.when or 99,
(index > i and "-") or (index < i and "+") or "=",sortedkeys(features))
end
end
sequence.what = nil
sequence.when = nil
sequence.index = sequence.ondex
end
if swapped > 0 then
if trace_reorder then
report_reorder()
report_reorder("stop swapping lookups, %i lookups swapped",swapped)
report_reorder()
end
-- tfmdata.resources.sequences = sequences
tfmdata.shared.reorderedsequences = sequences
end
end
end
-- maybe delay till ra is filled
local function reorderlookups(tfmdata,key,value)
if value then
otf.reorderlookups(tfmdata,value)
end
end
registerotffeature {
name = "reorderlookups",
description = "reorder lookups",
manipulators = {
base = reorderlookups,
node = reorderlookups,
}
}
end
-- maybe useful
local function initializeoutline(tfmdata,value)
value = tonumber(value)
if not value then
value = 0
else
value = tonumber(value) or 0
end
if value then
value = value * 1000
end
tfmdata.parameters.mode = 1
tfmdata.parameters.width = value
end
local outline_specification = {
name = "outline",
description = "outline glyphs",
initializers = {
base = initializeoutline,
node = initializeoutline,
}
}
registerotffeature(outline_specification)
registerafmfeature(outline_specification)
-- definitely ugly
local report_effect = logs.reporter("fonts","effect")
local trace_effect = false
trackers.register("fonts.effect", function(v) trace_effect = v end)
local effects = {
inner = 0,
normal = 0,
outer = 1,
outline = 1,
both = 2,
hidden = 3,
}
local function initializeeffect(tfmdata,value)
local spec
if type(value) == "number" then
spec = { width = value }
else
spec = settings_to_hash(value)
end
local effect = spec.effect or "both"
local width = tonumber(spec.width) or 0
local mode = effects[effect]
if not mode then
report_effect("invalid effect %a",effect)
elseif width == 0 and mode == 0 then
report_effect("invalid width %a for effect %a",width,effect)
else
local parameters = tfmdata.parameters
local properties = tfmdata.properties
parameters.mode = mode
parameters.width = width * 1000
local factor = tonumber(spec.factor) or 0
local hfactor = tonumber(spec.vfactor) or factor
local vfactor = tonumber(spec.hfactor) or factor
local delta = tonumber(spec.delta) or 1
local wdelta = tonumber(spec.wdelta) or delta
local hdelta = tonumber(spec.hdelta) or delta
local ddelta = tonumber(spec.ddelta) or hdelta
properties.effect = {
effect = effect,
width = width,
factor = factor,
hfactor = hfactor,
vfactor = vfactor,
wdelta = wdelta,
hdelta = hdelta,
ddelta = ddelta,
}
end
end
local function manipulateeffect(tfmdata)
local effect = tfmdata.properties.effect
if effect then
local characters = tfmdata.characters
local parameters = tfmdata.parameters
local multiplier = effect.width * 100
local wdelta = effect.wdelta * parameters.hfactor * multiplier
local hdelta = effect.hdelta * parameters.vfactor * multiplier
local ddelta = effect.ddelta * parameters.vfactor * multiplier
local hshift = wdelta / 2
local factor = (1 + effect.factor) * parameters.factor
local hfactor = (1 + effect.hfactor) * parameters.hfactor
local vfactor = (1 + effect.vfactor) * parameters.vfactor
for unicode, old_c in next, characters do
local oldwidth = old_c.width
local oldheight = old_c.height
local olddepth = old_c.depth
if oldwidth and oldwidth > 0 then
old_c.width = oldwidth + wdelta
old_c.commands = {
{ "right", hshift },
{ "char", unicode },
}
end
if oldheight and oldheight > 0 then
old_c.height = oldheight + hdelta
end
if olddepth and olddepth > 0 then
old_c.depth = olddepth + ddelta
end
end
parameters.factor = factor
parameters.hfactor = hfactor
parameters.vfactor = vfactor
if trace_effect then
report_effect("applying effect")
report_effect(" effect : %s", effect.effect)
report_effect(" width : %s => %s", effect.width, multiplier)
report_effect(" factor : %s => %s", effect.factor, factor )
report_effect(" hfactor : %s => %s", effect.hfactor,hfactor)
report_effect(" vfactor : %s => %s", effect.vfactor,vfactor)
report_effect(" wdelta : %s => %s", effect.wdelta, wdelta)
report_effect(" hdelta : %s => %s", effect.hdelta, hdelta)
report_effect(" ddelta : %s => %s", effect.ddelta, ddelta)
end
end
end
local effect_specification = {
name = "effect",
description = "apply effects to glyphs",
initializers = {
base = initializeeffect,
node = initializeeffect,
},
manipulators = {
base = manipulateeffect,
node = manipulateeffect,
},
}
registerotffeature(effect_specification)
registerafmfeature(effect_specification)