local function parse_charstring(cs, subrs, result) result = result or {{false}} local lastresult = result[#result] local i = 1 while i~=#cs+1 do local cmd = cs:byte(i) if cmd == 255 then lastresult[#lastresult+1] = string.unpack(">i4", cs:sub(i+1, i+4)) i = i+4 elseif cmd >= 251 then lastresult[#lastresult+1] = -((cmd-251)*256)-string.byte(cs, i+1)-108 i = i+1 elseif cmd >= 247 then lastresult[#lastresult+1] = (cmd-247)*256+string.byte(cs, i+1)+108 i = i+1 elseif cmd >= 32 then lastresult[#lastresult+1] = cmd-139 elseif cmd == 9 then -- closepath, implicit in Type2 elseif cmd == 10 then local subr = subrs[lastresult[#lastresult]] lastresult[#lastresult] = nil parse_charstring(subr, subrs, result) lastresult = result[#result] elseif cmd == 11 then break -- We do not keep subroutines, so drop returns and continue with the outer commands elseif cmd == 12 then i = i+1 cmd = cs:byte(i) if cmd == 12 then -- div, we might have huge parameters, so execute directly lastresult[#lastresult-1] = lastresult[#lastresult-1]/lastresult[#lastresult] lastresult[#lastresult] = nil elseif cmd == 16 then -- othersubr... cmd = lastresult[#lastresult] lastresult[#lastresult] = nil local numargs = lastresult[#lastresult] lastresult[#lastresult] = nil if cmd == 3 then -- Hint replacement. This is easy, we support hint replacement, so we -- keep the original subr number assert(numargs == 1) elseif cmd == 1 then -- Flex initialization elseif cmd == 2 then -- Flex parameter if result[#result-1].flex then result[#result] = nil -- TODO: Warn if there were additional values in lastresult. 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 if lastresult.pendingargs then for i = 1,#lastresult.pendingargs do pending[numargs+i] = lastresult.pendingargs[i] end end if cmd == 12 then lastresult.pendingargs = pending else lastresult.pendingargs = nil local n = pending[1] local i = 2 local groups = {} for group = 1, n do local current = {20} local last = 0 while pending[i+1] > 0 do last = last + pending[i] current[#current+1] = {1, last, pending[i+1]} last = last + pending[i+1] i = i+2 end last = last + pending[i] current[#current+1] = {1, last + pending[i+1], -pending[i+1]} groups[group] = current i = i+2 end n = pending[i] i = i+1 for group = 1, n do local current = groups[group] or {20} local last = 0 while pending[i+1] > 0 do last = last + pending[i] current[#current+1] = {3, last, pending[i+1]} last = last + pending[i+1] i = i+2 end last = last + pending[i] current[#current+1] = {3, last + pending[i+1], -pending[i+1]} groups[group] = current i = i+2 end assert(i == #pending+1) table.move(groups, 1, #groups, #result, result) -- This overwrites lastresult result[#result+1] = lastresult -- And restore lastresult 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 use counter hinting: Please test this -- and report back if it works, because this is pretty much untested. local stems = {} local stem3 = {20} local cntrs = {} -- First iterate over the charstring, recording all hints and collecting them in stems/stem3/cntrs 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 elseif cmd[1] == 20 then cntrs[#cntrs+1] = cmd table.move(cmd, 2, #cmd, #stems+1, stems) 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 counters or stem3 occured. -- This is done before writing the stem list to make the thable.insert parameters easier. -- First translate stem3 into a counter group if stem3[2] then cntrs[#cntrs+1] = stem3 table.insert(cs, 2, stem3) end local bytes = {} for i=1, #cntrs do local cntr = cntrs[i] for l = 1, math.floor((j + 7)/8) do bytes[l] = 0 end for l = 2, #cntr do local idx = cntr[l].idx-1 bytes[math.floor(idx/8) + 1] = bytes[math.floor(idx/8) + 1] | (1<<(7-idx%8)) cntr[l] = nil end cntr[2] = 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 if stem3[2] then local s3 = stem3[2] for l = 1, math.floor((j + 7)/8) do bytes[l] = string.byte(s3, l) end else for l = 1, math.floor((j + 7)/8) do bytes[l] = 0 end 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 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