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
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
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")
144loadmodule("font-oup.lua")
145
146loadmodule("font-otl.lua")
147loadmodule("font-onr.lua")
148
149
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")
205
206 fonts.names.filters.list = simplelist
207 fonts.names.version = simpleversion
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
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
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), " "))
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
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")
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
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
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
452 report("not supported: --list --spec --pattern",name)
453 elseif filter then
454
455 list_specifications(fonts.names.getlookups(filter),info)
456 elseif given then
457
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
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
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
476 list_matches(fonts.names.list(string.topattern(pattern,true),reload,all),info)
477 elseif given then
478
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
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
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
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)
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()
612 local name = givenfiles[1] or ""
613 local sub = givenfiles[2] or ""
614 if name and name ~= "" then
615 local filename = resolvers.findfile(name)
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 |