diff options
Diffstat (limited to 'tex/context/base/font-sol.lua')
-rw-r--r-- | tex/context/base/font-sol.lua | 898 |
1 files changed, 898 insertions, 0 deletions
diff --git a/tex/context/base/font-sol.lua b/tex/context/base/font-sol.lua new file mode 100644 index 000000000..b44594349 --- /dev/null +++ b/tex/context/base/font-sol.lua @@ -0,0 +1,898 @@ +if not modules then modules = { } end modules ['font-sol'] = { -- this was: node-spl + version = 1.001, + comment = "companion to font-sol.mkiv", + author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", + copyright = "PRAGMA ADE / ConTeXt Development Team", + license = "see context related readme files" +} + +-- This module is dedicated to the oriental tex project and for +-- the moment is too experimental to be publicly supported. +-- +-- We could cache solutions: say that we store the featureset and +-- all 'words' -> replacement ... so we create a large solution +-- database (per font) +-- +-- This module can be optimized by using a dedicated dynamics handler +-- but I'll only do that when the rest of the code is stable. +-- +-- Todo: bind setups to paragraph. + +local gmatch, concat, format, remove = string.gmatch, table.concat, string.format, table.remove +local next, tostring, tonumber = next, tostring, tonumber +local insert, remove = table.insert, table.remove +local utfchar = utf.char +local random = math.random + +local utilities, logs, statistics, fonts, trackers = utilities, logs, statistics, fonts, trackers +local interfaces, commands, attributes = interfaces, commands, attributes +local nodes, node, tex = nodes, node, tex + +local trace_split = false trackers.register("builders.paragraphs.solutions.splitters.splitter", function(v) trace_split = v end) +local trace_optimize = false trackers.register("builders.paragraphs.solutions.splitters.optimizer", function(v) trace_optimize = v end) +local trace_colors = false trackers.register("builders.paragraphs.solutions.splitters.colors", function(v) trace_colors = v end) +local trace_goodies = false trackers.register("fonts.goodies", function(v) trace_goodies = v end) + +local report_solutions = logs.reporter("fonts","solutions") +local report_splitters = logs.reporter("fonts","splitters") +local report_optimizers = logs.reporter("fonts","optimizers") + +local variables = interfaces.variables + +local v_normal = variables.normal +local v_reverse = variables.reverse +local v_preroll = variables.preroll +local v_random = variables.random +local v_split = variables.split + +local settings_to_array = utilities.parsers.settings_to_array +local settings_to_hash = utilities.parsers.settings_to_hash + +local find_node_tail = node.tail or node.slide +local free_node = node.free +local free_nodelist = node.flush_list +local has_attribute = node.has_attribute +local set_attribute = node.set_attribute +local copy_nodelist = node.copy_list +local traverse_nodes = node.traverse +local traverse_ids = node.traverse_id +local protect_glyphs = nodes.handlers.protectglyphs or node.protect_glyphs +local hpack_nodes = node.hpack +local insert_node_before = node.insert_before +local insert_node_after = node.insert_after +local repack_hlist = nodes.repackhlist +local nodes_to_utf = nodes.listtoutf + +local setnodecolor = nodes.tracers.colors.set + +local nodecodes = nodes.nodecodes +local whatsitcodes = nodes.whatsitcodes +local kerncodes = nodes.kerncodes + +local glyph_code = nodecodes.glyph +local disc_code = nodecodes.disc +local kern_code = nodecodes.kern +local hlist_code = nodecodes.hlist +local whatsit_code = nodecodes.whatsit + +local fontkern_code = kerncodes.fontkern + +local localpar_code = whatsitcodes.localpar +local dir_code = whatsitcodes.dir +local userdefined_code = whatsitcodes.userdefined + +local nodepool = nodes.pool +local tasks = nodes.tasks +local usernodeids = nodepool.userids + +local new_textdir = nodepool.textdir +local new_usernumber = nodepool.usernumber +local new_glue = nodepool.glue +local new_leftskip = nodepool.leftskip + +local starttiming = statistics.starttiming +local stoptiming = statistics.stoptiming +local process_characters = nodes.handlers.characters +local inject_kerns = nodes.injections.handler + +local fonthashes = fonts.hashes +local fontdata = fonthashes.identifiers +local setfontdynamics = fonthashes.setdynamics +local fontprocesses = fonthashes.processes + +local texsetattribute = tex.setattribute +local unsetvalue = attributes.unsetvalue + +local parbuilders = builders.paragraphs +parbuilders.solutions = parbuilders.solutions or { } +local parsolutions = parbuilders.solutions +parsolutions.splitters = parsolutions.splitters or { } +local splitters = parsolutions.splitters + +local solutions = { } -- attribute sets +local registered = { } -- backmapping +splitters.registered = registered + +local a_split = attributes.private('splitter') + +local preroll = true +local criterium = 0 +local randomseed = nil +local optimize = nil -- set later +local variant = v_normal +local splitwords = true + +local cache = { } +local variants = { } +local max_less = 0 +local max_more = 0 + +local stack = { } + +local dummy = { + attribute = unsetvalue, + randomseed = 0, + criterium = 0, + preroll = false, + optimize = nil, + splitwords = false, + variant = v_normal, +} + +local function checksettings(r,settings) + local s = r.settings + local method = settings_to_hash(settings.method or "") + local optimize, preroll, splitwords + for k, v in next, method do + if k == v_preroll then + preroll = true + elseif k == v_split then + splitwords = true + elseif variants[k] then + variant = k + optimize = variants[k] -- last one wins + end + end + r.randomseed = tonumber(settings.randomseed) or s.randomseed or r.randomseed or 0 + r.criterium = tonumber(settings.criterium ) or s.criterium or r.criterium or 0 + r.preroll = preroll or false + r.splitwords = splitwords or false + r.optimize = optimize or s.optimize or r.optimize or variants[v_normal] +end + +local function pushsplitter(name,settings) + local r = name and registered[name] + if r then + if settings then + checksettings(r,settings) + end + else + r = dummy + end + insert(stack,r) + -- brr + randomseed = r.randomseed or 0 + criterium = r.criterium or 0 + preroll = r.preroll or false + optimize = r.optimize or nil + splitwords = r.splitwords or nil + -- + texsetattribute(a_split,r.attribute) + return #stack +end + +local function popsplitter() + remove(stack) + local n = #stack + local r = stack[n] or dummy + -- + randomseed = r.randomseed or 0 + criterium = r.criterium or 0 + preroll = r.preroll or false + optimize = r.optimize or nil + -- + texsetattribute(a_split,r.attribute) + return n +end + +local contextsetups = fonts.specifiers.contextsetups + +local function convert(featuresets,name,list) + if list then + local numbers = { } + local nofnumbers = 0 + for i=1,#list do + local feature = list[i] + local fs = featuresets[feature] + local fn = fs and fs.number + if not fn then + -- fall back on global features + fs = contextsetups[feature] + fn = fs and fs.number + end + if fn then + nofnumbers = nofnumbers + 1 + numbers[nofnumbers] = fn + if trace_goodies or trace_optimize then + report_solutions("solution %s of '%s' uses feature '%s' with number %s",i,name,feature,fn) + end + else + report_solutions("solution %s has an invalid feature reference '%s'",i,name,tostring(feature)) + end + end + return nofnumbers > 0 and numbers + end +end + +local function initialize(goodies) + local solutions = goodies.solutions + if solutions then + local featuresets = goodies.featuresets + local goodiesname = goodies.name + if trace_goodies or trace_optimize then + report_solutions("checking solutions in '%s'",goodiesname) + end + for name, set in next, solutions do + set.less = convert(featuresets,name,set.less) + set.more = convert(featuresets,name,set.more) + end + end +end + +fonts.goodies.register("solutions",initialize) + +function splitters.define(name,settings) + local goodies = settings.goodies + local solution = settings.solution + local less = settings.less + local more = settings.more + local less_set, more_set + local l = less and settings_to_array(less) + local m = more and settings_to_array(more) + if goodies then + goodies = fonts.goodies.load(goodies) -- also in tfmdata + if goodies then + local featuresets = goodies.featuresets + local solution = solution and goodies.solutions[solution] + if l and #l > 0 then + less_set = convert(featuresets,name,less) -- take from settings + else + less_set = solution and solution.less -- take from goodies + end + if m and #m > 0 then + more_set = convert(featuresets,name,more) -- take from settings + else + more_set = solution and solution.more -- take from goodies + end + end + else + if l then + local n = #less_set + for i=1,#l do + local ss = contextsetups[l[i]] + if ss then + n = n + 1 + less_set[n] = ss.number + end + end + end + if m then + local n = #more_set + for i=1,#m do + local ss = contextsetups[m[i]] + if ss then + n = n + 1 + more_set[n] = ss.number + end + end + end + end + if trace_optimize then + report_solutions("defining solutions '%s', less: '%s', more: '%s'",name,concat(less_set or {}," "),concat(more_set or {}," ")) + end + local nofsolutions = #solutions + 1 + local t = { + solution = solution, + less = less_set or { }, + more = more_set or { }, + settings = settings, -- for tracing + attribute = nofsolutions, + } + solutions[nofsolutions] = t + registered[name] = t + return nofsolutions +end + +local nofwords, noftries, nofadapted, nofkept, nofparagraphs = 0, 0, 0, 0, 0 + +local splitter_one = usernodeids["splitters.one"] +local splitter_two = usernodeids["splitters.two"] + +local a_word = attributes.private('word') +local a_fontkern = attributes.private('fontkern') + +local encapsulate = false + +directives.register("builders.paragraphs.solutions.splitters.encapsulate", function(v) + encapsulate = v +end) + +function splitters.split(head) + -- quite fast + local current, done, rlmode, start, stop, attribute = head, false, false, nil, nil, 0 + cache, max_less, max_more = { }, 0, 0 + local function flush() -- we can move this + local font = start.font + local last = stop.next + local list = last and copy_nodelist(start,last) or copy_nodelist(start) + local n = #cache + 1 + if encapsulate then + local user_one = new_usernumber(splitter_one,n) + local user_two = new_usernumber(splitter_two,n) + head, start = insert_node_before(head,start,user_one) + insert_node_after(head,stop,user_two) + else + local current = start + while true do + set_attribute(current,a_word,n) + if current == stop then + break + else + current = current.next + end + end + end + if rlmode == "TRT" or rlmode == "+TRT" then + local dirnode = new_textdir("+TRT") + list.prev = dirnode + dirnode.next = list + list = dirnode + end + local c = { + original = list, + attribute = attribute, + direction = rlmode, + font = font + } + if trace_split then + report_splitters("cached %4i: font: %s, attribute: %s, direction: %s, word: %s", + n, font, attribute, nodes_to_utf(list,true), rlmode and "r2l" or "l2r") + end + cache[n] = c + local solution = solutions[attribute] + local l, m = #solution.less, #solution.more + if l > max_less then max_less = l end + if m > max_more then max_more = m end + start, stop, done = nil, nil, true + end + while current do -- also nextid + local next = current.next + local id = current.id + if id == glyph_code then + if current.subtype < 256 then + local a = has_attribute(current,a_split) + if not a then + start, stop = nil, nil + elseif not start then + start, stop, attribute = current, current, a + elseif a ~= attribute then + start, stop = nil, nil + else + stop = current + end + end + elseif id == disc_code then + if splitwords then + if start then + flush() + end + elseif start and next and next.id == glyph_code and next.subtype < 256 then + -- beware: we can cross future lines + stop = next + else + start, stop = nil, nil + end + elseif id == whatsit_code then + if start then + flush() + end + local subtype = current.subtype + if subtype == dir_code or subtype == localpar_code then + rlmode = current.dir + end + else + if start then + flush() + end + end + current = next + end + if start then + flush() + end + nofparagraphs = nofparagraphs + 1 + nofwords = nofwords + #cache + return head, done +end + +local function collect_words(list) -- can be made faster for attributes + local words, w, word = { }, 0, nil + if encapsulate then + for current in traverse_ids(whatsit_code,list) do + if current.subtype == userdefined_code then -- hm + local user_id = current.user_id + if user_id == splitter_one then + word = { current.value, current, current } + w = w + 1 + words[w] = word + elseif user_id == splitter_two then + if word then + word[3] = current + else + -- something is wrong + end + end + end + end + else + local current, first, last, index = list, nil, nil, nil + while current do + -- todo: disc and kern + local id = current.id + if id == glyph_code or id == disc_code then + local a = has_attribute(current,a_word) + if a then + if a == index then + -- same word + last = current + elseif index then + w = w + 1 + words[w] = { index, first, last } + first = current + last = current + index = a + elseif first then + last = current + index = a + else + first = current + last = current + index = a + end + elseif index then + if first then + w = w + 1 + words[w] = { index, first, last } + end + index = nil + first = nil + elseif trace_split then + if id == disc_code then + report_splitters("skipped: disc node") + else + report_splitters("skipped: %s",utfchar(current.char)) + end + end + elseif id == kern_code and (current.subtype == fontkern_code or has_attribute(current,a_fontkern)) then + if first then + last = current + else + first = current + last = current + end + elseif index then + w = w + 1 + words[w] = { index, first, last } + index = nil + first = nil + if id == disc_node then + if trace_split then + report_splitters("skipped disc node") + end + end + end + current = current.next + end + if index then + w = w + 1 + words[w] = { index, first, last } + end + if trace_split then + for i=1,#words do + local w = words[i] + local n, f, l = w[1], w[2], w[3] + local c = cache[n] + if c then + report_splitters("found %4i: word: %s, cached: %s",n,nodes_to_utf(f,true,true,l),nodes_to_utf(c.original,true)) + else + report_splitters("found %4i: word: %s, not in cache",n,nodes_to_utf(f,true,true,l)) + end + end + end + end + return words, list -- check for empty (elsewhere) +end + +-- we could avoid a hpack but hpack is not that slow + +local function doit(word,list,best,width,badness,line,set,listdir) + local changed = 0 + local n = word[1] + local found = cache[n] + if found then + local h, t + if encapsulate then + h = word[2].next -- head of current word + t = word[3].prev -- tail of current word + else + h = word[2] + t = word[3] + end + if splitwords then + -- there are no lines crossed in a word + else + local ok = false + local c = h + while c do + if c == t then + ok = true + break + else + c = c.next + end + end + if not ok then + report_solutions("skipping hyphenated word (for now)") + -- todo: mark in words as skipped, saves a bit runtime + return false, changed + end + end + local original, attribute, direction = found.original, found.attribute, found.direction + local solution = solutions[attribute] + local features = solution and solution[set] + if features then + local featurenumber = features[best] -- not ok probably + if featurenumber then + noftries = noftries + 1 + local first = copy_nodelist(original) + if not trace_colors then + for n in traverse_nodes(first) do -- maybe fast force so no attr needed + set_attribute(n,0,featurenumber) -- this forces dynamics + end + elseif set == "less" then + for n in traverse_nodes(first) do + setnodecolor(n,"font:isol") -- yellow + set_attribute(n,0,featurenumber) + end + else + for n in traverse_nodes(first) do + setnodecolor(n,"font:medi") -- green + set_attribute(n,0,featurenumber) + end + end + local font = found.font + -- local dynamics = found.dynamics + -- local shared = fontdata[font].shared + -- if not dynamics then -- we cache this + -- dynamics = shared.dynamics + -- found.dynamics = dynamics + -- end + -- local processors = found[featurenumber] + -- if not processors then -- we cache this too + -- processors = fonts.handlers.otf.setdynamics(font,featurenumber) + -- found[featurenumber] = processors + -- end + local setdynamics = setfontdynamics[font] + if setdynamics then + local processes = setdynamics(font,featurenumber) + for i=1,#processes do -- often more than 1 + first = processes[i](first,font,featurenumber) + end + else + report_solutions("fatal error, no dynamics for font %s",font) + end + first = inject_kerns(first) + if first.id == whatsit_code then + local temp = first + first = first.next + free_node(temp) + end + local last = find_node_tail(first) + -- replace [u]h->t by [u]first->last + local prev = h.prev + local next = t.next + prev.next = first + first.prev = prev + if next then + last.next = next + next.prev = last + end + -- check new pack + local temp, b = repack_hlist(list,width,'exactly',listdir) + if b > badness then + if trace_optimize then + report_optimizers("line %s, badness before: %s, after: %s, criterium: %s -> quit",line,badness,b,criterium) + end + -- remove last insert + prev.next = h + h.prev = prev + if next then + t.next = next + next.prev = t + else + t.next = nil + end + last.next = nil + free_nodelist(first) + else + if trace_optimize then + report_optimizers("line %s, badness before: %s, after: %s, criterium: %s -> continue",line,badness,b,criterium) + end + -- free old h->t + t.next = nil + free_nodelist(h) -- somhow fails + if not encapsulate then + word[2] = first + word[3] = last + end + changed, badness = changed + 1, b + end + if b <= criterium then + return true, changed + end + end + end + end + return false, changed +end + +-- We repeat some code but adding yet another layer of indirectness is not +-- making things better. + +variants[v_normal] = function(words,list,best,width,badness,line,set,listdir) + local changed = 0 + for i=1,#words do + local done, c = doit(words[i],list,best,width,badness,line,set,listdir) + changed = changed + c + if done then + break + end + end + if changed > 0 then + nofadapted = nofadapted + 1 + -- todo: get rid of pack when ok because we already have packed and we only need the last b + local list, b = repack_hlist(list,width,'exactly',listdir) + return list, true, changed, b -- badness + else + nofkept = nofkept + 1 + return list, false, 0, badness + end +end + +variants[v_reverse] = function(words,list,best,width,badness,line,set,listdir) + local changed = 0 + for i=#words,1,-1 do + local done, c = doit(words[i],list,best,width,badness,line,set,listdir) + changed = changed + c + if done then + break + end + end + if changed > 0 then + nofadapted = nofadapted + 1 + -- todo: get rid of pack when ok because we already have packed and we only need the last b + local list, b = repack_hlist(list,width,'exactly',listdir) + return list, true, changed, b -- badness + else + nofkept = nofkept + 1 + return list, false, 0, badness + end +end + +variants[v_random] = function(words,list,best,width,badness,line,set,listdir) + local changed = 0 + while #words > 0 do + local done, c = doit(remove(words,random(1,#words)),list,best,width,badness,line,set,listdir) + changed = changed + c + if done then + break + end + end + if changed > 0 then + nofadapted = nofadapted + 1 + -- todo: get rid of pack when ok because we already have packed and we only need the last b + local list, b = repack_hlist(list,width,'exactly',listdir) + return list, true, changed, b -- badness + else + nofkept = nofkept + 1 + return list, false, 0, badness + end +end + +local function show_quality(current,what,line) + local set = current.glue_set + local sign = current.glue_sign + local order = current.glue_order + local amount = set * ((sign == 2 and -1) or 1) + report_optimizers("line %s, %s, amount %s, set %s, sign %s (%s), order %s",line,what,amount,set,sign,how,order) +end + +function splitters.optimize(head) + if not optimize then + report_optimizers("no optimizer set") + return + end + local nc = #cache + if nc == 0 then + return + end + starttiming(splitters) + local listdir = nil -- todo ! ! ! + if randomseed then + math.setrandomseedi(randomseed) + randomseed = nil + end + local line = 0 + local tex_hbadness, tex_hfuzz = tex.hbadness, tex.hfuzz + tex.hbadness, tex.hfuzz = 10000, number.maxdimen + if trace_optimize then + report_optimizers("preroll: %s, variant: %s, preroll criterium: %s, cache size: %s", + tostring(preroll),variant,criterium,nc) + end + for current in traverse_ids(hlist_code,head) do + -- report_splitters("before: [%s] => %s",current.dir,nodes.tosequence(current.list,nil)) + line = line + 1 + local sign, dir, list, width = current.glue_sign, current.dir, current.list, current.width +if not encapsulate and list.id == glyph_code then + -- nasty .. we always assume a prev being there .. future luatex will always have a leftskip set + -- current.list, list = insert_node_before(list,list,new_glue(0)) + current.list, list = insert_node_before(list,list,new_leftskip(0)) +end + local temp, badness = repack_hlist(list,width,'exactly',dir) -- it would be nice if the badness was stored in the node + if badness > 0 then + if sign == 0 then + if trace_optimize then + report_optimizers("line %s, badness %s, okay",line,badness) + end + else + local set, max + if sign == 1 then + if trace_optimize then + report_optimizers("line %s, badness %s, underfull, trying more",line,badness) + end + set, max = "more", max_more + else + if trace_optimize then + report_optimizers("line %s, badness %s, overfull, trying less",line,badness) + end + set, max = "less", max_less + end + -- we can keep the best variants + local lastbest, lastbadness = nil, badness + if preroll then + local bb, base + for i=1,max do + if base then + free_nodelist(base) + end + base = copy_nodelist(list) + local words = collect_words(base) -- beware: words is adapted + for j=i,max do + local temp, done, changes, b = optimize(words,base,j,width,badness,line,set,dir) + base = temp + if trace_optimize then + report_optimizers("line %s, alternative: %s.%s, changes: %s, badness %s",line,i,j,changes,b) + end + bb = b + if b <= criterium then + break + end + -- if done then + -- break + -- end + end + if bb and bb > criterium then -- needs checking + if not lastbest then + lastbest, lastbadness = i, bb + elseif bb > lastbadness then + lastbest, lastbadness = i, bb + end + else + break + end + end + free_nodelist(base) + end + local words = collect_words(list) + for best=lastbest or 1,max do + local temp, done, changes, b = optimize(words,list,best,width,badness,line,set,dir) + current.list = temp + if trace_optimize then + report_optimizers("line %s, alternative: %s, changes: %s, badness %s",line,best,changes,b) + end + if done then + if b <= criterium then -- was == 0 + protect_glyphs(list) + break + end + end + end + end + else + if trace_optimize then + report_optimizers("line %s, not bad enough",line) + end + end + -- we pack inside the outer hpack and that way keep the original wd/ht/dp as bonus + current.list = hpack_nodes(current.list,width,'exactly',listdir) + -- report_splitters("after: [%s] => %s",temp.dir,nodes.tosequence(temp.list,nil)) + end + for i=1,nc do + local ci = cache[i] + free_nodelist(ci.original) + end + cache = { } + tex.hbadness, tex.hfuzz = tex_hbadness, tex_hfuzz + stoptiming(splitters) +end + +statistics.register("optimizer statistics", function() + if nofwords > 0 then + local elapsed = statistics.elapsedtime(splitters) + local average = noftries/elapsed + return format("%s words identified in %s paragraphs, %s words retried, %s lines tried, %0.3f seconds used, %s adapted, %0.1f lines per second", + nofwords,nofparagraphs,noftries,nofadapted+nofkept,elapsed,nofadapted,average) + end +end) + +-- we could use a stack + +local enableaction = tasks.enableaction +local disableaction = tasks.disableaction + +local function enable() + enableaction("processors", "builders.paragraphs.solutions.splitters.split") + enableaction("finalizers", "builders.paragraphs.solutions.splitters.optimize") +end + +local function disable() + disableaction("processors", "builders.paragraphs.solutions.splitters.split") + disableaction("finalizers", "builders.paragraphs.solutions.splitters.optimize") +end + +function splitters.start(name,settings) + if pushsplitter(name,settings) == 1 then + enable() + end +end + +function splitters.stop() + if popsplitter() == 0 then + disable() + end +end + +function splitters.set(name,settings) + if #stack > 0 then + stack = { } + else + enable() + end + pushsplitter(name,settings) -- sets attribute etc +end + +function splitters.reset() + if #stack > 0 then + stack = { } + popsplitter() -- resets attribute etc + disable() + end +end + +-- interface + +commands.definefontsolution = splitters.define +commands.startfontsolution = splitters.start +commands.stopfontsolution = splitters.stop +commands.setfontsolution = splitters.set +commands.resetfontsolution = splitters.reset |