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
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)
74
75local everywhere = { ["*"] = { ["*"] = true } }
76local noflags = { false, false, false, false }
77
78
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
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,filename)
134
135
136
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
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
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
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)
210 if u then
211
212 return u
213 end
214 end
215 local u = lpegmatch(p,code)
216 if u then
217
218 return u
219 end
220 if not aglunicodes then
221 aglunicodes = fonts.encodings.agl.unicodes
222 end
223 local u = aglunicodes[code]
224 if u then
225
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
236
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
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
365
366
367 data.properties.hasspacekerns = true
368 data.resources .spacekerns = nil
369 end
370
371 local function prepare_kern(list,featuretype,nocheck)
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 not nocheck and not description then
379 skip = skip + 1
380 elseif type(replacement) == "table" then
381 local r = { }
382 for k, v in next, replacement do
383 local u = tounicode(k)
384 if u then
385 r[u] = v
386 if u == 32 then
387 isspace = true
388 end
389 end
390 end
391 if next(r) then
392 cover(coverage,unicode,r)
393 done = done + 1
394 if unicode == 32 then
395 isspace = true
396 end
397 else
398 skip = skip + 1
399 end
400 else
401 skip = skip + 1
402 end
403 end
404 if isspace then
405 resetspacekerns()
406 end
407 return coverage
408 end
409
410 local function prepare_pair(list,featuretype,nocheck)
411 local coverage = { }
412 local cover = coveractions[featuretype]
413 if cover then
414 for code, replacement in next, list do
415 local unicode = tounicode(code)
416 local description = descriptions[unicode]
417 if not nocheck and not description then
418 skip = skip + 1
419 elseif type(replacement) == "table" then
420 local r = { }
421 for k, v in next, replacement do
422 local u = tounicode(k)
423 if u then
424 r[u] = v
425 if u == 32 then
426 isspace = true
427 end
428 end
429 end
430 if next(r) then
431 cover(coverage,unicode,r)
432 done = done + 1
433 if unicode == 32 then
434 isspace = true
435 end
436 else
437 skip = skip + 1
438 end
439 else
440 skip = skip + 1
441 end
442 end
443 if isspace then
444 resetspacekerns()
445 end
446 else
447 report_otf("unknown cover type %a",featuretype)
448 end
449 return coverage
450 end
451
452 local prepare_single = prepare_pair
453
454 local function hassteps(lookups)
455 if lookups then
456 for i=1,#lookups do
457 local l = lookups[i]
458 if l then
459 for j=1,#l do
460 local l = l[j]
461 if l then
462 local n = l.nofsteps
463 if not n then
464
465 return true
466 elseif n > 0 then
467 return true
468 end
469 end
470 end
471 end
472 end
473 end
474 return false
475 end
476
477
478
479 local function prepare_chain(list,featuretype,sublookups,nocheck)
480
481 local rules = list.rules
482 local coverage = { }
483 if rules then
484 local lookuptype = types[featuretype]
485 for nofrules=1,#rules do
486 local rule = rules[nofrules]
487 local current = rule.current
488 local before = rule.before
489 local after = rule.after
490 local replacements = rule.replacements or false
491 local sequence = { }
492 local nofsequences = 0
493 if before then
494 for n=1,#before do
495 nofsequences = nofsequences + 1
496 sequence[nofsequences] = before[n]
497 end
498 end
499 local start = nofsequences + 1
500 for n=1,#current do
501 nofsequences = nofsequences + 1
502 sequence[nofsequences] = current[n]
503 end
504 local stop = nofsequences
505 if after then
506 for n=1,#after do
507 nofsequences = nofsequences + 1
508 sequence[nofsequences] = after[n]
509 end
510 end
511 local lookups = rule.lookups or false
512 local subtype = nil
513 if lookups and sublookups then
514
515 if #lookups > 0 then
516 local ns = stop - start + 1
517 for i=1,ns do
518 if lookups[i] == nil then
519 lookups[i] = 0
520 end
521 end
522 end
523 local l = { }
524 for k, v in sortedhash(lookups) do
525 local t = type(v)
526 if t == "table" then
527
528 for i=1,#v do
529 local vi = v[i]
530 if type(vi) ~= "table" then
531 v[i] = { vi }
532 end
533 end
534 l[k] = v
535 elseif t == "number" then
536 local lookup = sublookups[v]
537 if lookup then
538 l[k] = { lookup }
539 if not subtype then
540 subtype = lookup.type
541 end
542 elseif v == 0 then
543 l[k] = { { type = "gsub_remove", nosteps = true } }
544 else
545 l[k] = false
546 end
547 else
548 l[k] = false
549 end
550 end
551 if nocheck then
552
553 rule.lookups = l
554 end
555 lookups = l
556 end
557 if nofsequences > 0 then
558
559 if hassteps(lookups) then
560
561 local hashed = { }
562 for i=1,nofsequences do
563 local t = { }
564 local s = sequence[i]
565 for i=1,#s do
566 local u = tounicode(s[i])
567 if u then
568 t[u] = true
569 end
570 end
571 hashed[i] = t
572 end
573
574 sequence = hashed
575 local ruleset = {
576 nofrules,
577 lookuptype,
578 sequence,
579 start,
580 stop,
581 lookups,
582 replacements,
583 subtype,
584 }
585 for unic in sortedhash(sequence[start]) do
586 local cu = coverage[unic]
587 if cu then
588 local n = cu.n + 1
589 cu[n] = ruleset
590 cu.n = n
591 else
592 coverage[unic] = {
593 ruleset,
594 n = 1,
595 }
596 end
597 end
598 sequence.n = nofsequences
599 else
600
601 end
602 end
603 end
604 end
605 return coverage
606 end
607
608 local dataset = specifications.dataset
609
610 local function report(name,category,position,first,last,sequences)
611 report_otf("injecting name %a of category %a at position %i in [%i,%i] of [%i,%i]",
612 name,category,position,first,last,1,#sequences)
613 end
614
615 local function inject(specification,sequences,sequence,first,last,category,name)
616 local position = specification.position or false
617 if not position then
618 position = specification.prepend
619 if position == true then
620 if trace_loading then
621 report(name,category,first,first,last,sequences)
622 end
623 insert(sequences,first,sequence)
624 return
625 end
626 end
627 if not position then
628 position = specification.append
629 if position == true then
630 if trace_loading then
631 report(name,category,last+1,first,last,sequences)
632 end
633 insert(sequences,last+1,sequence)
634 return
635 end
636 end
637 local kind = type(position)
638 if kind == "string" then
639 local index = false
640 for i=first,last do
641 local s = sequences[i]
642 local f = s.features
643 if f then
644 for k in sortedhash(f) do
645 if k == position then
646 index = i
647 break
648 end
649 end
650 if index then
651 break
652 end
653 end
654 end
655 if index then
656 position = index
657 else
658 position = last + 1
659 end
660 elseif kind == "number" then
661 if position < 0 then
662 position = last - position + 1
663 end
664 if position > last then
665 position = last + 1
666 elseif position < first then
667 position = first
668 end
669 else
670 position = last + 1
671 end
672 if trace_loading then
673 report(name,category,position,first,last,sequences)
674 end
675 insert(sequences,position,sequence)
676 end
677
678 for s=1,#dataset do
679 local specification = dataset[s]
680 local valid = specification.valid
681 local files = specification.files
682 if files and filename then
683 local name = string.lower(file.basename(filename))
684
685 local okay = files[name]
686
687 if not okay then
688 for i=1,#files do
689 if name == files[i] then
690 okay = true
691 break
692 end
693 end
694 end
695 if okay then
696
697 else
698
699 return
700 end
701 end
702
703 local feature = specification.name or feature
704 if not feature or feature == "" then
705 report_otf("no valid name given for extra feature")
706 elseif not valid or valid(data,specification,feature) then
707 local initialize = specification.initialize
708 if initialize then
709
710 specification.initialize = initialize(specification,data) and initialize or nil
711 end
712 local askedfeatures = specification.features or everywhere
713 local askedsteps = specification.steps or specification.subtables or { specification.data } or { }
714 local featuretype = specification.type or "substitution"
715 local featureaction = false
716 local featureflags = specification.flags or noflags
717 local nocheck = specification.nocheck
718 local mapping = specification.mapping
719 local featureorder = specification.order or { feature }
720 local featurechain = (featuretype == "chainsubstitution" or featuretype == "chainposition") and 1 or 0
721 local nofsteps = 0
722 local steps = { }
723 local sublookups = specification.lookups
724 local category = nil
725 local steptype = nil
726 local sequence = nil
727
728 if fonts.handlers.otf.handlers[featuretype] then
729 featureaction = true
730 else
731 featuretype = normalized[specification.type or "substitution"] or "substitution"
732 end
733
734 checkflags(specification,resources)
735
736 for k, v in next, askedfeatures do
737 if v[1] then
738 askedfeatures[k] = tohash(v)
739 end
740 end
741
742 if featureflags[1] then featureflags[1] = "mark" end
743 if featureflags[2] then featureflags[2] = "ligature" end
744 if featureflags[3] then featureflags[3] = "base" end
745
746 if featureaction then
747
748 category = "gsub"
749 sequence = {
750 features = { [feature] = askedfeatures },
751 flags = featureflags,
752 name = feature,
753 order = featureorder,
754 type = featuretype,
755
756 nofsteps = 0,
757 }
758
759 else
760
761 if sublookups then
762 local s = { }
763 for i=1,#sublookups do
764 local specification = sublookups[i]
765 local askedsteps = specification.steps or specification.subtables or { specification.data } or { }
766 local featuretype = normalized[specification.type or "substitution"] or "substitution"
767 local featureflags = specification.flags or noflags
768 local nofsteps = 0
769 local steps = { }
770 for i=1,#askedsteps do
771 local list = askedsteps[i]
772 local coverage = nil
773 local format = nil
774 if featuretype == "substitution" then
775 coverage = prepare_substitution(list,featuretype,nocheck)
776 elseif featuretype == "ligature" then
777 coverage = prepare_ligature(list,featuretype,nocheck)
778 elseif featuretype == "alternate" then
779 coverage = prepare_alternate(list,featuretype,nocheck)
780 elseif featuretype == "multiple" then
781 coverage = prepare_multiple(list,featuretype,nocheck)
782 elseif featuretype == "kern" or featuretype == "move" then
783 format = featuretype
784 coverage = prepare_kern(list,featuretype,nocheck)
785 elseif featuretype == "pair" then
786 format = "pair"
787 coverage = prepare_pair(list,featuretype,nocheck)
788 elseif featuretype == "single" then
789 format = "single"
790 coverage = prepare_single(list,featuretype,nocheck)
791 end
792 if coverage and next(coverage) then
793 nofsteps = nofsteps + 1
794 steps[nofsteps] = register(coverage,featuretype,format,feature,nofsteps,descriptions,resources)
795 end
796 end
797
798 checkmerge(specification)
799 checksteps(specification)
800
801 s[i] = {
802 [stepkey] = steps,
803 nofsteps = nofsteps,
804 flags = featureflags,
805 type = types[featuretype],
806 }
807 end
808 sublookups = s
809 end
810
811 for i=1,#askedsteps do
812 local list = askedsteps[i]
813 local coverage = nil
814 local format = nil
815 if type(list) == "function" then
816 list = list(data,specification,list,i)
817 end
818 if not list then
819
820 elseif featuretype == "substitution" then
821
822 category = "gsub"
823 coverage = (mapping and list) or prepare_substitution(list,featuretype,nocheck)
824 elseif featuretype == "ligature" then
825 category = "gsub"
826 coverage = prepare_ligature(list,featuretype,nocheck)
827 elseif featuretype == "alternate" then
828 category = "gsub"
829 coverage = prepare_alternate(list,featuretype,nocheck)
830 elseif featuretype == "multiple" then
831 category = "gsub"
832 coverage = prepare_multiple(list,featuretype,nocheck)
833 elseif featuretype == "kern" or featuretype == "move" then
834 category = "gpos"
835 format = featuretype
836 coverage = prepare_kern(list,featuretype,nocheck)
837 elseif featuretype == "pair" then
838 category = "gpos"
839 format = "pair"
840 coverage = prepare_pair(list,featuretype,nocheck)
841 elseif featuretype == "single" then
842 category = "gpos"
843 format = "single"
844 coverage = prepare_single(list,featuretype,nocheck)
845 elseif featuretype == "chainsubstitution" then
846 category = "gsub"
847 coverage = prepare_chain(list,featuretype,sublookups,nocheck)
848 elseif featuretype == "chainposition" then
849 category = "gpos"
850 coverage = prepare_chain(list,featuretype,sublookups,nocheck)
851 else
852 report_otf("not registering feature %a, unknown category",feature)
853 return
854 end
855 if coverage and next(coverage) then
856 nofsteps = nofsteps + 1
857 steps[nofsteps] = register(coverage,featuretype,format,feature,nofsteps,descriptions,resources)
858 end
859 end
860
861 if nofsteps > 0 then
862 sequence = {
863 chain = featurechain,
864 features = { [feature] = askedfeatures },
865 flags = featureflags,
866 name = feature,
867 order = featureorder,
868 [stepkey] = steps,
869 nofsteps = nofsteps,
870 type = specification.handler or types[featuretype],
871 }
872 if prepareonly then
873 return sequence
874 end
875 end
876 end
877
878 if sequence then
879
880 checkflags(sequence,resources)
881 checkmerge(sequence)
882 checksteps(sequence)
883
884 local first, last = getrange(sequences,category)
885 inject(specification,sequences,sequence,first,last,category,feature)
886
887 local features = fontfeatures[category]
888 if not features then
889 features = { }
890 fontfeatures[category] = features
891 end
892 local k = features[feature]
893 if not k then
894 k = { }
895 features[feature] = k
896 end
897
898 for script, languages in next, askedfeatures do
899 local kk = k[script]
900 if not kk then
901 kk = { }
902 k[script] = kk
903 end
904 for language, value in next, languages do
905 kk[language] = value
906 end
907 end
908 end
909
910 end
911 end
912 if trace_loading then
913 report_otf("registering feature %a, affected glyphs %a, skipped glyphs %a",feature,done,skip)
914 end
915
916end
917
918otf.enhancers.addfeature = addfeature
919
920local extrafeatures = { }
921local knownfeatures = { }
922
923function otf.addfeature(name,specification)
924 if type(name) == "table" then
925 specification = name
926 end
927 if type(specification) ~= "table" then
928 report_otf("invalid feature specification, no valid table")
929 return
930 end
931 specification, name = validspecification(specification,name)
932 if name and specification then
933 local slot = knownfeatures[name]
934 if not slot then
935
936 slot = #extrafeatures + 1
937 knownfeatures[name] = slot
938 elseif specification.overload == false then
939
940 slot = #extrafeatures + 1
941 knownfeatures[name] = slot
942 else
943
944 end
945 specification.name = name
946 extrafeatures[slot] = specification
947
948 end
949end
950
951
952
953
954
955local function enhance(data,filename,raw)
956 for slot=1,#extrafeatures do
957 local specification = extrafeatures[slot]
958 addfeature(data,specification.name,specification,nil,filename)
959 end
960end
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979otf.enhancers.enhance = enhance
980
981otf.enhancers.register("check extra features",enhance)
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027 |