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 = "*"
88local default = "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
314local function copytotfm(data,cache_id,wipemath)
315 if data then
316 local metadata = data.metadata
317 local properties = derivetable(data.properties)
318 local descriptions = derivetable(data.descriptions)
319 local goodies = derivetable(data.goodies)
320 local characters = { }
321 local parameters = { }
322 local mathparameters = { }
323
324 local resources = data.resources
325 local unicodes = resources.unicodes
326 local spaceunits = 500
327 local spacer = "space"
328 local designsize = metadata.designsize or 100
329 local minsize = metadata.minsize or designsize
330 local maxsize = metadata.maxsize or designsize
331 local mathspecs = metadata.math
332
333 if designsize == 0 then
334 designsize = 100
335 minsize = 100
336 maxsize = 100
337 end
338
339 for unicode in next, data.descriptions do
340 characters[unicode] = { }
341 end
342
343
344
345
346 local filename = constructors.checkedfilename(resources)
347 local fontname = metadata.fontname
348 local fullname = metadata.fullname or fontname
349 local psname = fontname or fullname
350 local subfont = metadata.subfontindex
351 local units = metadata.units or 1000
352
353 if units == 0 then
354 units = 1000
355 metadata.units = 1000
356 report_otf("changing %a units to %a",0,units)
357 end
358
359 if not mathspecs then
360
361 elseif wipemath then
362
363
364
365 local wiped = false
366 local features = resources.features
367 local sequences = resources.sequences
368 if features then
369 local gsub = features.gsub
370 if gsub and gsub.ssty then
371 gsub.ssty = nil
372 wiped = true
373 end
374 end
375 if sequences then
376 for i=#sequences,1,-1 do
377 local sequence = sequences[i]
378 local features = sequence.features
379 if features then
380 if features.ssty then
381 remove(sequences,i)
382 wiped = true
383 end
384 end
385 end
386 end
387 if resources.mathconstants then
388 resources.mathconstants = nil
389 wiped = true
390 end
391 if resources.math then
392 metadata.math = nil
393 wiped = true
394 end
395 if wiped then
396 report_otf("math data wiped from %a",fullname)
397 end
398 else
399 for name, value in next, mathspecs do
400 mathparameters[name] = value
401 end
402 for unicode, character in next, characters do
403 local d = descriptions[unicode]
404 local m = d.math
405 if m then
406
407
408 local italic = m.italic
409 local vitalic = m.vitalic
410
411 local variants = m.hvariants
412 local parts = m.hparts
413 if variants then
414 local c = character
415 for i=1,#variants do
416
417 local un = variants[i]
418 c.next = un
419 c = characters[un]
420 end
421 c.horiz_variants = parts
422 elseif parts then
423 character.horiz_variants = parts
424 italic = m.hitalic
425 end
426
427 local variants = m.vvariants
428 local parts = m.vparts
429 if variants then
430 local c = character
431 for i=1,#variants do
432
433 local un = variants[i]
434 c.next = un
435 c = characters[un]
436 end
437 c.vert_variants = parts
438 elseif parts then
439 character.vert_variants = parts
440 end
441
442 if italic and italic ~= 0 then
443 character.italic = italic
444 end
445
446 if vitalic and vitalic ~= 0 then
447 character.vert_italic = vitalic
448 end
449
450 local accent = m.accent
451 if accent then
452 character.accent = accent
453 end
454
455 local kerns = m.kerns
456 if kerns then
457 character.mathkerns = kerns
458 end
459 end
460 end
461 end
462
463 local monospaced = metadata.monospaced
464 local charwidth = metadata.averagewidth
465 local charxheight = metadata.xheight
466 local italicangle = metadata.italicangle
467 local hasitalics = metadata.hasitalics
468 properties.monospaced = monospaced
469 properties.hasitalics = hasitalics
470 parameters.italicangle = italicangle
471 parameters.charwidth = charwidth
472 parameters.charxheight = charxheight
473
474 local space = 0x0020
475 local emdash = 0x2014
476 if monospaced then
477 if descriptions[space] then
478 spaceunits, spacer = descriptions[space].width, "space"
479 end
480 if not spaceunits and descriptions[emdash] then
481 spaceunits, spacer = descriptions[emdash].width, "emdash"
482 end
483 if not spaceunits and charwidth then
484 spaceunits, spacer = charwidth, "charwidth"
485 end
486 else
487 if descriptions[space] then
488 spaceunits, spacer = descriptions[space].width, "space"
489 end
490 if not spaceunits and descriptions[emdash] then
491 spaceunits, spacer = descriptions[emdash].width/2, "emdash/2"
492 end
493 if not spaceunits and charwidth then
494 spaceunits, spacer = charwidth, "charwidth"
495 end
496 end
497 spaceunits = tonumber(spaceunits) or units/2
498
499 parameters.slant = 0
500 parameters.space = spaceunits
501 parameters.space_stretch = 1*units/2
502 parameters.space_shrink = 1*units/3
503 parameters.x_height = 2*units/5
504 parameters.quad = units
505 if spaceunits < 2*units/5 then
506
507 end
508 if italicangle and italicangle ~= 0 then
509 parameters.italicangle = italicangle
510 parameters.italicfactor = math.cos(math.rad(90+italicangle))
511 parameters.slant = - math.tan(italicangle*math.pi/180)
512 end
513 if monospaced then
514 parameters.space_stretch = 0
515 parameters.space_shrink = 0
516 elseif syncspace then
517 parameters.space_stretch = spaceunits/2
518 parameters.space_shrink = spaceunits/3
519 end
520 parameters.extra_space = parameters.space_shrink
521 if charxheight then
522 parameters.x_height = charxheight
523 else
524 local x = 0x0078
525 if x then
526 local x = descriptions[x]
527 if x then
528 parameters.x_height = x.height
529 end
530 end
531 end
532
533 parameters.designsize = (designsize/10)*65536
534 parameters.minsize = (minsize /10)*65536
535 parameters.maxsize = (maxsize /10)*65536
536 parameters.ascender = abs(metadata.ascender or 0)
537 parameters.descender = abs(metadata.descender or 0)
538 parameters.units = units
539 parameters.vheight = metadata.defaultvheight
540
541 properties.space = spacer
542 properties.format = data.format or formats.otf
543 properties.filename = filename
544 properties.fontname = fontname
545 properties.fullname = fullname
546 properties.psname = psname
547 properties.name = filename or fullname
548 properties.subfont = subfont
549
550if not CONTEXTLMTXMODE or CONTEXTLMTXMODE == 0 then
551
552 properties.encodingbytes = 2
553elseif CONTEXTLMTXMODE then
554 local duplicates = resources and resources.duplicates
555 if duplicates then
556 local maxindex = data.nofglyphs or metadata.nofglyphs
557 if maxindex then
558 for u, d in sortedhash(duplicates) do
559 local du = descriptions[u]
560 if du then
561 for uu in sortedhash(d) do
562 maxindex = maxindex + 1
563 descriptions[uu].dupindex = du.index
564 descriptions[uu].index = maxindex
565 end
566 else
567
568 end
569 end
570 end
571 end
572
573end
574
575
576
577
578 properties.private = properties.private or data.private or privateoffset
579
580 return {
581 characters = characters,
582 descriptions = descriptions,
583 parameters = parameters,
584 mathparameters = mathparameters,
585 resources = resources,
586 properties = properties,
587 goodies = goodies,
588 }
589 end
590end
591
592
593
594
595
596
597
598local converters = {
599 woff = {
600 cachename = "webfonts",
601 action = otf.readers.woff2otf,
602 }
603}
604
605
606
607
608local function checkconversion(specification)
609 local filename = specification.filename
610 local converter = converters[lower(file.suffix(filename))]
611 if converter then
612 local base = file.basename(filename)
613 local name = file.removesuffix(base)
614 local attr = lfs.attributes(filename)
615 local size = attr and attr.size or 0
616 local time = attr and attr.modification or 0
617 if size > 0 then
618 local cleanname = containers.cleanname(name)
619 local cachename = caches.setfirstwritablefile(cleanname,converter.cachename)
620 if not io.exists(cachename) or (time ~= lfs.attributes(cachename).modification) then
621 report_otf("caching font %a in %a",filename,cachename)
622 converter.action(filename,cachename)
623 lfs.touch(cachename,time,time)
624 end
625 specification.filename = cachename
626 end
627 end
628end
629
630local function otftotfm(specification)
631 local cache_id = specification.hash
632 local tfmdata = containers.read(constructors.cache,cache_id)
633 if not tfmdata then
634 checkconversion(specification)
635 local name = specification.name
636 local sub = specification.sub
637 local subindex = specification.subindex
638 local filename = specification.filename
639 local features = specification.features.normal
640 local instance = specification.instance or (features and features.axis)
641 local rawdata = otf.load(filename,sub,instance)
642 if rawdata and next(rawdata) then
643 local descriptions = rawdata.descriptions
644 rawdata.lookuphash = { }
645 tfmdata = copytotfm(rawdata,cache_id,features and features.wipemath)
646 if tfmdata and next(tfmdata) then
647
648 local features = constructors.checkedfeatures("otf",features)
649 local shared = tfmdata.shared
650 if not shared then
651 shared = { }
652 tfmdata.shared = shared
653 end
654 shared.rawdata = rawdata
655
656 shared.dynamics = { }
657
658 tfmdata.changed = { }
659 shared.features = features
660 shared.processes = otf.setfeatures(tfmdata,features)
661 end
662 end
663 containers.write(constructors.cache,cache_id,tfmdata)
664 end
665 return tfmdata
666end
667
668local function read_from_otf(specification)
669 local tfmdata = otftotfm(specification)
670 if tfmdata then
671
672 tfmdata.properties.name = specification.name
673 tfmdata.properties.sub = specification.sub
674 tfmdata.properties.id = specification.id
675
676 tfmdata = constructors.scale(tfmdata,specification)
677 local allfeatures = tfmdata.shared.features or specification.features.normal
678 constructors.applymanipulators("otf",tfmdata,allfeatures,trace_features,report_otf)
679 constructors.setname(tfmdata,specification)
680 fonts.loggers.register(tfmdata,file.suffix(specification.filename),specification)
681 end
682 return tfmdata
683end
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715function otf.collectlookups(rawdata,kind,script,language)
716 if not kind then
717 return
718 end
719 if not script then
720 script = default
721 end
722 if not language then
723 language = default
724 end
725 local lookupcache = rawdata.lookupcache
726 if not lookupcache then
727 lookupcache = { }
728 rawdata.lookupcache = lookupcache
729 end
730 local kindlookup = lookupcache[kind]
731 if not kindlookup then
732 kindlookup = { }
733 lookupcache[kind] = kindlookup
734 end
735 local scriptlookup = kindlookup[script]
736 if not scriptlookup then
737 scriptlookup = { }
738 kindlookup[script] = scriptlookup
739 end
740 local languagelookup = scriptlookup[language]
741 if not languagelookup then
742 local sequences = rawdata.resources.sequences
743 local featuremap = { }
744 local featurelist = { }
745 if sequences then
746 for s=1,#sequences do
747 local sequence = sequences[s]
748 local features = sequence.features
749 if features then
750 features = features[kind]
751 if features then
752
753 features = features[script] or features[wildcard]
754 if features then
755
756 features = features[language] or features[wildcard]
757 if features then
758 if not featuremap[sequence] then
759 featuremap[sequence] = true
760 featurelist[#featurelist+1] = sequence
761 end
762 end
763 end
764 end
765 end
766 end
767 if #featurelist == 0 then
768 featuremap, featurelist = false, false
769 end
770 else
771 featuremap, featurelist = false, false
772 end
773 languagelookup = { featuremap, featurelist }
774 scriptlookup[language] = languagelookup
775 end
776 return unpack(languagelookup)
777end
778
779
780
781local function getgsub(tfmdata,k,kind,value)
782 local shared = tfmdata.shared
783 local rawdata = shared and shared.rawdata
784 if rawdata then
785 local sequences = rawdata.resources.sequences
786 if sequences then
787 local properties = tfmdata.properties
788 local validlookups, lookuplist = otf.collectlookups(rawdata,kind,properties.script,properties.language)
789 if validlookups then
790
791 for i=1,#lookuplist do
792 local lookup = lookuplist[i]
793 local steps = lookup.steps
794 local nofsteps = lookup.nofsteps
795 for i=1,nofsteps do
796 local coverage = steps[i].coverage
797 if coverage then
798 local found = coverage[k]
799 if found then
800 return found, lookup.type
801 end
802 end
803 end
804 end
805 end
806 end
807 end
808end
809
810otf.getgsub = getgsub
811
812function otf.getsubstitution(tfmdata,k,kind,value)
813 local found, kind = getgsub(tfmdata,k,kind,value)
814 if not found then
815
816 elseif kind == "gsub_single" then
817 return found
818 elseif kind == "gsub_alternate" then
819 local choice = tonumber(value) or 1
820 return found[choice] or found[1] or k
821 end
822 return k
823end
824
825otf.getalternate = otf.getsubstitution
826
827function otf.getmultiple(tfmdata,k,kind)
828 local found, kind = getgsub(tfmdata,k,kind)
829 if found and kind == "gsub_multiple" then
830 return found
831 end
832 return { k }
833end
834
835function otf.getkern(tfmdata,left,right,kind)
836 local kerns = getgsub(tfmdata,left,kind or "kern",true)
837 if kerns then
838 local found = kerns[right]
839 local kind = type(found)
840 if kind == "table" then
841 found = found[1][3]
842 elseif kind ~= "number" then
843 found = false
844 end
845 if found then
846 return found * tfmdata.parameters.factor
847 end
848 end
849 return 0
850end
851
852local function check_otf(forced,specification,suffix)
853 local name = specification.name
854 if forced then
855 name = specification.forcedname
856 end
857 local fullname = findbinfile(name,suffix) or ""
858 if fullname == "" then
859 fullname = fonts.names.getfilename(name,suffix) or ""
860 end
861 if fullname ~= "" and not fonts.names.ignoredfile(fullname) then
862 specification.filename = fullname
863 return read_from_otf(specification)
864 end
865end
866
867local function opentypereader(specification,suffix)
868 local forced = specification.forced or ""
869 if formats[forced] then
870 return check_otf(true,specification,forced)
871 else
872 return check_otf(false,specification,suffix)
873 end
874end
875
876readers.opentype = opentypereader
877
878function readers.otf(specification) return opentypereader(specification,"otf") end
879function readers.ttf(specification) return opentypereader(specification,"ttf") end
880function readers.ttc(specification) return opentypereader(specification,"ttf") end
881
882function readers.woff(specification)
883 checkconversion(specification)
884 opentypereader(specification,"")
885end
886
887
888
889function otf.scriptandlanguage(tfmdata,attr)
890 local properties = tfmdata.properties
891 return properties.script or "dflt", properties.language or "dflt"
892end
893
894
895
896local function justset(coverage,unicode,replacement)
897 coverage[unicode] = replacement
898end
899
900otf.coverup = {
901 stepkey = "steps",
902 actions = {
903 chainsubstitution = justset,
904 chainposition = justset,
905 substitution = justset,
906 alternate = justset,
907 multiple = justset,
908 kern = justset,
909 pair = justset,
910 single = justset,
911 ligature = function(coverage,unicode,ligature)
912 local first = ligature[1]
913 local tree = coverage[first]
914 if not tree then
915 tree = { }
916 coverage[first] = tree
917 end
918 for i=2,#ligature do
919 local l = ligature[i]
920 local t = tree[l]
921 if not t then
922 t = { }
923 tree[l] = t
924 end
925 tree = t
926 end
927 tree.ligature = unicode
928 end,
929 },
930 register = function(coverage,featuretype,format)
931 return {
932 format = format,
933 coverage = coverage,
934 }
935 end
936}
937 |