1if not modules then modules = { } end modules ['font-otl'] = {
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
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26local lower = string.lower
27local type, next, tonumber, tostring, unpack = type, next, tonumber, tostring, unpack
28local abs = math.abs
29local derivetable, sortedhash, remove = table.derive, table.sortedhash, table.remove
30local formatters = string.formatters
31
32local setmetatableindex = table.setmetatableindex
33local allocate = utilities.storage.allocate
34local registertracker = trackers.register
35local registerdirective = directives.register
36local starttiming = statistics.starttiming
37local stoptiming = statistics.stoptiming
38local elapsedtime = statistics.elapsedtime
39local findbinfile = resolvers.findbinfile
40
41
42
43local trace_loading = false registertracker("otf.loading", function(v) trace_loading = v end)
44local trace_features = false registertracker("otf.features", function(v) trace_features = v end)
45
46
47
48local trace_defining = false registertracker("fonts.defining", function(v) trace_defining = v end)
49
50local report_otf = logs.reporter("fonts","otf loading")
51
52local fonts = fonts
53local otf = fonts.handlers.otf
54
55otf.version = 3.144
56otf.cache = containers.define("fonts", "otl", otf.version, true)
57otf.svgcache = containers.define("fonts", "svg", otf.version, true)
58otf.pngcache = containers.define("fonts", "png", otf.version, true)
59otf.pdfcache = containers.define("fonts", "pdf", otf.version, true)
60otf.mpscache = containers.define("fonts", "mps", otf.version, true)
61
62otf.svgenabled = false
63otf.pngenabled = false
64
65local otfreaders = otf.readers
66
67local hashes = fonts.hashes
68local definers = fonts.definers
69local readers = fonts.readers
70local constructors = fonts.constructors
71
72local otffeatures = constructors.features.otf
73local registerotffeature = otffeatures.register
74
75local otfenhancers = constructors.enhancers.otf
76local registerotfenhancer = otfenhancers.register
77
78local forceload = false
79local cleanup = 0
80local syncspace = true
81local forcenotdef = false
82
83local privateoffset = fonts.constructors and fonts.constructors.privateoffset or 0xF0000
84
85local applyruntimefixes = fonts.treatments and fonts.treatments.applyfixes
86
87local wildcard <const> = "*"
88local default <const> = "dflt"
89
90local formats = fonts.formats
91
92formats.otf = "opentype"
93formats.ttf = "truetype"
94formats.ttc = "truetype"
95
96registerdirective("fonts.otf.loader.cleanup", function(v) cleanup = tonumber(v) or (v and 1) or 0 end)
97registerdirective("fonts.otf.loader.force", function(v) forceload = v end)
98registerdirective("fonts.otf.loader.syncspace", function(v) syncspace = v end)
99registerdirective("fonts.otf.loader.forcenotdef", function(v) forcenotdef = v end)
100
101
102
103registerotfenhancer("check extra features", function() end)
104
105
106
107
108local checkmemory = utilities.lua and utilities.lua.checkmemory
109local threshold = 100
110local tracememory = false
111
112registertracker("fonts.otf.loader.memory",function(v) tracememory = v end)
113
114if not checkmemory then
115
116 local collectgarbage = collectgarbage
117
118 checkmemory = function(previous,threshold)
119 local current = collectgarbage("count")
120 if previous then
121 local checked = (threshold or 64)*1024
122 if current - previous > checked then
123 collectgarbage("collect")
124 current = collectgarbage("count")
125 end
126 end
127 return current
128 end
129
130end
131
132function otf.load(filename,sub,instance)
133 local base = file.basename(file.removesuffix(filename))
134 local name = file.removesuffix(base)
135 local attr = lfs.attributes(filename)
136 local size = attr and attr.size or 0
137 local time = attr and attr.modification or 0
138
139 if sub == "" then
140 sub = false
141 end
142 local hash = name
143 if sub then
144 hash = hash .. "-" .. sub
145 end
146 if instance then
147 hash = hash .. "-" .. instance
148 end
149 hash = containers.cleanname(hash)
150 local data = containers.read(otf.cache,hash)
151 local reload = not data or data.size ~= size or data.time ~= time or data.tableversion ~= otfreaders.tableversion
152 if forceload then
153 report_otf("forced reload of %a due to hard coded flag",filename)
154 reload = true
155 end
156 if reload then
157 report_otf("loading %a, hash %a",filename,hash)
158
159 starttiming(otfreaders,true)
160 data = otfreaders.loadfont(filename,sub or 1,instance)
161 if data then
162
163 local used = checkmemory()
164 local resources = data.resources
165 local svgshapes = resources.svgshapes
166 local pngshapes = resources.pngshapes
167 if cleanup == 0 then
168 checkmemory(used,threshold,tracememory)
169 end
170 if svgshapes then
171 resources.svgshapes = nil
172 if otf.svgenabled then
173 local timestamp = os.date()
174
175 containers.write(otf.svgcache,hash, {
176 svgshapes = svgshapes,
177 timestamp = timestamp,
178 })
179 data.properties.svg = {
180 hash = hash,
181 timestamp = timestamp,
182 }
183 end
184 if cleanup > 1 then
185 collectgarbage("collect")
186 else
187 checkmemory(used,threshold,tracememory)
188 end
189 end
190 if pngshapes then
191 resources.pngshapes = nil
192 if otf.pngenabled then
193 local timestamp = os.date()
194
195 containers.write(otf.pngcache,hash, {
196 pngshapes = pngshapes,
197 timestamp = timestamp,
198 })
199 data.properties.png = {
200 hash = hash,
201 timestamp = timestamp,
202 }
203 end
204 if cleanup > 1 then
205 collectgarbage("collect")
206 else
207 checkmemory(used,threshold,tracememory)
208 end
209 end
210
211 otfreaders.compact(data)
212 if cleanup == 0 then
213 checkmemory(used,threshold,tracememory)
214 end
215 otfreaders.rehash(data,"unicodes")
216 otfreaders.addunicodetable(data)
217 otfreaders.extend(data)
218 if cleanup == 0 then
219 checkmemory(used,threshold,tracememory)
220 end
221 if context then
222 otfreaders.condense(data)
223 end
224 otfreaders.pack(data)
225 report_otf("loading done")
226 report_otf("saving %a in cache",filename)
227 data = containers.write(otf.cache,hash,data)
228 if cleanup > 1 then
229 collectgarbage("collect")
230 else
231 checkmemory(used,threshold,tracememory)
232 end
233 stoptiming(otfreaders)
234 if elapsedtime then
235 report_otf("loading, optimizing, packing and caching time %s", elapsedtime(otfreaders))
236 end
237 if cleanup > 3 then
238 collectgarbage("collect")
239 else
240 checkmemory(used,threshold,tracememory)
241 end
242 data = containers.read(otf.cache,hash)
243 if cleanup > 2 then
244 collectgarbage("collect")
245 else
246 checkmemory(used,threshold,tracememory)
247 end
248 else
249 stoptiming(otfreaders)
250 data = nil
251 report_otf("loading failed due to read error")
252 end
253 end
254 if data then
255 if trace_defining then
256 report_otf("loading from cache using hash %a",hash)
257 end
258
259 otfreaders.unpack(data)
260 otfreaders.expand(data)
261 otfreaders.addunicodetable(data)
262
263 otfenhancers.apply(data,filename,data)
264
265
266
267 if applyruntimefixes then
268 applyruntimefixes(filename,data)
269 end
270
271 data.metadata.math = data.resources.mathconstants
272
273
274
275 local classes = data.resources.classes
276 if not classes then
277 local descriptions = data.descriptions
278 classes = setmetatableindex(function(t,k)
279 local d = descriptions[k]
280 local v = (d and d.class or "base") or false
281 t[k] = v
282 return v
283 end)
284 data.resources.classes = classes
285 end
286
287 end
288
289 return data
290end
291
292
293
294function otf.setfeatures(tfmdata,features)
295 local okay = constructors.initializefeatures("otf",tfmdata,features,trace_features,report_otf)
296 if okay then
297 return constructors.collectprocessors("otf",tfmdata,features,trace_features,report_otf)
298 else
299 return { }
300 end
301end
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349local function copytotfm(data,cache_id,wipemath)
350 if data and data.descriptions then
351 local metadata = data.metadata
352 local properties = derivetable(data.properties)
353 local descriptions = derivetable(data.descriptions)
354 local goodies = derivetable(data.goodies)
355 local characters = { }
356 local parameters = { }
357 local mathparameters = { }
358
359 local resources = data.resources
360 local unicodes = resources.unicodes
361 local spaceunits = 500
362 local spacer = "space"
363 local designsize = metadata.designsize or 100
364 local minsize = metadata.minsize or designsize
365 local maxsize = metadata.maxsize or designsize
366 local mathspecs = metadata.math
367
368 if designsize == 0 then
369 designsize = 100
370 minsize = 100
371 maxsize = 100
372 end
373
374 for unicode in next, data.descriptions do
375 characters[unicode] = { }
376 end
377
378
379
380
381 local filename = constructors.checkedfilename(resources)
382 local fontname = metadata.fontname
383 local fullname = metadata.fullname or fontname
384 local psname = fontname or fullname
385 local subfont = metadata.subfontindex
386 local units = metadata.units or 1000
387
388 if units == 0 then
389 units = 1000
390 metadata.units = 1000
391 report_otf("changing %a units to %a",0,units)
392 end
393
394 if not mathspecs then
395
396 elseif wipemath then
397
398
399
400 local wiped = false
401 local features = resources.features
402 local sequences = resources.sequences
403 if features then
404 local gsub = features.gsub
405 if gsub and gsub.ssty then
406 gsub.ssty = nil
407 wiped = true
408 end
409 end
410 if sequences then
411 for i=#sequences,1,-1 do
412 local sequence = sequences[i]
413 local features = sequence.features
414 if features then
415 if features.ssty then
416 remove(sequences,i)
417 wiped = true
418 end
419 end
420 end
421 end
422 if resources.mathconstants then
423 resources.mathconstants = nil
424 wiped = true
425 end
426 if resources.math then
427 metadata.math = nil
428 wiped = true
429 end
430 if wiped then
431 report_otf("math data wiped from %a",fullname)
432 end
433 else
434 for name, value in next, mathspecs do
435 mathparameters[name] = value
436 end
437 for unicode, character in next, characters do
438 local d = descriptions[unicode]
439 local m = d.math
440 if m then
441
442 local italic = m.italic
443 if italic and italic ~= 0 then
444 character.italic = italic
445 end
446
447 local variants = m.variants
448 local parts = m.parts
449 local partsitalic = m.partsitalic
450 local partsorientation = m.partsorientation
451 local mainunicode = d.unicode
452 if variants then
453 local c = character
454 for i=1,#variants do
455 local un = variants[i]
456 c.next = un
457 c = characters[un]
458 end
459 c.parts = parts
460 c.partsorientation = partsorientation
461 if partsitalic and partsitalic ~= 0 then
462 c.partsitalic = partsitalic
463 end
464 elseif parts then
465 character.parts = parts
466 character.partsorientation = partsorientation
467 if partsitalic and partsitalic ~= 0 then
468 character.partsitalic = partsitalic
469 end
470 end
471
472 local topanchor = m.topanchor or m.accent
473 if topanchor then
474 character.topanchor = topanchor
475 end
476
477 local kerns = m.kerns
478 if kerns then
479 character.mathkerns = kerns
480 end
481 end
482 end
483 end
484
485 local monospaced = metadata.monospaced
486 local charwidth = metadata.averagewidth
487 local charxheight = metadata.xheight
488 local italicangle = metadata.italicangle
489 local hasitalics = metadata.hasitalics
490 properties.monospaced = monospaced
491 properties.hasitalics = hasitalics
492 parameters.italicangle = italicangle
493 parameters.charwidth = charwidth
494 parameters.charxheight = charxheight
495
496 local space = 0x0020
497 local emdash = 0x2014
498 if monospaced then
499 if descriptions[space] then
500 spaceunits, spacer = descriptions[space].width, "space"
501 end
502 if not spaceunits and descriptions[emdash] then
503 spaceunits, spacer = descriptions[emdash].width, "emdash"
504 end
505 if not spaceunits and charwidth then
506 spaceunits, spacer = charwidth, "charwidth"
507 end
508 else
509 if descriptions[space] then
510 spaceunits, spacer = descriptions[space].width, "space"
511 end
512 if not spaceunits and descriptions[emdash] then
513 spaceunits, spacer = descriptions[emdash].width/2, "emdash/2"
514 end
515 if not spaceunits and charwidth then
516 spaceunits, spacer = charwidth, "charwidth"
517 end
518 end
519 spaceunits = tonumber(spaceunits) or units/2
520
521 parameters.slant = 0
522 parameters.space = spaceunits
523 parameters.spacestretch = 1*units/2
524 parameters.spaceshrink = 1*units/3
525 parameters.xheight = 2*units/5
526 parameters.quad = units
527 if spaceunits < 2*units/5 then
528
529 end
530 if italicangle and italicangle ~= 0 then
531 parameters.italicangle = italicangle
532 parameters.italicfactor = math.cos(math.rad(90+italicangle))
533 parameters.slant = - math.tan(italicangle*math.pi/180)
534 end
535 if monospaced then
536 parameters.spacestretch = 0
537 parameters.spaceshrink = 0
538 elseif syncspace then
539 parameters.spacestretch = spaceunits/2
540 parameters.spaceshrink = spaceunits/3
541 end
542 parameters.extraspace = parameters.spaceshrink
543 if charxheight then
544 parameters.xheight = charxheight
545 else
546 local x = 0x0078
547 if x then
548 local x = descriptions[x]
549 if x then
550 parameters.xheight = x.height
551 end
552 end
553 end
554
555 parameters.designsize = (designsize/10)*65536
556 parameters.minsize = (minsize /10)*65536
557 parameters.maxsize = (maxsize /10)*65536
558 parameters.ascender = abs(metadata.ascender or 0)
559 parameters.descender = abs(metadata.descender or 0)
560 parameters.capheight = abs(metadata.capheight or 0)
561 parameters.ascent = abs(metadata.ascent or parameters.ascender or 0)
562 parameters.descent = abs(metadata.descent or parameters.descender or 0)
563 parameters.units = units
564 parameters.vheight = metadata.defaultvheight
565
566 properties.space = spacer
567 properties.format = data.format or formats.otf
568 properties.filename = filename
569 properties.fontname = fontname
570 properties.fullname = fullname
571 properties.psname = psname
572 properties.name = filename or fullname
573 properties.subfont = subfont
574
575 local duplicates = resources and resources.duplicates
576 if duplicates then
577 local maxindex = data.nofglyphs or metadata.nofglyphs
578 if maxindex then
579 for u, d in sortedhash(duplicates) do
580 local du = descriptions[u]
581 if du then
582 for uu in sortedhash(d) do
583 local duu = descriptions[uu]
584 if duu then
585 maxindex = maxindex + 1
586 duu.dupindex = du.index
587 duu.index = maxindex
588 end
589 end
590 else
591
592 end
593 end
594 end
595 end
596
597
598
599
600 properties.private = properties.private or data.private or privateoffset
601
602 return {
603 characters = characters,
604 descriptions = descriptions,
605 parameters = parameters,
606 mathparameters = mathparameters,
607 resources = resources,
608 properties = properties,
609 goodies = goodies,
610 }
611 end
612end
613
614
615
616
617
618
619
620local converters = {
621 woff = {
622 cachename = "webfonts",
623 action = otf.readers.woff2otf,
624 }
625}
626
627
628
629
630local function checkconversion(specification)
631 local filename = specification.filename
632 local converter = converters[lower(file.suffix(filename))]
633 if converter then
634 local base = file.basename(filename)
635 local name = file.removesuffix(base)
636 local attr = lfs.attributes(filename)
637 local size = attr and attr.size or 0
638 local time = attr and attr.modification or 0
639 if size > 0 then
640 local cleanname = containers.cleanname(name)
641 local cachename = caches.setfirstwritablefile(cleanname,converter.cachename)
642 if not io.exists(cachename) or (time ~= lfs.attributes(cachename).modification) then
643 report_otf("caching font %a in %a",filename,cachename)
644 converter.action(filename,cachename)
645 lfs.touch(cachename,time,time)
646 end
647 specification.filename = cachename
648 end
649 end
650end
651
652local function otftotfm(specification)
653 local cache_id = specification.hash
654 local tfmdata = containers.read(constructors.cache,cache_id)
655 if not tfmdata then
656 checkconversion(specification)
657 local name = specification.name
658 local sub = specification.sub
659 local subindex = specification.subindex
660 local filename = specification.filename
661 local features = specification.features.normal
662 local instance = specification.instance or (features and features.axis)
663 local rawdata = otf.load(filename,sub,instance)
664 if rawdata and next(rawdata) then
665 local descriptions = rawdata.descriptions
666 rawdata.lookuphash = { }
667 tfmdata = copytotfm(rawdata,cache_id,features and features.wipemath)
668 if tfmdata and next(tfmdata) then
669
670 local features = constructors.checkedfeatures("otf",features)
671 local shared = tfmdata.shared
672 if not shared then
673 shared = { }
674 tfmdata.shared = shared
675 end
676 shared.rawdata = rawdata
677
678 shared.dynamics = { }
679
680 tfmdata.changed = { }
681 shared.features = features
682 shared.processes = otf.setfeatures(tfmdata,features)
683 end
684 end
685 containers.write(constructors.cache,cache_id,tfmdata)
686 end
687 return tfmdata
688end
689
690local function read_from_otf(specification)
691 local tfmdata = otftotfm(specification)
692 if tfmdata then
693
694 tfmdata.properties.name = specification.name
695 tfmdata.properties.sub = specification.sub
696 tfmdata.properties.id = specification.id
697
698 tfmdata = constructors.scale(tfmdata,specification)
699 local allfeatures = tfmdata.shared.features or specification.features.normal
700 constructors.applymanipulators("otf",tfmdata,allfeatures,trace_features,report_otf)
701 constructors.setname(tfmdata,specification)
702 fonts.loggers.register(tfmdata,file.suffix(specification.filename),specification)
703 end
704 return tfmdata
705end
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763function otf.collectlookups(rawdata,kind,script,language)
764 if not kind then
765 return
766 end
767 if not script then
768 script = default
769 end
770 if not language then
771 language = default
772 end
773 local lookupcache = rawdata.lookupcache
774 if not lookupcache then
775 lookupcache = { }
776 rawdata.lookupcache = lookupcache
777 end
778 local kindlookup = lookupcache[kind]
779 if not kindlookup then
780 kindlookup = { }
781 lookupcache[kind] = kindlookup
782 end
783 local scriptlookup = kindlookup[script]
784 if not scriptlookup then
785 scriptlookup = { }
786 kindlookup[script] = scriptlookup
787 end
788 local languagelookup = scriptlookup[language]
789 if not languagelookup then
790 local sequences = rawdata.resources.sequences
791 local featuremap = { }
792 local featurelist = { }
793 if sequences then
794 for s=1,#sequences do
795 local sequence = sequences[s]
796 local features = sequence.features
797 if features then
798 features = features[kind]
799 if features then
800
801 features = features[script] or features[wildcard]
802 if features then
803
804 features = features[language] or features[wildcard]
805 if features then
806 if not featuremap[sequence] then
807 featuremap[sequence] = true
808 featurelist[#featurelist+1] = sequence
809 end
810 end
811 end
812 end
813 end
814 end
815 if #featurelist == 0 then
816 featuremap, featurelist = false, false
817 end
818 else
819 featuremap, featurelist = false, false
820 end
821 languagelookup = { featuremap, featurelist }
822 scriptlookup[language] = languagelookup
823 end
824 return unpack(languagelookup)
825end
826
827
828
829local function getgsub(tfmdata,k,kind,value,script,language)
830 local resources = nil
831
832 local shared = tfmdata.shared
833 local rawdata = shared and shared.rawdata
834 if rawdata then
835 resources = rawdata.resources
836 else
837
838 resources = tfmdata.resources
839 if resources then
840
841 rawdata = { resources = resources }
842 end
843 end
844
845 if resources then
846 local sequences = resources.sequences
847 if sequences then
848 local properties = tfmdata.properties
849 local validlookups, lookuplist = otf.collectlookups(rawdata,kind,script or properties.script,language or properties.language)
850 if validlookups then
851
852 for i=1,#lookuplist do
853 local lookup = lookuplist[i]
854 local steps = lookup.steps
855 local nofsteps = lookup.nofsteps
856 for i=1,nofsteps do
857 local coverage = steps[i].coverage
858 if coverage then
859 local found = coverage[k]
860 if found then
861 return found, lookup.type
862 end
863 end
864 end
865 end
866 end
867 end
868 end
869end
870
871otf.getgsub = getgsub
872
873function otf.getsubstitution(tfmdata,k,kind,value,script,language)
874 local found, kind = getgsub(tfmdata,k,kind,value,script,language)
875 if not found then
876
877 elseif kind == "gsub_single" then
878 return found
879 elseif kind == "gsub_alternate" then
880 local choice = tonumber(value) or 1
881 return found[choice] or found[1] or k
882 end
883 return k
884end
885
886otf.getalternate = otf.getsubstitution
887
888function otf.getmultiple(tfmdata,k,kind,value,script,language)
889 local found, kind = getgsub(tfmdata,k,kind,value,script,language)
890 if found and kind == "gsub_multiple" then
891 return found
892 end
893 return { k }
894end
895
896function otf.getkern(tfmdata,left,right,kind,value,script,language)
897 local kerns = getgsub(tfmdata,left,kind or "kern",true,script,language)
898 if kerns then
899 local found = kerns[right]
900 local kind = type(found)
901 if kind == "table" then
902 found = found[1][3]
903 elseif kind ~= "number" then
904 found = false
905 end
906 if found then
907 return found * tfmdata.parameters.factor
908 end
909 end
910 return 0
911end
912
913local function check_otf(forced,specification,suffix)
914 local name = specification.name
915 if forced then
916 name = specification.forcedname
917 end
918 local fullname = findbinfile(name,suffix) or ""
919 if fullname == "" then
920 fullname = fonts.names.getfilename(name,suffix) or ""
921 end
922 if fullname ~= "" and not fonts.names.ignoredfile(fullname) then
923 specification.filename = fullname
924 return read_from_otf(specification)
925 end
926end
927
928local function opentypereader(specification,suffix)
929 local forced = specification.forced or ""
930 if formats[forced] then
931 return check_otf(true,specification,forced)
932 else
933 return check_otf(false,specification,suffix)
934 end
935end
936
937readers.opentype = opentypereader
938
939function readers.otf(specification) return opentypereader(specification,"otf") end
940function readers.ttf(specification) return opentypereader(specification,"ttf") end
941function readers.ttc(specification) return opentypereader(specification,"ttf") end
942
943function readers.woff(specification)
944 checkconversion(specification)
945 opentypereader(specification,"")
946end
947
948
949
950function otf.scriptandlanguage(tfmdata,attr)
951 local properties = tfmdata.properties
952 return properties.script or "dflt", properties.language or "dflt"
953end
954
955
956
957local function justset(coverage,unicode,replacement)
958 coverage[unicode] = replacement
959end
960
961otf.coverup = {
962 stepkey = "steps",
963 actions = {
964 chainsubstitution = justset,
965 chainposition = justset,
966 substitution = justset,
967 alternate = justset,
968 multiple = justset,
969 kern = justset,
970 pair = justset,
971 single = justset,
972 ligature = function(coverage,unicode,ligature)
973 local first = ligature[1]
974 local tree = coverage[first]
975 if not tree then
976 tree = { }
977 coverage[first] = tree
978 end
979 for i=2,#ligature do
980 local l = ligature[i]
981 local t = tree[l]
982 if not t then
983 t = { }
984 tree[l] = t
985 end
986 tree = t
987 end
988 tree.ligature = unicode
989 end,
990 },
991 register = function(coverage,featuretype,format)
992 return {
993 format = format,
994 coverage = coverage,
995 }
996 end
997}
998 |