commit 3815ab3963982ef43f583da9df267d93f6dcc5a5 Author: Marcel Fabian Krüger Date: Wed Jul 17 21:14:34 2019 +0200 Initial commit diff --git a/luametalatex-back-pdf.lua b/luametalatex-back-pdf.lua new file mode 100644 index 0000000..632367a --- /dev/null +++ b/luametalatex-back-pdf.lua @@ -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([[<>]], 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([[<>]], pfile:writepages())) + pfile:close() +end, "Finish PDF file") diff --git a/luametalatex-baseregisters.tex b/luametalatex-baseregisters.tex new file mode 100644 index 0000000..98a6e80 --- /dev/null +++ b/luametalatex-baseregisters.tex @@ -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 diff --git a/luametalatex-bit32.lua b/luametalatex-bit32.lua new file mode 100644 index 0000000..42dac55 --- /dev/null +++ b/luametalatex-bit32.lua @@ -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<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 diff --git a/luametalatex-font-sfnt.lua b/luametalatex-font-sfnt.lua new file mode 100644 index 0000000..9c6de8b --- /dev/null +++ b/luametalatex-font-sfnt.lua @@ -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]c4I2I2I2I2", magic, #tabdata, 1<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, +} diff --git a/luametalatex-font-t1.lua b/luametalatex-font-t1.lua new file mode 100644 index 0000000..11a6141 --- /dev/null +++ b/luametalatex-font-t1.lua @@ -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("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 diff --git a/luametalatex-font-t2-opt.lua b/luametalatex-font-t2-opt.lua new file mode 100644 index 0000000..0bf7afa --- /dev/null +++ b/luametalatex-font-t2-opt.lua @@ -0,0 +1,168 @@ +-- This is optimizet2.lua. +-- Copyright 2019 Marcel Krüger +-- +-- 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 diff --git a/luametalatex-font-t2.lua b/luametalatex-font-t2.lua new file mode 100644 index 0000000..9a5a363 --- /dev/null +++ b/luametalatex-font-t2.lua @@ -0,0 +1,66 @@ +-- This is serializet2.lua. +-- Copyright 2019 Marcel Krüger +-- +-- 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 diff --git a/luametalatex-font-tfm.lua b/luametalatex-font-tfm.lua new file mode 100644 index 0000000..fd0161c --- /dev/null +++ b/luametalatex-font-tfm.lua @@ -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 diff --git a/luametalatex-font-vf.lua b/luametalatex-font-vf.lua new file mode 100644 index 0000000..c67f44f --- /dev/null +++ b/luametalatex-font-vf.lua @@ -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 diff --git a/luametalatex-init.lua b/luametalatex-init.lua new file mode 100644 index 0000000..afc8445 --- /dev/null +++ b/luametalatex-init.lua @@ -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 diff --git a/luametalatex-nodewriter.lua b/luametalatex-nodewriter.lua new file mode 100644 index 0000000..562c010 --- /dev/null +++ b/luametalatex-nodewriter.lua @@ -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("<>", 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 diff --git a/luametalatex-pdf-font-cff.lua b/luametalatex-pdf-font-cff.lua new file mode 100644 index 0000000..fd22ecc --- /dev/null +++ b/luametalatex-pdf-font-cff.lua @@ -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> def +/CMapName /TeX-%s-0 def +/CMapType 2 def +1 begincodespacerange +<00> +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 diff --git a/luametalatex-pdf-font-cmap2.lua b/luametalatex-pdf-font-cmap2.lua new file mode 100644 index 0000000..14d24ff --- /dev/null +++ b/luametalatex-pdf-font-cmap2.lua @@ -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> +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 diff --git a/luametalatex-pdf-font-cmap3.lua b/luametalatex-pdf-font-cmap3.lua new file mode 100644 index 0000000..423e554 --- /dev/null +++ b/luametalatex-pdf-font-cmap3.lua @@ -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> + +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 diff --git a/luametalatex-pdf-font-enc.lua b/luametalatex-pdf-font-enc.lua new file mode 100644 index 0000000..56ac8e3 --- /dev/null +++ b/luametalatex-pdf-font-enc.lua @@ -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 diff --git a/luametalatex-pdf-font-map.lua b/luametalatex-pdf-font-map.lua new file mode 100644 index 0000000..25dc899 --- /dev/null +++ b/luametalatex-pdf-font-map.lua @@ -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 +} diff --git a/luametalatex-pdf-font-t1.lua b/luametalatex-pdf-font-t1.lua new file mode 100644 index 0000000..79eb2eb --- /dev/null +++ b/luametalatex-pdf-font-t1.lua @@ -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 diff --git a/luametalatex-pdf-font-ttf.lua b/luametalatex-pdf-font-ttf.lua new file mode 100644 index 0000000..8aa0ddd --- /dev/null +++ b/luametalatex-pdf-font-ttf.lua @@ -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 diff --git a/luametalatex-pdf-font.lua b/luametalatex-pdf-font.lua new file mode 100644 index 0000000..4247758 --- /dev/null +++ b/luametalatex-pdf-font.lua @@ -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( + "<>", + 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<>]], + [[%!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> +endcodespacerange +1 begincidrange +<00> 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<>]], + [[%!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 + +<00> <7F> +<8000> +endcodespacerange +3 begincidrange +<00> <7F> 0 +<8000> 128 + 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("<>", 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( + "<>/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( + "<>/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]>", + 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 diff --git a/luametalatex-pdf-pagetree.lua b/luametalatex-pdf-pagetree.lua new file mode 100644 index 0000000..878331d --- /dev/null +++ b/luametalatex-pdf-pagetree.lua @@ -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, '<>') + 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('<>', 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 diff --git a/luametalatex-pdf.lua b/luametalatex-pdf.lua new file mode 100644 index 0000000..695813e --- /dev/null +++ b/luametalatex-pdf.lua @@ -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, +} diff --git a/luametalatex.ini b/luametalatex.ini new file mode 100644 index 0000000..eb667c0 --- /dev/null +++ b/luametalatex.ini @@ -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 diff --git a/luametalatex.lua b/luametalatex.lua new file mode 100644 index 0000000..660db70 --- /dev/null +++ b/luametalatex.lua @@ -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)