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