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