mtx-fonts.lua /size: 24 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['mtx-fonts'] = {
2    version   = 1.001,
3    comment   = "companion to mtxrun.lua",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9local getargument = environment.getargument
10local setargument = environment.setargument
11local givenfiles  = environment.files
12
13local suffix, addsuffix, removesuffix, replacesuffix = file.suffix, file.addsuffix, file.removesuffix, file.replacesuffix
14local nameonly, basename, joinpath, collapsepath = file.nameonly, file.basename, file.join, file.collapsepath
15local lower, gsub = string.lower, string.gsub
16local concat = table.concat
17local write_nl = (logs and logs.writer) or (texio and texio.write_nl) or print
18
19local versions = {
20    otl = 3.144,
21    one = 1.541,
22    afm = 1.541,
23    pfb = 1.003,
24}
25
26local helpinfo = [[
27<?xml version="1.0"?>
28<application>
29 <metadata>
30  <entry name="name">mtx-fonts</entry>
31  <entry name="detail">ConTeXt Font Database Management</entry>
32  <entry name="version">1.21</entry>
33 </metadata>
34 <flags>
35  <category name="basic">
36   <subcategory>
37    <flag name="convert"><short>save open type font in raw table</short></flag>
38    <flag name="unpack"><short>save a tma file in a more readable format</short></flag>
39   </subcategory>
40   <subcategory>
41    <flag name="reload"><short>generate new font database (use <ref name="force"/> when in doubt)</short></flag>
42    <flag name="reload"><short><ref name="simple"/>:generate luatex-fonts-names.lua (not for context!)</short></flag>
43   </subcategory>
44   <subcategory>
45    <flag name="list"><short><ref name="name"/>: list installed fonts, filter by name [<ref name="pattern"/>]</short></flag>
46    <flag name="list"><short><ref name="spec"/>: list installed fonts, filter by spec [<ref name="filter"/>]</short></flag>
47    <flag name="list"><short><ref name="file"/>: list installed fonts, filter by file [<ref name="pattern"/>]</short></flag>
48   </subcategory>
49   <subcategory>
50    <flag name="pattern" value="str"><short>filter files using pattern</short></flag>
51    <flag name="filter" value="list"><short>key-value pairs</short></flag>
52    <flag name="all"><short>show all found instances (combined with other flags)</short></flag>
53    <flag name="info"><short>give more details</short></flag>
54    <flag name="trackers" value="list"><short>enable trackers</short></flag>
55    <flag name="statistics"><short>some info about the database</short></flag>
56    <flag name="names"><short>use name instead of unicodes</short></flag>
57    <flag name="cache" value="str"><short>use specific cache (otl or otf)</short></flag>
58   </subcategory>
59   <subcategory>
60    <flag name="pattern" value="str"><short>filter files using pattern</short></flag>
61    <flag name="coverage" value="str"><short>character list</short></flag>
62   </subcategory>
63  </category>
64 </flags>
65 <examples>
66  <category>
67   <title>Examples</title>
68   <subcategory>
69    <example><command>mtxrun --script font --list somename (== --pattern=*somename*)</command></example>
70   </subcategory>
71   <subcategory>
72    <example><command>mtxrun --script font --list --file filename</command></example>
73    <example><command>mtxrun --script font --list --name --pattern=*somefile*</command></example>
74   </subcategory>
75   <subcategory>
76    <example><command>mtxrun --script font --list --name somename</command></example>
77    <example><command>mtxrun --script font --list --name --pattern=*somename*</command></example>
78   </subcategory>
79   <subcategory>
80    <example><command>mtxrun --script font --list --spec somename</command></example>
81    <example><command>mtxrun --script font --list --spec somename-bold-italic</command></example>
82    <example><command>mtxrun --script font --list --spec --pattern=*somename*</command></example>
83    <example><command>mtxrun --script font --list --spec --filter="fontname=somename"</command></example>
84    <example><command>mtxrun --script font --list --spec --filter="familyname=somename,weight=bold,style=italic,width=condensed"</command></example>
85    <example><command>mtxrun --script font --list --spec --filter="familyname=crap*,weight=bold,style=italic"</command></example>
86   </subcategory>
87   <subcategory>
88    <example><command>mtxrun --script font --list --all</command></example>
89    <example><command>mtxrun --script font --list --file somename</command></example>
90    <example><command>mtxrun --script font --list --file --all somename</command></example>
91    <example><command>mtxrun --script font --list --file --pattern=*somename*</command></example>
92   </subcategory>
93   <subcategory>
94    <example><command>mtxrun --script font --convert texgyrepagella-regular.otf</command></example>
95    <example><command>mtxrun --script font --convert --names texgyrepagella-regular.otf</command></example>
96   </subcategory>
97   <subcategory>
98    <example><command>mtxrun --script font --coverage="U+123 U+124" --pattern=texgyre*</command></example>
99    <example><command>mtxrun --script font --coverage="✓"</command></example>
100   </subcategory>
101  </category>
102 </examples>
103</application>
104]]
105
106local application = logs.application {
107    name     = "mtx-fonts",
108    banner   = "ConTeXt Font Database Management 1.22",
109    helpinfo = helpinfo,
110}
111
112local report = application.report
113
114-- todo: fc-cache -v en check dirs, or better is: fc-cat -v | grep Directory
115
116if not fontloader then fontloader = fontforge end
117
118local function loadmodule(filename)
119    local fullname = resolvers.findfile(filename,"tex")
120    if fullname and fullname ~= "" then
121        dofile(fullname)
122    end
123end
124
125-- loader code
126
127loadmodule("char-def.lua")
128
129loadmodule("font-ini.lua")
130loadmodule("font-log.lua")
131loadmodule("font-con.lua")
132loadmodule("font-cft.lua")
133loadmodule("font-enc.lua")
134loadmodule("font-agl.lua")
135loadmodule("font-cid.lua")
136loadmodule("font-map.lua")
137loadmodule("font-oti.lua")
138
139loadmodule("font-otr.lua")
140loadmodule("font-cff.lua")
141loadmodule("font-ttf.lua")
142loadmodule("font-tmp.lua")
143loadmodule("font-dsp.lua") -- autosuffix
144loadmodule("font-oup.lua")
145
146loadmodule("font-otl.lua")
147loadmodule("font-onr.lua")
148
149-- extra code
150
151loadmodule("font-syn.lua")
152loadmodule("font-trt.lua")
153loadmodule("font-mis.lua")
154
155scripts       = scripts       or { }
156scripts.fonts = scripts.fonts or { }
157
158function fonts.names.statistics()
159    fonts.names.load()
160
161    local data = fonts.names.data
162    local statistics = data.statistics
163
164    local function counted(t)
165        local n = { }
166        for k, v in next, t do
167            n[k] = table.count(v)
168        end
169        return table.sequenced(n)
170    end
171
172    report("cache uuid      : %s", data.cache_uuid)
173    report("cache version   : %s", data.cache_version)
174    report("number of trees : %s", #data.datastate)
175    report()
176    report("number of fonts : %s", statistics.fonts or 0)
177    report("used files      : %s", statistics.readfiles or 0)
178    report("skipped files   : %s", statistics.skippedfiles or 0)
179    report("duplicate files : %s", statistics.duplicatefiles or 0)
180    report("specifications  : %s", #data.specifications)
181    report("families        : %s", table.count(data.families))
182    report()
183    report("mappings        : %s", counted(data.mappings))
184    report("fallbacks       : %s", counted(data.fallbacks))
185    report()
186    report("used styles     : %s", table.sequenced(statistics.used_styles))
187    report("used variants   : %s", table.sequenced(statistics.used_variants))
188    report("used weights    : %s", table.sequenced(statistics.used_weights))
189    report("used widths     : %s", table.sequenced(statistics.used_widths))
190    report()
191    report("found styles    : %s", table.sequenced(statistics.styles))
192    report("found variants  : %s", table.sequenced(statistics.variants))
193    report("found weights   : %s", table.sequenced(statistics.weights))
194    report("found widths    : %s", table.sequenced(statistics.widths))
195
196end
197
198function fonts.names.simple(alsotypeone)
199    local simpleversion = 1.001
200    local simplelist = { "ttf", "otf", "ttc", alsotypeone and "afm" or nil }
201    local name = "luatex-fonts-names.lua"
202    local path = collapsepath(caches.getwritablepath("..","..","generic","fonts","data"))
203
204    path = gsub(path, "luametatex%-cache", "luatex-cache") -- maybe have an option to force it
205
206    fonts.names.filters.list = simplelist
207    fonts.names.version = simpleversion -- this number is the same as in font-dum.lua
208    report("generating font database for 'luatex-fonts' version %s",fonts.names.version)
209    fonts.names.identify(true)
210    local data = fonts.names.data
211    if data then
212        local simplemappings = { }
213        local simplified = {
214            mappings      = simplemappings,
215            version       = simpleversion,
216            cache_version = simpleversion,
217        }
218        local specifications = data.specifications
219        for i=1,#simplelist do
220            local format = simplelist[i]
221            for tag, index in next, data.mappings[format] do
222                local s = specifications[index]
223                simplemappings[tag] = { s.rawname or nameonly(s.filename), s.filename, s.subfont }
224            end
225        end
226        if environment.arguments.nocache then
227            report("not using cache path %a",path)
228        else
229            dir.mkdirs(path)
230            if lfs.isdir(path) then
231                report("saving names on cache path %a",path)
232                name = joinpath(path,name)
233            else
234                report("invalid cache path %a",path)
235            end
236        end
237        report("saving names in %a",name)
238        io.savedata(name,table.serialize(simplified,true))
239        local data = io.loaddata(resolvers.findfile("luatex-fonts-syn.lua","tex")) or ""
240        local dummy = string.match(data,"fonts%.names%.version%s*=%s*([%d%.]+)")
241        if tonumber(dummy) ~= simpleversion then
242            report("warning: version number %s in 'font-dum' does not match database version number %s",dummy or "?",simpleversion)
243        end
244    elseif lfs.isfile(name) then
245        os.remove(name)
246    end
247end
248
249function scripts.fonts.reload()
250    if getargument("simple") then
251        fonts.names.simple(getargument("typeone"))
252    else
253        fonts.names.load(true,getargument("force"))
254    end
255end
256
257local function fontweight(fw)
258    if fw then
259        return string.format("conflict: %s", fw)
260    else
261        return ""
262    end
263end
264
265local function indeed(f,s)
266    if s and s ~= "" then
267        report(f,s)
268    end
269end
270
271local function showfeatures(tag,specification)
272    report()
273    indeed("mapping   : %s",tag)
274    indeed("fontname  : %s",specification.fontname)
275    indeed("fullname  : %s",specification.fullname)
276    indeed("filename  : %s",specification.filename)
277    indeed("family    : %s",specification.familyname or "<nofamily>")
278 -- indeed("subfamily : %s",specification.subfamilyname or "<nosubfamily>")
279    indeed("weight    : %s",specification.weight or "<noweight>")
280    indeed("style     : %s",specification.style or "<nostyle>")
281    indeed("width     : %s",specification.width or "<nowidth>")
282    indeed("variant   : %s",specification.variant or "<novariant>")
283    indeed("subfont   : %s",specification.subfont or "")
284    indeed("fweight   : %s",fontweight(specification.fontweight))
285    -- maybe more
286    local instancenames = specification.instancenames
287    if instancenames then
288        report()
289        indeed("instances : % t",instancenames)
290    end
291    local features, tables = fonts.helpers.getfeatures(specification.filename,not getargument("nosave"))
292    if features then
293        for what, v in table.sortedhash(features) do
294            local data = features[what]
295            if data and next(data) then
296                report()
297                report("%s features:",what)
298                report()
299                report("  feature  script   languages")
300                report()
301                for f,ff in table.sortedhash(data) do
302                    local done = false
303                    for s, ss in table.sortedhash(ff) do
304                        local s = s == "*" and all or s
305                        if ss["*"] then
306                            ss["*"] = nil
307                            ss.all  = true
308                        end
309                        local name
310                        if done then
311                            name = ""
312                        else
313                            done = true
314                            name = f
315                        end
316                        report("  %-8s %-8s %-8s",name,s,concat(table.sortedkeys(ss), " ")) -- todo: padd 4
317                    end
318                end
319            end
320        end
321    else
322        report("no features")
323    end
324    if tables then
325        tables = table.tohash(tables)
326        local methods = {
327            overlay = (tables.colr or tables.cpal) and { format = "cff/ttf", feature = "color:overlay" } or nil,
328            bitmap  = (tables.cblc or tables.cbdt) and { format = "png",     feature = "color:bitmap"  } or nil,
329            outline = (tables.svg                ) and { format = "svg",     feature = "color:svg"     } or nil,
330        }
331        if next(methods) then
332            report()
333            report("color features:")
334            report()
335            report("  method   feature         formats")
336            report()
337            for k, v in table.sortedhash(methods) do
338                report("  %-8s %-14s  %s",k,v.feature,v.format)
339            end
340        end
341    end
342    report()
343    collectgarbage("collect")
344end
345
346local function reloadbase(reload)
347    if reload then
348        report("fontnames, reloading font database")
349        names.load(true,getargument("force"))
350        report("fontnames, done\n\n")
351    end
352end
353
354local function list_specifications(t,info)
355    if t then
356        local s = table.sortedkeys(t)
357        if info then
358            for k=1,#s do
359                local v = s[k]
360                showfeatures(v,t[v])
361            end
362        else
363            for k=1,#s do
364                local v = s[k]
365                local entry = t[v]
366                s[k] = {
367                    entry.familyname    or "<nofamily>",
368                 -- entry.subfamilyname or "<nosubfamily>",
369                    entry.weight        or "<noweight>",
370                    entry.style         or "<nostyle>",
371                    entry.width         or "<nowidth>",
372                    entry.variant       or "<novariant>",
373                    entry.fontname,
374                    entry.filename,
375                    entry.subfont or "",
376                    fontweight(entry.fontweight),
377                }
378            end
379            local h = {
380                {"familyname","weight","style","width","variant","fontname","filename","subfont","fontweight"},
381                {"","","","","","","","",""}
382            }
383            utilities.formatters.formatcolumns(s,false,h)
384            for k=1,#h do
385                write_nl(h[k])
386            end
387            for k=1,#s do
388                write_nl(s[k])
389            end
390        end
391    end
392end
393
394local function list_matches(t,info)
395    if t then
396        local s, w = table.sortedkeys(t), { 0, 0, 0, 0 }
397        if info then
398            for k=1,#s do
399                local v = s[k]
400                showfeatures(v,t[v])
401                collectgarbage("collect") -- in case we load a lot
402            end
403        else
404            for k=1,#s do
405                local v = s[k]
406                local entry = t[v]
407                s[k] = {
408                    v,
409                    entry.familyname,
410                    entry.fontname,
411                    entry.filename,
412                    tostring(entry.subfont or ""),
413                    concat(entry.instancenames or { }, " "),
414                }
415            end
416            table.insert(s,1,{"identifier","familyname","fontname","filename","subfont","instances"})
417            table.insert(s,2,{"","","","","","",""})
418            utilities.formatters.formatcolumns(s)
419            for k=1,#s do
420                write_nl(s[k])
421            end
422        end
423    end
424end
425
426function scripts.fonts.list()
427
428    local all     = getargument("all")
429    local info    = getargument("info")
430    local reload  = getargument("reload")
431    local pattern = getargument("pattern")
432    local filter  = getargument("filter")
433    local given   = givenfiles[1]
434
435    reloadbase(reload)
436
437    if getargument("name") then
438        if pattern then
439            -- mtxrun --script font --list --name --pattern=*somename*
440            list_matches(fonts.names.list(string.topattern(pattern,true),reload,all),info)
441        elseif filter then
442            report("not supported: --list --name --filter",name)
443        elseif given then
444            -- mtxrun --script font --list --name somename
445            list_matches(fonts.names.list(given,reload,all),info)
446        else
447            report("not supported: --list --name <no specification>",name)
448        end
449    elseif getargument("spec") then
450        if pattern then
451            -- mtxrun --script font --list --spec --pattern=*somename*
452            report("not supported: --list --spec --pattern",name)
453        elseif filter then
454            -- mtxrun --script font --list --spec --filter="fontname=somename"
455            list_specifications(fonts.names.getlookups(filter),info)
456        elseif given then
457            -- mtxrun --script font --list --spec somename
458            list_specifications(fonts.names.collectspec(given,reload,all),info)
459        else
460            report("not supported: --list --spec <no specification>",name)
461        end
462    elseif getargument("file") then
463        if pattern then
464            -- mtxrun --script font --list --file --pattern=*somename*
465            list_specifications(fonts.names.collectfiles(string.topattern(pattern,true),reload,all),info)
466        elseif filter then
467            report("not supported: --list --spec",name)
468        elseif given then
469            -- mtxrun --script font --list --file somename
470            list_specifications(fonts.names.collectfiles(given,reload,all),info)
471        else
472            report("not supported: --list --file <no specification>",name)
473        end
474    elseif pattern then
475        -- mtxrun --script font --list --pattern=*somename*
476        list_matches(fonts.names.list(string.topattern(pattern,true),reload,all),info)
477    elseif given then
478        -- mtxrun --script font --list somename
479        list_matches(fonts.names.list(given,reload,all),info)
480    elseif all then
481        pattern = "*"
482        list_matches(fonts.names.list(string.topattern(pattern,true),reload,all),info)
483    else
484        report("not supported: --list <no specification>",name)
485    end
486
487end
488
489-- For Mikael S:
490
491function scripts.fonts.coverage()
492
493    local coverage = getargument("coverage")
494    local reload   = getargument("reload")
495    local pattern  = getargument("pattern")
496
497    if type(coverage) ~= "string" or coverage == "" then
498        return
499    end
500
501    coverage = string.gsub(coverage,"0x([0-9A-Fa-f]+)",function(s)
502        return utf.char(tonumber(s,16))
503    end)
504
505    coverage = string.gsub(coverage,"U%+([0-9A-F]+)",function(s)
506        return utf.char(tonumber(s,16))
507    end)
508
509    coverage = string.gsub(coverage,"%s+","")
510
511    local chars = table.unique(utf.split(coverage))
512    local bytes = { }
513
514    for i=1,#chars do
515        bytes[#bytes+1] = utf.byte(chars[i])
516    end
517
518    local f_c = string.formatters["%C"]
519
520    local function okay(data)
521        if data then
522            local descriptions = data.descriptions
523            if descriptions then
524                local f = { }
525                local d = false
526                for i=1,#bytes do
527                    local b = bytes[i]
528                    if descriptions[b] then
529                        f[i] = f_c(b)
530                        d = true
531                    else
532                        f[i] = "[" .. f_c(b) .. "]"
533                    end
534                end
535                return d and f or false
536            end
537        end
538    end
539
540    reloadbase(reload)
541
542    local files = fonts.names.list(pattern and string.topattern(pattern,true) or "",reload,true)
543    local found = { }
544    local done  = { }
545
546--     inspect(files)
547
548    if files then
549        for id, specification in next, files do
550            if specification.format == "otf" or specification.format == "ttf" then
551                if not done[specification.filename] then
552                    local fullname = resolvers.findfile(specification.filename) or ""
553                    if fullname ~= "" then
554                        local data = fonts.handlers.otf.load(fullname)
555                        local list = okay(data)
556                        if list then
557                            found[id] = list
558                        end
559                    end
560                    done[specification.filename] = true
561                end
562            end
563    --     collectgarbage("collect")
564        end
565    end
566
567    if next(found) then
568        report()
569        for id, found in table.sortedhash(found) do
570            local specification = files[id]
571            indeed("filename  : %s",specification.filename)
572            indeed("fontname  : %s",specification.fontname)
573            indeed("coverage  : % t",found)
574            report()
575        end
576    end
577end
578
579function scripts.fonts.unpack()
580    local name = removesuffix(basename(givenfiles[1] or ""))
581    if name and name ~= "" then
582        local cacheid   = false
583        local cache     = false
584        local cleanname = false
585        local data      = false
586        local list = { getargument("cache") or false, "otl", "one" }
587        for i=1,#list do
588            cacheid   = list[i]
589            if cacheid then
590                cache     = containers.define("fonts", cacheid, versions[cacheid], true) -- cache is temp
591                cleanname = containers.cleanname(name)
592                data      = containers.read(cache,cleanname)
593                if data then
594                    break
595                end
596            end
597        end
598        if data then
599            local savename = addsuffix(cleanname .. "-unpacked","tma")
600            report("fontsave, saving data in %s",savename)
601            if data.creator == "context mkiv" then
602                fonts.handlers.otf.readers.unpack(data)
603            end
604            io.savedata(savename,table.serialize(data,true))
605        else
606            report("unknown file %a in cache %a",name,cacheid)
607        end
608    end
609end
610
611function scripts.fonts.convert() -- new save
612    local name = givenfiles[1] or ""
613    local sub  = givenfiles[2] or ""
614    if name and name ~= "" then
615        local filename = resolvers.findfile(name) -- maybe also search for opentype
616        if filename and filename ~= "" then
617            local suffix = lower(suffix(filename))
618            if suffix == 'ttf' or suffix == 'otf' or suffix == 'ttc' then
619                local data = fonts.handlers.otf.readers.loadfont(filename,sub)
620                if data then
621                    local nofsubfonts = data and data.properties and data.properties.nofsubfonts or 0
622                    fonts.handlers.otf.readers.compact(data)
623                    fonts.handlers.otf.readers.rehash(data,getargument("names") and "names" or "unicodes")
624                    local savename = replacesuffix(lower(data.metadata.fullname or filename),"lua")
625                    table.save(savename,data)
626                    if nofsubfonts == 0 then
627                        report("font: %a saved as %a",filename,savename)
628                    else
629                        report("font: %a saved as %a, %i subfonts found, provide number if wanted",filename,savename,nofsubfonts)
630                    end
631                else
632                    report("font: %a not loaded",filename)
633                end
634            else
635                report("font: %a not saved",filename)
636            end
637        else
638            report("font: %a not found",name)
639        end
640    else
641        report("font: no name given")
642    end
643end
644
645
646if getargument("names") then
647    setargument("reload",true)
648    setargument("simple",true)
649end
650
651if getargument("list") then
652    scripts.fonts.list()
653elseif getargument("reload") then
654    scripts.fonts.reload()
655elseif getargument("convert") then
656    scripts.fonts.convert()
657elseif getargument("coverage") then
658    scripts.fonts.coverage()
659elseif getargument("unpack") then
660    scripts.fonts.unpack()
661elseif getargument("statistics") then
662    fonts.names.statistics()
663elseif getargument("exporthelp") then
664    application.export(getargument("exporthelp"),givenfiles[1])
665else
666    application.help()
667end
668