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