font-ogr.lmt /size: 28 Kb    last modification: 2021-10-28 13:51
1if not modules then modules = { } end modules ['font-ogr'] = {
2    version   = 1.001,
3    comment   = "companion to font-ini.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- Here we deal with graphic variants and for now also color support ends up here
10-- but that might change. It's lmtx only code.
11
12local tostring, tonumber, next, type, rawget = tostring, tonumber, next, type, rawget
13local round, max, mod, div = math.round, math.max, math.mod, math.div
14local find = string.find
15local concat, setmetatableindex, sortedhash = table.concat, table.setmetatableindex, table.sortedhash
16local utfbyte = utf.byte
17local formatters = string.formatters
18local settings_to_hash_strict, settings_to_array = utilities.parsers.settings_to_hash_strict, utilities.parsers.settings_to_array
19
20local otf         = fonts.handlers.otf
21local otfregister = otf.features.register
22otf.svgenabled    = true
23otf.pngenabled    = true
24
25-- Just to remind me ... rewritten around the time this was posted on YT which
26-- was also around the 2019 ctx meeting:
27--
28-- Gavin Harrison - "Threatening War" by The Pineapple Thief
29-- https://www.youtube.com/watch?v=ENF9wth4kwM
30
31-- todo: svg color plugin
32-- todo: get rid of double cm in svg (tricky as also elsewhere)
33-- todo: png offsets (depth)
34-- todo: maybe collapse indices so that we have less files (harder to debug)
35-- todo: manage (read: assign) font id's in lua so we know in advance
36
37-- what here and what in backend ...
38
39local report_fonts = logs.reporter("backend","fonts")
40local trace_fonts    trackers.register("backend.fonts",function(v) trace_fonts = v end)
41
42do
43
44    -- This is a prelude to something better but I'm still experimenting. We should delay more.
45
46    local dropins     = { }
47    fonts.dropins     = dropins
48    local droppedin   = 0
49    local identifiers = fonts.hashes.identifiers
50
51    function dropins.nextid()
52        droppedin = droppedin - 1
53        return droppedin
54    end
55
56    -- todo: pass specification table instead
57
58    function dropins.provide(method,t_tfmdata,indexdata,...)
59        local droppedin           = dropins.nextid()
60        local t_characters        = t_tfmdata.characters
61        local t_descriptions      = t_tfmdata.descriptions
62        local t_properties        = t_tfmdata.properties
63        local d_tfmdata           = setmetatableindex({ },t_tfmdata)
64        local d_properties        = setmetatableindex({ },t_properties)
65        local d_basefontname      = "ContextRuntimeFont" .. droppedin
66        d_properties.basefontname = d_basefontname
67        d_tfmdata.properties      = d_properties
68        local d_characters        = { } -- setmetatableindex({ },t_characters)   -- hm, index vs unicode
69        local d_descriptions      = { } -- setmetatableindex({ },t_descriptions) -- hm, index vs unicode
70        d_tfmdata.characters      = d_characters
71        d_tfmdata.descriptions    = d_descriptions
72        d_tfmdata.parentdata      = t_tfmdata -- so we can access it if needed
73        d_properties.instance     = - droppedin -- will become an extra element in the hash
74        identifiers[droppedin]    = d_tfmdata
75        local fonts               = t_tfmdata.fonts or { }
76        t_tfmdata.fonts           = fonts
77        d_properties.format       = "type3"
78        d_properties.method       = method
79        d_properties.indexdata    = { indexdata, ... } -- can take quite some memory
80        local slot                = #fonts + 1
81        fonts[slot]               = { id = droppedin }
82        if trace_fonts then
83            report_fonts("registering dropin %a using method %a",d_basefontname,method)
84        end
85        return slot, droppedin, d_tfmdata, d_properties
86    end
87
88    -- todo: delay this, in which case we can be leaner and meaner
89
90    function dropins.clone(method,tfmdata,shapes,...) -- by index
91        if method and shapes then
92            local characters   = tfmdata.characters
93            local descriptions = tfmdata.descriptions
94            local droppedin, tfmdrop, dropchars, dropdescs, colrshapes, props
95            local idx  = 255
96            local slot = 0
97            -- sorted ?
98            for k, v in next, characters do
99                local index = v.index
100                if index then
101                    local description = descriptions[k]
102                    if description then
103                        local shape = shapes[index]
104                        if shape then
105                            if idx >= 255 then
106                                idx = 1
107                                colrshapes = setmetatableindex({ },shapes)
108                                slot, droppedin, tfmdrop, props = dropins.provide(method,tfmdata,colrshapes)
109                                dropchars = tfmdrop.characters
110                                dropdescs = tfmdrop.descriptions
111                            else
112                                idx = idx + 1
113                            end
114                            colrshapes[idx] = shape -- so not: description
115                            -- todo: prepend
116                            v.commands = { { "slot", slot, idx } }
117                            -- hack to prevent that type 3 also gets 'use' flags .. todo
118                            local c = { commands = false, index = idx, dropin = tfmdrop }
119                            local d = { } -- { index = idx, dropin = tfmdrop }
120                            setmetatableindex(c,v)
121                            setmetatableindex(d,description)
122                            dropchars[idx] = c
123                            dropdescs[idx] = d -- not needed
124                        end
125                    end
126                end
127            end
128        else
129            -- error
130        end
131    end
132
133    function dropins.swap(method,tfmdata,shapes) -- by unicode
134        if method and shapes then
135            local characters   = tfmdata.characters
136            local descriptions = tfmdata.descriptions
137            local droppedin    = tfmdata.droppedin
138            local tfmdrop      = tfmdata.tfmdrop
139            local dropchars    = tfmdata.dropchars
140            local dropdescs    = tfmdata.dropdescs
141            local colrshapes   = tfmdata.colrshapes
142            local idx          = tfmdata.dropindex or 255
143            local slot         = tfmdata.dropslot  or 0
144            -- we can have a variant where shaped are by unicode and not by index
145            for k, v in next, characters do
146                local description = descriptions[k]
147                if description then
148                    local shape = shapes[k]
149                    if shape then
150                        if idx >= 255 then
151                            idx = 1
152                            colrshapes = setmetatableindex({ },shapes)
153                            slot, droppedin, tfmdrop = dropins.provide(method,tfmdata,colrshapes)
154                            dropchars = tfmdrop.characters
155                            dropdescs = tfmdrop.descriptions
156                        else
157                            idx = idx + 1
158                        end
159                        colrshapes[idx] = shape -- so not: description
160                        -- todo: prepend
161                        v.commands = { { "slot", slot, idx } }
162                        -- hack to prevent that type 3 also gets 'use' flags .. todo
163                        local c = { commands = false, index = idx, dropin = tfmdrop }
164                        local d = { } -- index = idx, dropin = tfmdrop }
165                        setmetatableindex(c,v)
166                        setmetatableindex(d,description)
167                        dropchars[idx] = c
168                        dropdescs[idx] = d -- not needed
169                    end
170                end
171            end
172            tfmdata.droppedin  = droppedin
173            tfmdata.tfmdrop    = tfmdrop
174            tfmdata.dropchars  = dropchars
175            tfmdata.dropdescs  = dropdescs
176            tfmdata.colrshapes = colrshapes
177            tfmdata.dropindex  = idx
178            tfmdata.dropslot   = slot
179        else
180            -- error
181        end
182    end
183
184    function dropins.swapone(method,tfmdata,shape,unicode)
185        if method and shape then
186            local characters   = tfmdata.characters
187            local descriptions = tfmdata.descriptions
188            local droppedin    = tfmdata.droppedin
189            local tfmdrop      = tfmdata.tfmdrop
190            local dropchars    = tfmdata.dropchars
191--             local dropdescs    = tfmdata.dropdescs
192            local colrshapes   = tfmdata.colrshapes
193            local idx          = tfmdata.dropindex or 255
194            local slot         = tfmdata.dropslot  or 0
195            local character    = characters[unicode]
196--             local description  = descriptions[unicode] or { }
197            if character then
198                if idx >= 255 then
199                    idx = 1
200                    colrshapes = setmetatableindex({ },shapes)
201                    slot, droppedin, tfmdrop = dropins.provide(method,tfmdata,colrshapes)
202                    dropchars = tfmdrop.characters
203                    dropdescs = tfmdrop.descriptions
204                else
205                    idx = idx + 1
206                end
207                colrshapes[idx] = shape.code -- so not: description
208                -- todo: prepend
209                character.commands = { { "slot", slot, idx } }
210                -- hack to prevent that type 3 also gets 'use' flags .. todo
211                local c = { commands = false, index = idx, dropin = tfmdrop }
212--                 local d = { } -- index = idx, dropin = tfmdrop }
213                setmetatableindex(c,character)
214--                 setmetatableindex(d,description)
215                dropchars[idx] = c
216--                 dropdescs[idx] = d -- not needed
217            end
218            tfmdata.droppedin  = droppedin
219            tfmdata.tfmdrop    = tfmdrop
220            tfmdata.dropchars  = dropchars
221--             tfmdata.dropdescs  = dropdescs
222            tfmdata.colrshapes = colrshapes
223            tfmdata.dropindex  = idx
224            tfmdata.dropslot   = slot
225        end
226    end
227
228end
229
230do -- this will move to its own module
231
232    local dropins = fonts.dropins
233
234    local shapes = setmetatableindex(function(t,k)
235        local v = {
236            glyphs     = { },
237            parameters = {
238                units = 10
239            },
240        }
241        t[k] = v
242        return v
243    end)
244
245    function dropins.getshape(name,n)
246        local s = shapes[name]
247        return s and s.glyphs and s.glyphs[n]
248    end
249
250    function dropins.getshapes(name)
251        return shapes[name]
252    end
253
254    function dropins.registerglyphs(parameters)
255        local category = parameters.name
256        local target   = shapes[category].parameters
257        for k, v in next, parameters do
258            if k ~= "glyphs" then
259                target[k] = v
260            end
261        end
262    end
263
264    function dropins.registerglyph(parameters)
265        local category = parameters.category
266        local unicode  = parameters.unicode
267        local private  = parameters.private
268        local unichar  = parameters.unichar
269        if private then
270            unicode = fonts.helpers.newprivateslot(private)
271        elseif type(unichar) == "string" then
272            unicode = utfbyte(unichar)
273        else
274            local unitype = type(unicode)
275            if unitype == "string" then
276                local uninumber = tonumber(unicode)
277                if uninumber then
278                    unicode = round(uninumber)
279                else
280                    unicode = utfbyte(unicode)
281                end
282            elseif unitype == "number" then
283                unicode = round(unicode)
284            end
285        end
286        if unicode then
287            parameters.unicode = unicode
288         -- print(category,unicode)
289            shapes[category].glyphs[unicode] = parameters
290        else
291            -- error
292        end
293    end
294
295 -- local function hascolorspec(t)
296 --     if (t.color or "") ~= "" then
297 --         return true
298 --     elseif (t.fillcolor or "") ~= "" then
299 --         return true
300 --     elseif (t.drawcolor or "") ~= "" then
301 --         return true
302 --     elseif (t.linecolor or "") ~= "" then
303 --         return true
304 --     else
305 --         return false
306 --     end
307 -- end
308
309    local function hascolorspec(t)
310        for k, v in next, t do
311            if find(k,"color") then
312                return true
313            end
314        end
315        return false
316    end
317
318    -- list of tonumber keywords
319
320    local function initializemps(tfmdata,kind,value)
321        if value then
322            local specification = settings_to_hash_strict(value)
323            if not specification or not next(specification) then
324                specification = { category = value }
325            end
326            -- todo: multiple categories but then maybe also different
327            -- clones because of the units .. for now we assume the same
328            -- units
329            local category = specification.category
330            if category and category ~= "" then
331                local categories = settings_to_array(category)
332                local usedshapes = nil
333                local index      = 0
334                local spread     = tonumber(specification.spread or 0) -- hm
335                local hascolor   = hascolorspec(specification)
336
337                specification.spread = spread -- now a number, maybe also for slant, weight etc
338
339                local preroll = specification.preroll
340                if preroll then
341                    metapost.simple(instance,"begingroup;",true,true)
342                    metapost.setparameterset("mpsfont",specification)
343                    metapost.simple("simplefun",preroll)
344                    metapost.setparameterset("mpsfont")
345                    metapost.simple(instance,"endgroup;",true,true)
346                end
347
348                for i=1,#categories do
349                    local category  = categories[i]
350                    local mpsshapes = shapes[category]
351                    if mpsshapes then
352                        local properties    = tfmdata.properties
353                        local parameters    = tfmdata.parameters
354                        local characters    = tfmdata.characters
355                        local descriptions  = tfmdata.descriptions
356                        local mpsparameters = mpsshapes.parameters
357                        local units         = mpsparameters.units  or 1000
358                        local defaultwidth  = mpsparameters.width  or 0
359                        local defaultheight = mpsparameters.height or 0
360                        local defaultdepth  = mpsparameters.depth  or 0
361                        local usecolor      = mpsparameters.usecolor
362                        local spread        = spread * units
363                        local defaultcode   = mpsparameters.code or ""
364                        local scale         = parameters.size / units
365                        if hascolor then
366                            -- the graphic has color
367                            usecolor = false
368                        else
369                            -- do whatever is specified
370                        end
371                        usedshapes = usedshapes or {
372                            instance      = "simplefun",
373                            units         = units,
374                            usecolor      = usecolor,
375                            specification = specification,
376                            shapes        = mpsshapes,
377                        }
378                        -- todo: deal with extensibles and more properties
379                        for unicode, shape in sortedhash(mpsshapes.glyphs) do
380                            index = index + 1 -- todo: somehow we end up with 2 as first entry after 0
381                            local wd = shape.width  or defaultwidth
382                            local ht = shape.height or defaultheight
383                            local dp = shape.depth  or defaultdepth
384                            local bb = shape.boundingbox
385                            local uc = shape.tounicode
386                            if uc then
387                                uc = round(uc) -- brrr can be 123.0
388                            end
389                            if bb then
390                                for i=1,4 do bb[i] = scale * bb[i] end
391                            end
392                            local newc = {
393                                index       = index, -- into usedshapes -- used?
394                                width       = scale * (wd + spread),
395                                height      = scale * ht,
396                                depth       = scale * dp,
397                                boundingbox = bb,
398                                unicode     = uc or unicode,
399                             -- shape       = shape, -- maybe a copy
400                            }
401                            --
402                            characters  [unicode] = newc
403                            descriptions[unicode] = newc
404                            usedshapes  [unicode] = shape.code or defaultcode
405                            --
406                            -- This is a way to get/use randomized shapes (see punk example).
407                            --
408                            if uc and uc ~= unicode then
409                                local c = characters[uc]
410                                if c then
411                                    local v = c.variants
412                                    if v then
413                                        v[#v + 1] = unicode
414                                    else
415                                        c.variants = { unicode }
416                                    end
417                                end
418                            end
419                        end
420                    end
421                end
422                if usedshapes then
423                    -- todo: different font when units and usecolor changes, maybe move into loop
424                    -- above
425                    dropins.swap("mps",tfmdata,usedshapes)
426                end
427            end
428        end
429    end
430
431    -- This kicks in quite late, after features have been checked. So if needed
432    -- substitutions need to be defined with force.
433
434    otfregister {
435        name         = "metapost",
436        description  = "metapost glyphs",
437        manipulators = {
438            base = initializemps,
439            node = initializemps,
440        }
441    }
442
443end
444
445-- This sits here for historical reasons so for now we keep it here.
446
447local startactualtext = nil
448local stopactualtext  = nil
449
450function otf.getactualtext(s)
451    if not startactualtext then
452        startactualtext = backends.codeinjections.startunicodetoactualtextdirect
453        stopactualtext  = backends.codeinjections.stopunicodetoactualtextdirect
454    end
455    return startactualtext(s), stopactualtext()
456end
457
458local sharedpalettes = { }  do
459
460    local colors         = attributes.list[attributes.private('color')] or { }
461    local transparencies = attributes.list[attributes.private('transparency')] or { }
462
463    function otf.registerpalette(name,values)
464        sharedpalettes[name] = values
465        for i=1,#values do
466            local v = values[i]
467            if v == "textcolor" then
468                values[i] = false
469            elseif type(v) == "table" then
470                values[i] = { kind = "values", data = v }
471            else -- freezing
472                values[i] = { kind = "attributes", color = colors[v], transparency = transparencies[v] }
473            end
474        end
475    end
476
477end
478
479local initializeoverlay  do
480
481    initializeoverlay = function(tfmdata,kind,value) -- we really need the id ... todo
482        if value then
483            local resources = tfmdata.resources
484            local palettes  = resources.colorpalettes
485            if palettes then
486                local colorvalues = false
487                local colordata = sharedpalettes[value]
488                if colordata and #colordata > 0 then
489                    colorvalues = {
490                        kind = "user",
491                        data = colordata,
492                    }
493                else
494                    colordata = palettes[tonumber(value) or 1] or palettes[1]
495                    if colordata and #colordata > 0 then
496                        colorvalues = {
497                            kind = "font",
498                            data = colordata,
499                        }
500                    end
501                end
502                if colorvalues then
503                    local characters   = tfmdata.characters
504                    local descriptions = tfmdata.descriptions
505                    local droppedin, tfmdrop, dropchars, dropdescs, colrshapes
506                    local idx  = 255
507                    local slot = 0
508                    --
509                    -- maybe delay in which case we have less fonts as we can be sparse
510                    --
511                    for k, v in next, characters do
512                        local index = v.index
513                        if index then
514                            local description = descriptions[k]
515                            if description then
516                                local colorlist = description.colors
517                                if colorlist then
518                                    if idx >= 255 then
519                                        idx = 1
520                                        colrshapes = { }
521                                        slot, droppedin, tfmdrop = fonts.dropins.provide("color",tfmdata,colrshapes,colorvalues)
522                                        dropchars = tfmdrop.characters
523                                        dropdescs = tfmdrop.descriptions
524                                    else
525                                        idx = idx + 1
526                                    end
527                                    --
528                                    colrshapes[idx] = description
529                                    -- todo: use extender
530                                    local u = { "use", 0 }
531                                    for i=1,#colorlist do
532                                        u[i+2] = colorlist[i].slot
533                                    end
534                                    v.commands = { u, { "slot", slot, idx } }
535                                    -- hack to prevent that type 3 also gets 'use' flags .. todo
536                                    local c = { commands = false, index = idx, dropin = tfmdata }
537                                    local d = { } -- index = idx, dropin = tfmdrop
538                                    setmetatableindex(c,v)
539                                    setmetatableindex(d,description)
540                                    dropchars[idx] = c
541                                    dropdescs[idx] = d -- not needed
542                                end
543                            end
544                        end
545                    end
546                    return true
547                end
548            end
549        end
550    end
551
552    fonts.handlers.otf.features.register {
553        name         = "colr",
554        description  = "color glyphs",
555        manipulators = {
556            base = initializeoverlay,
557            node = initializeoverlay,
558        }
559    }
560
561end
562
563local initializesvg  do
564
565    local report_svg = logs.reporter("fonts","svg")
566
567    local cached = true -- maybe always false (after i've optimized the lot)
568
569    directives.register("fonts.svg.cached", function(v) cached = v end)
570
571    initializesvg = function(tfmdata,kind,value) -- hm, always value
572        if value then
573            local properties = tfmdata.properties
574            local svg        = properties.svg
575            local hash       = svg and svg.hash
576            local timestamp  = svg and svg.timestamp
577            if not hash then
578                return
579            end
580            local shapes  = nil
581            local method  = nil
582            local enforce = attributes.colors.model == "cmyk"
583            if cached and not enforce then
584             -- we need a different hash than for mkiv, so we append:
585                local pdfhash   = hash .. "-svg"
586                local pdffile   = containers.read(otf.pdfcache,pdfhash)
587                local pdfshapes = pdffile and pdffile.pdfshapes
588                local pdftarget = file.join(otf.pdfcache.writable,file.addsuffix(pdfhash,"pdf"))
589                if not pdfshapes or pdffile.timestamp ~= timestamp or not next(pdfshapes) or not lfs.isfile(pdftarget) then
590                    local svgfile   = containers.read(otf.svgcache,hash)
591                    local svgshapes = svgfile and svgfile.svgshapes
592                    pdfshapes = svgshapes and metapost.svgshapestopdf(svgshapes,pdftarget,report_svg,tfmdata.parameters.units) or { }
593                    -- look at ocl: we should store scale and x and y
594                    containers.write(otf.pdfcache, pdfhash, {
595                        pdfshapes = pdfshapes,
596                        timestamp = timestamp,
597                    })
598                end
599                shapes = pdfshapes
600                method = "pdf"
601            else
602                local mpsfile   = containers.read(otf.mpscache,hash)
603                local mpsshapes = mpsfile and mpsfile.mpsshapes
604                if not mpsshapes or mpsfile.timestamp ~= timestamp or not next(mpsshapes) then
605                    local svgfile   = containers.read(otf.svgcache,hash)
606                    local svgshapes = svgfile and svgfile.svgshapes
607                    -- still suboptimal
608                    mpsshapes = svgshapes and metapost.svgshapestomp(svgshapes,report_svg,tfmdata.parameters.units) or { }
609                    if enforce then
610                        -- cheap conversion, no black component generation
611                        mpsshapes.preamble = "interim svgforcecmyk := 1;"
612                    end
613                    containers.write(otf.mpscache, hash, {
614                        mpsshapes = mpsshapes,
615                        timestamp = timestamp,
616                    })
617                end
618                shapes = mpsshapes
619                method = "mps"
620            end
621            if shapes then
622                shapes.fixdepth = value == "fixdepth"
623                fonts.dropins.clone(method,tfmdata,shapes)
624            end
625            return true
626        end
627    end
628
629    otfregister {
630        name         = "svg",
631        description  = "svg glyphs",
632        manipulators = {
633            base = initializesvg,
634            node = initializesvg,
635        }
636    }
637
638end
639
640local initializepng  do
641
642    -- If this is really critical we can also use a pdf file as cache but I don't expect
643    -- png fonts to remain used.
644
645    local colors = attributes.colors
646
647    local report_png = logs.reporter("fonts","png conversion")
648
649    initializepng = function(tfmdata,kind,value) -- hm, always value
650        if value then
651            local properties = tfmdata.properties
652            local png        = properties.png
653            local hash       = png and png.hash
654            local timestamp  = png and png.timestamp
655            if not hash then
656                return
657            end
658            local pngfile    = containers.read(otf.pngcache,hash)
659            local pngshapes  = pngfile and pngfile.pngshapes
660            if pngshapes then
661                if colors.model == "cmyk" then
662                    pngshapes.enforcecmyk = true
663                end
664                fonts.dropins.clone("png",tfmdata,pngshapes)
665            end
666            return true
667        end
668    end
669
670    otfregister {
671        name         = "sbix",
672        description  = "sbix glyphs",
673        manipulators = {
674            base = initializepng,
675            node = initializepng,
676        }
677    }
678
679    otfregister {
680        name         = "cblc",
681        description  = "cblc glyphs",
682        manipulators = {
683            base = initializepng,
684            node = initializepng,
685        }
686    }
687
688end
689
690do
691
692    -- I need to check jpeg and such but will do that when I run into
693    -- it.
694
695    local function initializecolor(tfmdata,kind,value)
696        if value == "auto" then
697            return
698                initializeoverlay(tfmdata,kind,value) or
699                initializesvg(tfmdata,kind,value) or
700                initializepng(tfmdata,kind,value)
701        elseif value == "overlay" then
702            return initializeoverlay(tfmdata,kind,value)
703        elseif value == "svg" then
704            return initializesvg(tfmdata,kind,value)
705        elseif value == "png" or value == "bitmap" then
706            return initializepng(tfmdata,kind,value)
707        else
708            -- forget about it
709        end
710    end
711
712    otfregister {
713        name         = "color",
714        description  = "color glyphs",
715        manipulators = {
716            base = initializecolor,
717            node = initializecolor,
718        }
719    }
720
721end
722