if not modules then modules = { } end modules ['font-dsp'] = { version = 1.002, optimize = true, comment = "companion to font-ini.mkiv", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } -- many 0,0 entry/exit -- This loader went through a few iterations. First I made a ff compatible one so -- that we could do some basic checking. Also some verbosity was added (named -- glyphs). Eventually all that was dropped for a context friendly format, simply -- because keeping the different table models in sync too to much time. I have the -- old file somewhere. A positive side effect is that we get an (upto) much smaller -- smaller tma/tmc file. In the end the loader will be not much slower than the -- c based ff one. -- Being binary encoded, an opentype is rather compact. When expanded into a Lua table -- quite some memory can be used. This is very noticeable in the ff loader, which for -- a good reason uses a verbose format. However, when we use that data we create a couple -- of hashes. In the Lua loader we create these hashes directly, which save quite some -- memory. -- -- We convert a font file only once and then cache it. Before creating the cached instance -- packing takes place: common tables get shared. After (re)loading and unpacking we then -- get a rather efficient internal representation of the font. In the new loader there is a -- pitfall. Because we use some common coverage magic we put a bit more information in -- the mark and cursive coverage tables than strickly needed: a reference to the coverage -- itself. This permits a fast lookup of the second glyph involved. In the marks we -- expand the class indicator to a class hash, in the cursive we use a placeholder that gets -- a self reference. This means that we cannot pack these subtables unless we add a unique -- id per entry (the same one per coverage) and that makes the tables larger. Because only a -- few fonts benefit from this, I decided to not do this. Experiments demonstrated that it -- only gives a few percent gain (on for instance husayni we can go from 845K to 828K -- bytecode). Better stay conceptually clean than messy compact. -- When we can reduce all basic lookups to one step we might safe a bit in the processing -- so then only chains are multiple. -- I used to flatten kerns here but that has been moved elsewhere because it polutes the code -- here and can be done fast afterwards. One can even wonder if it makes sense to do it as we -- pack anyway. In a similar fashion the unique placeholders in anchors in marks have been -- removed because packing doesn't save much there anyway. -- Although we have a bit more efficient tables in the cached files, the internals are still -- pretty similar. And although we have a slightly more direct coverage access the processing -- of node lists is not noticeable faster for latin texts, but for arabic we gain some 10% -- (and could probably gain a bit more). -- All this packing in the otf format is somewhat obsessive as nowadays 4K resolution -- multi-gig videos pass through our networks and storage and memory is abundant. -- Although we use a few table readers there is no real gain in there (apart from having -- less code. After all there are often not that many demanding features. -- The color v2 spec, gasp and bitmap readers were done with the help of Codex Luna 2026 as -- we have no fonts to check it, so that is on the todo. local next, type, tonumber = next, type, tonumber local gsub = string.gsub local lower = string.lower local sub = string.sub local strip = string.strip local tohash = table.tohash local concat = table.concat local copy = table.copy local reversed = table.reversed local sort = table.sort local insert = table.insert local round = math.round local floor = math.floor local ceil = math.ceil local settings_to_hash = utilities.parsers.settings_to_hash_colon_too local setmetatableindex = table.setmetatableindex local formatters = string.formatters local sortedkeys = table.sortedkeys local sortedhash = table.sortedhash local sequenced = table.sequenced local report = logs.reporter("otf reader") local readers = fonts.handlers.otf.readers local streamreader = readers.streamreader local setposition = streamreader.setposition local getposition = streamreader.getposition local readuinteger = streamreader.readcardinal1 local readushort = streamreader.readcardinal2 local readuoffset = streamreader.readcardinal3 local readulong = streamreader.readcardinal4 local readinteger = streamreader.readinteger1 local readshort = streamreader.readinteger2 local readstring = streamreader.readstring local readtag = streamreader.readtag local readbytes = streamreader.readbytes local readfixed = streamreader.readfixed4 local read2dot14 = streamreader.read2dot14 local skipshort = streamreader.skipshort local skipbytes = streamreader.skip local readbytetable = streamreader.readbytetable local readbyte = streamreader.readbyte local readcardinaltable = streamreader.readcardinaltable local readintegertable = streamreader.readintegertable local readfword = readshort local short = 2 local ushort = 2 local uoffset = 3 local ulong = 4 directives.register("fonts.streamreader",function() streamreader = utilities.streams setposition = streamreader.setposition getposition = streamreader.getposition readuinteger = streamreader.readcardinal1 readushort = streamreader.readcardinal2 readuoffset = streamreader.readcardinal3 readulong = streamreader.readcardinal4 readinteger = streamreader.readinteger1 readshort = streamreader.readinteger2 readstring = streamreader.readstring readtag = streamreader.readtag readbytes = streamreader.readbytes readfixed = streamreader.readfixed4 read2dot14 = streamreader.read2dot14 skipshort = streamreader.skipshort skipbytes = streamreader.skip readbytetable = streamreader.readbytetable readbyte = streamreader.readbyte readcardinaltable = streamreader.readcardinaltable readintegertable = streamreader.readintegertable readfword = readshort end) local gsubhandlers = { } local gposhandlers = { } readers.gsubhandlers = gsubhandlers readers.gposhandlers = gposhandlers local attachments = false local bitmapdata = false local helpers = readers.helpers local gotodatatable = helpers.gotodatatable local setvariabledata = helpers.setvariabledata local lookupidoffset = -1 -- will become 1 when we migrate (only -1 for comparing with old) local classes = { "base", "ligature", "mark", "component", } local gsubtypes = { "single", "multiple", "alternate", "ligature", "context", "chainedcontext", "extension", "reversechainedcontextsingle", } local gpostypes = { "single", "pair", "cursive", "marktobase", "marktoligature", "marktomark", "context", "chainedcontext", "extension", } local chaindirections = { context = 0, chainedcontext = 1, reversechainedcontextsingle = -1, } local function setmetrics(data,where,tag,d) local w = data[where] if w then local v = w[tag] w[tag] = (v or 0) + d end end local function setgasp(data,index,d) local gasp = data.gasp local ranges = gasp and gasp.ranges local range = ranges and ranges[index] if range then range.maxPPEM = (range.maxPPEM or 0) + d end end local variabletags = { hasc = function(data,d) setmetrics(data,"windowsmetrics","typoascender",d) end, hdsc = function(data,d) setmetrics(data,"windowsmetrics","typodescender",d) end, hlgp = function(data,d) setmetrics(data,"windowsmetrics","typolinegap",d) end, hcla = function(data,d) setmetrics(data,"windowsmetrics","winascent",d) end, hcld = function(data,d) setmetrics(data,"windowsmetrics","windescent",d) end, vasc = function(data,d) setmetrics(data,"verticalheader","ascender",d) end, vdsc = function(data,d) setmetrics(data,"verticalheader","descender",d) end, vlgp = function(data,d) setmetrics(data,"verticalheader","linegap",d) end, hcrs = function(data,d) setmetrics(data,"horizontalheader","caretsloperise",d) end, hcrn = function(data,d) setmetrics(data,"horizontalheader","caretsloperun",d) end, hcof = function(data,d) setmetrics(data,"horizontalheader","caretoffset",d) end, vcrs = function(data,d) setmetrics(data,"verticalheader","caretsloperise",d) end, vcrn = function(data,d) setmetrics(data,"verticalheader","caretsloperun",d) end, vcof = function(data,d) setmetrics(data,"verticalheader","caretoffset",d) end, xhgt = function(data,d) setmetrics(data,"windowsmetrics","xheight",d) end, cpht = function(data,d) setmetrics(data,"windowsmetrics","capheight",d) end, sbxs = function(data,d) setmetrics(data,"windowsmetrics","subscriptxsize",d) end, sbys = function(data,d) setmetrics(data,"windowsmetrics","subscriptysize",d) end, sbxo = function(data,d) setmetrics(data,"windowsmetrics","subscriptxoffset",d) end, sbyo = function(data,d) setmetrics(data,"windowsmetrics","subscriptyoffset",d) end, spxs = function(data,d) setmetrics(data,"windowsmetrics","superscriptxsize",d) end, spys = function(data,d) setmetrics(data,"windowsmetrics","superscriptysize",d) end, spxo = function(data,d) setmetrics(data,"windowsmetrics","superscriptxoffset",d) end, spyo = function(data,d) setmetrics(data,"windowsmetrics","superscriptyoffset",d) end, strs = function(data,d) setmetrics(data,"windowsmetrics","strikeoutsize",d) end, stro = function(data,d) setmetrics(data,"windowsmetrics","strikeoutpos",d) end, unds = function(data,d) setmetrics(data,"postscript","underlinethickness",d) end, undo = function(data,d) setmetrics(data,"postscript","underlineposition",d) end, gsp0 = function(data,d) setgasp(data,1,d) end, gsp1 = function(data,d) setgasp(data,2,d) end, gsp2 = function(data,d) setgasp(data,3,d) end, gsp3 = function(data,d) setgasp(data,4,d) end, gsp4 = function(data,d) setgasp(data,5,d) end, gsp5 = function(data,d) setgasp(data,6,d) end, gsp6 = function(data,d) setgasp(data,7,d) end, gsp7 = function(data,d) setgasp(data,8,d) end, gsp8 = function(data,d) setgasp(data,9,d) end, gsp9 = function(data,d) setgasp(data,10,d) end, } local read_cardinal = { streamreader.readcardinal1, streamreader.readcardinal2, streamreader.readcardinal3, streamreader.readcardinal4, } local read_integer = { streamreader.readinteger1, streamreader.readinteger2, streamreader.readinteger3, streamreader.readinteger4, } directives.register("fonts.streamreader",function() read_cardinal = { streamreader.readcardinal1, streamreader.readcardinal2, streamreader.readcardinal3, streamreader.readcardinal4, } read_integer = { streamreader.readinteger1, streamreader.readinteger2, streamreader.readinteger3, streamreader.readinteger4, } end) -- Traditionally we use these unique names (so that we can flatten the lookup list -- (we create subsets runtime) but I will adapt the old code to newer names. -- chainsub -- reversesub local lookupnames = { gsub = { single = "gsub_single", multiple = "gsub_multiple", alternate = "gsub_alternate", ligature = "gsub_ligature", context = "gsub_context", chainedcontext = "gsub_contextchain", reversechainedcontextsingle = "gsub_reversecontextchain", -- reversesub }, gpos = { single = "gpos_single", pair = "gpos_pair", cursive = "gpos_cursive", marktobase = "gpos_mark2base", marktoligature = "gpos_mark2ligature", marktomark = "gpos_mark2mark", context = "gpos_context", chainedcontext = "gpos_contextchain", } } -- keep this as reference: -- -- local lookupbits = { -- [0x0001] = "righttoleft", -- [0x0002] = "ignorebaseglyphs", -- [0x0004] = "ignoreligatures", -- [0x0008] = "ignoremarks", -- [0x0010] = "usemarkfilteringset", -- [0x00E0] = "reserved", -- [0xFF00] = "markattachmenttype", -- } -- -- local lookupstate = setmetatableindex(function(t,k) -- local v = { } -- for kk, vv in next, lookupbits do -- if (k & kk) ~= 0 then -- v[vv] = true -- end -- end -- t[k] = v -- return v -- end) local lookupflags = setmetatableindex(function(t,k) local v = { (k & 0x0008) ~= 0 and true or false, -- ignoremarks (k & 0x0004) ~= 0 and true or false, -- ignoreligatures (k & 0x0002) ~= 0 and true or false, -- ignorebaseglyphs (k & 0x0001) ~= 0 and true or false, -- r2l } t[k] = v return v end) -- Variation stores: it's not entirely clear if the regions are a shared -- resource (it looks like they are). Anyway, we play safe and use a -- share. -- values can be anything the min/max permits so we can either think of -- real values of a fraction along the axis (probably easier) -- wght=400,wdth=100,ital=1 local function axistofactors(str) local t = settings_to_hash(str) for k, v in next, t do t[k] = tonumber(v) or v -- this also normalizes numbers itself end return t end local hash = table.setmetatableindex(function(t,k) local v = sequenced(axistofactors(k),",") t[k] = v return v end) helpers.normalizedaxishash = hash local cleanname = fonts.names and fonts.names.cleanname or function(name) return name and (gsub(lower(name),"[^%a%d]","")) or nil end helpers.cleanname = cleanname function helpers.normalizedaxis(str) return hash[str] or str end -- contradicting spec ... (signs) so i'll check it and fix it once we have -- proper fonts local function getaxisscale(segments,minimum,default,maximum,user) -- -- returns the right values cf example in standard -- if not minimum or not default or not maximum then return 0 end if type(user) ~= "number" then return 0 end if user < minimum then user = minimum elseif user > maximum then user = maximum end if user < default then default = - (default - user) / (default - minimum) elseif user > default then default = (user - default) / (maximum - default) else default = 0 end if not segments then return default end local first = segments[1] if type(first) ~= "table" then return default elseif default <= first[1] then return first[2] end local previous = first for i=2,#segments do local current = segments[i] if type(current) ~= "table" then break elseif default <= current[1] then local span = current[1] - previous[1] if span == 0 then return current[2] else return previous[2] + (current[2] - previous[2]) * (default - previous[1]) / span end end previous = current end return previous[2] end local function getfactors(data,instancespec) if instancespec == true then -- take default elseif type(instancespec) ~= "string" or instancespec == "" then return end local variabledata = data.variabledata if not variabledata then return end local instances = variabledata.instances local axis = variabledata.axis local segments = variabledata.segments if instances and axis then local values if instancespec == true then -- first instance: -- values = instances[1].values -- axis defaults: values = { } for i=1,#axis do values[i] = { -- axis = axis[i].tag, value = axis[i].default, } end else for i=1,#instances do local instance = instances[i] if cleanname(instance.subfamily) == instancespec then values = instance.values break end end end if values then local factors = { } for i=1,#axis do local a = axis[i] factors[i] = getaxisscale(segments and segments[i],a.minimum,a.default,a.maximum,values[i].value) end return factors end local values = axistofactors(hash[instancespec] or instancespec) if values then local factors = { } for i=1,#axis do local a = axis[i] local d = a.default factors[i] = getaxisscale(segments and segments[i],a.minimum,d,a.maximum,values[a.name or a.tag] or values[a.tag] or d) end return factors end end end local function getscales(regions,factors) local scales = { } for i=1,#regions do local region = regions[i] local s = 1 for j=1,#region do local axis = region[j] local f = factors[j] local start = axis.start local peak = axis.peak local stop = axis.stop -- get rid of these tests, false flag if start > peak or peak > stop or (start < 0 and stop > 0 and peak ~= 0) then s = 0 break elseif peak == 0 then -- * 1 elseif f < start or f > stop then -- * 0 s = 0 break elseif f < peak then -- s = - s * (f - start) / (peak - start) s = s * (f - start) / (peak - start) elseif f > peak then s = s * (stop - f) / (stop - peak) else -- * 1 end end scales[i] = s end return scales end helpers.getaxisscale = getaxisscale helpers.getfactors = getfactors helpers.getscales = getscales helpers.axistofactors = axistofactors local function readvariationdata(f,storeoffset,factors) -- store local position = getposition(f) setposition(f,storeoffset) -- header local format = readushort(f) local regionoffset = storeoffset + readulong(f) local nofdeltadata = readushort(f) local deltadata = readcardinaltable(f,nofdeltadata,ulong) -- regions setposition(f,regionoffset) local nofaxis = readushort(f) local nofregions = readushort(f) local regions = { } for i=1,nofregions do -- 0 local t = { } for i=1,nofaxis do t[i] = { -- maybe no keys, just 1..3 start = read2dot14(f), peak = read2dot14(f), stop = read2dot14(f), } end regions[i] = t end -- deltas -- if factors then for i=1,nofdeltadata do setposition(f,storeoffset+deltadata[i]) local nofdeltasets = readushort(f) local worddeltacount = readushort(f) local longwords = (worddeltacount & 0x8000) ~= 0 local nofshorts = worddeltacount & 0x7FFF local nofregions = readushort(f) local usedregions = { } local deltas = { } for i=1,nofregions do usedregions[i] = regions[readushort(f)+1] end -- we could test before and save a for for i=1,nofdeltasets do local t = readintegertable(f,nofshorts,longwords and ulong or short) for i=nofshorts+1,nofregions do t[i] = longwords and readshort(f) or readinteger(f) end deltas[i] = t end deltadata[i] = { regions = usedregions, deltas = deltas, scales = factors and getscales(usedregions,factors) or nil, } end -- end setposition(f,position) return regions, deltadata end helpers.readvariationdata = readvariationdata -- Beware: only use the simple variant if we don't set keys/values (otherwise too many entries). We -- could also have a variant that applies a function but there is no real benefit in this. local function readcoverage(f,offset,simple) setposition(f,offset) local coverageformat = readushort(f) if coverageformat == 1 then local nofcoverage = readushort(f) if simple then -- often 1 or 2 if nofcoverage == 1 then return { readushort(f) } elseif nofcoverage == 2 then return { readushort(f), readushort(f) } else return readcardinaltable(f,nofcoverage,ushort) end elseif nofcoverage == 1 then return { [readushort(f)] = 0 } elseif nofcoverage == 2 then return { [readushort(f)] = 0, [readushort(f)] = 1 } else local coverage = { } for i=0,nofcoverage-1 do coverage[readushort(f)] = i -- index in record end return coverage end elseif coverageformat == 2 then local nofranges = readushort(f) local coverage = { } local n = simple and 1 or 0 for i=1,nofranges do local firstindex = readushort(f) local lastindex = readushort(f) local coverindex = readushort(f) n = (simple and coverindex + 1 or coverindex) if simple then for i=firstindex,lastindex do coverage[n] = i n = n + 1 end else for i=firstindex,lastindex do coverage[i] = n n = n + 1 end end end return coverage else report("unknown coverage format %a ",coverageformat) return { } end end local function readclassdef(f,offset,preset) setposition(f,offset) local classdefformat = readushort(f) local classdef = { } if type(preset) == "number" then for k=0,preset-1 do classdef[k] = 1 end end if classdefformat == 1 then local index = readushort(f) local nofclassdef = readushort(f) for i=1,nofclassdef do classdef[index] = readushort(f) + 1 index = index + 1 end elseif classdefformat == 2 then local nofranges = readushort(f) local n = 0 for i=1,nofranges do local firstindex = readushort(f) local lastindex = readushort(f) local class = readushort(f) + 1 for i=firstindex,lastindex do classdef[i] = class end end else report("unknown classdef format %a ",classdefformat) end if type(preset) == "table" then for k in next, preset do if not classdef[k] then classdef[k] = 1 end end end return classdef end local function classtocoverage(defs) if defs then local list = { } for index, class in next, defs do local c = list[class] if c then c[#c+1] = index else list[class] = { index } end end return list end end -- extra readers local skips = { [0] = 0, -- ---- 1, -- ---x 1, -- --y- 2, -- --yx 1, -- -h-- 2, -- -h-x 2, -- -hy- 3, -- -hyx 2, -- v--x 2, -- v-y- 3, -- v-yx 2, -- vh-- 3, -- vh-x 3, -- vhy- 4, -- vhyx } -- We can assume that 0 is nothing and in fact we can start at 1 as -- usual in Lua to make sure of that. local function readvariation(f,offset) local p = getposition(f) setposition(f,offset) local outer = readushort(f) local inner = readushort(f) local format = readushort(f) setposition(f,p) if format == 0x8000 then return outer, inner end end local function readdevice(f,offset,ppem) if not offset or offset == 0 then return 0 end local position = getposition(f) setposition(f,offset) local start = readushort(f) local stop = readushort(f) local format = readushort(f) local delta = 0 if ppem and ppem >= start and ppem <= stop then local index = ppem - start local bits, perword, mask, signed if format == 1 then bits, perword, mask, signed = 2, 8, 0x3, 0x2 elseif format == 2 then bits, perword, mask, signed = 4, 4, 0xF, 0x8 elseif format == 3 then bits, perword, mask, signed = 8, 2, 0xFF, 0x80 end if bits then local word = floor(index / perword) local slot = index % perword setposition(f,offset + 6 + word * 2) local value = (readushort(f) >> ((perword - slot - 1) * bits)) & mask if value >= signed then value = value - (mask + 1) end delta = value end end setposition(f,position) return delta end local function readdevicevalue(f,offset,getdelta,getdevice) if not offset or offset == 0 then return 0 end local position = getposition(f) setposition(f,offset) local outer = readushort(f) local inner = readushort(f) local format = readushort(f) setposition(f,position) if format == 0x8000 then return getdelta and getdelta(outer,inner) or 0 elseif getdevice then return getdevice(f,offset) or 0 else return 0 end end helpers.readdevice = readdevice local function setdevicecallback(fontdata,specification) local temporary = fontdata.temporary or { } fontdata.temporary = temporary local ppem = specification and specification.ppem if not ppem and specification and specification.size then local size = tonumber(specification.size) ppem = size and round(size / 65536) or nil end if ppem and ppem > 0 then temporary.getdevice = function(f,offset) return readdevice(f,offset,ppem) end else temporary.getdevice = nil end return temporary.getdevice end local function readposition(f,format,mainoffset,getdelta,getdevice) if format == 0 then return false end -- a few happen often if format == 0x04 then local h = readshort(f) if h == 0 then return true -- all zero else return { 0, 0, h, 0 } end end if format == 0x05 then local x = readshort(f) local h = readshort(f) if x == 0 and h == 0 then return true -- all zero else return { x, 0, h, 0 } end end if format == 0x44 then local h = readshort(f) if getdelta or getdevice then local d = readushort(f) if d > 0 then h = h + readdevicevalue(f,mainoffset+d,getdelta,getdevice) end else readushort(f) end if h == 0 then return true -- all zero else return { 0, 0, h, 0 } end end -- -- todo: -- -- if format == 0x55 then -- local x = readshort(f) -- local h = readshort(f) -- .... -- end -- local x = (format & 0x1) ~= 0 and readshort(f) or 0 -- x placement local y = (format & 0x2) ~= 0 and readshort(f) or 0 -- y placement local h = (format & 0x4) ~= 0 and readshort(f) or 0 -- h advance local v = (format & 0x8) ~= 0 and readshort(f) or 0 -- v advance if format >= 0x10 then local X = (format & 0x10) ~= 0 and readushort(f) or 0 local Y = (format & 0x20) ~= 0 and readushort(f) or 0 local H = (format & 0x40) ~= 0 and readushort(f) or 0 local V = (format & 0x80) ~= 0 and readushort(f) or 0 if getdelta or getdevice then if X > 0 then x = x + readdevicevalue(f,mainoffset+X,getdelta,getdevice) end if Y > 0 then y = y + readdevicevalue(f,mainoffset+Y,getdelta,getdevice) end if H > 0 then h = h + readdevicevalue(f,mainoffset+H,getdelta,getdevice) end if V > 0 then v = v + readdevicevalue(f,mainoffset+V,getdelta,getdevice) end end return { x, y, h, v } elseif x == 0 and y == 0 and h == 0 and v == 0 then return true -- all zero else return { x, y, h, v } end end local function readanchor(f,offset,getdelta,getdevice) -- maybe also ignore 0's as in pos if not offset or offset == 0 then return nil -- false end setposition(f,offset) -- no need to skip as we position each local format = readushort(f) -- 1: x y 2: x y point 3 x y X Y local x = readshort(f) local y = readshort(f) if format == 3 then if getdelta or getdevice then local X = readushort(f) local Y = readushort(f) if X > 0 then x = x + readdevicevalue(f,offset+X,getdelta,getdevice) end if Y > 0 then y = y + readdevicevalue(f,offset+Y,getdelta,getdevice) end else skipshort(f,2) end return { x, y } elseif format == 2 then return { x, y, point = readushort(f) } else return { x, y } end end -- common handlers: inlining can be faster but we cache anyway -- so we don't bother too much about speed here local function readfirst(f,offset) if offset then setposition(f,offset) end return { readushort(f) } end -- quite often 0, 1, 2 local function readarray(f,offset) if offset then setposition(f,offset) end local n = readushort(f) if n == 1 then return { readushort(f) }, 1 elseif n > 0 then return readcardinaltable(f,n,ushort), n end end local function readcoveragearray(f,offset,t,simple) if not t then return nil end local n = #t if n == 0 then return nil end for i=1,n do t[i] = readcoverage(f,offset+t[i],simple) end return t end local function covered(subset,all) local used, u for i=1,#subset do local s = subset[i] if all[s] then if used then u = u + 1 used[u] = s else u = 1 used = { s } end end end return used end -- We generalize the chained lookups so that we can do with only one handler -- when processing them. -- pruned local function readlookuparray(f,noflookups,nofcurrent) local lookups = { } if noflookups > 0 then local length = 0 for i=1,noflookups do local index = readushort(f) + 1 if index > length then length = index end local lookup = readushort(f) + 1 local list = lookups[index] if list then list[#list+1] = lookup else lookups[index] = { lookup } end end for index=1,length do if not lookups[index] then lookups[index] = false end end -- if length > nofcurrent then -- report("more lookups than currently matched characters") -- end end return lookups end -- not pruned -- -- local function readlookuparray(f,noflookups,nofcurrent) -- local lookups = { } -- for i=1,nofcurrent do -- lookups[i] = false -- end -- for i=1,noflookups do -- local index = readushort(f) + 1 -- if index > nofcurrent then -- report("more lookups than currently matched characters") -- for i=nofcurrent+1,index-1 do -- lookups[i] = false -- end -- nofcurrent = index -- end -- lookups[index] = readushort(f) + 1 -- end -- return lookups -- end local function unchainedcontext(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs,what) local tableoffset = lookupoffset + offset setposition(f,tableoffset) local subtype = readushort(f) if subtype == 1 then local coverage = readushort(f) local subclasssets = readarray(f) local rules = { } if subclasssets then coverage = readcoverage(f,tableoffset+coverage,true) for i=1,#subclasssets do local offset = subclasssets[i] if offset > 0 then local firstcoverage = coverage[i] local rulesoffset = tableoffset + offset local subclassrules = readarray(f,rulesoffset) for rule=1,#subclassrules do setposition(f,rulesoffset + subclassrules[rule]) local nofcurrent = readushort(f) local noflookups = readushort(f) local current = { { firstcoverage } } for i=2,nofcurrent do current[i] = { readushort(f) } end local lookups = readlookuparray(f,noflookups,nofcurrent) rules[#rules+1] = { current = current, lookups = lookups } end end end else report("empty subclassset in %a subtype %i","unchainedcontext",subtype) end return { format = "glyphs", rules = rules, } elseif subtype == 2 then -- We expand the classes as later on we do a pack over the whole table so then we get -- back efficiency. This way we can also apply the coverage to the first current. local coverage = readushort(f) local currentclassdef = readushort(f) local subclasssets = readarray(f) local rules = { } if subclasssets then coverage = readcoverage(f,tableoffset + coverage) currentclassdef = readclassdef(f,tableoffset + currentclassdef,coverage) local currentclasses = classtocoverage(currentclassdef,fontdata.glyphs) for class=1,#subclasssets do local offset = subclasssets[class] if offset > 0 then local firstcoverage = currentclasses[class] if firstcoverage then firstcoverage = covered(firstcoverage,coverage) -- bonus if firstcoverage then local rulesoffset = tableoffset + offset local subclassrules = readarray(f,rulesoffset) for rule=1,#subclassrules do setposition(f,rulesoffset + subclassrules[rule]) local nofcurrent = readushort(f) local noflookups = readushort(f) local current = { firstcoverage } for i=2,nofcurrent do current[i] = currentclasses[readushort(f) + 1] end local lookups = readlookuparray(f,noflookups,nofcurrent) rules[#rules+1] = { current = current, lookups = lookups } end else report("no coverage") end else report("no coverage class") end end end else report("empty subclassset in %a subtype %i","unchainedcontext",subtype) end return { format = "class", rules = rules, } elseif subtype == 3 then local nofglyphs = readushort(f) local noflookups = readushort(f) local current = readcardinaltable(f,nofglyphs,ushort) local lookups = readlookuparray(f,noflookups,#current) current = readcoveragearray(f,tableoffset,current,true) return { format = "coverage", rules = { { current = current, lookups = lookups, } } } else report("unsupported subtype %a in %a %s",subtype,"unchainedcontext",what) end end -- todo: optimize for n=1 ? -- class index needs checking, probably no need for +1 local function chainedcontext(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs,what) local tableoffset = lookupoffset + offset setposition(f,tableoffset) local subtype = readushort(f) if subtype == 1 then local coverage = readushort(f) local subclasssets = readarray(f) local rules = { } if subclasssets then coverage = readcoverage(f,tableoffset+coverage,true) for i=1,#subclasssets do local offset = subclasssets[i] if offset > 0 then local firstcoverage = coverage[i] local rulesoffset = tableoffset + offset local subclassrules = readarray(f,rulesoffset) for rule=1,#subclassrules do setposition(f,rulesoffset + subclassrules[rule]) local nofbefore = readushort(f) local before if nofbefore > 0 then before = { } for i=1,nofbefore do before[i] = { readushort(f) } end end local nofcurrent = readushort(f) local current = { { firstcoverage } } for i=2,nofcurrent do current[i] = { readushort(f) } end local nofafter = readushort(f) local after if nofafter > 0 then after = { } for i=1,nofafter do after[i] = { readushort(f) } end end local noflookups = readushort(f) local lookups = readlookuparray(f,noflookups,nofcurrent) rules[#rules+1] = { before = before, current = current, after = after, lookups = lookups, } end end end else report("empty subclassset in %a subtype %i","chainedcontext",subtype) end return { format = "glyphs", rules = rules, } elseif subtype == 2 then local coverage = readushort(f) local beforeclassdef = readushort(f) local currentclassdef = readushort(f) local afterclassdef = readushort(f) local subclasssets = readarray(f) local rules = { } if subclasssets then local coverage = readcoverage(f,tableoffset + coverage) local beforeclassdef = readclassdef(f,tableoffset + beforeclassdef,nofglyphs) local currentclassdef = readclassdef(f,tableoffset + currentclassdef,coverage) local afterclassdef = readclassdef(f,tableoffset + afterclassdef,nofglyphs) local beforeclasses = classtocoverage(beforeclassdef,fontdata.glyphs) local currentclasses = classtocoverage(currentclassdef,fontdata.glyphs) local afterclasses = classtocoverage(afterclassdef,fontdata.glyphs) for class=1,#subclasssets do local offset = subclasssets[class] if offset > 0 then local firstcoverage = currentclasses[class] if firstcoverage then firstcoverage = covered(firstcoverage,coverage) -- bonus if firstcoverage then local rulesoffset = tableoffset + offset local subclassrules = readarray(f,rulesoffset) for rule=1,#subclassrules do -- watch out, in context we first get the counts and then the arrays while -- here we get them mixed setposition(f,rulesoffset + subclassrules[rule]) local nofbefore = readushort(f) local before if nofbefore > 0 then before = { } for i=1,nofbefore do before[i] = beforeclasses[readushort(f) + 1] end end local nofcurrent = readushort(f) local current = { firstcoverage } for i=2,nofcurrent do current[i] = currentclasses[readushort(f)+ 1] end local nofafter = readushort(f) local after if nofafter > 0 then after = { } for i=1,nofafter do after[i] = afterclasses[readushort(f) + 1] end end -- no sequence index here (so why in context as it saves nothing) local noflookups = readushort(f) local lookups = readlookuparray(f,noflookups,nofcurrent) rules[#rules+1] = { before = before, current = current, after = after, lookups = lookups, } end else report("no coverage") end else report("class is not covered") end end end else report("empty subclassset in %a subtype %i","chainedcontext",subtype) end return { format = "class", rules = rules, } elseif subtype == 3 then -- Maybe this one needs checking. Anyway zero current is bad. local before = readarray(f) local current = readarray(f) local after = readarray(f) local noflookups = readushort(f) local lookups = current and readlookuparray(f,noflookups,#current) if lookups then before = readcoveragearray(f,tableoffset,before,true) current = readcoveragearray(f,tableoffset,current,true) after = readcoveragearray(f,tableoffset,after,true) return { format = "coverage", rules = { { before = before, current = current, after = after, lookups = lookups, } } } else report("confusing subtype %a in %a %s",subtype,"chainedcontext",what) end else report("unsupported subtype %a in %a %s",subtype,"chainedcontext",what) end end local function extension(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs,types,handlers,what) local tableoffset = lookupoffset + offset setposition(f,tableoffset) local subtype = readushort(f) if subtype == 1 then local lookuptype = types[readushort(f)] local faroffset = readulong(f) local handler = handlers[lookuptype] if handler then -- maybe we can just pass one offset (or tableoffset first) return handler(f,fontdata,lookupid,tableoffset + faroffset,0,glyphs,nofglyphs), lookuptype else report("no handler for lookuptype %a subtype %a in %s %s",lookuptype,subtype,what,"extension") end else report("unsupported subtype %a in %s %s",subtype,what,"extension") end end -- gsub handlers function gsubhandlers.single(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) local tableoffset = lookupoffset + offset setposition(f,tableoffset) local subtype = readushort(f) if subtype == 1 then local coverage = readushort(f) local delta = readshort(f) -- can be negative local coverage = readcoverage(f,tableoffset+coverage) -- not simple as we need to set key/value anyway for index in next, coverage do local newindex = (index + delta) % 65536 -- modulo is new in 1.8.3 if index > nofglyphs or newindex > nofglyphs then report("invalid index in %s format %i: %i -> %i (max %i)","single",subtype,index,newindex,nofglyphs) coverage[index] = nil else coverage[index] = newindex end end return { coverage = coverage } elseif subtype == 2 then -- in streamreader a seek and fetch is faster than a temp table local coverage = readushort(f) local nofreplacements = readushort(f) local replacements = readcardinaltable(f,nofreplacements,ushort) local coverage = readcoverage(f,tableoffset + coverage) -- not simple as we need to set key/value anyway for index, newindex in next, coverage do newindex = newindex + 1 if index > nofglyphs or newindex > nofglyphs then report("invalid index in %s format %i: %i -> %i (max %i)","single",subtype,index,newindex,nofglyphs) coverage[index] = nil else coverage[index] = replacements[newindex] end end return { coverage = coverage } else report("unsupported subtype %a in %a substitution",subtype,"single") end end -- we see coverage format 0x300 in some old ms fonts local function sethandler(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs,what) local tableoffset = lookupoffset + offset setposition(f,tableoffset) local subtype = readushort(f) if subtype == 1 then local coverage = readushort(f) local nofsequence = readushort(f) local sequences = readcardinaltable(f,nofsequence,ushort) for i=1,nofsequence do setposition(f,tableoffset + sequences[i]) sequences[i] = readcardinaltable(f,readushort(f),ushort) end local coverage = readcoverage(f,tableoffset + coverage) for index, newindex in next, coverage do newindex = newindex + 1 if index > nofglyphs or newindex > nofglyphs then report("invalid index in %s format %i: %i -> %i (max %i)",what,subtype,index,newindex,nofglyphs) coverage[index] = nil else coverage[index] = sequences[newindex] end end return { coverage = coverage } else report("unsupported subtype %a in %a substitution",subtype,what) end end function gsubhandlers.multiple(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) return sethandler(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs,"multiple") end function gsubhandlers.alternate(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) return sethandler(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs,"alternate") end function gsubhandlers.ligature(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) local tableoffset = lookupoffset + offset setposition(f,tableoffset) local subtype = readushort(f) if subtype == 1 then local coverage = readushort(f) local nofsets = readushort(f) local ligatures = readcardinaltable(f,nofsets,ushort) for i=1,nofsets do local offset = lookupoffset + offset + ligatures[i] setposition(f,offset) local n = readushort(f) if n == 1 then ligatures[i] = { offset + readushort(f) } else local l = { } for i=1,n do l[i] = offset + readushort(f) end ligatures[i] = l end end local coverage = readcoverage(f,tableoffset + coverage) for index, newindex in next, coverage do local hash = { } local ligatures = ligatures[newindex+1] for i=1,#ligatures do local offset = ligatures[i] setposition(f,offset) local lig = readushort(f) local cnt = readushort(f) local hsh = hash for i=2,cnt do local c = readushort(f) local h = hsh[c] if not h then h = { } hsh[c] = h end hsh = h end hsh.ligature = lig end coverage[index] = hash end return { coverage = coverage } else report("unsupported subtype %a in %a substitution",subtype,"ligature") end end function gsubhandlers.context(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) return unchainedcontext(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs,"substitution"), "context" end function gsubhandlers.chainedcontext(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) return chainedcontext(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs,"substitution"), "chainedcontext" end function gsubhandlers.extension(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) return extension(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs,gsubtypes,gsubhandlers,"substitution") end function gsubhandlers.reversechainedcontextsingle(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) local tableoffset = lookupoffset + offset setposition(f,tableoffset) local subtype = readushort(f) if subtype == 1 then -- NEEDS CHECKING local current = readfirst(f) local before = readarray(f) local after = readarray(f) local replacements = readarray(f) current = readcoveragearray(f,tableoffset,current,true) before = readcoveragearray(f,tableoffset,before,true) after = readcoveragearray(f,tableoffset,after,true) return { format = "reversecoverage", -- reversesub rules = { { before = before, current = current, after = after, replacements = replacements, } } }, "reversechainedcontextsingle" else report("unsupported subtype %a in %a substitution",subtype,"reversechainedcontextsingle") end end -- gpos handlers local function readpairsets(f,tableoffset,sets,format1,format2,mainoffset,getdelta,getdevice) local done = { } for i=1,#sets do local offset = sets[i] local reused = done[offset] if not reused then offset = tableoffset + offset setposition(f,offset) local n = readushort(f) reused = { } for i=1,n do reused[i] = { readushort(f), -- second glyph id readposition(f,format1,offset,getdelta,getdevice), readposition(f,format2,offset,getdelta,getdevice), } end done[offset] = reused end sets[i] = reused end return sets end local function readpairclasssets(f,nofclasses1,nofclasses2,format1,format2,mainoffset,getdelta,getdevice) local classlist1 = { } for i=1,nofclasses1 do local classlist2 = { } classlist1[i] = classlist2 for j=1,nofclasses2 do local one = readposition(f,format1,mainoffset,getdelta,getdevice) local two = readposition(f,format2,mainoffset,getdelta,getdevice) if one or two then classlist2[j] = { one, two } else classlist2[j] = false end end end return classlist1 end -- no real gain in kerns as we pack function gposhandlers.single(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) local tableoffset = lookupoffset + offset setposition(f,tableoffset) local subtype = readushort(f) local getdelta = fontdata.temporary.getdelta local getdevice = fontdata.temporary.getdevice if subtype == 1 then local coverage = readushort(f) local format = readushort(f) local value = readposition(f,format,tableoffset,getdelta,getdevice) local coverage = readcoverage(f,tableoffset+coverage) for index, newindex in next, coverage do coverage[index] = value -- will be packed and shared anyway end return { format = "single", coverage = coverage, } elseif subtype == 2 then local coverage = readushort(f) local format = readushort(f) local nofvalues = readushort(f) local values = { } for i=1,nofvalues do values[i] = readposition(f,format,tableoffset,getdelta,getdevice) end local coverage = readcoverage(f,tableoffset+coverage) for index, newindex in next, coverage do coverage[index] = values[newindex+1] end return { format = "single", coverage = coverage, } else report("unsupported subtype %a in %a positioning",subtype,"single") end end function gposhandlers.pair(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) local tableoffset = lookupoffset + offset setposition(f,tableoffset) local subtype = readushort(f) local getdelta = fontdata.temporary.getdelta local getdevice = fontdata.temporary.getdevice if subtype == 1 then local coverage = readushort(f) local format1 = readushort(f) local format2 = readushort(f) local sets = readarray(f) sets = readpairsets(f,tableoffset,sets,format1,format2,tableoffset,getdelta,getdevice) coverage = readcoverage(f,tableoffset + coverage) local shared = { } -- partial sparse, when set also needs to be handled in the packer for index, newindex in next, coverage do local set = sets[newindex+1] local hash = { } for i=1,#set do local value = set[i] if value then local other = value[1] local share = shared[value] if share == nil then local first = value[2] local second = value[3] if first or second then share = { first, second or nil } -- needs checking else share = false end shared[value] = share end hash[other] = share or nil -- really overload ? end end coverage[index] = hash end return { shared = shared and true or nil, format = "pair", coverage = coverage, } elseif subtype == 2 then local coverage = readushort(f) local format1 = readushort(f) local format2 = readushort(f) local classdef1 = readushort(f) local classdef2 = readushort(f) local nofclasses1 = readushort(f) -- incl class 0 local nofclasses2 = readushort(f) -- incl class 0 local classlist = readpairclasssets(f,nofclasses1,nofclasses2,format1,format2,tableoffset,getdelta,getdevice) coverage = readcoverage(f,tableoffset+coverage) classdef1 = readclassdef(f,tableoffset+classdef1,coverage) classdef2 = readclassdef(f,tableoffset+classdef2,nofglyphs) local usedcoverage = { } local shared = { } -- partial sparse, when set also needs to be handled in the packer for g1, c1 in next, classdef1 do if coverage[g1] then local l1 = classlist[c1] if l1 then local hash = { } for paired, class in next, classdef2 do local offsets = l1[class] if offsets then local first = offsets[1] local second = offsets[2] if first or second then local s1 = shared[first] if s1 == nil then s1 = { } shared[first] = s1 end local s2 = s1[second] if s2 == nil then s2 = { first, second or nil } s1[second] = s2 end hash[paired] = s2 end end end usedcoverage[g1] = hash end end end return { shared = shared and true or nil, format = "pair", coverage = usedcoverage, } elseif subtype == 3 then report("yet unsupported subtype %a in %a positioning",subtype,"pair") else report("unsupported subtype %a in %a positioning",subtype,"pair") end end function gposhandlers.cursive(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) local tableoffset = lookupoffset + offset setposition(f,tableoffset) local subtype = readushort(f) local getdelta = fontdata.temporary.getdelta local getdevice = fontdata.temporary.getdevice if subtype == 1 then local coverage = tableoffset + readushort(f) local nofrecords = readushort(f) local records = { } for i=1,nofrecords do local entry = readushort(f) local exit = readushort(f) records[i] = { -- entry = entry ~= 0 and (tableoffset + entry) or false, -- exit = exit ~= 0 and (tableoffset + exit ) or nil, entry ~= 0 and (tableoffset + entry) or false, exit ~= 0 and (tableoffset + exit ) or nil, } end -- slot 1 will become hash after loading and it must be unique because we -- pack the tables (packed we turn the cc-* into a zero) local cc = (fontdata.temporary.cursivecount or 0) + 1 fontdata.temporary.cursivecount = cc cc = "cc-" .. cc coverage = readcoverage(f,coverage) for i=1,nofrecords do local r = records[i] records[i] = { -- 1, cc, -- readanchor(f,r.entry,getdelta) or false, -- readanchor(f,r.exit, getdelta) or nil, readanchor(f,r[1],getdelta,getdevice) or false, readanchor(f,r[2],getdelta,getdevice) or nil, } end for index, newindex in next, coverage do coverage[index] = records[newindex+1] end return { coverage = coverage, } else report("unsupported subtype %a in %a positioning",subtype,"cursive") end end local function handlemark(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs,ligature) local tableoffset = lookupoffset + offset setposition(f,tableoffset) local subtype = readushort(f) local getdelta = fontdata.temporary.getdelta local getdevice = fontdata.temporary.getdevice if subtype == 1 then -- we are one based, not zero local markcoverage = tableoffset + readushort(f) local basecoverage = tableoffset + readushort(f) local nofclasses = readushort(f) local markoffset = tableoffset + readushort(f) local baseoffset = tableoffset + readushort(f) -- local markcoverage = readcoverage(f,markcoverage) local basecoverage = readcoverage(f,basecoverage,true) -- TO BE CHECKED: true -- setposition(f,markoffset) local markclasses = { } local nofmarkclasses = readushort(f) -- local lastanchor = fontdata.lastanchor or 0 local usedanchors = { } -- for i=1,nofmarkclasses do local class = readushort(f) + 1 local offset = readushort(f) if offset == 0 then markclasses[i] = false else markclasses[i] = { class, markoffset + offset } end usedanchors[class] = true end for i=1,nofmarkclasses do local mc = markclasses[i] if mc then mc[2] = readanchor(f,mc[2],getdelta,getdevice) end end -- setposition(f,baseoffset) local nofbaserecords = readushort(f) local baserecords = { } -- if ligature then -- 3 components -- 1 : class .. nofclasses -- NULL when empty -- 2 : class .. nofclasses -- NULL when empty -- 3 : class .. nofclasses -- NULL when empty for i=1,nofbaserecords do -- here i is the class local offset = readushort(f) if offset == 0 then baserecords[i] = false else baserecords[i] = baseoffset + offset end end for i=1,nofbaserecords do local recordoffset = baserecords[i] if recordoffset then setposition(f,recordoffset) local nofcomponents = readushort(f) local components = { } for i=1,nofcomponents do local classes = { } for i=1,nofclasses do local offset = readushort(f) if offset ~= 0 then classes[i] = recordoffset + offset else classes[i] = false end end components[i] = classes end baserecords[i] = components end end local baseclasses = { } -- setmetatableindex("table") for i=1,nofclasses do baseclasses[i] = { } end for i=1,nofbaserecords do local components = baserecords[i] if components then local b = basecoverage[i] for c=1,#components do local classes = components[c] if classes then for i=1,nofclasses do local anchor = readanchor(f,classes[i],getdelta,getdevice) local bclass = baseclasses[i] local bentry = bclass[b] if bentry then bentry[c] = anchor else bclass[b]= { [c] = anchor } end end end end end end for index, newindex in next, markcoverage do markcoverage[index] = markclasses[newindex+1] or nil end return { format = "ligature", baseclasses = baseclasses, coverage = markcoverage, } else for i=1,nofbaserecords do local r = { } for j=1,nofclasses do local offset = readushort(f) if offset == 0 then r[j] = false else r[j] = baseoffset + offset end end baserecords[i] = r end local baseclasses = { } -- setmetatableindex("table") for i=1,nofclasses do baseclasses[i] = { } end for i=1,nofbaserecords do local r = baserecords[i] local b = basecoverage[i] for j=1,nofclasses do baseclasses[j][b] = readanchor(f,r[j],getdelta,getdevice) end end for index, newindex in next, markcoverage do markcoverage[index] = markclasses[newindex+1] or nil end -- we could actually already calculate the displacement if we want return { format = "base", baseclasses = baseclasses, coverage = markcoverage, } end else report("unsupported subtype %a in",subtype) end end function gposhandlers.marktobase(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) return handlemark(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) end function gposhandlers.marktoligature(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) return handlemark(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs,true) end function gposhandlers.marktomark(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) return handlemark(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) end function gposhandlers.context(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) return unchainedcontext(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs,"positioning"), "context" end function gposhandlers.chainedcontext(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) return chainedcontext(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs,"positioning"), "chainedcontext" end function gposhandlers.extension(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs) return extension(f,fontdata,lookupid,lookupoffset,offset,glyphs,nofglyphs,gpostypes,gposhandlers,"positioning") end -- main loader do local plugins = { } function plugins.size(f,fontdata,tableoffset,feature) if fontdata.designsize then -- yes, there are fonts with multiple size entries ... it probably relates -- to the other two fields (menu entries in some language) else local function check(offset) setposition(f,offset) local designsize = readushort(f) if designsize > 0 then -- we could also have a threshold local fontstyleid = readushort(f) local guimenuid = readushort(f) local minsize = readushort(f) local maxsize = readushort(f) if minsize == 0 and maxsize == 0 and fontstyleid == 0 and guimenuid == 0 then minsize = designsize maxsize = designsize end if designsize >= minsize and designsize <= maxsize then return minsize, maxsize, designsize end end end local minsize, maxsize, designsize = check(tableoffset+feature.offset+feature.parameters) if not designsize then -- some old adobe fonts have: tableoffset+feature.parameters and we could -- use some heuristic but why bother ... this extra check will be removed -- some day and/or when we run into an issue minsize, maxsize, designsize = check(tableoffset+feature.parameters) if designsize then report("bad size feature in %a, falling back to wrong offset",fontdata.filename or "?") else report("bad size feature in %a,",fontdata.filename or "?") end end if designsize then fontdata.minsize = minsize fontdata.maxsize = maxsize fontdata.designsize = designsize end end end -- function plugins.rvrn(f,fontdata,tableoffset,feature) -- -- todo, at least a message -- end -- feature order needs checking ... as we loop over a hash ... however, in the file -- they are sorted so order is not that relevant local function reorderfeatures(fontdata,scripts,features) local scriptlangs = { } local featurehash = { } local featureorder = { } for script, languages in next, scripts do for language, record in next, languages do local hash = { } local list = { } if record.requiredindex and record.requiredindex ~= 0xFFFF then list[#list+1] = record.requiredindex end for i=1,#record.featureindices do local index = record.featureindices[i] if index ~= record.requiredindex then list[#list+1] = index end end for k=1,#list do local index = list[k] local feature = features[index] if feature then local lookups = feature.lookups local tag = feature.tag if tag then hash[tag] = true end if lookups then for i=1,#lookups do local lookup = lookups[i] local o = featureorder[lookup] if o then local okay = true for i=1,#o do if o[i] == tag then okay = false break end end if okay then o[#o+1] = tag end else featureorder[lookup] = { tag } end local f = featurehash[lookup] if f then local h = f[tag] if h then local s = h[script] if s then s[language] = true else h[script] = { [language] = true } end else f[tag] = { [script] = { [language] = true } } end else featurehash[lookup] = { [tag] = { [script] = { [language] = true } } } end -- local h = scriptlangs[tag] if h then local s = h[script] if s then s[language] = true else h[script] = { [language] = true } end else scriptlangs[tag] = { [script] = { [language] = true } } end end end end end end end return scriptlangs, featurehash, featureorder end local function readscriplan(f,fontdata,scriptoffset) setposition(f,scriptoffset) local nofscripts = readushort(f) local scripts = { } for i=1,nofscripts do scripts[readtag(f)] = scriptoffset + readushort(f) end -- script list -> language system info local languagesystems = setmetatableindex("table") for script, offset in next, scripts do setposition(f,offset) local defaultoffset = readushort(f) local noflanguages = readushort(f) local languages = { } if defaultoffset > 0 then languages.dflt = languagesystems[offset + defaultoffset] end for i=1,noflanguages do local language = readtag(f) local offset = offset + readushort(f) languages[language] = languagesystems[offset] end scripts[script] = languages end -- script list -> language system info -> feature list for offset, usedfeatures in next, languagesystems do if offset > 0 then setposition(f,offset) local featureindices = { } usedfeatures.featureindices = featureindices usedfeatures.lookuporder = readushort(f) -- reserved, not used (yet) local requiredindex = readushort(f) -- relates to required (can be 0xFFFF) usedfeatures.requiredindex = requiredindex ~= 0xFFFF and requiredindex + 1 or requiredindex local noffeatures = readushort(f) for i=1,noffeatures do featureindices[i] = readushort(f) + 1 end end end return scripts end local function readfeatures(f,fontdata,featureoffset) setposition(f,featureoffset) local features = { } local noffeatures = readushort(f) for i=1,noffeatures do -- also shared? features[i] = { tag = readtag(f), offset = readushort(f) } end -- for i=1,noffeatures do local feature = features[i] local offset = featureoffset+feature.offset setposition(f,offset) local parameters = readushort(f) -- feature.parameters local noflookups = readushort(f) if noflookups > 0 then -- local lookups = { } -- feature.lookups = lookups -- for j=1,noflookups do -- lookups[j] = readushort(f) + 1 -- end local lookups = readcardinaltable(f,noflookups,ushort) feature.lookups = lookups for j=1,noflookups do lookups[j] = lookups[j] + 1 end end if parameters > 0 then feature.parameters = parameters local plugin = plugins[feature.tag] if plugin then plugin(f,fontdata,featureoffset,feature) end end end return features end local function readlookups(f,lookupoffset,lookuptypes,featurehash,featureorder,nofmarkclasses) setposition(f,lookupoffset) local noflookups = readushort(f) local lookups = readcardinaltable(f,noflookups,ushort) for lookupid=1,noflookups do local offset = lookups[lookupid] setposition(f,lookupoffset+offset) local subtables = { } local typebits = readushort(f) local flagbits = readushort(f) local lookuptype = lookuptypes[typebits] local lookupflags = lookupflags[flagbits] local nofsubtables = readushort(f) for j=1,nofsubtables do subtables[j] = offset + readushort(f) -- we can probably put lookupoffset here end local markclass = (flagbits >> 8) & 0xFFFFFFFF if markclass == 0 then local markset = (flagbits & 0x0010) ~= 0 -- usemarkfilteringset if markset then markclass = readushort(f) + 1 + nofmarkclasses -- KE: both else markclass = false end end lookups[lookupid] = { type = lookuptype, -- chain = chaindirections[lookuptype] or nil, flags = lookupflags, name = lookupid, subtables = subtables, markclass = markclass, features = featurehash[lookupid], -- not if extension order = featureorder[lookupid], } end return lookups end local f_lookupname = formatters["%s_%s_%s"] local function resolvelookups(f,lookupoffset,fontdata,lookups,lookuptypes,lookuphandlers,what,tableoffset) local sequences = fontdata.sequences or { } local sublookuplist = fontdata.sublookups or { } fontdata.sequences = sequences fontdata.sublookups = sublookuplist local nofsublookups = #sublookuplist local nofsequences = #sequences -- 0 local lastsublookup = nofsublookups local lastsequence = nofsequences local lookupnames = lookupnames[what] local sublookuphash = { } local sublookupcheck = { } local glyphs = fontdata.glyphs local nofglyphs = fontdata.nofglyphs or #glyphs local noflookups = #lookups local lookupprefix = sub(what,2,2) -- g[s|p][ub|os] -- local usedlookups = false -- setmetatableindex("number") -- local allsteps = { } -- new per 2022-09-25 for lookupid=1,noflookups do local lookup = lookups[lookupid] local lookuptype = lookup.type local subtables = lookup.subtables local features = lookup.features local handler = lookuphandlers[lookuptype] if handler then local nofsubtables = #subtables local order = lookup.order local flags = lookup.flags -- this is expected in the font handler (faster checking) if flags[1] then flags[1] = "mark" end if flags[2] then flags[2] = "ligature" end if flags[3] then flags[3] = "base" end -- local markclass = lookup.markclass -- local chain = lookup.chain if nofsubtables > 0 then local steps = { } local nofsteps = 0 local oldtype = nil for s=1,nofsubtables do local step, lt = handler(f,fontdata,lookupid,lookupoffset,subtables[s],glyphs,nofglyphs) if lt then lookuptype = lt if oldtype and lt ~= oldtype then report("messy %s lookup type %a and %a",what,lookuptype,oldtype) end oldtype = lookuptype end if not step then report("unsupported %s lookup type %a",what,lookuptype) else nofsteps = nofsteps + 1 steps[nofsteps] = step local rules = step.rules if rules then allsteps[#allsteps+1] = step -- new per 2022-09-25 for i=1,#rules do local rule = rules[i] local before = rule.before local current = rule.current local after = rule.after local replacements = rule.replacements if before then for i=1,#before do before[i] = tohash(before[i]) end -- as with original ctx ff loader rule.before = reversed(before) end if current then if replacements then -- We have a reverse lookup and therefore only one current entry. We might need -- to reverse the order in the before and after lists so that needs checking. local first = current[1] local hash = { } local repl = { } for i=1,#first do local c = first[i] hash[c] = true repl[c] = replacements[i] end rule.current = { hash } rule.replacements = repl else for i=1,#current do current[i] = tohash(current[i]) end end else -- weird lookup end if after then for i=1,#after do after[i] = tohash(after[i]) end end if usedlookups then local lookups = rule.lookups if lookups then for k, v in next, lookups do if v then for k, v in next, v do usedlookups[v] = usedlookups[v] + 1 end end end end end end end end end if nofsteps ~= nofsubtables then report("bogus subtables removed in %s lookup type %a",what,lookuptype) end lookuptype = lookupnames[lookuptype] or lookuptype if features then nofsequences = nofsequences + 1 -- report("registering %i as sequence step %i",lookupid,nofsequences) local l = { index = nofsequences, name = f_lookupname(lookupprefix,"s",lookupid+lookupidoffset), steps = steps, nofsteps = nofsteps, type = lookuptype, markclass = markclass or nil, flags = flags, -- chain = chain, order = order, features = features, } sequences[nofsequences] = l lookup.done = l else nofsublookups = nofsublookups + 1 -- report("registering %i as sublookup %i",lookupid,nofsublookups) local l = { index = nofsublookups, name = f_lookupname(lookupprefix,"l",lookupid+lookupidoffset), steps = steps, nofsteps = nofsteps, type = lookuptype, markclass = markclass or nil, flags = flags, -- chain = chain, } sublookuplist[nofsublookups] = l sublookuphash[lookupid] = nofsublookups sublookupcheck[lookupid] = 0 lookup.done = l end else report("no subtables for lookup %a",lookupid) end else report("no handler for lookup %a with type %a",lookupid,lookuptype) end end if usedlookups then report("used %s lookups: % t",what,sortedkeys(usedlookups)) end -- When we have a context, we have sublookups that resolve into lookups for which we need to -- know the type. We split the main lookuptable in two parts: sequences (the main lookups) -- and subtable lookups (simple specs with no features). We could keep them merged and might do -- that once we only use this loader. Then we can also move the simple specs into the sequence. -- After all, we pack afterwards. local reported = { } local function report_issue(i,what,step,kind) -- if not reported[step] then report("rule %i in step %i of %s has %s lookups",i,step,what,kind) -- reported[name] = true -- end end -- for i=lastsequence+1,nofsequences do -- local sequence = sequences[i] -- local steps = sequence.steps -- for i=1,#steps do -- local step = steps[i] for s=1,#allsteps do -- new per 2022-09-25 local step = allsteps[s] -- new per 2022-09-25 local rules = step.rules if rules then for i=1,#rules do local rule = rules[i] local rlookups = rule.lookups if not rlookups then report_issue(i,what,s,"no") elseif not next(rlookups) then -- can be ok as it aborts a chain sequence -- report_issue(i,what,s,"empty") rule.lookups = nil else -- we can have holes in rlookups flagged false and we can have multiple lookups -- applied (first time seen in seguemj) local length = #rlookups for index=1,length do local lookuplist = rlookups[index] if lookuplist then local length = #lookuplist local found = { } local noffound = 0 for index=1,length do local lookupid = lookuplist[index] if lookupid then local h = sublookuphash[lookupid] if not h then -- here we have a lookup that is used independent as well -- as in another one local lookup = lookups[lookupid] if lookup then local d = lookup.done if d then nofsublookups = nofsublookups + 1 -- report("registering %i as sublookup %i",lookupid,nofsublookups) local l = { index = nofsublookups, -- handy for tracing name = f_lookupname(lookupprefix,"d",lookupid+lookupidoffset), derived = true, -- handy for tracing steps = d.steps, nofsteps = d.nofsteps, type = d.lookuptype or "gsub_single", -- todo: check type markclass = d.markclass or nil, flags = d.flags, -- chain = d.chain, } sublookuplist[nofsublookups] = copy(l) -- we repack later sublookuphash[lookupid] = nofsublookups sublookupcheck[lookupid] = 1 h = nofsublookups else report_issue(i,what,s,"missing") rule.lookups = nil break end else report_issue(i,what,s,"bad") rule.lookups = nil break end else sublookupcheck[lookupid] = sublookupcheck[lookupid] + 1 end if h then noffound = noffound + 1 found[noffound] = h end end end rlookups[index] = noffound > 0 and found or false else rlookups[index] = false end end end end end end -- end -- new per 2022-09-25 for i, n in sortedhash(sublookupcheck) do local l = lookups[i] local t = l.type if n == 0 and t ~= "extension" then local d = l.done report("%s lookup %s of type %a is not used",what,d and d.name or l.name,t) end end end local function loadvariations(f,fontdata,variationsoffset,features,specification) setposition(f,variationsoffset) local version = readulong(f) -- two times readushort local nofrecords = readulong(f) local records = { } for i=1,nofrecords do records[i] = { conditions = readulong(f), substitutions = readulong(f), } end for i=1,nofrecords do local record = records[i] local offset = record.conditions if offset == 0 then record.condition = nil record.matchtype = "always" else local offset = variationsoffset+offset setposition(f,offset) local nofconditions = readushort(f) local conditions = { } for i=1,nofconditions do conditions[i] = offset + readulong(f) end record.conditions = conditions record.matchtype = "condition" end end for i=1,nofrecords do local record = records[i] if record.matchtype == "condition" then local conditions = record.conditions for i=1,#conditions do setposition(f,conditions[i]) conditions[i] = { format = readushort(f), axis = readushort(f), minvalue = read2dot14(f), maxvalue = read2dot14(f), } end end end local selected = false for i=1,nofrecords do local record = records[i] local offset = record.substitutions local match = record.matchtype == "always" if not match then match = true for j=1,#record.conditions do local condition = record.conditions[j] if condition.format ~= 1 then match = false break end local factor = specification.factors and specification.factors[condition.axis+1] or 0 if factor < condition.minvalue or factor > condition.maxvalue then match = false break end end end if match and not selected then selected = true record.selected = true record.substitutions = { } if offset ~= 0 then setposition(f,variationsoffset + offset) local version = readulong(f) local nofsubstitutions = readushort(f) local substitutions = { } for j=1,nofsubstitutions do substitutions[readushort(f)] = readulong(f) end for index, alternates in sortedhash(substitutions) do local tableoffset = variationsoffset + offset + alternates setposition(f,tableoffset) local parameters = readushort(f) local noflookups = readushort(f) local lookups = readcardinaltable(f,noflookups,ushort) for i=1,noflookups do lookups[i] = lookups[i] + 1 end record.substitutions[index] = lookups local feature = features[index+1] if feature then local alternate = { tag = feature.tag, lookups = lookups, offset = feature.offset, parameters = parameters ~= 0 and parameters or nil, } features[index+1] = alternate end end end elseif offset == 0 then record.substitutions = { } end end setvariabledata(fontdata,"features",records) return features end local function readscripts(f,fontdata,what,lookuptypes,lookuphandlers,specification) local lookupstoo = specification.lookups local tableoffset = gotodatatable(f,fontdata,what,true) if tableoffset then local version = readulong(f) local scriptoffset = tableoffset + readushort(f) local featureoffset = tableoffset + readushort(f) local lookupoffset = tableoffset + readushort(f) -- MFK : Rubik-Regular.ttf : we need to delay adding the offset -- local variationsoffset = version > 0x00010000 and (tableoffset + readulong(f)) or 0 local variationsoffset = version > 0x00010000 and readulong(f) or 0 if not scriptoffset then return end local scripts = readscriplan(f,fontdata,scriptoffset) local features = readfeatures(f,fontdata,featureoffset) if variationsoffset > 0 then features = loadvariations(f,fontdata,tableoffset + variationsoffset,features,specification) or features end -- local scriptlangs, featurehash, featureorder = reorderfeatures(fontdata,scripts,features) -- if fontdata.features then fontdata.features[what] = scriptlangs else fontdata.features = { [what] = scriptlangs } end -- if not lookupstoo then return end -- local markclasses = fontdata.markclasses local nofmarkclasses = fontdata.markclasscount or (markclasses and #markclasses or 0) local lookups = readlookups(f,lookupoffset,lookuptypes,featurehash,featureorder,nofmarkclasses) -- if lookups then resolvelookups(f,lookupoffset,fontdata,lookups,lookuptypes,lookuphandlers,what,tableoffset) end -- end end local function checkkerns(f,fontdata,specification) local datatable = fontdata.tables.kern if not datatable then return -- no kerns end local features = fontdata.features local gposfeatures = features and features.gpos local name if not gposfeatures or not gposfeatures.kern then name = "kern" elseif specification.globalkerns then name = "globalkern" else report("ignoring global kern table, using gpos kern feature") return end setposition(f,datatable.offset) local version = readushort(f) local noftables = readushort(f) if noftables > 1 then report("adding global kern table as gpos feature %a",name) local kerns = setmetatableindex("table") for i=1,noftables do local version = readushort(f) local length = readushort(f) local coverage = readushort(f) -- bit 8-15 of coverage: format 0 or 2 local format = (coverage >> 8) & 0xFFFFFFFF if format == 0 then local nofpairs = readushort(f) local searchrange = readushort(f) local entryselector = readushort(f) local rangeshift = readushort(f) for i=1,nofpairs do kerns[readushort(f)][readushort(f)] = readfword(f) end elseif format == 2 then -- apple specific so let's ignore it else -- not supported by ms end end local feature = { dflt = { dflt = true } } if not features then fontdata.features = { gpos = { [name] = feature } } elseif not gposfeatures then fontdata.features.gpos = { [name] = feature } else gposfeatures[name] = feature end local sequences = fontdata.sequences if not sequences then sequences = { } fontdata.sequences = sequences end local nofsequences = #sequences + 1 sequences[nofsequences] = { index = nofsequences, name = name, steps = { { coverage = kerns, format = "kern", }, }, nofsteps = 1, type = "gpos_pair", flags = { false, false, false, false }, order = { name }, features = { [name] = feature }, } else report("ignoring empty kern table of feature %a",name) end end function readers.gsub(f,fontdata,specification) if specification.details then setdevicecallback(fontdata,specification) readscripts(f,fontdata,"gsub",gsubtypes,gsubhandlers,specification) end end function readers.gpos(f,fontdata,specification) if specification.details then setdevicecallback(fontdata,specification) readscripts(f,fontdata,"gpos",gpostypes,gposhandlers,specification) if specification.lookups then checkkerns(f,fontdata,specification) end end end end function readers.gdef(f,fontdata,specification) if not specification.glyphs then return end local datatable = fontdata.tables.gdef if datatable then local tableoffset = datatable.offset setposition(f,tableoffset) local version = readulong(f) local classoffset = readushort(f) local attachmentoffset = readushort(f) -- used for bitmaps local ligaturecarets = readushort(f) -- used in editors (maybe nice for tracing) local markclassoffset = readushort(f) local marksetsoffset = version >= 0x00010002 and readushort(f) or 0 local varsetsoffset = version >= 0x00010003 and readulong(f) or 0 local glyphs = fontdata.glyphs local marks = { } local markclasses = setmetatableindex("table") local marksets = setmetatableindex("table") local markclasscount = 0 fontdata.marks = marks fontdata.markclasses = markclasses fontdata.marksets = marksets -- class definitions if classoffset ~= 0 then setposition(f,tableoffset + classoffset) local classformat = readushort(f) if classformat == 1 then local firstindex = readushort(f) local lastindex = firstindex + readushort(f) - 1 for index=firstindex,lastindex do local class = classes[readushort(f)] if class == "mark" then marks[index] = true end glyphs[index].class = class end elseif classformat == 2 then local nofranges = readushort(f) for i=1,nofranges do local firstindex = readushort(f) local lastindex = readushort(f) local class = classes[readushort(f)] if class then for index=firstindex,lastindex do glyphs[index].class = class if class == "mark" then marks[index] = true end end end end end end -- mark classes if markclassoffset ~= 0 then setposition(f,tableoffset + markclassoffset) local classformat = readushort(f) if classformat == 1 then local firstindex = readushort(f) local lastindex = firstindex + readushort(f) - 1 for index=firstindex,lastindex do local class = readushort(f) markclasses[class][index] = true if class + 1 > markclasscount then markclasscount = class + 1 end end elseif classformat == 2 then local nofranges = readushort(f) for i=1,nofranges do local firstindex = readushort(f) local lastindex = readushort(f) local classindex = readushort(f) local class = markclasses[classindex] if classindex + 1 > markclasscount then markclasscount = classindex + 1 end for index=firstindex,lastindex do class[index] = true end end end fontdata.markclasscount = markclasscount end -- mark sets if marksetsoffset ~= 0 then local nofmarkclasses = markclasscount marksetsoffset = tableoffset + marksetsoffset setposition(f,marksetsoffset) local format = readushort(f) if format == 1 then local nofsets = readushort(f) local sets = readcardinaltable(f,nofsets,ulong) for i=1,nofsets do local offset = sets[i] if offset ~= 0 then markclasses[nofmarkclasses + i] = readcoverage(f,marksetsoffset+offset) marksets[i] = { } end end end end -- AttachList and LigCaretList are not needed by shaping, but dropping -- them makes the GDEF table impossible to round-trip and loses data -- needed by editors and by clients that use glyph attachment points. if attachments then local getdelta = fontdata.temporary and fontdata.temporary.getdelta local getdevice = setdevicecallback(fontdata,specification) if attachmentoffset ~= 0 then local attachoffset = tableoffset + attachmentoffset setposition(f,attachoffset) local coverageoffset = readushort(f) local nofglyphs = readushort(f) local offsets = readcardinaltable(f,nofglyphs,ushort) local coverage = readcoverage(f,attachoffset + coverageoffset,true) local attachments = { } for i=1,nofglyphs do local glyph = coverage[i] local offset = offsets[i] if glyph and offset and offset ~= 0 then setposition(f,attachoffset + offset) local nofpoints = readushort(f) local points = { } for j=1,nofpoints do points[j] = readushort(f) end attachments[glyph] = points end end fontdata.attachments = attachments end if ligaturecarets ~= 0 then local caretlistoffset = tableoffset + ligaturecarets setposition(f,caretlistoffset) local coverageoffset = readushort(f) local nofglyphs = readushort(f) local offsets = readcardinaltable(f,nofglyphs,ushort) local coverage = readcoverage(f,caretlistoffset + coverageoffset,true) local carets = { } for i=1,nofglyphs do local glyph = coverage[i] local offset = offsets[i] if glyph and offset and offset ~= 0 then local ligatureoffset = caretlistoffset + offset setposition(f,ligatureoffset) local nofcarets = readushort(f) local caretoffsets = readcardinaltable(f,nofcarets,ushort) local values = { } for j=1,nofcarets do local caretvalueoffset = caretoffsets[j] if caretvalueoffset and caretvalueoffset ~= 0 then local valueoffset = ligatureoffset + caretvalueoffset setposition(f,valueoffset) local format = readushort(f) if format == 1 then values[j] = { format = format, coordinate = readshort(f) } elseif format == 2 then values[j] = { format = format, point = readushort(f) } elseif format == 3 then local coordinate = readshort(f) local deviceoffset = readushort(f) local delta = 0 local variation if deviceoffset ~= 0 then delta = readdevicevalue(f,valueoffset + deviceoffset,getdelta,getdevice) variation = not getdelta and readvariation(f,valueoffset + deviceoffset) or nil end values[j] = { format = format, coordinate = coordinate + delta, device = deviceoffset ~= 0 and valueoffset + deviceoffset or nil, variation = variation, } end end end carets[glyph] = values end end fontdata.ligaturecarets = carets end end local factors = specification.factors if (specification.variable or factors) and varsetsoffset ~= 0 then local regions, deltas = readvariationdata(f,tableoffset+varsetsoffset,factors) setvariabledata(fontdata,"gdef",{ regions = regions, deltas = deltas, }) if factors then fontdata.temporary = fontdata.temporary or { } fontdata.temporary.getdelta = function(outer,inner) local delta = deltas[outer+1] if delta then local d = delta.deltas[inner+1] if d then local scales = delta.scales local dd = 0 for i=1,#scales do local di = d[i] if di then dd = dd + scales[i] * di else break end end return round(dd) end end return 0 end local carets = fontdata.ligaturecarets if carets then for _, values in next, carets do for i=1,#values do local value = values[i] local variation = value.variation if variation then value.coordinate = value.coordinate + fontdata.temporary.getdelta(variation[1],variation[2]) value.variation = nil end end end end end end end end -- We keep this code here instead of font-otm.lua because we need coverage -- helpers. Okay, these helpers could go to the main reader file some day. local currentmathdevice local function readmathvalue(f) local offset = getposition(f) local v = readshort(f) local d = readushort(f) if d ~= 0 and currentmathdevice then v = v + currentmathdevice(f,offset+d) end return v end local function readmathconstants(f,fontdata,offset) setposition(f,offset) fontdata.mathconstants = { ScriptPercentScaleDown = readshort(f), ScriptScriptPercentScaleDown = readshort(f), DelimitedSubFormulaMinHeight = readushort(f), DisplayOperatorMinHeight = readushort(f), MathLeading = readmathvalue(f), AxisHeight = readmathvalue(f), AccentBaseHeight = readmathvalue(f), FlattenedAccentBaseHeight = readmathvalue(f), SubscriptShiftDown = readmathvalue(f), SubscriptTopMax = readmathvalue(f), SubscriptBaselineDropMin = readmathvalue(f), SuperscriptShiftUp = readmathvalue(f), SuperscriptShiftUpCramped = readmathvalue(f), SuperscriptBottomMin = readmathvalue(f), SuperscriptBaselineDropMax = readmathvalue(f), SubSuperscriptGapMin = readmathvalue(f), SuperscriptBottomMaxWithSubscript = readmathvalue(f), SpaceAfterScript = readmathvalue(f), UpperLimitGapMin = readmathvalue(f), UpperLimitBaselineRiseMin = readmathvalue(f), LowerLimitGapMin = readmathvalue(f), LowerLimitBaselineDropMin = readmathvalue(f), StackTopShiftUp = readmathvalue(f), StackTopDisplayStyleShiftUp = readmathvalue(f), StackBottomShiftDown = readmathvalue(f), StackBottomDisplayStyleShiftDown = readmathvalue(f), StackGapMin = readmathvalue(f), StackDisplayStyleGapMin = readmathvalue(f), StretchStackTopShiftUp = readmathvalue(f), StretchStackBottomShiftDown = readmathvalue(f), StretchStackGapAboveMin = readmathvalue(f), StretchStackGapBelowMin = readmathvalue(f), FractionNumeratorShiftUp = readmathvalue(f), FractionNumeratorDisplayStyleShiftUp = readmathvalue(f), FractionDenominatorShiftDown = readmathvalue(f), FractionDenominatorDisplayStyleShiftDown = readmathvalue(f), FractionNumeratorGapMin = readmathvalue(f), FractionNumeratorDisplayStyleGapMin = readmathvalue(f), FractionRuleThickness = readmathvalue(f), FractionDenominatorGapMin = readmathvalue(f), FractionDenominatorDisplayStyleGapMin = readmathvalue(f), SkewedFractionHorizontalGap = readmathvalue(f), SkewedFractionVerticalGap = readmathvalue(f), OverbarVerticalGap = readmathvalue(f), OverbarRuleThickness = readmathvalue(f), OverbarExtraAscender = readmathvalue(f), UnderbarVerticalGap = readmathvalue(f), UnderbarRuleThickness = readmathvalue(f), UnderbarExtraDescender = readmathvalue(f), RadicalVerticalGap = readmathvalue(f), RadicalDisplayStyleVerticalGap = readmathvalue(f), RadicalRuleThickness = readmathvalue(f), RadicalExtraAscender = readmathvalue(f), RadicalKernBeforeDegree = readmathvalue(f), RadicalKernAfterDegree = readmathvalue(f), RadicalDegreeBottomRaisePercent = readshort(f), } end local function readmathglyphinfo(f,fontdata,offset) setposition(f,offset) local italics = readushort(f) local accents = readushort(f) local extensions = readushort(f) local kerns = readushort(f) local glyphs = fontdata.glyphs if italics ~= 0 then setposition(f,offset+italics) local coverage = readushort(f) local nofglyphs = readushort(f) coverage = readcoverage(f,offset+italics+coverage,true) setposition(f,offset+italics+4) for i=1,nofglyphs do local italic = readmathvalue(f) if italic ~= 0 then local glyph = glyphs[coverage[i]] local math = glyph.math if not math then glyph.math = { italic = italic } else math.italic = italic end end end fontdata.hasitalics = true end if accents ~= 0 then setposition(f,offset+accents) local coverage = readushort(f) local nofglyphs = readushort(f) coverage = readcoverage(f,offset+accents+coverage,true) setposition(f,offset+accents+4) for i=1,nofglyphs do local accent = readmathvalue(f) if accent ~= 0 then local glyph = glyphs[coverage[i]] local math = glyph.math if not math then glyph.math = { accent = accent } else math.accent = accent -- will become math.topanchor end end end end if extensions ~= 0 then local extensionoffset = offset + extensions setposition(f,extensionoffset) local coverage = readcoverage(f,extensionoffset + readushort(f),true) local nofglyphs = #coverage for i=1,nofglyphs do local glyph = glyphs[coverage[i]] if glyph then local math = glyph.math if math then math.extended = true else glyph.math = { extended = true } end end end end if kerns ~= 0 then local kernoffset = offset + kerns setposition(f,kernoffset) local coverage = readushort(f) local nofglyphs = readushort(f) if nofglyphs > 0 then local function get(offset) setposition(f,kernoffset+offset) local n = readushort(f) if n == 0 then local k = readmathvalue(f) if k == 0 then -- no need for it (happens sometimes) else return { { kern = k } } end else local l = { } for i=1,n do l[i] = { height = readmathvalue(f) } end for i=1,n do l[i].kern = readmathvalue(f) end l[n+1] = { kern = readmathvalue(f) } return l end end local kernsets = { } for i=1,nofglyphs do local topright = readushort(f) local topleft = readushort(f) local bottomright = readushort(f) local bottomleft = readushort(f) kernsets[i] = { topright = topright ~= 0 and topright or nil, topleft = topleft ~= 0 and topleft or nil, bottomright = bottomright ~= 0 and bottomright or nil, bottomleft = bottomleft ~= 0 and bottomleft or nil, } end coverage = readcoverage(f,kernoffset+coverage,true) for i=1,nofglyphs do local kernset = kernsets[i] if next(kernset) then local k = kernset.topright if k then kernset.topright = get(k) end local k = kernset.topleft if k then kernset.topleft = get(k) end local k = kernset.bottomright if k then kernset.bottomright = get(k) end local k = kernset.bottomleft if k then kernset.bottomleft = get(k) end if next(kernset) then local glyph = glyphs[coverage[i]] local math = glyph.math if math then math.kerns = kernset else glyph.math = { kerns = kernset } end end end end end end end local function readmathvariants(f,fontdata,offset) setposition(f,offset) local glyphs = fontdata.glyphs local minoverlap = readushort(f) local vcoverage = readushort(f) local hcoverage = readushort(f) local vnofglyphs = readushort(f) local hnofglyphs = readushort(f) local vconstruction = readcardinaltable(f,vnofglyphs,ushort) local hconstruction = readcardinaltable(f,hnofglyphs,ushort) fontdata.mathconstants.MinConnectorOverlap = minoverlap -- variants[i] = { -- glyph = readushort(f), -- advance = readushort(f), -- } local function get(offset,coverage,nofglyphs,construction,kvariants,kparts,kitalic,korientation,orientation) if coverage ~= 0 and nofglyphs > 0 then local coverage = readcoverage(f,offset+coverage,true) local n = 0 for i=1,nofglyphs do local c = construction[i] if c ~= 0 then local index = coverage[i] local glyph = glyphs[index] local math = glyph.math setposition(f,offset+c) local assembly = readushort(f) local nofvariants = readushort(f) if nofvariants > 0 then local variants, v = nil, 0 for i=1,nofvariants do local variant = readushort(f) if variant == index then -- report("discarding %s variant %04X for %04X, %C","self referencing",variant,index,tonumber(glyph.unicode) or 0xFFFD) n = n + 1 elseif variants then v = v + 1 variants[v] = variant else v = 1 variants = { variant } end skipshort(f) end if not variants or not next(variants) then -- only self elseif not math then math = { [kvariants] = variants } glyph.math = math else math[kvariants] = variants end end if assembly ~= 0 then setposition(f,offset + c + assembly) local italic = readmathvalue(f) local nofparts = readushort(f) local parts = { } for i=1,nofparts do local p = { glyph = readushort(f), start = readushort(f), ["end"] = readushort(f), advance = readushort(f), } local flags = readushort(f) if (flags & 0x0001) ~= 0 then p.extender = 1 -- true end parts[i] = p end if not math then math = { [kparts] = parts } glyph.math = math else math[kparts] = parts end if italic and italic ~= 0 then math[kitalic] = italic end if korientation and orientation then -- was commented math[korientation] = orientation end -- was commented end end end -- There are quite some fonts out there (noto anno 2024) with these so we only report the -- number. It's often the first entry then. if n > 0 then report("discarding %i self referencing %s variant entries",n,orientation) end -- There are a few fonts out there (noto anno 2024) that have a variant list with entries -- that themselve have variants and that gives circular references in a 'next' chain. for index=1,#glyphs do local g = glyphs[index] local m = g.math if m then local v = m[kvariants] if v then local done = { [index] = true } local size = #v local i = 1 while i <= size do local vi = v[i] if done[vi] then report("discarding %sdirect circular %s variant index 0x%04X for index 0x%04X, %C","",orientation,vi,index,tonumber(g.unicode) or 0xFFFD) table.remove(v,i) size = size - 1 goto NEXT else local gg = glyphs[vi] if gg then local mm = gg.math if mm then local vv = mm[kvariants] if vv then report("discarding %sdirect circular %s variant index 0x%04X for index 0x%04X, %C","in",orientation,vi,index,tonumber(g.unicode) or 0xFFFD) table.remove(v,i) size = size - 1 goto NEXT end end end done[vi] = true end i = i + 1 ::NEXT:: end end end end end end get(offset,hcoverage,hnofglyphs,hconstruction,"variants","parts","partsitalic","partsorientation","horizontal") get(offset,vcoverage,vnofglyphs,vconstruction,"variants","parts","partsitalic","partsorientation","vertical") end function readers.math(f,fontdata,specification) local tableoffset = gotodatatable(f,fontdata,"math",specification.glyphs) if tableoffset then local previousdevice = currentmathdevice currentmathdevice = setdevicecallback(fontdata,specification) local version = readulong(f) -- if version ~= 0x00010000 then -- report("table version %a of %a is not supported (yet), maybe font %s is bad",version,"math",fontdata.filename) -- return -- end local constants = readushort(f) local glyphinfo = readushort(f) local variants = readushort(f) if constants == 0 then report("the math table of %a has no constants",fontdata.filename) else readmathconstants(f,fontdata,tableoffset+constants) end if glyphinfo ~= 0 then readmathglyphinfo(f,fontdata,tableoffset+glyphinfo) end if variants ~= 0 then readmathvariants(f,fontdata,tableoffset+variants) end currentmathdevice = previousdevice end end do -- format 1: PaintColrLayers -- for each referenced child paint table, in bottom-up z-order: -- call renderPaint() passing the child paint table -- compose the returned graphic onto the surface using simplealpha blending -- -- format 2, 3: PaintSolid -- paint the specified color onto the surface -- -- format 4, 5, 6, 7, 8, 9: PaintLinearGradient, PaintRadialGradient, PaintSweepGradient -- paint the gradient onto the surface following the gradient algorithm -- -- format 10: PaintGlyph -- apply the outline of the referenced glyph to the clip region -- (take the intersection of clip regions—see Filling shapes) -- call renderPaint() passing the child paint table -- restore the previous clip region -- -- format 11: PaintColrGlyph -- call renderPaint() passing the paint table referenced by the base glyph ID -- -- format 12 .. 31: Transform Translate Scale -- apply the specified transform, compose the transform with the current transform -- call renderPaint() passing the child paint table -- restore the previous transform state -- -- format 32: PaintComposite -- call renderPaint() passing the backdrop child paint table and save the result -- call renderPaint() passing the source child paint table and save the result -- compose the source and backdrop using the specified composite mode -- compose the result of the above composition onto the surface using simple alpha blending local paintdata -- for the moment verbose, will be just indexed local linesdata -- for the moment verbose, will be just indexed local affinedata -- for the moment verbose, will be just indexed local clipdata -- clipList records, indexed by glyph range local colrmap local colrdeltas local colrregions local layerlistoffset local layeroffset local function getpaintoffset(f,offset) local o = readuoffset(f) if o == 0 then return nil else -- Paint offsets are relative to the beginning of the current -- Paint table. Keep references as offsets until the tables are -- compacted below; storing the decoded table here breaks shared -- references during rehashing. local target = offset + o if paintdata then local child = paintdata[target] -- force decoding of the reachable child end return target end end local function getlinesoffset(f,offset,var) local relative = readuoffset(f) if relative == 0 then return nil end local absolute = offset + relative if linesdata[absolute] == nil then linesdata[absolute] = var elseif var then linesdata[absolute] = true end return absolute end local function getaffineoffset(f,offset,var) local relative = readuoffset(f) if relative == 0 then return nil end local absolute = offset + relative if affinedata[absolute] == nil then affinedata[absolute] = var elseif var then affinedata[absolute] = true end return absolute end local function readcolrindexmap(f,offset) if not offset or offset == 0 then return nil end setposition(f,offset) -- COLR uses the current DeltaSetIndexMap definition: unlike the -- older HVAR/VVAR form, format and entryFormat are separate bytes local format = readuinteger(f) local entryformat = readuinteger(f) local count = format == 0 and readushort(f) or readulong(f) local entrysize = (((entryformat & 0x30) >> 4) & 0x03) + 1 local innerbits = (entryformat & 0x0F) + 1 local innermask = (1 << innerbits) - 1 local reader = read_cardinal[entrysize] if not reader then return nil end local map = { } for i=0,count-1 do local value = reader(f) map[i] = { outer = (value >> innerbits) & 0xFFFFFFFF, inner = value & innermask, } end map.format = format map.entryformat = entryformat map.count = count return map end local function colrdelta(base,index,scale) if not colrdeltas or not colrregions or not base or base == 0xFFFFFFFF then return 0 end local entry = colrmap and colrmap[base + index] if not entry then if colrmap and colrmap.count and colrmap.count > 0 then entry = colrmap[colrmap.count-1] else -- With no mapping table, the varIndexBase sequence is an -- implicit sequence of 16.16 delta-set indices. local value = base + index entry = { outer = (value >> 16) & 0xFFFF, inner = value & 0xFFFF, } end end local variation = colrdeltas[entry.outer+1] local values = variation and variation.deltas[entry.inner+1] if not values then return 0 end local scales = variation.scales if not scales then return 0 end local delta = 0 for i=1,#scales do local value = values[i] if value then delta = delta + scales[i] * value else break end end return delta * (scale or 1) end local function adjustcolr(record,field,base,index,scale,integer) if record and record[field] ~= nil and base then local value = record[field] + colrdelta(base,index,scale) record[field] = type(integer) == "function" and integer(value) or integer and round(value) or value end end local paintreaders = { -- uint8 numLayers Number of offsets to paint tables to read from LayerList. -- uint32 firstLayerIndex Index (base 0) into the LayerList. [1] = function(f,format) return { format = format, name = "PaintColrLayers", count = readuinteger(f), index = readulong(f), list = false, } end, -- uint16 paletteIndex Index for a CPAL palette entry. -- F2DOT14 alpha Alpha value. [2] = function(f,format) return { format = format, name = "Paintsolid", palette = readushort(f), alpha = read2dot14(f), } end, -- uint16 paletteIndex Index for a CPAL palette entry. -- F2DOT14 alpha Alpha value. For variation, use varIndexBase + 0. -- uint32 varIndexBase Base index into DeltaSetIndexMap. [3] = function(f,format) return { format = format, name = "Paintsolid", palette = readushort(f), alpha = read2dot14(f), varbase = readulong(f), } end, -- Offset24 colorLineOffset Offset to VarColorLine table. -- FWORD x0 Start point (p₀) x coordinate. For variation, use varIndexBase + 0. -- FWORD y0 Start point (p₀) y coordinate. For variation, use varIndexBase + 1. -- FWORD x1 End point (p₁) x coordinate. For variation, use varIndexBase + 2. -- FWORD y1 End point (p₁) y coordinate. For variation, use varIndexBase + 3. -- FWORD x2 Rotation point (p₂) x coordinate. For variation, use varIndexBase + 4. -- FWORD y2 Rotation point (p₂) y coordinate. For variation, use varIndexBase + 5. -- uint32 varIndexBase Base index into DeltaSetIndexMap. [4] = function(f,format,offset) return { format = format, name = "PaintLinearGradient", color = getlinesoffset(f,offset,false), x0 = readfword(f), y0 = readfword(f), x1 = readfword(f), y1 = readfword(f), x2 = readfword(f), y2 = readfword(f), } end, [5] = function(f,format,offset) return { format = format, name = "PaintLinearGradient", color = getlinesoffset(f,offset,true), x0 = readfword(f), y0 = readfword(f), x1 = readfword(f), y1 = readfword(f), x2 = readfword(f), y2 = readfword(f), varbase = readulong(f), } end, -- Offset24 colorLineOffset Offset to VarColorLine table. -- FWORD x0 Start circle center x coordinate. For variation, use varIndexBase + 0. -- FWORD y0 Start circle center y coordinate. For variation, use varIndexBase + 1. -- UFWORD radius0 Start circle radius. For variation, use varIndexBase + 2. -- FWORD x1 End circle center x coordinate. For variation, use varIndexBase + 3. -- FWORD y1 End circle center y coordinate. For variation, use varIndexBase + 4. -- UFWORD radius1 End circle radius. For variation, use varIndexBase + 5. -- uint32 varIndexBase Base index into DeltaSetIndexMap. [6] = function(f,format,offset) return { format = format, name = "PaintRadialGradient", color = getlinesoffset(f,offset,false), x0 = readfword(f), y0 = readfword(f), radius0 = readushort(f), x1 = readfword(f), y1 = readfword(f), radius1 = readushort(f), } end, [7] = function(f,format,offset) return { format = format, name = "PaintRadialGradient", color = getlinesoffset(f,offset,true), x0 = readfword(f), y0 = readfword(f), radius0 = readushort(f), x1 = readfword(f), y1 = readfword(f), radius1 = readushort(f), varbase = readulong(f), } end, -- Offset24 colorLineOffset Offset to VarColorLine table. -- FWORD centerX Center x coordinate. For variation, use varIndexBase + 0. -- FWORD centerY Center y coordinate. For variation, use varIndexBase + 1. -- F2DOT14 startAngle Start of the angular range of the gradient, 180° in counter-clockwise degrees per 1.0 of value. For variation, use varIndexBase + 2. -- F2DOT14 endAngle End of the angular range of the gradient, 180° in counter-clockwise degrees per 1.0 of value. For variation, use varIndexBase + 3. -- uint32 varIndexBase Base index into DeltaSetIndexMap. [8] = function(f,format,offset) return { format = format, name = "PaintSweepGradient", color = getlinesoffset(f,offset,false), centerx = readfword(f), centery = readfword(f), startangle = read2dot14(f), endangle = read2dot14(f), } end, [9] = function(f,format,offset) return { format = format, name = "PaintSweepGradient", color = getlinesoffset(f,offset,true), centerx = readfword(f), centery = readfword(f), startangle = read2dot14(f), endangle = read2dot14(f), varbase = readulong(f), } end, -- Offset24 paintOffset Offset to a Paint table. -- uint16 glyphID Glyph ID for the source outline. [10] = function(f,format,offset) return { format = format, name = "PaintGlyph", paint = getpaintoffset(f,offset), glyph = readushort(f), } end, -- uint16 glyphID Glyph ID for a BaseGlyphList base glyph. [11] = function(f,format) return { format = format, name = "PaintColrGlyph", glyph = readushort(f), } end, -- Offset24 paintOffset Offset to a Paint subtable. -- Offset24 transformOffset Offset to an (Var)Affine2x3 table. [12] = function(f,format,offset) return { format = format, name = "PaintTransform", paint = getpaintoffset(f,offset), affine = getaffineoffset(f,offset,false), } end, [13] = function(f,format,offset) return { format = format, name = "PaintTransform", paint = getpaintoffset(f,offset), affine = getaffineoffset(f,offset,true), } end, -- Offset24 paintOffset Offset to a Paint subtable. -- FWORD dx Translation in x direction. For variation, use varIndexBase + 0. -- FWORD dy Translation in y direction. For variation, use varIndexBase + 1. -- uint32 varIndexBase Base index into DeltaSetIndexMap. [14] = function(f,format,offset) return { format = format, name = "PaintTranslate", paint = getpaintoffset(f,offset), dx = readfword(f), dy = readfword(f), } end, [15] = function(f,format,offset) return { format = format, name = "PaintTranslate", paint = getpaintoffset(f,offset), dx = readfword(f), dy = readfword(f), varbase = readulong(f), } end, -- Offset24 paintOffset Offset to a Paint subtable. -- F2DOT14 scaleX Scale factor in x direction. For variation, use varIndexBase + 0. -- F2DOT14 scaleY Scale factor in y direction. For variation, use varIndexBase + 1. -- uint32 varIndexBase Base index into DeltaSetIndexMap. [16] = function(f,format,offset) return { format = format, name = "PaintScale", paint = getpaintoffset(f,offset), scalex = read2dot14(f), scaley = read2dot14(f), } end, [17] = function(f,format,offset) return { format = format, name = "PaintScale", paint = getpaintoffset(f,offset), scalex = read2dot14(f), scaley = read2dot14(f), varbase = readulong(f), } end, -- Offset24 paintOffset Offset to a Paint subtable. -- F2DOT14 scaleX Scale factor in x direction. For variation, use varIndexBase + 0. -- F2DOT14 scaleY Scale factor in y direction. For variation, use varIndexBase + 1. -- FWORD centerX x coordinate for the center of scaling. For variation, use varIndexBase + 2. -- FWORD centerY y coordinate for the center of scaling. For variation, use varIndexBase + 3. -- uint32 varIndexBase Base index into DeltaSetIndexMap. [18] = function(f,format,offset) return { format = format, name = "PaintScale", paint = getpaintoffset(f,offset), scalex = read2dot14(f), scaley = read2dot14(f), centerx = readfword(f), centery = readfword(f), } end, [19] = function(f,format,offset) return { format = format, name = "PaintScale", paint = getpaintoffset(f,offset), scalex = read2dot14(f), scaley = read2dot14(f), centerx = readfword(f), centery = readfword(f), varbase = readulong(f), } end, -- Offset24 paintOffset Offset to a Paint subtable. -- F2DOT14 scale Scale factor in x and y directions. For variation, use varIndexBase + 0. -- uint32 varIndexBase Base index into DeltaSetIndexMap. [20] = function(f,format,offset) return { format = format, name = "PaintScale", paint = getpaintoffset(f,offset), scale = read2dot14(f), } end, [21] = function(f,format,offset) return { format = format, name = "PaintScale", paint = getpaintoffset(f,offset), scale = read2dot14(f), varbase = readulong(f), } end, -- Offset24 paintOffset Offset to a Paint subtable. -- F2DOT14 scale Scale factor in x and y directions. For variation, use varIndexBase + 0. -- FWORD centerX x coordinate for the center of scaling. For variation, use varIndexBase + 1. -- FWORD centerY y coordinate for the center of scaling. For variation, use varIndexBase + 2. -- uint32 varIndexBase Base index into DeltaSetIndexMap. [22] = function(f,format,offset) return { format = format, name = "PaintScale", paint = getpaintoffset(f,offset), scale = read2dot14(f), centerx = readfword(f), centery = readfword(f), } end, [23] = function(f,format,offset) return { format = format, name = "PaintScale", paint = getpaintoffset(f,offset), scale = read2dot14(f), centerx = readfword(f), centery = readfword(f), varbase = readulong(f), } end, -- Offset24 paintOffset Offset to a Paint subtable. -- F2DOT14 angle Rotation angle, 180° in counter-clockwise degrees per 1.0 of value. For variation, use varIndexBase + 0. -- uint32 varIndexBase Base index into DeltaSetIndexMap. [24] = function(f,format,offset) return { format = format, paint = getpaintoffset(f,offset), angle = read2dot14(f), name = "PaintRotate", } end, [25] = function(f,format,offset) return { format = format, name = "PaintRotate", paint = getpaintoffset(f,offset), angle = read2dot14(f), varbase = readulong(f), } end, -- Offset24 paintOffset Offset to a Paint subtable. -- F2DOT14 angle Rotation angle, 180° in counter-clockwise degrees per 1.0 of value. For variation, use varIndexBase + 0. -- FWORD centerX x coordinate for the center of rotation. For variation, use varIndexBase + 1. -- FWORD centerY y coordinate for the center of rotation. For variation, use varIndexBase + 2. -- uint32 varIndexBase Base index into DeltaSetIndexMap. [26] = function(f,format,offset) return { format = format, name = "PaintRotate", paint = getpaintoffset(f,offset), centerx = readfword(f), centery = readfword(f), } end, [27] = function(f,format,offset) return { format = format, name = "PaintRotate", paint = getpaintoffset(f,offset), centerx = readfword(f), centery = readfword(f), varbase = readulong(f), } end, -- Offset24 paintOffset Offset to a Paint subtable. -- F2DOT14 xSkewAngle Angle of skew in the direction of the x-axis, 180° in counter-clockwise degrees per 1.0 of value. For variation, use varIndexBase + 0. -- F2DOT14 ySkewAngle Angle of skew in the direction of the y-axis, 180° in counter-clockwise degrees per 1.0 of value. For variation, use varIndexBase + 1. -- uint32 varIndexBase Base index into DeltaSetIndexMap. [28] = function(f,format,offset) return { format = format, name = "PaintSkew", paint = getpaintoffset(f,offset), xangle = read2dot14(f), yangle = read2dot14(f), } end, [29] = function(f,format,offset) return { format = format, name = "PaintSkew", paint = getpaintoffset(f,offset), xangle = read2dot14(f), yangle = read2dot14(f), varbase = readulong(f), } end, -- Offset24 paintOffset Offset to a Paint subtable. -- F2DOT14 xSkewAngle Angle of skew in the direction of the x-axis, 180° in counter-clockwise degrees per 1.0 of value. For variation, use varIndexBase + 0. -- F2DOT14 ySkewAngle Angle of skew in the direction of the y-axis, 180° in counter-clockwise degrees per 1.0 of value. For variation, use varIndexBase + 1. -- FWORD centerX x coordinate for the center of rotation. For variation, use varIndexBase + 2. -- FWORD centerY y coordinate for the center of rotation. For variation, use varIndexBase + 3. -- uint32 varIndexBase Base index into DeltaSetIndexMap. [30] = function(f,format,offset) return { format = format, name = "PaintSkew", paint = getpaintoffset(f,offset), xangle = read2dot14(f), yangle = read2dot14(f), centerx = readfword(f), centery = readfword(f), } end, [31] = function(f,format,offset) return { format = format, name = "PaintSkew", paint = getpaintoffset(f,offset), xangle = read2dot14(f), yangle = read2dot14(f), centerx = readfword(f), centery = readfword(f), varbase = readulong(f), } end, -- Offset24 sourcePaintOffset Offset to a source Paint table. -- uint8 compositeMode A CompositeMode enumeration value. -- Offset24 backdropaintOffset Offset to a backdrop Paint table. [32] = function(f,format,offset) return { format = format, name = "PaintComposite", source = getpaintoffset(f,offset), mode = readuinteger(f), backdrop = getpaintoffset(f,offset), } end, } local unsupported = function() return false end setmetatableindex(paintreaders,function(t,format) if format then report("unsupported colr type 2 paint format %S",format) t[format] = unsupported -- problem: we don't know what to skip, so maybe best is to quit else report("possible error reading colr type 2 paint format") end return unsupported end) function readers.colr(f,fontdata,specification) local tableoffset = gotodatatable(f,fontdata,"colr",specification.glyphs) if tableoffset then paintdata, linesdata, affinedata, clipdata = nil, nil, nil, nil colrmap, colrdeltas, colrregions = nil, nil, nil local version = readushort(f) if version == 0 then -- we're okay elseif version == 1 then report("table version %a of %a is %s supported for font %s",version,"colr","partially",fontdata.filename) else report("table version %a of %a is %s supported for font %s",version,"colr","not",fontdata.filename) return end if not fontdata.tables.cpal then report("color table %a in font %a has no mandate %a table","colr",fontdata.filename,"cpal") return end local glyphs = fontdata.glyphs local nofglyphs = readushort(f) local baseoffset = readulong(f) layeroffset = readulong(f) local noflayers = readushort(f) local glyphlistoffset = 0 layerlistoffset = 0 local cliplistoffset = 0 local varindexmapoffset = 0 local variationoffset = 0 if version == 1 then glyphlistoffset = readulong(f) layerlistoffset = readulong(f) cliplistoffset = readulong(f) varindexmapoffset = readulong(f) variationoffset = readulong(f) end local layerrecords = { } local maxclass = 0 -- The special value 0xFFFF is foreground (but we index from 1). It -- more looks like indices into a palette so 'class' is a better name -- than 'palette'. if layeroffset > 0 and noflayers > 0 then setposition(f,tableoffset + layeroffset) for i=1,noflayers do local slot = readushort(f) local class = readushort(f) if class < 0xFFFF then class = class + 1 if class > maxclass then maxclass = class end end layerrecords[i] = { slot = slot, class = class, } end end fontdata.maxcolorclass = maxclass if baseoffset > 0 and nofglyphs > 0 then setposition(f,tableoffset + baseoffset) for i=0,nofglyphs-1 do local glyphindex = readushort(f) local firstlayer = readushort(f) local noflayers = readushort(f) if noflayers > 0 then local t = { } local g = glyphs[glyphindex] if g then for i=1,noflayers do t[i] = layerrecords[firstlayer+i] end g.colors = t end -- This is a catch for an emoji fonts where the base glyph has no dimensions and -- I admit that it's bad to fix this here instead of the font. But for sure someone -- will argue that the font is ok, as one can use ascender and descender for height -- and depth, but in tex we want granularity and exact dimensions. local b = g and g.boundingbox if g and (not b or (b[1] == 0 and b[2] == 0 and b[3] == 0 and b[4] == 0)) then local llx, lly, urx, ury for i=1,noflayers do local c = glyphs[t[i].slot] if c then b = c.boundingbox if b then local b1 = b[1] local b2 = b[2] local b3 = b[3] local b4 = b[4] if not llx then llx = b1 lly = b2 urx = b3 ury = b4 else if b1 < llx then llx = b1 end if b2 < lly then lly = b2 end if b3 > urx then urx = b3 end if b4 > ury then ury = b4 end end end end end if llx then g.boundingbox = { llx, lly, urx, ury } end end end end end if next(layerrecords) then report("table version %a of %a contains both legacy and paint data for font %s",version,"colr",fontdata.filename) end -- if true then -- return -- end if glyphlistoffset > 0 then local layers = { } local paints = { } local layercount = 0 if layerlistoffset > 0 then layeroffset = tableoffset + layerlistoffset setposition(f,layeroffset) layercount = readulong(f) -- layers = readcardinaltable(f,layercount,uoffset) for i=1,layercount do -- zero ? layers[i] = readulong(f) -- offsets to painttable end end -- local glyphoffset = tableoffset + glyphlistoffset setposition(f,glyphoffset) local glyphcount = readulong(f) for i=1,glyphcount do -- The Paint offset is relative to the beginning of the -- BaseGlyphList table. local glyph = readushort(f) local paintoffset = readulong(f) paints[glyph] = glyphoffset + paintoffset end paintdata = setmetatableindex(function(t,k) local p = getposition(f) setposition(f,k) local format = readuinteger(f) local v = paintreaders[format](f,format,k) setposition(f,p) t[k] = v return v end) linesdata = { } affinedata = { } for i=1,layercount do -- zero ? local o = layeroffset + layers[i] local l = paintdata[o] if not l then report("color table in font %a has an invalid layer entry %i, offset %i",i,layers[i]) end layers[i] = l and o or nil end for k, v in next, paints do if paintdata[v] then paints[k] = v -- first paint end end -- expand format 1 for k, v in next, paints do v = paintdata[v] if v then local format = v.format if format == 1 then -- name local count = v.count if count then local index = v.index + 1 local list = { } v.count = nil v.index = nil v.list = list for i=1,count do list[i] = layers[index] index = index + 1 end else -- already done end end else -- missing end end -- if variationoffset > 0 then local offsettostore = tableoffset + variationoffset colrregions, colrdeltas = readvariationdata(f,offsettostore,specification.factors) end if varindexmapoffset > 0 then colrmap = readcolrindexmap(f,tableoffset + varindexmapoffset) end setvariabledata(fontdata,"colr",{ regions = colrregions, deltas = colrdeltas, map = colrmap, }) -- ClipList records are ranges of glyph IDs. ClipBox format 2 -- adds a VarIndexBase; format 1 is the fixed form. if cliplistoffset > 0 then local clipoffset = tableoffset + cliplistoffset setposition(f,clipoffset) local clipformat = readuinteger(f) local nofclips = readulong(f) if clipformat == 1 then clipdata = { format = clipformat } for i=1,nofclips do local first = readushort(f) local last = readushort(f) local boxoffset = readuoffset(f) local box if boxoffset ~= 0 then local position = getposition(f) setposition(f,clipoffset + boxoffset) local format = readuinteger(f) if format == 1 then box = { format = format, xmin = readshort(f), ymin = readshort(f), xmax = readshort(f), ymax = readshort(f), } elseif format == 2 then box = { format = format, xmin = readshort(f), ymin = readshort(f), xmax = readshort(f), ymax = readshort(f), varbase = readulong(f), } end setposition(f,position) end clipdata[i] = { first = first, last = last, box = box, } end else report("unsupported ClipList format %a in font %a",clipformat,fontdata.filename) end end -- -- reindex tables, gives smaller files but optional because of tracing -- for k, v in next, linesdata do setposition(f,k) local extend = readuinteger(f) local count = readushort(f) local stops = { } if count then for i=1,count do stops[i] = { stop = read2dot14(f), palette = readushort(f), alpha = read2dot14(f), varbase = v and readulong(f) or nil, } end linesdata[k] = { extend = extend, stops = stops, } else report("running out of linedata in colr reading") linesdata[k] = { extend = 0, stops = stops, } break end end -- for k, v in next, affinedata do setposition(f,k) affinedata[k] = { xx = readfixed(f), yx = readfixed(f), xy = readfixed(f), yy = readfixed(f), dx = readfixed(f), dy = readfixed(f), varbase = v and readulong(f) or nil, } end -- if colrdeltas then local function adjustpaint(p) if not p then return end local base = p.varbase if base then local format = p.format if format == 3 then adjustcolr(p,"alpha",base,0,1/16384) elseif format == 5 then adjustcolr(p,"x0",base,0,1,true) adjustcolr(p,"y0",base,1,1,true) adjustcolr(p,"x1",base,2,1,true) adjustcolr(p,"y1",base,3,1,true) adjustcolr(p,"x2",base,4,1,true) adjustcolr(p,"y2",base,5,1,true) elseif format == 7 then adjustcolr(p,"x0",base,0,1,true) adjustcolr(p,"y0",base,1,1,true) adjustcolr(p,"radius0",base,2,1,true) adjustcolr(p,"x1",base,3,1,true) adjustcolr(p,"y1",base,4,1,true) adjustcolr(p,"radius1",base,5,1,true) elseif format == 9 then adjustcolr(p,"centerx",base,0,1,true) adjustcolr(p,"centery",base,1,1,true) adjustcolr(p,"startangle",base,2,1/16384) adjustcolr(p,"endangle",base,3,1/16384) elseif format == 15 then adjustcolr(p,"dx",base,0,1,true) adjustcolr(p,"dy",base,1,1,true) elseif format == 17 then adjustcolr(p,"scalex",base,0,1/16384) adjustcolr(p,"scaley",base,1,1/16384) elseif format == 19 then adjustcolr(p,"scalex",base,0,1/16384) adjustcolr(p,"scaley",base,1,1/16384) adjustcolr(p,"centerx",base,2,1,true) adjustcolr(p,"centery",base,3,1,true) elseif format == 21 then adjustcolr(p,"scale",base,0,1/16384) elseif format == 23 then adjustcolr(p,"scale",base,0,1/16384) adjustcolr(p,"centerx",base,1,1,true) adjustcolr(p,"centery",base,2,1,true) elseif format == 25 then adjustcolr(p,"angle",base,0,1/16384) elseif format == 27 then adjustcolr(p,"angle",base,0,1/16384) adjustcolr(p,"centerx",base,1,1,true) adjustcolr(p,"centery",base,2,1,true) elseif format == 29 then adjustcolr(p,"xangle",base,0,1/16384) adjustcolr(p,"yangle",base,1,1/16384) elseif format == 31 then adjustcolr(p,"xangle",base,0,1/16384) adjustcolr(p,"yangle",base,1,1/16384) adjustcolr(p,"centerx",base,2,1,true) adjustcolr(p,"centery",base,3,1,true) end end end for _, paint in next, paintdata do adjustpaint(paint) end for _, line in next, linesdata do if type(line) == "table" then for i=1,#line.stops do local stop = line.stops[i] if stop.varbase then stop.stop = stop.stop + colrdelta(stop.varbase,0,1/16384) stop.alpha = stop.alpha + colrdelta(stop.varbase,1,1/16384) end end end end for _, affine in next, affinedata do if type(affine) == "table" and affine.varbase then adjustcolr(affine,"xx",affine.varbase,0,1/65536) adjustcolr(affine,"yx",affine.varbase,1,1/65536) adjustcolr(affine,"xy",affine.varbase,2,1/65536) adjustcolr(affine,"yy",affine.varbase,3,1/65536) adjustcolr(affine,"dx",affine.varbase,4,1/65536) adjustcolr(affine,"dy",affine.varbase,5,1/65536) end end if clipdata then for i=1,#clipdata do local box = clipdata[i].box if box and box.varbase then adjustcolr(box,"xmin",box.varbase,0,1,floor) adjustcolr(box,"ymin",box.varbase,1,1,floor) adjustcolr(box,"xmax",box.varbase,2,1,ceil) adjustcolr(box,"ymax",box.varbase,3,1,ceil) end end end end -- local function rehash(t) local hash = { } local data = { } local n = 0 for k, v in sortedhash(t) do n = n + 1 hash[k] = n data[n] = v end return hash, data end -- if true then local phash, pdata = rehash(paintdata) local lhash, ldata = rehash(linesdata) local ahash, adata = rehash(affinedata) for k, v in next, paintdata do if v then local c = v.color if c then v.color = lhash[c] end local a = v.affine if a then v.affine = ahash[a] end local p = v.paint if p then v.paint = phash[p] end local l = v.list if l then for i=1,#l do l[i] = phash[l[i]] end end local s = v.source if s then v.source = phash[s] v.backdrop = phash[v.backdrop] end end end paintdata = pdata linesdata = ldata affinedata = adata for k, v in next, paints do -- zero indexed paints[k] = phash[v] end end -- if next(paints) then local function paletteof(index,seen) local paint = index and paintdata[index] if not paint then return nil end local format = paint.format if format == 2 or format == 3 then return paint.palette elseif format == 11 then seen = seen or { } if seen[paint.glyph] then return nil end seen[paint.glyph] = true return paletteof(paints[paint.glyph],seen) elseif paint.paint then return paletteof(paint.paint,seen) elseif paint.source then return paletteof(paint.source,seen) or paletteof(paint.backdrop,seen) end end local function collect(index,done,seen) local paint = index and paintdata[index] if not paint then return end local format = paint.format if format == 1 then local list = paint.list if list then for i=1,#list do collect(list[i],done,seen) end end elseif format == 10 then local palette = paletteof(paint.paint) done[#done+1] = { slot = paint.glyph, class = palette and (palette == 0xFFFF and 0xFFFF or palette + 1) or 1, } elseif format == 11 then local glyph = paint.glyph seen = seen or { } if not seen[glyph] then seen[glyph] = true collect(paints[glyph],done,seen) end end end for k, v in next, paints do local paint = paintdata[v] if paint and paint.format == 1 then local done = { } collect(v,done,{ }) local glyph = glyphs[k] if glyph then glyph.colors = done end elseif paint and paint.format == 11 then local done = { } collect(v,done,{ }) if #done > 0 then local glyph = glyphs[k] if glyph then glyph.colors = done end end end end end fontdata.colorpaintdata = paintdata fontdata.colorpaintlist = paints fontdata.colorlinesdata = linesdata fontdata.coloraffinedata = affinedata fontdata.colorclips = clipdata end end ::done:: fontdata.hascolor = true end end function readers.cpal(f,fontdata,specification) local tableoffset = gotodatatable(f,fontdata,"cpal",specification.glyphs) if tableoffset then local version = readushort(f) -- if version > 1 then -- report("table version %a of %a is not supported (yet), maybe font %s is bad",version,"cpal",fontdata.filename) -- return -- end local nofpaletteentries = readushort(f) local nofpalettes = readushort(f) local nofcolorrecords = readushort(f) local firstcoloroffset = readulong(f) local colorrecords = { } local palettes = readcardinaltable(f,nofpalettes,ushort) local palettetypesoffset local palettelabelsoffset local paletteentryoffset if version >= 1 then -- CPAL v1 adds palette usage flags and name IDs. The offsets are -- relative to the beginning of CPAL and may be NULL. palettetypesoffset = readulong(f) palettelabelsoffset = readulong(f) paletteentryoffset = readulong(f) end setposition(f,tableoffset+firstcoloroffset) for i=1,nofcolorrecords do local b, g, r, a = readbytes(f,4) colorrecords[i] = { r, g, b, a ~= 255 and a or nil, } end for i=1,nofpalettes do local p = { } local o = palettes[i] for j=1,nofpaletteentries do p[j] = colorrecords[o+j] end palettes[i] = p end fontdata.colorpalettes = palettes if version >= 1 then local info = { version = version } if palettetypesoffset and palettetypesoffset ~= 0 then setposition(f,tableoffset+palettetypesoffset) local types = { } for i=1,nofpalettes do types[i] = readulong(f) end info.paletteTypes = types end if palettelabelsoffset and palettelabelsoffset ~= 0 then setposition(f,tableoffset+palettelabelsoffset) local labels = { } for i=1,nofpalettes do labels[i] = readushort(f) end info.paletteLabels = labels end if paletteentryoffset and paletteentryoffset ~= 0 then setposition(f,tableoffset+paletteentryoffset) local labels = { } for i=1,nofcolorrecords do labels[i] = readushort(f) end info.entryLabels = labels end fontdata.colorpaletteinfo = info end end end local compress = gzip and gzip.compress local compressed = compress and gzip.compressed -- At some point I will delay loading and only store the offsets. -- compressed = false function readers.svg(f,fontdata,specification) local tableoffset = gotodatatable(f,fontdata,"svg",specification.glyphs) if tableoffset then local version = readushort(f) -- if version ~= 0 then -- report("table version %a of %a is not supported (yet), maybe font %s is bad",version,"svg",fontdata.filename) -- return -- end local glyphs = fontdata.glyphs local indexoffset = tableoffset + readulong(f) local reserved = readulong(f) setposition(f,indexoffset) local nofentries = readushort(f) local entries = { } for i=1,nofentries do entries[i] = { first = readushort(f), last = readushort(f), offset = indexoffset + readulong(f), length = readulong(f), } end for i=1,nofentries do local entry = entries[i] setposition(f,entry.offset) local data = readstring(f,entry.length) if compressed and not compressed(data) then data = compress(data) end entries[i] = { first = entry.first, last = entry.last, data = data } end fontdata.svgshapes = entries fontdata.hascolor = true end end function readers.sbix(f,fontdata,specification) local tableoffset = gotodatatable(f,fontdata,"sbix",specification.glyphs) if tableoffset then local version = readushort(f) local flags = readushort(f) local nofstrikes = readulong(f) local strikes = { } local nofglyphs = fontdata.nofglyphs for i=1,nofstrikes do strikes[i] = readulong(f) end local shapes = { } local done = 0 for i=1,nofstrikes do local strikeoffset = strikes[i] + tableoffset setposition(f,strikeoffset) strikes[i] = { ppem = readushort(f), ppi = readushort(f), offset = strikeoffset } end -- highest first sort(strikes,function(a,b) if b.ppem == a.ppem then return b.ppi < a.ppi else return b.ppem < a.ppem end end) local glyphs = { } -- local delayed = fonts.handlers.typethree for i=1,nofstrikes do local strike = strikes[i] local strikeppem = strike.ppem local strikeppi = strike.ppi local strikeoffset = strike.offset setposition(f,strikeoffset) for i=0,nofglyphs do glyphs[i] = readulong(f) end local glyphoffset = glyphs[0] for i=0,nofglyphs-1 do local nextoffset = glyphs[i+1] if not shapes[i] then local datasize = nextoffset - glyphoffset if datasize > 0 then setposition(f,strikeoffset + glyphoffset) local x = readshort(f) local y = readshort(f) local tag = readtag(f) -- or just skip, we never needed it till now local size = datasize - 8 local data = nil local offset = nil -- if delayed then offset = getposition(f) -- else -- data = readstring(f,size) -- size = nil -- end shapes[i] = { x = x, y = y, o = offset, s = size, data = data, -- tag = tag, -- maybe for tracing -- ppem = strikeppem, -- not used, for tracing -- ppi = strikeppi, -- not used, for tracing } done = done + 1 if done == nofglyphs then break end end end glyphoffset = nextoffset end end fontdata.pngshapes = shapes fontdata.hascolor = true end end -- Another bitmap (so not that useful) format. But Luigi found a font that -- has them , so ... do local function getmetrics(f) return { ascender = readinteger(f), descender = readinteger(f), widthmax = readuinteger(f), caretslopedumerator = readinteger(f), caretslopedenominator = readinteger(f), caretoffset = readinteger(f), minorigin = readinteger(f), minadvance = readinteger(f), maxbefore = readinteger(f), minafter = readinteger(f), pad1 = readinteger(f), pad2 = readinteger(f), } end -- bad names local function getbigmetrics(f) -- bigmetrics, maybe just skip 9 bytes return { height = readuinteger(f), width = readuinteger(f), horiBearingX = readinteger(f), horiBearingY = readinteger(f), horiAdvance = readuinteger(f), vertBearingX = readinteger(f), vertBearingY = readinteger(f), vertAdvance = readuinteger(f), } end local function getsmallmetrics(f) -- smallmetrics, maybe just skip 5 bytes return { height = readuinteger(f), width = readuinteger(f), bearingX = readinteger(f), bearingY = readinteger(f), advance = readuinteger(f), } end local function readbitmapfont(f,fontdata,specification,locatortag,datatag) local shapesfield = datatag == "cbdt" and "pngshapes" or "bitmapshapes" if not specification.glyphs or fontdata[shapesfield] then return end local datatableoffset = gotodatatable(f,fontdata,datatag,specification.glyphs) local locatoroffset = gotodatatable(f,fontdata,locatortag,specification.glyphs) if not datatableoffset or not locatoroffset then return end local majorversion = readushort(f) local minorversion = readushort(f) local nofsizetables = readulong(f) local sizetables = { } local shapes = { } local subtables = { } for i=1,nofsizetables do sizetables[i] = { subtables = readulong(f), indexsize = readulong(f), nofsubtables = readulong(f), colorref = readulong(f), hormetrics = getmetrics(f), vermetrics = getmetrics(f), firstindex = readushort(f), lastindex = readushort(f), ppemx = readbyte(f), ppemy = readbyte(f), bitdepth = readbyte(f), flags = readbyte(f), } end sort(sizetables,function(a,b) if b.ppemx == a.ppemx then return b.bitdepth < a.bitdepth else return b.ppemx < a.ppemx end end) for i=1,nofsizetables do local strike = sizetables[i] local used = false for glyph= strike.firstindex, strike.lastindex do if not shapes[glyph] then shapes[glyph] = i used = true end end strike.used = used end for i=1,nofsizetables do local strike = sizetables[i] if strike.used then local arrayoffset = locatoroffset + strike.subtables setposition(f,arrayoffset) for j=1,strike.nofsubtables do local firstindex = readushort(f) local lastindex = readushort(f) local relative = readulong(f) local subtableoffset = arrayoffset + relative local subtable = subtables[subtableoffset] if not subtable then subtable = { firstindex = firstindex, lastindex = lastindex, strike = strike, } subtables[subtableoffset] = subtable end for glyph=firstindex,lastindex do if shapes[glyph] == i then shapes[glyph] = subtable end end end end end for offset, subtable in sortedhash(subtables) do setposition(f,offset) subtable.indexformat = readushort(f) subtable.format = readushort(f) local imageoffset = readulong(f) local baseoffset = datatableoffset + imageoffset local offsets = { } local ends = { } local metrics = nil local firstindex = subtable.firstindex local lastindex = subtable.lastindex if subtable.indexformat == 1 then local values = { } for i=0,lastindex-firstindex+1 do values[i] = readulong(f) end for i=0,lastindex-firstindex do local glyph = firstindex + i offsets[glyph] = baseoffset + values[i] ends [glyph] = baseoffset + values[i+1] end elseif subtable.indexformat == 2 then local size = readulong(f) metrics = getbigmetrics(f) for glyph=firstindex,lastindex do local offset = baseoffset + (glyph-firstindex) * size offsets[glyph] = offset ends [glyph] = offset + size end elseif subtable.indexformat == 3 then local values = { } for i=0,lastindex-firstindex+1 do values[i] = readushort(f) end for i=0,lastindex-firstindex do local glyph = firstindex + i offsets[glyph] = baseoffset + values[i] ends [glyph] = baseoffset + values[i+1] end elseif subtable.indexformat == 4 then local count = readulong(f) local previousglyph, previousoffset for i=1,count+1 do local glyph = readushort(f) local value = readushort(f) if previousglyph then offsets[previousglyph] = baseoffset + previousoffset ends[previousglyph] = baseoffset + value end previousglyph, previousoffset = glyph, value end elseif subtable.indexformat == 5 then local size = readulong(f) subtable.imageSize = size metrics = getbigmetrics(f) local count = readulong(f) local glyphids = { } for i=1,count do glyphids[i] = readushort(f) end if math.odd(count) then skipbytes(f,2) end for i=1,count do local glyph = glyphids[i] local offset = baseoffset + (i-1) * size offsets[glyph] = offset ends [glyph] = offset + size end else report("unsupported %s index subtable format %a in font %a",locatortag,subtable.indexformat,fontdata.filename) end subtable.offsets = offsets subtable.ends = ends subtable.metrics = metrics end local default = { width = 0, height = 0 } local glyphs = fontdata.glyphs for index, subtable in sortedhash(shapes) do if type(subtable) == "table" then local start = subtable.offsets and subtable.offsets[index] local finish = subtable.ends and subtable.ends[index] local format = subtable.format local metrics = subtable.metrics or default local bitmap = true local payload, size, data, components if start then setposition(f,start) if format == 17 then metrics = getsmallmetrics(f) size = readulong(f) payload = getposition(f) elseif format == 18 then metrics = getbigmetrics(f) size = readulong(f) payload = getposition(f) elseif format == 19 then size = readulong(f) payload = getposition(f) elseif format == 1 then metrics = getsmallmetrics(f) payload = getposition(f) local rowbytes = (metrics.width * subtable.strike.bitdepth + 7) // 8 size = rowbytes * metrics.height elseif format == 2 then metrics = getsmallmetrics(f) payload = getposition(f) size = (metrics.width * metrics.height * subtable.strike.bitdepth + 7) // 8 elseif format == 5 then payload = start size = subtable.imageSize elseif format == 6 then metrics = getbigmetrics(f) payload = getposition(f) local rowbytes = (metrics.width * subtable.strike.bitdepth + 7) // 8 size = rowbytes * metrics.height elseif format == 7 then metrics = getbigmetrics(f) payload = getposition(f) size = (metrics.width * metrics.height * subtable.strike.bitdepth + 7) // 8 elseif format == 8 then metrics = getsmallmetrics(f) skipbytes(f,1) -- padding local count = readushort(f) components = { } for i=1,count do components[i] = { glyph = readushort(f), x = readinteger(f), y = readinteger(f), } end payload, size, bitmap = start, (finish and finish-start or 0), false elseif format == 9 then metrics = getbigmetrics(f) components = { } local count = readushort(f) for i=1,count do components[i] = { glyph = readushort(f), x = readinteger(f), y = readinteger(f), } end payload, size, bitmap = start, (finish and finish-start or 0), false else payload, size = start, finish and finish-start or 0 end if finish and payload then local available = finish - payload if size > available then size = available end end end local x = metrics.width or 0 local y = metrics.height or 0 shapes[index] = { x = x, y = y, o = payload, s = size, data = data, format = format, metrics = metrics, components = components, bitdepth = subtable.strike.bitdepth, bitmap = bitmap, ppem = subtable.strike.ppemx, ppi = subtable.strike.ppemy, } local glyph = glyphs and glyphs[index] if glyph and not glyph.boundingbox and x > 0 and y > 0 then local width = glyph.width or x glyph.boundingbox = { 0, 0, width, width * y / x } end else shapes[index] = { x = 0, y = 0, data = "" } end end fontdata.bitmapstrikes = fontdata.bitmapstrikes or { } fontdata.bitmapstrikes[datatag] = sizetables fontdata.bitmapformats = fontdata.bitmapformats or { } fontdata.bitmapformats[datatag] = { majorversion = majorversion, minorversion = minorversion, } fontdata[shapesfield] = shapes if datatag == "cbdt" then fontdata.hascolor = true end end function readers.cblc(f,fontdata,specification) readbitmapfont(f,fontdata,specification,"cblc","cbdt") end function readers.cbdt(f,fontdata,specification) if not fontdata.pngshapes then readbitmapfont(f,fontdata,specification,"cblc","cbdt") end end -- function readers.eblc(f,fontdata,specification) -- readbitmapfont(f,fontdata,specification,"eblc","ebdt") -- if fontdata.tables.ebsc and not fontdata.bitmapscales then -- readers.ebsc(f,fontdata,specification) -- end -- end -- -- function readers.ebdt(f,fontdata,specification) -- if not fontdata.bitmapshapes then -- readbitmapfont(f,fontdata,specification,"eblc","ebdt") -- end -- end -- -- function readers.ebsc(f,fontdata,specification) -- if fontdata.bitmapscales then -- return -- end -- local tableoffset = gotodatatable(f,fontdata,"ebsc",specification.glyphs) -- if tableoffset then -- local version = readulong(f) -- local nofsizes = readulong(f) -- local sizes = { } -- for i=1,nofsizes do -- sizes[i] = { -- ppemx = readbyte(f), -- ppemy = readbyte(f), -- substitutePpemx = readbyte(f), -- substitutePpemy = readbyte(f), -- } -- end -- fontdata.bitmapscales = { version = version, sizes = sizes } -- end -- end end -- + AVAR : optional -- + CFF2 : otf outlines -- - CVAR : ttf hinting, not needed -- + FVAR : the variations -- + GVAR : ttf outline changes -- + HVAR : horizontal changes -- + MVAR : metric changes -- + STAT : relations within fonts -- + VVAR : vertical changes -- -- * BASE : extra baseline adjustments -- + GASP : range behavior and MVAR targets -- + GDEF : classes, mark sets, attachments and carets -- + GPOS : device and variation-index tables -- + GSUB : new table -- + NAME : 25 added function readers.gasp(f,fontdata,specification) local tableoffset = gotodatatable(f,fontdata,"gasp",true) if tableoffset then local version = readushort(f) local nofranges = readushort(f) local ranges = { } for i=1,nofranges do ranges[i] = { maxPPEM = readushort(f), behavior = readushort(f), } end fontdata.gasp = { version = version, ranges = ranges, } end end function readers.stat(f,fontdata,specification) local tableoffset = gotodatatable(f,fontdata,"stat",true) -- specification.variable if tableoffset then local extras = fontdata.extras local version = readulong(f) -- 0x00010000 local axissize = readushort(f) local nofaxis = readushort(f) local axisoffset = readulong(f) local nofvalues = readushort(f) local valuesoffset = readulong(f) local fallbackname = version >= 0x00010001 and extras[readushort(f)] or nil local axis = { } local values = { } local combinations = { } setposition(f,tableoffset+axisoffset) for i=1,nofaxis do local tag = readtag(f) axis[i] = { tag = tag, name = lower(extras[readushort(f)] or tag), ordering = readushort(f), -- maybe gaps variants = { } } if axissize > 8 then skipbytes(f,axissize-8) end end -- flags: -- -- 0x0001 : OlderSiblingFontAttribute -- 0x0002 : ElidableAxisValueName -- 0xFFFC : reservedFlags -- setposition(f,tableoffset+valuesoffset) for i=1,nofvalues do values[i] = readushort(f) end for i=1,nofvalues do setposition(f,tableoffset + valuesoffset + values[i]) local format = readushort(f) local index, flags, name, value, variant if format == 4 then local nofaxes = readushort(f) flags = readushort(f) name = lower(extras[readushort(f)] or "no name") value = readfixed(f) variant = { flags = flags, name = name, value = value, axes = { }, } for j=1,nofaxes do variant.axes[j] = { axis = readushort(f) + 1, value = readfixed(f), } end combinations[#combinations+1] = variant else index = readushort(f) + 1 flags = readushort(f) name = lower(extras[readushort(f)] or "no name") value = readfixed(f) end if format == 1 then variant = { flags = flags, name = name, value = value, } elseif format == 2 then variant = { flags = flags, name = name, value = value, minimum = readfixed(f), maximum = readfixed(f), } elseif format == 3 then variant = { flags = flags, name = name, value = value, link = readfixed(f), } end if format ~= 4 and variant and axis[index] then insert(axis[index].variants,variant) end end -- axisIndex values in format 4 refer to the design-axis record -- order, while axisOrdering is only a UI ordering hint. Sort a -- separate view and remap the explicit axis indices accordingly. local designaxis = { } for i=1,#axis do designaxis[i] = axis[i] end sort(designaxis,function(a,b) return a.ordering < b.ordering end) local axisorder = { } for i=1,#designaxis do axisorder[designaxis[i]] = i end for i=1,#combinations do local combination = combinations[i] for j=1,#combination.axes do local record = combination.axes[j] local a = axis[record.axis] record.axis = axisorder[a] or record.axis record.tag = a and a.tag or nil end end for i=1,#designaxis do local a = designaxis[i] sort(a.variants,function(a,b) return a.name < b.name end) a.ordering = nil end setvariabledata(fontdata,"designaxis",designaxis) setvariabledata(fontdata,"axisvalues",combinations) setvariabledata(fontdata,"fallbackname",fallbackname) end end -- The avar table is optional and used in combination with fvar. Given the -- detailed explanation about bad values we expect the worst and do some -- checking. function readers.avar(f,fontdata,specification) local tableoffset = gotodatatable(f,fontdata,"avar",true) -- specification.variable if tableoffset then local function collect() local nofvalues = readushort(f) local values = { } local lastfrom = false local lastto = false for i=1,nofvalues do local from = read2dot14(f) local to = read2dot14(f) if lastfrom and from <= lastfrom then -- ignore elseif lastto and to < lastto then -- ignore else values[#values+1] = { from, to } lastfrom, lastto = from, to end end nofvalues = #values if nofvalues > 2 then local some = values[1] if some[1] == -1 and some[2] == -1 then some = values[nofvalues] if some[1] == 1 and some[2] == 1 then for i=2,nofvalues-1 do some = values[i] if some[1] == 0 and some[2] == 0 then return values end end end end end return false end local version = readulong(f) -- 0x00010000 local reserved = readushort(f) local nofaxis = readushort(f) local segments = { } for i=1,nofaxis do segments[i] = collect() end setvariabledata(fontdata,"segments",segments) end end function readers.fvar(f,fontdata,specification) local tableoffset = gotodatatable(f,fontdata,"fvar",true) -- specification.variable or specification.instancenames if tableoffset then local version = readulong(f) -- 0x00010000 local offsettoaxis = tableoffset + readushort(f) local reserved = skipshort(f) -- pair 1 local nofaxis = readushort(f) local sizeofaxis = readushort(f) -- pair 2 local nofinstances = readushort(f) local sizeofinstances = readushort(f) -- local extras = fontdata.extras local axis = { } local instances = { } -- setposition(f,offsettoaxis) -- for i=1,nofaxis do local tag = readtag(f) local minimum = readfixed(f) local default = readfixed(f) local maximum = readfixed(f) local flags = readushort(f) local axisnameid = readushort(f) axis[i] = { tag = tag, -- ital opsz slnt wdth wght minimum = minimum, default = default, maximum = maximum, flags = flags, name = axisnameid > 255 and axisnameid < 32768 and lower(extras[axisnameid] or "bad name") or "bad name", } local n = sizeofaxis - 20 if n > 0 then skipbytes(f,n) elseif n < 0 then -- error end end -- local nofbytes = 2 + 2 + 2 + nofaxis * 4 local readpsname = nofbytes <= sizeofinstances local skippable = sizeofinstances - nofbytes for i=1,nofinstances do local subfamid = readushort(f) local flags = readushort(f) -- 0, not used yet local values = { } for i=1,nofaxis do values[i] = { axis = axis[i].tag, value = readfixed(f), } end local psnameid = readpsname and readushort(f) or 0xFFFF if subfamid == 2 or subfamid == 17 then -- okay elseif subfamid == 0xFFFF then subfamid = nil elseif subfamid <= 255 or subfamid >= 32768 then subfamid = nil -- actually an error end if psnameid == 6 then -- okay elseif psnameid == 0xFFFF then psnameid = nil elseif psnameid <= 255 or psnameid >= 32768 then psnameid = nil -- actually an error end instances[i] = { -- flags = flags, subfamily = extras[subfamid], psname = psnameid and extras[psnameid] or nil, values = values, } if skippable > 0 then skipbytes(f,skippable) end end setvariabledata(fontdata,"axis",axis) setvariabledata(fontdata,"instances",instances) end end local function calculate(f,fontdata,specification,offset,field,regions,deltas,nozero,variabletag) -- -- innerIndexBitCountMask = 0x000F -- mapEntrySizeMask = 0x0030 -- reservedFlags = 0xFFC0 -- -- outerIndex = entry >> ((entryFormat & innerIndexBitCountMask) + 1) -- innerIndex = entry & ((1 << ((entryFormat & innerIndexBitCountMask) + 1)) - 1) -- local mapcount = 0 local innerindex = { } -- size is mapcount local outerindex = { } -- size is mapcount if offset and offset > 0 then setposition(f,offset) local format = readushort(f) mapcount = readushort(f) local entrysize = (((format & 0x0030) >> 4) & 0xFFFFFFFF) + 1 local nofinnerbits = (format & 0x000F) + 1 local innermask = (1 << nofinnerbits) - 1 local readcardinal = read_cardinal[entrysize] if readcardinal then for i=0,mapcount-1 do local mapdata = readcardinal(f) outerindex[i] = (mapdata >> nofinnerbits) & 0xFFFFFFFF innerindex[i] = (mapdata & innermask) end else mapcount = 0 end end if variabletag then setvariabledata(fontdata,variabletag,true) end local glyphs = fontdata.glyphs for i=0,fontdata.nofglyphs-1 do local glyph = glyphs[i] local outer, inner if mapcount == 0 then outer, inner = 0, i elseif i < mapcount then outer, inner = outerindex[i], innerindex[i] else outer, inner = outerindex[mapcount-1], innerindex[mapcount-1] end if outer and inner then -- not needed local delta = deltas[outer+1] if delta then local d = delta.deltas[inner+1] if d then local scales = delta.scales local deltaw = field and glyph[field] or 0 for i=1,#scales do local di = d[i] if di then deltaw = deltaw + scales[i] * di else break -- can't happen end end deltaw = round(deltaw) if nozero and deltaw == 0 then -- no need for dlsb zero else glyph[field] = deltaw end end end end end end -- Todo: when it's tested for a while the lsb hackery can be backported from the -- font-dsp.lmt file. function readers.hvar(f,fontdata,specification) local factors = specification.factors if not factors then return end local tableoffset = gotodatatable(f,fontdata,"hvar",specification.variable) if not tableoffset then -- report("no hvar table, expect problems due to messy widths") return end local version = readulong(f) -- 0x00010000 local variationoffset = tableoffset + readulong(f) -- the store local advanceoffset = tableoffset + readulong(f) local lsboffset = tableoffset + readulong(f) local rsboffset = tableoffset + readulong(f) if variationoffset > tableoffset then local regions, deltas = readvariationdata(f,variationoffset,factors) if regions then calculate(f,fontdata,specification,advanceoffset > tableoffset and advanceoffset or nil,"width",regions,deltas,false,"hvarwidths") -- I don't want to save the lsb as it is the llx that we already store but as -- we're ahead of reading the bounding box so we cannot set the right lsb here -- so we set the delta (d) instead. if lsboffset > tableoffset then calculate(f,fontdata,specification,lsboffset,"dlsb",regions,deltas,true) -- delta lsb end if rsboffset > tableoffset then calculate(f,fontdata,specification,rsboffset,"drsb",regions,deltas,true) end setvariabledata(fontdata,"hvar",{ regions = regions, deltas = deltas, }) -- setvariabledata(fontdata,"hregions",regions) end end end function readers.vvar(f,fontdata,specification) local factors = specification.factors if not factors then return end local tableoffset = gotodatatable(f,fontdata,"vvar",specification.variable) if not tableoffset then return end local version = readulong(f) local variationoffset = tableoffset + readulong(f) local advanceoffset = tableoffset + readulong(f) local tsboffset = tableoffset + readulong(f) local bsboffset = tableoffset + readulong(f) local originoffset = tableoffset + readulong(f) if variationoffset > tableoffset then local regions, deltas = readvariationdata(f,variationoffset,factors) if regions then calculate(f,fontdata,specification,advanceoffset > tableoffset and advanceoffset or nil,"dvadvance",regions,deltas,false,"vvar") if tsboffset > tableoffset then calculate(f,fontdata,specification,tsboffset,"dvtsb",regions,deltas,false,"vvar") end if bsboffset > tableoffset then calculate(f,fontdata,specification,bsboffset,"dvbsb",regions,deltas,false,"vvar") end if originoffset > tableoffset then calculate(f,fontdata,specification,originoffset,"dvorg",regions,deltas,false,"vvar") end setvariabledata(fontdata,"vvar",{ regions = regions, deltas = deltas, }) end end end function readers.mvar(f,fontdata,specification) local tableoffset = gotodatatable(f,fontdata,"mvar",specification.variable) if tableoffset then -- The core reader does not otherwise need the optional gasp table, -- but MVAR records may target gasp range limits (gsp0..gsp9). if fontdata.tables.gasp and not fontdata.gasp then readers.gasp(f,fontdata,specification) setposition(f,tableoffset) end local version = readulong(f) -- 0x00010000 local reserved = skipshort(f,1) local recordsize = readushort(f) local nofrecords = readushort(f) local storeoffset = readushort(f) local offsettostore = storeoffset ~= 0 and tableoffset + storeoffset or 0 local factors = specification.factors local records = { } if factors and offsettostore > 0 then local regions, deltas = readvariationdata(f,offsettostore,factors) for i=1,nofrecords do local tag = readtag(f) local outer = readushort(f) local inner = readushort(f) records[i] = { tag = tag, outer = outer, inner = inner } local var = variabletags[tag] if var then local delta = deltas[outer+1] if delta then local d = delta.deltas[inner+1] if d then local scales = delta.scales local dd = 0 for i=1,#scales do dd = dd + scales[i] * d[i] end var(fontdata,round(dd)) end end end if recordsize > 8 then -- 4 + 2 + 2 skipbytes(f,recordsize-8) end end else for i=1,nofrecords do local tag = readtag(f) local outer = readushort(f) local inner = readushort(f) records[i] = { tag = tag, outer = outer, inner = inner } if recordsize > 8 then skipbytes(f,recordsize-8) end end end setvariabledata(fontdata,"mvar",records) end end function readers.dsig(f,fontdata,specification) -- We're not going to deal with this security crap. end