diff options
Diffstat (limited to 'Master/texmf-dist/tex/context/base/mkiv/lpdf-emb.lua')
-rw-r--r-- | Master/texmf-dist/tex/context/base/mkiv/lpdf-emb.lua | 2224 |
1 files changed, 2224 insertions, 0 deletions
diff --git a/Master/texmf-dist/tex/context/base/mkiv/lpdf-emb.lua b/Master/texmf-dist/tex/context/base/mkiv/lpdf-emb.lua new file mode 100644 index 00000000000..4ffcfd8a846 --- /dev/null +++ b/Master/texmf-dist/tex/context/base/mkiv/lpdf-emb.lua @@ -0,0 +1,2224 @@ +if not modules then modules = { } end modules ['lpdf-ini'] = { + version = 1.001, + comment = "companion to lpdf-ini.mkiv", + author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", + copyright = "PRAGMA ADE / ConTeXt Development Team", + license = "see context related readme files" +} + +-- vkgoeswild: Pink Floyd - Shine on You Crazy Diamond - piano cover (around that +-- time I redid the code, a reminder so to say) + +-- At some point I wanted to have access to the shapes so that we could use them in +-- metapost. So, after looking at the cff and ttf specifications, I decided to write +-- parsers. At somepoint we needed a cff parser anyway in order to calculate the +-- dimensions. Then variable fonts came around and a option was added to recreate +-- streams of operators and a logical next step was to do all inclusion that way. It +-- was only then that I found out that some of the juggling also happens in the the +-- backend, but spread over places, so I could have saved myself some time +-- deciphering the specifications. Anyway, here we go. +-- +-- Color fonts are a bit messy. Catching issues with older fonts will break new ones +-- so I don't think that it's wise to build in too many catches (like for fonts with +-- zero boundingboxes, weird dimensions, transformations that in a next version are +-- fixed, etc.). Better is then to wait till something gets fixed. If a spec doesn't +-- tell me how to deal with it ... I'll happily wait till it does. After all we're +-- not in a hurry as these fonts are mostly meant for the web or special purposes +-- with manual tweaking in desk top publishing applications. Keep in mind that Emoji +-- can have funny dimensions (e.g. to be consistent within a font, so no tight ones). + +-- When we have moved to lmtx I will document a bit more. Till then it's experimental +-- and subjected to change. + +local next, type, unpack, rawget = next, type, unpack, rawget +local char, byte, gsub, sub, match, rep, gmatch = string.char, string.byte, string.gsub, string.sub, string.match, string.rep, string.gmatch +local formatters = string.formatters +local format = string.format +local concat, sortedhash, sort = table.concat, table.sortedhash, table.sort +local utfchar = utf.char +local random, round, max, abs, ceiling = math.random, math.round, math.max, math.abs, math.ceiling +local extract, lshift, rshift, band, bor = bit32.extract, bit32.lshift, bit32.rshift, bit32.band, bit32.bor +local idiv = number.idiv +local setmetatableindex = table.setmetatableindex + +local pdfnull = lpdf.null +local pdfdictionary = lpdf.dictionary +local pdfarray = lpdf.array +local pdfconstant = lpdf.constant +local pdfstring = lpdf.string +local pdfreference = lpdf.reference +local pdfreserveobject = lpdf.reserveobject +local pdfflushobject = lpdf.flushobject +local pdfflushstreamobject = lpdf.flushstreamobject + +local report_fonts = logs.reporter("backend","fonts") + +local trace_fonts = false +local trace_details = false + +local dimenfactors = number.dimenfactors +local bpfactor = dimenfactors.bp +local ptfactor = dimenfactors.pt + +trackers.register("backend.pdf.fonts",function(v) trace_fonts = v end) + +local readers = fonts.handlers.otf.readers +local getinfo = readers.getinfo + +local setposition = utilities.files.setposition +local readstring = utilities.files.readstring +local openfile = utilities.files.open +local closefile = utilities.files.close + + +-- needs checking: signed vs unsigned + +local tocardinal1 = char + +local function tocardinal2(n) + return char(extract(n,8,8),extract(n,0,8)) +end + +local function tocardinal3(n) + return char(extract(n,16,8),extract(n,8,8),extract(n,0,8)) +end + +local function tocardinal4(n) + return char(extract(n,24,8),extract(n,16,8),extract(n,8,8),extract(n,0,8)) +end + +local function tointeger2(n) + return char(extract(n,8,8),extract(n,0,8)) +end + +local function tointeger3(n) + return char(extract(n,16,8),extract(n,8,8),extract(n,0,8)) +end + +local function tointeger4(n) + return char(extract(n,24,8),extract(n,16,8),extract(n,8,8),extract(n,0,8)) +end + +local function tocardinal8(n) + local l = idiv(n,0x100000000) + local r = n % 0x100000000 + return char(extract(l,24,8),extract(l,16,8),extract(l,8,8),extract(l,0,8), + extract(r,24,8),extract(r,16,8),extract(r,8,8),extract(r,0,8)) +end + +-- A couple of shared helpers. + +local tounicodedictionary, widtharray, collectindices, subsetname, includecidset, forcecidset, tocidsetdictionary + +do + + -- Because we supply tounicodes ourselves we only use bfchar mappings (as in the + -- backend). In fact, we can now no longer pass the tounicodes to the frontend but + -- pick them up from the descriptions. + + local f_mapping_2 = formatters["<%02X> <%s>"] + local f_mapping_4 = formatters["<%04X> <%s>"] + + local tounicode = fonts.mappings.tounicode + +local tounicode_template = [[ +%%!PS-Adobe-3.0 Resource-CMap +%%%%DocumentNeededResources: ProcSet (CIDInit) +%%%%IncludeResource: ProcSet (CIDInit) +%%%%BeginResource: CMap (TeX-%s-0) +%%%%Title: (TeX-%s-0 TeX %s 0)| +%%%%Version: 1.000 +%%%%EndComments +/CIDInit /ProcSet findresource begin + 12 dict begin + begincmap + /CIDSystemInfo + << /Registry (TeX) /Ordering (%s) /Supplement 0 >> + def + /CMapName + /TeX-Identity-%s + def + /CMapType + 2 + def + 1 begincodespacerange + <%s> <%s> + endcodespacerange + %i beginbfchar +%s + endbfchar + endcmap + CMapName currentdict /CMap defineresource pop + end +end +%%%%EndResource +%%%%EOF]] + + tounicodedictionary = function(details,indices,maxindex,name,wide) + local mapping = { } + local length = 0 + if maxindex > 0 then + local f_mapping = wide and f_mapping_4 or f_mapping_2 + for index=1,maxindex do + local data = indices[index] + if data then + length = length + 1 + local unicode = data.unicode + if unicode then + unicode = tounicode(unicode) + else + unicode = "FFFD" + end + mapping[length] = f_mapping(index,unicode) + end + end + end + local name = gsub(name,"%+","-") -- like luatex does + local first = wide and "0000" or "00" + local last = wide and "FFFF" or "FF" + local blob = format(tounicode_template,name,name,name,name,name,first,last,length,concat(mapping,"\n")) + return blob + end + + widtharray = function(details,indices,maxindex,units) + local widths = pdfarray() + local length = 0 + local factor = 10000 / units + if maxindex > 0 then + local lastindex = -1 + local sublist = nil + for index=1,maxindex do + local data = indices[index] + if data then + local width = data.width -- hm, is inaccurate for cff, so take from elsewhere + if width then + -- width = round(width * 10000 / units) / 10 + width = round(width * factor) / 10 + else + width = 0 + end + if index == lastindex + 1 then + sublist[#sublist+1] = width + else + if sublist then + length = length + 1 + widths[length] = sublist + end + sublist = pdfarray { width } + length = length + 1 + widths[length] = index + end + lastindex = index + end + end + length = length + 1 + widths[length] = sublist + end + return widths + end + + collectindices = function(descriptions,indices) + local minindex = 0xFFFF + local maxindex = 0 + local reverse = { } + -- todo: already at definition time trigger copying streams + -- and add extra indices ... first i need a good example of + -- a clash + -- for unicode, data in next, descriptions do + -- local i = data.index or unicode + -- if reverse[i] then + -- print("CLASH") + -- else + -- reverse[i] = data + -- end + -- end + for unicode, data in next, descriptions do + reverse[data.index or unicode] = data + end + for index in next, indices do + if index > maxindex then + maxindex = index + end + if index < minindex then + minindex = index + end + indices[index] = reverse[index] + end + if minindex > maxindex then + minindex = maxindex + end + return indices, minindex, maxindex + end + + includecidset = false + forcecidset = false -- for private testing only + + directives.register("backend.pdf.forcecidset",function(v) + forcecidset = v + end) + + tocidsetdictionary = function(indices,min,max) + if includecidset or forcecidset then + local b = { } + local m = idiv(max+7,8) + for i=0,max do + b[i] = 0 + end + b[0] = bor(b[0],lshift(1,7)) -- force notdef into the file + for i=min,max do + if indices[i] then + local bi = idiv(i,8) + local ni = i % 8 + b[bi] = bor(b[bi],lshift(1,7-ni)) + end + end + b = char(unpack(b,0,#b)) + return pdfreference(pdfflushstreamobject(b)) + end + end + + -- Actually we can use the same as we only embed once. + + -- subsetname = function(name) + -- return "CONTEXT" .. name + -- end + + local prefixes = { } -- todo: set fixed one + + subsetname = function(name) + local prefix + while true do + prefix = utfchar(random(65,90),random(65,90),random(65,90),random(65,90),random(65,90),random(65,90)) + if not prefixes[prefix] then + prefixes[prefix] = true + break + end + end + return prefix .. "+" .. name + end + +end + +-- Map file mess. + +local loadmapfile, loadmapline, getmapentry do + + -- We only need to pick up the filename and optionally the enc file + -- as we only use them for old school virtual math fonts. We might as + -- we drop this completely. + + local find, match, splitlines = string.find, string.match, string.splitlines + + + local mappings = { } + + loadmapline = function(n) + if trace_fonts then + report_fonts("mapline: %s",n) + end + local name, fullname, encfile, pfbfile = match(n,"(%S+)%s+(%S+).-<(.-%.enc).-<(.-%.pfb)") + if name then + mappings[name] = { fullname, encfile, pfbfile } + end + end + + loadmapfile = function(n) + local okay, data = resolvers.loadbinfile(n,"map") + if okay and data then + data = splitlines(data) + for i=1,#data do + local d = data[i] + if d ~= "" and not find(d,"^[#%%]") then + loadmapline(d) + end + end + end + end + + getmapentry = function(n) + local n = file.nameonly(n) + local m = mappings[n] + if m then + local encfile = m[2] + local encoding = fonts.encodings.load(encfile) + if not encoding then + return + end + local pfbfile = resolvers.findfile(m[3],"pfb") + if not pfbfile or pfbfile == "" then + return + end + return encoding, pfbfile, encfile + end + end + + lpdf.getmapentry = getmapentry + +end + +-- The three writers: opentype, truetype and type1. + +local mainwriters = { } + +do + + -- advh = os2.ascender - os2.descender + -- tsb = default_advh - os2.ascender + + -- truetype has the following tables: + + -- head : mandate + -- hhea : mandate + -- vhea : mandate + -- hmtx : mandate + -- maxp : mandate + -- glyf : mandate + -- loca : mandate + -- + -- cvt : if needed (but we flatten) + -- fpgm : if needed (but we flatten) + -- prep : if needed (but we flatten) + -- PCLT : if needed (but we flatten) + -- + -- name : not needed for T2: backend does that + -- post : not needed for T2: backend does that + -- OS/2 : not needed for T2: backend does that + -- cmap : not needed for T2: backend does that + + local streams = utilities.streams + local openstring = streams.openstring + local readcardinal2 = streams.readcardinal2 + ----- readcardinal4 = streams.readcardinal4 + + local otfreaders = fonts.handlers.otf.readers + + local function readcardinal4(f) -- this needs to be sorted out + local a = readcardinal2(f) + local b = readcardinal2(f) + if a and b then + return a * 0x10000 + b + end + end + + -- -- -- + + local tablereaders = { } + local tablewriters = { } + local tablecreators = { } + local tableloaders = { } + + local openfontfile, closefontfile, makefontfile, makemetadata do + + local details = { + details = true, + platformnames = true, + platformextras = true, + } + + -- .022 sec on luatex manual, neglectable: + + -- local function checksum(data) + -- local s = openstring(data) + -- local n = 0 + -- local d = #data + -- while true do + -- local c = readcardinal4(s) + -- if c then + -- n = (n + c) % 0x100000000 + -- else + -- break + -- end + -- end + -- return n + -- end + + local function checksum(data) + local s = openstring(data) + local n = 0 + local d = #data + while true do + local a = readcardinal2(s) + local b = readcardinal2(s) + if b then + n = (n + a * 0x10000 + b) % 0x100000000 + else + break + end + end + return n + end + + openfontfile = function(details) + return { + offset = 0, + order = { }, + used = { }, + details = details, + streams = details.streams, + } + end + + closefontfile = function(fontfile) + for k, v in next, fontfile do + fontfile[k] = nil -- so it can be collected asap + end + end + + local metakeys = { + "uniqueid", "version", + "copyright", "license", "licenseurl", + "manufacturer", "vendorurl", + "family", "subfamily", + "typographicfamily", "typographicsubfamily", + "fullname", "postscriptname", + } + + local template = [[ +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> + <x:xmpmeta xmlns:x="adobe:ns:meta/"> + <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> + <rdf:Description rdf:about="" xmlns:pdfx="http://ns.adobe.com/pdfx/1.3/"> + +%s + + </rdf:Description> + </rdf:RDF> + </x:xmpmeta> +<?xpacket end="w"?>]] + + makemetadata = function(fontfile) + local names = fontfile.streams.names + local list = { } + local f_name = formatters["<pdfx:%s>%s</pdfx:%s>"] + for i=1,#metakeys do + local m = metakeys[i] + local n = names[m] + if n then + list[#list+1] = f_name(m,n,m) + end + end + return format(template,concat(list,"\n")) + end + + makefontfile = function(fontfile) + local order = fontfile.order + local used = fontfile.used + local count = 0 + for i=1,#order do + local tag = order[i] + local data = fontfile[tag] + if data and #data > 0 then + count = count + 1 + else + fontfile[tag] = false + end + end + local offset = 12 + (count * 16) + local headof = 0 + local list = { + "" -- placeholder + } + local i = 1 + local k = 0 + while i <= count do + i = lshift(i,1) + k = k + 1 + end + local searchrange = lshift(i,3) + local entryselector = k - 1 + local rangeshift = lshift(count,4) - lshift(i,3) + local index = { + tocardinal4(0x00010000), -- tables.version + tocardinal2(count), + tocardinal2(searchrange), + tocardinal2(entryselector), + tocardinal2(rangeshift), + } + -- + local ni = #index + local nl = #list + for i=1,#order do + local tag = order[i] + local data = fontfile[tag] + if data then + local csum = checksum(data) + local dlength = #data + local length = idiv(dlength+3,4) * 4 + local padding = length - dlength + nl = nl + 1 ; list[nl] = data + for i=1,padding do + nl = nl + 1 ; list[nl] = "\0" + end + if #tag == 3 then + tag = tag .. " " + end + ni = ni + 1 ; index[ni] = tag -- must be 4 chars + ni = ni + 1 ; index[ni] = tocardinal4(csum) + ni = ni + 1 ; index[ni] = tocardinal4(offset) + ni = ni + 1 ; index[ni] = tocardinal4(dlength) + used[i] = offset -- not used + if tag == "head" then + headof = offset + end + offset = offset + length + end + end + list[1] = concat(index) + local off = #list[1] + headof + 1 + 8 + list = concat(list) + local csum = (0xB1B0AFBA - checksum(list)) % 0x100000000 + list = sub(list,1,off-1) .. tocardinal4(csum) .. sub(list,off+4,#list) + return list + end + + local function register(fontfile,name) + local u = fontfile.used + local o = fontfile.order + if not u[name] then + o[#o+1] = name + u[name] = true + end + end + + local function create(fontfile,name) + local t = { } + fontfile[name] = t + return t + end + + local function write(fontfile,name) + local t = fontfile[name] + if not t then + return + end + register(fontfile,name) + if type(t) == "table" then + if t[0] then + fontfile[name] = concat(t,"",0,#t) + elseif #t > 0 then + fontfile[name] = concat(t) + else + fontfile[name] = false + end + end + end + + tablewriters.head = function(fontfile) + register(fontfile,"head") + local t = fontfile.streams.fontheader + fontfile.head = concat { + tocardinal4(t.version), + tocardinal4(t.fontversionnumber), + tocardinal4(t.checksum), + tocardinal4(t.magic), + tocardinal2(t.flags), + tocardinal2(t.units), + tocardinal8(t.created), + tocardinal8(t.modified), + tocardinal2(t.xmin), + tocardinal2(t.ymin), + tocardinal2(t.xmax), + tocardinal2(t.ymax), + tocardinal2(t.macstyle), + tocardinal2(t.smallpixels), + tocardinal2(t.directionhint), + tocardinal2(t.indextolocformat), + tocardinal2(t.glyphformat), + } + end + + tablewriters.hhea = function(fontfile) + register(fontfile,"hhea") + local t = fontfile.streams.horizontalheader + local n = t and fontfile.nofglyphs or 0 + fontfile.hhea = concat { + tocardinal4(t.version), + tocardinal2(t.ascender), + tocardinal2(t.descender), + tocardinal2(t.linegap), + tocardinal2(t.maxadvancewidth), + tocardinal2(t.minleftsidebearing), + tocardinal2(t.minrightsidebearing), + tocardinal2(t.maxextent), + tocardinal2(t.caretsloperise), + tocardinal2(t.caretsloperun), + tocardinal2(t.caretoffset), + tocardinal2(t.reserved_1), + tocardinal2(t.reserved_2), + tocardinal2(t.reserved_3), + tocardinal2(t.reserved_4), + tocardinal2(t.metricdataformat), + tocardinal2(n) -- t.nofmetrics + } + end + + tablewriters.vhea = function(fontfile) + local t = fontfile.streams.verticalheader + local n = t and fontfile.nofglyphs or 0 + register(fontfile,"vhea") + fontfile.vhea = concat { + tocardinal4(t.version), + tocardinal2(t.ascender), + tocardinal2(t.descender), + tocardinal2(t.linegap), + tocardinal2(t.maxadvanceheight), + tocardinal2(t.mintopsidebearing), + tocardinal2(t.minbottomsidebearing), + tocardinal2(t.maxextent), + tocardinal2(t.caretsloperise), + tocardinal2(t.caretsloperun), + tocardinal2(t.caretoffset), + tocardinal2(t.reserved_1), + tocardinal2(t.reserved_2), + tocardinal2(t.reserved_3), + tocardinal2(t.reserved_4), + tocardinal2(t.metricdataformat), + tocardinal2(n) -- t.nofmetrics + } + end + + tablewriters.maxp = function(fontfile) + register(fontfile,"maxp") + local t = fontfile.streams.maximumprofile + local n = fontfile.nofglyphs + -- if fontfile.streams.cffinfo then + -- error + -- end + fontfile.maxp = concat { + tocardinal4(0x00010000), + tocardinal2(n), + tocardinal2(t.points), + tocardinal2(t.contours), + tocardinal2(t.compositepoints), + tocardinal2(t.compositecontours), + tocardinal2(t.zones), + tocardinal2(t.twilightpoints), + tocardinal2(t.storage), + tocardinal2(t.functiondefs), + tocardinal2(t.instructiondefs), + tocardinal2(t.stackelements), + tocardinal2(t.sizeofinstructions), + tocardinal2(t.componentelements), + tocardinal2(t.componentdepth), + } + end + + tablecreators.loca = function(fontfile) return create(fontfile,"loca") end + tablewriters .loca = function(fontfile) return write (fontfile,"loca") end + + tablecreators.glyf = function(fontfile) return create(fontfile,"glyf") end + tablewriters .glyf = function(fontfile) return write (fontfile,"glyf") end + + tablecreators.hmtx = function(fontfile) return create(fontfile,"hmtx") end + tablewriters .hmtx = function(fontfile) return write (fontfile,"hmtx") end + + tablecreators.vmtx = function(fontfile) return create(fontfile,"vmtx") end + tablewriters .vmtx = function(fontfile) return write (fontfile,"vmtx") end + + tableloaders .cmap = function(fontfile) return read (fontfile,"cmap") end + tablewriters .cmap = function(fontfile) return write (fontfile,"cmap") end + + tableloaders .name = function(fontfile) return read (fontfile,"name") end + tablewriters .name = function(fontfile) return write (fontfile,"name") end + + tableloaders .post = function(fontfile) return read (fontfile,"post") end + tablewriters .post = function(fontfile) return write (fontfile,"post") end + + end + + mainwriters["truetype"] = function(details) + -- + local fontfile = openfontfile(details) + local basefontname = details.basefontname + local streams = details.streams + local blobs = streams.streams + local fontheader = streams.fontheader + local horizontalheader = streams.horizontalheader + local verticalheader = streams.verticalheader + local maximumprofile = streams.maximumprofile + local names = streams.names + local descriptions = details.rawdata.descriptions + local metadata = details.rawdata.metadata + local indices = details.indices + local metabbox = { fontheader.xmin, fontheader.ymin, fontheader.xmax, fontheader.ymax } + local indices, + minindex, + maxindex = collectindices(descriptions,indices) + local glyphstreams = tablecreators.glyf(fontfile) + local locations = tablecreators.loca(fontfile) + local horizontals = tablecreators.hmtx(fontfile) + local verticals = tablecreators.vmtx(fontfile) + -- + local zero2 = tocardinal2(0) + local zero4 = tocardinal4(0) + -- + local horizontal = horizontalheader.nofmetrics > 0 + local vertical = verticalheader.nofmetrics > 0 + -- + local streamoffset = 0 + local lastoffset = zero4 + local g, h, v = 0, 0, 0 + -- + -- todo: locate notdef + -- + if minindex > 0 then + local blob = blobs[0] + if blob and #blob > 0 then + locations[0] = lastoffset + g = g + 1 ; glyphstreams[g] = blob + h = h + 1 ; horizontals [h] = zero4 + if vertical then + v = v + 1 ; verticals[v] = zero4 + end + streamoffset = streamoffset + #blob + lastoffset = tocardinal4(streamoffset) + else + report_fonts("missing .notdef in font %a",basefontname) + end + -- todo: use a rep for h/v + for index=1,minindex-1 do + locations[index] = lastoffset + h = h + 1 ; horizontals[h] = zero4 + if vertical then + v = v + 1 ; verticals[v] = zero4 + end + end + end + for index=minindex,maxindex do + locations[index] = lastoffset + local data = indices[index] + if data then + local blob = blobs[index] -- we assume padding + if blob and #blob > 0 then + g = g + 1 ; glyphstreams[g] = blob + h = h + 1 ; horizontals [h] = tocardinal2(data.width or 0) + h = h + 1 ; horizontals [h] = tocardinal2(data.boundingbox[1]) + if vertical then + v = v + 1 ; verticals[v] = tocardinal2(data.height or 0) + v = v + 1 ; verticals[v] = tocardinal2(data.boundingbox[3]) + end + streamoffset = streamoffset + #blob + lastoffset = tocardinal4(streamoffset) + else + h = h + 1 ; horizontals[h] = zero4 + if vertical then + v = v + 1 ; verticals[v] = zero4 + end + report_fonts("missing blob for index %i in font %a",index,basefontname) + end + else + h = h + 1 ; horizontals[h] = zero4 + if vertical then + v = v + 1 ; verticals[v] = zero4 + end + end + end + locations[maxindex+1] = lastoffset -- cf spec + -- + local nofglyphs = maxindex + 1 -- include zero + -- + fontheader.checksum = 0 + fontheader.indextolocformat = 1 + maximumprofile.nofglyphs = nofglyphs + -- + fontfile.format = "tff" + fontfile.basefontname = basefontname + fontfile.nofglyphs = nofglyphs + -- + tablewriters.head(fontfile) + tablewriters.hhea(fontfile) + if vertical then + tablewriters.vhea(fontfile) + end + tablewriters.maxp(fontfile) + + tablewriters.loca(fontfile) + tablewriters.glyf(fontfile) + + tablewriters.hmtx(fontfile) + if vertical then + tablewriters.vmtx(fontfile) + end + -- + local fontdata = makefontfile(fontfile) + local fontmeta = makemetadata(fontfile) + -- + fontfile = closefontfile(fontfile) + -- + local units = metadata.units + local basefont = pdfconstant(basefontname) + local widths = widtharray(details,indices,maxindex,units) + local object = details.objectnumber + local tounicode = tounicodedictionary(details,indices,maxindex,basefontname,true) + local tocidset = tocidsetdictionary(indices,minindex,maxindex) + local metabbox = metadata.boundingbox or { 0, 0, 0, 0 } + local fontbbox = pdfarray { unpack(metabbox) } + local ascender = metadata.ascender + local descender = metadata.descender + local capheight = metadata.capheight or fontbbox[4] + local stemv = metadata.weightclass + local italicangle = metadata.italicangle + local xheight = metadata.xheight or fontbbox[4] + -- + if stemv then + stemv = (stemv/65)^2 + 50 + end + -- + local function scale(n) + if n then + return round((n) * 10000 / units) / 10 + else + return 0 + end + end + -- + local reserved = pdfreserveobject() + local child = pdfdictionary { + Type = pdfconstant("Font"), + Subtype = pdfconstant("CIDFontType2"), + BaseFont = basefont, + FontDescriptor = pdfreference(reserved), + W = pdfreference(pdfflushobject(widths)), + CIDToGIDMap = pdfconstant("Identity"), + CIDSystemInfo = pdfdictionary { + Registry = pdfstring("Adobe"), + Ordering = pdfstring("Identity"), + Supplement = 0, + } + } + local descendants = pdfarray { + pdfreference(pdfflushobject(child)), + } + local descriptor = pdfdictionary { + Type = pdfconstant("FontDescriptor"), + FontName = basefont, + Flags = 4, + FontBBox = fontbbox, + Ascent = scale(ascender), + Descent = scale(descender), + ItalicAngle = round(italicangle or 0), + CapHeight = scale(capheight), + StemV = scale(stemv), + XHeight = scale(xheight), + FontFile2 = pdfreference(pdfflushstreamobject(fontdata)), + CIDSet = tocidset, + Metadata = fontmeta and pdfreference(pdfflushstreamobject(fontmeta)) or nil, + } + local parent = pdfdictionary { + Type = pdfconstant("Font"), + Subtype = pdfconstant("Type0"), + Encoding = pdfconstant(details.properties.writingmode == "vertical" and "Identity-V" or "Identity-H"), + BaseFont = basefont, + DescendantFonts = descendants, + ToUnicode = pdfreference(pdfflushstreamobject(tounicode)), + } + pdfflushobject(reserved,descriptor) + pdfflushobject(object,parent) + -- + -- if trace_details then + -- local name = "temp.ttf" + -- report_fonts("saving %a",name) + -- io.savedata(name,fontdata) + -- inspect(fonts.handlers.otf.readers.loadfont(name)) + -- end + -- + end + + do + -- todo : cff2 + + local details = { + details = true, + platformnames = true, + platformextras = true, + } + + tablecreators.cff = function(fontfile) + fontfile.charstrings = { } + fontfile.charmappings = { } + fontfile.cffstrings = { } + fontfile.cffhash = { } + return fontfile.charstrings , fontfile.charmappings + end + + local todictnumber, todictreal, todictinteger, todictoffset do + + local maxnum = 0x7FFFFFFF + local minnum = - 0x7FFFFFFF - 1 + local epsilon = 1.0e-5 + + local int2tag = "\28" + local int4tag = "\29" + local realtag = "\30" + + todictinteger = function(n) + if not n then + return char(band(139,0xFF)) + elseif n >= -107 and n <= 107 then + return char(band(n + 139,0xFF)) + elseif n >= 108 and n <= 1131 then + n = 0xF700 + n - 108 + return char(band(rshift(n,8),0xFF),band(n,0xFF)) + elseif n >= -1131 and n <= -108 then + n = 0xFB00 - n - 108 + return char(band(rshift(n,8),0xFF),band(n,0xFF)) + elseif n >= -32768 and n <= 32767 then + -- return int2tag .. tointeger2(n) + return char(28,extract(n,8,8),extract(n,0,8)) + else + -- return int4tag .. tointeger4(n) + return char(29,extract(n,24,8),extract(n,16,8),extract(n,8,8),extract(n,0,8)) + end + end + + -- -- not called that often + -- + -- local encoder = readers.cffencoder + -- + -- todictinteger = function(n) + -- if not n then + -- return encoder[0] + -- elseif n >= -1131 and n <= 1131 then + -- return encoder[n] + -- elseif n >= -32768 and n <= 32767 then + -- -- return int2tag .. tointeger2(n) + -- return char(28,extract(n,8,8),extract(n,0,8)) + -- else + -- -- return int4tag .. tointeger4(n) + -- return char(29,extract(n,24,8),extract(n,16,8),extract(n,8,8),extract(n,0,8)) + -- end + -- end + + todictoffset = function(n) + return int4tag .. tointeger4(n) + end + + local e = false + local z = byte("0") + local dp = 10 + local ep = 11 + local em = 12 + local mn = 14 + local es = 15 + + local fg = formatters["%g"] + + todictreal = function(v) + local s = fg(v) + local t = { [0] = realtag } + local n = 0 + for s in gmatch(s,".") do + if s == "e" or s == "E" then + e = true + elseif s == "+" then + -- skip + elseif s == "-" then + n = n + 1 + if e then + t[n] = em + e = false + else + t[n] = mn + end + else + if e then + n = n + 1 + t[n] = ep + e = false + end + n = n + 1 + if s == "." then + t[n] = dp + else + t[n] = byte(s) - z + end + end + end + n = n + 1 + t[n] = es + if (n % 2) ~= 0 then + n = n + 1 + t[n] = es + end + local j = 0 + for i=1,n,2 do + j = j + 1 + t[j] = char(t[i]*0x10+t[i+1]) + end + t = concat(t,"",0,j) + return t + end + + todictnumber = function(n) + if not n or n == 0 then + return todictinteger(0) + elseif (n > maxnum or n < minnum or (abs(n - round(n)) > epsilon)) then + return todictreal(n) + else + return todictinteger(n) + end + end + + end + + local todictkey = char + + local function todictstring(fontfile,value) + if not value then + value = "" + end + local s = fontfile.cffstrings + local h = fontfile.cffhash + local n = h[value] + if not n then + n = #s + 1 + s[n] = value + h[value] = n + end + return todictinteger(390+n) + end + + local function todictboolean(b) + return todictinteger(b and 1 or 0) + end + + local function todictdeltas(t) + local r = { } + for i=1,#t do + r[i] = todictnumber(t[i]-(t[i-1] or 0)) + end + return concat(r) + end + + local function todictarray(t) + local r = { } + for i=1,#t do + r[i] = todictnumber(t[i]) + end + return concat(r) + end + + local function writestring(target,source,offset,what) + target[#target+1] = source + -- report_fonts("string : %-11s %06i # %05i",what,offset,#source) + return offset + #source + end + + local function writetable(target,source,offset,what) + source = concat(source) + target[#target+1] = source + -- report_fonts("table : %-11s %06i # %05i",what,offset,#source) + return offset + #source + end + + local function writeindex(target,source,offset,what) + local n = #source + local t = #target + t = t + 1 ; target[t] = tocardinal2(n) + if n > 0 then + local data = concat(source) + local size = #data -- assume the worst + local offsetsize, tocardinal + if size < 0xFF then + offsetsize, tocardinal = 1, tocardinal1 + elseif size < 0xFFFF then + offsetsize, tocardinal = 2, tocardinal2 + elseif size < 0xFFFFFF then + offsetsize, tocardinal = 3, tocardinal3 + elseif size < 0xFFFFFFFF then + offsetsize, tocardinal = 4, tocardinal4 + end + -- report_fonts("index : %-11s %06i # %05i (%i entries with offset size %i)",what,offset,#data,n,offsetsize) + offset = offset + 2 + 1 + (n + 1) * offsetsize + size + -- bytes per offset + t = t + 1 ; target[t] = tocardinal1(offsetsize) + -- list of offsets (one larger for length calculation) + local offset = 1 -- mandate + t = t + 1 ; target[t] = tocardinal(offset) + for i=1,n do + offset = offset + #source[i] + t = t + 1 ; target[t] = tocardinal(offset) + end + t = t + 1 ; target[t] = data + else + -- report_fonts("index : %-11s %06i # %05i (no entries)",what,offset,0) + offset = offset + 2 + end + -- print("offset",offset,#concat(target)) + return offset + end + + tablewriters.cff = function(fontfile) + -- + local streams = fontfile.streams + local cffinfo = streams.cffinfo or { } + local names = streams.names or { } + local fontheader = streams.fontheader or { } + local basefontname = fontfile.basefontname + -- + local offset = 0 + local dictof = 0 + local target = { } + -- + local charstrings = fontfile.charstrings + local nofglyphs = #charstrings + 1 + local fontmatrix = { 0.001, 0, 0, 0.001, 0, 0 } -- todo + local fontbbox = fontfile.fontbbox + local defaultwidth = cffinfo.defaultwidth or 0 + local nominalwidth = cffinfo.nominalwidth or 0 + local bluevalues = cffinfo.bluevalues + local otherblues = cffinfo.otherblues + local familyblues = cffinfo.familyblues + local familyotherblues = cffinfo.familyotherblues + local bluescale = cffinfo.bluescale + local blueshift = cffinfo.blueshift + local bluefuzz = cffinfo.bluefuzz + local stdhw = cffinfo.stdhw + local stdvw = cffinfo.stdvw + -- + if defaultwidth == 0 then defaultwidth = nil end + if nomimalwidth == 0 then nominalwidth = nil end + if bluevalues then bluevalues = todictarray(bluevalues) end + if otherblues then otherblues = todictarray(otherblues) end + if familyblues then familyblues = todictarray(familyblues) end + if familyotherblues then familyotherblues = todictarray(familyotherblues) end + if bluescale then bluescale = todictnumber(bluescale) end + if blueshift then blueshift = todictnumber(blueshift) end + if bluefuzz then bluefuzz = todictnumber(bluefuzz) end + if stdhw then stdhw = todictdeltas(stdhw) end + if stdvw then stdvw = todictdeltas(stdvw) end + -- + local fontversion = todictstring(fontfile,fontheader.fontversion or "uknown version") + local familyname = todictstring(fontfile,cffinfo.familyname or names.family or basefontname) + local fullname = todictstring(fontfile,cffinfo.fullname or basefontname) + local weight = todictstring(fontfile,cffinfo.weight or "Normal") + local fontbbox = todictarray(fontbbox) + local strokewidth = todictnumber(cffinfo.strokewidth) + local monospaced = todictboolean(cffinfo.monospaced) + local italicangle = todictnumber(cffinfo.italicangle) + local underlineposition = todictnumber(cffinfo.underlineposition) + local underlinethickness = todictnumber(cffinfo.underlinethickness) + local charstringtype = todictnumber(2) + local fontmatrix = todictarray(fontmatrix) + local ros = todictstring(fontfile,"Adobe") -- registry + .. todictstring(fontfile,"Identity") -- identity + .. todictnumber(0) -- supplement + local cidcount = todictnumber(fontfile.nofglyphs) + local fontname = todictstring(fontfile,basefontname) + local fdarrayoffset = todictoffset(0) + local fdselectoffset = todictoffset(0) + local charstringoffset = todictoffset(0) + local charsetoffset = todictoffset(0) + local privateoffset = todictoffset(0) + -- + local defaultwidthx = todictnumber(defaultwidth) + local nominalwidthx = todictnumber(nominalwidth) + local private = "" + .. (defaultwidthx and (defaultwidthx .. todictkey(20)) or "") + .. (nominalwidthx and (nominalwidthx .. todictkey(21)) or "") + .. (bluevalues and (bluevalues .. todictkey(6)) or "") + .. (otherblues and (otherblues .. todictkey(7)) or "") + .. (familyblues and (familyblues .. todictkey(8)) or "") + .. (familyotherblues and (familyotherblues .. todictkey(9)) or "") + .. (bluescale and (bluescale .. todictkey(12,9)) or "") + .. (blueshift and (blueshift .. todictkey(12,10)) or "") + .. (bluefuzz and (bluefuzz .. todictkey(12,11)) or "") + .. (stdhw and (stdhw .. todictkey(12,12)) or "") + .. (stdvw and (stdvw .. todictkey(12,13)) or "") + local privatesize = todictnumber(#private) + local privatespec = privatesize .. privateoffset + -- + -- header (fixed @ 1) + -- + local header = + tocardinal1(1) -- major + .. tocardinal1(0) -- minor + .. tocardinal1(4) -- header size + .. tocardinal1(4) -- offset size + -- + offset = writestring(target,header,offset,"header") + -- + -- name index (fixed @ 2) (has to be sorted) + -- + local names = { + basefontname, + } + -- + offset = writeindex(target,names,offset,"names") + -- + -- topdict index (fixed @ 3) + -- + local topvars = + charstringoffset .. todictkey(17) + .. charsetoffset .. todictkey(15) + .. fdarrayoffset .. todictkey(12,36) + .. fdselectoffset .. todictkey(12,37) + .. privatespec .. todictkey(18) + -- + local topdict = { + ros .. todictkey(12,30) -- first + .. cidcount .. todictkey(12,34) + .. familyname .. todictkey( 3) + .. fullname .. todictkey( 2) + .. weight .. todictkey( 4) + .. fontbbox .. todictkey( 5) + .. monospaced .. todictkey(12, 1) + .. italicangle .. todictkey(12, 2) + .. underlineposition .. todictkey(12, 3) + .. underlinethickness .. todictkey(12, 4) + .. charstringtype .. todictkey(12, 6) + .. fontmatrix .. todictkey(12, 7) + .. strokewidth .. todictkey(12, 8) + .. topvars + } + -- + offset = writeindex(target,topdict,offset,"topdict") + dictof = #target + -- + -- string index (fixed @ 4) + -- + offset = writeindex(target,fontfile.cffstrings,offset,"strings") + -- + -- global subroutine index (fixed @ 5) + -- + offset = writeindex(target,{},offset,"globals") + -- + -- Encoding (cff1) + -- + -- offset = writeindex(target,{},offset,"encoding") + -- + -- Charsets + -- + charsetoffset = todictoffset(offset) + offset = writetable(target,fontfile.charmappings,offset,"charsets") + -- + -- fdselect + -- + local fdselect = + tocardinal1(3) -- format + .. tocardinal2(1) -- n of ranges + -- entry 1 + .. tocardinal2(0) -- first gid + .. tocardinal1(0) -- fd index + -- entry 2 +-- .. tocardinal2(fontfile.sparsemax-1) -- sentinel + .. tocardinal2(fontfile.sparsemax) -- sentinel + -- + fdselectoffset = todictoffset(offset) + offset = writestring(target,fdselect,offset,"fdselect") + -- + -- charstrings + -- + charstringoffset = todictoffset(offset) + offset = writeindex(target,charstrings,offset,"charstrings") + -- + -- font dict + -- + -- offset = writeindex(target,{},offset,"fontdict") + -- + -- private + -- + privateoffset = todictoffset(offset) + privatespec = privatesize .. privateoffset + offset = writestring(target,private,offset,"private") + -- + local fdarray = { + fontname .. todictkey(12,38) + .. privatespec .. todictkey(18) + } + fdarrayoffset = todictoffset(offset) + offset = writeindex(target,fdarray,offset,"fdarray") + -- + topdict = target[dictof] + topdict = sub(topdict,1,#topdict-#topvars) + topvars = + charstringoffset .. todictkey(17) + .. charsetoffset .. todictkey(15) + .. fdarrayoffset .. todictkey(12,36) + .. fdselectoffset .. todictkey(12,37) + .. privatespec .. todictkey(18) + target[dictof] = topdict .. topvars + -- + target = concat(target) + -- if trace_details then + -- local name = "temp.cff" + -- report_fonts("saving %a",name) + -- io.savedata(name,target) + -- inspect(fonts.handlers.otf.readers.cffcheck(name)) + -- end + return target + end + + end + + -- todo: check widths (missing a decimal) + + mainwriters["opentype"] = function(details) + -- + local fontfile = openfontfile(details) + local basefontname = details.basefontname + local streams = details.streams + local blobs = streams.streams + local fontheader = streams.fontheader + local maximumprofile = streams.maximumprofile + local names = streams.names + local descriptions = details.rawdata.descriptions + local metadata = details.rawdata.metadata + local indices = details.indices + local metabbox = { fontheader.xmin, fontheader.ymin, fontheader.xmax, fontheader.ymax } + local correction = 1 + + -- (*) We share code with type1 and when we have old school tfm with + -- pfb but without descriptions we're kind of toast. + + if not descriptions or not next(descriptions) then + -- This is good enough, we only need indices and widths. + descriptions = details.fontdata.characters + -- This is a hack, we have no basepoints. + correction = details.fontdata.parameters.size / 1000 + -- And this needs checking. + correction = correction * bpfactor / ptfactor + metadata = { } + end + + -- + + local indices, + minindex, + maxindex = collectindices(descriptions,indices) + local streamoffset = 0 + local glyphstreams, + charmappings = tablecreators.cff(fontfile) + -- + local zero2 = tocardinal2(0) + local zero4 = tocardinal4(0) + -- + -- we need to locate notdef (or store its unicode someplace) + -- + local blob = blobs[0] or "\14" + local sparsemax = 1 + local lastoffset = zero4 + glyphstreams[sparsemax] = blob + charmappings[sparsemax] = tocardinal1(0) -- format 0 + streamoffset = streamoffset + #blob + lastoffset = tocardinal4(streamoffset) + if minindex == 0 then + minindex = 1 + end + for index=minindex,maxindex do + if indices[index] then + local blob = blobs[index] or "\14" + sparsemax = sparsemax + 1 + glyphstreams[sparsemax] = blob + charmappings[sparsemax] = tocardinal2(index) + streamoffset = streamoffset + #blob + lastoffset = tocardinal4(streamoffset) + end + end + -- + fontfile.nofglyphs = maxindex + 1 + fontfile.sparsemax = sparsemax + fontfile.format = "cff" + fontfile.basefontname = basefontname + fontfile.fontbbox = metabbox + -- + local fontdata = tablewriters.cff(fontfile) + local fontmeta = makemetadata(fontfile) + -- + fontfile = closefontfile(fontfile) + -- + local units = fontheader.units or metadata.units + + local basefont = pdfconstant(basefontname) + local widths = widtharray(details,indices,maxindex,units * correction) + local object = details.objectnumber + local tounicode = tounicodedictionary(details,indices,maxindex,basefontname,true) + local tocidset = tocidsetdictionary(indices,minindex,maxindex) + local fontbbox = pdfarray { unpack(metabbox) } + local ascender = metadata.ascender or 0 + local descender = metadata.descender or 0 + local capheight = metadata.capheight or fontbbox[4] + local stemv = metadata.weightclass + local italicangle = metadata.italicangle + local xheight = metadata.xheight or fontbbox[4] + if stemv then + stemv = (stemv/65)^2 + 50 + else +-- stemv = 2 + end + -- + local function scale(n) + if n then + return round((n) * 10000 / units) / 10 + else + return 0 + end + end + -- + local reserved = pdfreserveobject() + local child = pdfdictionary { + Type = pdfconstant("Font"), + Subtype = pdfconstant("CIDFontType0"), + BaseFont = basefont, + FontDescriptor = pdfreference(reserved), + W = pdfreference(pdfflushobject(widths)), + CIDSystemInfo = pdfdictionary { + Registry = pdfstring("Adobe"), + Ordering = pdfstring("Identity"), + Supplement = 0, + } + } + local descendants = pdfarray { + pdfreference(pdfflushobject(child)), + } + local fontstream = pdfdictionary { + Subtype = pdfconstant("CIDFontType0C"), + } + local descriptor = pdfdictionary { + Type = pdfconstant("FontDescriptor"), + FontName = basefont, + Flags = 4, + FontBBox = fontbbox, + Ascent = scale(ascender), + Descent = scale(descender), + ItalicAngle = round(italicangle or 0), + CapHeight = scale(capheight), + StemV = scale(stemv), + XHeight = scale(xheight), + CIDSet = tocidset, + FontFile3 = pdfreference(pdfflushstreamobject(fontdata,fontstream())), + Metadata = fontmeta and pdfreference(pdfflushstreamobject(fontmeta)) or nil, + } + local parent = pdfdictionary { + Type = pdfconstant("Font"), + Subtype = pdfconstant("Type0"), + Encoding = pdfconstant(details.properties.writingmode == "vertical" and "Identity-V" or "Identity-H"), + BaseFont = basefont, + DescendantFonts = descendants, + ToUnicode = pdfreference(pdfflushstreamobject(tounicode)), + } + pdfflushobject(reserved,descriptor) + pdfflushobject(object,parent) + end + + mainwriters["type1"] = function(details) + -- We abuse the cff includer which is ok but for special cases like + -- tfm -> pfb we don't have the right descriptions and scale so this + -- is why we cheat elsewhere. Maybe I should just drop that kind of + -- support and assume afm files to be present. See (*) above. + local s = details.streams + local m = details.rawdata.metadata + if m then + local h = s.horizontalheader + local c = s.cffinfo + local n = s.names + h.ascender = m.ascender or h.ascender + h.descender = m.descender or h.descender + n.copyright = m.copyright or n.copyright + n.family = m.familyname or n.familyname + n.fullname = m.fullname or n.fullname + n.fontname = m.fontname or n.fontname + n.subfamily = m.subfamilyname or n.subfamilyname + n.version = m.version or n.version + setmetatableindex(h,m) + setmetatableindex(c,m) + setmetatableindex(n,m) + end + mainwriters["opentype"](details) + end + + do + + -- The methods might become plugins. + + local methods = { } + + local pdfimage = lpdf.epdf.image + local openpdf = pdfimage.open + local closepdf = pdfimage.close + local copypage = pdfimage.copy + + local embedimage = images.embed + + local f_glyph = formatters["G%d"] + local f_char = formatters["BT /V%d 1 Tf [<%04X>] TJ ET"] + local f_width = formatters["%.6N 0 d0"] + local f_index = formatters["I%d"] + local f_image_xy = formatters["%.6N 0 d0 1 0 0 1 %.6N %.6N cm /%s Do"] + local f_image_c = formatters["/%s Do"] + local f_image_c_xy = formatters["%.6N 0 0 %.6N %.6N %.6N cm /%s Do"] + local f_image_w = formatters["%.6N 0 d0 %s"] + local f_image_d = formatters["%.6N 0 d0 1 0 0 1 0 %.6N cm /%s Do"] + local f_stream = formatters["%.6N 0 d0 %s"] + local f_stream_c = formatters["%.6N 0 0 0 0 0 d1 %s"] + local f_stream_d = formatters["%.6N 0 d0 1 0 0 1 0 %.6N cm %s"] + + -- A type 3 font has at most 256 characters and Acrobat also wants a zero slot + -- to be filled. We can share a mandate zero slot character. We also need to + -- make sure that we use bytes as index in the page stream as well as in the + -- tounicode vector. + + local c_notdef = nil + local r_notdef = nil + local w_notdef = nil + local fontbbox = nil + + -- pk inclusion (not really tested but not really used either) + + function methods.pk(filename) + local pkfullname = resolvers.findpk(basedfontname,resolution) + if not pkfullname or pkfullname == "" then + return + end + local readers = fonts.handlers.tfm.readers + local result = readers.loadpk(pkfullname) + local convert = readers.pktopdf + if not result or result.error then + return + end + local resolution = 600 + local widthfactor = resolution / 72 + local scalefactor = 72 / resolution / 10 + local factor = widthfactor / 65536 + local function pktopdf(glyph,data) + return convert(glyph,data,factor) -- return pdfcode, width + end + return result.glyphs, scalefactor, pktopdf, false, false + end + + -- pdf inclusion + + local used = setmetatableindex("table") + + function methods.pdf(filename,details) + local properties = details.properties + local pdfshapes = properties.indexdata[1] + local pdfdoc = openpdf(pdfshapes.filename) + local xforms = pdfdictionary() + local nofglyphs = 0 + if pdfdoc then + local scale = 10 * details.parameters.size/details.parameters.designsize + local units = details.parameters.units + local factor = units * bpfactor / scale + local fixdepth = pdfshapes.fixdepth + local useddoc = used[pdfdoc] + local function pdftopdf(glyph,data) + local width = (data.width or 0) * factor + local image = useddoc[glyph] + local reference = nil + if not image then + image = embedimage(copypage(pdfdoc,glyph)) + nofglyphs = nofglyphs + 1 + local name = f_glyph(nofglyphs) + local stream = nil + if fixdepth then + local depth = data.depth or 0 + local height = data.height or 0 + if depth ~= 0 or height ~= 0 then + local d = data.dropin.descriptions[data.index] + local b = d.boundingbox + local l = b[1] + local r = b[3] + local w = r - l + local scale = w / d.width + local x = l + local y = - b[4] - b[2] - d.depth + local scale = w / (image.width * bpfactor) + stream = f_image_c_xy(scale,scale,x,y,name) + else + stream = f_image_c(name) + end + else + stream = f_image_c(name) + end + useddoc[glyph] = image + image.embedded_name = name + image.embedded_stream = stream + image.embedded_reference = pdfreference(image.objnum) + end + xforms[image.embedded_name] = image.embedded_reference + return f_image_w(width,image.embedded_stream), width + end + local function closepdf() + -- closepdf(pdfdoc) + end + local function getresources() + return pdfdictionary { XObject = xforms } + end + return pdfshapes, 1/units, pdftopdf, closepdf, getresources + end + end + + -- mps inclusion + + local decompress = gzip.decompress + local metapost = metapost + local simplemprun = metapost.simple + local setparameterset = metapost.setparameterset + + function methods.mps(filename,details) + local properties = details.properties + local parameters = details.parameters + local mpshapes = properties.indexdata[1] -- indexdata will change + if mpshapes then + local scale = 10 * parameters.size/parameters.designsize + local units = mpshapes.units or parameters.units + local factor = units * bpfactor / scale + local fixdepth = mpshapes.fixdepth + local usecolor = mpshapes.usecolor + local specification = mpshapes.specification + local shapedefinitions = mpshapes.shapes + local instance = mpshapes.instance + -- + simplemprun(instance,"begingroup;",true,true) + setparameterset("mpsfont",specification) + if shapedefinitions then + local preamble = shapedefinitions.parameters.preamble + if preamble then + simplemprun(instance,preamble,true,true) + end + end + -- + local function mpstopdf(mp,data) + local width = data.width + if decompress then + mp = decompress(mp) + end + local pdf = simplemprun(instance,mp,true) -- can be sped up, minifun + local width = width * factor + if usecolor then + return f_stream_c(width,pdf), width + elseif fixdepth then + local depth = data.depth or 0 + local height = data.height or 0 + if depth ~= 0 or height ~= 0 then + return f_stream_d(width,(-height-depth)*factor,pdf), width + end + end + return f_stream(width,pdf), width + end + -- + local function resetmps() + setparameterset("mpsfont") + simplemprun(instance,"endgroup;",true,true) + end + -- + local function getresources() + return lpdf.collectedresources { + serialize = false, + } + end + -- + return mpshapes, 1/units, mpstopdf, resetmps, getresources + end + end + + -- png inclusion + + -- With d1 the image mask is used when given and obeys color. So it is needed for pure bw + -- bitmap fonts, so here we really need d0. + -- + -- Acrobat X pro only seems to see the image mask but other viewers are doing it ok. Acrobat + -- reader crashes. We really need to add a notdef! + + local files = utilities.files + local openfile = files.open + local closefile = files.close + local setposition = files.setposition + local readstring = files.readstring + + function methods.png(filename,details) + local properties = details.properties + local pngshapes = properties.indexdata[1] + if pngshapes then + local parameters = details.parameters + local png = properties.png + local hash = png.hash + local xforms = pdfdictionary() + local nofglyphs = 0 + local scale = 10 * parameters.size/parameters.designsize + local factor = bpfactor / scale + -- local units = parameters.units -- / 1000 + local units = 1000 + local filehandle = openfile(details.filename,true) + local function pngtopdf(glyph,data) + -- local info = graphics.identifiers.png(glyph.data,"string") + local offset = glyph.o + local size = glyph.s + local pdfdata = nil + if offset and size then + setposition(filehandle,offset) + local blob = readstring(filehandle,size) + local info = graphics.identifiers.png(blob,"string") + info.enforcecmyk = pngshapes.enforcecmyk + local image = lpdf.injectors.png(info,"string") + local width = (data.width or 0) * factor + if image then + embedimage(image) + nofglyphs = nofglyphs + 1 + local xoffset = (glyph.x or 0) / units + local yoffset = (glyph.y or 0) / units + local name = f_glyph(nofglyphs) + xforms[name] = pdfreference(image.objnum) + pdfdata = f_image_xy(width,xoffset,yoffset,name) + end + end + return pdfdata or f_stream(width), width + end + local function closepng() + if filehandle then + closefile(filehandle) + end + pngshapes = nil + end + local function getresources() + return pdfdictionary { XObject = xforms } + end + return pngshapes, 1, pngtopdf, closepng, getresources + end + end + + function methods.color(filename,details) + local colrshapes = details.properties.indexdata[1] + local colrvalues = details.properties.indexdata[2] + local usedfonts = { } + local function colrtopdf(description,data) + -- descriptions by index + local colorlist = description.colors + if colorlist then + local dropdata = data.dropin + local dropid = dropdata.properties.id + local dropunits = dropdata.parameters.units -- shared + local descriptions = dropdata.descriptions + usedfonts[dropid] = dropid + local w = description.width or 0 + local s = #colorlist + local l = nil + local t = { f_width(w) } + local n = 1 + local d = colrvalues[#colrvalues] + for i=1,s do + local entry = colorlist[i] + local v = colrvalues[entry.class] or d + if v and l ~= v then + n = n + 1 ; t[n] = v + l = v + end + local e = descriptions[entry.slot] + if e then + n = n + 1 ; t[n] = f_char(dropid,e.index) + end + end + return concat(t," "), w / dropunits + end + end + local function getresources() + return lpdf.collectedresources { + serialize = false, + fonts = usedfonts, + fontprefix = "V", + } + end + return colrshapes, 1, colrtopdf, false, getresources + end + + mainwriters["type3"] = function(details) + local properties = details.properties + local basefontname = details.basefontname or properties.name + local askedmethod = properties.method or "pk" + local method = methods[askedmethod] or methods.pk + if not method then + return + end + local glyphs, scalefactor, glyphtopdf, reset, getresources = method(basedfontname,details) + if not glyphs then + return + end + local parameters = details.parameters + local object = details.objectnumber + local factor = parameters.factor -- normally 1 + local fontmatrix = pdfarray { scalefactor, 0, 0, scalefactor, 0, 0 } + local indices, + minindex, + maxindex = collectindices(details.fontdata.characters,details.indices) + local widths = pdfarray() + local differences = pdfarray() + local charprocs = pdfdictionary() + local basefont = pdfconstant(basefontname) + local d = 0 + local w = 0 + local forcenotdef = minindex > 0 + local lastindex = -0xFF + if forcenotdef then + widths[0] = 0 + minindex = 0 + lastindex = 0 + d = 2 + if not c_notdef then + w_notdef = 0 + c_notdef = pdfconstant(".notdef") + r_notdef = pdfreference(pdfflushstreamobject("0 0 d0")) + end + differences[1] = w_notdef + differences[2] = c_notdef + charprocs[".notdef"] = r_notdef + end + + for i=1,maxindex-minindex+1 do + widths[i] = 0 + end + + for index, data in sortedhash(indices) do + local name = f_index(index) + local glyph = glyphs[index] + if glyph then + local stream, width = glyphtopdf(glyph,data) + if stream then + if index - 1 ~= lastindex then + d = d + 1 differences[d] = index + end + lastindex = index + d = d + 1 differences[d] = pdfconstant(name) + charprocs[name] = pdfreference(pdfflushstreamobject(stream)) + widths[index-minindex+1] = width + end + else + report_fonts("missing glyph %i in type3 font %a",index,basefontname) + end + end + if not fontbbox then + -- The specification permits zero values and these are actually also more + -- robust as then there are no assumptions and no accuracy is needed. + fontbbox = pdfarray { 0, 0, 0, 0 } + end + local encoding = pdfdictionary { + Type = pdfconstant("Encoding"), + Differences = differences, + } + local tounicode = tounicodedictionary(details,indices,maxindex,basefontname,false) + local resources = getresources and getresources() + if not resources or not next(resources) then + -- resources = lpdf.procset(true) + resources = nil + end + local descriptor = pdfdictionary { + -- most is optional in type3 + Type = pdfconstant("FontDescriptor"), + FontName = basefont, + Flags = 4, + ItalicAngle = 0, + } + local parent = pdfdictionary { + Type = pdfconstant("Font"), + Subtype = pdfconstant("Type3"), + Name = basefont, + FontBBox = fontbbox, + FontMatrix = fontmatrix, + CharProcs = pdfreference(pdfflushobject(charprocs)), + Encoding = pdfreference(pdfflushobject(encoding)), + FirstChar = minindex, + LastChar = maxindex, + Widths = pdfreference(pdfflushobject(widths)), + FontDescriptor = pdfreference(pdfflushobject(descriptor)), + Resources = resources, + ToUnicode = tounicode and pdfreference(pdfflushstreamobject(tounicode)), + } + pdfflushobject(object,parent) + if reset then + reset() + end + end + + end + +end + +-- writingmode + +local usedfonts = fonts.hashes.identifiers -- for now +local noffonts = 0 + +-- The main injector. + +-- here we need to test for sharing otherwise we reserve too many +-- objects + +local getstreamhash = fonts.handlers.otf.getstreamhash +local loadstreamdata = fonts.handlers.otf.loadstreamdata + +-- we can actually now number upwards (so not use fontid in /F) + +local objects = setmetatableindex(function(t,k) + local v + if type(k) == "number" then + local h = getstreamhash(k) + v = rawget(t,h) + if not v then + v = pdfreserveobject() + t[h] = v + end + if trace_fonts then + report_fonts("font id %i bound to hash %s and object %i",k,h,v) + end + else + -- no problem as it can be svg only + -- report_fonts("fatal error, hash %s asked but not used",k,h,v) + v = pdfreserveobject() + t[k] = v + end + return v +end) + +local n = 0 + +local names = setmetatableindex(function(t,k) + local v + if type(k) == "number" then + local h = getstreamhash(k) + v = rawget(t,h) + if not v then + n = n + 1 + v = n + t[h] = v + end + if trace_fonts then + report_fonts("font id %i bound to hash %s and name %i",k,h,n) + end + end + t[k] = v + return v +end) + +function lpdf.flushfonts() + + local mainfonts = { } + + statistics.starttiming(objects) + + for fontid, used in sortedhash(lpdf.usedcharacters) do + + -- for a bitmap we need a different hash unless we stick to a fixed high + -- resolution which makes much sense + + local hash = getstreamhash(fontid) + if hash then + local parent = mainfonts[hash] + if not parent then + local fontdata = usedfonts[fontid] + local rawdata = fontdata.shared and fontdata.shared.rawdata + local resources = fontdata.resources + local properties = fontdata.properties -- writingmode and type3 + local parameters = fontdata.parameters -- used in type3 + if not rawdata then + -- we have a virtual font that loaded directly ... at some point i will + -- sort this out (in readanddefine we need to do a bit more) .. the problem + -- is that we have a hybrid font then + for xfontid, xfontdata in next, fonts.hashes.identifiers do + if fontid ~= xfontid then + local xhash = getstreamhash(xfontid) + if hash == xhash then + rawdata = xfontdata.shared and xfontdata.shared.rawdata + if rawdata then + resources = xfontdata.resources + properties = xfontdata.properties + parameters = xfontdata.parameters + break + end + end + end + end + end + if rawdata then + parent = { + hash = hash, + fontdata = fontdata, + filename = resources.filename or properties.filename or "unset", + indices = { }, + rawdata = rawdata, + properties = properties, -- we assume consistency + parameters = parameters, -- we assume consistency + streams = { }, + objectnumber = objects[hash], + basefontname = subsetname(properties.psname or properties.name or "unset"), + name = names[hash], + } + mainfonts[hash] = parent + noffonts = noffonts + 1 + end + end + if parent then + local indices = parent.indices + for k in next, used do + indices[k] = true + end + end + end + end + + -- this is no not yet ok for tfm / type 1 .. we need to move the nested blobs ourside the loop + + for hash, details in sortedhash(mainfonts) do + local filename = details.filename + if next(details.indices) then + if trace_fonts then + report_fonts("embedding %a hashed as %a",filename,hash) + end + local properties = details.properties + local bitmap = properties.usedbitmap + local method = properties.method -- will be pk | pdf | svg | ... + if bitmap or method then + local format = "type3" + local writer = mainwriters[format] + if writer then + if trace_fonts then + report_fonts("using main writer %a",format) + end + writer(details) + end + else + local format = properties.format + local writer = mainwriters[format] + if not writer then + -- This will move to the tpk module where we will also deal + -- with bitmaps then. + local encoding, pfbfile, encfile = getmapentry(filename) + if trace_fonts then + report_fonts("file %a resolved to encoding %a and file %a",filename,encoding,pfbfile) + end + if encoding and pfbfile then + filename = pfbfile + format = "type1" + -- + -- another (temp) hack + local size = details.fontdata.parameters.size + local factor = details.fontdata.parameters.factor + local descriptions = { } + local characters = details.fontdata.characters + -- + local names, _, _, metadata = fonts.constructors.handlers.pfb.loadvector(pfbfile) + local reverse = table.swapped(names) + local vector = encoding.vector + local indices = details.indices + local remapped = { } + local factor = bpfactor * size / 65536 + for k, v in next, indices do + local name = vector[k] + local index = reverse[name] or 0 + local width = factor * (characters[k].width or 0) + descriptions[k] = { + width = width, + index = index, + name = name, + } + remapped[index] = true + end + details.indices = remapped + -- + details.rawdata.descriptions = descriptions + details.filename = filename + details.rawdata.metadata = { } + -- + properties.filename = filename + properties.format = format + writer = mainwriters[format] + end + end + if not writer then + local pfbfile = file.replacesuffix(filename,"pfb") + if encoding and pfbfile then + filename = pfbfile + format = "type1" + -- + -- another (temp) hack + local size = details.fontdata.parameters.size + local factor = details.fontdata.parameters.factor + local descriptions = { } + local characters = details.fontdata.characters + -- + local names, _, _, metadata = fonts.constructors.handlers.pfb.loadvector(pfbfile) + local reverse = table.swapped(names) + local vector = encoding.vector + local indices = details.indices + local remapped = { } + local factor = bpfactor * size / 65536 + for k, v in next, indices do + local name = vector[k] + local index = reverse[name] or 0 + local width = factor * (characters[k].width or 0) + descriptions[k] = { + width = width, + index = index, + name = name, + } + remapped[index] = true + end + details.indices = remapped + -- + details.rawdata.descriptions = descriptions + details.filename = filename + details.rawdata.metadata = { } + -- + properties.filename = filename + properties.format = format + writer = mainwriters[format] + end + end + if writer then + if trace_fonts then + report_fonts("using main writer %a",format) + end + -- better move this test to the writers .. cleaner + local streams = loadstreamdata(details.fontdata) + if streams and streams.fontheader and streams.names then + details.streams = streams + writer(details) + details.streams = { } + elseif trace_fonts then + -- can be ok for e.g. emoji + report_fonts("no streams in %a",filename) + end + -- free some memory + else -- if trace_fonts then + report_fonts("no %a writer for %a",format,filename) + end + end + else -- not problem for svg ... + -- report_fonts("no indices for %a",filename) + end + if trace_fonts then + report_fonts("embedded indices: % t",table.sortedkeys(details.indices)) + end + mainfonts[details.hash] = false -- done + end + + statistics.stoptiming(objects) + +end + +statistics.register("font embedding time",function() + if noffonts > 0 then + return format("%s seconds, %s fonts", statistics.elapsedtime(objects),noffonts) + end +end) + +-- this is temporary + +local done = false + +updaters.register("backend.update.pdf",function() + if not done then + function pdf.getfontobjnum (k) return objects[k] end + function pdf.getfontname (k) return names [k] end + function pdf.includechar () end -- maybe, when we need it + function pdf.includefont () end -- maybe, when we need it + function pdf.includecharlist () end -- maybe, when we need it + function pdf.setomitcidset (v) includecidset = not toboolean(v) end + function pdf.setomitcharset () end -- we don't need that in lmtx + function pdf.setsuppressoptionalinfo() end -- we don't need that in lmtx + function pdf.mapfile (n) loadmapfile(n) end + function pdf.mapline (n) loadmapline(n) end + -- this will change + lpdf.registerdocumentfinalizer(lpdf.flushfonts,1,"wrapping up fonts") + done = true + end +end) |