-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Addition to the original:
-i
can be used to show only one font with specified index in TrueType/OpenType Collection file.