font-otc.lua /size: 35 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['font-otc'] = {
2    version   = 1.001,
3    comment   = "companion to font-ini.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9local insert, sortedkeys, sortedhash, tohash = table.insert, table.sortedkeys, table.sortedhash, table.tohash
10local type, next, tonumber = type, next, tonumber
11local lpegmatch = lpeg.match
12local utfbyte, utflen = utf.byte, utf.len
13local sortedhash = table.sortedhash
14
15-- we assume that the other otf stuff is loaded already
16
17local trace_loading       = false  trackers.register("otf.loading", function(v) trace_loading = v end)
18local report_otf          = logs.reporter("fonts","otf loading")
19
20local fonts               = fonts
21local otf                 = fonts.handlers.otf
22local registerotffeature  = otf.features.register
23local setmetatableindex   = table.setmetatableindex
24
25local fonthelpers         = fonts.helpers
26local checkmerge          = fonthelpers.checkmerge
27local checkflags          = fonthelpers.checkflags
28local checksteps          = fonthelpers.checksteps
29
30local normalized = {
31    substitution      = "substitution",
32    single            = "substitution",
33    ligature          = "ligature",
34    alternate         = "alternate",
35    multiple          = "multiple",
36    kern              = "kern",
37    pair              = "pair",
38    single            = "single",
39    chainsubstitution = "chainsubstitution",
40    chainposition     = "chainposition",
41}
42
43local types = {
44    substitution      = "gsub_single",
45    ligature          = "gsub_ligature",
46    alternate         = "gsub_alternate",
47    multiple          = "gsub_multiple",
48    kern              = "gpos_pair",
49    pair              = "gpos_pair",
50    single            = "gpos_single",
51    chainsubstitution = "gsub_contextchain",
52    chainposition     = "gpos_contextchain",
53}
54
55local names = {
56    gsub_single              = "gsub",
57    gsub_multiple            = "gsub",
58    gsub_alternate           = "gsub",
59    gsub_ligature            = "gsub",
60    gsub_context             = "gsub",
61    gsub_contextchain        = "gsub",
62    gsub_reversecontextchain = "gsub",
63    gpos_single              = "gpos",
64    gpos_pair                = "gpos",
65    gpos_cursive             = "gpos",
66    gpos_mark2base           = "gpos",
67    gpos_mark2ligature       = "gpos",
68    gpos_mark2mark           = "gpos",
69    gpos_context             = "gpos",
70    gpos_contextchain        = "gpos",
71}
72
73setmetatableindex(types, function(t,k) t[k] = k return k end) -- "key"
74
75local everywhere = { ["*"] = { ["*"] = true } } -- or: { ["*"] = { "*" } }
76local noflags    = { false, false, false, false }
77
78-- beware: shared, maybe we should copy the sequence
79
80local function getrange(sequences,category)
81    local count = #sequences
82    local first = nil
83    local last  = nil
84    for i=1,count do
85        local t = sequences[i].type
86        if t and names[t] == category then
87            if not first then
88                first = i
89            end
90            last  = i
91        end
92    end
93    return first or 1, last or count
94end
95
96local function validspecification(specification,name)
97    local dataset = specification.dataset
98    if dataset then
99        -- okay
100    elseif specification[1] then
101        dataset = specification
102        specification = { dataset = dataset }
103    else
104        dataset = { { data = specification.data } }
105        specification.data     = nil
106        specification.coverage = dataset
107        specification.dataset  = dataset
108    end
109    local first = dataset[1]
110    if first then
111        first = first.data
112    end
113    if not first then
114        report_otf("invalid feature specification, no dataset")
115        return
116    end
117    if type(name) ~= "string" then
118        name = specification.name or first.name
119    end
120    if type(name) ~= "string" then
121        report_otf("invalid feature specification, no name")
122        return
123    end
124    local n = #dataset
125    if n > 0 then
126        for i=1,n do
127            setmetatableindex(dataset[i],specification)
128        end
129        return specification, name
130    end
131end
132
133local function addfeature(data,feature,specifications,prepareonly)
134
135    -- todo: add some validator / check code so that we're more tolerant to
136    -- user errors
137
138    if not specifications then
139        report_otf("missing specification")
140        return
141    end
142
143    local descriptions = data.descriptions
144    local resources    = data.resources
145
146    if not descriptions or not resources then
147        report_otf("missing specification")
148        return
149    end
150
151    local features     = resources.features
152    local sequences    = resources.sequences
153
154    if not features or not sequences then
155        report_otf("missing specification")
156        return
157    end
158
159    local alreadydone = resources.alreadydone
160    if not alreadydone then
161        alreadydone = { }
162        resources.alreadydone = alreadydone
163    end
164    if alreadydone[specifications] then
165        return
166    else
167        alreadydone[specifications] = true
168    end
169
170    -- feature has to be unique but the name entry wins eventually
171
172    local fontfeatures = resources.features or everywhere
173    local unicodes     = resources.unicodes
174    local splitter     = lpeg.splitter(" ",unicodes)
175    local done         = 0
176    local skip         = 0
177    local aglunicodes  = false
178    local privateslot  = fonthelpers.privateslot
179
180    local specifications = validspecification(specifications,feature)
181    if not specifications then
182     -- report_otf("invalid specification")
183        return
184    end
185
186    local p = lpeg.P("P")
187            * (lpeg.patterns.hexdigit^1/function(s) return tonumber(s,16) end)
188            * lpeg.P(-1)
189
190    local function tounicode(code)
191        if not code then
192            return
193        end
194        if type(code) == "number" then
195            return code
196        end
197        local u = unicodes[code]
198        if u then
199         -- unicodes[code] = u
200            return u
201        end
202        if utflen(code) == 1 then
203            u = utfbyte(code)
204            if u then
205                return u
206            end
207        end
208        if privateslot then
209            u = privateslot(code) -- no creation !
210            if u then
211             -- unicodes[code] = u
212                return u
213            end
214        end
215        local u = lpegmatch(p,code)
216        if u then
217         -- unicodes[code] = u
218            return u
219        end
220        if not aglunicodes then
221            aglunicodes = fonts.encodings.agl.unicodes -- delayed
222        end
223        local u = aglunicodes[code]
224        if u then
225         -- unicodes[code] = u
226            return u
227        end
228    end
229
230    local coverup      = otf.coverup
231    local coveractions = coverup.actions
232    local stepkey      = coverup.stepkey
233    local register     = coverup.register
234
235    -- todo: directly pass a coverage i.e. for privates that later will be
236    -- set
237
238    local function prepare_substitution(list,featuretype,nocheck)
239        local coverage = { }
240        local cover    = coveractions[featuretype]
241        for code, replacement in next, list do
242            local unicode     = tounicode(code)
243            local description = descriptions[unicode]
244            if not nocheck and not description then
245                -- todo: trace !
246                skip = skip + 1
247            else
248                if type(replacement) == "table" then
249                    replacement = replacement[1]
250                end
251                replacement = tounicode(replacement)
252                if replacement and (nocheck or descriptions[replacement]) then
253                    cover(coverage,unicode,replacement)
254                    done = done + 1
255                else
256                    skip = skip + 1
257                end
258            end
259        end
260        return coverage
261    end
262
263    local function prepare_alternate(list,featuretype,nocheck)
264        local coverage = { }
265        local cover    = coveractions[featuretype]
266        for code, replacement in next, list do
267            local unicode     = tounicode(code)
268            local description = descriptions[unicode]
269            if not nocheck and not description then
270                skip = skip + 1
271            elseif type(replacement) == "table" then
272                local r = { }
273                for i=1,#replacement do
274                    local u = tounicode(replacement[i])
275                    r[i] = (nocheck or descriptions[u]) and u or unicode
276                end
277                cover(coverage,unicode,r)
278                done = done + 1
279            else
280                local u = tounicode(replacement)
281                if u then
282                    cover(coverage,unicode,{ u })
283                    done = done + 1
284                else
285                    skip = skip + 1
286                end
287            end
288        end
289        return coverage
290    end
291
292    local function prepare_multiple(list,featuretype,nocheck)
293        local coverage = { }
294        local cover    = coveractions[featuretype]
295        for code, replacement in next, list do
296            local unicode     = tounicode(code)
297            local description = descriptions[unicode]
298            if not nocheck and not description then
299                skip = skip + 1
300            elseif type(replacement) == "table" then
301                local r = { }
302                local n = 0
303                for i=1,#replacement do
304                    local u = tounicode(replacement[i])
305                    if nocheck or descriptions[u] then
306                        n = n + 1
307                        r[n] = u
308                    end
309                end
310                if n > 0 then
311                    cover(coverage,unicode,r)
312                    done = done + 1
313                else
314                    skip = skip + 1
315                end
316            else
317                local u = tounicode(replacement)
318                if u then
319                    cover(coverage,unicode,{ u })
320                    done = done + 1
321                else
322                    skip = skip + 1
323                end
324            end
325        end
326        return coverage
327    end
328
329    local function prepare_ligature(list,featuretype,nocheck)
330        local coverage = { }
331        local cover    = coveractions[featuretype]
332        for code, ligature in next, list do
333            local unicode     = tounicode(code)
334            local description = descriptions[unicode]
335            if not nocheck and not description then
336                skip = skip + 1
337            else
338                if type(ligature) == "string" then
339                    ligature = { lpegmatch(splitter,ligature) }
340                end
341                local present = true
342                for i=1,#ligature do
343                    local l = ligature[i]
344                    local u = tounicode(l)
345                    if nocheck or descriptions[u] then
346                        ligature[i] = u
347                    else
348                        present = false
349                        break
350                    end
351                end
352                if present then
353                    cover(coverage,unicode,ligature)
354                    done = done + 1
355                else
356                    skip = skip + 1
357                end
358            end
359        end
360        return coverage
361    end
362
363    local function resetspacekerns()
364        -- a bit of a hack, this nil setting but it forces a
365        -- rehash of the resources needed .. the feature itself
366        -- should be a kern (at least for now)
367        data.properties.hasspacekerns = true
368        data.resources .spacekerns    = nil
369    end
370
371    local function prepare_kern(list,featuretype)
372        local coverage = { }
373        local cover    = coveractions[featuretype]
374        local isspace  = false
375        for code, replacement in next, list do
376            local unicode     = tounicode(code)
377            local description = descriptions[unicode]
378            if description and type(replacement) == "table" then
379                local r = { }
380                for k, v in next, replacement do
381                    local u = tounicode(k)
382                    if u then
383                        r[u] = v
384                        if u == 32 then
385                            isspace = true
386                        end
387                    end
388                end
389                if next(r) then
390                    cover(coverage,unicode,r)
391                    done = done + 1
392                    if unicode == 32 then
393                        isspace = true
394                    end
395                else
396                    skip = skip + 1
397                end
398            else
399                skip = skip + 1
400            end
401        end
402        if isspace then
403            resetspacekerns()
404        end
405        return coverage
406    end
407
408    local function prepare_pair(list,featuretype)
409        local coverage = { }
410        local cover    = coveractions[featuretype]
411        if cover then
412            for code, replacement in next, list do
413                local unicode     = tounicode(code)
414                local description = descriptions[unicode]
415                if description and type(replacement) == "table" then
416                    local r = { }
417                    for k, v in next, replacement do
418                        local u = tounicode(k)
419                        if u then
420                            r[u] = v
421                            if u == 32 then
422                                isspace = true
423                            end
424                        end
425                    end
426                    if next(r) then
427                        cover(coverage,unicode,r)
428                        done = done + 1
429                        if unicode == 32 then
430                            isspace = true
431                        end
432                    else
433                        skip = skip + 1
434                    end
435                else
436                    skip = skip + 1
437                end
438            end
439            if isspace then
440                resetspacekerns()
441            end
442        else
443            report_otf("unknown cover type %a",featuretype)
444        end
445        return coverage
446    end
447
448    local prepare_single = prepare_pair -- we could have a better test on the spec
449
450    local function hassteps(lookups)
451        if lookups then
452            for i=1,#lookups do
453                local l = lookups[i]
454                if l then
455                    for j=1,#l do
456                        local l = l[j]
457                        if l then
458                            local n = l.nofsteps
459                            if not n then
460                                -- gsub_remove
461                                return true
462                            elseif n > 0 then
463                                return true
464                            end
465                        end
466                    end
467                end
468            end
469        end
470        return false
471    end
472
473    -- 0 == remove, false = ignore (remove is default)
474
475    local function prepare_chain(list,featuretype,sublookups,nocheck)
476        -- todo: coveractions
477        local rules    = list.rules
478        local coverage = { }
479        if rules then
480            local lookuptype = types[featuretype]
481            for nofrules=1,#rules do
482                local rule         = rules[nofrules]
483                local current      = rule.current
484                local before       = rule.before
485                local after        = rule.after
486                local replacements = rule.replacements or false
487                local sequence     = { }
488                local nofsequences = 0
489                if before then
490                    for n=1,#before do
491                        nofsequences = nofsequences + 1
492                        sequence[nofsequences] = before[n]
493                    end
494                end
495                local start = nofsequences + 1
496                for n=1,#current do
497                    nofsequences = nofsequences + 1
498                    sequence[nofsequences] = current[n]
499                end
500                local stop = nofsequences
501                if after then
502                    for n=1,#after do
503                        nofsequences = nofsequences + 1
504                        sequence[nofsequences] = after[n]
505                    end
506                end
507                local lookups = rule.lookups or false
508                local subtype = nil
509                if lookups and sublookups then
510                    -- inspect(lookups)
511                    if #lookups > 0 then
512                        local ns = stop - start + 1
513                        for i=1,ns do
514                            if lookups[i] == nil then
515                                lookups[i] = 0
516                            end
517                        end
518                    end
519                    local l = { }
520                    for k, v in sortedhash(lookups) do
521                        local t = type(v)
522                        if t == "table" then
523                            -- already ok
524                            for i=1,#v do
525                                local vi = v[i]
526                                if type(vi) ~= "table" then
527                                    v[i] = { vi }
528                                end
529                            end
530                            l[k] = v
531                        elseif t == "number" then
532                            local lookup = sublookups[v]
533                            if lookup then
534                                l[k] = { lookup }
535                                if not subtype then
536                                    subtype = lookup.type
537                                end
538                            elseif v == 0 then
539                                l[k] = { { type = "gsub_remove", nosteps = true } }
540                            else
541                                l[k] = false -- { false } -- new
542                            end
543                        else
544                            l[k] = false -- { false } -- new
545                        end
546                    end
547                    if nocheck then
548                        -- fragile
549                        rule.lookups = l --no, because checking can spoil it
550                    end
551                    lookups = l
552                end
553                if nofsequences > 0 then -- we merge coverage into one
554                    -- we copy as we can have different fonts
555                    if hassteps(lookups) then
556                        -- sequence is the before|current|after match list
557                        local hashed = { }
558                        for i=1,nofsequences do
559                            local t = { }
560                            local s = sequence[i]
561                            for i=1,#s do
562                                local u = tounicode(s[i])
563                                if u then
564                                    t[u] = true
565                                end
566                            end
567                            hashed[i] = t
568                        end
569                        -- hashed is the before|current|after match hash
570                        sequence = hashed
571                        local ruleset = {
572                            nofrules,     -- 1
573                            lookuptype,   -- 2
574                            sequence,     -- 3
575                            start,        -- 4
576                            stop,         -- 5
577                            lookups,      -- 6 (6/7 also signal of what to do)
578                            replacements, -- 7
579                            subtype,      -- 8
580                        }
581                        for unic in sortedhash(sequence[start]) do
582                            local cu = coverage[unic]
583                            if cu then
584                                local n = cu.n + 1
585                                cu[n] = ruleset
586                                cu.n = n
587                            else
588                                coverage[unic] = {
589                                    ruleset,
590                                    n = 1,
591                                }
592                            end
593                        end
594                        sequence.n = nofsequences
595                    else
596                     -- report_otf("no steps for %a",lookuptype) -- e.g. in primes feature
597                    end
598                end
599            end
600        end
601        return coverage
602    end
603
604    local dataset = specifications.dataset
605
606    local function report(name,category,position,first,last,sequences)
607        report_otf("injecting name %a of category %a at position %i in [%i,%i] of [%i,%i]",
608            name,category,position,first,last,1,#sequences)
609    end
610
611    local function inject(specification,sequences,sequence,first,last,category,name)
612        local position = specification.position or false
613        if not position then
614            position = specification.prepend
615            if position == true then
616                if trace_loading then
617                    report(name,category,first,first,last,sequences)
618                end
619                insert(sequences,first,sequence)
620                return
621            end
622        end
623        if not position then
624            position = specification.append
625            if position == true then
626                if trace_loading then
627                    report(name,category,last+1,first,last,sequences)
628                end
629                insert(sequences,last+1,sequence)
630                return
631            end
632        end
633        local kind = type(position)
634        if kind == "string" then
635            local index = false
636            for i=first,last do
637                local s = sequences[i]
638                local f = s.features
639                if f then
640                    for k in sortedhash(f) do -- next, f do
641                        if k == position then
642                            index = i
643                            break
644                        end
645                    end
646                    if index then
647                        break
648                    end
649                end
650            end
651            if index then
652                position = index
653            else
654                position = last + 1
655            end
656        elseif kind == "number" then
657            if position < 0 then
658                position = last - position + 1
659            end
660            if position > last then
661                position = last + 1
662            elseif position < first then
663                position = first
664            end
665        else
666            position = last + 1
667        end
668        if trace_loading then
669            report(name,category,position,first,last,sequences)
670        end
671        insert(sequences,position,sequence)
672    end
673
674    for s=1,#dataset do
675        local specification = dataset[s]
676        local valid = specification.valid -- nowhere used
677        local feature = specification.name or feature
678        if not feature or feature == "" then
679            report_otf("no valid name given for extra feature")
680        elseif not valid or valid(data,specification,feature) then -- anum uses this
681            local initialize = specification.initialize
682            if initialize then
683                -- when false is returned we initialize only once
684                specification.initialize = initialize(specification,data) and initialize or nil
685            end
686            local askedfeatures = specification.features or everywhere
687            local askedsteps    = specification.steps or specification.subtables or { specification.data } or { }
688            local featuretype   = specification.type or "substitution"
689            local featureaction = false
690            local featureflags  = specification.flags or noflags
691            local nocheck       = specification.nocheck
692            local mapping       = specification.mapping
693            local featureorder  = specification.order or { feature }
694            local featurechain  = (featuretype == "chainsubstitution" or featuretype == "chainposition") and 1 or 0
695            local nofsteps      = 0
696            local steps         = { }
697            local sublookups    = specification.lookups
698            local category      = nil
699            local steptype      = nil
700            local sequence      = nil
701            --
702            if fonts.handlers.otf.handlers[featuretype] then
703                featureaction = true -- function based
704            else
705                featuretype = normalized[specification.type or "substitution"] or "substitution"
706            end
707            --
708            checkflags(specification,resources)
709            --
710            for k, v in next, askedfeatures do
711                if v[1] then
712                    askedfeatures[k] = tohash(v)
713                end
714            end
715            --
716            if featureflags[1] then featureflags[1] = "mark" end
717            if featureflags[2] then featureflags[2] = "ligature" end
718            if featureflags[3] then featureflags[3] = "base" end
719            --
720            if featureaction then
721
722                category = "gsub"
723                sequence = {
724                    features  = { [feature] = askedfeatures },
725                    flags     = featureflags,
726                    name      = feature, -- redundant
727                    order     = featureorder,
728                    type      = featuretype,
729                 -- steps     = { },
730                    nofsteps  = 0, -- just in case we test for that
731                }
732
733            else
734
735                if sublookups then
736                    local s = { }
737                    for i=1,#sublookups do
738                        local specification = sublookups[i]
739                        local askedsteps    = specification.steps or specification.subtables or { specification.data } or { }
740                        local featuretype   = normalized[specification.type or "substitution"] or "substitution"
741                        local featureflags  = specification.flags or noflags
742                        local nofsteps      = 0
743                        local steps         = { }
744                        for i=1,#askedsteps do
745                            local list     = askedsteps[i]
746                            local coverage = nil
747                            local format   = nil
748                            if featuretype == "substitution" then
749                                coverage = prepare_substitution(list,featuretype,nocheck)
750                            elseif featuretype == "ligature" then
751                                coverage = prepare_ligature(list,featuretype,nocheck)
752                            elseif featuretype == "alternate" then
753                                coverage = prepare_alternate(list,featuretype,nocheck)
754                            elseif featuretype == "multiple" then
755                                coverage = prepare_multiple(list,featuretype,nocheck)
756                            elseif featuretype == "kern" or featuretype == "move" then
757                                format   = featuretype
758                                coverage = prepare_kern(list,featuretype)
759                            elseif featuretype == "pair" then
760                                format   = "pair"
761                                coverage = prepare_pair(list,featuretype)
762                            elseif featuretype == "single" then
763                                format   = "single"
764                                coverage = prepare_single(list,featuretype)
765                            end
766                            if coverage and next(coverage) then
767                                nofsteps = nofsteps + 1
768                                steps[nofsteps] = register(coverage,featuretype,format,feature,nofsteps,descriptions,resources)
769                            end
770                        end
771                        --
772                        checkmerge(specification)
773                        checksteps(specification)
774                        --
775                        s[i] = {
776                            [stepkey] = steps,
777                            nofsteps  = nofsteps,
778                            flags     = featureflags,
779                            type      = types[featuretype],
780                        }
781                    end
782                    sublookups = s
783                end
784
785                for i=1,#askedsteps do
786                    local list     = askedsteps[i]
787                    local coverage = nil
788                    local format   = nil
789if type(list) == "function" then
790    list = list(data,specification,list,i)
791end
792                    if not list then
793                        -- see ebgaramond hack
794                    elseif featuretype == "substitution" then
795                        -- see font-imp-tweaks: we directly pass a mapping so no checks done
796                        category = "gsub"
797                        coverage = (mapping and list) or prepare_substitution(list,featuretype,nocheck)
798                    elseif featuretype == "ligature" then
799                        category = "gsub"
800                        coverage = prepare_ligature(list,featuretype,nocheck)
801                    elseif featuretype == "alternate" then
802                        category = "gsub"
803                        coverage = prepare_alternate(list,featuretype,nocheck)
804                    elseif featuretype == "multiple" then
805                        category = "gsub"
806                        coverage = prepare_multiple(list,featuretype,nocheck)
807                    elseif featuretype == "kern" or featuretype == "move" then
808                        category = "gpos"
809                        format   = featuretype
810                        coverage = prepare_kern(list,featuretype)
811                    elseif featuretype == "pair" then
812                        category = "gpos"
813                        format   = "pair"
814                        coverage = prepare_pair(list,featuretype)
815                    elseif featuretype == "single" then
816                        category = "gpos"
817                        format   = "single"
818                        coverage = prepare_single(list,featuretype)
819                    elseif featuretype == "chainsubstitution" then
820                        category = "gsub"
821                        coverage = prepare_chain(list,featuretype,sublookups,nocheck)
822                    elseif featuretype == "chainposition" then
823                        category = "gpos"
824                        coverage = prepare_chain(list,featuretype,sublookups,nocheck)
825                    else
826                        report_otf("not registering feature %a, unknown category",feature)
827                        return
828                    end
829                    if coverage and next(coverage) then
830                        nofsteps = nofsteps + 1
831                        steps[nofsteps] = register(coverage,featuretype,format,feature,nofsteps,descriptions,resources)
832                    end
833                end
834
835                if nofsteps > 0 then
836                    sequence = {
837                        chain     = featurechain,
838                        features  = { [feature] = askedfeatures },
839                        flags     = featureflags,
840                        name      = feature, -- redundant
841                        order     = featureorder,
842                        [stepkey] = steps,
843                        nofsteps  = nofsteps,
844                        type      = specification.handler or types[featuretype],
845                    }
846                    if prepareonly then
847                        return sequence
848                    end
849                end
850            end
851
852            if sequence then
853                -- script = { lang1, lang2, lang3 } or script = { lang1 = true, ... }
854                checkflags(sequence,resources)
855                checkmerge(sequence)
856                checksteps(sequence)
857                -- position | prepend | append
858                local first, last = getrange(sequences,category)
859                inject(specification,sequences,sequence,first,last,category,feature)
860                -- register in metadata (merge as there can be a few)
861                local features = fontfeatures[category]
862                if not features then
863                    features  = { }
864                    fontfeatures[category] = features
865                end
866                local k = features[feature]
867                if not k then
868                    k = { }
869                    features[feature] = k
870                end
871                --
872                for script, languages in next, askedfeatures do
873                    local kk = k[script]
874                    if not kk then
875                        kk = { }
876                        k[script] = kk
877                    end
878                    for language, value in next, languages do
879                        kk[language] = value
880                    end
881                end
882            end
883
884        end
885    end
886    if trace_loading then
887        report_otf("registering feature %a, affected glyphs %a, skipped glyphs %a",feature,done,skip)
888    end
889
890end
891
892otf.enhancers.addfeature = addfeature
893
894local extrafeatures = { }
895local knownfeatures = { }
896
897function otf.addfeature(name,specification)
898    if type(name) == "table" then
899        specification = name
900    end
901    if type(specification) ~= "table" then
902        report_otf("invalid feature specification, no valid table")
903        return
904    end
905    specification, name = validspecification(specification,name)
906    if name and specification then
907        local slot = knownfeatures[name]
908        if not slot then
909            -- we have a new one
910            slot = #extrafeatures + 1
911            knownfeatures[name] = slot
912        elseif specification.overload == false then
913            -- we add an extre one
914            slot = #extrafeatures + 1
915            knownfeatures[name] = slot
916        else
917            -- we overload a previous one
918        end
919        specification.name  = name -- to be sure
920        extrafeatures[slot] = specification
921     -- report_otf("adding feature %a @ %i",name,slot)
922    end
923end
924
925-- for feature, specification in next, extrafeatures do
926--     addfeature(data,feature,specification)
927-- end
928
929local function enhance(data,filename,raw)
930    for slot=1,#extrafeatures do
931        local specification = extrafeatures[slot]
932        addfeature(data,specification.name,specification)
933    end
934end
935
936-- local function enhance(data,filename,raw)
937--     local first = 1
938--     local last  = #extrafeatures
939--     while true do
940--         for slot=first,last do
941--             local specification = extrafeatures[slot]
942--             addfeature(data,specification.name,specification)
943--         end
944--         if #extrafeatures > last then
945--             first = last + 1
946--             last  = #extrafeatures
947--         else
948--             break
949--         end
950--     end
951-- end
952
953otf.enhancers.enhance = enhance
954
955otf.enhancers.register("check extra features",enhance)
956
957-- fonts.handlers.otf.features.register {
958--     name        = 'hangulfix',
959--     description = 'fixes for hangul',
960-- }
961
962-- fonts.handlers.otf.addfeature {
963--     name = "stest",
964--     type = "substitution",
965--     data = {
966--         a = "X",
967--         b = "P",
968--     }
969-- }
970-- fonts.handlers.otf.addfeature {
971--     name = "atest",
972--     type = "alternate",
973--     data = {
974--         a = { "X", "Y" },
975--         b = { "P", "Q" },
976--     }
977-- }
978-- fonts.handlers.otf.addfeature {
979--     name = "mtest",
980--     type = "multiple",
981--     data = {
982--         a = { "X", "Y" },
983--         b = { "P", "Q" },
984--     }
985-- }
986-- fonts.handlers.otf.addfeature {
987--     name = "ltest",
988--     type = "ligature",
989--     data = {
990--         X = { "a", "b" },
991--         Y = { "d", "a" },
992--     }
993-- }
994-- fonts.handlers.otf.addfeature {
995--     name = "ktest",
996--     type = "kern",
997--     data = {
998--         a = { b = -500 },
999--     }
1000-- }
1001