font-ogr.lmt /size: 29 Kb    last modification: 2025-02-21 11:03
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("simplefun","begingroup;",true,true)
344                    metapost.setparameterset("mpsfont",specification)
345                    metapost.simple("simplefun",preroll)
346                    metapost.setparameterset("mpsfont")
347                    metapost.simple("simplefun","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 -- brrr can be 123.0
388                            if type(uc) == "table" then
389                                for i=1,#uc do
390                                    uc[i] = round(uc[i])
391                                end
392                            elseif uc then
393                                uc = round(uc)
394                            end
395                            if bb then
396                                for i=1,4 do bb[i] = scale * bb[i] end
397                            end
398                            local newc = {
399                                index       = index, -- into usedshapes -- used?
400                                width       = scale * (wd + spread),
401                                height      = scale * ht,
402                                depth       = scale * dp,
403                                boundingbox = bb,
404                                unicode     = uc or unicode,
405                             -- shape       = shape, -- maybe a copy
406                            }
407                            --
408                            characters  [unicode] = newc
409                            descriptions[unicode] = newc
410                            usedshapes  [unicode] = shape.code or defaultcode
411                            --
412                            -- This is a way to get/use randomized shapes (see punk example).
413                            --
414                            if uc and uc ~= unicode then
415                                local c = characters[uc]
416                                if c then
417                                    local v = c.variants
418                                    if v then
419                                        v[#v + 1] = unicode
420                                    else
421                                        c.variants = { unicode }
422                                    end
423                                end
424                            end
425                        end
426                    end
427                end
428                if usedshapes then
429                    -- todo: different font when units and usecolor changes, maybe move into loop
430                    -- above
431                    dropins.swap("mps",tfmdata,usedshapes)
432                end
433            end
434        end
435    end
436
437    -- This kicks in quite late, after features have been checked. So if needed
438    -- substitutions need to be defined with force.
439
440    otfregister {
441        name         = "metapost",
442        description  = "metapost glyphs",
443        manipulators = {
444            base = initializemps,
445            node = initializemps,
446        }
447    }
448
449end
450
451-- This sits here for historical reasons so for now we keep it here.
452
453local startactualtext = nil
454local stopactualtext  = nil
455
456function otf.getactualtext(s)
457    if not startactualtext then
458        startactualtext = backends.codeinjections.startunicodetoactualtextdirect
459        stopactualtext  = backends.codeinjections.stopunicodetoactualtextdirect
460    end
461    return startactualtext(s), stopactualtext()
462end
463
464local sharedpalettes = { }  do
465
466    local colors         <const> = attributes.list[attributes.private('color')] or { }
467    local transparencies <const> = attributes.list[attributes.private('transparency')] or { }
468
469    function otf.registerpalette(name,values)
470        sharedpalettes[name] = values
471        for i=1,#values do
472            local v = values[i]
473            if v == "textcolor" then
474                values[i] = false
475            elseif type(v) == "table" then
476                values[i] = { kind = "values", data = v }
477            else -- freezing
478                values[i] = { kind = "attributes", color = colors[v], transparency = transparencies[v] }
479            end
480        end
481    end
482
483end
484
485local initializeoverlay  do
486
487    initializeoverlay = function(tfmdata,kind,value) -- we really need the id ... todo
488        if value then
489            local resources = tfmdata.resources
490            local palettes  = resources.colorpalettes
491            if palettes then
492                local colorvalues = false
493                local colordata = sharedpalettes[value]
494                if colordata and #colordata > 0 then
495                    colorvalues = {
496                        kind = "user",
497                        data = colordata,
498                    }
499                else
500                    colordata = palettes[tonumber(value) or 1] or palettes[1]
501                    if colordata and #colordata > 0 then
502                        colorvalues = {
503                            kind = "font",
504                            data = colordata,
505                        }
506                    end
507                end
508                if colorvalues then
509                    local characters   = tfmdata.characters
510                    local descriptions = tfmdata.descriptions
511                    local droppedin, tfmdrop, dropchars, dropdescs, colrshapes
512                    local idx  = 255
513                    local slot = 0
514                    --
515                    -- maybe delay in which case we have less fonts as we can be sparse
516                    --
517                    for k, v in next, characters do
518                        local index = v.index
519                        if index then
520                            local description = descriptions[k]
521                            if description then
522                                local colorlist = description.colors
523                                if colorlist then
524                                    if idx >= 255 then
525                                        idx = 1
526                                        colrshapes = { }
527                                        slot, droppedin, tfmdrop = fonts.dropins.provide("color",tfmdata,colrshapes,colorvalues)
528                                        dropchars = tfmdrop.characters
529                                        dropdescs = tfmdrop.descriptions
530                                    else
531                                        idx = idx + 1
532                                    end
533                                    --
534                                    colrshapes[idx] = description
535                                    -- todo: use extender
536                                    local u = { "use", 0 }
537                                    for i=1,#colorlist do
538                                        local c = colorlist[i]
539                                        if c then
540                                            u[i+2] = c.slot
541                                        else
542                                            -- some error
543                                        end
544                                    end
545                                    v.commands = { u, slotcommand[slot][idx] }
546                                    -- hack to prevent that type 3 also gets 'use' flags .. todo
547                                    local c = { commands = false, index = idx, dropin = tfmdata }
548                                    local d = { } -- index = idx, dropin = tfmdrop
549                                    setmetatableindex(c,v)
550                                    setmetatableindex(d,description)
551                                    dropchars[idx] = c
552                                    dropdescs[idx] = d -- not needed
553                                end
554                            end
555                        end
556                    end
557                    return true
558                end
559            end
560        end
561    end
562
563    fonts.handlers.otf.features.register {
564        name         = "colr",
565        description  = "color glyphs",
566        manipulators = {
567            base = initializeoverlay,
568            node = initializeoverlay,
569            plug = initializeoverlay,
570        }
571    }
572
573end
574
575local initializesvg  do
576
577    local report_svg = logs.reporter("fonts","svg")
578
579    local cached = true -- maybe always false (after i've optimized the lot)
580
581    directives.register("fonts.svg.cached", function(v) cached = v end)
582
583    initializesvg = function(tfmdata,kind,value) -- hm, always value
584        if value then
585            local properties = tfmdata.properties
586            local svg        = properties.svg
587            local hash       = svg and svg.hash
588            local timestamp  = svg and svg.timestamp
589            if not hash then
590                return
591            end
592            local shapes   = nil
593            local method   = nil
594            local fixdepth = value == "fixdepth"
595            local enforce  = attributes.colors.model == "cmyk"
596            if cached and not enforce then
597             -- we need a different hash than for mkiv, so we append:
598                local pdfhash   = hash .. "-svg"
599                local pdffile   = containers.read(otf.pdfcache,pdfhash)
600                local pdfshapes = pdffile and pdffile.pdfshapes
601                local pdftarget = file.join(otf.pdfcache.writable,file.addsuffix(pdfhash,"pdf"))
602                if not pdfshapes or pdffile.timestamp ~= timestamp or not next(pdfshapes) or not lfs.isfile(pdftarget) then
603                    local svgfile   = containers.read(otf.svgcache,hash)
604                    local svgshapes = svgfile and svgfile.svgshapes
605                    pdfshapes = svgshapes and metapost.svgshapestopdf(svgshapes,pdftarget,report_svg,tfmdata.parameters.units) or { }
606                    -- look at ocl: we should store scale and x and y
607                    containers.write(otf.pdfcache, pdfhash, {
608                        pdfshapes = pdfshapes,
609                        timestamp = timestamp,
610                    })
611                end
612                shapes   = pdfshapes
613                method   = "pdf"
614                fixdepth = true -- needed for svg
615            else
616                local mpsfile   = containers.read(otf.mpscache,hash)
617                local mpsshapes = mpsfile and mpsfile.mpsshapes
618                if not mpsshapes or mpsfile.timestamp ~= timestamp or not next(mpsshapes) then
619                    local svgfile   = containers.read(otf.svgcache,hash)
620                    local svgshapes = svgfile and svgfile.svgshapes
621                    -- still suboptimal
622                    mpsshapes = svgshapes and metapost.svgshapestomp(svgshapes,report_svg,tfmdata.parameters.units) or { }
623                    if enforce then
624                        -- cheap conversion, no black component generation
625                        mpsshapes.preamble = "interim svgforcecmyk := 1;"
626                    end
627                    containers.write(otf.mpscache, hash, {
628                        mpsshapes = mpsshapes,
629                        timestamp = timestamp,
630                    })
631                end
632                shapes = mpsshapes
633                method = "mps"
634            end
635            if shapes then
636                shapes.fixdepth = fixdepth
637                fonts.dropins.clone(method,tfmdata,shapes)
638            end
639            return true
640        end
641    end
642
643    otfregister {
644        name         = "svg",
645        description  = "svg glyphs",
646        manipulators = {
647            base = initializesvg,
648            node = initializesvg,
649            plug = initializesvg,
650        }
651    }
652
653end
654
655local initializepng  do
656
657    -- If this is really critical we can also use a pdf file as cache but I don't expect
658    -- png fonts to remain used.
659
660    local colors = attributes.colors
661
662    local report_png = logs.reporter("fonts","png conversion")
663
664    initializepng = function(tfmdata,kind,value) -- hm, always value
665        if value then
666            local properties = tfmdata.properties
667            local png        = properties.png
668            local hash       = png and png.hash
669            local timestamp  = png and png.timestamp
670            if not hash then
671                return
672            end
673            local pngfile    = containers.read(otf.pngcache,hash)
674            local pngshapes  = pngfile and pngfile.pngshapes
675            if pngshapes then
676                if colors.model == "cmyk" then
677                    pngshapes.enforcecmyk = true
678                end
679                fonts.dropins.clone("png",tfmdata,pngshapes)
680            end
681            return true
682        end
683    end
684
685    otfregister {
686        name         = "sbix",
687        description  = "sbix glyphs",
688        manipulators = {
689            base = initializepng,
690            node = initializepng,
691            plug = initializepng,
692        }
693    }
694
695    otfregister {
696        name         = "cblc",
697        description  = "cblc glyphs",
698        manipulators = {
699            base = initializepng,
700            node = initializepng,
701            plug = initializepng,
702        }
703    }
704
705end
706
707do
708
709    -- I need to check jpeg and such but will do that when I run into
710    -- it.
711
712    local function initializecolor(tfmdata,kind,value)
713        if value == "auto" then
714            return
715                initializeoverlay(tfmdata,kind,value) or
716                initializesvg(tfmdata,kind,value) or
717                initializepng(tfmdata,kind,value)
718        elseif value == "overlay" then
719            return initializeoverlay(tfmdata,kind,value)
720        elseif value == "svg" then
721            return initializesvg(tfmdata,kind,value)
722        elseif value == "png" or value == "bitmap" then
723            return initializepng(tfmdata,kind,value)
724        else
725            -- forget about it
726        end
727    end
728
729    otfregister {
730        name         = "color",
731        description  = "color glyphs",
732        manipulators = {
733            base = initializecolor,
734            node = initializecolor,
735            plug = initializecolor,
736        }
737    }
738
739end
740