if not modules then modules = { } end modules ['font-ogr'] = { 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" } -- Here we deal with graphic variants and for now also color support ends up here -- but that might change. It's lmtx only code. if not context then return elseif CONTEXTLMTXMODE == 0 then return end local tostring, tonumber, next, type = tostring, tonumber, next, type local round, max, mod, div = math.round, math.max, math.mod, math.div local find = string.find local concat, setmetatableindex, sortedhash = table.concat, table.setmetatableindex, table.sortedhash local utfbyte = utf.byte local formatters = string.formatters local settings_to_hash_strict, settings_to_array = utilities.parsers.settings_to_hash_strict, utilities.parsers.settings_to_array local otf = fonts.handlers.otf local otfregister = otf.features.register otf.svgenabled = true otf.pngenabled = true -- Just to remind me ... rewritten around the time this was posted on YT which -- was also around the 2019 ctx meeting: -- -- Gavin Harrison - "Threatening War" by The Pineapple Thief -- https://www.youtube.com/watch?v=ENF9wth4kwM -- todo: svg color plugin -- todo: get rid of double cm in svg (tricky as also elsewhere) -- todo: png offsets (depth) -- todo: maybe collapse indices so that we have less files (harder to debug) -- todo: manage (read: assign) font id's in lua so we know in advance -- what here and what in backend ... do -- This is a prelude to something better but I'm still experimenting. local dropins = { } fonts.dropins = dropins local droppedin = 0 local identifiers = fonts.hashes.identifiers function dropins.nextid() droppedin = droppedin - 1 return droppedin end -- todo: pass specification table instead function dropins.provide(method,t_tfmdata,indexdata,...) local droppedin = dropins.nextid() local t_characters = t_tfmdata.characters local t_descriptions = t_tfmdata.descriptions local t_properties = t_tfmdata.properties local d_tfmdata = setmetatableindex({ },t_tfmdata) local d_properties = setmetatableindex({ },t_properties) d_tfmdata.properties = d_properties local d_characters = { } -- setmetatableindex({ },t_characters) -- hm, index vs unicode local d_descriptions = { } -- setmetatableindex({ },t_descriptions) -- hm, index vs unicode d_tfmdata.characters = d_characters d_tfmdata.descriptions = d_descriptions d_properties.instance = - droppedin -- will become an extra element in the hash t_properties.virtualized = true identifiers[droppedin] = d_tfmdata local fonts = t_tfmdata.fonts or { } t_tfmdata.fonts = fonts d_properties.format = "type3" d_properties.method = method d_properties.indexdata = { indexdata, ... } -- can take quite some memory local slot = #fonts + 1 fonts[slot] = { id = droppedin } return slot, droppedin, d_tfmdata, d_properties end function dropins.clone(method,tfmdata,shapes,...) -- by index if method and shapes then local characters = tfmdata.characters local descriptions = tfmdata.descriptions local droppedin, tfmdrop, dropchars, dropdescs, colrshapes local idx = 255 local slot = 0 for k, v in next, characters do local index = v.index if index then local description = descriptions[k] if description then local shape = shapes[index] if shape then if idx >= 255 then idx = 1 colrshapes = setmetatableindex({ },shapes) slot, droppedin, tfmdrop = dropins.provide(method,tfmdata,colrshapes) dropchars = tfmdrop.characters dropdescs = tfmdrop.descriptions else idx = idx + 1 end colrshapes[idx] = shape -- so not: description -- todo: prepend v.commands = { { "slot", slot, idx } } -- hack to prevent that type 3 also gets 'use' flags .. todo local c = { commands = false, index = idx, dropin = tfmdrop } local d = { } -- { index = idx, dropin = tfmdrop } setmetatableindex(c,v) setmetatableindex(d,description) dropchars[idx] = c dropdescs[idx] = d -- not needed end end end end else -- error end end function dropins.swap(method,tfmdata,shapes,...) -- by unicode if method and shapes then local characters = tfmdata.characters local descriptions = tfmdata.descriptions local droppedin, tfmdrop, dropchars, dropdescs, colrshapes local idx = 255 local slot = 0 -- we can have a variant where shaped are by unicode and not by index for k, v in next, characters do local description = descriptions[k] if description then local shape = shapes[k] if shape then if idx >= 255 then idx = 1 colrshapes = setmetatableindex({ },shapes) slot, droppedin, tfmdrop = dropins.provide(method,tfmdata,colrshapes) dropchars = tfmdrop.characters dropdescs = tfmdrop.descriptions else idx = idx + 1 end colrshapes[idx] = shape -- so not: description -- todo: prepend v.commands = { { "slot", slot, idx } } -- hack to prevent that type 3 also gets 'use' flags .. todo local c = { commands = false, index = idx, dropin = tfmdrop } local d = { } -- index = idx, dropin = tfmdrop } setmetatableindex(c,v) setmetatableindex(d,description) dropchars[idx] = c dropdescs[idx] = d -- not needed end end end else -- error end end end do -- this will move to its own module local dropins = fonts.dropins local shapes = setmetatableindex(function(t,k) local v = { glyphs = { }, parameters = { units = 1000 }, } t[k] = v return v end) function dropins.registerglyphs(parameters) local category = parameters.name local target = shapes[category].parameters for k, v in next, parameters do if k ~= "glyphs" then target[k] = v end end end function dropins.registerglyph(parameters) local category = parameters.category local unicode = parameters.unicode local private = parameters.private local unichar = parameters.unichar if private then unicode = fonts.helpers.newprivateslot(private) elseif type(unichar) == "string" then unicode = utfbyte(unichar) else local unitype = type(unicode) if unitype == "string" then local uninumber = tonumber(unicode) if uninumber then unicode = round(uninumber) else unicode = utfbyte(unicode) end elseif unitype == "number" then unicode = round(unicode) end end if unicode then parameters.unicode = unicode -- print(category,unicode) shapes[category].glyphs[unicode] = parameters else -- error end end -- local function hascolorspec(t) -- if (t.color or "") ~= "" then -- return true -- elseif (t.fillcolor or "") ~= "" then -- return true -- elseif (t.drawcolor or "") ~= "" then -- return true -- elseif (t.linecolor or "") ~= "" then -- return true -- else -- return false -- end -- end local function hascolorspec(t) for k, v in next, t do if find(k,"color") then return true end end return false end local function initializemps(tfmdata,kind,value) if value then local specification = settings_to_hash_strict(value) if not specification or not next(specification) then specification = { category = value } end -- todo: multiple categories but then maybe also different -- clones because of the units .. for now we assume the same -- units local category = specification.category if category and category ~= "" then local categories = settings_to_array(category) local usedshapes = nil local index = 0 local spread = tonumber(specification.spread or 0) local hascolor = hascolorspec(specification) specification.spread = spread -- now a number for i=1,#categories do local category = categories[i] local mpsshapes = shapes[category] if mpsshapes then local properties = tfmdata.properties local parameters = tfmdata.parameters local characters = tfmdata.characters local descriptions = tfmdata.descriptions local mpsparameters = mpsshapes.parameters local units = mpsparameters.units or 1000 local defaultwidth = mpsparameters.width or 0 local defaultheight = mpsparameters.height or 0 local defaultdepth = mpsparameters.depth or 0 local usecolor = mpsparameters.usecolor local spread = spread * units local defaultcode = mpsparameters.code or "" local scale = parameters.size / units if hascolor then -- the graphic has color usecolor = false else -- do whatever is specified end usedshapes = usedshapes or { instance = "simplefun", units = units, usecolor = usecolor, specification = specification, shapes = mpsshapes, } -- todo: deal with extensibles and more properties for unicode, shape in sortedhash(mpsshapes.glyphs) do -- local oldc = characters[unicode] -- if oldc then index = index + 1 -- todo: somehow we end up with 2 as first entry after 0 local wd = shape.width or defaultwidth local ht = shape.height or defaultheight local dp = shape.depth or defaultdepth local newc = { index = index, -- into usedshapes width = scale * (wd + spread), height = scale * ht, depth = scale * dp, unicode = unicode, } -- characters [unicode] = newc descriptions[unicode] = newc -- usedshapes[unicode] = shape.code or defaultcode -- end end end end if usedshapes then -- todo: different font when units and usecolor changes, maybe move into loop -- above dropins.swap("mps",tfmdata,usedshapes) end end end end -- This kicks in quite late, after features have been checked. So if needed -- substitutions need to be defined with force. otfregister { name = "metapost", description = "metapost glyphs", manipulators = { base = initializemps, node = initializemps, } } end -- This sits here for historcal reasons so for now we keep it here. 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 -- This is also somewhat specific. local sharedpalettes do 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 end local initializeoverlay do -- we should use the proper interface instead but for now: local colors = attributes.colors local rgbtocmyk = colors.rgbtocmyk local f_cmyk = formatters["%.3N %.3f %.3N %.3N k"] local f_rgb = formatters["%.3N %.3f %.3N rg"] local f_gray = formatters["%.3N g"] local function convert(t,k) local v = { } local m = colors.model for i=1,#k do local p = k[i] local r, g, b = p[1]/255, p[2]/255, p[3]/255 if r == g and g == b then p = f_gray(r) elseif m == "cmyk" then p = f_cmyk(rgbtocmyk(r,g,b)) else p = f_rgb(r,g,b) end v[i] = p end t[k] = v return v end initializeoverlay = function(tfmdata,kind,value) -- we really need the id ... todo 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 droppedin, tfmdrop, dropchars, dropdescs, colrshapes local idx = 255 local slot = 0 -- -- todo: delay -- for k, v in next, characters do local index = v.index if index then local description = descriptions[k] if description then local colorlist = description.colors if colorlist then if idx >= 255 then idx = 1 colrshapes = { } slot, droppedin, tfmdrop = fonts.dropins.provide("color",tfmdata,colrshapes,colorvalues) dropchars = tfmdrop.characters dropdescs = tfmdrop.descriptions else idx = idx + 1 end -- colrshapes[idx] = description -- todo: use extender local u = { "use", 0 } for i=1,#colorlist do u[i+2] = colorlist[i].slot end v.commands = { u, { "slot", slot, idx } } -- hack to prevent that type 3 also gets 'use' flags .. todo local c = { commands = false, index = idx, dropin = tfmdata } local d = { } -- index = idx, dropin = tfmdrop setmetatableindex(c,v) setmetatableindex(d,description) dropchars[idx] = c dropdescs[idx] = d -- not needed end end end end return true end end end fonts.handlers.otf.features.register { name = "colr", description = "color glyphs", manipulators = { base = initializeoverlay, node = initializeoverlay, } } end local initializesvg do local report_svg = logs.reporter("fonts","svg") local cached = true -- maybe always false (after i've optimized the lot) directives.register("fonts.svg.cached", function(v) cached = v end) initializesvg = function(tfmdata,kind,value) -- hm, always value if value then local properties = tfmdata.properties local svg = properties.svg local hash = svg and svg.hash local timestamp = svg and svg.timestamp if not hash then return end local shapes = nil local method = nil local enforce = attributes.colors.model == "cmyk" if cached and not enforce then -- we need a different hash than for mkiv, so we append: local pdfhash = hash .. "-svg" local pdffile = containers.read(otf.pdfcache,pdfhash) local pdfshapes = pdffile and pdffile.pdfshapes local pdftarget = file.join(otf.pdfcache.writable,file.addsuffix(pdfhash,"pdf")) if not pdfshapes or pdffile.timestamp ~= timestamp or not next(pdfshapes) or not lfs.isfile(pdftarget) then local svgfile = containers.read(otf.svgcache,hash) local svgshapes = svgfile and svgfile.svgshapes pdfshapes = svgshapes and metapost.svgshapestopdf(svgshapes,pdftarget,report_svg,tfmdata.parameters.units) or { } -- look at ocl: we should store scale and x and y containers.write(otf.pdfcache, pdfhash, { pdfshapes = pdfshapes, timestamp = timestamp, }) end shapes = pdfshapes method = "pdf" else local mpsfile = containers.read(otf.mpscache,hash) local mpsshapes = mpsfile and mpsfile.mpsshapes if not mpsshapes or mpsfile.timestamp ~= timestamp or not next(mpsshapes) then local svgfile = containers.read(otf.svgcache,hash) local svgshapes = svgfile and svgfile.svgshapes -- still suboptimal mpsshapes = svgshapes and metapost.svgshapestomp(svgshapes,report_svg,tfmdata.parameters.units) or { } if enforce then -- cheap conversion, no black component generation mpsshapes.preamble = "interim svgforcecmyk := 1;" end containers.write(otf.mpscache, hash, { mpsshapes = mpsshapes, timestamp = timestamp, }) end shapes = mpsshapes method = "mps" end if shapes then shapes.fixdepth = value == "fixdepth" fonts.dropins.clone(method,tfmdata,shapes) end return true end end otfregister { name = "svg", description = "svg glyphs", manipulators = { base = initializesvg, node = initializesvg, } } end local initializepng do -- If this is really critical we can also use a pdf file as cache but I don't expect -- png fonts to remain used. local colors = attributes.colors local report_png = logs.reporter("fonts","png conversion") initializepng = function(tfmdata,kind,value) -- hm, always value if value then local properties = tfmdata.properties local png = properties.png local hash = png and png.hash local timestamp = png and png.timestamp if not hash then return end local pngfile = containers.read(otf.pngcache,hash) local pngshapes = pngfile and pngfile.pngshapes if pngshapes then if colors.model == "cmyk" then pngshapes.enforcecmyk = true end fonts.dropins.clone("png",tfmdata,pngshapes) end 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 -- I need to check jpeg and such but will do that when I run into -- it. 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