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