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