if not modules then modules = { } end modules ['font-ocl'] = { version = 1.001, comment = "companion to font-otf.lua (context)", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } -- todo : user list of colors local tostring, next, format = tostring, next, string.format local round, max = math.round, math.round local sortedkeys, sortedhash = table.sortedkeys, table.sortedhash local setmetatableindex = table.setmetatableindex local formatters = string.formatters local tounicode = fonts.mappings.tounicode local otf = fonts.handlers.otf local f_color = formatters["pdf:direct:%f %f %f rg"] local f_gray = formatters["pdf:direct:%f g"] if context then 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 else local tounicode = fonts.mappings.tounicode16 function otf.getactualtext(s) return "/Span << /ActualText >> BDC", "EMC" end end local sharedpalettes = { } local hash = setmetatableindex(function(t,k) local v = { "special", k } t[k] = v return v end) if context then local colors = attributes.list[attributes.private('color')] or { } local transparencies = attributes.list[attributes.private('transparency')] or { } function otf.registerpalette(name,values) sharedpalettes[name] = values for i=1,#values do local v = values[i] local c = nil local t = nil if type(v) == "table" then c = colors.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] = hash["pdf:direct:" .. lpdf.color(1,c) .. " " .. lpdf.transparency(t)] elseif c then values[i] = hash["pdf:direct:" .. lpdf.color(1,c)] elseif t then values[i] = hash["pdf:direct:" .. lpdf.color(1,t)] end end end else -- for generic function otf.registerpalette(name,values) sharedpalettes[name] = values for i=1,#values do local v = values[i] values[i] = hash[f_color( 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 )] end end end -- We need to force page first because otherwise the q's get outside -- the font switch and as a consequence the next character has no font -- set (well, it has: the preceding one). As a consequence these fonts -- are somewhat inefficient as each glyph gets the font set. It's a -- side effect of the fact that a font is handled when a character gets -- flushed. 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] = hash[f_gray(r/255)] else v[i] = hash[f_color(r/255,g/255,b/255)] end end t[k] = v return v end local function initializecolr(tfmdata,kind,value) -- hm, always 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] or converted[palettes[tonumber(value) or 1] or palettes[1]] or { } local classes = #colorvalues if classes == 0 then return end -- local characters = tfmdata.characters local descriptions = tfmdata.descriptions local properties = tfmdata.properties -- properties.virtualized = true tfmdata.fonts = { { id = 0 } } local widths = setmetatableindex(function(t,k) local v = { "right", -k } t[k] = v return v end) -- local getactualtext = otf.getactualtext local default = colorvalues[#colorvalues] local b, e = getactualtext(tounicode(0xFFFD)) local start = { "special", "pdf:page:q" } local stop = { "special", "pdf:raw:Q" } local actualb = { "special", "pdf:page:" .. b } -- saves tables local actuale = { "special", "pdf:page:" .. e } -- saves tables -- local cache = setmetatableindex(function(t,k) local v = { "char", k } t[k] = v return v end) -- for unicode, character in next, characters do local description = descriptions[unicode] if description then local colorlist = description.colors if colorlist then local u = description.unicode or characters[unicode].unicode local w = character.width or 0 local s = #colorlist local goback = w ~= 0 and widths[w] or nil -- needs checking: are widths the same local t = { start, not u and actualb or { "special", "pdf:raw:" .. getactualtext(tounicode(u)) } } local n = 2 local l = nil for i=1,s do local entry = colorlist[i] local v = colorvalues[entry.class] or default if v and l ~= v then n = n + 1 t[n] = v l = v end n = n + 1 t[n] = cache[entry.slot] if s > 1 and i < s and goback then n = n + 1 t[n] = goback end end n = n + 1 t[n] = actuale n = n + 1 t[n] = stop character.commands = t end end end end end end fonts.handlers.otf.features.register { name = "colr", description = "color glyphs", manipulators = { base = initializecolr, node = initializecolr, } } do -- local f_setstream = formatters[ [[io.savedata("svg-glyph-%05i",%q)]] ] -- local f_getstream = formatters[ [[svg-glyph-%05i]] ] -- function otfsvg.storepdfdata(pdf) -- nofstreams = nofstreams + 1 -- storepdfdata = function(pdf) -- nofstreams = nofstreams + 1 -- return f_setstream(nofstreams,pdf), f_getstream(nofstreams) -- end -- end 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 = { } function otf.storepdfdata(pdf) local done = hashed[pdf] if not done then nofstreams = nofstreams + 1 local o, n = epdf.openMemStream(pdf,#pdf,f_name(nofstreams)) cache[n] = o -- we need to keep in mem done = f_used(n) hashed[pdf] = done end return nil, done, nil end -- maybe more efficient but much slower (and we hash already) -- -- if context then -- -- local storepdfdata = otf.storepdfdata -- local initialized = false -- -- function otf.storepdfdata(pdf) -- if not initialized then -- if resolvers.setmemstream then -- local f_setstream = formatters[ [[resolvers.setmemstream("pdf-glyph-%05i",%q,true)]] ] -- local f_getstream = formatters[ [[memstream:///pdf-glyph-%05i]] ] -- local f_nilstream = formatters[ [[resolvers.resetmemstream("pdf-glyph-%05i",true)]] ] -- storepdfdata = function(pdf) -- local done = hashed[pdf] -- local set = nil -- local reset = nil -- if not done then -- nofstreams = nofstreams + 1 -- set = f_setstream(nofstreams,pdf) -- done = f_getstream(nofstreams) -- reset = f_nilstream(nofstreams) -- hashed[pdf] = done -- end -- return set, done, reset -- end -- otf.storepdfdata = storepdfdata -- end -- initialized = true -- end -- return storepdfdata(pdf) -- end -- -- end end local function pdftovirtual(tfmdata,pdfshapes,kind) -- kind = sbix|svg if not tfmdata or not pdfshapes or not kind then return end -- local characters = tfmdata.characters local properties = tfmdata.properties local parameters = tfmdata.parameters local hfactor = parameters.hfactor -- properties.virtualized = true -- tfmdata.fonts = { { id = 0 } } -- local getactualtext = otf.getactualtext local storepdfdata = otf.storepdfdata -- -- local nop = { "nop" } -- for unicode, character in sortedhash(characters) do -- sort is nicer for svg local index = character.index if index then local pdf = pdfshapes[index] local typ = type(pdf) local data = nil local dx = nil local dy = nil if typ == "table" then data = pdf.data dx = pdf.dx or 0 dy = pdf.dy or 0 elseif typ == "string" then data = pdf dx = 0 dy = 0 end if data then local setcode, name, nilcode = storepdfdata(data) if name then local bt, et = getactualtext(unicode) local wd = character.width or 0 local ht = character.height or 0 local dp = character.depth or 0 character.commands = { { "special", "pdf:direct:" .. bt }, { "down", dp + dy * hfactor }, { "right", dx * hfactor }, -- setcode and { "lua", setcode } or nop, { "image", { filename = name, width = wd, height = ht, depth = dp } }, -- nilcode and { "lua", nilcode } or nop, { "special", "pdf:direct:" .. et }, } character[kind] = true end end end end end local otfsvg = otf.svg or { } otf.svg = otfsvg otf.svgenabled = true do local report_svg = logs.reporter("fonts","svg conversion") local loaddata = io.loaddata local savedata = io.savedata local remove = os.remove if context and xml.convert then local xmlconvert = xml.convert local xmlfirst = xml.first function otfsvg.filterglyph(entry,index) local svg = xmlconvert(entry.data) local root = svg and xmlfirst(svg,"/svg[@id='glyph"..index.."']") local data = root and tostring(root) -- report_svg("data for glyph %04X: %s",index,data) return data end else function otfsvg.filterglyph(entry,index) -- can be overloaded return entry.data end end local runner = sandbox and sandbox.registerrunner { name = "otfsvg", program = "inkscape", method = "pipeto", template = "--shell > temp-otf-svg-shape.log", reporter = report_svg, } if not runner then -- -- poor mans variant for generic: -- runner = function() return io.open("inkscape --shell > temp-otf-svg-shape.log","w") end end function otfsvg.topdf(svgshapes) local pdfshapes = { } local inkscape = runner() if inkscape then local nofshapes = #svgshapes local f_svgfile = formatters["temp-otf-svg-shape-%i.svg"] local f_pdffile = formatters["temp-otf-svg-shape-%i.pdf"] local f_convert = formatters["%s --export-pdf=%s\n"] local filterglyph = otfsvg.filterglyph local nofdone = 0 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,pdffile)) pdfshapes[index] = true nofdone = nofdone + 1 if nofdone % 100 == 0 then report_svg("%i shapes processed",nofdone) end end end end inkscape:write("quit\n") inkscape:close() report_svg("processing %i pdf results",nofshapes) for index in next, pdfshapes do local svgfile = f_svgfile(index) local pdffile = f_pdffile(index) pdfshapes[index] = loaddata(pdffile) remove(svgfile) remove(pdffile) end statistics.stoptiming() if statistics.elapsedseconds then report_svg("svg conversion time %s",statistics.elapsedseconds() or "-") end end return pdfshapes end end local function initializesvg(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 then local svgfile = containers.read(otf.svgcache,hash) local svgshapes = svgfile and svgfile.svgshapes pdfshapes = svgshapes and otfsvg.topdf(svgshapes) or { } containers.write(otf.pdfcache, hash, { pdfshapes = pdfshapes, timestamp = timestamp, }) end pdftovirtual(tfmdata,pdfshapes,"svg") end end fonts.handlers.otf.features.register { name = "svg", description = "svg glyphs", manipulators = { base = initializesvg, node = initializesvg, } } -- 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. local otfsbix = otf.sbix or { } otf.sbix = otfsbix otf.sbixenabled = true do -- for now png but also other bitmap formats local report_sbix = logs.reporter("fonts","sbix conversion") local loaddata = io.loaddata local savedata = io.savedata local remove = os.remove local runner = sandbox and sandbox.registerrunner { name = "otfsbix", program = "gm", template = "convert -quality 100 temp-otf-sbix-shape.sbix temp-otf-sbix-shape.pdf > temp-otf-svg-shape.log", -- reporter = report_sbix, } if not runner then -- -- poor mans variant for generic: -- runner = function() return os.execute("gm convert -quality 100 temp-otf-sbix-shape.sbix temp-otf-sbix-shape.pdf > temp-otf-svg-shape.log") end end -- 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. function otfsbix.topdf(sbixshapes) local pdfshapes = { } local sbixfile = "temp-otf-sbix-shape.sbix" local pdffile = "temp-otf-sbix-shape.pdf" local nofdone = 0 local indices = sortedkeys(sbixshapes) -- can be sparse local nofindices = #indices report_sbix("processing %i sbix containers",nofindices) statistics.starttiming() for i=1,nofindices do local index = indices[i] local entry = sbixshapes[index] local data = entry.data local x = entry.x local y = entry.y savedata(sbixfile,data) runner() pdfshapes[index] = { x = x ~= 0 and x or nil, y = y ~= 0 and y or nil, data = loaddata(pdffile), } nofdone = nofdone + 1 if nofdone % 100 == 0 then report_sbix("%i shapes processed",nofdone) end end report_sbix("processing %i pdf results",nofindices) remove(sbixfile) remove(pdffile) statistics.stoptiming() if statistics.elapsedseconds then report_sbix("sbix conversion time %s",statistics.elapsedseconds() or "-") end return pdfshapes -- end end end local function initializesbix(tfmdata,kind,value) -- hm, always value if value and otf.sbixenabled then local sbix = tfmdata.properties.sbix local hash = sbix and sbix.hash local timestamp = sbix and sbix.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 then local sbixfile = containers.read(otf.sbixcache,hash) local sbixshapes = sbixfile and sbixfile.sbixshapes pdfshapes = sbixshapes and otfsbix.topdf(sbixshapes) or { } containers.write(otf.pdfcache, hash, { pdfshapes = pdfshapes, timestamp = timestamp, }) end -- pdftovirtual(tfmdata,pdfshapes,"sbix") end end fonts.handlers.otf.features.register { name = "sbix", description = "sbix glyphs", manipulators = { base = initializesbix, node = initializesbix, } }