Initial commit

This commit is contained in:
Marcel Krüger 2019-07-17 21:14:34 +02:00
commit 3815ab3963
28 changed files with 4239 additions and 0 deletions

36
luametalatex-back-pdf.lua Normal file
View File

@ -0,0 +1,36 @@
local writer = require'luametalatex-nodewriter'
local newpdf = require'luametalatex-pdf'
local pfile = newpdf.open(tex.jobname .. '.pdf')
local fontdirs = setmetatable({}, {__index=function(t, k)t[k] = pfile:getobj() return t[k] end})
local usedglyphs = {}
print(token, token.luacmd)
token.luacmd("shipout", function()
local voff = node.new'kern'
voff.kern = tex.voffset + tex.sp'1in'
voff.next = token.scan_list()
voff.next.shift = tex.hoffset + tex.sp'1in'
local list = node.vpack(voff)
list.height = tex.pageheight
list.width = tex.pagewidth
local out, resources, annots = writer(pfile, list, fontdirs, usedglyphs)
local page, parent = pfile:newpage()
local content = pfile:stream(nil, '', out)
pfile:indirect(page, string.format([[<</Type/Page/Parent %i 0 R/Contents %i 0 R/MediaBox[0 %i %i %i]/Resources%s%s>>]], parent, content, -math.ceil(list.depth/65781.76), math.ceil(list.width/65781.76), math.ceil(list.height/65781.76), resources, annots))
token.put_next(token.create'immediateassignment', token.create'global', token.create'deadcycles', token.create(0x30), token.create'relax')
token.scan_token()
end, 'protected')
callback.register("stop_run", function()
for fid, id in pairs(fontdirs) do
local f = font.getfont(fid)
local psname = f.psname or f.fullname
local sorted = {}
for k,v in pairs(usedglyphs[fid]) do
sorted[#sorted+1] = v
end
table.sort(sorted, function(a,b) return a[1] < b[1] end)
pfile:indirect(id, require'luametalatex-pdf-font'(pfile, f, sorted))
end
pfile.root = pfile:getobj()
pfile:indirect(pfile.root, string.format([[<</Type/Catalog/Version/1.7/Pages %i 0 R>>]], pfile:writepages()))
pfile:close()
end, "Finish PDF file")

View File

@ -0,0 +1,49 @@
\begingroup
\catcode`\^^^^ffff=11
\catcode`\@=11
\toks0{%
local dimen_cmd = token.command_id'assign_dimen'
local tex_params = {}
local texmeta = getmetatable(tex)
local texmetaoldindex = texmeta.__index
local texmetaoldnewindex = texmeta.__newindex
function texmeta.__index(t, k)
local v = tex_params[k]
if v then
if v.command == dimen_cmd then
return tex.dimen[v.index]
end
else
return texmetaoldindex(t, k)
end
end
function texmeta.__newindex(t, k, v)
local p = tex_params[k]
if p then
if p.command == dimen_cmd then
tex.dimen[p.index] = v
end
else
return texmetaoldnewindex(t, k, v)
end
end
}
\def\InternalAlloc#1#2#3#4#5{%
\csname new#3\endcsname#1%
\global#1=#5\relax
\etoksapp0{#2_params["\luaescapestring{#4}"] = token.create"\luaescapestring{\csstring#1}"}
}
\def\internalAlloc#1#2#3{%
\expandafter\InternalAlloc\csname ^^^^fffe#3@#1\endcsname{#1}{#2}{#3}%
}
\def\texAlloc#1#2{%
\expandafter\InternalAlloc\csname #2\endcsname{tex}{#1}{#2}%
}
\texAlloc{dimen}{pageheight}{11in}
\texAlloc{dimen}{pagewidth}{8.5in}
% \internalAlloc{tex}{dimen}{pagewidth}
\directlua{
lua.prepared_code[\csstring#lua.prepared_code+1] = tex.toks[0]
\the\toks0
}
\endgroup

21
luametalatex-bit32.lua Normal file
View File

@ -0,0 +1,21 @@
local mask32 = 0xFFFFFFFF
return {
rshift = function(i, s)
return (mask32 & i) >> s
end,
lshift = function(i, s)
return mask32 & (i << s)
end,
band = function(i, j)
return i & j & mask32
end,
bor = function(i, j)
return (i | j) & mask32
end,
bor = function(i, j)
return (i ^ j) & mask32
end,
extract = function(v, shift, count)
return ((bit32 & v) >> shift) & ((1<<count)-1)
end,
}

179
luametalatex-firstcode.lua Normal file
View File

@ -0,0 +1,179 @@
local functions = lua.getfunctionstable()
-- I am not sure why this is necessary, but otherwise LuaMetaTeX resets
-- the functions table every time the getter is called
function lua.get_functions_table() return functions end
local set_lua = token.set_lua
-- local new_luafunction = luatexbase.new_luafunction
local i=12342
local function new_luafunction(name)
i = i+1
return i
end
function token.luacmd(name, func, ...)
local idx = new_luafunction(name)
set_lua(name, idx, ...)
functions[idx] = func
end
local properties = node.get_properties_table()
-- setmetatable(node.direct.get_properties_table(), {
-- __index = function(t, id)
-- local new = {}
-- t[id] = new
-- return new
-- end
-- })
local whatsit_id = node.id'whatsit'
local whatsits = {
[0] = "open",
"write",
"close",
nil,
nil,
nil,
"save_pos",
"late_lua",
"user_defined",
nil,
nil,
nil,
nil,
nil,
nil,
nil,
"pdf_literal",
"pdf_refobj",
"pdf_annot",
"pdf_start_link",
"pdf_end_link",
"pdf_dest",
"pdf_action",
"pdf_thread",
"pdf_start_thread",
"pdf_end_thread",
"pdf_thread_data",
"pdf_link_data",
"pdf_colorstack",
"pdf_setmatrix",
"pdf_save",
"pdf_restore",
}
whatsits[whatsits[0]] = 0
for i = 0,#whatsits do
local v = whatsits[i]
if v then
whatsits[v] = i
end
end
function node.whatsits() return whatsits end
function node.subtype(s) return type(s) == "string" and whatsits[s] or nil end
local spacer_cmd, relax_cmd = token.command_id'spacer', token.command_id'relax'
local function scan_filename()
local name = {}
local quoted = false
local tok, cmd
repeat
tok = token.scan_token()
cmd = tok.command
until cmd ~= spacer_cmd and cmd ~= relax_cmd
while (tok.command <= 12 and tok.mode <= token.biggest_char()
or (token.put_next(tok) and false))
and (quoted or tok.mode ~= string.byte' ') do
if tok.mode == string.byte'"' then
quoted = not quoted
else
name[#name+1] = tok.mode
end
tok = token.scan_token()
end
return utf8.char(table.unpack(name))
end
local ofiles = {}
local function do_openout(p)
if ofiles[p.file] then
error[[Existing file]]
else
local msg
ofiles[p.file], msg = io.open(p.name, 'w')
if not ofiles[p.file] then
error(msg)
end
end
end
functions[39] = function(_, immediate) -- \openout
local file = token.scan_int()
token.scan_keyword'='
local name = scan_filename()
local props = {file = file, name = name, handle = do_openout}
if immediate == "immediate" then
do_openout(props)
else
local whatsit = node.new(whatsit_id, whatsits.open)
properties[whatsit] = props
node.write(whatsit)
end
end
local function do_closeout(p)
if ofiles[p.file] then
ofiles[p.file]:close()
ofiles[p.file] = nil
end
end
functions[40] = function(_, immediate) -- \closeout
local file = token.scan_int()
local props = {file = file, handle = do_closeout}
if immediate == "immediate" then
do_closeout(props)
else
local whatsit = node.new(whatsit_id, whatsits.close)
properties[whatsit] = props
node.write(whatsit)
end
end
local function do_write(p)
local content = token.to_string(p.data) .. '\n'
local file = ofiles[p.file]
if file then
file:write(content)
else
texio.write_nl(p.file < 0 and "log" or "term and log", content)
end
end
functions[41] = function(_, immediate) -- \write
local file = token.scan_int()
local content = token.scan_tokenlist()
local props = {file = file, data = content, handle = do_write}
if immediate == "immediate" then
do_write(props)
else
local whatsit = node.new(whatsit_id, whatsits.write)
properties[whatsit] = props
node.write(whatsit)
end
end
local lua_call_cmd = token.command_id'lua_call'
functions[42] = function() -- \immediate
local next_tok = token.scan_token()
if next_tok.command ~= lua_call_cmd then
return token.put_next(next_tok)
end
local function_id = next_tok.index
functions[function_id](function_id, 'immediate')
end
functions[43] = function() -- \pdfvariable
local name = token.scan_string()
print('Missing \\pdf' .. name)
end
token.set_lua("openout", 39, "protected")
token.set_lua("closeout", 40, "protected")
token.set_lua("write", 41, "protected")
write_tok = token.create'write'
token.set_lua("immediate", 42, "protected")
token.set_lua("pdfvariable", 43)
local prepared_code = lua.bytecode[1]
if prepared_code then
prepared_code()
prepared_code, lua.bytecode[1] = nil, nil
end
require'luametalatex-back-pdf'

View File

@ -0,0 +1,72 @@
-- Copied from the CFF specification...
local strings = {[".notdef"] = 0, [0] =
".notdef", "space", "exclam", "quotedbl", "numbersign", "dollar", "percent",
"ampersand", "quoteright", "parenleft", "parenright", "asterisk", "plus",
"comma", "hyphen", "period", "slash", "zero", "one", "two", "three", "four",
"five", "six", "seven", "eight", "nine", "colon", "semicolon", "less",
"equal", "greater", "question", "at", "A", "B", "C", "D", "E", "F", "G", "H",
"I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W",
"X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum",
"underscore", "quoteleft", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y",
"z", "braceleft", "bar", "braceright", "asciitilde", "exclamdown", "cent",
"sterling", "fraction", "yen", "florin", "section", "currency",
"quotesingle", "quotedblleft", "guillemotleft", "guilsinglleft",
"guilsinglright", "fi", "fl", "endash", "dagger", "daggerdbl",
"periodcentered", "paragraph", "bullet", "quotesinglbase", "quotedblbase",
"quotedblright", "guillemotright", "ellipsis", "perthousand", "questiondown",
"grave", "acute", "circumflex", "tilde", "macron", "breve", "dotaccent",
"dieresis", "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "emdash",
"AE", "ordfeminine", "Lslash", "Oslash", "OE", "ordmasculine", "ae",
"dotlessi", "lslash", "oslash", "oe", "germandbls", "onesuperior",
"logicalnot", "mu", "trademark", "Eth", "onehalf", "plusminus", "Thorn",
"onequarter", "divide", "brokenbar", "degree", "thorn", "threequarters",
"twosuperior", "registered", "minus", "eth", "multiply", "threesuperior",
"copyright", "Aacute", "Acircumflex", "Adieresis", "Agrave", "Aring",
"Atilde", "Ccedilla", "Eacute", "Ecircumflex", "Edieresis", "Egrave",
"Iacute", "Icircumflex", "Idieresis", "Igrave", "Ntilde", "Oacute",
"Ocircumflex", "Odieresis", "Ograve", "Otilde", "Scaron", "Uacute",
"Ucircumflex", "Udieresis", "Ugrave", "Yacute", "Ydieresis", "Zcaron",
"aacute", "acircumflex", "adieresis", "agrave", "aring", "atilde",
"ccedilla", "eacute", "ecircumflex", "edieresis", "egrave", "iacute",
"icircumflex", "idieresis", "igrave", "ntilde", "oacute", "ocircumflex",
"odieresis", "ograve", "otilde", "scaron", "uacute", "ucircumflex",
"udieresis", "ugrave", "yacute", "ydieresis", "zcaron", "exclamsmall",
"Hungarumlautsmall", "dollaroldstyle", "dollarsuperior", "ampersandsmall",
"Acutesmall", "parenleftsuperior", "parenrightsuperior", "twodotenleader",
"onedotenleader", "zerooldstyle", "oneoldstyle", "twooldstyle",
"threeoldstyle", "fouroldstyle", "fiveoldstyle", "sixoldstyle",
"sevenoldstyle", "eightoldstyle", "nineoldstyle", "commasuperior",
"threequartersemdash", "periodsuperior", "questionsmall", "asuperior",
"bsuperior", "centsuperior", "dsuperior", "esuperior", "isuperior",
"lsuperior", "msuperior", "nsuperior", "osuperior", "rsuperior", "ssuperior",
"tsuperior", "ff", "ffi", "ffl", "parenleftinferior", "parenrightinferior",
"Circumflexsmall", "hyphensuperior", "Gravesmall", "Asmall", "Bsmall",
"Csmall", "Dsmall", "Esmall", "Fsmall", "Gsmall", "Hsmall", "Ismall",
"Jsmall", "Ksmall", "Lsmall", "Msmall", "Nsmall", "Osmall", "Psmall",
"Qsmall", "Rsmall", "Ssmall", "Tsmall", "Usmall", "Vsmall", "Wsmall",
"Xsmall", "Ysmall", "Zsmall", "colonmonetary", "onefitted", "rupiah",
"Tildesmall", "exclamdownsmall", "centoldstyle", "Lslashsmall",
"Scaronsmall", "Zcaronsmall", "Dieresissmall", "Brevesmall", "Caronsmall",
"Dotaccentsmall", "Macronsmall", "figuredash", "hypheninferior",
"Ogoneksmall", "Ringsmall", "Cedillasmall", "questiondownsmall", "oneeighth",
"threeeighths", "fiveeighths", "seveneighths", "onethird", "twothirds",
"zerosuperior", "foursuperior", "fivesuperior", "sixsuperior",
"sevensuperior", "eightsuperior", "ninesuperior", "zeroinferior",
"oneinferior", "twoinferior", "threeinferior", "fourinferior",
"fiveinferior", "sixinferior", "seveninferior", "eightinferior",
"nineinferior", "centinferior", "dollarinferior", "periodinferior",
"commainferior", "Agravesmall", "Aacutesmall", "Acircumflexsmall",
"Atildesmall", "Adieresissmall", "Aringsmall", "AEsmall", "Ccedillasmall",
"Egravesmall", "Eacutesmall", "Ecircumflexsmall", "Edieresissmall",
"Igravesmall", "Iacutesmall", "Icircumflexsmall", "Idieresissmall",
"Ethsmall", "Ntildesmall", "Ogravesmall", "Oacutesmall", "Ocircumflexsmall",
"Otildesmall", "Odieresissmall", "OEsmall", "Oslashsmall", "Ugravesmall",
"Uacutesmall", "Ucircumflexsmall", "Udieresissmall", "Yacutesmall",
"Thornsmall", "Ydieresissmall", "001.000", "001.001", "001.002", "001.003",
"Black", "Bold", "Book", "Light", "Medium", "Regular", "Roman", "Semibold"
}
for i,v in ipairs(strings) do
strings[v] = i
end
return strings

321
luametalatex-font-cff.lua Normal file
View File

@ -0,0 +1,321 @@
local pack = string.pack
local strings = require'luametalatex-font-cff-data'
local function getstring(cff, str)
local i = strings[str] or cff.strings[str]
if not i then
i = #strings + #cff.strings + 1
cff.strings[str] = i
cff.strings[i - #strings] = str
end
return i
end
local function serialize_index(index, element_serializer)
local sizes = {1}
local length = 1
local data = {}
for i=1,#index do
data[i] = element_serializer(index[i])
length = length + #data[i]
sizes[#sizes+1] = length
end
data = table.concat(data)
if data == "" then return "\0\0" end
local offSize = length < 2^8 and 1 or length < 2^16 and 2 or length < 2^24 and 3 or 4
local offsetfmt = string.format(">I%i", offSize)
local offsets = ""
for i = #sizes,1,-1 do
sizes[i+1] = pack(offsetfmt, sizes[i])
end
sizes[1] = pack(">I2B", #index, offSize)
sizes[#sizes+1] = data
return table.concat(sizes)
end
local function ident(...)
return ...
end
local real_lookup = {
['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4, ['5'] = 5, ['6'] = 6, ['7'] = 7, ['8'] = 8, ['9'] = 9,
['.'] = 0xa, ['-'] = 0xe
}
local function dictInt(n)
local num = math.floor(n)
if num ~= n then
num = tostring(n)
local i, result, tmp = 1, string.char(0x1e)
while i <= #num do
local c = real_lookup[num:sub(i, i)]
if not c then -- We got an 'e'
c = num:sub(i+1, i+1) == '+' and 0xb or 0xc
repeat
i = i + 1
until num:sub(i+1, i+1) ~= '0'
end
if tmp then
result = result .. string.char(tmp * 16 + c)
tmp = nil
else
tmp = c
end
i = i + 1
end
return result .. string.char((tmp or 0xf) * 16 + 0xf)
elseif num >= -107 and num <= 107 then
return string.char(num + 139)
elseif num >= 108 and num <= 1131 then
num = num - 108
return string.char(247 + ((num >> 8) & 0xFF), num & 0xFF)
elseif num >= -1131 and num <= -108 then
num = -num - 108
return string.char(251 + ((num >> 8) & 0xFF), num & 0xFF)
elseif num >= -32768 and num <= 32767 then
return string.char(28, (num >> 8) & 0xFF, num & 0xFF)
else
return string.char(29, (num >> 24) & 0xFF, (num >> 16) & 0xFF,
(num >> 8) & 0xFF, num & 0xFF)
end
end
local function serialize_top(cff)
local data = dictInt(getstring(cff, cff.registry or 'Adobe'))
.. dictInt(getstring(cff, cff.ordering or 'Identity'))
.. dictInt( cff.supplement or 0)
.. string.char(12, 30)
if cff.version then
data = data .. dictInt(getstring(cff, cff.version)) .. string.char(0)
end
if cff.Notice then
data = data .. dictInt(getstring(cff, cff.Notice)) .. string.char(1)
end
if cff.FullName then
data = data .. dictInt(getstring(cff, cff.FullName)) .. string.char(2)
end
if cff.FamilyName then
data = data .. dictInt(getstring(cff, cff.FamilyName)) .. string.char(3)
end
if cff.Weight then
data = data .. dictInt(getstring(cff, cff.Weight)) .. string.char(4)
end
if cff.isFixedPitch then
data = data .. dictInt(1) .. string.char(12, 1)
end
if cff.ItalicAngle and cff.ItalicAngle ~= 0 then
data = data .. dictInt(cff.ItalicAngle) .. string.char(12, 2)
end
if cff.UnderlinePosition then
data = data .. dictInt(cff.UnderlinePosition) .. string.char(12, 3)
end
if cff.UnderlineThickness then
data = data .. dictInt(cff.UnderlineThickness) .. string.char(12, 4)
end
if cff.FontMatrix then
data = data .. dictInt(cff.FontMatrix[1]) .. dictInt(cff.FontMatrix[2])
.. dictInt(cff.FontMatrix[3]) .. dictInt(cff.FontMatrix[4])
.. dictInt(cff.FontMatrix[5]) .. dictInt(cff.FontMatrix[6])
.. string.char(12, 7)
end
if cff.FontBBox then
data = data .. dictInt(cff.FontBBox[1]) .. dictInt(cff.FontBBox[2])
.. dictInt(cff.FontBBox[3]) .. dictInt(cff.FontBBox[4])
.. string.char(5)
end
if cff.PostScript then
data = data .. dictInt(getstring(cff, cff.PostScript)) .. string.char(12, 21)
end
data = data .. dictInt(cff.charset_offset) .. string.char(15)
data = data .. dictInt(cff.charstrings_offset) .. string.char(17)
data = data .. dictInt(cff.fdarray_offset) .. string.char(12, 36)
data = data .. dictInt(cff.fdselect_offset) .. string.char(12, 37)
-- data = data .. dictInt(cff.private_size) .. dictInt(cff.private_offset) .. string.char(18)
return data
end
local function serialize_font(cff, offset0) return function(private)
local data = dictInt(private[3]) .. string.char(12, 38)
data = data .. dictInt(private[2]) .. dictInt(offset0 + private[1]) .. string.char(18)
return data
end end
local function va_minone(...)
if select('#', ...) ~= 0 then
return (...+1), select(2, ...)
end
end
-- local function serialize_fdselect(cff)
-- local fdselect = cff.FDSelect or {format=3, {0,1}}
-- if not fdselect then
-- return '\3\0\1\0\0\0' .. string.pack('>I2', #cff.glyphs)
-- end
-- if fdselect.format == 0 then
-- return string.char(0, va_minone(table.unpack(fdselect)))
-- elseif fdselect.format == 3 then
-- local fdparts = {string.pack(">BI2", 3, #fdselect)}
-- for i=1,#fdselect do
-- fdparts[i+1] = string.pack(">I2B", fdselect[i][1], fdselect[i][2]-1)
-- end
-- fdparts[#fdselect+2] = string.pack(">I2", #cff.glyphs)
-- return table.concat(fdparts)
-- else
-- error[[Confusion]]
-- end
-- end
local function serialize_fdselect(cff)
local fdselect = {""}
local lastfont = -1
for i, g in ipairs(cff.glyphs) do
local font = g.cidfont or 1
if font ~= lastfont then
fdselect[#fdselect+1] = string.pack(">I2B", i-1, font-1)
lastfont = font
end
end
if #fdselect*3+2 > #cff.glyphs+1 then
fdselect[1] = string.pack("B", 0)
for i, g in ipairs(cff.glyphs) do
local font = g.cidfont or 1
fdselect[i+1] = string.pack("B", font-1)
end
else
fdselect[1] = string.pack(">BI2", 3, #fdselect-1)
fdselect[#fdselect+1] = string.pack(">I2", #cff.glyphs)
end
return table.concat(fdselect)
end
local function serialize_private(private, subrsoffset)
local data = ""
if not private.BlueValues then
private.BlueValues = { }
end
local last = 0
for _, v in ipairs(private.BlueValues) do
data = data .. dictInt(v - last)
last = v
end
data = data .. '\6'
if private.OtherBlues and #private.OtherBlues ~= 0 then
last = 0
for _, v in ipairs(private.OtherBlues) do
data = data .. dictInt(v - last)
last = v
end
data = data .. '\7'
end
if private.BlueScale then
data = data .. dictInt(private.BlueScale) .. '\12\9'
end
if private.BlueShift then
data = data .. dictInt(private.BlueShift) .. '\12\10'
end
if private.BlueFuzz then
data = data .. dictInt(private.BlueFuzz) .. '\12\11'
end
if private.ForceBold then
data = data .. dictInt(1) .. '\12\14'
end
if private.StdHW then
data = data .. dictInt(private.StdHW) .. '\10'
end
if private.StdVW then
data = data .. dictInt(private.StdVW) .. '\11'
end
if private.StemSnapH and #private.StemSnapH ~= 0 then
last = 0
for _, v in ipairs(private.StemSnapH) do
data = data .. dictInt(v - last)
last = v
end
data = data .. '\12\12'
end
if private.StemSnapV and #private.StemSnapV ~= 0 then
last = 0
for _, v in ipairs(private.StemSnapV) do
data = data .. dictInt(v - last)
last = v
end
data = data .. '\12\13'
end
if subrsoffset and subrsoffset ~= 0 then
data = data .. dictInt(subrsoffset) .. '\19'
end
if private.defaultWidthX then
data = data .. dictInt(private.defaultWidthX) .. '\20'
end
if private.nominalWidthX then
data = data .. dictInt(private.nominalWidthX) .. '\21'
end
return data
end
local function serialize_charset(cff)
local data = string.char(2)
local last, count = -42, -1
for _, glyph in ipairs(cff.glyphs) do
if not glyph.cid then glyph.cid = glyph.index end
if glyph.cid ~= 0 then
if glyph.cid ~= last + 1 or count == 0xFFFF then
if count >= 0 then
data = data .. string.pack(">I2", count)
count = -1
end
data = data .. string.pack(">I2", glyph.cid)
end
last = glyph.cid
count = count + 1
end
end
if count >= 0 then
data = data .. string.pack(">I2", count)
count = -1
end
return data
end
return function(cff)
cff.strings = {}
cff.private_offset = 0
cff.charstrings_offset = 0
cff.fdarray_offset = 0
cff.fdselect_offset = 0
cff.charset_offset = 0
local top = serialize_index({serialize_top(cff)}, ident)
local data = string.char(1, 0, 4, 2) -- Assuming 16Bit offsets (Where are they used?)
local name = serialize_index({cff.FontName}, ident)
local globalsubrs = serialize_index(cff.GlobalSubrs or {}, ident)
local private_offsets, privates = {}, {}
local privates_size = 0 -- These include the localsubrs sizes
for i, p in ipairs(cff.Privates or {cff}) do
local subrs = p.Subrs
if not subrs or subrs and #subrs == 0 then
subrs = ""
else
subrs = serialize_index(subrs, ident)
end
local serialized = ""
if subrs ~= "" then
local last_size = 0
repeat
last_size = #serialized
serialized = serialize_private(p, last_size)
until last_size == #serialized
else
serialized = serialize_private(p)
end
-- serialized = serialize_private(p, -#subrs)
privates[i] = serialized .. subrs
private_offsets[i] = {privates_size + 0*#subrs, #serialized, getstring(cff, p.FontName or (cff.FontName .. '-' .. i))}
privates_size = privates_size + #subrs + #serialized
end
if cff.glyphs[1].index ~= 0 then
table.insert(cff.glyphs, 1, {index = 0, cs = string.char(14), cidfont = cff.glyphs[1].cidfont})
end
local strings = serialize_index(cff.strings, ident)
local charset = serialize_charset(cff)
local fdselect = serialize_fdselect(cff)
local charstrings = serialize_index(cff.glyphs, function(g) return g.cs end)
local pre_private, top_size = #data + #name + #strings + #globalsubrs
repeat
top_size = #top
cff.charstrings_offset = pre_private + top_size + privates_size
cff.charset_offset = cff.charstrings_offset + #charstrings
cff.fdselect_offset = cff.charset_offset + #charset
cff.fdarray_offset = cff.fdselect_offset + #fdselect
top = serialize_index({serialize_top(cff)}, ident)
until top_size == #top
local fdarray = serialize_index(private_offsets, serialize_font(cff, top_size + pre_private), ident)
return data .. name .. top .. strings .. globalsubrs .. table.concat(privates) .. charstrings .. charset .. fdselect .. fdarray
end

View File

@ -0,0 +1,99 @@
local function check(buf, i, afterI)
local checksum = 0
while i < afterI do
if i+60 < afterI then
local num1, num2, num3, num4, num5, num6, num7, num8, num9, num10, num11, num12, num13, num14, num15, num16, newI = string.unpack(">I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4", buf, i)
i = newI
checksum = checksum + num1 + num2 + num3 + num4 + num5 + num6 + num7 + num8 + num9 + num10 + num11 + num12 + num13 + num14 + num15 + num16
elseif i+28 < afterI then
local num1, num2, num3, num4, num5, num6, num7, num8, newI = string.unpack(">I4I4I4I4I4I4I4I4", buf, i)
i = newI
checksum = checksum + num1 + num2 + num3 + num4 + num5 + num6 + num7 + num8
elseif i+12 < afterI then
local num1, num2, num3, num4, newI = string.unpack(">I4I4I4I4", buf, i)
i = newI
checksum = checksum + num1 + num2 + num3 + num4
else
local num
num, i = string.unpack(">I4", buf, i)
checksum = checksum + num
end
end
return checksum & 0xFFFFFFFF
end
local function log2floor(i)
local j = 0
if i>>8 ~= 0 then
j = j + 8
end
if i>>(j+4) ~= 0 then
j = j + 4
end
if i>>(j+2) ~= 0 then
j = j + 2
end
if i>>(j+1) ~= 0 then
j = j + 1
end
return j
end
return {
write = function(magic, tables)
local tabdata = {}
for t, val in next, tables do
tabdata[#tabdata+1] = {t, val .. string.rep("\0", (#val+3&~3)-#val), #val}
end
table.sort(tabdata, function(a,b)return a[1]<b[1]end)
local logtabs = log2floor(#tabdata)
local tabs = {string.pack(">c4I2I2I2I2", magic, #tabdata, 1<<logtabs+4, logtabs, #tabdata-(1<<logtabs)<<4)}
local offset = #tabs[1]+#tabdata*16
local checksum, headindex = check(tabs[1], 1, 1+#tabs[1])
for i=1,#tabdata do
local tab = tabdata[i]
local data = tab[2]
if tab[1] == "head" then
headindex = i+1+#tabdata
data = data:sub(1,8) .. '\0\0\0\0' .. data:sub(13) -- Benchmarking suggests that this is faster than a LPEG version
end
local thischeck = check(data, 1, 1+tab[3])
tabs[i+1] = string.pack(">c4I4I4I4", tab[1], thischeck, offset, tab[3])
checksum = checksum + check(tabs[i+1], 1, 17) + thischeck
offset = offset + #data
tabs[i+1+#tabdata] = data
end
if headindex then
local data = tabs[headindex]
data = data:sub(1,8) .. string.pack(">I4", 0xB1B0AFBA-checksum&0xFFFFFFFF) .. data:sub(13) -- Benchmarking suggests that this is faster than a LPEG version
tabs[headindex] = data
end
return table.concat(tabs)
end,
parse = function(buf, off, fontid)
off = off or 1
local headMagic, numTables
headMagic, numTables, off = string.unpack(">c4I2", buf, off)
if headMagic == "ttcf" then
if numTables > 2 then -- numTables is actually the major version here, 1&2 are basically equal
error[[Unsupported TTC header version]]
end
headMagic, numTables, off = string.unpack(">I2I4", buf, off)
fontid = fontid or 1
if numTables < fontid then
error[[There aren't that many fonts in this file]]
end
off = string.unpack(">I4", buf, off + (fontid-1)*4)+1
headMagic, numTables, off = string.unpack(">c4I2", buf, off)
end
off = off+6
local tables = {}
for i=1,numTables do
local tag, check, offset, len, newOff = string.unpack(">c4I4I4I4", buf, off)
off = newOff
tables[tag] = {offset+1, len}
if offset+len > #buf then
error[[Font file too small]]
end
end
return headMagic, tables
end,
}

139
luametalatex-font-t1.lua Normal file
View File

@ -0,0 +1,139 @@
local white = (lpeg.S'\0\9\10\12\13\32' + '%' * (1 - lpeg.S'\r\n')^0)^1
local regular = 1-lpeg.S'()<>[]{}/%\0\9\10\12\13\32'
local lastbase = '123456789abcdefghiklmnopqrstuvwxyz'
local number = lpeg.Cmt(lpeg.R'09'^1/tonumber * '#', function(s, p, base)
if base < 2 then return end
local pattern
if base <= 10 then
pattern = lpeg.R('0' .. lastbase:sub(base-1, base-1))
else
pattern = lpeg.R'09' + lpeg.R('a' .. lastbase:sub(base-1, base-1)) + lpeg.R('A' .. lastbase:sub(base-1, base-1):upper())
end
local num, p = (lpeg.C(pattern^1) * lpeg.Cp()):match(s, p)
return p, num and tonumber(num, base)
end)
+ (lpeg.S'+-'^-1 * ('.' * lpeg.R'09'^1 + lpeg.R'09'^1 * lpeg.P'.'^-1 * lpeg.R'09'^0) * (lpeg.S'eE' * lpeg.S'+-'^-1 * lpeg.R'09'^1)^-1)/tonumber
local literalstring = lpeg.P{'(' * lpeg.Cs((
lpeg.P'\\n'/'\n'+lpeg.P'\\r'/'\r'+lpeg.P'\\t'/'\t'+lpeg.P'\\b'/'\b'+lpeg.P'\\f'/'\f'
+'\\'*lpeg.C(lpeg.R'07'*lpeg.R'07'^-2)/function(n)return string.char(tonumber(n, 8))end
+'\\'*('\n' + ('\r' * lpeg.P'\n'^-1))/''
+'\\'*lpeg.C(1)/1
+('\n' + ('\r' * lpeg.P'\n'^-1))/'\n'
+(1-lpeg.S'()\\')+lpeg.V(1))^0) * ')'}
local hexstring = '<' * lpeg.Cs((
lpeg.C(lpeg.R'09'+lpeg.R'af'+lpeg.R'AF')*(lpeg.C(lpeg.R'09'+lpeg.R'af'+lpeg.R'AF')+lpeg.Cc'0')/function(a,b)return string.char(tonumber(a..b, 16))end)^0) * '>'
local name = lpeg.C(regular^1)
local lname = '/' * name / 1
local function decrypt(key, n, cipher)
-- Generally you should never implement your own crypto. So we call a well known, peer reviewed,
-- high-quality cryptographic library. --- Ha-Ha, of course we are implementing by ourselves.
-- That might be completely unsecure, but given that the encryption keys are well known constants
-- documented in the T1 Spec, there is no need to worry about it.
-- Also I do not think any cryptorgraphic library would implement this anyway, it doesn't even
-- really deserve the term encryption.
local decoded = {string.byte(cipher, 1,-1)}
for i=1,#decoded do
local c = decoded[i]
decoded[i] = c ~ (key>>8)
key = (((c+key)&0xFFFF)*52845+22719)&0xFFFF
end
return string.char(table.unpack(decoded, n+1))
end
-- io.stdout:write(decrypt(55665, 4, string.sub(io.stdin:read'a', 7)))
local boolean = (lpeg.P'true' + 'false')/{["true"] = true, ["false"] = false}
local anytype = {hexstring + literalstring + number + lname + boolean + lpeg.V(2) + name, lpeg.Ct('[' * (white^-1 * lpeg.V(1))^0 * white^-1 * ']' + '{' * (white^-1 * lpeg.V(1))^0 * white^-1 * '}' * white^-1 * lpeg.P"executeonly"^-1)}
local dict = lpeg.Cf(lpeg.Carg(1) * lpeg.Cg(white^-1*lname*white^-1*(anytype)*white^-1*lpeg.P"readonly"^-1*white^-1*lpeg.P"noaccess"^-1*white^-1*(lpeg.P"def"+"ND"+"|-"))^0, rawset)
local encoding = (white+anytype-("for"*white))^0*"for"*white/0
* lpeg.Cf(lpeg.Ct''
* lpeg.Cg("dup"*white*number*white^-1*lname*white^-1*"put"*white)^0
, rawset)
* lpeg.P"readonly"^-1*white*"def"
local function parse_encoding(offset, str)
local found
found, offset = (encoding*lpeg.Cp()):match(str, offset)
return found, offset
end
local function parse_fontinfo(offset, str)
local found
repeat
found, offset = ((white+(anytype-name))^0/0*name*lpeg.Cp()):match(str, offset)
until found == 'begin'
found, offset = (dict*lpeg.Cp()):match(str, offset, {})
offset = (white^-1*"end"*white^-1*lpeg.P"readonly"^-1*white^-1*"def"):match(str, offset)
return found, offset
end
local binary_bytes = lpeg.Cmt(number*white^-1*(lpeg.P'-| ' + 'RD '), function(s, p, l)return p+l, s:sub(p, p+l-1) end)*white*(lpeg.P"|-"+"|"+"ND"+"NP")
local charstr = white^-1*lname*(white^-1*(anytype-lname))^0/0*white^-1
* lpeg.Cf(lpeg.Ct''
* lpeg.Cg(lname*white^-1*binary_bytes*white)^0
, rawset)
* lpeg.P"end"*white
local subrs = (white^-1*(anytype-("dup"*white)))^0/0*white^-1
* lpeg.Cf(lpeg.Ct''
* lpeg.Cg("dup"*white^-1*number*white^-1*binary_bytes*white)^0
, rawset)
* (lpeg.P"readonly"*white)^-1 * (lpeg.P"noaccess"*white)^-1*(lpeg.P"def"+"ND"+"|-")
local function parse_private(offset, str)
local mydict, found
repeat
found, offset = ((white+(anytype-name))^0/0*name*lpeg.Cp()):match(str, offset)
until found == 'begin'
mydict, offset = (dict*lpeg.Cp()):match(str, offset, {})
found = (white^-1*lname):match(str, offset)
if found == "Subrs" then
mydict.Subrs, offset = (subrs*lpeg.Cp()):match(str, offset)
end
return mydict, offset
end
local function continue_maintable(offset, str, mydict)
mydict, offset = (dict*lpeg.Cp()):match(str, offset, mydict)
local found = (white^-1*lname):match(str, offset)
if found == "FontInfo" then
mydict.FontInfo, offset = parse_fontinfo(offset, str)
return continue_maintable(offset, str, mydict)
elseif found == "Encoding" then
mydict.Encoding, offset = parse_encoding(offset, str)
return continue_maintable(offset, str, mydict)
elseif found == "Private" then
mydict.Private, offset = parse_private(offset, str)
return continue_maintable(offset, str, mydict)
elseif found == "CharStrings" then
mydict.CharStrings, offset = (charstr*lpeg.Cp()):match(str, offset)
return mydict
else
local newoffset = ((white+name)^1/0*lpeg.Cp()):match(str, offset)
if newoffset and offset <= #str then
return continue_maintable(newoffset, str, mydict)
end
end
print(str:sub(offset))
error[[Unable to read Type 1 font]]
end
local function parse_maintable(offset, str)
local found
repeat
found, offset = ((white+(anytype-name))^0/0*name*lpeg.Cp()):match(str, offset)
until found == 'begin'
return continue_maintable(offset, str, {})
end
return function(filename)
local file = io.open(filename)
local _, length = string.unpack("<I2I4", file:read(6))
local preface = file:read(length)
_, length = string.unpack("<I2I4", file:read(6))
local private = decrypt(55665, 4, file:read(length))
file:close()
local after = parse_maintable(1, preface .. private)
local lenIV = after.Private.lenIV or 4
local chars = after.CharStrings
for k, v in pairs(chars) do
chars[k] = decrypt(4330, lenIV, v)
end
local subrs = after.Private.Subrs
for k, v in pairs(subrs) do
subrs[k] = decrypt(4330, lenIV, v)
end
return after
end

View File

@ -0,0 +1,237 @@
local function parse_charstring(cs, subrs, result)
result = result or {{false}}
local lastresult = result[#result]
local i = 1
while i~=#cs+1 do
local cmd = cs:byte(i)
if cmd == 255 then
lastresult[#lastresult+1] = string.unpack(">i4", cs:sub(i+1, i+4))
i = i+4
elseif cmd >= 251 then
lastresult[#lastresult+1] = -((cmd-251)*256)-string.byte(cs, i+1)-108
i = i+1
elseif cmd >= 247 then
lastresult[#lastresult+1] = (cmd-247)*256+string.byte(cs, i+1)+108
i = i+1
elseif cmd >= 32 then
lastresult[#lastresult+1] = cmd-139
elseif cmd == 9 then -- closepath, implicit in Type2
elseif cmd == 10 then
local subr = subrs[lastresult[#lastresult]]
lastresult[#lastresult] = nil
parse_charstring(subr, subrs, result)
lastresult = result[#result]
elseif cmd == 11 then
break -- We do not keep subroutines, so drop returns and continue with the outer commands
elseif cmd == 12 then
i = i+1
cmd = cs:byte(i)
if cmd == 12 then -- div, we might have huge parameters, so execute directly
lastresult[#lastresult-1] = lastresult[#lastresult-1]/lastresult[#lastresult]
lastresult[#lastresult] = nil
elseif cmd == 16 then -- othersubr...
cmd = lastresult[#lastresult]
lastresult[#lastresult] = nil
local numargs = lastresult[#lastresult]
lastresult[#lastresult] = nil
if cmd == 3 then -- Hint replacement. This is easy, we support hint replacement, so we
-- keep the original subr number
assert(numargs == 1)
elseif cmd == 1 then -- Flex initialization
elseif cmd == 2 then -- Flex parameter
if result[#result-1].flex then
result[#result] = nil -- TODO: Warn if there were values.
lastresult = result[#result] -- We keep collecting arguments
end
lastresult.flex = true
elseif cmd == 0 then -- Flex
local flexinit = result[#result-1]
lastresult[2] = lastresult[2] + flexinit[2]
lastresult[3] = lastresult[3] + flexinit[3]
lastresult.flex = nil
result[#result-1] = lastresult
result[#result] = nil
lastresult[#lastresult] = nil
lastresult[#lastresult] = nil
lastresult[1] = -36
lastresult = {false}
result[#result+1] = lastresult
lastresult[#lastresult+1] = "setcurrentpointmark"
elseif cmd == 12 or cmd == 13 then
local pending = {}
local results = #lastresult
for i = 1,numargs do
pending[i] = lastresult[results-numargs+i]
lastresult[results-numargs+i] = nil
end
for i = 1,#lastresult.pendingargs do
pending[numargs+i] = lastresult.pendingargs[i]
end
if cmd == 12 then
lastresult.pendingargs = pending
else
lastresult.pendingargs = nil
-- TODO Translate pending to counter mask
end
else
error[[UNSUPPORTED Othersubr]]
end
elseif cmd == 17 then -- pop... Ignore them, they should already be handled by othersubr.
-- Compatibility with unknown othersubrs is futile, because
-- we can't interpret PostScript
elseif cmd == 33 then -- setcurrentpoint... If we expected this, it is already handled.
-- Otherwise fail, according to the spec it should
-- only be used with othersubrs.
assert(lastresult[#lastresult] == "setcurrentpointmark")
lastresult[#lastresult] = nil
else
lastresult[1] = -cmd-1
lastresult = {false}
result[#result+1] = lastresult
end
else
lastresult[1] = cmd
lastresult = {false}
result[#result+1] = lastresult
end
i = i+1
end
return result
end
local function adjust_charstring(cs) -- Here we get a not yet optimized but parsed Type1 charstring and
-- do some adjustments to make them more "Type2-like".
cs[#cs] = nil -- parse_charstring adds a `{false}` for internal reasons. Just drop it here. FIXME: Check that #cs[#cs]==1, otherwise there were values left on the charstring stack
if cs[1][1] ~= 13 then
error[[Unsupported first Type1 operator]] -- probably cs[1][1] == sbw
-- If you find a font using this, I'm sorry for you.
end
local hoffset = cs[1][2]
if hoffset ~= 0 then
-- non-zero sidebearings :-(
for i, cmd in ipairs(cs) do
if cmd[1] == 21 or cmd[1] == 22 then
cmd[2] = cmd[2] + cs[1][2]
break
elseif cmd[1] == 4 then
cmd[3] = cmd[2]
cmd[2] = cs[1][2]
cmd[1] = 21
break
end
-- Here I rely on the fact that the first relative command is always [hvr]moveto.
-- This is based on "Use rmoveto for the first point in the path." in the T1 spec
-- for hsbw. I am not entirely sure if this is a strict requirement or if there could
-- be weird charstrings where this fails (esp. since [hv]moveto are also used in the example),
-- but I decided to take the risk.
-- hints are affected too. They do not use relative coordinates in T1, so we store the offset
-- and handle hints later
end
end
cs[1][2] = cs[1][3]
cs[1][3] = nil
cs[1][1] = nil
-- That's it for the width, now we need some hinting stuff. This would be easy, if hint replacement
-- wouldn't require hint masks in Type2. And because we really enjoy this BS, we get counter
-- hinting as an additional treat... Oh, if you actually you counter hinting: Please test this
-- and report back if it works, because this is pretty much untested.
-- TODO: Even more than that, counters are not implemented at all right now, except for [hv]stem3
local stems = {}
local stem3 = {}
-- First iterate over the charstring, recording all hints and collecting them in stems/stem3
for i, cmd in ipairs(cs) do
if cmd[1] == 1 or cmd[1] == 3 then
stems[#stems + 1] = cmd
elseif cmd[1] == -2 or cmd[1] == -3 then
local c = cmd[1] == -2 and 3 or 1
stems[#stems + 1] = {c, cmd[2], cmd[3]}
stems[#stems + 1] = {c, cmd[4], cmd[5]}
stems[#stems + 1] = {c, cmd[6], cmd[7]}
table.move(stems, #stems-2, #stems, #stem3+1, stem3)
cs[i] = false
end
end
table.sort(stems, function(first, second)
if first[1] ~= second[1] then return first[1] < second[1] end
if first[2] ~= second[2] then return first[2] < second[2] end
return first[3] < second[3]
end)
-- Now store the index of every stem in the idx member of the hint command
-- After that `j` stores the number of stems
local j,k = 1,1
if stems[1] then stems[1].idx = 1 end
for i = 2,#stems do
if stems[i][2] == stems[k][2] and stems[i][3] == stems[k][3] then
stems[i].idx = j
stems[i] = false
else
j, k = j+1, i
stems[i].idx = j
end
end
-- Now the indices are known, so the cntrmask can be written, if stem3 occured.
-- This is done before writing the stem list to make the thable.insert parameters easier.
local bytes = {}
if stem3[1] then
for l = 1, math.floor((j + 7)/8) do
bytes[l] = 0
end
for l = 1, #stem3 do
local idx = stem3[l].idx-1
bytes[math.floor(idx/8) + 1] = bytes[math.floor(idx/8) + 1] | (1<<(7-idx%8))
end
table.insert(cs, 2, {20, string.char(table.unpack(bytes))})
end
local current = 1
-- Then list the collected stems at the beginning of the charstring
if stems[current] and stems[current][1] == 1 then
local stem_tbl, last = {18}, 0
while stems[current] ~= nil and (not stems[current] or stems[current][1] == 1) do
if stems[current] then
stem_tbl[#stem_tbl + 1] = stems[current][2] - last
last = stems[current][2] + stems[current][3]
stem_tbl[#stem_tbl + 1] = stems[current][3]
end
current = current + 1
end
table.insert(cs, 2, stem_tbl)
end
if stems[current] and stems[current][1] == 3 then
local stem_tbl, last = {false}, -hoffset
while stems[current] ~= nil and (not stems[current] or stems[current][1] == 3) do
if stems[current] then
stem_tbl[#stem_tbl + 1] = stems[current][2] - last
last = stems[current][2] + stems[current][3]
stem_tbl[#stem_tbl + 1] = stems[current][3]
end
current = current + 1
end
table.insert(cs, stems[1][1] == 1 and 3 or 2, stem_tbl)
end
-- Finally, replace every run of hint commands, corresponding to a hint replacement, by a single hintmask
local i = 1
while cs[i] ~= nil do
if cs[i] and cs[i].idx then
for l = 1, math.floor((j + 7)/8) do
bytes[l] = 0
end
while (cs[i] or {}).idx do
local idx = cs[i].idx-1
bytes[math.floor(idx/8) + 1] = bytes[math.floor(idx/8) + 1] | (1<<(7-idx%8))
cs[i] = false
i = i+1
end
for l = 1, #stem3 do
local idx = stem3[l].idx-1
bytes[math.floor(idx/8) + 1] = bytes[math.floor(idx/8) + 1] | (1<<(7-idx%8))
end
i = i-1
cs[i] = {19, string.char(table.unpack(bytes))}
end
i = i+1
end
end
return function(cs, subrs)
local parsed = parse_charstring(cs, subrs)
adjust_charstring(parsed)
return parsed
end

View File

@ -0,0 +1,168 @@
-- This is optimizet2.lua.
-- Copyright 2019 Marcel Krüger <tex@2krueger.de>
--
-- This work may be distributed and/or modified under the
-- conditions of the LaTeX Project Public License version 1.3c.
-- The full text of this version can be found at
-- http://www.latex-project.org/lppl/lppl-1-3c.txt
--
-- This work has the LPPL maintenance status `maintained'
--
-- The Current Maintainer of this work is Marcel Krüger.
--
-- This work consists of the files buildcffwrapper.lua, buildotfwrapper.lua,
-- finish_pdffont.lua, finisht3.lua, glyph2char.lua, libSfnt.lua,
-- luaglyphlist.lua, luaotfprovider.lua, make_extensible_per_char.lua,
-- mpfont.lua, mplibtolist.lua, mplibtot2.lua, mpnodelib.lua,
-- mt1_fontloader.lua, nodebuilder.lua, optimizet2.lua, serializet2.lua.
return function(cs)
-- cs might contain some false entries, delete them
do
local j=1
for i = 1,#cs do
if cs[i] then
if i ~= j then
cs[j] = cs[i]
end
j = j+1
end
end
for i = j,#cs do
cs[i] = nil
end
end
-- First some easy replacements:
local use_hintmask
for i, v in ipairs(cs) do
if v[1] == 19 then
-- If this happens only one time, we do not want masks
use_hintmask = use_hintmask ~= nil
elseif v[1] == 21 then -- rmoveto
if v[2] == 0 then
v[1] = 4
v[2] = v[3]
v[3] = nil
elseif v[3] == 0 then
v[1] = 22
v[3] = nil
end
elseif v[1] == 5 then -- rlineto
if v[2] == 0 then
v[1] = 7
v[2] = v[3]
v[3] = nil
elseif v[3] == 0 then
v[1] = 6
v[3] = nil
end
elseif v[1] == 8 then -- rrcurveto
if v[2] == 0 then
if v[6] == 0 then
v[1] = 26 -- vvcurveto (even argument case)
table.remove(v, 6)
table.remove(v, 2)
else
v[1] = 30 -- vhcurveto
table.remove(v, 2)
if v[6] == 0 then table.remove(v, 6) end
end
elseif v[3] == 0 then
if v[7] == 0 then
v[1] = 27 -- hhcurveto (even argument case)
table.remove(v, 7)
table.remove(v, 3)
else
v[1] = 31 -- hvcurveto
table.remove(v, 3)
local t = v[5]
table.remove(v, 5)
if t ~= 0 then table.insert(v, t) end
end
elseif v[6] == 0 then
v[1] = 26 -- vvcurveto (odd argument case)
table.remove(v, 6)
elseif v[7] == 0 then
v[1] = 27 -- hhcurveto (odd argument case)
table.remove(v, 7)
local t = v[2]
v[2] = v[3]
v[3] = t
end
end
end
if not use_hintmask then
for i, v in ipairs(cs) do
if v[1] == 18 then
v[1] = 1
elseif not v[1] and cs[i+1] and cs[i+1][1] == 19 then
v[1] = 3
elseif v[1] == 19 then
table.remove(cs, i)
break
end
end
end
-- Try combining lineto segments. We could try harder, but this should
-- never be triggered anyway.
for i, v in ipairs(cs) do
if v[1] == 6 or v[1] == 7 then
while cs[i+1] and v[1] == cs[i+1][1] do
v[2] = v[2] + cs[i+1][2]
table.remove(cs, i+1)
end
end
end
-- Now use the variable argument versions of most commands
for i, v in ipairs(cs) do
if v[1] == 5 then -- rlineto
while cs[i+1] and 5 == cs[i+1][1] do
table.insert(v, cs[i+1][2])
table.insert(v, cs[i+1][3])
table.remove(cs, i+1)
end
if cs[i+1] and 8 == cs[i+1][1] then -- rrcurveto
v[1] = 25 -- rlinecurveto
for j=2,7 do table.insert(v, cs[i+1][j]) end
table.remove(cs, i+1)
end
elseif v[1] == 6 or v[1] == 7 then
local next_cmd = (v[1]-5)%2+6
while cs[i+1][1] == next_cmd do
next_cmd = (cs[i+1][1]-5)%2+6
table.insert(v, cs[i+1][2])
table.remove(cs, i+1)
end
elseif v[1] == 8 then
while cs[i+1] and 8 == cs[i+1][1] do -- rrcurveto
for j=2,7 do table.insert(v, cs[i+1][j]) end
table.remove(cs, i+1)
end
if cs[i+1] and 5 == cs[i+1][1] then -- rlineto
v[1] = 24 -- rcurveline
table.insert(v, cs[i+1][2])
table.insert(v, cs[i+1][3])
table.remove(cs, i+1)
end
elseif v[1] == 27 then
while cs[i+1] and 27 == cs[i+1][1] and #cs[i+1] == 5 do -- hhcurveto
for j=2,5 do table.insert(v, cs[i+1][j]) end
table.remove(cs, i+1)
end
elseif v[1] == 26 then
while cs[i+1] and 26 == cs[i+1][1] and #cs[i+1] == 5 do -- vvcurveto
for j=2,5 do table.insert(v, cs[i+1][j]) end
table.remove(cs, i+1)
end
elseif v[1] == 30 or v[1] == 31 then
local next_cmd = (v[1]-29)%2+30
while #v % 2 == 1 and cs[i+1] and next_cmd == cs[i+1][1] do -- [vh|hv]curveto
local next_cmd = (cs[i+1][1]-29)%2+30
for j=2,#cs[i+1] do table.insert(v, cs[i+1][j]) end
table.remove(cs, i+1)
end
elseif false then
-- TODO: More commands
end
end
end

66
luametalatex-font-t2.lua Normal file
View File

@ -0,0 +1,66 @@
-- This is serializet2.lua.
-- Copyright 2019 Marcel Krüger <tex@2krueger.de>
--
-- This work may be distributed and/or modified under the
-- conditions of the LaTeX Project Public License version 1.3c.
-- The full text of this version can be found at
-- http://www.latex-project.org/lppl/lppl-1-3c.txt
--
-- This work has the LPPL maintenance status `maintained'
--
-- The Current Maintainer of this work is Marcel Krüger.
--
-- This work consists of the files buildcffwrapper.lua, buildotfwrapper.lua,
-- finish_pdffont.lua, finisht3.lua, glyph2char.lua, libSfnt.lua,
-- luaglyphlist.lua, luaotfprovider.lua, make_extensible_per_char.lua,
-- mpfont.lua, mplibtolist.lua, mplibtot2.lua, mpnodelib.lua,
-- mt1_fontloader.lua, nodebuilder.lua, optimizet2.lua, serializet2.lua.
local pack = string.pack
local optimizet2 = require'luametalatex-font-t2-opt'
local function numbertot2(n)
if math.abs(n) > 2^15 then
error[[Number too big]]
end
local num = math.floor(n + .5)
if n ~= 0 and math.abs((num-n)/n) > 0.001 then
num = math.floor(n * 2^16 + 0.5)
return pack(">Bi4", 255, math.floor(n * 2^16 + 0.5))
elseif num >= -107 and num <= 107 then
return string.char(num + 139)
elseif num >= 108 and num <= 1131 then
return pack(">I2", num+0xF694) -- -108+(247*0x100)
elseif num >= -1131 and num <= -108 then
return pack(">I2", -num+0xFA94) -- -108+(251*0x100)
else
return pack(">Bi2", 28, num)
end
end
local function convert_cs(cs, upem)
local cs_parts = {}
local function add(cmd, first, ...)
if cmd == 19 or cmd == 20 then
cs_parts[#cs_parts+1] = string.char(cmd)
cs_parts[#cs_parts+1] = first
return
end
if first then
cs_parts[#cs_parts+1] = numbertot2(first*upem/1000)
return add(cmd, ...)
end
if cmd then
if cmd < 0 then
cs_parts[#cs_parts+1] = string.char(12, -cmd-1)
else
cs_parts[#cs_parts+1] = string.char(cmd)
end
end
end
for _, args in ipairs(cs) do if args then add(table.unpack(args)) end end
return table.concat(cs_parts)
end
return function(cs, upem, raw)
if not raw then optimizet2(cs) end
return convert_cs(cs, upem or 1000)
end

146
luametalatex-font-tfm.lua Normal file
View File

@ -0,0 +1,146 @@
local upper_mask = (1<<20)-1<<44
local shifted_sign = 1<<43
local function scale(factor1, factor2)
local result = factor1*factor2 >> 20
if result & shifted_sign == shifted_sign then
return result | upper_mask
else
return result
end
end
local function read_scaled(buf, i, count, factor)
local result = {}
for j = 1, count do
result[j] = scale(factor, string.unpack(">i4", buf, i + (j-1)*4))
end
return result, i + count * 4
end
local function parse_tfm(buf, i, size)
local lf, lh, bc, ec, nw, nh, nd, ni, nl, nk, ne, np
lf, lh, bc, ec, nw, nh, nd, ni, nl, nk, ne, np, i =
string.unpack(">HHHHHHHHHHHH", buf, i)
assert(bc-1 <= ec and ec <= 255)
assert(lf == 6 + lh + (ec - bc + 1) + nw + nh + nd + ni + nl + nk + ne + np)
assert(lh >= 2)
local checksum, designsize
checksum, designsize = string.unpack(">I4i4", buf, i)
i = i + 4*lh
designsize = designsize>>4 -- Adjust TFM sizes to sp
if size < 0 then
size = math.floor(-size*designsize/1000+.5)
end
-- In contrast to TeX, we will assume that multiplication of two 32 bit
-- integers never overflows. This is safe if Lua integers have 64 bit,
-- which is the default.
local ligatureoffset, r_boundary
local widths, heights, depths, italics, kerns, parameters
local extensibles = {}
do
local i = i + (ec - bc + 1) * 4
widths, i = read_scaled(buf, i, nw, size)
heights, i = read_scaled(buf, i, nh, size)
depths, i = read_scaled(buf, i, nd, size)
italics, i = read_scaled(buf, i, ni, size)
for k,v in ipairs(italics) do if v == 0 then italics[k] = nil end end
ligatureoffset = i
if string.byte(buf, i, i) > 128 then
r_boundary = string.byte(buf, i+1, i+1)
end
i = i + nl * 4
kerns, i = read_scaled(buf, i, nk, size)
for j = 1, ne do
local ext = {}
ext.top, ext.mid, ext.bot, ext.rep, i = string.unpack("BBBB", buf, i)
for k,v in pairs(ext) do
if v == 0 then ext[k] = nil end
end
extensibles[j] = ext
end
local slant = string.unpack(">i4", buf, i) >> 4
parameters = read_scaled(buf, i, np, size)
parameters[1] = slant
end
local characters = {}
for cc = bc,ec do
local charinfo
charinfo, i = string.unpack(">I4", buf, i)
if (charinfo >> 24) & 0xFF ~= 0 then
local char = {
width = widths[((charinfo >> 24) & 0xFF) + 1],
height = heights[((charinfo >> 20) & 0xF) + 1],
depth = depths[((charinfo >> 16) & 0xF) + 1],
italic = italics[((charinfo >> 10) & 0xF) + 1],
}
local tag = (charinfo >> 8) & 0x3
if tag == 0 then
elseif tag == 1 then
local offset = (charinfo & 0xFF) * 4 + ligatureoffset
if string.byte(buf, offset, offset) > 128 then
offset = string.unpack(">H", buf, offset + 2)
end
char.kerns, char.ligatures = {}, {}
local done = {}
repeat
local skip, next, op, rem
skip, next, op, rem, offset = string.unpack("BBBB", buf, offset)
if skip > 128 then break end
if next == r_boundary then next = "right_boundary" end
if not done[next] then
done[next] = true
if op >= 128 then
char.kerns[next] = kerns[(op - 128 << 8) + rem + 1]
else
char.ligatures[next] = {
type = op,
char = rem,
}
end
end
offset = offset + 4*skip
until skip == 128
if not next(char.kerns) then char.kerns = nil end
if not next(char.ligatures) then char.ligatures = nil end
elseif tag == 2 then
char.next = charinfo & 0xFF
elseif tag == 3 then
char.extensible = extensibles[(charinfo & 0xFF) + 1]
end
characters[cc] = char
end
end
return {
checksum = checksum,
direction = 0,
embedding = "unknown",
-- encodingbytes = 0,
extend = 1000,
format = "unknown",
identity = "unknown",
mode = 0,
slant = 0,
squeeze = 1000,
oldmath = false,
streamprovider = 0,
tounicode = 0,
type = "unknown",
units_per_em = 0,
used = false,
width = 0,
writingmode = "unknown",
size = size,
designsize = designsize,
parameters = parameters,
characters = characters,
}
end
local basename = ((1-lpeg.S'\\/')^0*lpeg.S'\\/')^0*lpeg.C((1-lpeg.P'.tfm'*-1)^0)
return function(name, size)
local filename = kpse.find_file(name, 'tfm', true)
local f = io.open(filename)
if not f then return end
local buf = f:read'*a'
f:close()
local result = parse_tfm(buf, 1, size)
result.name = basename:match(name)
return result
end

176
luametalatex-font-vf.lua Normal file
View File

@ -0,0 +1,176 @@
local fontcmds = {
[243] = ">I1I4I4I4BB",
[244] = ">I2I4I4I4BB",
[245] = ">I3I4I4I4BB",
[246] = ">I4I4I4I4BB",
}
local function read_fonts(buf, i, fonts, size)
local cmd = fontcmds[string.byte(buf, i)]
if not cmd then return i end
local fid, check, scale, designsize, arealen, namelen, i =
string.unpack(cmd, buf, i + 1)
local fsize = size * scale >> 20
if fonts[fid] then error[[font number reused in VF file]] end
fonts[fid] = {
area = string.sub(buf, i, i+arealen-1),
name = string.sub(buf, i+arealen, i+arealen+namelen-1),
size = fsize,
designsize = designsize >> 4,
checksum = check,
}
return read_fonts(buf, i+arealen+namelen, fonts, size)
end
local Cmds = {
[1] = ">I1",
[2] = ">I2",
[3] = ">I3",
[4] = ">I4",
}
local cmds = {
[1] = ">i1",
[2] = ">i2",
[3] = ">i3",
[4] = ">i4",
}
local xxx = {
[239] = ">s1",
[240] = ">s2",
[241] = ">s3",
[242] = ">s4",
}
local function read_chars(buf, i, characters, size)
local cmd = string.byte(buf, i)
if cmd > 242 then return i end
local code, tfmwidth
if cmd == 242 then
cmd, code, tfmwidth, i = string.unpack(">I4I4I4", buf, i + 1)
else
code, tfmwidth, i = string.unpack(">BI3", buf, i + 1)
end
local commands = {}
local character = {
width = tfmwidth, -- Unscaled for compatibility with LuaTeX
commands = commands,
}
characters[code] = character
local after = i + cmd
local w, x, y, z, stack = 0, 0, 0, 0, {}
while i < after do
local cmd = string.byte(buf, i)
if cmd <= 131 then
if cmd >= 128 then
cmd, i = string.unpack(Cmds[cmd-127], buf, i + 1)
else
i = i + 1
end
commands[#commands + 1] = { "char", cmd }
elseif cmd == 132 then
local height, width
height, width, i = string.unpack(">I4I4", buf, i + 1)
commands[#commands + 1] =
{ "rule", height * size >> 20, width * size >> 20 }
elseif cmd <= 136 then
cmd, i = string.unpack(Cmds[cmd-132], buf, i + 1)
commands[#commands + 1] = { "push" }
commands[#commands + 1] = { "char", cmd }
commands[#commands + 1] = { "pop" }
elseif cmd == 137 then
local height, width
height, width, i = string.unpack(">I4I4", buf, i + 1)
commands[#commands + 1] = { "push" }
commands[#commands + 1] =
{ "rule", height * size >> 20, width * size >> 20 }
commands[#commands + 1] = { "pop" }
elseif cmd == 138 then -- NOP
i = i + 1
elseif cmd <= 140 then
error[[Invalid command in packet]]
elseif cmd == 141 then
stack[#stack+1] = {w, x, y, z}
commands[#commands + 1] = { "push" }
i = i + 1
elseif cmd == 142 then
local top = stack[#stack]
if not top then error[[Attempt to pop with empty stack]] end
stack[#stack] = nil
w, x, y, z = top[1], top[2], top[3], top[4]
commands[#commands + 1] = { "pop" }
i = i + 1
elseif cmd <= 146 then
cmd, i = string.unpack(cmds[cmd-142], buf, i + 1)
commands[#commands + 1] = { "right", (cmd * size >> 20) | (cmd < 0 and 0xFFFFFFFF00000000 or 0) }
elseif cmd == 147 then
commands[#commands + 1] = { "right", w }
i = i + 1
elseif cmd <= 151 then
cmd, i = string.unpack(cmds[cmd-147], buf, i + 1)
w = (cmd * size >> 20) | (cmd < 0 and 0xFFFFFFFF00000000 or 0)
commands[#commands + 1] = { "right", w }
elseif cmd == 152 then
commands[#commands + 1] = { "right", x }
i = i + 1
elseif cmd <= 156 then
cmd, i = string.unpack(cmds[cmd-152], buf, i + 1)
x = (cmd * size >> 20) | (cmd < 0 and 0xFFFFFFFF00000000 or 0)
commands[#commands + 1] = { "right", x }
elseif cmd <= 160 then
cmd, i = string.unpack(cmds[cmd-156], buf, i + 1)
commands[#commands + 1] = { "down", (cmd * size >> 20) | (cmd < 0 and 0xFFFFFFFF00000000 or 0) }
elseif cmd == 161 then
commands[#commands + 1] = { "down", y }
i = i + 1
elseif cmd <= 165 then
cmd, i = string.unpack(cmds[cmd-161], buf, i + 1)
y = (cmd * size >> 20) | (cmd < 0 and 0xFFFFFFFF00000000 or 0)
commands[#commands + 1] = { "down", y }
elseif cmd == 166 then
commands[#commands + 1] = { "down", z }
i = i + 1
elseif cmd <= 170 then
cmd, i = string.unpack(cmds[cmd-166], buf, i + 1)
z = (cmd * size >> 20) | (cmd < 0 and 0xFFFFFFFF00000000 or 0)
commands[#commands + 1] = { "down", z }
elseif cmd <= 238 then
if cmd >= 235 then
cmd, i = string.unpack(Cmds[cmd-234], buf, i + 1)
else
i = i + 1
end
commands[#commands + 1] = { "font", cmd }
elseif xxx[cmd] then
cmd, i = string.unpack(xxx[cmd], buf, i + 1)
commands[#commands + 1] = { "special", cmd }
else
error[[Invalid command in packet]]
end
end
if i > after then error[[Ill-formed packet]] end
return read_chars(buf, after, characters, size)
end
local function parse_vf(buf, i, size)
local font = {}
local magic, designsize
magic, font.header, font.checksum, designsize, i =
string.unpack(">Hs1I4I4", buf, i)
if magic ~= 247*256+202 then error[[Not a VF file]] end
font.designsize = designsize >> 4
local fonts, characters = {}, {}
font.fonts, font.characters = fonts, characters
i = read_fonts(buf, i, fonts, size)
i = read_chars(buf, i, characters, size)
print(require'inspect'(font))
end
local basename = ((1-lpeg.S'\\/')^0*lpeg.S'\\/')^0*lpeg.C((1-lpeg.P'.tfm'*-1)^0)
return function(name, size, must_exist)
local filename = kpse.find_file(name, 'vf', must_exist)
local f = io.open(filename)
if not f then return end
local buf = f:read'*a'
f:close()
local result = parse_tfm(buf, 1, size)
result.name = basename:match(name)
return result
end

399
luametalatex-init.lua Normal file
View File

@ -0,0 +1,399 @@
do
ffi.cdef[[
typedef enum
{
kpse_gf_format,
kpse_pk_format,
kpse_any_glyph_format,
kpse_tfm_format,
kpse_afm_format,
kpse_base_format,
kpse_bib_format,
kpse_bst_format,
kpse_cnf_format,
kpse_db_format,
kpse_fmt_format,
kpse_fontmap_format,
kpse_mem_format,
kpse_mf_format,
kpse_mfpool_format,
kpse_mft_format,
kpse_mp_format,
kpse_mppool_format,
kpse_mpsupport_format,
kpse_ocp_format,
kpse_ofm_format,
kpse_opl_format,
kpse_otp_format,
kpse_ovf_format,
kpse_ovp_format,
kpse_pict_format,
kpse_tex_format,
kpse_texdoc_format,
kpse_texpool_format,
kpse_texsource_format,
kpse_tex_ps_header_format,
kpse_troff_font_format,
kpse_type1_format,
kpse_vf_format,
kpse_dvips_config_format,
kpse_ist_format,
kpse_truetype_format,
kpse_type42_format,
kpse_web2c_format,
kpse_program_text_format,
kpse_program_binary_format,
kpse_miscfonts_format,
kpse_web_format,
kpse_cweb_format,
kpse_enc_format,
kpse_cmap_format,
kpse_sfd_format,
kpse_opentype_format,
kpse_pdftex_config_format,
kpse_lig_format,
kpse_texmfscripts_format,
kpse_lua_format,
kpse_fea_format,
kpse_cid_format,
kpse_mlbib_format,
kpse_mlbst_format,
kpse_clua_format,
kpse_ris_format,
kpse_bltxml_format,
kpse_last_format /* one past last index */
} kpse_file_format_type;
typedef enum {
kpse_glyph_source_normal,
kpse_glyph_source_alias,
kpse_glyph_source_maketex,
kpse_glyph_source_fallback_res,
kpse_glyph_source_fallback
} kpse_glyph_source_type;
typedef enum {
kpse_src_implicit,
kpse_src_compile,
kpse_src_texmf_cnf,
kpse_src_client_cnf,
kpse_src_env,
kpse_src_x,
kpse_src_cmdline
} kpse_src_type;
typedef struct {
const char *name;
unsigned dpi;
kpse_file_format_type format;
kpse_glyph_source_type source;
} kpse_glyph_file_type;
void *kpathsea_new(void);
void kpathsea_set_program_name(void*, const char*, const char*);
void kpathsea_init_prog(void*, const char*, unsigned, const char *, const char *);
const char *kpathsea_find_file(void*, const char *, kpse_file_format_type, int);
const char *kpathsea_find_glyph(void*, const char *, unsigned, kpse_file_format_type, kpse_glyph_file_type*);
const char *kpathsea_brace_expand(void*, const char *);
const char *kpathsea_path_expand(void*, const char *);
const char *kpathsea_var_expand(void*, const char *);
const char *kpathsea_var_value(void*, const char *);
void kpathsea_set_program_enabled (void *, kpse_file_format_type, int, kpse_src_type);
int kpathsea_in_name_ok(void*, const char *);
int kpathsea_out_name_ok(void*, const char *);
void kpathsea_finish(void*);
const char *kpathsea_version_string;
]]
local kpse_glyph_file_type = ffi.typeof'kpse_glyph_file_type'
local type_remap = {
-- These are the command line/LuaTeX names
gf = "kpse_gf_format",
pk = "kpse_pk_format",
["bitmap font"] = "kpse_any_glyph_format",
tfm = "kpse_tfm_format",
afm = "kpse_afm_format",
base = "kpse_base_format",
bib = "kpse_bib_format",
bst = "kpse_bst_format",
cnf = "kpse_cnf_format",
["ls-R"] = "kpse_db_format",
fmt = "kpse_fmt_format",
map = "kpse_fontmap_format",
mem = "kpse_mem_format",
mf = "kpse_mf_format",
mfpool = "kpse_mfpool_format",
mft = "kpse_mft_format",
mp = "kpse_mp_format",
mppool = "kpse_mppool_format",
["MetaPost support"] = "kpse_mpsupport_format",
ocp = "kpse_ocp_format",
ofm = "kpse_ofm_format",
opl = "kpse_opl_format",
otp = "kpse_otp_format",
ovf = "kpse_ovf_format",
ovp = "kpse_ovp_format",
["graphic/figure"] = "kpse_pict_format",
tex = "kpse_tex_format",
["TeX system documentation"] = "kpse_texdoc_format",
texpool = "kpse_texpool_format",
["TeX system sources"] = "kpse_texsource_format",
["PostScript header"] = "kpse_tex_ps_header_format",
["Troff fonts"] = "kpse_troff_font_format",
["type1 fonts"] = "kpse_type1_format",
vf = "kpse_vf_format",
["dvips config"] = "kpse_dvips_config_format",
ist = "kpse_ist_format",
["truetype fonts"] = "kpse_truetype_format",
["type42 fonts"] = "kpse_type42_format",
["web2c files"] = "kpse_web2c_format",
["other text files"] = "kpse_program_text_format",
["other binary files"] = "kpse_program_binary_format",
["mics fonts"] = "kpse_miscfonts_format",
web = "kpse_web_format",
cweb = "kpse_cweb_format",
["enc files"] = "kpse_enc_format",
["cmap files"] = "kpse_cmap_format",
["subfont definition files"] = "kpse_sfd_format",
["opentype fonts"] = "kpse_opentype_format",
["pdftex config"] = "kpse_pdftex_config_format",
["lig files"] = "kpse_lig_format",
texmfscripts = "kpse_texmfscripts_format",
lua = "kpse_lua_format",
["font feature files"] = "kpse_fea_format",
["cid maps"] = "kpse_cid_format",
mlbib = "kpse_mlbib_format",
mlbst = "kpse_mlbst_format",
clua = "kpse_clua_format",
ris = "kpse_ris_format",
bltxml = "kpse_bltxml_format",
-- Some additional aliases to make naming more consistant
any_glyph = "kpse_any_glyph_format",
db = "kpse_db_format",
fontmap = "kpse_fontmap_format",
mpsupport = "kpse_mpsupport_format",
pict = "kpse_pict_format",
texdoc = "kpse_texdoc_format",
texsource = "kpse_texsource_format",
tex_ps_header = "kpse_tex_ps_header_format",
troff_font = "kpse_troff_font_format",
type1 = "kpse_type1_format",
dvips_config = "kpse_dvips_config_format",
truetype = "kpse_truetype_format",
type42 = "kpse_type42_format",
web2c = "kpse_web2c_format",
program_text = "kpse_program_text_format",
program_binary = "kpse_program_binary_format",
miscfonts = "kpse_miscfonts_format",
enc = "kpse_enc_format",
cmap = "kpse_cmap_format",
sfd = "kpse_sfd_format",
opentype = "kpse_opentype_format",
pdftex_config = "kpse_pdftex_config_format",
lig = "kpse_lig_format",
fea = "kpse_fea_format",
cid = "kpse_cid_format",
-- And some other aliases
eps = "kpse_pict_format",
pfb = "kpse_type1_format",
ttc = "kpse_truetype_format",
ttf = "kpse_truetype_format",
otf = "kpse_opentype_format",
text = "kpse_program_text_format",
binary = "kpse_program_binary_format",
}
local kpselib = ffi.load("kpathsea")
local realarg0
if arg[1]:sub(1,7) == "--arg0=" then
realarg0 = arg[1]:sub(8)
else
local i = 0
while arg[i] do
realarg0 = arg[i]
i = i-1
end
end
local file_format = ffi.typeof'kpse_file_format_type'
local NULL = ffi.new("const char*", nil)
local function get_string(s) return s ~= NULL and ffi.string(s) end
local function set_program_name (t, arg0, progname)
kpselib.kpathsea_set_program_name(t.cdata, arg0 or realarg0, progname)
return t
end
local methods = {
init_prog = function(t, prefix, dpi, mode, fallback)
kpselib.kpathsea_init_prog(t.cdata, prefix, dpi, mode, fallback)
end,
find_file = function(t, name, ...)
local ftype, must_exist = "kpse_tex_format", 0
for i=select('#', ...),1,-1 do
local arg = select(i, ...)
local argtype = type(arg)
if argtype == "string" then
ftype = arg
elseif argtype == "number" then
must_exist = arg
elseif argtype == "boolean" then
must_exist = arg and 1 or 0
end
end
ftype = type_remap[ftype] or ftype
if ftype == "kpse_gf_format" or ftype == "kpse_pk_format"
or ftype == "kpse_any_glyph_format" then
local glyph_file = kpse_glyph_file_type()
local res = kpselib.kpathsea_find_glyph(t.cdata, name, must_exist, ftype, glyph_file)
if res ~= NULL then
return ffi.string(res), ffi.string(glyph_file.name), glyph_file.dpi, glyph_file.format, glyph_file.source
end
else
return get_string(kpselib.kpathsea_find_file(t.cdata, name, ftype, must_exist > 0 and 1 or 0))
end
end,
-- show_path = function(t, ftype)
-- error [[Not yet implemented]]
-- end,
expand_braces = function(t, path)
return get_string(kpselib.kpathsea_brace_expand(t.cdata, path))
end,
expand_path = function(t, path)
return get_string(kpselib.kpathsea_path_expand(t.cdata, path))
end,
expand_var = function(t, var)
return get_string(kpselib.kpathsea_var_expand(t.cdata, var))
end,
var_value = function(t, var)
return get_string(kpselib.kpathsea_var_value(t.cdata, var))
end,
set_maketex = function(t, ftype, value, src)
ftype = type_remap[ftype] or ftype
kpselib.kpathsea_set_program_enabled(t.cdata, ftype, value and 1 or 0, src or "kpse_src_cmdline")
end,
finish = function(t)
if t.cdata then
kpselib.kpathsea_finish(t.cdata)
end
end,
}
local meta = {
__index = methods,
__gc = methods.finish,
}
local global_kpse = setmetatable({cdata = kpselib.kpathsea_new()}, meta)
kpse = {
set_program_name = function(...)
set_program_name(global_kpse, ...)
end,
set_maketex = function(...)
return global_kpse:set_maketex(...)
end,
init_prog = function(...)
return global_kpse:init_prog(...)
end,
new = function(...)
return set_program_name(setmetatable({cdata = kpselib.kpathsea_new()}, meta), ...)
end,
expand_braces = function(...)
return global_kpse:expand_braces(...)
end,
expand_path = function(...)
return global_kpse:expand_path(...)
end,
expand_var = function(...)
return global_kpse:expand_var(...)
end,
var_value = function(...)
return global_kpse:var_value(...)
end,
find_file = function(...)
return global_kpse:find_file(...)
end,
version = function()
return ffi.string(kpselib.kpathsea_version_string)
end,
}
end
--
-- unicode = {utf8 = utf8}
-- utf8.byte = utf8.codepoint
kpse.set_program_name()
package.searchers[2] = function(modname)
local filename = kpse.find_file(modname, "kpse_lua_format", true)
if not filename then
return string.format("\n\tno file located through kpse for %s", modname)
end
local mod, msg = loadfile(filename)
if msg then
error(string.format("error loading module '%s' from file '%s':\n\t%s", modname, filename, msg))
end
return mod, filename
end
kpse.set_maketex("kpse_fmt_format", true)
bit32 = require'luametalatex-bit32'
kpse.init_prog("LUATEX", 400, "nexthi", nil)
status.init_kpse = 1
status.safer_option = 0
local read_tfm = require'luametalatex-font-tfm'
read_vf = require'luametalatex-font-vf'
font.read_tfm = read_tfm
font.read_vf = read_vf
local reserved_ids = -1
font.fonts = {}
function font.getfont(id)
return font.fonts[id]
end
pdf = {
getfontname = function(id) -- No font sharing
return id
end,
}
callback.register('define_font', function(name, size)
if status.ini_version then
reserved_ids = font.nextid()-1
lua.prepared_code[#lua.prepared_code+1] = string.format("font.define(%i, font.read_tfm(%q, %i))", reserved_ids, name, size)
end
local f = read_tfm(name, size)
font.fonts[font.nextid()-1] = f
return f
end)
local olddefinefont = font.define
function font.define(i, f)
if not f then
f = i
i = font.nextid(true)
end
font.fonts[i] = f
return olddefinefont(i, f)
end
-- do
-- local register = callback.register
-- function callback.register(...)
-- print('callback.register', ...)
-- return register(...)
-- end
-- end
callback.register('find_log_file', function(name) return name end)
callback.register('find_data_file', function(name) return kpse.find_file(name, 'kpse_tex_format', true) end)
callback.register('find_format_file', function(name) return kpse.find_file(name, 'kpse_fmt_format', true) end)
callback.register('show_warning_message', function()
texio.write_nl('WARNING Tag: ' .. status.lastwarningtag)
texio.write_nl(status.lastwarningstring)
end)
callback.register('show_error_message', function()
if status.lasterrorcontext then
texio.write_nl('ERROR Context: ' .. status.lasterrorcontext)
end
texio.write_nl(status.lasterrorstring)
end)
callback.register('pre_dump', function()
-- for k,v in pairs(callback.list()) do print('CB', k,v) end
lua.bytecode[1], msg = load("do local id "
.. "repeat id = font.nextid(true) "
.. "until id == " .. reserved_ids
.. " end "
.. table.concat(lua.prepared_code, ' '))
end)
if status.ini_version then
lua.prepared_code = {}
local code = package.searchers[2]('luametalatex-firstcode')
if type(code) == "string" then error(string.format("Initialization code not found %s", code)) end
lua.bytecode[2] = code
end

528
luametalatex-nodewriter.lua Normal file
View File

@ -0,0 +1,528 @@
local format = string.format
local concat = table.concat
local write = texio.write_nl
local properties = node.get_properties_table()
local function doublekeyed(t, id2name, name2id, index)
return setmetatable(t, {
__index = index,
__newindex = function(t, k, v)
rawset(t, k, v)
if type(k) == 'string' then
rawset(t, name2id(k), v)
else
rawset(t, id2name(k), v)
end
end,
})
end
local nodehandler = (function()
local function unknown_handler(_, n, x, y)
write(format("Sorry, but the PDF backend does not support %q (id = %i) nodes right now. The supplied node will be dropped at coordinates (%i, %i).", node.type(n.id), n.id, x, y))
end
return doublekeyed({}, node.type, node.id, function()
return unknown_handler
end)
end)()
local whatsithandler = (function()
local whatsits = node.whatsits()
local function unknown_handler(p, n, x, y, ...)
local prop = properties[n]
if prop and prop.handle then
prop:handle(p, n, x, y, ...)
else
write(format("Sorry, but the PDF backend does not support %q (id = %i) whatsits right now. The supplied node will be dropped at coordinates (%i, %i).", whatsits[n.subtype], n.subtype, x, y))
end
end
return doublekeyed({}, function(n)return whatsits[n]end, function(n)return whatsits[n]end, function()
return unknown_handler
end)
end)()
local glyph, text, page, cm_pending = 1, 2, 3, 4
local gsub = string.gsub
local function projected_point(m, x, y, w)
w = w or 1
return x*m[1] + y*m[3] + w*m[5], x*m[2] + y*m[4] + w*m[6]
end
local function pdfsave(p, x, y)
local lastmatrix = p.matrix
p.matrix = {[0] = lastmatrix, table.unpack(lastmatrix)}
end
local function pdfmatrix(p, a, b, c, d, e, f)
local m = p.matrix
a, b = projected_point(m, a, b, 0)
c, d = projected_point(m, c, d, 0)
e, f = projected_point(m, e, f, 1)
m[1], m[2], m[3], m[4], m[5], m[6] = a, b, c, d, e, f
end
local function pdfrestore(p, x, y)
-- TODO: Check x, y
p.matrix = p.matrix[0]
end
local function sp2bp(sp)
return sp/65781.76
end
local topage
local function totext(p, fid)
local last = p.mode
if last == glyph then
p.pending[#p.pending+1] = ")]TJ"
p.strings[#p.strings+1] = concat(p.pending)
for i=1,#p.pending do p.pending[i] = nil end
last = text
end
if last == cm_pending then topage(p) end
p.mode = text
if last == text and p.font.fid == fid then return end
local f = font.getfont(fid) or font.fonts[fid]
if last ~= text then p.strings[#p.strings+1] = "BT" p.pos.lx, p.pos.ly, p.pos.x, p.pos.y, p.font.exfactor = 0, 0, 0, 0, 0 end
p:fontprovider(f, fid)
-- p.strings[#p.strings+1] = format("/F%i %f Tf 0 Tr", f.parent, sp2bp(f.size)) -- TODO: Setting the mode, expansion, etc.
p.font.fid = fid
p.font.font = f
return false -- Return true if we need a new textmatrix
end
local inspect = require'inspect'
local function show(t) print(inspect(t)) end
local function topage(p)
local last = p.mode
if last == page then return end
if last <= text then
totext(p, p.font.fid) -- First make sure we are really in text mode
p.strings[#p.strings+1] = "ET"
elseif last == cm_pending then
local pending = p.pending_matrix
if pending[1] ~= 1 or pending[2] ~= 0 or pending[3] ~= 0 or pending[4] ~= 1 or pending[5] ~= 0 or pending[6] ~= 0 then
p.strings[#p.strings+1] = format("%f %f %f %f %f %f cm", pending[1], pending[2], pending[3], pending[4], sp2bp(pending[5]), sp2bp(pending[6]))
end
else
error[[Unknown mode]]
end
p.mode = page
end
local function toglyph(p, fid, x, y, exfactor)
local last = p.mode
if last == glyph and p.font.fid == fid and p.pos.y == y and p.font.exfactor == exfactor then
if x == p.pos.x then return end
local xoffset = (x - p.pos.x)/p.font.font.size * 1000 / (1+exfactor/1000000)
if math.abs(xoffset) < 1000000 then -- 1000000 is arbitrary
p.pending[#p.pending+1] = format(")%i(", math.floor(-xoffset))
p.pos.x = x
return
end
end
if totext(p, fid) or exfactor ~= p.font.exfactor then
p.font.exfactor = exfactor
p.strings[#p.strings+1] = gsub(format("%f 0.0 %f %f %f %f Tm", 1+exfactor/1000000, 0, 1, sp2bp(x), sp2bp(y)), '%.?0+ ', ' ')
else
p.strings[#p.strings+1] = gsub(format("%f %f Td", sp2bp((x - p.pos.lx)/(1+exfactor/1000000)), sp2bp(y - p.pos.ly)), '%.?0+ ', ' ')
end
p.pos.lx, p.pos.ly, p.pos.x, p.pos.y = x, y, x, y
p.mode = glyph
p.pending[1] = "[("
end
local function write_matrix(p, a, b, c, d, e, f)
local pending = p.pending_matrix
if p.mode ~= cm_pending then
topage(p)
p.mode = cm_pending
pending[1], pending[2], pending[3], pending[4], pending[5], pending[6] = a, b, c, d, e, f
return
else
a, b = projected_point(pending, a, b, 0)
c, d = projected_point(pending, c, d, 0)
e, f = projected_point(pending, e, f, 1)
pending[1], pending[2], pending[3], pending[4], pending[5], pending[6] = a, b, c, d, e, f
return
end
end
local linkcontext = {}
-- local function get_action_attr(p, action)
-- if action.action_type == 3 then
-- return action.data
-- elseif action.action_type == 2 then
local function write_link(p, link)
local quads = link.quads
local minX, maxX, minY, maxY = math.huge, -math.huge, math.huge, -math.huge
assert(link.action.action_type == 3) -- TODO: Other types
local attr = link.attr .. link.action.data
assert(#quads%8==0)
local quadStr = {}
for i=1,#quads,8 do
local x1, y1, x4, y4, x2, y2, x3, y3 = table.unpack(quads, i, i+7)
x1, y1, x2, y2, x3, y3, x4, y4 = sp2bp(x1), sp2bp(y1), sp2bp(x2), sp2bp(y2), sp2bp(x3), sp2bp(y3), sp2bp(x4), sp2bp(y4)
quadStr[i//8+1] = string.format("%f %f %f %f %f %f %f %f", x1, y1, x2, y2, x3, y3, x4, y4)
minX = math.min(minX, x1, x2, x3, x4)
minY = math.min(minY, y1, y2, y3, y4)
maxX = math.max(maxX, x1, x2, x3, x4)
maxY = math.max(maxY, y1, y2, y3, y4)
end
p.file:indirect(link.objnum, string.format("<</Rect[%f %f %f %f]/QuadPoints[%s]%s>>", minX-.2, minY-.2, maxX+.2, maxY+.2, table.concat(quadStr, ' '), attr))
for i=1,#quads do quads[i] = nil end
link.objnum = nil
end
local function addlinkpoint(p, link, x, y, list, final)
local quads = link.quads
print'addlink'
if link.annots and link.annots ~= p.annots then -- We started on another page, let's finish that before starting the new page
write_link(p, link)
link.annots = nil
end
if not link.annots then
link.annots = p.annots -- Just a marker to indicate the page
link.objnum = link.objnum or p.file:getobj()
p.annots[#p.annots+1] = link.objnum .. " 0 R"
end
local m = p.matrix
local lx, ly = projected_point(m, x, y-(link.depth or list.depth))
local ux, uy = projected_point(m, x, y+(link.height or list.height))
local n = #quads
quads[n+1], quads[n+2], quads[n+3], quads[n+4] = lx, ly, ux, uy
if final or (link.force_separate and (n+4)%8 == 0) then
print(final, n, link.force_separate)
write_link(p, link)
link.annots = nil
end
end
function nodehandler.hlist(p, list, x0, y, outerlist, origin, level)
if outerlist then
if outerlist.id == 0 then
y = y - list.shift
else
x0 = x0 + list.shift
end
end
local x = x0
for _,l in ipairs(p.linkcontext) do if l.level == level+1 then
addlinkpoint(p, l, x, y, list)
end end
for n in node.traverse(list.head) do
local next = n.next
local w = next and node.rangedimensions(list, n, next) or node.rangedimensions(list, n)
nodehandler[n.id](p, n, x, y, list, x0, level+1)
x = w + x
end
for _,l in ipairs(p.linkcontext) do if l.level == level+1 then
addlinkpoint(p, l, x, y, list)
end end
end
function nodehandler.vlist(p, list, x, y0, outerlist, origin, level)
if outerlist then
if outerlist.id == 0 then
y0 = y0 - list.shift
else
x = x + list.shift
end
end
y0 = y0 + list.height
local y = y0
for n in node.traverse(list.head) do
local d, h, _ = 0, node.effective_glue(n, list) or math.tointeger(n.kern)
if not h then
_, h, d = node.direct.getwhd(node.direct.todirect(n))
end
y = y - (h or 0)
nodehandler[n.id](p, n, x, y, list, y0, level+1)
y = y - (d or 0)
end
end
function nodehandler.rule(p, n, x, y, outer)
if n.width == -1073741824 then n.width = outer.width end
if n.height == -1073741824 then n.height = outer.height end
if n.depth == -1073741824 then n.depth = outer.depth end
local sub = n.subtype
if sub == 1 then
error[[We can't handle boxes yet]]
elseif sub == 2 then
error[[We can't handle images yet]]
elseif sub == 3 then
elseif sub == 4 then
error[[We can't handle user rules yet]]
elseif sub == 9 then
error[[We can't handle outline rules yet]]
else
if n.width <= 0 or n.depth + n.height <= 0 then return end
topage(p)
p.strings[#p.strings+1] = gsub(format("%f %f %f %f re f", sp2bp(x), sp2bp(y - n.depth), sp2bp(n.width), sp2bp(n.depth + n.height)), '%.?0+ ', ' ')
end
end
function nodehandler.boundary() end
function nodehandler.disc(p, n, x, y, list, ...) -- FIXME: I am not sure why this can happen, let's assume we can use .replace
for n in node.traverse(n.replace) do
local next = n.next
local w = next and node.rangedimensions(list, n, next) or node.rangedimensions(list, n)
nodehandler[n.id](p, n, x, y, list, ...)
x = w + x
end
end
function nodehandler.local_par() end
function nodehandler.math() end
function nodehandler.glue(p, n, x, y, outer, origin, level) -- Naturally this is an interesting one.
local subtype = n.subtype
if subtype < 100 then return end -- We only really care about leaders
local leader = n.leader
local w = node.effective_glue(n, outer)
if leader.id == 2 then -- We got a rule, this should be easy
if outer.id == 0 then
leader.width = w
else
leader.height = w
leader.depth = 0
end
return nodehandler.rule(p, leader, x, y, outer)
end
local lwidth = outer.id == 0 and leader.width or leader.height + leader.depth
if outer.id ~= 0 then
y = y + w
end
if subtype == 100 then
if outer.id == 0 then
local newx = ((x-origin - 1)//lwidth + 1) * lwidth + origin
-- local newx = -(origin-x)//lwidth * lwidth + origin
w = w + x - newx
x = newx
else
-- local newy = -(origin-y)//lwidth * lwidth + origin
local newy = (y-origin)//lwidth * lwidth + origin
w = w + newy - y
y = newy
end
elseif subtype == 101 then
local inner = w - (w // lwidth) * lwidth
if outer.id == 0 then
x = x + inner/2
else
y = y - inner/2
end
elseif subtype == 102 then
local count = w // lwidth
local skip = (w - count * lwidth) / (count + 1)
if outer.id == 0 then
x = x + skip
else
y = y - skip
end
lwidth = lwidth + skip
elseif subtype == 103 then
if outer.id == 0 then
local newx = ((x - 1)//lwidth + 1) * lwidth
w = w + x - newx
x = newx
else
local newy = y//lwidth * lwidth
w = w + newy - y
y = newy
end
end
local handler = nodehandler[leader.id]
if outer.id == 0 then
while w >= lwidth do
handler(p, leader, x, y, outer, origin, level+1)
w = w - lwidth
x = x + lwidth
end
else
y = y - leader.height
while w >= lwidth do
handler(p, leader, x, y, outer, origin, level+1)
w = w - lwidth
y = y - lwidth
end
end
end
function nodehandler.kern() end
function nodehandler.penalty() end
local literalescape = lpeg.Cs((lpeg.S'\\()\r'/{['\\'] = '\\\\', ['('] = '\\(', [')'] = '\\)', ['\r'] = '\\r'}+1)^0)
local match = lpeg.match
local function do_commands(p, c, f, fid, x, y, outer, ...)
local fonts = f.fonts
local stack, current_font = {}, fonts[1]
for _, cmd in ipairs(c.commands) do
if cmd[1] == "node" then
local cmd = cmd[2]
nodehandler[cmd.id](p, cmd, x, y, nil, ...)
x = x + cmd.width
elseif cmd[1] == "font" then
current_font = fonts[cmd[2]]
elseif cmd[1] == "char" then
local n = node.new'glyph'
n.subtype, n.font, n.char = 256, current_font.id, cmd[2]
nodehandler.glyph(p, n, x, y, outer, ...)
node.free(n)
x = x + n.width
elseif cmd[1] == "slot" then
local n = node.new'glyph'
n.subtype, n.font, n.char = 256, cmd[2], cmd[3]
nodehandler.glyph(p, n, x, y, outer, ...)
node.free(n)
x = x + n.width
elseif cmd[1] == "rule" then
local n = node.new'rule'
n.height, n.width = cmd[2], cmd[3]
nodehandler.rule(p, n, x, y, outer, ...)
node.free(n)
x = x + n.width
elseif cmd[1] == "left" then
x = x + cmd[2]
elseif cmd[1] == "down" then
y = y + cmd[2]
elseif cmd[1] == "push" then
stack[#stack + 1] = {x, y}
elseif cmd[1] == "pop" then
local top = stack[#stack]
stack[#stack] = nil
x, y = top[1], top[2]
elseif cmd[1] == "special" then
-- ???
elseif cmd[1] == "pdf" then
-- ???
elseif cmd[1] == "lua" then
cmd = cmd[2]
if type(cmd) == "string" then cmd = load(cmd) end
assert(type(cmd) == "function")
elseif cmd[1] == "image" then
-- ???
-- else
-- NOP, comment and invalid commands ignored
end
if #commands ~= 1 then error[[Unsupported command number]] end
if commands[1][1] ~= "node" then error[[Unsupported command name]] end
commands = commands[1][2]
nodehandler[commands.id](p, commands, x, y, nil, ...)
end
end
function nodehandler.glyph(p, n, x, y, ...)
if n.font ~= p.vfont.fid then
p.vfont.fid = n.font
p.vfont.font = font.getfont(n.font) or font.fonts[n.font]
end
local f, fid = p.vfont.font, p.vfont.fid
local c = f.characters[n.char]
if not c then
error[[Invalid characters]]
end
if c.commands then return do_commands(p, c, f, fid, x, y, ...) end
toglyph(p, n.font, x + n.xoffset, y + n.yoffset, n.expansion_factor)
local index = c.index
if index then
-- if f.encodingbytes == -3 then
if true then
if index < 0x80 then
p.pending[#p.pending+1] = match(literalescape, string.pack('>B', index))
elseif index < 0x7F80 then
p.pending[#p.pending+1] = match(literalescape, string.pack('>H', index+0x7F80))
else
p.pending[#p.pending+1] = match(literalescape, string.pack('>BH', 0xFF, index-0x7F80))
end
else
p.pending[#p.pending+1] = match(literalescape, string.pack('>H', index))
end
if not p.usedglyphs[index] then
p.usedglyphs[index] = {index, math.floor(c.width * 1000 / f.size + .5), c.tounicode}
end
else
p.pending[#p.pending+1] = match(literalescape, string.char(n.char))
if not p.usedglyphs[n.char] then
p.usedglyphs[n.char] = {n.char, math.floor(c.width * 1000 / f.size + .5), c.tounicode}
end
end
p.pos.x = p.pos.x + math.floor(n.width*(1+n.expansion_factor/1000000)+.5)
end
function nodehandler.whatsit(p, n, ...) -- Whatsit?
return whatsithandler[n.subtype](p, n, ...)
end
function whatsithandler.pdf_literal(p, n, x, y)
if n.mode == 2 then
topage(p)
p.strings[#p.strings + 1] = n.data
elseif n.mode == 0 then
write_matrix(p, 1, 0, 0, 1, x, y)
topage(p)
p.strings[#p.strings + 1] = n.data
write_matrix(p, 1, 0, 0, 1, -x, -y)
else
write(format('Literal type %i', n.mode))
end
end
function whatsithandler.pdf_save(p, n, x, y, outer)
topage(p)
p.strings[#p.strings + 1] = 'q'
pdfsave(p, x, y)
end
function whatsithandler.pdf_restore(p, n, x, y, outer)
topage(p)
p.strings[#p.strings + 1] = 'Q'
pdfrestore(p, x, y)
end
local numberpattern = (lpeg.R'09'^0 * ('.' * lpeg.R'09'^0)^-1)/tonumber
local matrixpattern = numberpattern * ' ' * numberpattern * ' ' * numberpattern * ' ' * numberpattern
function whatsithandler.pdf_setmatrix(p, n, x, y, outer)
local a, b, c, d = matrixpattern:match(n.data)
local e, f = (1-a)*x-c*y, (1-d)*y-b*x
write_matrix(p, a, b, c, d, e, f)
end
function whatsithandler.pdf_start_link(p, n, x, y, outer, _, level)
local links = p.linkcontext
local link = {quads = {}, attr = n.link_attr, action = n.action, level = level, force_separate = false} -- force_separate should become an option
links[#links+1] = link
addlinkpoint(p, link, x, y, outer)
end
function whatsithandler.pdf_end_link(p, n, x, y, outer, _, level)
local links = p.linkcontext
local link = links[#links]
links[#links] = nil
if link.level ~= level then error[[Wrong link level]] end
addlinkpoint(p, link, x, y, outer, true)
end
local ondemandmeta = {
__index = function(t, k)
t[k] = {}
return t[k]
end
}
local function writeresources(p)
local resources = p.resources
local result = {"<<"}
for kind, t in pairs(resources) do if next(t) then
result[#result+1] = format("/%s<<", kind)
for name, value in pairs(t) do
result[#result+1] = format("/%s %i 0 R", name, value)
t[name] = nil
end
result[#result+1] = ">>"
end end
result[#result+1] = ">>"
return concat(result)
end
local fontnames = setmetatable({}, {__index = function(t, k) local res = format("F%i", k) t[k] = res return res end})
return function(file, n, fontdirs, usedglyphs)
setmetatable(usedglyphs, ondemandmeta)
local linkcontext = file.linkcontext
if not linkcontext then
linkcontext = {}
file.linkcontext = linkcontext
end
local p = {
file = file,
mode = 3,
strings = {},
pending = {},
pos = {},
fontprovider = function(p, f, fid)
if not f.parent then f.parent = pdf.getfontname(fid) end
p.resources.Font[fontnames[f.parent]] = fontdirs[f.parent]
p.strings[#p.strings+1] = format("/F%i %f Tf 0 Tr", f.parent, sp2bp(f.size)) -- TODO: Setting the mode, expansion, etc.
p.usedglyphs = usedglyphs[f.parent]
end,
font = {},
vfont = {},
matrix = {1, 0, 0, 1, 0, 0},
pending_matrix = {},
resources = setmetatable({}, ondemandmeta),
annots = {},
linkcontext = file.linkcontext,
}
nodehandler[n.id](p, n, 0, 0, n, nil, 0)
-- nodehandler[n.id](p, n, 0, n.depth, n)
topage(p)
return concat(p.strings, '\n'), writeresources(p), (p.annots[1] and string.format("/Annots[%s]", table.concat(p.annots, ' ')) or "")
end

View File

@ -0,0 +1,601 @@
local sfnt = require'luametalatex-font-sfnt'
local stdStrings = require'luametalatex-font-cff-data'
local offsetfmt = ">I%i"
local function parse_index(buf, i)
local count, offsize
count, offsize, i = string.unpack(">I2B", buf, i)
if count == 0 then return {}, i-1 end
local fmt = offsetfmt:format(offsize)
local offsets = {}
local dataoffset = i + offsize*count - 1
for j=1,count+1 do
offsets[j], i = string.unpack(fmt, buf, i)
end
for j=1,count+1 do
offsets[j] = offsets[j] + i - 1
end
return offsets, offsets[#offsets]
end
local real_mapping = { [0] = '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'.', 'E', 'E-', nil, '-', nil}
local function parse_real(cs, offset)
local c = cs:byte(offset)
if not c then return offset end
local c1, c2 = real_mapping[c>>4], real_mapping[c&0xF]
if not c1 or not c2 then
return c1 or offset, c1 and offset
else
return c1, c2, parse_real(cs, offset+1) --Warning: This is not a tail-call,
-- so we are affected by the stack limit. On the other hand, as long as
-- there are less than ~50 bytes we should be safe.
end
end
local function get_number(result)
if #result ~= 1 then
print(require'inspect'(result))
end
assert(#result == 1)
local num = result[1]
result[1] = nil
return num
end
local function get_bool(result)
return get_number(result) == 1
end
local function get_string(result, strings)
local sid = get_number(result)
return stdStrings[sid] or strings[sid-#stdStrings]
end
local function get_array(result)
local arr = table.move(result, 1, #result, 1, {})
for i=1,#result do result[i] = nil end
return arr
end
local function get_delta(result)
local arr = get_array(result)
local last = 0
for i=1,#arr do
arr[i] = arr[i]+last
last = arr[i]
end
return arr
end
local function get_private(result)
local arr = get_array(result)
assert(#arr == 2)
return arr
end
local function get_ros(result, strings)
local arr = get_array(result)
assert(#arr == 3)
result[1] = arr[1] arr[1] = get_string(result, strings)
result[1] = arr[2] arr[2] = get_string(result, strings)
return arr
end
local function apply_matrix(m, x, y)
return (m[1] * x + m[3] * y + m[5])*1000, (m[2] * x + m[4] * y + m[6])*1000
end
local operators = {
[0] = {'version', get_string},
{'Notice', get_string},
{'FullName', get_string},
{'FamilyName', get_string},
{'Weight', get_string},
{'FontBBox', get_array},
{'BlueValues', get_delta},
{'OtherBlues', get_delta},
{'FamilyBlues', get_delta},
{'FamilyOtherBlues', get_delta},
{'StdHW', get_number},
{'StdVW', get_number},
nil, -- 12, escape
{'UniqueID', get_number},
{'XUID', get_array},
{'charset', get_number},
{'Encoding', get_number},
{'CharStrings', get_number},
{'Private', get_private},
{'Subrs', get_number},
{'defaultWidthX', get_number},
{'nominalWidthX', get_number},
[-1] = {'Copyright', get_string},
[-2] = {'isFixedPitch', get_bool},
[-3] = {'ItalicAngle', get_number},
[-4] = {'UnderlinePosition', get_number},
[-5] = {'UnderlineThickness', get_number},
[-6] = {'PaintType', get_number},
[-7] = {'CharstringType', get_number},
[-8] = {'FontMatrix', get_array},
[-9] = {'StrokeWidth', get_number},
[-10] = {'BlueScale', get_number},
[-11] = {'BlueShift', get_number},
[-12] = {'BlueFuzz', get_number},
[-13] = {'StemSnapH', get_delta},
[-14] = {'StemSnapV', get_delta},
[-15] = {'ForceBold', get_bool},
[-18] = {'LanguageGroup', get_number},
[-19] = {'ExpansionFactor', get_number},
[-20] = {'initialRandomSeed', get_number},
[-21] = {'SyntheticBase', get_number},
[-22] = {'PostScript', get_string},
[-23] = {'BaseFontName', get_string},
[-24] = {'BaseFontBlend', get_delta},
[-31] = {'ROS', get_ros},
[-32] = {'CIDFontVersion', get_number},
[-33] = {'CIDFontRevision', get_number},
[-34] = {'CIDFontType', get_number},
[-35] = {'CIDCount', get_number},
[-36] = {'UIDBase', get_number},
[-37] = {'FDArray', get_number},
[-38] = {'FDSelect', get_number},
[-39] = {'FontName', get_string},
}
local function parse_dict(buf, i, j, strings)
result = {}
while i<=j do
local cmd = buf:byte(i)
if cmd == 29 then
result[#result+1] = string.unpack(">i4", buf:sub(i+1, i+4))
i = i+4
elseif cmd == 28 then
result[#result+1] = string.unpack(">i2", buf:sub(i+1, i+2))
i = i+2
elseif cmd >= 251 then -- Actually "and cmd ~= 255", but 255 is reserved
result[#result+1] = -((cmd-251)*256)-string.byte(buf, i+1)-108
i = i+1
elseif cmd >= 247 then
result[#result+1] = (cmd-247)*256+string.byte(buf, i+1)+108
i = i+1
elseif cmd >= 32 then
result[#result+1] = cmd-139
elseif cmd == 30 then -- 31 is reserved again
local real = {parse_real(buf, i+1)}
i = real[#real]
real[#real] = nil
result[#result+1] = tonumber(table.concat(real))
else
if cmd == 12 then
i = i+1
cmd = -buf:byte(i)-1
end
local op = operators[cmd]
if not op then error[[Unknown CFF operator]] end
result[op[1]] = op[2](result, strings)
end
i = i+1
end
return result
end
local function parse_charstring(cs, globalsubrs, subrs, result)
result = result or {{false}, stemcount = 0}
local lastresult = result[#result]
local i = 1
while i~=#cs+1 do
local cmd = cs:byte(i)
if cmd == 28 then
lastresult[#lastresult+1] = string.unpack(">i2", cs:sub(i+1, i+2))
i = i+2
elseif cmd == 255 then
lastresult[#lastresult+1] = string.unpack(">i4", cs:sub(i+1, i+4))/0x10000
i = i+4
elseif cmd >= 251 then
lastresult[#lastresult+1] = -((cmd-251)*256)-string.byte(cs, i+1)-108
i = i+1
elseif cmd >= 247 then
lastresult[#lastresult+1] = (cmd-247)*256+string.byte(cs, i+1)+108
i = i+1
elseif cmd >= 32 then
lastresult[#lastresult+1] = cmd-139
elseif cmd == 10 then
local idx = lastresult[#lastresult]+subrs.bias
local subr = subrs[idx]
subrs.used[idx] = true
lastresult[#lastresult] = nil
parse_charstring(subr, globalsubrs, subrs, result)
lastresult = result[#result]
elseif cmd == 29 then
local idx = lastresult[#lastresult]+globalsubrs.bias
local subr = globalsubrs[idx]
globalsubrs.used[idx] = true
lastresult[#lastresult] = nil
parse_charstring(subr, globalsubrs, subrs, result)
lastresult = result[#result]
elseif cmd == 11 then
break -- We do not keep subroutines, so drop returns and continue with the outer commands
elseif cmd == 12 then
i = i+1
cmd = cs:byte(i)
lastresult[1] = -cmd-1
lastresult = {false}
result[#result+1] = lastresult
elseif cmd == 19 or cmd == 20 then
if #result == 1 then
lastresult = {}
result[#result+1] = lastresult
end
lastresult[1] = cmd
local newi = i+(result.stemcount+7)//8
lastresult[2] = cs:sub(i+1, newi)
i = newi
else
if cmd == 21 and #result == 1 then
table.insert(result, 1, {false})
if #lastresult == 4 then
result[1][2] = lastresult[2]
table.remove(lastresult, 2)
end
elseif (cmd == 4 or cmd == 22) and #result == 1 then
table.insert(result, 1, {false})
if #lastresult == 3 then
result[1][2] = lastresult[2]
table.remove(lastresult, 2)
end
elseif cmd == 14 and #result == 1 then
table.insert(result, 1, {false})
if #lastresult == 2 or #lastresult == 6 then
result[1][2] = lastresult[2]
table.remove(lastresult, 2)
end
elseif cmd == 1 or cmd == 3 or cmd == 18 or cmd == 23 then
if #result == 1 then
table.insert(result, 1, {false})
if #lastresult % 2 == 0 then
result[1][2] = lastresult[2]
table.remove(lastresult, 2)
end
end
result.stemcount = result.stemcount + #lastresult//2
end
lastresult[1] = cmd
lastresult = {false}
result[#result+1] = lastresult
end
i = i+1
end
return result
end
local function parse_charset(buf, i0, offset, strings, num)
if not offset then offset = 0 end
if offset == 0 then
return ISOAdobe
elseif offset == 1 then
return Expert
elseif offset == 2 then
return ExpertSubset
else offset = i0+offset end
local format
format, offset = string.unpack(">B", buf, offset)
local charset = {[0] = 0}
if format == 0 then
for i=1,num-1 do
charset[i], offset = string.unpack(">I2", buf, offset)
end
elseif format == 1 then
local i = 1
while i < num do
local first, nLeft
first, nLeft, offset = string.unpack(">I2I1", buf, offset)
for j=0,nLeft do
charset[i+j] = first+j
end
i = i+1+nLeft
end
elseif format == 2 then
local i = 1
while i < num do
local first, nLeft
first, nLeft, offset = string.unpack(">I2I2", buf, offset)
for j=0,nLeft do
charset[i+j] = first+j
end
i = i+1+nLeft
end
else
error[[Invalid Charset format]]
end
if strings then -- We are not CID-keyed, so we should use strings instead of numbers
local string_charset = {}
for i=#charset,0,-1 do
local sid = charset[i]
charset[i] = nil
string_charset[i] = stdStrings[sid] or strings[sid-#stdStrings]
end
charset = string_charset
end
return charset
end
local function parse_encoding(buf, i0, offset, CharStrings)
if not offset then offset = 0 end
if offset == 0 then
error[[TODO]]
return "StandardEncoding"
elseif offset == 1 then
error[[TODO]]
return "ExpertEncoding"
else offset = i0+offset end
local format, num
format, num, offset = string.unpack(">BB", buf, offset)
local encoding = {}
if format == 0 then
for i=1,num do
local code
code, offset = string.unpack(">B", buf, offset)
encoding[code] = CharStrings[i]
end
elseif format == 1 then
local i = 1
while i <= num do
local first, nLeft
first, nLeft, offset = string.unpack(">BB", buf, offset)
for j=0,nLeft do
encoding[first + j] = CharStrings[i + j]
end
i = i+1+nLeft
end
else
error[[Invalid Encoding format]]
end
return encoding
end
local function parse_fdselect(buf, offset, CharStrings)
local format
format, offset = string.unpack(">B", buf, offset)
if format == 0 then
for i=1,#CharStrings-1 do
local code
code, offset = string.unpack(">B", buf, offset)
CharStrings[i][3] = code + 1
end -- Reimplement with string.byte
elseif format == 3 then
local count, last
count, offset = string.unpack(">I2", buf, offset)
for i=1,count do
local first, code, after = string.unpack(">I2BI2", buf, offset)
for j=first, after-1 do
CharStrings[j][3] = code + 1
end
offset = offset + 3
end
else
error[[Invalid FDSelect format]]
end
end
local function applyencoding(buf, i, usedcids, encoding)
local usednames = {}
local numglyphs
numglyphs, i = string.unpack(">I2", buf, i)
local stroffset = 2*numglyphs + i
local names = setmetatable({}, {__index = function(t, i)
for j=#t+1,i do
t[j], stroffset = string.unpack("s1", buf, stroffset)
end
return t[i]
end})
local newusedcids = {}
for j=1,#usedcids do
local name = encoding[usedcids[j][1]]
if name then
local new = {old = usedcids[j]}
usednames[name], newusedcids[j] = new, new
else
newusedcids[j] = {j} -- FIXME: Someone used a character which does not exists in the encoding.
-- This should probably at least trigger a warning.
end
end
for j=1,numglyphs do
local name
name, i = string.unpack(">I2", buf, i)
if name < 258 then
name = stdnames[name]
else
name = names[name-257]
end
if usednames[name] then
usednames[name][1] = j-1
usednames[name] = nil
end
end
if next(usednames) then
error[[Missing character]]
end
return newusedcids
end
-- The encoding parameter might be:
-- an encoding dictionary - Use the supplied encoding
-- true - Use the build-in encoding
-- false - Use GIDs
-- nil - Use CIDs, falling back to GIDs in name.based fonts
function myfunc(buf, i0, fontid, usedcids, encoding, trust_widths)
-- return function(filename, fontid)
fontid = fontid or 1
local major, minor, hdrSize, offSize = string.unpack(">BBBB", buf, i0)
if major ~= 1 then error[[Unsupported CFF version]] end
-- local offfmt = offsetfmt:format(offSize)
local nameoffsets, topoffsets, stringoffsets, globalsubrs
local i = i0+hdrSize
nameoffsets, i = parse_index(buf, i)
topoffsets, i = parse_index(buf, i)
stringoffsets, i = parse_index(buf, i)
globalsubrs, i = parse_index(buf, i)
local strings = {}
for j=1,#stringoffsets-1 do
strings[j] = buf:sub(stringoffsets[j], stringoffsets[j+1]-1)
end
if #nameoffsets ~= #topoffsets then error[[Inconsistant size of FontSet]] end
if fontid >= #nameoffsets then error[[Invalid font id]] end
local top = parse_dict(buf, topoffsets[fontid], topoffsets[fontid+1]-1, strings)
top.FontName = buf:sub(nameoffsets[fontid], nameoffsets[fontid+1]-1)
local gsubrsdict = {}
for i=1,#globalsubrs-1 do
gsubrsdict[i] = buf:sub(globalsubrs[i], globalsubrs[i+1]-1)
end
gsubrsdict.used = {}
gsubrsdict.bias = #gsubrsdict < 1240 and 108 or #gsubrsdict < 33900 and 1132 or 32769
top.GlobalSubrs = gsubrsdict
local CharStrings = parse_index(buf, i0+top.CharStrings)
if not not encoding ~= encoding and (encoding or top.ROS) then -- If we use the build-in encoding *or* GIDs, we do not need to waste our time making sense of the charset
local charset = parse_charset(buf, i0, top.charset, not top.ROS and strings, #CharStrings-1)
named_charstrings = {}
for i=1,#CharStrings-1 do
named_charstrings[charset[i-1]] = {CharStrings[i], CharStrings[i+1]-1}
end
CharStrings = named_charstrings
else
for i=1,#CharStrings-1 do
CharStrings[i-1] = {CharStrings[i], CharStrings[i+1]-1}
end
CharStrings[#CharStrings] = nil
CharStrings[#CharStrings] = nil
end
-- top.CharStrings = named_charstrings
if not top.ROS then
-- if encoding == true and top.Encoding < 3 then
-- if not reencode and parsed_t1.Encoding == "StandardEncoding" then
-- reencode = kpse.find_file("8a.enc", "enc files")
-- end
-- end
if encoding == true then -- Use the built-in encoding
CharStrings = parse_encoding(buf, i0, top.Encoding, CharStrings)
elseif encoding then
encoding = require'parseEnc'(encoding)
local encoded = {}
for i, n in pairs(encoding) do
encoded[i] = CharStrings[n]
end
CharStrings = encoded
end -- else: Use GIDs
top.Privates = {parse_dict(buf, i0+top.Private[2], i0+top.Private[2]+top.Private[1]-1, strings)}
local subrs = top.Privates[1].Subrs
if subrs then
subrs = parse_index(buf, i0+top.Private[2]+subrs)
local subrsdict ={}
for j=1,#subrs-1 do
subrsdict[j] = buf:sub(subrs[j], subrs[j+1]-1)
end
subrsdict.used = {}
subrsdict.bias = #subrsdict < 1240 and 108 or #subrsdict < 33900 and 1132 or 32769
top.Privates[1].Subrs = subrsdict
end
top.Private = nil
else
assert(not encoding) -- FIXME: If we actually get these from OpenType, the glyph names might be hidden there...
-- Would that even be allowed?
local fonts = parse_index(buf, i0+top.FDArray)
local privates = {}
top.Privates = privates
for i=1,#fonts-1 do
local font = fonts[i]
local fontdir = parse_dict(buf, fonts[i], fonts[i+1]-1, strings)
privates[i] = parse_dict(buf, i0+fontdir.Private[2], i0+fontdir.Private[2]+fontdir.Private[1]-1, strings)
privates[i].FontName = fontdir.FontName
local subrs = privates[i].Subrs
if subrs then
subrs = parse_index(buf, i0+fontdir.Private[2]+subrs)
local subrsdict ={}
for j=1,#subrs-1 do
subrsdict[j] = buf:sub(subrs[j], subrs[j+1]-1)
end
subrsdict.used = {}
subrsdict.bias = #subrsdict < 1240 and 108 or #subrsdict < 33900 and 1132 or 32769
privates[i].Subrs = subrsdict
end
end
top.FDArray = nil
parse_fdselect(buf, i0+top.FDSelect, CharStrings)
end
local glyphs = {}
-- if false and usedcids then -- Subsetting FIXME: Disabled, because other tables have to be fixed up first
if usedcids then -- Subsetting FIXME: Should be Disabled, because other tables have to be fixed up first -- Actually seems to work now, let's test it a bit more
local usedfonts = {}
for i=1,#usedcids do
local cid = usedcids[i][1]
local cs = CharStrings[cid]
glyphs[i] = {cs = buf:sub(cs[1], cs[2]), index = cid, cidfont = cs[3], usedcid = usedcids[i]}
usedfonts[CharStrings[cid][3] or 1] = true
end
local lastfont = 0
for i=1,#top.Privates do
if usedfonts[i] then
lastfont = lastfont + 1
usedfonts[i] = lastfont
top.Privates[lastfont] = top.Privates[i]
end
end
for i=lastfont+1,#top.Privates do
top.Privates[i] = nil
end
for i=1,#glyphs do
glyphs[i].cidfont = usedfonts[glyphs[i].cidfont]
end
-- TODO: CIDFont / Privates subsetting... DONE(?)
-- TODO: Subrs subsetting... Instead of deleting unused SubRs, we only make them empty.
-- This avoids problems with renumberings whiuch would have to be consitant across
-- Fonts in some odd way, because they might be used by globalsubrs.
for i=1,#glyphs do
local g = glyphs[i]
local private = top.Privates[g.cidfont or 1]
local parsed = parse_charstring(g.cs, top.GlobalSubrs, private.Subrs) -- TODO: Implement
local width = parsed[1][2]
if width then
width = width + (private.nominalWidthX or 0)
else
width = private.defaultWidthX or 0
end
local m = top.FontMatrix or {.001, 0, 0, .001, 0, 0}
width = width * m[1] + m[3] -- I really have no idea why m[3] /= 0 might happen, but why not?
width = math.floor(width*1000+.5) -- Thats rescale into "PDF glyph space"
if g.usedcid[2] ~= width then print("MISMATCH:", g.usedcid[1], g.usedcid[2], width) end
g.usedcid[2] = width
end
for i=1,#top.GlobalSubrs do
if not top.GlobalSubrs.used[i] then
top.GlobalSubrs[i] = ""
end
end
for _, priv in ipairs(top.Privates) do if priv.Subrs then
for i=1,#priv.Subrs do
if not priv.Subrs.used[i] then
priv.Subrs[i] = ""
end
end
end end
else
for i, cs in pairs(CharStrings) do -- Not subsetting
glyphs[#glyphs+1] = {cs = buf:sub(cs[1], cs[2]), index = i, cidfont = cs.font}
end
end
top.glyphs = glyphs
table.sort(glyphs, function(a,b)return a.index<b.index end)
local bbox
if top.FontMatrix then
local x0, y0 = apply_matrix(top.FontMatrix, top.FontBBox[1], top.FontBBox[2])
local x1, y1 = apply_matrix(top.FontMatrix, top.FontBBox[3], top.FontBBox[4])
bbox = {x0, y0, x1, y1}
else
bbox = top.FontBBox
end
return require'luametalatex-font-cff'(top), bbox
end
-- local file = io.open(arg[1])
-- local buf = file:read'a'
-- file:close()
-- io.open(arg[3], 'w'):write(myfunc(buf, 1, 1, nil, {{3}, {200}, {1000}, {1329}, {1330}, {1331}})):close()
return function(filename, encoding) return function(fontdir, usedcids)
local file = io.open(filename)
local buf = file:read'a'
local i = 1
file:close()
local magic = buf:sub(1, 4)
if magic == "ttcf" or magic == "OTTO" then
-- assert(not encoding) -- nil or false
encoding = encoding or false
local magic, tables = sfnt.parse(buf, 1) -- TODO: Interpret widths etc, they might differ from the CFF ones.
assert(magic == "OTTO")
-- Also CFF2 would be nice to have
i = tables['CFF '][1]
end
local content, bbox = myfunc(buf, i, 1, usedcids, encoding)
fontdir.bbox = bbox
return content
end end
-- io.open(arg[3], 'w'):write(myfunc(buf, 1, 1, require'parseEnc'(arg[2]), {{string.byte'a'}, {string.byte'b'}, {string.byte'-'}})):close()

View File

@ -0,0 +1,54 @@
local template = [[%%!PS-Adobe-3.0 Resource-CMap
%%%%DocumentNeededResources: ProcSet (CIDInit)
%%%%IncludeResource: ProcSet (CIDInit)
%%%%BeginResource: CMap (TeX-%s-0)
%%%%Title: (TeX-%s-0 TeX %s 0)
%%%%Version: 1.000
%%%%EndComments
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CIDSystemInfo
<< /Registry (TeX)
/Ordering (%s)
/Supplement 0
>> def
/CMapName /TeX-%s-0 def
/CMapType 2 def
1 begincodespacerange
<00> <FF>
endcodespacerange
]]
local separator = [[endbfrange
%i beginbfchar
]]
local betweenchars = [[endbfchar
%i beginbfchar
]]
local trailer = [[endbfchar
endcmap
CMapName currentdict /CMap defineresource pop
end
end
%%EndResource
%%EOF
]]
return function(f)
local name = 'XXXstuffXXX-' .. f.name
local text = template:format(name, name, name, name, name)
text = text .. "0 beginbfrange\n"
local count, chars = 0, ""
local next_head = separator
for u, char in pairs(f.characters) do
if char.used and char.tounicode then
count = count + 1
chars = chars .. ("<%02X> <%s>\n"):format(u, char.tounicode)
if count == 100 then
text = text .. next_head:format(100) .. chars
next_head = betweenchars
end
end
end
text = text .. next_head:format(count) .. chars .. trailer
return text
end

View File

@ -0,0 +1,54 @@
local template = [[%%!PS-Adobe-3.0 Resource-CMap
%%%%DocumentNeededResources: ProcSet (CIDInit)
%%%%IncludeResource: ProcSet (CIDInit)
%%%%BeginResource: CMap (TeX-%s-0)
%%%%Title: (TeX-%s-0 TeX %s 0)
%%%%Version: 1.000
%%%%EndComments
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CIDSystemInfo
<< /Registry (TeX)
/Ordering (%s)
/Supplement 0
>> def
/CMapName /TeX-%s-0 def
/CMapType 2 def
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
]]
local separator = [[endbfrange
%i beginbfchar
]]
local betweenchars = [[endbfchar
%i beginbfchar
]]
local trailer = [[endbfchar
endcmap
CMapName currentdict /CMap defineresource pop
end
end
%%EndResource
%%EOF
]]
return function(f, usedcids)
local name = 'XXXstuffXXX-' .. f.name
local text = template:format(name, name, name, name, name)
text = text .. "0 beginbfrange\n"
local count, chars = 0, ""
local next_head = separator
for _, cid in ipairs(usedcids) do
if cid[3] then
count = count + 1
chars = chars .. (type(cid[3]) == 'string' and "<%04X> <%s>\n" or "<%04X> <%04X>\n"):format(cid[1], cid[3])
if count == 100 then
text = text .. next_head:format(100) .. chars
next_head = betweenchars
end
end
end
text = text .. next_head:format(count) .. chars .. trailer
return text
end

View File

@ -0,0 +1,62 @@
local template = [[%%!PS-Adobe-3.0 Resource-CMap
%%%%DocumentNeededResources: ProcSet (CIDInit)
%%%%IncludeResource: ProcSet (CIDInit)
%%%%BeginResource: CMap (TeX-%s-0)
%%%%Title: (TeX-%s-0 TeX %s 0)
%%%%Version: 1.000
%%%%EndComments
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CIDSystemInfo
<< /Registry (TeX)
/Ordering (%s)
/Supplement 0
>> def
/CMapName /TeX-%s-0 def
/CMapType 2 def
3 begincodespacerange
<00> <7F>
<8000> <FEFF>
<FF0000> <FF807F>
endcodespacerange
]]
local separator = [[endbfrange
%i beginbfchar
]]
local betweenchars = [[endbfchar
%i beginbfchar
]]
local trailer = [[endbfchar
endcmap
CMapName currentdict /CMap defineresource pop
end
end
%%EndResource
%%EOF
]]
return function(f, usedcids)
local name = 'XXXstuffXXX-' .. f.name
local text = template:format(name, name, name, name, name)
text = text .. "0 beginbfrange\n"
local count, chars = 0, ""
local next_head = separator
for _, cid in ipairs(usedcids) do
if cid[3] then
count = count + 1
if cid[1] < 0x80 then
chars = chars .. (type(cid[3]) == 'string' and "<%02X> <%s>\n" or "<%02X> <%04X>\n"):format(cid[1], cid[3])
elseif cid[1] < 0x7F80 then
chars = chars .. (type(cid[3]) == 'string' and "<%04X> <%s>\n" or "<%04X> <%04X>\n"):format(cid[1]+0x7F80, cid[3])
else
chars = chars .. (type(cid[3]) == 'string' and "<%06X> <%s>\n" or "<%06X> <%04X>\n"):format(cid[1]-0x7F80, cid[3])
end
if count == 100 then
text = text .. next_head:format(100) .. chars
next_head = betweenchars
end
end
end
text = text .. next_head:format(count) .. chars .. trailer
return text
end

View File

@ -0,0 +1,12 @@
local white = (lpeg.S'\0\9\10\12\13\32' + '%' * (1 - lpeg.S'\r\n')^0)^1
local regular = 1-lpeg.S'()<>[]{}/%\0\9\10\12\13\32'
local name = lpeg.C(regular^1)
local lname = '/' * name / 1
local namearray = lpeg.Ct('['*white^0*lpeg.Cg(lname*white^0, 0)^-1*(lname*white^0)^0*']')
local encfile = white^0*lname*white^0*namearray*white^0*'def'*white^0*-1
return function(filename)
local file = io.open(filename)
local name, encoding = encfile:match(file:read'a')
file:close()
return encoding, name
end

View File

@ -0,0 +1,49 @@
local purenumber = lpeg.R'09'^1
local optoperator = lpeg.C(lpeg.S'+-='^-1)*lpeg.C(lpeg.P(1)^0)
local commentchar = lpeg.S' %*;#'+-1
local wordpatt = (lpeg.C('"') * lpeg.C((1-lpeg.P'"')^0) * lpeg.P'"'^-1 + lpeg.C(('<' * lpeg.S'<['^-1)^-1) * lpeg.S' \t'^0 * lpeg.C((1-lpeg.S' \t')^0)) * lpeg.S' \t'^0 * lpeg.Cp()
local fontmap = {}
local function mapline(line, operator)
if not operator then
operator, line = optoperator:match(line)
end
if commentchar:match(line) then return end
local pos = 1
local tfmname, psname, flags, special, enc, font, subset
local kind, word
while pos ~= #line+1 do
kind, word, pos = wordpatt:match(line, pos)
if kind == "" then
if not tfmname then tfmname = word
elseif not psname and not purenumber:match(word) then
psname = word
elseif purenumber:match(word) then flags = tonumber(word)
else
error[[Invalid map file line, excessive simple words]]
end
elseif kind == '"' then
special = word
else
if kind == "<[" or (kind ~= "<<" and word:sub(-4) == ".enc") then
enc = word
else
font = word
subset = kind ~= "<<"
end
end
end
fontmap[tfmname] = {psname or tfmname, flags or (font and 4 or 0x22), font, enc, special, subset}
end
local function mapfile(filename, operator)
if not operator then
operator, filename = optoperator:match(filename)
end
local file = io.open(kpse.find_file(filename, 'map'))
for line in file:lines() do mapline(line, operator) end
file:close()
end
return {
mapline = mapline,
mapfile = mapfile,
fontmap = fontmap
}

View File

@ -0,0 +1,56 @@
-- nodefont = true
-- Some helpers:
-- A kpse wrapper
local serialize_cff = require'luametalatex-font-cff'
local serializet2 = require'luametalatex-font-t2'
local parseT1 = require'luametalatex-font-t1'
local t1tot2 = require'luametalatex-font-t1tot2'
return function(filename, reencode)
local parsed_t1 = parseT1(filename)
return function(f, usedcids)
f.bbox = parsed_t1.FontBBox
local fonttable = {
version = parsed_t1.FontInfo.version,
Notice = parsed_t1.FontInfo.Notice,
FullName = parsed_t1.FontInfo.FullName,
FamilyName = parsed_t1.FontInfo.FamilyName,
Weight = parsed_t1.FontInfo.Weight,
ItalicAngle = parsed_t1.FontInfo.ItalicAngle,
isFixedPitch = parsed_t1.FontInfo.isFixedPitch,
UnderlinePosition = parsed_t1.FontInfo.UnderlinePosition,
UnderlineThickness = parsed_t1.FontInfo.UnderlineThickness,
FontName = parsed_t1.FontName,
FontMatrix = parsed_t1.FontMatrix,
FontBBox = parsed_t1.FontBBox,
-- UniqueID = parsed_t1.UniqueID,
-- ? = parsed_t1.Metrics,
---- PRIVATE ----
BlueValues = parsed_t1.Private.BlueValues,
OtherBlues = parsed_t1.Private.OtherBlues,
-- FamilyBlues?
BlueScale = parsed_t1.Private.BlueScale,
BlueShift = parsed_t1.Private.BlueShift,
BlueFuzz = parsed_t1.Private.BlueFuzz,
StdHW = (parsed_t1.Private.StdHW or {})[1], -- Why are these arrays in T1?
StdVW = (parsed_t1.Private.StdVW or {})[1], -- They are also undocumented in the spec...
StemSnapH = parsed_t1.Private.StemSnapH,
StemSnapV = parsed_t1.Private.StemSnapV,
ForceBold = parsed_t1.Private.ForceBold,
-- LanguageGroup = parsed_t1.Private.LanguageGroup,
}
if not reencode and parsed_t1.Encoding == "StandardEncoding" then
reencode = kpse.find_file("8a.enc", "enc files")
end
if reencode then
parsed_t1.Encoding = require'parseEnc'(reencode)
end
-- parsed_t1.Encoding[0] = ".notdef"
local glyphs = {}
fonttable.glyphs = glyphs
for i=1,#usedcids do
local name = parsed_t1.Encoding[usedcids[i][1]] -- TODO: Reencoding and StandardEncoding
glyphs[#glyphs + 1] = {index = usedcids[i][1], name = name, cs = serializet2(t1tot2(parsed_t1.CharStrings[name], parsed_t1.Private.Subrs))} -- TODO: Missing glyphs
end
return serialize_cff(fonttable)
end
end

View File

@ -0,0 +1,155 @@
local sfnt = require'libSfnt'
local stdnames = require'ttfstaticstrings'
local function round(x)
local i, f = math.modf(x)
if f < 0 then
return i - (f<=-0.5 and 1 or 0)
else
return i + (f>=-0.5 and 1 or 0)
end
end
local function addglyph(glyph, usedcids, cidtogid)
-- FIXME: Pseudocode
if string.unpack(">i2", glyph) < 0 then -- We have a composite glyph.
-- This is an untested mess. Disaster will follow.
local offset = 11
while offset do
local flags, component = string.unpack(">I2I2", glyph, offset)
local gid = cidtogid[component]
if not gid then
gid = #usedcids+1
usedcids[gid] = {component}
cidtogid[component] = gid
end
glyph = glyph:sub(1, offset-1) .. string.pack(">I2", gid).. glyph:sub(offset+2)
offset = flags&32==32 and offset + 4 + (flags&1==1 and 4 or 2) + (flags&8==8 and 2 or (flags&64==64 and 4 or (flags&128==128 and 8 or 0)))
end
end
return glyph
end
local function readpostnames(buf, i, usedcids, encoding)
local usednames = {}
local numglyphs
numglyphs, i = string.unpack(">I2", buf, i)
local stroffset = 2*numglyphs + i
local names = setmetatable({}, {__index = function(t, i)
for j=#t+1,i do
t[j], stroffset = string.unpack("s1", buf, stroffset)
end
return t[i]
end})
local newusedcids = {}
for j=1,#usedcids do
local name = encoding[usedcids[j][1]]
if name then
local new = {}
usednames[name], newusedcids[j] = new, new
else
newusedcids[j] = {j} -- FIXME: Someone used a character which does not exists in the encoding.
-- This should probably at least trigger a warning.
end
end
for j=1,numglyphs do
local name
name, i = string.unpack(">I2", buf, i)
if name < 258 then
name = stdnames[name]
else
name = names[name-257]
end
if usednames[name] then
usednames[name][1] = j-1
usednames[name] = nil
end
end
if next(usednames) then
error[[Missing character]]
end
return newusedcids
end
return function(filename, fontid, reencode)
local file = io.open(filename)
local buf = file:read'a'
file:close()
local magic, tables = sfnt.parse(buf, 1, fontid)
if magic ~= "\0\1\0\0" then error[[Invalid TTF font]] end
-- TODO: Parse post table and add reencoding support
-- if tables.post and string.unpack(">I4", buf, tables.post[1]) == 0x00020000 and reencode then
-- local encoding = require'parseEnc'(reencode)
-- if encoding then
-- local names = {}
-- local off = tables.post[1] + 4
-- for i = 1,string.unpack(">I2", buf, tables.maxp[1] + 4) do
return function(fontdir, usedcids)
if reencode and string.unpack(">I4", buf, tables.post[1]) == 0x00020000 then
usedcids = readpostnames(buf, tables.post[1] + 32, usedcids, require'parseEnc'(reencode))
else
usedcids = table.move(usedcids, 1, #usedcids, 1, {})
end
table.insert(usedcids, 1, {0})
local newtables = {}
newtables.head = buf:sub(tables.head[1], tables.head[1]+tables.head[2]-1)
local bbox1, bbox2, bbox3, bbox4, scale, _
scale, _, _, bbox1, bbox2, bbox3, bbox4 = string.unpack(">I2I8I8i2i2i2i2", buf, tables.head[1]+18)
scale = 1000/scale
fontdir.bbox = {math.floor(bbox1*scale), math.floor(bbox2*scale), math.ceil(bbox3*scale), math.ceil(bbox4*scale)}
local cidtogid = {}
for i=1,#usedcids do
cidtogid[usedcids[i][1]] = i
end
local loca, glyf, locaOff, glyfOff = {}, {}, tables.loca[1], tables.glyf[1]
hmtx = nil
if string.unpack(">i2", buf, tables.head[1]+50) == 0 then -- short offsets
local s, i = 0, 1
while i <= #usedcids do
local from, til = string.unpack(">I2I2", buf, locaOff+2*usedcids[i][1])
loca[i] = string.pack(">I2", s)
s = s+til-from
glyf[i] = from ~= til and addglyph(buf:sub(glyfOff+from*2, glyfOff+til*2-1), usedcids, cidtogid) or ""
i = i+1
end
loca[#usedcids+1] = string.pack(">I2", s)
else -- long offsets
local s, i = 0, 1
while i <= #usedcids do
local from, til = string.unpack(">I4I4", buf, locaOff+4*usedcids[i][1])
loca[i] = string.pack(">I4", s)
s = s+til-from
glyf[i] = til == from and "" or addglyph(buf:sub(glyfOff+from, glyfOff+til-1), usedcids, cidtogid)
i = i+1
end
loca[#usedcids+1] = string.pack(">I4", s)
end
newtables.loca = table.concat(loca)
loca = nil
newtables.glyf = table.concat(glyf)
local hmtx = glyf
glyf = nil
for i = 1,#hmtx do hmtx[i] = nil end
assert(tables.hhea[2] == 36)
local numhmetrics = string.unpack(">I2", buf, tables.hhea[1]+34)
newtables.hhea = buf:sub(tables.hhea[1], tables.hhea[1]+33) .. string.pack(">I2", #usedcids)
local off = tables.hmtx[1]
local finaladv, off2 = buf:sub(off+(numhmetrics-1)*4, off+numhmetrics*4-3), off+2*numhmetrics
for i=1,#usedcids do
if usedcids[i][1] < numhmetrics then
hmtx[i] = buf:sub(off+usedcids[i][1]*4, off+usedcids[i][1]*4+3)
else
hmtx[i] = finaladv .. buf:sub(off2+usedcids[i][1]*2, off2+usedcids[i][1]*2+1)
end
end
newtables.hmtx = table.concat(hmtx)
newtables.maxp = buf:sub(tables.maxp[1], tables.maxp[1]+3) .. string.pack(">I2", #usedcids) .. buf:sub(tables.maxp[1]+6, tables.maxp[1]+tables.maxp[2]-1)
if tables.fpgm then
newtables.fpgm = buf:sub(tables.fpgm[1], tables.fpgm[1]+tables.fpgm[2]-1)
end
if tables.prep then
newtables.prep = buf:sub(tables.prep[1], tables.prep[1]+tables.prep[2]-1)
end
if tables['cvt '] then
newtables['cvt '] = buf:sub(tables['cvt '][1], tables['cvt '][1]+tables['cvt '][2]-1)
end
return sfnt.write(magic, newtables)
end
end

274
luametalatex-pdf-font.lua Normal file
View File

@ -0,0 +1,274 @@
local mapping = require'luametalatex-pdf-font-map'
mapping.mapfile'/usr/local/texlive/2019/texmf-var/fonts/map/pdftex/updmap/pdftex.map'
local tounicode = {
[-3] = require'luametalatex-pdf-font-cmap3',
require'luametalatex-pdf-font-cmap1',
require'luametalatex-pdf-font-cmap2',
}
local function allcids(fontdir)
local cids = {}
local scale = 1000/fontdir.size
for i,v in pairs(fontdir.characters) do
v.used = true
cids[#cids+1] = {v.index or i, math.floor(v.width*scale+.5), v.tounicode}
end
return cids
end
local function buildW(f, usedcids)
local used = #usedcids
if used == 0 then return "" end
local result = {}
local index = 1
while index <= used do
local width = usedcids[index][2]
local last = index
while last ~= used and usedcids[last+1][2] == width do
last = last + 1
end
if index == last then
local span = {width}
width = (usedcids[last+1] or {})[2]
while (last + 2 <= used
and usedcids[last+2][2] ~= width
or last + 1 == used)
and usedcids[last+1][1]-usedcids[last][1] <= 2 do
for i=usedcids[last][1]+1,usedcids[last+1][1]-1 do
span[#span+1] = 0
end
last = last + 1
span[#span+1] = width
width = (usedcids[last + 1] or {})[2]
end
result[#result+1] = string.format("%i[%s]", usedcids[index][1], table.concat(span, ' '))
else
result[#result+1] = string.format("%i %i %i ", usedcids[index][1], usedcids[last][1], width)
end
index = last + 1
end
return table.concat(result)
end
local function fontdescriptor(pdf, basefont, fontdir, stream, kind)
local scale = 1000/fontdir.size
return string.format(
"<</Type/FontDescriptor/FontName/%s/Flags %i/FontBBox[%i %i %i %i]/ItalicAngle %i/Ascent %i/Descent %i/CapHeight %i/StemV %i/FontFile%s %i 0 R>>",
basefont,
4,-- FIXME: Flags ??? (4 means "symbolic")
fontdir.bbox[1], fontdir.bbox[2], fontdir.bbox[3], fontdir.bbox[4], -- FIXME: How to determine BBox?
math.floor(math.atan(fontdir.parameters.slant or fontdir.parameters[1] or 0, 0x10000)+.5),
fontdir.bbox[4], fontdir.bbox[2],
fontdir.parameters[8] and math.floor(fontdir.parameters[8]*scale+0.5) or fontdir.bbox[4],
fontdir.StemV or 100, -- FIXME: How to determine StemV?
kind, stream)
end
local function cidmap1byte(pdf)
if not pdf.cidmap1byte then
pdf.cidmap1byte = string.format(" %i 0 R", pdf:stream(nil, [[/Type/CMap/CMapName/Identity-8-H/CIDSystemInfo<</Registry(Adobe)/Ordering(Identity)/Supplement 0>>]],
[[%!PS-Adobe-3.0 Resource-CMap
%%DocumentNeededResources : ProcSet (CIDInit)
%%IncludeResource : ProcSet (CIDInit)
%%BeginResource : CMap (Identity-8-H)
%%Title: (Custom 8bit Identity CMap)
%%Version: 1.000
%%EndComments
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CIDSystemInfo
3 dict dup begin
/Registry (Adobe) def
/Ordering (Identity) def
/Supplement 0 def
end def
/CMapName /Identity-8-H def
/CMapVersion 1.000 def
/CMapType 1 def
/WMode 0 def
1 begincodespacerange
<00> <FF>
endcodespacerange
1 begincidrange
<00> <FF> 0
endcidrange
endcmap
CMapName currentdict /CMap defineresource pop
end
end
%%EndResource
%%EOF]]))
end
return pdf.cidmap1byte
end
local function cidmap3byte(pdf)
if not pdf.cidmap3byte then
pdf.cidmap3byte = string.format(" %i 0 R", pdf:stream(nil, [[/Type/CMap/CMapName/Identity-Var-H/CIDSystemInfo<</Registry(Adobe)/Ordering(Identity)/Supplement 0>>]],
[[%!PS-Adobe-3.0 Resource-CMap
%%DocumentNeededResources : ProcSet (CIDInit)
%%IncludeResource : ProcSet (CIDInit)
%%BeginResource : CMap (Identity-Var-H)
%%Title: (Custom 8-24bit variable size Identity CMap)
%%Version: 1.000
%%EndComments
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CIDSystemInfo
3 dict dup begin
/Registry (Adobe) def
/Ordering (Identity) def
/Supplement 0 def
end def
/CMapName /Identity-Var-H def
/CMapVersion 1.000 def
/CMapType 1 def
/WMode 0 def
3 begincodespacerange
<FF0000> <FF807F>
<00> <7F>
<8000> <FEFF>
endcodespacerange
3 begincidrange
<00> <7F> 0
<8000> <FEFF> 128
<FF0000> <FF807F> 32640
endcidrange
endcmap
CMapName currentdict /CMap defineresource pop
end
end
%%EndResource
%%EOF]]))
end
return pdf.cidmap3byte
end
local capitals = {string.byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1, -1)}
local function gen_subsettagchar(i, left, ...)
if left == 0 then return ... end
return gen_subsettagchar(i//#capitals, left-1, capitals[i%#capitals+1], ...)
end
local function gen_subsettag(ident)
local i = string.unpack("j", sha2.digest256(ident))
return string.char(gen_subsettagchar(i, 7, 43))
end
local function buildfont0cff(pdf, fontdir, usedcids)
local basefont = fontdir.psname or fontdir.fullname or fontdir.name -- FIXME: Subset-tag(?), Name-Escaping(?), fallback
if fontdir.cff then
cff = fontdir:cff(usedcids)
else
if fontdir.filename then
if fontdir.format == "type1" then
cff = require'luametalatex-pdf-font-t1'(fontdir.filename, fontdir.encoding)(fontdir, usedcids)
elseif fontdir.format == "opentype" then
cff = require'luametalatex-pdf-font-cff'(fontdir.filename, fontdir.encodingbytes == 1 and (fontdir.encoding or true))(fontdir, usedcids)
else
error[[Unsupported format]]
end
else
return string.format("<</Type/Font/Subtype/Type1/BaseFont/%s/FontDescriptor %i 0 R/FirstChar %i/LastChar %i>>", basefont, -42, usedcids[1][1], usedcids[#usedcids][1])
end
end
local widths = buildW(fontdir, usedcids) -- Do this after generating the CFF to allow for extracting the widths from the font file
basefont = gen_subsettag(widths)..basefont
local cidfont = pdf:indirect(nil, string.format(
"<</Type/Font/Subtype/CIDFontType0/BaseFont/%s/CIDSystemInfo<</Registry(Adobe)/Ordering(Identity)/Supplement 0>>/FontDescriptor %i 0 R/W[%s]>>",
basefont,
pdf:indirect(nil, fontdescriptor(pdf, basefont, fontdir, pdf:stream(nil, '/Subtype/CIDFontType0C', cff), 3)),
widths
))
return basefont, cidfont
end
local function buildfont0ttf(pdf, fontdir, usedcids)
local basefont = fontdir.psname or fontdir.fullname or fontdir.name -- FIXME: Subset-tag(?), Name-Escaping(?), fallback
local ttf
if fontdir.ttf then
ttf = fontdir:ttf(usedcids) -- WARNING: If you implement this: You always have to add a .notdef glyph at index 0. This one is *not* included in usedcids
else
ttf = require'luametalatex-pdf-font-ttf'(fontdir.filename, 1, fontdir.encoding)(fontdir, usedcids)
end
local lastcid = -1
local cidtogid = {}
for i=1,#usedcids do
cidtogid[2*i-1] = string.rep("\0\0", usedcids[i][1]-lastcid-1)
cidtogid[2*i] = string.pack(">I2", i)
lastcid = usedcids[i][1]
end
cidtogid = pdf:stream(nil, "", table.concat(cidtogid))
local widths = buildW(fontdir, usedcids)
basefont = gen_subsettag(widths)..basefont
local cidfont = pdf:indirect(nil, string.format(
"<</Type/Font/Subtype/CIDFontType2/BaseFont/%s/CIDSystemInfo<</Registry(Adobe)/Ordering(Identity)/Supplement 0>>/FontDescriptor %i 0 R/W[%s]/CIDToGIDMap %i 0 R>>",
basefont,
pdf:indirect(nil, fontdescriptor(pdf, basefont, fontdir, pdf:stream(nil, string.format('/Length1 %i', #ttf), ttf), 2)),
widths,
cidtogid
))
return basefont, cidfont
end
local function buildfont0(pdf, fontdir, usedcids)
usedcids = usedcids or allcids(fontdir)
table.sort(usedcids, function(a,b) return a[1]<b[1] end)
local enc
if fontdir.encodingbytes == 1 then
enc = cidmap1byte(pdf)
elseif true then -- FIXME: This should only be used for encodingbyzes == -3 (variable, max 3)
fontdir.encodingbytes = -3 -- FIXME
enc = cidmap3byte(pdf)
else
enc = "/Identity-H"
end
local basefont, cidfont = (fontdir.format == "truetype" and buildfont0ttf or buildfont0cff)(pdf, fontdir, usedcids)
local touni = pdf:stream(nil, "", tounicode[fontdir.encodingbytes](fontdir, usedcids)) -- Done late to allow for defaults set from the font file
return string.format(
"<</Type/Font/Subtype/Type0/BaseFont/%s/Encoding%s/ToUnicode %i 0 R/DescendantFonts[%i 0 R]>>",
basefont,
enc,
touni,
cidfont)
end
local fontextensions = {
ttf = {"truetype", "truetype fonts",},
otf = {"opentype", "opentype fonts",},
pfb = {"type1", "type1 fonts",},
}
fontextensions.cff = fontextensions.otf
local fontformats = {
fontextensions.pfb, fontextensions.otf, fontextensions.ttf,
}
return function(pdf, fontdir, usedcids)
if fontdir.encodingbytes == 0 then fontdir.encodingbytes = nil end
if fontdir.format == "unknown" or not fontdir.format or fontdir.encodingbytes == 1 then -- TODO: How to check this?
fontdir.encodingbytes = fontdir.encodingbytes or 1
local mapentry = mapping.fontmap[fontdir.name]
if mapentry then
local format = mapentry[3] and mapentry[3]:sub(-4, -4) == '.' and fontextensions[mapentry[3]:sub(-3, -1)]
if format then
fontdir.format = format[1]
fontdir.filename = kpse.find_file(mapentry[3], format[2])
if mapentry[4] then
fontdir.encoding = kpse.find_file(mapentry[4], 'enc files')
end
goto format_set
else
for _, format in ipairs(fontformats) do
local font = kpse.find_file(mapentry[3],format[2])
if font then
fontdir.format = "type1"
fontdir.filename = font
if mapentry[4] then
fontdir.encoding = kpse.find_file(mapentry[4], 'enc files')
end
goto format_set
end
end
end
end
fontdir.format = "type3"
::format_set::
else
fontdir.encodingbytes = fontdir.encodingbytes or 2
end
if fontdir.format == "type3" then
error[[Currently unsupported]] -- TODO
else
return buildfont0(pdf, fontdir, usedcids)
end
end

View File

@ -0,0 +1,44 @@
local min = math.min
local format = string.format
local concat = table.concat
local _ENV = {}
function write(pdf, tree, total, max)
tree = tree or pdf.pages
if #tree == 0 then
local id = pdf:getobj()
pdf:indirect(id, '<</Type/Pages/Kids[]/Count 0>>')
return id
end
max = max or 6 -- These defaults only work on the lowest level
total = total or #tree
local remaining = total
-- if #tree == 1 then
-- retur
local newtree = {}
local parent = ""
for i=0,(#tree-1)//6 do
local id = tree[-i]
newtree[i+1] = id
if 0 == i % 6 and #tree > 6 then
local parentid = pdf:getid()
newtree[-(i//6)] = parentid
parent = format("/Parent %i 0 R", parentid)
end
pdf:indirect(id, format('<</Type/Pages%s/Kids[%s 0 R]/Count %i>>', parent, concat(tree, ' 0 R ', 6*i+1, min(#tree, 6*i+6)), min(remaining, max)))
remaining = remaining - max
end
if #parent > 0 then
return writetree(pdf, newtree, total, max*6)
end
return newtree[1]
end
function newpage(pdf)
local pageid = pdf:getobj()
local pagenumber = #pdf.pages
pdf.pages[pagenumber+1] = pageid
if 0 == pagenumber % 6 then
pdf.pages[-(pagenumber//6)] = pdf:getobj()
end
return pageid, pdf.pages[-(pagenumber//6)]
end
return _ENV

87
luametalatex-pdf.lua Normal file
View File

@ -0,0 +1,87 @@
local format = string.format
local gsub = string.gsub
local byte = string.byte
local pack = string.pack
local error = error
local pairs = pairs
local setmetatable = setmetatable
local assigned = {}
local function stream(pdf, num, dict, content)
if not num then num = pdf:getobj() end
if pdf[num] ~= assigned then
error[[Invalid object]]
end
pdf[num] = {offset = pdf.file:seek()}
pdf.file:write(format('%i 0 obj\n<<%s/Length %i>>stream\n', num, dict, #content))
pdf.file:write(content)
pdf.file:write'\nendstream\nendobj\n'
return num
end
local function indirect(pdf, num, content)
if not num then num = pdf:getobj() end
if pdf[num] ~= assigned then
error[[Invalid object]]
end
pdf[num] = {offset = pdf.file:seek()}
pdf.file:write(format('%i 0 obj\n', num))
pdf.file:write(content)
pdf.file:write'\nendobj\n'
return num
end
local function getid(pdf)
local id = pdf[0] + 1
pdf[0] = id
pdf[id] = assigned
return id
end
local function trailer(pdf)
local nextid = getid(pdf)
local myoff = pdf.file:seek()
pdf[nextid] = {offset = myoff}
local linked = 0
local offsets = {}
for i=1,nextid do
local off = pdf[i].offset
if off then
offsets[i+1] = pack(">I1I3I1", 1, off, 0)
else
offsets[linked+1] = pack(">I1I3I1", 0, i, 255)
linked = i
end
end
offsets[linked+1] = '\0\0\0\0\255'
pdf[nextid] = assigned
-- TODO: Add an /ID according to 14.4
stream(pdf, nextid, format([[/Type/XRef/Size %i/W[1 3 1]/Root %i 0 R]], nextid+1, pdf.root), table.concat(offsets))
pdf.file:write('startxref\n', myoff, '\n%%EOF')
end
local function close(pdf)
trailer(pdf)
if #pdf.version ~= 3 then
error[[Invalid PDF version]]
end
pdf.file:seek('set', 5)
pdf.file:write(pdf.version)
pdf.file:close()
end
local pagetree = require'luametalatex-pdf-pagetree'
local pdfmeta = {
close = close,
getobj = getid,
indirect = indirect,
stream = stream,
newpage = pagetree.newpage,
writepages = pagetree.write,
-- delayed = delayed,
-- delayedstream = delayedstream,
-- reference
}
pdfmeta.__index = pdfmeta
local function open(filename)
local file = io.open(filename, 'w')
file:write"%PDF-X.X\n%🖋\n"
return setmetatable({file = file, version = '1.7', [0] = 0, pages = {}}, pdfmeta)
end
return {
open = open,
}

34
luametalatex.ini Normal file
View File

@ -0,0 +1,34 @@
% Thomas Esser, 1998. public domain.
%
\ifx\pdfoutput\undefined \else
\ifx\pdfoutput\relax \else
%
% We're building the latex format with the pdfetex engine (started 2004).
\input pdftexconfig
\pdfoutput=0
%
% pdfTeX related primitives are no longer hidden by default
% (started 2005). Uncomment and recreate the format files by running
% "fmtutil --all" resp. "fmtutil-sys --all" to revert to the old
% (2004) behaviour.
% \input pdftex-dvi.tex
%
\fi
\fi
%
% the usual format initialization.
\scrollmode
\luabytecode2
\begingroup
\catcode`\{=1
\catcode`\}=2
\def\x{\everyjob{\luabytecode2}}
\expandafter\endgroup\x
\let\savedversionofdump\dump
\let\dump\relax
\input latex.ltx
\input luametalatex-baseregisters
\let\dump\savedversionofdump
\let\savedversionofdump\undefined
\dump
\endinput

121
luametalatex.lua Normal file
View File

@ -0,0 +1,121 @@
-- Some helpers based on Penlight
local absdir, dirsep
do
local sep = package.config:sub(1,1)
local is_windows = sep == "\\"
dirsep = lpeg.S(is_windows and '\\/' or '/')
local anchor_pattern = lpeg.Cs(is_windows
and lpeg.P'\\\\' + dirsep/'\\' + 1*lpeg.P':'*dirsep^-1/'\\'
or lpeg.P'//' + dirsep^1/'/')
function isabs(P)
return P:sub(1,1) == '/' or (is_windows and (P:sub(1,1)=='\\' or P:sub(2,2)==':'))
end
local insert, remove, concat = table.insert, table.remove, table.concat
function normpath(P)
-- Split path into anchor and relative path.
local anchor, P = ((anchor_pattern + lpeg.Cc'') * lpeg.C(lpeg.P(1)^0)):match(P)
if is_windows then
P = P:gsub('/','\\')
end
local parts = {}
for part in P:gmatch('[^'..sep..']+') do
if part == '..' then
if #parts ~= 0 and parts[#parts] ~= '..' then
remove(parts)
else
insert(parts, part)
end
elseif part ~= '.' then
insert(parts, part)
end
end
P = anchor..concat(parts, sep)
if P == '' then P = '.' end
return P
end
function join(p1,p2,...)
if select('#',...) > 0 then
local p = join(p1,p2)
return join(p, ...)
end
if isabs(p2) then return p2 end
local endc = p1:sub(#p1,#p1)
if endc ~= "/" and (not is_windows or endc ~= "\\") and endc ~= "" then
p1 = p1..sep
end
return p1..p2
end
function absdir(P,pwd)
local use_pwd = pwd ~= nil
pwd = pwd or lfs.currentdir()
if not isabs(P) then
P = join(pwd,P)
elseif is_windows and not use_pwd and P:sub(2,2) ~= ':' and P:sub(2,2) ~= '\\' then
P = pwd:sub(1,2)..P -- attach current drive to path like '\\fred.txt'
end
return normpath(P) .. sep
end
end
-- Who are we anyway?
local format = os.selfname -- or os.selfcore, I couldn't find a difference yet
local ourname = arg[0] -- This might not be os.selfarg[0]
if os.selfarg[0] == ourname then
ourname = nil
end
for i, a in ipairs(os.selfarg) do
-- LuaMetaTeX needs -- to introduce parameters,
-- but fmtutil uses just -. Let's rewrite this on the fly:
if a == ourname then -- Avoid recursion
table.remove(os.selfarg, i)
ourname = nil
a = os.selfarg[i]
end
if a == "--" then break end
a = a:gsub("^%-%-?", "--")
os.selfarg[i] = a
if a:sub(1, 11) == "--progname=" then
format = a:sub(12)
elseif a == '--ini' then
is_initex = true
end
end
local dir = absdir(os.selfdir)
local dirseparators = {((lpeg.S'\\/'^1 + 1 * lpeg.P':' * lpeg.S'\\/'^-1) * lpeg.Cp() * ((1-lpeg.S'\\/')^0*lpeg.S'\\/'*lpeg.Cp())^0):match(dir)}
-- First step: Find our actual format.
local init_script = format .. "-init.lua"
local texmf_dir = "tex/lualatex/" .. format .. '/' .. init_script
local paths = {
init_script,
"share/texmf-local/" .. texmf_dir,
"share/texmf-dist/" .. texmf_dir,
"share/texmf/" .. texmf_dir,
"texmf-local/" .. texmf_dir,
"texmf-dist/" .. texmf_dir,
"texmf/" .. texmf_dir,
}
for i = #dirseparators, 1, -1 do
dir = dir:sub(1, dirseparators[i] - 1)
for _, subdir in ipairs(paths) do
local full_path = dir .. subdir
local attr = lfs.attributes(full_path)
if attr then
dir = full_path
goto FOUND
end
end
end
error[[CRITICAL: Initialization script not found]]
::FOUND::
-- table.insert(arg, 1, "--lua=" .. dir)
-- table.insert(arg, 1, "luametatex")
-- arg[0] = nil
-- local _, msg = os.exec(arg)
-- error(msg)
os.setenv("engine", status.luatex_engine)
local ret_value
if is_initex then
ret_value = os.execute(string.format("luametatex \"--lua=%s\" --arg0=\"%s\" \"%s\"", dir, os.selfarg[0], table.concat(os.selfarg, "\" \"")))
else
ret_value = os.execute(string.format("luametatex \"--fmt=%s\" \"--lua=%s\" --arg0=\"%s\" \"%s\"", format, dir, os.selfarg[0], table.concat(os.selfarg, "\" \"")))
end
os.exit(x)