if not modules then modules = { } end modules ['font-ocm'] = { version = 1.001, 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" } if not context then return elseif CONTEXTLMTXMODE and CONTEXTLMTXMODE > 0 then return else -- Maybe I'll also make a generic variant but for now I just test this in -- MkIV. After all, color fonts are not that much used (and generic is for -- serious looking articles and books and not for fancy documents using -- emoji.) Below is a quick and dirty implementation. Also, it looks like -- these features were never used outside context anyway (in spite of being -- in generic). end local tostring, tonumber, next = tostring, tonumber, next local round, max = math.round, math.round local sortedkeys, sortedhash, concat = table.sortedkeys, table.sortedhash, table.concat local setmetatableindex = table.setmetatableindex local formatters = string.formatters local otf = fonts.handlers.otf local otfregister = otf.features.register local bpfactor = number.dimenfactors.bp local typethree = { } callback.register("provide_charproc_data",function(action,f,...) local registered = typethree[f] if registered then return registered(action,f,...) else return 0, 0 -- this will also disable further calls end end) local defaults = { function() return 0, 0 end, function() return 0, 0 end, function() return 0.001, "" end, } local function registeractions(t) return { t.preroll or defaults[1], t.collect or defaults[2], t.wrapup or defaults[3], } end local function registertypethreeresource(specification,n,o) specification.usedobjects["X"..n] = lpdf.reference(o) end local function registertypethreefont(specification,n,o) specification.usedfonts["F"..n] = lpdf.reference(o) end local function typethreeresources(specification) local usedobjects = specification.usedobjects local usedfonts = specification.usedfonts local resources = { } if next(usedobjects) then resources[#resources+1] = "/XObject << " .. usedobjects() .. " >>" end if next(usedfonts) then resources[#resources+1] = "/Font << " .. usedfonts() .. " >>" end -- resources[#resources+1] = lpdf.collectedresources() specification.usedfonts = nil specification.usedobjects = nil return concat(resources, " ") end local function registerfont(specification,actions) specification.usedfonts = lpdf.dictionary() specification.usedobjects = lpdf.dictionary() typethree[specification.id] = function(action,f,c) return actions[action](specification,f,c) end end fonts.handlers.typethree = { register = function(id,handler) -- needed for manual if not typethree[id] then logs.report("fonts","low level Type3 handler registered for font with id %i",id) typethree[id] = handler end end } local initializeoverlay do local f_color = formatters["%.3f %.3f %.3f rg"] local f_gray = formatters["%.3f g"] local sharedpalettes = { } local colors = attributes.list[attributes.private('color')] or { } local transparencies = attributes.list[attributes.private('transparency')] or { } function otf.registerpalette(name,values) sharedpalettes[name] = values local color = lpdf.color local transparency = lpdf.transparency local register = colors.register for i=1,#values do local v = values[i] if v == "textcolor" then values[i] = false else local c = nil local t = nil if type(v) == "table" then c = register(name,"rgb", max(round((v.r or 0)*255),255)/255, max(round((v.g or 0)*255),255)/255, max(round((v.b or 0)*255),255)/255 ) else c = colors[v] t = transparencies[v] end if c and t then values[i] = color(1,c) .. " " .. transparency(t) elseif c then values[i] = color(1,c) elseif t then values[i] = color(1,t) end end end end local function convert(t,k) local v = { } for i=1,#k do local p = k[i] local r, g, b = p[1], p[2], p[3] if r == g and g == b then v[i] = f_gray(r/255) else v[i] = f_color(r/255,g/255,b/255) end end t[k] = v return v end -- This is by no means watertight (the id mess) especially because we -- don't know it yet. Instead we can just assemble here and avoid the -- box approach. I might do that (so then we need to pass fonts and -- extra resource entries. local f_stream = formatters["%s 0 d0 %s 0 0 %s 0 %s cm /X%i Do"] local fontorder = 0 local actions = registeractions { preroll = function(specification,f,c) local data = specification.delegated[c] local colorlist = data.colorlist local colorvalues = specification.colorvalues local default = specification.default local mainid = specification.mainid local t = { "\\typethreefont{", mainid, "}" } local n = 3 local l = nil local m = #colorlist for i=1,m do local entry = colorlist[i] local v = colorvalues[entry.class] or default if v and l ~= v then n = n + 1 ; t[n] = "\\typethreecode{" n = n + 1 ; t[n] = v n = n + 1 ; t[n] = "}" l = v end if i < m then n = n + 1 ; t[n] = "\\typethreechar{" else n = n + 1 ; t[n] = "\\typethreelast{" end n = n + 1 ; t[n] = entry.slot n = n + 1 ; t[n] = "}" end token.set_macro("typethreemacro",concat(t)) tex.runlocal("typethreetoks") registertypethreeresource(specification,c,tex.saveboxresource(0,nil,lpdf.collectedresources(),true)) -- registertypethreefont(specification,mainid,lpdf.reference(lpdf.getfontobjnumber(mainid))) return 0, 0 end, collect = function(specification,f,c) local parameters = specification.parameters local data = specification.delegated[c] local factor = parameters.hfactor local units = parameters.units local width = (data.width or 0) / factor local scale = 100 local factor = units * bpfactor -- / scale local depth = (data.depth or 0)*factor local shift = - depth / (10*units/1000) local object = pdf.immediateobj("stream",f_stream(width,scale,scale,shift,c)) return object, width end, wrapup = function(specification,f) return 0.001, typethreeresources(specification) end, } local function register(specification) registerfont(specification,actions) end initializeoverlay = function(tfmdata,kind,value) if value then local resources = tfmdata.resources local palettes = resources.colorpalettes if palettes then local converted = resources.converted if not converted then converted = setmetatableindex(convert) resources.converted = converted end local colorvalues = sharedpalettes[value] local default = false -- so the text color (bad for icon overloads) if colorvalues then default = colorvalues[#colorvalues] else colorvalues = converted[palettes[tonumber(value) or 1] or palettes[1]] or { } end local classes = #colorvalues if classes == 0 then return end -- local characters = tfmdata.characters local descriptions = tfmdata.descriptions local properties = tfmdata.properties local parameters = tfmdata.parameters -- properties.virtualized = true -- local delegated = { } local index = 0 local fonts = tfmdata.fonts or { } local fontindex = #fonts + 1 tfmdata.fonts = fonts local function flush() if index > 0 then fontorder = fontorder + 1 local f = { characters = delegated, parameters = parameters, tounicode = true, format = "type3", name = "InternalTypeThreeFont" , -- .. fontorder, psname = "none", } fonts[fontindex] = { id = font.define(f), delegated = delegated, parameters = parameters, colorvalues = colorvalues, default = default, } end fontindex = fontindex + 1 index = 0 delegated = { } end for unicode, character in sortedhash(characters) do local description = descriptions[unicode] if description then local colorlist = description.colors if colorlist then if index == 255 then flush() end index = index + 1 delegated[index] = { width = character.width, height = character.height, depth = character.depth, tounicode = character.tounicode, colorlist = colorlist, } character.commands = { { "slot", fontindex, index }, } end end end flush() local mainid = font.nextid() for i=1,#fonts do local f = fonts[i] if f.delegated then f.mainid = mainid register(f) end end return true end end end otfregister { name = "colr", description = "color glyphs", manipulators = { base = initializeoverlay, node = initializeoverlay, } } end do local nofstreams = 0 local f_name = formatters[ [[pdf-glyph-%05i]] ] local f_used = context and formatters[ [[original:///%s]] ] or formatters[ [[%s]] ] local hashed = { } local cache = { } local openpdf = pdfe.new function otf.storepdfdata(pdf) if pdf then local done = hashed[pdf] if not done then nofstreams = nofstreams + 1 local f = f_name(nofstreams) local n = openpdf(pdf,#pdf,f) done = f_used(n) hashed[pdf] = done end return done end end end local pdftovirtual do local f_stream = formatters["%s 0 d0 %s 0 0 %s %s %s cm /X%i Do"] local fontorder = 0 local shared = { } local actions = registeractions { preroll = function(specification,f,c) return 0, 0 end, collect = function(specification,f,c) local parameters = specification.parameters local data = specification.delegated[c] local desdata = data.desdata local pdfdata = data.pdfdata local width = desdata.width or 0 local height = desdata.height or 0 local depth = desdata.depth or 0 local factor = parameters.hfactor local units = parameters.units local typ = type(pdfdata) local dx = 0 local dy = 0 local scale = 1 if typ == "table" then data = pdfdata.data dx = pdfdata.x or pdfdata.dx or 0 dy = pdfdata.y or pdfdata.dy or 0 scale = pdfdata.scale or 1 elseif typ == "string" then data = pdfdata dx = 0 dy = 0 else return 0, 0 end if not data then return 0, 0 end local name = otf.storepdfdata(data) local xform = shared[name] if not xform then xform = images.embed(images.create { filename = name }) shared[name] = xform end registertypethreeresource(specification,c,xform.objnum) scale = scale * (width / (xform.width * bpfactor)) dy = - depth + dy -- png .. no time to figure it out now -- dx = 0 -- dy = 0 local object = pdf.immediateobj("stream",f_stream(width,scale,scale,dx,dy,c)), width return object, width end, wrapup = function(specification,f) return 1/specification.parameters.units, typethreeresources(specification) end, } local function register(specification) registerfont(specification,actions) end pdftovirtual = function(tfmdata,pdfshapes,kind) -- kind = png|svg if not tfmdata or not pdfshapes or not kind then return end -- local characters = tfmdata.characters local descriptions = tfmdata.descriptions local properties = tfmdata.properties local parameters = tfmdata.parameters local hfactor = parameters.hfactor -- properties.virtualized = true -- local storepdfdata = otf.storepdfdata -- local delegated = { } local index = 0 local fonts = tfmdata.fonts or { } local fontindex = #fonts + 1 tfmdata.fonts = fonts local function flush() if index > 0 then fontorder = fontorder + 1 local f = { characters = delegated, parameters = parameters, tounicode = true, format = "type3", name = "InternalTypeThreeFont" .. fontorder, psname = "none", size = parameters.size, } fonts[fontindex] = { id = font.define(f), delegated = delegated, parameters = parameters, } end fontindex = fontindex + 1 index = 0 delegated = { } end for unicode, character in sortedhash(characters) do local idx = character.index if idx then local pdfdata = pdfshapes[idx] local description = descriptions[unicode] if pdfdata and description then if index == 255 then flush() end index = index + 1 delegated[index] = { desdata = description, width = character.width, height = character.width, depth = character.width, tounicode = character.tounicode, pdfdata = pdfdata, } character.commands = { { "slot", fontindex, index }, } end end end -- flush() local mainid = font.nextid() for i=1,#fonts do local f = fonts[i] if f.delegated then f.mainid = mainid register(f) end end -- end end local initializesvg do local otfsvg = otf.svg or { } otf.svg = otfsvg otf.svgenabled = true local report_svg = logs.reporter("fonts","svg conversion") local loaddata = io.loaddata local savedata = io.savedata local remove = os.remove local xmlconvert = xml.convert local xmlfirst = xml.first function otfsvg.filterglyph(entry,index) local d = entry.data if gzip.compressed(d) then d = gzip.decompress(d) or d end local svg = xmlconvert(d) local root = svg and xmlfirst(svg,"/svg[@id='glyph"..index.."']") local data = root and tostring(root) return data end local runner = sandbox and sandbox.registerrunner { name = "otfsvg", program = "inkscape", method = "pipeto", template = "--export-area-drawing --shell > temp-otf-svg-shape.log", reporter = report_svg, } if not runner then -- -- poor mans variant for generic: -- runner = function() return io.popen("inkscape --export-area-drawing --shell > temp-otf-svg-shape.log","w") end end -- There are svg out there with bad viewBox specifications where shapes lay outside that area, -- but trying to correct that didn't work out well enough so I discarded that code. BTW, we -- decouple the inskape run and the loading run because inkscape is working in the background -- in the files so we need to have unique files. -- -- Because a generic setup can be flawed we need to catch bad inkscape runs which add a bit of -- ugly overhead. Bah. -- -- In the long run this method is a dead end because we cannot rely on command line arguments -- etc to be upward compatible (so no real batch tool). local new = nil local function inkscapeformat(suffix) if new == nil then new = os.resultof("inkscape --version") or "" new = new == "" or not find(new,"Inkscape%s*0") end return new and "filename" or suffix end function otfsvg.topdf(svgshapes,tfmdata) local pdfshapes = { } local inkscape = runner() if inkscape then -- local indices = fonts.getindices(tfmdata) local descriptions = tfmdata.descriptions local nofshapes = #svgshapes local s_format = inkscapeformat("pdf") -- hack, this will go away when is >= 0 is everywhere local f_svgfile = formatters["temp-otf-svg-shape-%i.svg"] local f_pdffile = formatters["temp-otf-svg-shape-%i.pdf"] local f_convert = formatters[new and "file-open:%s; export-%s:%s; export-do\n" or "%s --export-%s=%s\n"] local filterglyph = otfsvg.filterglyph local nofdone = 0 local processed = { } report_svg("processing %i svg containers",nofshapes) statistics.starttiming() for i=1,nofshapes do local entry = svgshapes[i] for index=entry.first,entry.last do local data = filterglyph(entry,index) if data and data ~= "" then local svgfile = f_svgfile(index) local pdffile = f_pdffile(index) savedata(svgfile,data) inkscape:write(f_convert(svgfile,s_format,pdffile)) processed[index] = true nofdone = nofdone + 1 if nofdone % 25 == 0 then report_svg("%i shapes submitted",nofdone) end end end end if nofdone % 25 ~= 0 then report_svg("%i shapes submitted",nofdone) end report_svg("processing can be going on for a while") inkscape:write("quit\n") inkscape:close() report_svg("processing %i pdf results",nofshapes) for index in next, processed do local svgfile = f_svgfile(index) local pdffile = f_pdffile(index) -- local fntdata = descriptions[indices[index]] -- local bounds = fntdata and fntdata.boundingbox local pdfdata = loaddata(pdffile) if pdfdata and pdfdata ~= "" then pdfshapes[index] = { data = pdfdata, -- x = bounds and bounds[1] or 0, -- y = bounds and bounds[2] or 0, } end remove(svgfile) remove(pdffile) end local characters = tfmdata.characters for k, v in next, characters do local d = descriptions[k] local i = d.index if i then local p = pdfshapes[i] if p then local w = d.width local l = d.boundingbox[1] local r = d.boundingbox[3] p.scale = (r - l) / w p.x = l end end end if not next(pdfshapes) then report_svg("there are no converted shapes, fix your setup") end statistics.stoptiming() if statistics.elapsedseconds then report_svg("svg conversion time %s",statistics.elapsedseconds() or "-") end end return pdfshapes end initializesvg = function(tfmdata,kind,value) -- hm, always value if value and otf.svgenabled then local svg = tfmdata.properties.svg local hash = svg and svg.hash local timestamp = svg and svg.timestamp if not hash then return end local pdffile = containers.read(otf.pdfcache,hash) local pdfshapes = pdffile and pdffile.pdfshapes if not pdfshapes or pdffile.timestamp ~= timestamp or not next(pdfshapes) then -- the next test tries to catch errors in generic usage but of course can result -- in running again and again local svgfile = containers.read(otf.svgcache,hash) local svgshapes = svgfile and svgfile.svgshapes pdfshapes = svgshapes and otfsvg.topdf(svgshapes,tfmdata,otf.pdfcache.writable,hash) or { } containers.write(otf.pdfcache, hash, { pdfshapes = pdfshapes, timestamp = timestamp, }) end pdftovirtual(tfmdata,pdfshapes,"svg") return true end end otfregister { name = "svg", description = "svg glyphs", manipulators = { base = initializesvg, node = initializesvg, } } end -- This can be done differently e.g. with ffi and gm and we can share code anway. Using -- batchmode in gm is not faster and as it accumulates we would need to flush all -- individual shapes. But ... in context lmtx (and maybe the backport) we will use -- a different and more efficient method anyway. I'm still wondering if I should -- keep color code in generic. Maybe it should be optional. local initializepng do -- Alternatively we can create a single pdf file with -adjoin and then pick up pages from -- that file but creating thousands of small files is no fun either. local otfpng = otf.png or { } otf.png = otfpng otf.pngenabled = true local report_png = logs.reporter("fonts","png conversion") local loaddata = io.loaddata local savedata = io.savedata local remove = os.remove local texhack = [[\startTEXpage\externalfigure[temp-otf-png-shape.png]\stopTEXpage]] local runner = false local method = "gm" local function initialize(v) if v == "lmtx" then report_png("using lmtx converter, slow but okay") runner = sandbox.registerrunner { -- reporter = report_png, name = "otfpng", program = "mtxrun --script context", template = "--once --batch --silent temp-otf-png-shape.tex > temp-otf-svg-shape.log", } method = v elseif v == "mutool" then report_png("using lmtx converter, no mask, black background") runner = sandbox.registerrunner { -- reporter = report_png, name = "otfpng", program = "mutool", template = "convert -o temp-otf-png-shape.pdf temp-otf-png-shape.png", } method = v else report_png("using lmtx converter, no mask, white background") runner = sandbox.registerrunner { -- reporter = report_png, name = "otfpng", program = "gm", template = "convert -quality 100 temp-otf-png-shape.png temp-otf-png-shape.pdf > temp-otf-svg-shape.log", } method = "gm" end return runner end directives.register("backend.otfpng.method",initialize) local files = utilities.files local openfile = files.open local closefile = files.close local setposition = files.setposition local readstring = files.readstring function otfpng.topdf(pngshapes,filename) if pngshapes and filename then local pdfshapes = { } local pngfile = "temp-otf-png-shape.png" local pdffile = "temp-otf-png-shape.pdf" local logfile = "temp-otf-png-shape.log" local texfile = "temp-otf-png-shape.tex" local tucfile = "temp-otf-png-shape.tuc" local nofdone = 0 local indices = sortedkeys(pngshapes) -- can be sparse local nofindices = #indices report_png("processing %i png containers",nofindices) statistics.starttiming() local filehandle = openfile(filename) savedata(texfile,texhack) -- not always used but who cares if not runner then initialize() end for i=1,nofindices do local index = indices[i] local entry = pngshapes[index] -- local data = entry.data -- or placeholder local offset = entry.o local size = entry.s local x = entry.x local y = entry.y local data = nil if offset and size then setposition(filehandle,offset) data = readstring(filehandle,size) savedata(pngfile,data) runner() data = loaddata(pdffile) end pdfshapes[index] = { -- x = x ~= 0 and x or nil, -- y = y ~= 0 and y or nil, data = data, } nofdone = nofdone + 1 if nofdone % 100 == 0 then report_png("%i shapes processed",nofdone) end end closefile(filehandle) report_png("processing %i pdf results",nofindices) remove(pngfile) remove(pdffile) remove(logfile) remove(texfile) remove(tucfile) statistics.stoptiming() if statistics.elapsedseconds then report_png("png conversion time %s",statistics.elapsedseconds() or "-") end return pdfshapes end end initializepng = function(tfmdata,kind,value) -- hm, always value if value and otf.pngenabled then local png = tfmdata.properties.png local hash = png and png.hash local timestamp = png and png.timestamp if not hash then return end local pdffile = containers.read(otf.pdfcache,hash) local pdfshapes = pdffile and pdffile.pdfshapes if not pdfshapes or pdffile.timestamp ~= timestamp or pdffile.timestamp ~= method then local pngfile = containers.read(otf.pngcache,hash) local filename = tfmdata.resources.filename local pngshapes = pngfile and pngfile.pngshapes pdfshapes = pngshapes and otfpng.topdf(pngshapes,filename) or { } containers.write(otf.pdfcache, hash, { pdfshapes = pdfshapes, timestamp = timestamp, method = method, }) end -- pdftovirtual(tfmdata,pdfshapes,"png") return true end end otfregister { name = "sbix", description = "sbix glyphs", manipulators = { base = initializepng, node = initializepng, } } otfregister { name = "cblc", description = "cblc glyphs", manipulators = { base = initializepng, node = initializepng, } } end do local function initializecolor(tfmdata,kind,value) if value == "auto" then return initializeoverlay(tfmdata,kind,value) or initializesvg (tfmdata,kind,value) or initializepng (tfmdata,kind,value) elseif value == "overlay" then return initializeoverlay(tfmdata,kind,value) elseif value == "svg" then return initializesvg(tfmdata,kind,value) elseif value == "png" or value == "bitmap" then return initializepng(tfmdata,kind,value) else -- forget about it end end otfregister { name = "color", description = "color glyphs", manipulators = { base = initializecolor, node = initializecolor, } } end -- Old stuff: do local startactualtext = nil local stopactualtext = nil function otf.getactualtext(s) if not startactualtext then startactualtext = backends.codeinjections.startunicodetoactualtextdirect stopactualtext = backends.codeinjections.stopunicodetoactualtextdirect end return startactualtext(s), stopactualtext() end end