Skip to content

Instantly share code, notes, and snippets.

@aminophen
Forked from zr-tex8r/zrlistttc.lua
Last active September 1, 2019 13:45
Show Gist options
  • Save aminophen/4758840f8c2c45d2b4708de0e16e2a41 to your computer and use it in GitHub Desktop.
Save aminophen/4758840f8c2c45d2b4708de0e16e2a41 to your computer and use it in GitHub Desktop.
Lua: To list the font names (and other information) of all the fonts in a TTC/OTC file
-- zrlistttc.lua
-- ref. zrdecompttc.lua
prog_name = 'zrlistttc'
version = '0.4'
mod_date = '2019/08/31'
----------------------------------------
verbose = false
ttc_index = nil
content = { 6 }
langid = nil
ttc_file = nil
----------------------------------------
do
local reader_meta = {
__tostring = function(self)
return "reader("..self.name..")"
end;
__index = {
cdata = function(self, ofs, len)
return make_cdata(self:read(ofs, len))
end;
read = function(self, ofs, len)
self.file:seek("set", ofs)
local data = self.file:read(len)
sure(data:len() == len, 1)
return data
end;
close = function(self)
self.file:close()
end;
}
}
function make_reader(fname)
local file = io.open(fname, "rb")
sure(file, "cannot open for input", fname)
return setmetatable({
name = fname, file = file
}, reader_meta)
end
end
----------------------------------------
do
local cdata_meta = {
__tostring = function(self)
return "cdata(pos="..self._pos..")"
end;
__index = {
pos = function(self, p)
if not p then return self._pos end
self._pos = p
return self
end;
_unum = function(self, b)
local v, data = 0, self.data
sure(#data >= self._pos + b, 11)
for i = 1, b do
self._pos = self._pos + 1
v = v * 256 + data:byte(self._pos)
end
return v
end;
_setunum = function(self, b, v)
local t, data = {}, self.data
t[1] = data:sub(1, self._pos)
self._pos = self._pos + b
sure(#data >= self._pos, 12)
t[b + 2] = data:sub(self._pos + 1)
for i = 1, b do
t[b + 2 - i] = string.char(v % 256)
v = math.floor(v / 256)
end
self.data = table.concat(t, '')
return self
end;
str = function(self, b)
local data = self.data
self._pos = self._pos + b
sure(#data >= self._pos, 13)
return data:sub(self._pos - b + 1, self._pos)
end;
setstr = function(self, s)
local t, data = {}, self.data
t[1], t[2] = data:sub(1, self._pos), s
self._pos = self._pos + #s
sure(#data >= self._pos, 14)
t[3] = data:sub(self._pos + 1)
self.data = table.concat(t, '')
return self
end;
ushort = function(self)
return self:_unum(2)
end;
ulong = function(self)
return self:_unum(4)
end;
setulong = function(self, v)
return self:_setunum(4, v)
end;
ulongs = function(self, num)
local t = {}
for i = 1, num do
t[i] = self:_unum(4)
end
return t
end;
}
}
function make_cdata(data)
return setmetatable({
data = data, _pos = 0
}, cdata_meta)
end
end
----------------------------------------
do
local floor, ceil = math.floor, math.ceil
local function div(x, y)
return floor(x / y), x % y
end
local function utf16betoutf8(src)
local s, d = { tostring(src):byte(1, -1) }, {}
for i = 1, #s - 1, 2 do
local c = s[i] * 256 + s[i+1]
if c < 0x80 then d[#d+1] = c
elseif c < 0x800 then
local x, y = div(c, 0x40)
d[#d+1] = x + 0xC0; d[#d+1] = y + 0x80
elseif c < 0x10000 then
local x, y, z = div(c, 0x1000); y, z = div(y, 0x40)
d[#d+1] = x + 0xE0; d[#d+1] = y + 0x80; d[#d+1] = z + 0x80
else sure(nil)
end
end
return string.char(unpack(d))
end
local file_type = {
[0x74746366] = 'ttc'; [0x10000] = 'ttf'; [0x4F54544F] = 'otf';
[0x74727565] = 'ttf'
}
function otf_offset(reader)
local cd = reader:cdata(0, 12)
local tag = cd:ulong()
local ftype = file_type[tag]; info("type", ftype)
if ftype == 'ttc' then
local ver = cd:ulong(); info("version", ver)
local num = cd:ulong(); info("#fonts", num)
cd = reader:cdata(12, 4 * num)
local res = cd:ulongs(num); info("offset", stt(res))
return res
elseif ftype == 'otf' or ftype == 'ttf' then
return { 0 }
else sure(nil, "unknown file tag", tag)
end
end
local function otf_name_table(reader, fofs, ntbl)
local cd_d = reader:cdata(fofs + 12, 16 * ntbl)
for i = 1, ntbl do
local t = stt({-- tag, csum, ofs, len
cd_d:str(4), cd_d:ulong(), cd_d:ulong(), cd_d:ulong()
})
if t[1] == 'name' then
info("name table index", i)
return reader:cdata(t[3], ceil(t[4] / 4) * 4)
end
end
sure(nil, "name table is missing")
end
local function otf_name_records(cdata)
local nfmt, nnum, nofs = cdata:ushort(), cdata:ushort(), cdata:ushort()
sure(nfmt == 0, "unsupported name table format", nfmt)
local nr = stt({})
for i = 1, nnum do
nr[i] = stt({ -- pid, eid, langid, nameid, len, ofs
cdata:ushort(), cdata:ushort(), cdata:ushort(),
cdata:ushort(), cdata:ushort(), cdata:ushort() + nofs
})
end
return nr
end
function otf_name(cdata, nr, nameid)
local function seek(pid, eid, lid)
for i = 1, #nr do
local t = nr[i]
local ok = (t[4] == nameid and t[1] == pid and t[2] == eid and
t[3] == lid)
if ok then return t end
end
end
local rec
if langid then
rec = seek(unpack(langid))
else
rec = seek(3, 1, 0x409) or seek(3, 10, 0x409) or
seek(1, 0, 0) or seek(0, 3, 0) or
seek(0, 4, 0) or seek(0, 6, 0)
end
info("name record", rec or 'none')
if not rec then return '' end
local s = cdata:pos(rec[6]):str(rec[5])
return (rec[1] == 3) and utf16betoutf8(s) or s
end
function otf_list(reader, fid, fofs)
local cd_fh = reader:cdata(fofs, 12)
local tag = cd_fh:ulong(); info("tag", tag)
local ntbl = cd_fh:ushort(); info("#tables", ntbl)
local cd_n = otf_name_table(reader, fofs, ntbl)
local ext = { id = fid; type = file_type[tag] or '' }
local nr, val = otf_name_records(cd_n), stt({})
info("font", otf_name(cd_n, nr, 6))
for i = 1, #content do
local key = content[i]
val[i] = (type(key) == 'string') and ext[key] or
otf_name(cd_n, nr, key)
end
io.stdout:write(concat(val, ",").."\n")
end
end
----------------------------------------
do
unpack = unpack or table.unpack
local stt_meta = {
__tostring = function(self)
return "{"..concat(self, ",").."}"
end
}
function stt(tbl)
return setmetatable(tbl, stt_meta)
end
function concat(tbl, ...)
local t = {}
for i = 1, #tbl do t[i] = tostring(tbl[i]) end
return table.concat(t, ...)
end
function info(...)
if not verbose then return end
local t = { prog_name, ... }
io.stderr:write(concat(t, ": ").."\n")
end
function abort(...)
verbose = true; info(...)
os.exit(-1)
end
function sure(val, a1, ...)
if val then return val end
if type(a1) == "number" then
a1 = "error("..a1..")"
end
abort(a1, ...)
end
end
----------------------------------------
do
local function show_usage()
io.stdout:write(([[
This is %s v%s <%s> by 'ZR'
Usage: %s[.lua] [-v] [-c <spec>] <ttc_file>
-v be verbose
-i show only one font with a specified index
-c content specification; comma-separated list of items,
where an item is either 'id', 'type', or an name-ID
]]):format(prog_name, version, mod_date, prog_name))
os.exit(0)
end
local function langid_spec(str)
local p, e, l = str:match('^(%d+),(%d+),(%d+)$')
sure(p, "invalid langid spec", str)
return { tonumber(p), tonumber(e), tonumber(l) }
end
local function content_spec(str)
local t, repo = {}, {
copyright = 0; family = 1; subfamily = 2; fullname = 4;
version = 5; psname = 6; url = 11; license = 13;
tfamily = 16; tsubfamily = 17;
id = -1; type = -1;
}
for k in str:gmatch('[^,]+') do
local v = (k:match('^%d+$')) and tonumber(k) or repo[k]
sure(v, "unknown content key", k)
t[#t+1] = (v < 0) and k or v
end
return t
end
local function ttc_index_spec(str)
local p = str:match('^(%d+)$')
sure(p, "invalid ttc_index spec", str)
return tonumber(p)
end
function read_option()
if #arg == 0 then show_usage() end
local idx = 1
while idx <= #arg do
local opt = arg[idx]
if opt:sub(1, 1) ~= '-' then break end
if opt == '-h' or opt == '--help' then
show_usage()
elseif opt == '-v' then
verbose = true
elseif opt == '-i' then
idx = idx + 1; sure(arg[idx], "ttc_index spec is missing")
ttc_index = ttc_index_spec(arg[idx])
elseif opt == '-c' then
idx = idx + 1; sure(arg[idx], "content spec is missing")
content = stt(content_spec(arg[idx]))
elseif opt == '-l' then
idx = idx + 1; sure(arg[idx], "langid spec is missing")
langid = stt(langid_spec(arg[idx]))
else abort("invalid option", opt)
end
idx = idx + 1
end
sure(#arg == idx, "wrong number of arguments")
ttc_file = arg[idx]
end
function main()
read_option()
local reader = make_reader(ttc_file)
local tofs = otf_offset(reader)
if ttc_index then
if ttc_index < 0 or ttc_index > #tofs - 1 then
abort("non-existing ttc_index", ttc_index)
end
otf_list(reader, ttc_index, tofs[ttc_index + 1])
else
for i = 1, #tofs do
otf_list(reader, i - 1, tofs[i])
end
end
reader:close()
end
end
----------------------------------------
main()
-- EOF
@aminophen
Copy link
Author

Addition to the original:

  • Option -i can be used to show only one font with specified index in TrueType/OpenType Collection file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment