mlib-fnt.lmt /size: 19 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['font-mps'] = {
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
9local type, tonumber, tostring = type, tonumber, tostring
10local concat, insert, remove = table.concat, table.insert, table.remove
11local formatters, match = string.formatters, string.match
12local utfbyte = utf.byte
13
14-- QP0 [QP1] QP2 => CP0 [CP1 CP2] CP3
15
16-- CP0 = QP0
17-- CP3 = QP2
18--
19-- CP1 = QP0 + 2/3 *(QP1-QP0)
20-- CP2 = QP2 + 2/3 *(QP1-QP2)
21
22fonts               = fonts or { }
23local metapost      = fonts.metapost or { }
24fonts.metapost      = metapost
25
26local f_moveto      = formatters["(%N,%N)"]
27local f_lineto      = formatters["--(%N,%N)"]
28local f_curveto     = formatters["..controls(%N,%N)and(%N,%N)..(%N,%N)"]
29local s_cycle       <const> = "--cycle"
30
31local f_nofill      = formatters["nofill %s;"]
32local f_dofill      = formatters["fill %s;"]
33
34local f_draw_trace  = formatters["drawpathonly %s;"]
35local f_draw        = formatters["draw %s;"]
36
37local f_rectangle   = formatters["((%N,%N)--(%N,%N)--(%N,%N)--(%N,%N)--cycle)"]
38local f_line        = formatters["((%N,%N)--(%N,%N))"]
39
40function metapost.boundingbox(d,factor)
41    local bounds = d.boundingbox
42    local factor = factor or 1
43    local llx    = factor*bounds[1]
44    local lly    = factor*bounds[2]
45    local urx    = factor*bounds[3]
46    local ury    = factor*bounds[4]
47    return f_rectangle(llx,lly,urx,lly,urx,ury,llx,ury)
48end
49
50function metapost.baseline(d,factor)
51    local bounds = d.boundingbox
52    local factor = factor or 1
53    local llx    = factor*bounds[1]
54    local urx    = factor*bounds[3]
55    return f_line(llx,0,urx,0)
56end
57
58function metapost.widthline(d,factor)
59    local bounds = d.boundingbox
60    local factor = factor or 1
61    local lly    = factor*bounds[2]
62    local ury    = factor*bounds[4]
63    local width  = factor*d.width
64    return f_line(width,lly,width,ury)
65end
66
67function metapost.zeroline(d,factor)
68    local bounds = d.boundingbox
69    local factor = factor or 1
70    local lly    = factor*bounds[2]
71    local ury    = factor*bounds[4]
72    return f_line(0,lly,0,ury)
73end
74
75function metapost.paths(d,xfactor,yfactor)
76    -- todo: add tracer that shows d
77    local sequence = d.sequence
78    local segments = d.segments
79    local list     = { }
80    local path     = { } -- recycled
81    local size     = 0
82    local xfactor  = xfactor or 1
83    local yfactor  = yfactor or xfactor
84    if sequence then
85        local i = 1
86        local n = #sequence
87        if sequence[1] ~= "m" then
88            report()
89        end
90        while i < n do
91            local operator = sequence[i]
92            if operator == "m" then -- "moveto"
93                if size > 0 then
94                    size = size + 1
95                    path[size] = s_cycle
96                    list[#list+1] = concat(path,"",1,size)
97                    size = 1
98                else
99                    size = size + 1
100                end
101                path[size] = f_moveto(xfactor*sequence[i+1],yfactor*sequence[i+2])
102                i = i + 3
103            elseif operator == "l" then -- "lineto"
104                size = size + 1
105                path[size] = f_lineto(xfactor*sequence[i+1],yfactor*sequence[i+2])
106                i = i + 3
107            elseif operator == "c" then -- "curveto"
108                size = size + 1
109                path[size] = f_curveto(xfactor*sequence[i+1],yfactor*sequence[i+2],xfactor*sequence[i+3],yfactor*sequence[i+4],xfactor*sequence[i+5],yfactor*sequence[i+6])
110                i = i + 7
111            elseif operator =="q" then -- "quadraticto"
112                size = size + 1
113                -- first is always a moveto
114                local l_x = xfactor*sequence[i-2]
115                local l_y = yfactor*sequence[i-1]
116                local m_x = xfactor*sequence[i+1]
117                local m_y = yfactor*sequence[i+2]
118                local r_x = xfactor*sequence[i+3]
119                local r_y = yfactor*sequence[i+4]
120                path[size] = f_curveto (
121                    l_x + 2/3 * (m_x-l_x),
122                    l_y + 2/3 * (m_y-l_y),
123                    r_x + 2/3 * (m_x-r_x),
124                    r_y + 2/3 * (m_y-r_y),
125                    r_x, r_y
126                )
127                i = i + 5
128            else
129                -- weird
130                i = i + 1
131            end
132        end
133    elseif segments then
134        for i=1,#segments do
135            local segment  = segments[i]
136            local operator = segment[#segment]
137            if operator == "m" then -- "moveto"
138                if size > 0 then
139                    size = size + 1
140                    path[size] = s_cycle
141                    list[#list+1] = concat(path,"",1,size)
142                    size = 1
143                else
144                    size = size + 1
145                end
146                path[size] = f_moveto(xfactor*segment[1],yfactor*segment[2])
147            elseif operator == "l" then -- "lineto"
148                size = size + 1
149                path[size] = f_lineto(xfactor*segment[1],yfactor*segment[2])
150            elseif operator == "c" then -- "curveto"
151                size = size + 1
152                path[size] = f_curveto(xfactor*segment[1],yfactor*segment[2],xfactor*segment[3],yfactor*segment[4],xfactor*segment[5],yfactor*segment[6])
153            elseif operator == "q" then -- "quadraticto"
154                size = size + 1
155                -- first is always a moveto
156                local prev = segments[i-1]
157                local l_x = xfactor*prev[#prev-2]
158                local l_y = yfactor*prev[#prev-1]
159                local m_x = xfactor*segment[1]
160                local m_y = yfactor*segment[2]
161                local r_x = xfactor*segment[3]
162                local r_y = yfactor*segment[4]
163                path[size] = f_curveto (
164                    l_x + 2/3 * (m_x-l_x),
165                    l_y + 2/3 * (m_y-l_y),
166                    r_x + 2/3 * (m_x-r_x),
167                    r_y + 2/3 * (m_y-r_y),
168                    r_x, r_y
169                )
170            elseif operator == "close" then -- second argument is diagnostic
171                size = size + 1
172                path[size] = s_cycle
173                list[#list+1] = concat(path,"",1,size)
174                size = 0
175            else
176                -- weird
177            end
178        end
179    else
180        return
181    end
182    if size > 0 then
183        size = size + 1
184        path[size] = s_cycle
185        list[#list+1] = concat(path,"",1,size)
186    end
187    return list
188end
189
190function metapost.fill(paths)
191    local r = { }
192    local n = #paths
193    for i=1,n do
194        if i < n then
195            r[i] = f_nofill(paths[i])
196        else
197            r[i] = f_dofill(paths[i])
198        end
199    end
200    return concat(r)
201end
202
203function metapost.draw(paths,trace)
204    local r = { }
205    local n = #paths
206    for i=1,n do
207        if trace then
208            r[i] = f_draw_trace(paths[i])
209        else
210            r[i] = f_draw(paths[i])
211        end
212    end
213    return concat(r)
214end
215
216function metapost.maxbounds(data,index,factor)
217    local maxbounds   = data.maxbounds
218    local factor      = factor or 1
219    local glyphs      = data.glyphs
220    local glyph       = glyphs[index]
221    local boundingbox = glyph.boundingbox
222    local xmin, ymin, xmax, ymax
223    if not maxbounds then
224        xmin = 0
225        ymin = 0
226        xmax = 0
227        ymax = 0
228        for i=1,#glyphs do
229            local d = glyphs[i]
230            if d then
231                local b = d.boundingbox
232                if b then
233                    if b[1] < xmin then xmin = b[1] end
234                    if b[2] < ymin then ymin = b[2] end
235                    if b[3] > xmax then xmax = b[3] end
236                    if b[4] > ymax then ymax = b[4] end
237                end
238            end
239        end
240        maxbounds = { xmin, ymin, xmax, ymax }
241        data.maxbounds = maxbounds
242    else
243        xmin = maxbounds[1]
244        ymin = maxbounds[2]
245        xmax = maxbounds[3]
246        ymax = maxbounds[4]
247    end
248    local llx   = boundingbox[1]
249    local lly   = boundingbox[2]
250    local urx   = boundingbox[3]
251    local ury   = boundingbox[4]
252    local width = glyph.width
253    if llx > 0 then
254        llx = 0
255    end
256    if width > urx then
257        urx = width
258    end
259    return f_rectangle(
260        factor*llx,factor*ymin,
261        factor*urx,factor*ymin,
262        factor*urx,factor*ymax,
263        factor*llx,factor*ymax
264    )
265end
266
267-- This is a nice example of tex, metapost and lua working in tandem. Each kicks in at the
268-- right time. It's probably why I like watching https://www.youtube.com/watch?v=c5FqpddnJmc
269-- so much: precisely (and perfectly) timed too.
270
271local texgetbox          = tex.getbox
272
273local nodecodes          = nodes.nodecodes -- no nuts yet
274local rulecodes          = nodes.rulecodes
275
276local rule_code          <const> = nodecodes.rule
277
278local normalrule_code    <const> = rulecodes.normal
279local outlinerule_code   <const> = rulecodes.outline
280local userrule_code      <const> = rulecodes.user
281local emptyrule_code     <const> = rulecodes.empty
282
283local nuts               = nodes.nuts
284----- getwhd             = nuts.getwhd
285local getexpansion       = nuts.getexpansion
286local getscales          = nuts.getscales
287local isglyph            = nuts.isglyph
288local getglyphdimensions = nuts.getglyphdimensions
289
290local fonthashes         = fonts.hashes
291local fontcharacters     = fonthashes.characters
292local fontparameters     = fonthashes.parameters
293local fontshapes         = fonthashes.shapes
294local fontdescriptions   = fonthashes.descriptions
295
296local topaths            = metapost.paths
297
298local f_text             = formatters["mfun_do_outline_text_flush(%q,%i,%N,%N,%q)(%,t);"]
299local f_rule             = formatters["mfun_do_outline_rule_flush(%q,%N,%N,%N,%N);"]
300local f_bounds           = formatters["checkbounds(%N,%N,%N,%N);"]
301local s_nothing          = "(origin scaled 10)"
302
303local sc                 = 10
304local fc                 = number.dimenfactors.bp
305
306-- handle compact mode here:
307
308local function glyph(kind,font,char,advance,shift,ex,s,sx,sy)
309    local character = fontcharacters[font][char]
310    if character then
311        local index = character.index
312        if index then
313            local shapedata = fontshapes[font]
314            local glyphs    = shapedata.glyphs
315            if glyphs then
316                local glyf = glyphs[index]
317                if glyf then
318                    local units    = 1000 -- factor already takes shapedata.units into account
319                    local yfactor  = (sc/units) * fontparameters[font].factor / 655.36
320                    local xfactor  = yfactor
321                    local shift    = shift or 0
322                    local advance  = advance or 0
323                    local exfactor = ex or 0
324                    local wfactor  = 1
325                    local detail   = kind == "p" and tostring(char) or ""
326                    -- what about other effects
327                    local xoffset  = character.xoffset or 0
328                    local yoffset  = character.yoffset or 0 -- todo
329                    --
330                    if exfactor ~= 0 then
331                        wfactor = (1+(ex/units)/1000)
332                        xfactor = xfactor * wfactor
333                    end
334                    if xoffset ~= 0 then
335                        advance = advance + s * sx * xoffset * fc / 1000000
336                    end
337                    if yoffset ~= 0 then
338                        shift = shift + s * sy * yoffset * fc / 1000000
339                    end
340                    if s then
341                        xfactor = (s/1000) * ((sx or 1000)/1000) * xfactor
342                        yfactor = (s/1000) * ((sy or 1000)/1000) * yfactor
343                    end
344                    local paths = topaths(glyf,xfactor,yfactor)
345                    if paths then
346                        return f_text(kind,#paths,advance,shift,detail,paths) -- , character.width * fc * wfactor
347                    end
348                end
349            end
350        end
351    end
352end
353
354metapost.glyph = glyph
355
356local kind   = ""
357local buffer = { }
358local b      = 0
359
360local function reset()
361    buffer = { }
362    b      = 0
363end
364
365local function flushcharacter(current, pos_h, pos_v, pod_r, font, char)
366    if current then
367        local char, font = isglyph(current)
368        local s, sx, sy = getscales(current)
369        local code = glyph(kind,font,char,pos_h*fc,pos_v*fc,getexpansion(current),s,sx,sy)
370        if code then
371            b = b + 1
372            buffer[b] = code
373        end
374    else
375        logs.report("mlib-fnt","check 'flushcharacter', no current, font %i, char %i", font or 0, char or 0)
376    end
377end
378
379-- Messy ... todo:
380
381local function flushcharacter(current,pos_h,pos_v,pos_r,font,char,data,csx,csy,factor,ssx,ssy)
382    if current then
383        local char, font = isglyph(current)
384        local s, sx, sy = getscales(current)
385        local code = glyph(kind,font,char,pos_h*fc,pos_v*fc,getexpansion(current),s,sx,sy)
386        if code then
387            b = b + 1
388            buffer[b] = code
389        end
390    elseif font and char then
391        if not data then
392            data = fontcharacters[font][char] or { }
393        end
394        if data then
395            local width  = data.width or 0
396            local height = data.height or 0
397            local depth  = data.depth or 0
398            local sx = 1
399            local sy = 1
400            if csx then sx = sx * csx end
401            if csy then sy = sy * csy end
402            if ssx then sx = sx * ssx end
403            if ssy then sy = sy * ssy end
404            local code = glyph(kind,font,char,pos_h*fc,pos_v*fc,factor,s,sx,sy)
405            if code then
406                b = b + 1
407                buffer[b] = code
408            end
409        else
410            logs.report("mlib-fnt","no font %i with char %i", font, char)
411        end
412     -- return width, height, depth
413    else
414        logs.report("mlib-fnt","no current, font, and/or char")
415    end
416end
417
418local function flushrule(current,pos_h,pos_v,pos_r,size_h,size_v,subtype)
419    if subtype == normalrule_code then
420        b = b + 1
421        buffer[b] = f_rule(kind,pos_h*fc,pos_v*fc,size_h*fc,size_v*fc)
422    elseif subtype == outlinerule_code then
423        b = b + 1
424        buffer[b] = f_rule("d",pos_h*fc,pos_v*fc,size_h*fc,size_v*fc)
425    elseif subtype == userrule_code then
426     -- print("USER RULE")
427     -- b = b + 1
428     -- buffer[b] = f_rule("d",size_h*fc,size_v*fc,pos_h*fc,pos_v*fc)
429    elseif subtype == emptyrule_code then
430        -- ignore
431    else
432     -- b = b + 1
433     -- buffer[b] = f_rule("f",pos_h*fc,pos_v*fc,size_h*fc,size_v*fc)
434    end
435end
436
437local function flushsimplerule(pos_h, pos_v, pos_r, size_h, size_v)
438    flushrule(false,pos_h,pos_v,pos_r,size_h,size_v,normalrule_code)
439end
440
441local function flushspecialrule(pos_h, pos_v, pos_r, w, h, d, l, outline)
442    flushrule(false,pos_h,pos_v-d,pos_r,w,h+d,outline and outlinerule_code or normalrule_code)
443end
444
445-- installer
446
447drivers.install {
448    name    = "mpo",
449    actions = {
450        initialize = function()
451            reset()
452        end,
453        finalize = function(driver,details)
454            local bb  = details.boundingbox
455            local llx = bb[1] * fc
456            local lly = bb[2] * fc
457            local urx = bb[3] * fc
458            local ury = bb[4] * fc
459            b = b + 1
460            buffer[b] = f_bounds(llx,lly,urx,ury)
461         -- inspect(buffer)
462        end,
463    },
464    flushers = {
465        updatefontstate = updatefontstate,
466        character       = flushcharacter,
467        rule            = flushrule,
468        simplerule      = flushsimplerule,
469        specialrule     = flushspecialrule,
470    }
471}
472
473function metapost.boxtomp(n,k)
474    kind = k
475    nodes.handlers.finalizebox(n,false)
476    drivers.converters.lmtx(drivers.instances.mpo,texgetbox(n),"box",1)
477    local result = concat(buffer,";")
478    reset()
479    return result
480end
481
482-- This is a new set of commands:
483
484local loaded = table.setmetatableindex(function(t,k)
485    local v = fonts.definers.internal({ name = k } ,"<lmt:glyphshape:font>")
486    t[k] = v
487    return v
488end)
489
490local mpdata  = 0
491local mpstack = { }
492
493function mp.lmt_glyphshape_start(id,character)
494    if type(id) == "string" then
495        id = loaded[id]
496    end
497    local fontid       = (id and id ~= 0 and id) or font.current()
498    local shapedata    = fontshapes      [fontid] -- by index
499    local characters   = fontcharacters  [fontid] -- by unicode
500    local descriptions = fontdescriptions[fontid] -- by unicode
501    local mathgaps     = mathematics.gaps -- not yet loaded
502    local shapeglyphs  = shapedata.glyphs or { }
503    if type(character) == "string" and character ~= "" then
504        local hex = match(character,"^0x(.+)")
505        if hex then
506            character = tonumber(hex,16)
507        else
508            character = utfbyte(character)
509        end
510    else
511        character = tonumber(character)
512    end
513    local unicode  = mathgaps[character] or character
514    local chardata = characters[unicode]
515    local descdata = descriptions[unicode]
516    if chardata then
517        glyph = shapeglyphs[chardata.index]
518        if glyph then
519            mpdata = glyph.mpdata
520            if not mpdata then
521                if glyph.segments or glyph.sequence then
522                    local units  = shapedata.units or 1000
523                    local factor = 100/units
524                    local width  = (descdata.width or 0)  * factor
525                    local height = descdata.boundingbox[4] * factor
526                    local depth  = descdata.boundingbox[2] * factor
527                    local llx    = descdata.boundingbox[1] * factor
528                    local math   = descdata.math
529                    local italic = (math and math.italic or 0) * factor
530                    local accent = (math and math.accent or 0) * factor
531                    mpdata = {
532                        paths       = metapost.paths(glyph,factor),
533                        boundingbox = metapost.boundingbox(glyph,factor),
534                        baseline    = metapost.baseline(glyph,factor),
535                        width       = width,
536                        height      = height,
537                        depth       = depth,
538                        italic      = italic,
539                        accent      = accent,
540                        llx         = llx,
541                        usedbox     = f_rectangle(llx,depth,llx+width,depth,llx+width,height,llx,height),
542                        usedline    = f_line(llx,0,llx+width,0),
543                    }
544                    glyph.mpdata = mpdata
545                else
546                    print("CHECK 1",id,character)
547                end
548            end
549        end
550    else
551        print("CHECK 2",id,character)
552    end
553    insert(mpstack, mpdata)
554end
555
556local mpprint       = mp.print
557local injectpair    = mp.inject.pair
558local injectnumeric = mp.inject.numeric
559
560function mp.lmt_glyphshape_stop()
561    mpdata = remove(mpstack)
562end
563
564function mp.lmt_glyphshape_n()
565    if mpdata then
566        mpprint(#mpdata.paths)
567    else
568        injectnumeric(0)
569    end
570end
571
572function mp.lmt_glyphshape_path(i)
573    if mpdata then
574        mpprint(mpdata.paths[i])
575    else
576        injectpair(0,0)
577    end
578end
579
580function mp.lmt_glyphshape_boundingbox()
581    if mpdata then
582        mpprint(mpdata.boundingbox)
583    else
584        injectpair(0,0)
585    end
586end
587function mp.lmt_glyphshape_usedbox()
588    if mpdata then
589        mpprint(mpdata.usedbox)
590    else
591        injectpair(0,0)
592    end
593end
594
595function mp.lmt_glyphshape_baseline()
596    if mpdata then
597        mpprint(mpdata.baseline)
598    else
599        injectpair(0,0)
600    end
601end
602function mp.lmt_glyphshape_usedline()
603    if mpdata then
604        mpprint(mpdata.usedline)
605    else
606        injectpair(0,0)
607    end
608end
609
610function mp.lmt_glyphshape_width () injectnumeric(mpdata and mpdata.width  or 0) end
611function mp.lmt_glyphshape_depth () injectnumeric(mpdata and mpdata.depth  or 0) end
612function mp.lmt_glyphshape_height() injectnumeric(mpdata and mpdata.height or 0) end
613function mp.lmt_glyphshape_italic() injectnumeric(mpdata and mpdata.italic or 0) end
614function mp.lmt_glyphshape_accent() injectnumeric(mpdata and mpdata.accent or 0) end
615function mp.lmt_glyphshape_llx   () injectnumeric(mpdata and mpdata.llx    or 0) end
616