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)