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)
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)
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
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
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
474
475 local function prepare_chain(list,featuretype,sublookups,nocheck)
476
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
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
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
542 end
543 else
544 l[k] = false
545 end
546 end
547 if nocheck then
548
549 rule.lookups = l
550 end
551 lookups = l
552 end
553 if nofsequences > 0 then
554
555 if hassteps(lookups) then
556
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
570 sequence = hashed
571 local ruleset = {
572 nofrules,
573 lookuptype,
574 sequence,
575 start,
576 stop,
577 lookups,
578 replacements,
579 subtype,
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
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
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
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
681 local initialize = specification.initialize
682 if initialize then
683
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
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,
727 order = featureorder,
728 type = featuretype,
729
730 nofsteps = 0,
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
794 elseif featuretype == "substitution" then
795
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,
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
854 checkflags(sequence,resources)
855 checkmerge(sequence)
856 checksteps(sequence)
857
858 local first, last = getrange(sequences,category)
859 inject(specification,sequences,sequence,first,last,category,feature)
860
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
910 slot = #extrafeatures + 1
911 knownfeatures[name] = slot
912 elseif specification.overload == false then
913
914 slot = #extrafeatures + 1
915 knownfeatures[name] = slot
916 else
917
918 end
919 specification.name = name
920 extrafeatures[slot] = specification
921
922 end
923end
924
925
926
927
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
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953otf.enhancers.enhance = enhance
954
955otf.enhancers.register("check extra features",enhance)
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001 |