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.119
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
646local function checkmathsize(tfmdata,mathsize)
647 local mathdata = tfmdata.shared.rawdata.metadata.math
648 local mathsize = tonumber(mathsize)
649 if mathdata then
650 local parameters = tfmdata.parameters
651 parameters.scriptpercentage = mathdata.ScriptPercentScaleDown
652 parameters.scriptscriptpercentage = mathdata.ScriptScriptPercentScaleDown
653 parameters.mathsize = mathsize
654 end
655end
656
657registerotffeature {
658 name = "mathsize",
659 description = "apply mathsize specified in the font",
660 initializers = {
661 base = checkmathsize,
662 node = checkmathsize,
663 }
664}
665
666
667
668function otf.collectlookups(rawdata,kind,script,language)
669 if not kind then
670 return
671 end
672 if not script then
673 script = default
674 end
675 if not language then
676 language = default
677 end
678 local lookupcache = rawdata.lookupcache
679 if not lookupcache then
680 lookupcache = { }
681 rawdata.lookupcache = lookupcache
682 end
683 local kindlookup = lookupcache[kind]
684 if not kindlookup then
685 kindlookup = { }
686 lookupcache[kind] = kindlookup
687 end
688 local scriptlookup = kindlookup[script]
689 if not scriptlookup then
690 scriptlookup = { }
691 kindlookup[script] = scriptlookup
692 end
693 local languagelookup = scriptlookup[language]
694 if not languagelookup then
695 local sequences = rawdata.resources.sequences
696 local featuremap = { }
697 local featurelist = { }
698 if sequences then
699 for s=1,#sequences do
700 local sequence = sequences[s]
701 local features = sequence.features
702 if features then
703 features = features[kind]
704 if features then
705
706 features = features[script] or features[wildcard]
707 if features then
708
709 features = features[language] or features[wildcard]
710 if features then
711 if not featuremap[sequence] then
712 featuremap[sequence] = true
713 featurelist[#featurelist+1] = sequence
714 end
715 end
716 end
717 end
718 end
719 end
720 if #featurelist == 0 then
721 featuremap, featurelist = false, false
722 end
723 else
724 featuremap, featurelist = false, false
725 end
726 languagelookup = { featuremap, featurelist }
727 scriptlookup[language] = languagelookup
728 end
729 return unpack(languagelookup)
730end
731
732
733
734local function getgsub(tfmdata,k,kind,value)
735 local shared = tfmdata.shared
736 local rawdata = shared and shared.rawdata
737 if rawdata then
738 local sequences = rawdata.resources.sequences
739 if sequences then
740 local properties = tfmdata.properties
741 local validlookups, lookuplist = otf.collectlookups(rawdata,kind,properties.script,properties.language)
742 if validlookups then
743
744 for i=1,#lookuplist do
745 local lookup = lookuplist[i]
746 local steps = lookup.steps
747 local nofsteps = lookup.nofsteps
748 for i=1,nofsteps do
749 local coverage = steps[i].coverage
750 if coverage then
751 local found = coverage[k]
752 if found then
753 return found, lookup.type
754 end
755 end
756 end
757 end
758 end
759 end
760 end
761end
762
763otf.getgsub = getgsub
764
765function otf.getsubstitution(tfmdata,k,kind,value)
766 local found, kind = getgsub(tfmdata,k,kind,value)
767 if not found then
768
769 elseif kind == "gsub_single" then
770 return found
771 elseif kind == "gsub_alternate" then
772 local choice = tonumber(value) or 1
773 return found[choice] or found[1] or k
774 end
775 return k
776end
777
778otf.getalternate = otf.getsubstitution
779
780function otf.getmultiple(tfmdata,k,kind)
781 local found, kind = getgsub(tfmdata,k,kind)
782 if found and kind == "gsub_multiple" then
783 return found
784 end
785 return { k }
786end
787
788function otf.getkern(tfmdata,left,right,kind)
789 local kerns = getgsub(tfmdata,left,kind or "kern",true)
790 if kerns then
791 local found = kerns[right]
792 local kind = type(found)
793 if kind == "table" then
794 found = found[1][3]
795 elseif kind ~= "number" then
796 found = false
797 end
798 if found then
799 return found * tfmdata.parameters.factor
800 end
801 end
802 return 0
803end
804
805local function check_otf(forced,specification,suffix)
806 local name = specification.name
807 if forced then
808 name = specification.forcedname
809 end
810 local fullname = findbinfile(name,suffix) or ""
811 if fullname == "" then
812 fullname = fonts.names.getfilename(name,suffix) or ""
813 end
814 if fullname ~= "" and not fonts.names.ignoredfile(fullname) then
815 specification.filename = fullname
816 return read_from_otf(specification)
817 end
818end
819
820local function opentypereader(specification,suffix)
821 local forced = specification.forced or ""
822 if formats[forced] then
823 return check_otf(true,specification,forced)
824 else
825 return check_otf(false,specification,suffix)
826 end
827end
828
829readers.opentype = opentypereader
830
831function readers.otf(specification) return opentypereader(specification,"otf") end
832function readers.ttf(specification) return opentypereader(specification,"ttf") end
833function readers.ttc(specification) return opentypereader(specification,"ttf") end
834
835function readers.woff(specification)
836 checkconversion(specification)
837 opentypereader(specification,"")
838end
839
840
841
842function otf.scriptandlanguage(tfmdata,attr)
843 local properties = tfmdata.properties
844 return properties.script or "dflt", properties.language or "dflt"
845end
846
847
848
849local function justset(coverage,unicode,replacement)
850 coverage[unicode] = replacement
851end
852
853otf.coverup = {
854 stepkey = "steps",
855 actions = {
856 chainsubstitution = justset,
857 chainposition = justset,
858 substitution = justset,
859 alternate = justset,
860 multiple = justset,
861 kern = justset,
862 pair = justset,
863 single = justset,
864 ligature = function(coverage,unicode,ligature)
865 local first = ligature[1]
866 local tree = coverage[first]
867 if not tree then
868 tree = { }
869 coverage[first] = tree
870 end
871 for i=2,#ligature do
872 local l = ligature[i]
873 local t = tree[l]
874 if not t then
875 t = { }
876 tree[l] = t
877 end
878 tree = t
879 end
880 tree.ligature = unicode
881 end,
882 },
883 register = function(coverage,featuretype,format)
884 return {
885 format = format,
886 coverage = coverage,
887 }
888 end
889}
890 |