luametalatex/luametalatex-pdf-font-cff.lua

592 lines
19 KiB
Lua
Raw Normal View History

2020-07-15 04:51:59 +02:00
local readfile = require'luametalatex-readfile'
2019-07-17 21:14:34 +02:00
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 then -- Use the built-in encoding
CharStrings = parse_encoding(buf, i0, top.Encoding, CharStrings)
elseif encoding then
2020-06-16 21:17:43 +02:00
encoding = require'luametalatex-font-enc'(encoding)
2019-07-17 21:14:34 +02:00
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 = {}
2020-07-31 12:04:02 +02:00
if usedcids then -- Subsetting maybeFIXME: Should be Disabled, because other tables have to be fixed up first -- Actually seems to work now, let's test it a bit more
2019-07-17 21:14:34 +02:00
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
2020-07-31 12:04:02 +02:00
-- Subrs subsetting... Instead of deleting unused SubRS, we only make them empty.
-- This avoids problems with renumberings which would have to be consitant across
-- Fonts in some odd way, because they might be used by globalsubrs.
2019-07-17 21:14:34 +02:00
for i=1,#glyphs do
local g = glyphs[i]
local private = top.Privates[g.cidfont or 1]
2020-07-31 12:04:02 +02:00
local parsed = parse_charstring(g.cs, top.GlobalSubrs, private.Subrs)
2019-07-17 21:14:34 +02:00
local width = parsed[1][2]
if width then
width = width + (private.nominalWidthX or 0)
else
width = private.defaultWidthX or 0
end
local m = top.FontMatrix or {.001, 0, 0, .001, 0, 0}
width = width * m[1] + m[3] -- I really have no idea why m[3] /= 0 might happen, but why not?
width = math.floor(width*1000+.5) -- Thats rescale into "PDF glyph space"
if g.usedcid[2] ~= width then print("MISMATCH:", g.usedcid[1], g.usedcid[2], width) end
g.usedcid[2] = width
end
for i=1,#top.GlobalSubrs do
if not top.GlobalSubrs.used[i] then
top.GlobalSubrs[i] = ""
end
end
for _, priv in ipairs(top.Privates) do if priv.Subrs then
for i=1,#priv.Subrs do
if not priv.Subrs.used[i] then
priv.Subrs[i] = ""
end
end
end end
else
for i, cs in pairs(CharStrings) do -- Not subsetting
glyphs[#glyphs+1] = {cs = buf:sub(cs[1], cs[2]), index = i, cidfont = cs.font}
end
end
top.glyphs = glyphs
table.sort(glyphs, function(a,b)return a.index<b.index end)
local bbox
if top.FontMatrix then
local x0, y0 = apply_matrix(top.FontMatrix, top.FontBBox[1], top.FontBBox[2])
local x1, y1 = apply_matrix(top.FontMatrix, top.FontBBox[3], top.FontBBox[4])
bbox = {x0, y0, x1, y1}
else
bbox = top.FontBBox
end
return require'luametalatex-font-cff'(top), bbox
end
2020-07-15 04:51:59 +02:00
2020-07-06 13:30:03 +02:00
return function(filename, fontid, encoding) return function(fontdir, usedcids)
2020-07-15 16:36:11 +02:00
local file <close> = readfile('opentype', filename)
2020-07-15 04:51:59 +02:00
local buf = file()
2019-07-17 21:14:34 +02:00
local i = 1
local magic = buf:sub(1, 4)
if magic == "ttcf" or magic == "OTTO" then
-- assert(not encoding) -- nil or false
encoding = encoding or false
2020-07-06 13:30:03 +02:00
local magic, tables = sfnt.parse(buf, fontid) -- TODO: Interpret widths etc, they might differ from the CFF ones.
2019-07-17 21:14:34 +02:00
assert(magic == "OTTO")
-- Also CFF2 would be nice to have
i = tables['CFF '][1]
end
2020-07-06 13:30:03 +02:00
local content, bbox = myfunc(buf, i, fontid, usedcids, encoding)
2019-07-17 21:14:34 +02:00
fontdir.bbox = bbox
return content
end end