mlib-pps.lmt /size: 61 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['mlib-pps'] = {
2    version   = 1.001,
3    comment   = "companion to mlib-ctx.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 next = next
10local format, gmatch, match, split, gsub = string.format, string.gmatch, string.match, string.split, string.gsub
11local tonumber, type, unpack, next, select = tonumber, type, unpack, next, select
12local round, sqrt, min, max, abs = math.round, math.sqrt, math.min, math.max, math.abs
13local insert, remove, concat = table.insert, table.remove, table.concat
14local Cs, Cf, C, Cg, Ct, P, S, V, Carg = lpeg.Cs, lpeg.Cf, lpeg.C, lpeg.Cg, lpeg.Ct, lpeg.P, lpeg.S, lpeg.V, lpeg.Carg
15local lpegmatch, tsplitat, tsplitter = lpeg.match, lpeg.tsplitat, lpeg.tsplitter
16local formatters, toboolean = string.formatters, string.toboolean
17local stepper = utilities.parsers.stepper
18
19local mplib                = mplib
20local metapost             = metapost
21local lpdf                 = lpdf
22local context              = context
23
24local scan                 = mp.scan
25local inject               = mp.inject
26
27local mpscanstring         = scan.string
28local mpscannumeric        = scan.numeric
29
30local injecttriplet        = inject.triplet
31
32local registerdirect       = metapost.registerdirect
33
34local implement            = interfaces.implement
35local setmacro             = interfaces.setmacro
36
37local texsetbox            = tex.setbox
38local textakebox           = tex.takebox -- or: nodes.takebox
39local texrunlocal          = tex.runlocal
40local copylist             = nodes.copylist
41local flushlist            = nodes.flushlist
42local setmetatableindex    = table.setmetatableindex
43local sortedhash           = table.sortedhash
44
45local new_hlist            = nodes.pool.hlist
46
47local starttiming          = statistics.starttiming
48local stoptiming           = statistics.stoptiming
49
50local trace_runs           = false  trackers.register("metapost.runs",     function(v) trace_runs     = v end)
51local trace_textexts       = false  trackers.register("metapost.textexts", function(v) trace_textexts = v end)
52local trace_scripts        = false  trackers.register("metapost.scripts",  function(v) trace_scripts  = v end)
53local trace_btexetex       = false  trackers.register("metapost.btexetex", function(v) trace_btexetex = v end)
54
55local report_metapost      = logs.reporter("metapost")
56local report_textexts      = logs.reporter("metapost","textexts")
57local report_scripts       = logs.reporter("metapost","scripts")
58
59local colors               = attributes.colors
60local defineprocesscolor   = colors.defineprocesscolor
61local definespotcolor      = colors.definespotcolor
62local definemultitonecolor = colors.definemultitonecolor
63local colorvalue           = colors.value
64
65local transparencies       = attributes.transparencies
66local registertransparency = transparencies.register
67local transparencyvalue    = transparencies.value
68
69local rgbtocmyk            = colors.rgbtocmyk  -- or function() return 0,0,0,1 end
70local cmyktorgb            = colors.cmyktorgb  -- or function() return 0,0,0   end
71local rgbtogray            = colors.rgbtogray  -- or function() return 0       end
72local cmyktogray           = colors.cmyktogray -- or function() return 0       end
73
74local nooutercolor         = "0 g 0 G"
75local nooutertransparency  = "/Tr0 gs" -- only when set
76local outercolormode       = 0
77local outercolormodel      = 1
78local outercolor           = nooutercolor
79local outertransparency    = nooutertransparency
80local innercolor           = nooutercolor
81local innertransparency    = nooutertransparency
82
83local pdfcolor             = lpdf.color
84local pdftransparency      = lpdf.transparency
85
86function metapost.setoutercolor(mode,colormodel,colorattribute,transparencyattribute)
87    -- has always to be called before conversion
88    -- todo: transparency (not in the mood now)
89    outercolormode  = mode
90    outercolormodel = colormodel
91    if mode == 1 or mode == 3 then
92        -- inherit from outer (registered color)
93        outercolor        = pdfcolor(colormodel,colorattribute)    or nooutercolor
94        outertransparency = pdftransparency(transparencyattribute) or nooutertransparency
95    elseif mode == 2 then
96        -- stand alone (see m-punk.tex)
97        outercolor        = ""
98        outertransparency = ""
99    else -- 0
100        outercolor        = nooutercolor
101        outertransparency = nooutertransparency
102    end
103    innercolor        = outercolor
104    innertransparency = outertransparency -- not yet used
105end
106
107-- todo: get this from the lpdf module
108
109local f_f     = formatters["%.6N"]
110local f_f3    = formatters["%.3N"]
111local f_gray  = formatters["%.3N g %.3N G"]
112local f_rgb   = formatters["%.3N %.3N %.3N rg %.3N %.3N %.3N RG"]
113local f_cmyk  = formatters["%.3N %.3N %.3N %.3N k %.3N %.3N %.3N %.3N K"]
114local f_cm    = formatters["%.6N %.6N %.6N %.6N %.6N %.6N cm"]
115local f_cm_b  = formatters["q %.6N %.6N %.6N %.6N %.6N %.6N cm"]
116
117local f_scn   = formatters["%.3N"]
118
119local f_shade = formatters["MpSh%s"]
120local f_spot  = formatters["/%s cs /%s CS %s SCN %s scn"]
121
122local s_cm_e <const> = "Q"
123
124local function checked_color_pair(color,...)
125    if not color then
126        return innercolor, outercolor
127    end
128    if outercolormode == 3 then
129        innercolor = color(...)
130        return innercolor, innercolor
131    else
132        return color(...), outercolor
133    end
134end
135
136function metapost.colorinitializer()
137    innercolor = outercolor
138    innertransparency = outertransparency
139    return outercolor, outertransparency
140end
141
142--~
143
144local specificationsplitter = tsplitat(" ")
145local colorsplitter         = tsplitter(":",tonumber) -- no need for :
146local domainsplitter        = tsplitter(" ",tonumber)
147local centersplitter        = domainsplitter
148local coordinatesplitter    = domainsplitter
149
150-- thanks to taco's reading of the postscript manual:
151--
152-- x' = sx * x + ry * y + tx
153-- y' = rx * x + sy * y + ty
154
155local nofshades = 0 -- todo: hash resources, start at 1000 in order not to clash with older
156
157local function normalize(ca,cb)
158    if #cb == 1 then
159        if #ca == 4 then
160            cb[1], cb[2], cb[3], cb[4] = 0, 0, 0, 1-cb[1]
161        else
162            cb[1], cb[2], cb[3] = cb[1], cb[1], cb[1]
163        end
164    elseif #cb == 3 then
165        if #ca == 4 then
166            cb[1], cb[2], cb[3], cb[4] = rgbtocmyk(cb[1],cb[2],cb[3])
167        else
168            cb[1], cb[2], cb[3] = cmyktorgb(cb[1],cb[2],cb[3],cb[4])
169        end
170    end
171end
172
173
174local commasplitter = tsplitat(",")
175
176local function checkandconvertspot(n_a,f_a,c_a,v_a,n_b,f_b,c_b,v_b)
177    -- must be the same but we don't check
178    local name = f_shade(nofshades)
179    local ca = lpegmatch(commasplitter,v_a)
180    local cb = lpegmatch(commasplitter,v_b)
181    if #ca == 0 or #cb == 0 then
182        return { 0 }, { 1 }, "DeviceGray", name
183    else
184        for i=1,#ca do ca[i] = tonumber(ca[i]) or 0 end
185        for i=1,#cb do cb[i] = tonumber(cb[i]) or 1 end
186    --~ spotcolorconverter(n_a,f_a,c_a,v_a) -- not really needed
187        return ca, cb, n_a or n_b, name
188    end
189end
190
191local function checkandconvert(ca,cb,model)
192    local name = f_shade(nofshades)
193    if not ca or not cb or type(ca) == "string" then
194        return { 0 }, { 1 }, "DeviceGray", name
195    else
196        if #ca > #cb then
197            normalize(ca,cb)
198        elseif #ca < #cb then
199            normalize(cb,ca)
200        end
201        if not model then
202            model = colors.currentnamedmodel()
203        end
204        if model == "all" then
205            model= (#ca == 4 and "cmyk") or (#ca == 3 and "rgb") or "gray"
206        end
207        if model == "rgb" then
208            if #ca == 4 then
209                ca = { cmyktorgb(ca[1],ca[2],ca[3],ca[4]) }
210                cb = { cmyktorgb(cb[1],cb[2],cb[3],cb[4]) }
211            elseif #ca == 1 then
212                local a = 1 - ca[1]
213                local b = 1 - cb[1]
214                ca = { a, a, a }
215                cb = { b, b, b }
216            end
217            return ca, cb, "DeviceRGB", name, model
218        elseif model == "cmyk" then
219            if #ca == 3 then
220                ca = { rgbtocmyk(ca[1],ca[2],ca[3]) }
221                cb = { rgbtocmyk(cb[1],cb[2],cb[3]) }
222            elseif #ca == 1 then
223                ca = { 0, 0, 0, ca[1] }
224                cb = { 0, 0, 0, ca[1] }
225            end
226            return ca, cb, "DeviceCMYK", name, model
227        else
228            if #ca == 4 then
229                ca = { cmyktogray(ca[1],ca[2],ca[3],ca[4]) }
230                cb = { cmyktogray(cb[1],cb[2],cb[3],cb[4]) }
231            elseif #ca == 3 then
232                ca = { rgbtogray(ca[1],ca[2],ca[3]) }
233                cb = { rgbtogray(cb[1],cb[2],cb[3]) }
234            end
235            -- backend specific (will be renamed)
236            return ca, cb, "DeviceGray", name, model
237        end
238    end
239end
240
241-- We keep textexts in a shared list (as it's easier that way and we also had that in
242-- the beginning). Each graphic gets its own (1 based) subtable so that we can also
243-- handle multiple conversions in one go which is needed when we process mp files
244-- directly.
245
246local stack   = { } -- quick hack, we will pass topofstack around
247local top     = nil
248local nofruns = 0 -- askedfig: "all", "first", number
249
250local function preset(t,k)
251    -- references to textexts by mp index
252    local v = {
253        textrial = 0,
254        texfinal = 0,
255        texslots = { },
256        texorder = { },
257        texhash  = { },
258    }
259    t[k] = v
260    return v
261end
262
263-- todo: nested startMPcode .. stopMPcode does weird
264
265local function startjob(plugmode,kind,mpx)
266    insert(stack,top)
267    top = {
268        textexts   = { },                          -- all boxes, optionally with a different color
269        texstrings = { },
270        texregimes = { },
271        mapstrings = { },
272        mapindices = { },
273        mapmoves   = { },
274        texlast    = 0,
275        texdata    = setmetatableindex({},preset), -- references to textexts in order or usage
276        plugmode   = plugmode,                     -- some day we can then skip all pre/postscripts
277        extradata  = mpx and metapost.getextradata(mpx),
278    }
279    if trace_runs then
280        report_metapost("starting %s run at level %i in %s mode",
281            kind,#stack+1,plugmode and "plug" or "normal")
282    end
283    return top
284end
285
286local function stopjob()
287    if top then
288        for slot, content in next, top.textexts do
289            if content then
290                flushlist(content)
291                if trace_textexts then
292                    report_textexts("freeing text %s",slot)
293                end
294            end
295        end
296        if trace_runs then
297            report_metapost("stopping run at level %i",#stack+1)
298        end
299        top = remove(stack)
300        return top
301    end
302end
303
304function metapost.getjobdata()
305    return top
306end
307
308-- end of new
309
310local settext = function(box,slot,str)
311    if top then
312     -- if trace_textexts then
313     --     report_textexts("getting text %s from box %s",slot,box)
314     -- end
315        top.textexts[slot] = textakebox(box)
316    end
317end
318
319local gettext = function(box,slot)
320    if top then
321        texsetbox(box,top.textexts[slot])
322        top.textexts[slot] = false
323     -- if trace_textexts then
324     --     report_textexts("putting text %s in box %s",slot,box)
325     -- end
326    end
327end
328
329metapost.settext = settext
330metapost.gettext = gettext
331
332implement { name = "mpsettext", actions = settext, arguments = { "integer", "integer" } } -- box slot
333implement { name = "mpgettext", actions = gettext, arguments = { "integer", "integer" } } -- box slot
334
335-- rather generic pdf, so use this elsewhere too it no longer pays
336-- off to distinguish between outline and fill (we now have both
337-- too, e.g. in arrows)
338
339metapost.reducetogray = true
340
341local models = { }
342
343function models.all(cr)
344    local n = #cr
345    if n == 0 then
346        return checked_color_pair()
347    elseif metapost.reducetogray then
348        if n == 1 then
349            local s = cr[1]
350            return checked_color_pair(f_gray,s,s)
351        elseif n == 3 then
352            local r = cr[1]
353            local g = cr[2]
354            local b = cr[3]
355            if r == g and g == b then
356                return checked_color_pair(f_gray,r,r)
357            else
358                return checked_color_pair(f_rgb,r,g,b,r,g,b)
359            end
360        else
361            local c = cr[1]
362            local m = cr[2]
363            local y = cr[3]
364            local k = cr[4]
365            if c == m and m == y and y == 0 then
366                k = 1 - k
367                return checked_color_pair(f_gray,k,k)
368            else
369                return checked_color_pair(f_cmyk,c,m,y,k,c,m,y,k)
370            end
371        end
372    elseif n == 1 then
373        local s = cr[1]
374        return checked_color_pair(f_gray,s,s)
375    elseif n == 3 then
376        local r = cr[1]
377        local g = cr[2]
378        local b = cr[3]
379        return checked_color_pair(f_rgb,r,g,b,r,g,b)
380    else
381        local c = cr[1]
382        local m = cr[2]
383        local y = cr[3]
384        local k = cr[4]
385        return checked_color_pair(f_cmyk,c,m,y,k,c,m,y,k)
386    end
387end
388
389function models.rgb(cr)
390    local n = #cr
391    if n == 0 then
392        return checked_color_pair()
393    elseif metapost.reducetogray then
394        if n == 1 then
395            local s = cr[1]
396            return checked_color_pair(f_gray,s,s)
397        elseif n == 3 then
398            local r = cr[1]
399            local g = cr[2]
400            local b = cr[3]
401            if r == g and g == b then
402                return checked_color_pair(f_gray,r,r)
403            else
404                return checked_color_pair(f_rgb,r,g,b,r,g,b)
405            end
406        else
407            local c = cr[1]
408            local m = cr[2]
409            local y = cr[3]
410            local k = cr[4]
411            if c == m and m == y and y == 0 then
412                k = 1 - k
413                return checked_color_pair(f_gray,k,k)
414            else
415                local r, g, b = cmyktorgb(c,m,y,k)
416                return checked_color_pair(f_rgb,r,g,b,r,g,b)
417            end
418        end
419    elseif n == 1 then
420        local s = cr[1]
421        return checked_color_pair(f_gray,s,s)
422    else
423        local r = cr[1]
424        local g = cr[2]
425        local b = cr[3]
426        local r, g, b
427        if n == 3 then
428            r, g, b = cmyktorgb(r,g,b,cr[4])
429        end
430        return checked_color_pair(f_rgb,r,g,b,r,g,b)
431    end
432end
433
434function models.cmyk(cr)
435    local n = #cr
436    if n == 0 then
437        return checked_color_pair()
438    elseif metapost.reducetogray then
439        if n == 1 then
440            local s = cr[1]
441            return checked_color_pair(f_gray,s,s)
442        elseif n == 3 then
443            local r = cr[1]
444            local g = cr[2]
445            local b = cr[3]
446            if r == g and g == b then
447                return checked_color_pair(f_gray,r,r)
448            else
449                local c, m, y, k = rgbtocmyk(r,g,b)
450                return checked_color_pair(f_cmyk,c,m,y,k,c,m,y,k)
451            end
452        else
453            local c = cr[1]
454            local m = cr[2]
455            local y = cr[3]
456            local k = cr[4]
457            if c == m and m == y and y == 0 then
458                k = 1 - k
459                return checked_color_pair(f_gray,k,k)
460            else
461                return checked_color_pair(f_cmyk,c,m,y,k,c,m,y,k)
462            end
463        end
464    elseif n == 1 then
465        local s = cr[1]
466        return checked_color_pair(f_gray,s,s)
467    else
468        local c = cr[1]
469        local m = cr[2]
470        local y = cr[3]
471        local k = cr[4]
472        if n == 3 then
473            if c == m and m == y then
474                k, c, m, y = 1 - c, 0, 0, 0
475            else
476                c, m, y, k = rgbtocmyk(c,m,y)
477            end
478        end
479        return checked_color_pair(f_cmyk,c,m,y,k,c,m,y,k)
480    end
481end
482
483function models.gray(cr)
484    local n = #cr
485    local s = 0
486    if n == 0 then
487        return checked_color_pair()
488    elseif n == 4 then
489        s = cmyktogray(cr[1],cr[2],cr[3],cr[4])
490    elseif n == 3 then
491        s = rgbtogray(cr[1],cr[2],cr[3])
492    else
493        s = cr[1]
494    end
495    return checked_color_pair(f_gray,s,s)
496end
497
498models[1] = models.all
499models[2] = models.gray
500models[3] = models.rgb
501models[4] = models.cmyk
502
503setmetatableindex(models, function(t,k)
504    local v = models.gray
505    t[k] = v
506    return v
507end)
508
509local function colorconverter(cs)
510    return models[outercolormodel](cs)
511end
512
513local factor = 65536*(7227/7200)
514
515implement {
516    name       = "mpsetsxsy",
517    arguments  = { "dimension", "dimension", "dimension" },
518    actions    = function(wd,ht,dp)
519        local hd = ht + dp
520        setmacro("mlib_sx",wd ~= 0 and factor/wd or 0)
521        setmacro("mlib_sy",hd ~= 0 and factor/hd or 0)
522    end
523}
524
525local function sxsy(wd,ht,dp) -- helper for text
526    local hd = ht + dp
527    return (wd ~= 0 and factor/wd) or 0, (hd ~= 0 and factor/hd) or 0
528end
529
530metapost.sxsy = sxsy
531
532-- for stock mp we need to declare the booleans first
533
534local do_begin_fig <const> = "; beginfig(1) ; "
535local do_end_fig   <const> = "; endfig ;"
536local do_safeguard <const> = ";"
537
538function metapost.preparetextextsdata()
539    local textexts  = top.textexts
540    local collected = { }
541    for k, data in sortedhash(top.texdata) do -- sort is nicer in trace
542        local texorder = data.texorder
543        for n=1,#texorder do
544            local box = textexts[texorder[n]]
545            if box then
546                collected[n] = box
547            else
548                break
549            end
550        end
551    end
552    mp.mf_tt_initialize(collected)
553end
554
555local runmetapost = metapost.run
556
557local function checkaskedfig(askedfig) -- return askedfig, wrappit
558    if not askedfig then
559        return "direct", true
560    elseif askedfig == "all" then
561        return "all", false
562    elseif askedfig == "direct" then
563        return "all", true
564    else
565        askedfig = tonumber(askedfig)
566        if askedfig then
567            return askedfig, false
568        else
569            return "direct", true
570        end
571    end
572end
573
574-- This one is called from the \TEX\ end so the specification is different
575-- from the specification to metapost.run cum suis! The definitions and
576-- extension used to be handled here but are now delegated to the format
577-- initializers because we need to accumulate them for nested instances (a
578-- side effect of going single pass).
579
580function metapost.graphic_base_pass(specification)
581    local mpx             = specification.mpx -- mandate
582    local top             = startjob(true,"base",mpx)
583    local data            = specification.data or ""
584    local inclusions      = specification.inclusions or ""
585    local filtering       = specification.filtering
586    local initializations = specification.initializations or ""
587    local askedfig,
588          wrappit         = checkaskedfig(specification.figure)
589    nofruns               = nofruns + 1
590    top.askedfig          = askedfig
591    top.wrappit           = wrappit
592    top.nofruns           = nofruns
593    metapost.namespace    = specification.namespace or ""
594    top.mpx               = mpx
595    top.data              = data
596    top.initializations   = initializations
597    if filtering then
598        if #filtering > 0 then
599            local t = typesetters.stacking.getselection(filtering)
600            filtering = next(t) and t or false
601        else
602            filtering = false
603        end
604    end
605    if trace_runs then
606        report_metapost("running job %s, asked figure %a",nofruns,askedfig)
607    end
608    runmetapost {
609        mpx         = mpx,
610        askedfig    = askedfig,
611        incontext   = true,
612        filtering   = filtering,
613        data        = {
614            inclusions,
615            wrappit and do_begin_fig or "",
616            initializations,
617            do_safeguard,
618            data,
619            wrappit and do_end_fig or "",
620        },
621    }
622    context(stopjob)
623end
624
625local function oldschool(mpx, data, trial_run, flusher, was_multi_pass, is_extra_pass, askedfig, incontext)
626    metapost.process {
627        mpx        = mpx,
628        flusher    = flusher,
629        askedfig   = askedfig,
630        useplugins = incontext,
631        incontext  = incontext,
632        data       = data,
633    }
634end
635
636function metapost.process(specification,...)
637    if type(specification) ~= "table" then
638        oldschool(specification,...)
639    else
640        startjob(specification.incontext or specification.useplugins,"process",false)
641        runmetapost(specification)
642        stopjob()
643    end
644end
645
646-- -- the new plugin handler -- --
647
648local sequencers       = utilities.sequencers
649local appendgroup      = sequencers.appendgroup
650local appendaction     = sequencers.appendaction
651
652local resetteractions  = sequencers.new { arguments = "t" }
653local processoractions = sequencers.new { arguments = "object,prescript,before,after" }
654
655appendgroup(resetteractions, "system")
656appendgroup(processoractions,"system")
657
658-- later entries come first
659
660local scriptsplitter = Ct ( Ct (
661    C((1-S("= "))^1) * S("= ")^1 * C((1-S("\n\r"))^0) * S("\n\r")^0
662)^0 )
663
664local function splitprescript(script)
665    local hash = lpegmatch(scriptsplitter,script)
666    for i=#hash,1,-1 do
667        local h = hash[i]
668        if h == "reset" then
669            for k, v in next, hash do
670                if type(k) ~= "number" then
671                    hash[k] = nil
672                end
673            end
674        else
675            hash[h[1]] = h[2]
676        end
677    end
678    if trace_scripts then
679        report_scripts(table.serialize(hash,"prescript"))
680    end
681    return hash
682end
683
684metapost.splitprescript = splitprescript
685
686-- -- not used:
687--
688-- local function splitpostscript(script)
689--     local hash = lpegmatch(scriptsplitter,script)
690--     for i=1,#hash do
691--         local h = hash[i]
692--         hash[h[1]] = h[2]
693--     end
694--     if trace_scripts then
695--         report_scripts(table.serialize(hash,"postscript"))
696--     end
697--     return hash
698-- end
699
700function metapost.pluginactions(what,t,flushfigure) -- before/after object, depending on what
701    if top and top.plugmode then -- hm, what about other features
702        for i=1,#what do
703            local wi = what[i]
704            if type(wi) == "function" then
705                -- assume injection
706                flushfigure(t) -- to be checked: too many 0 g 0 G
707                t = { }
708                wi()
709            else
710                t[#t+1] = wi
711            end
712        end
713        return t
714    end
715end
716
717function metapost.resetplugins(t) -- intialize plugins, before figure
718    if top and top.plugmode then
719        outercolormodel = colors.currentmodel() -- currently overloads the one set at the tex end
720        resetteractions.runner(t)
721    end
722end
723
724function metapost.processplugins(object) -- each object (second pass)
725    if top and top.plugmode then
726        local prescript = object.prescript   -- specifications
727        if prescript and #prescript > 0 then
728            local before  = { }
729            local after   = { }
730            local options = splitprescript(prescript) or { }
731            processoractions.runner(object,options,before,after)
732            return #before > 0 and before, #after > 0 and after, options
733        else
734            local c = object.color
735            if c and #c > 0 then
736                local b, a = colorconverter(c)
737                return { b }, { a }, { }
738            end
739        end
740    end
741end
742
743-- helpers
744
745local basepoints = number.dimenfactors["bp"]
746
747local function cm(object)
748    local op = object.path
749    if op then
750        local first  = op[1]
751        local second = op[2]
752        local fourth = op[4]
753        if fourth then
754            local tx = first.x_coord
755            local ty = first.y_coord
756            local sx = second.x_coord - tx
757            local sy = fourth.y_coord - ty
758            local rx = second.y_coord - ty
759            local ry = fourth.x_coord - tx
760            if sx == 0 then sx = 0.00001 end
761            if sy == 0 then sy = 0.00001 end
762            return sx, rx, ry, sy, tx, ty -- different order elsewhere !
763        end
764    end
765    return 1, 0, 0, 1, 0, 0 -- weird case
766end
767
768metapost.cm = cm
769
770-- color
771
772local function cl_reset(t)
773    t[#t+1] = metapost.colorinitializer() -- only color
774end
775
776-- text
777
778local tx_reset, tx_process  do
779
780    -- The dilemma is that we need to process the text in order to know the
781    -- dimensions but afterwards also need to apply color and such. So, we need
782    -- two passes, one immediately and one when we use it.
783
784    local eol      = S("\n\r")^1
785    local cleaner  = Cs((P("@@")/"@" + P("@")/"%%" + P(1))^0)
786    local splitter = Ct(
787        ( (
788            P("s:") * C((1-eol)^1)
789          + P("n:") *  ((1-eol)^1/tonumber)
790          + P("b:") *  ((1-eol)^1/toboolean)
791        ) * eol^0 )^0)
792
793    local function applyformat(s)
794        local t = lpegmatch(splitter,s)
795        if #t == 1 then
796            return s
797        else
798            local f = lpegmatch(cleaner,t[1])
799            return formatters[f](unpack(t,2))
800        end
801    end
802
803    local fmt = formatters["%s %s %s % t"]
804    ----- pat = tsplitat(":")
805    local pat = lpeg.tsplitter(":",tonumber) -- so that %F can do its work
806
807    local f_gray_yes = formatters["s=%.3N,a=%i,t=%.3N"]
808    local f_gray_nop = formatters["s=%.3N"]
809    local f_rgb_yes  = formatters["r=%.3N,g=%.3N,b=%.3N,a=%.3N,t=%.3N"]
810    local f_rgb_nop  = formatters["r=%.3N,g=%.3N,b=%.3N"]
811    local f_cmyk_yes = formatters["c=%.3N,m=%.3N,y=%.3N,k=%.3N,a=%.3N,t=%.3N"]
812    local f_cmyk_nop = formatters["c=%.3N,m=%.3N,y=%.3N,k=%.3N"]
813
814    local ctx_MPLIBsetNtext = context.MPLIBsetNtextX
815    local ctx_MPLIBsetCtext = context.MPLIBsetCtextX
816    local ctx_MPLIBsettext  = context.MPLIBsettextX
817
818    local bp = number.dimenfactors.bp
819
820    local mp_index  = 0
821    local mp_target = 0
822    local mp_c      = nil
823    local mp_a      = nil
824    local mp_t      = nil
825
826    local function processtext()
827        local mp_text   = top.texstrings[mp_index]
828        local mp_regime = top.texregimes[mp_index]
829        if mp_regime and tonumber(mp_regime) >= 0 then
830            mp_text = function()
831                context.sprint(mp_regime,top.texstrings[mp_index] or "")
832            end
833        end
834        if not mp_text then
835            report_textexts("missing text for index %a",mp_index)
836        elseif not mp_c then
837            ctx_MPLIBsetNtext(mp_target,mp_text)
838        elseif #mp_c == 1 then
839            if mp_a and mp_t then
840                ctx_MPLIBsetCtext(mp_target,f_gray_yes(mp_c[1],mp_a,mp_t),mp_text)
841            else
842                ctx_MPLIBsetCtext(mp_target,f_gray_nop(mp_c[1]),mp_text)
843            end
844        elseif #mp_c == 3 then
845            if mp_a and mp_t then
846                ctx_MPLIBsetCtext(mp_target,f_rgb_yes(mp_c[1],mp_c[2],mp_c[3],mp_a,mp_t),mp_text)
847            else
848                ctx_MPLIBsetCtext(mp_target,f_rgb_nop(mp_c[1],mp_c[2],mp_c[3]),mp_text)
849            end
850        elseif #mp_c == 4 then
851            if mp_a and mp_t then
852                ctx_MPLIBsetCtext(mp_target,f_cmyk_yes(mp_c[1],mp_c[2],mp_c[3],mp_c[4],mp_a,mp_t),mp_text)
853            else
854                ctx_MPLIBsetCtext(mp_target,f_cmyk_nop(mp_c[1],mp_c[2],mp_c[3],mp_c[4]),mp_text)
855            end
856        else
857            -- can't happen
858            ctx_MPLIBsetNtext(mp_target,mp_text)
859        end
860    end
861
862    local madetext  = nil
863    local fasttrack = false -- we loose colors done with withcolor when true
864
865    directives.register("metapost.text.fasttrack", function(v) fasttrack = v end)
866
867    -- We could set trialtypesetting when we don't fasttrac.
868
869    local function mf_some_text(index,str,regime)
870        mp_target = index
871        mp_index  = index
872        mp_c      = nil
873        mp_a      = nil
874        mp_t      = nil
875        top.texstrings[mp_index] = str
876        top.texregimes[mp_index] = regime or -1
877        if trace_textexts then
878            report_textexts("running mptextoks, case 1: %s",top.texstrings[mp_index])
879        end
880        texrunlocal("mptexttoks")
881        local box = textakebox("mptextbox")
882        top.textexts[mp_target] = box
883        injecttriplet(bp*box.width,bp*box.height,bp*box.depth)
884        madetext = nil
885    end
886
887    local function mf_made_text(index)
888        mf_some_text(index,madetext,catcodes.numbers.ctxcatcodes) -- btex/etex ..
889    end
890
891    registerdirect("sometextext", function() mf_some_text(mpscannumeric(),mpscanstring(),mpscannumeric()) end)
892    registerdirect("madetextext", function() mf_made_text(mpscannumeric()) end)
893
894    -- a label can be anything, also something mp doesn't like in strings
895    -- so we return an index instead
896
897    function metapost.processing()
898        return top and true or false
899    end
900
901    function metapost.remaptext(replacement)
902        if top then
903            local mapstrings = top.mapstrings
904            local mapindices = top.mapindices
905            local label      = replacement.label
906            local index      = 0
907            if label then
908                local found = mapstrings[label]
909                if found then
910                    setmetatableindex(found,replacement)
911                    index = found.index
912                else
913                    index = #mapindices + 1
914                    replacement.index = index
915                    mapindices[index] = replacement
916                    mapstrings[label] = replacement
917                end
918            end
919            return index
920        else
921            return 0
922        end
923    end
924
925    function metapost.remappedtext(what)
926        return top and (top.mapstrings[what] or top.mapindices[tonumber(what)])
927    end
928
929    function mp.mf_map_move(index)
930        mp.triplet(top.mapmoves[index])
931    end
932
933    function mp.mf_map_text(index,str,regime)
934        local map = top.mapindices[tonumber(str)]
935        if type(map) == "table" then
936            local text     = map.text
937            local overload = map.overload
938            local offset   = 0
939            local width    = 0
940            local where    = nil
941            --
942            mp_index = index
943            -- the image text
944            if overload then
945                top.texstrings[mp_index] = map.template or map.label or "error"
946                top.texregimes[mp_index] = regime or -1
947                if trace_textexts then
948                    report_textexts("running mptextoks, case 2",top.texstrings[mp_index])
949                end
950                texrunlocal("mptexttoks")
951                local box = textakebox("mptextbox") or new_hlist()
952                width = bp * box.width
953                where = overload.where
954            end
955            -- the real text
956            top.texstrings[mp_index] = overload and overload.text or text or "error"
957            top.texregimes[mp_index] = regime or -1
958            if trace_textexts then
959                report_textexts("running mptextoks, case 3",top.texstrings[mp_index])
960            end
961            texrunlocal("mptexttoks")
962            local box = textakebox("mptextbox") or new_hlist()
963            local twd = bp * box.width
964            local tht = bp * box.height
965            local tdp = bp * box.depth
966            -- the check
967            if where then
968                local scale = 1 --  / (map.scale or 1)
969                if where == "l" or where == "left" then
970                    offset = scale * (twd - width)
971                elseif where == "m" or where == "middle" then
972                    offset = scale * (twd - width) / 2
973                end
974            end
975            -- the result
976            top.textexts[mp_index] = box
977            top.mapmoves[mp_index] = { offset, map.dx or 0, map.dy or 0 }
978            --
979            mp.triplet(twd,tht,tdp)
980            madetext = nil
981            return
982        else
983            map = type(map) == "string" and map or str
984            return mf_some_text(index,context.escape(map) or map)
985        end
986    end
987
988    -- This is a bit messy. In regular metapost it's a kind of immediate replacement
989    -- so embedded btex ... etex is not really working as one would expect. We now have
990    -- a mix: it's immediate when we are at the outer level (rawmadetext) and indirect
991    -- (with the danger of stuff that doesn't work well in strings) when we are for
992    -- instance in a macro definition (rawtextext (pass back string)) ... of course one
993    -- should use textext so this is just a catch. When not in lmtx it's never immediate.
994
995    local reported   = false
996    local alwayswrap = false -- CONTEXTLMTXMODE <= 1 -- was always nil due to typo
997
998    function metapost.maketext(s,mode)
999        if not reported then
1000            reported = true
1001            report_metapost("use 'textext(.....)' instead of 'btex ..... etex'")
1002        end
1003        if mode and mode == 1 then
1004            if trace_btexetex then
1005                report_metapost("ignoring verbatimtex: [[%s]]",s)
1006            end
1007        elseif alwayswrap then
1008            if trace_btexetex then
1009                report_metapost("rewrapping btex ... etex [[%s]]",s)
1010            end
1011            return 'rawtextext("' .. gsub(s,'"','"&ditto&"') .. '")' -- nullpicture
1012        elseif metapost.currentmpxstatus() ~= 0 then
1013            if trace_btexetex then
1014                report_metapost("rewrapping btex ... etex at the outer level [[%s]]",s)
1015            end
1016            return 'rawtextext("' .. gsub(s,'"','"&ditto&"') .. '")' -- nullpicture
1017        else
1018            if trace_btexetex then
1019                report_metapost("handling btex ... etex: [[%s]]",s)
1020            end
1021         -- madetext = utilities.strings.collapse(s)
1022            madetext = s
1023            return "rawmadetext" -- is assuming immediate processing
1024        end
1025    end
1026
1027    function mp.mf_formatted_text(index,fmt,...)
1028        local t = { }
1029        for i=1,select("#",...) do
1030            local ti = select(i,...)
1031            if type(ti) ~= "table" then
1032                t[#t+1] = ti
1033            end
1034        end
1035        local f = lpegmatch(cleaner,fmt)
1036        local s = formatters[f](unpack(t)) or ""
1037        mf_some_text(index,s)
1038    end
1039
1040    interfaces.implement {
1041        name    = "mptexttoks",
1042        actions = processtext,
1043    }
1044
1045    tx_reset = function()
1046        if top then
1047            top.texhash = { }
1048            top.texlast = 0
1049        end
1050    end
1051
1052    tx_process = function(object,prescript,before,after)
1053        local data  = top.texdata[metapost.properties.number] -- the current figure number, messy
1054        local index = tonumber(prescript.tx_index)
1055        if index then
1056            if trace_textexts then
1057                report_textexts("using index %a",index)
1058            end
1059            --
1060            mp_c = object.color
1061            if #mp_c == 0 then
1062                local txc = prescript.tx_color
1063                if txc then
1064                    mp_c = lpegmatch(pat,txc)
1065                end
1066            end
1067            mp_a = tonumber(prescript.tr_alternative)
1068            mp_t = tonumber(prescript.tr_transparency)
1069            --
1070            mp_index  = index
1071            mp_target = top.texlast - 1
1072            top.texlast = mp_target
1073            --
1074            local mp_text = top.texstrings[mp_index]
1075            local mp_hash = prescript.tx_cache
1076            local box
1077            local donebox = fasttrack and top.textexts[mp_index]
1078            if mp_hash == "no" then
1079                if donebox then
1080                    box = copylist(donebox)
1081                else
1082                    if trace_textexts then
1083                        report_textexts("running mptextoks, case 4",top.texstrings[mp_index])
1084                    end
1085                    texrunlocal("mptexttoks")
1086                    box = textakebox("mptextbox")
1087                end
1088            else
1089                local cache = data.texhash
1090                if mp_hash then
1091                    mp_hash = tonumber(mp_hash)
1092                end
1093                if mp_hash then
1094                    local extradata = top.extradata
1095                    if extradata then
1096                        cache = extradata.globalcache
1097                        if not cache then
1098                            cache = { }
1099                            extradata.globalcache = cache
1100                        end
1101                        if trace_runs then
1102                            if cache[mp_hash] then
1103                                report_textexts("reusing global entry %i",mp_hash)
1104                            else
1105                                report_textexts("storing global entry %i",mp_hash)
1106                            end
1107                        end
1108                    else
1109                        mp_hash = nil
1110                    end
1111                end
1112                if not mp_hash then
1113                    mp_hash = fmt(mp_text,mp_a or "-",mp_t or "-",mp_c or "-")
1114                end
1115                box = cache[mp_hash]
1116                if box then
1117                    box = copylist(box)
1118                else
1119                    if donebox then
1120                        box = copylist(donebox)
1121                    else
1122                        if trace_textexts then
1123                            report_textexts("running mptextoks, case 5",top.texstrings[mp_index])
1124                        end
1125                        texrunlocal("mptexttoks")
1126                        box = textakebox("mptextbox")
1127                    end
1128                    cache[mp_hash] = box
1129                end
1130            end
1131            top.textexts[mp_target] = box
1132            --
1133            if box then
1134                -- we need to freeze the variables outside the function
1135                local sx, rx, ry, sy, tx, ty = cm(object)
1136                local target = mp_target
1137                before[#before+1] = function()
1138                    context.MPLIBgettextscaledcm(target,
1139                        f_f(sx), -- bah ... %s no longer checks
1140                        f_f(rx), -- bah ... %s no longer checks
1141                        f_f(ry), -- bah ... %s no longer checks
1142                        f_f(sy), -- bah ... %s no longer checks
1143                        f_f(tx), -- bah ... %s no longer checks
1144                        f_f(ty), -- bah ... %s no longer checks
1145                        sxsy(box.width,box.height,box.depth))
1146                end
1147            else
1148                before[#before+1] = function()
1149                    report_textexts("unknown %s",index)
1150                end
1151            end
1152            if not trace_textexts then
1153                object.path = false -- else: keep it
1154            end
1155            object.color   = false
1156            object.grouped = true
1157            object.istext  = true
1158        end
1159    end
1160
1161end
1162
1163-- we could probably redo normal textexts in the next way but as it's rather optimized
1164-- we keep away from that (at least for now)
1165
1166local function bx_process(object,prescript,before,after)
1167    local bx_category = prescript.bx_category
1168    local bx_name     = prescript.bx_name
1169    if bx_category and bx_name then
1170        if trace_textexts then
1171            report_textexts("category %a, name %a",bx_category,bx_name)
1172        end
1173        local sx, rx, ry, sy, tx, ty = cm(object) -- needs to be frozen outside the function
1174        local wd, ht, dp = nodes.boxes.dimensions(bx_category,bx_name)
1175        before[#before+1] = function()
1176            context.MPLIBgetboxscaledcm(bx_category,bx_name,
1177                f_f(sx), -- bah ... %s no longer checks
1178                f_f(rx), -- bah ... %s no longer checks
1179                f_f(ry), -- bah ... %s no longer checks
1180                f_f(sy), -- bah ... %s no longer checks
1181                f_f(tx), -- bah ... %s no longer checks
1182                f_f(ty), -- bah ... %s no longer checks
1183                sxsy(wd,ht,dp))
1184        end
1185        if not trace_textexts then
1186            object.path = false -- else: keep it
1187        end
1188        object.color   = false
1189        object.grouped = true
1190        object.istext  = true
1191    end
1192end
1193
1194-- graphics (we use the given index because pictures can be reused)
1195
1196
1197local gt_reset, gt_process do
1198
1199    local graphics = { }
1200
1201    local mp_index = 0
1202    local mp_str   = ""
1203
1204    function mp.mf_graphic_text(index,str)
1205        if not graphics[index] then
1206            mp_index = index
1207            mp_str   = str
1208            texrunlocal("mpgraphictexttoks")
1209        end
1210    end
1211
1212    interfaces.implement {
1213        name    = "mpgraphictexttoks",
1214        actions = function()
1215            context.MPLIBgraphictext(mp_index,mp_str)
1216        end,
1217    }
1218
1219end
1220
1221-- shades: transformation is for afterward transform so then one also need to inverse transform
1222-- the path (see svg)
1223
1224local function sh_process(object,prescript,before,after)
1225    local sh_type = prescript.sh_type
1226    if sh_type then
1227        nofshades = nofshades + 1
1228        local domain    = lpegmatch(domainsplitter,prescript.sh_domain   or "0 1")
1229        local centera   = lpegmatch(centersplitter,prescript.sh_center_a or "0 0")
1230        local centerb   = lpegmatch(centersplitter,prescript.sh_center_b or "0 0")
1231        local transform = toboolean(prescript.sh_transform or "yes",true)
1232        local transformation = prescript.sh_transformation
1233        if transformation then
1234            transformation = lpegmatch(coordinatesplitter,transformation)
1235        end
1236        -- compensation for scaling
1237        local sx = 1
1238        local sy = 1
1239        local sr = 1
1240        local dx = 0
1241        local dy = 0
1242        if transform then
1243            local first = lpegmatch(coordinatesplitter,prescript.sh_first or "0 0")
1244            local setx  = lpegmatch(coordinatesplitter,prescript.sh_set_x or "0 0")
1245            local sety  = lpegmatch(coordinatesplitter,prescript.sh_set_y or "0 0")
1246
1247            local x = setx[1] -- point that has different x
1248            local y = sety[1] -- point that has different y
1249
1250            if x == 0 or y == 0 then
1251                -- forget about it
1252            else
1253                local path   = object.path
1254                local path1x = path[1].x_coord
1255                local path1y = path[1].y_coord
1256                local path2x = path[x].x_coord
1257                local path2y = path[y].y_coord
1258
1259                local dxa = path2x - path1x
1260                local dya = path2y - path1y
1261                local dxb = setx[2] - first[1]
1262                local dyb = sety[2] - first[2]
1263
1264                if dxa == 0 or dya == 0 or dxb == 0 or dyb == 0 then
1265                    -- forget about it
1266                else
1267                    sx = dxa / dxb ; if sx < 0 then sx = - sx end -- yes or no
1268                    sy = dya / dyb ; if sy < 0 then sy = - sy end -- yes or no
1269
1270                    sr = sqrt(sx^2 + sy^2)
1271
1272                    dx = path1x - sx*first[1]
1273                    dy = path1y - sy*first[2]
1274                end
1275            end
1276        end
1277-- local transformation = prescript.sh_transformation
1278-- if transformation then
1279--     print("todo: " .. transformation)
1280-- end
1281        local steps      = tonumber(prescript.sh_step) or 1
1282        local sh_color_a = prescript.sh_color_a_1 or prescript.sh_color_a or "1"
1283        local sh_color_b = prescript.sh_color_b_1 or prescript.sh_color_b or "1" -- sh_color_b_<sh_steps>
1284        local ca, cb, colorspace, name, model, separation, fractions, opacities
1285        if prescript.sh_color == "into" and prescript.sp_name then
1286            -- some spotcolor
1287            local value_a, components_a, fractions_a, name_a
1288            local value_b, components_b, fractions_b, name_b
1289            for i=1,#prescript do
1290                -- { "sh_color_a", "1" },
1291                -- { "sh_color", "into" },
1292                -- { "sh_radius_b", "0" },
1293                -- { "sh_radius_a", "141.73225" },
1294                -- { "sh_center_b", "425.19676 141.73225" },
1295                -- { "sh_center_a", "425.19676 0" },
1296                -- { "sh_factor", "1" },
1297                local tag = prescript[i][1]
1298                if not name_a and tag == "sh_color_a" then
1299                    value_a      = prescript[i-5][2]
1300                    components_a = prescript[i-4][2]
1301                    fractions_a  = prescript[i-3][2]
1302                    name_a       = prescript[i-2][2]
1303                elseif not name_b and tag == "sh_color_b" then
1304                    value_b      = prescript[i-5][2]
1305                    components_b = prescript[i-4][2]
1306                    fractions_b  = prescript[i-3][2]
1307                    name_b       = prescript[i-2][2]
1308                end
1309                if name_a and name_b then
1310                    break
1311                end
1312            end
1313            ca, cb, separation, name = checkandconvertspot(
1314                name_a,fractions_a,components_a,value_a,
1315                name_b,fractions_b,components_b,value_b
1316            )
1317        else
1318            local colora = lpegmatch(colorsplitter,sh_color_a)
1319            local colorb = lpegmatch(colorsplitter,sh_color_b)
1320            ca, cb, colorspace, name, model = checkandconvert(colora,colorb)
1321            -- test:
1322            if steps > 1 then
1323                ca = { ca }
1324                cb = { cb }
1325                -- todo: cache the formatted
1326                fractions = { tonumber(prescript[formatters["sh_fraction_%i"](1)]) or 0 }
1327                opacities = { tonumber(prescript[formatters["sh_opacity_%i"](1)]) or false }
1328                for i=2,steps do
1329                    local colora = lpegmatch(colorsplitter,prescript[formatters["sh_color_a_%i"](i)])
1330                    local colorb = lpegmatch(colorsplitter,prescript[formatters["sh_color_b_%i"](i)])
1331                    ca[i], cb[i] = checkandconvert(colora,colorb,model)
1332                    fractions[i] = tonumber(prescript[formatters["sh_fraction_%i"](i)]) or (i/steps)
1333                    opacities[i] = tonumber(prescript[formatters["sh_opacity_%i"](i)]) or false
1334                end
1335            end
1336        end
1337        if not ca or not cb then
1338            ca, cb, colorspace, name = checkandconvert()
1339            steps = 1
1340        end
1341        if sh_type == "linear" then
1342            local coordinates = { dx + sx*centera[1], dy + sy*centera[2], dx + sx*centerb[1], dy + sy*centerb[2] }
1343            lpdf.linearshade(name,domain,ca,cb,1,colorspace,coordinates,separation,steps>1 and steps,fractions,opacities) -- backend specific (will be renamed)
1344        elseif sh_type == "circular" then
1345            local factor  = tonumber(prescript.sh_factor) or 1
1346            local radiusa = factor * tonumber(prescript.sh_radius_a)
1347            local radiusb = factor * tonumber(prescript.sh_radius_b)
1348            local coordinates = { dx + sx*centera[1], dy + sy*centera[2], sr*radiusa, dx + sx*centerb[1], dy + sy*centerb[2], sr*radiusb }
1349            lpdf.circularshade(name,domain,ca,cb,1,colorspace,coordinates,separation,steps>1 and steps,fractions,opacities) -- backend specific (will be renamed)
1350        else
1351            -- fatal error
1352        end
1353        -- kind of ugly the pdf code here
1354        before[#before+1] = "q /Pattern cs" -- when did pattern space creep in?
1355        if transformation then
1356            before[#before+1] = f_cm(unpack(transformation))
1357        end
1358        after[#after+1] = formatters["W n /%s sh Q"](name)
1359        -- false, not nil, else mt triggered
1360        object.color   = false -- hm, not object.color ?
1361        object.type    = false
1362        object.grouped = true
1363    end
1364end
1365
1366-- bitmaps
1367
1368local function bm_process(object,prescript,before,after)
1369    local bm_xresolution = prescript.bm_xresolution
1370    local bm_yresolution = prescript.bm_yresolution
1371    if bm_xresolution then
1372     -- before[#before+1] = f_cm_b(cm(object))
1373        local sx, rx, ry, sy, tx, ty = cm(object)
1374        local postscript = object.postscript
1375        before[#before+1] = function()
1376            context.MPLIBscaledcm(
1377                function()
1378                    figures.bitmapimage {
1379                        xresolution = tonumber(bm_xresolution),
1380                        yresolution = tonumber(bm_yresolution),
1381                        width       = 1/basepoints,
1382                        height      = 1/basepoints,
1383                        data        = postscript,
1384                        format      = "png",
1385                    }
1386                end,
1387                sx, rx, ry, sy, tx, ty
1388            )
1389        end
1390     -- before[#before+1] = s_cm_e
1391        object.path = false
1392        object.color = false
1393        object.grouped = true
1394    end
1395end
1396
1397-- positions
1398
1399local function ps_process(object,prescript,before,after)
1400    local ps_label = prescript.ps_label
1401    if ps_label then
1402        local op    = object.path
1403        local first = op[1]
1404        local third = op[3]
1405        local x, y, w, h
1406        if first and third then
1407            local properties = metapost.properties
1408            x = first.x_coord
1409            y = first.y_coord
1410            w = third.x_coord - x
1411            h = third.y_coord - y
1412            x = x - properties.llx
1413            y = properties.ury - y
1414        else
1415            x = 0
1416            y = 0
1417            w = 0
1418            h = 0
1419        end
1420        before[#before+1] = function()
1421            context.MPLIBpositionwhd(ps_label,x,y,w,h)
1422        end
1423        object.path = false
1424    end
1425end
1426
1427-- figures
1428
1429-- local sx, rx, ry, sy, tx, ty = cm(object)
1430-- sxsy(box.width,box.height,box.depth))
1431
1432function mp.mf_external_figure(filename)
1433    local f = figures.getinfo(filename)
1434    local w = 0
1435    local h = 0
1436    if f then
1437        local u = f.used
1438        if u and u.fullname then
1439            w = u.width or 0
1440            h = u.height or 0
1441        end
1442    else
1443        report_metapost("external figure %a not found",filename)
1444    end
1445    mp.triplet(w/65536,h/65536,0)
1446end
1447
1448local function fg_process(object,prescript,before,after)
1449    local fg_name = prescript.fg_name
1450    if fg_name then
1451        before[#before+1] = f_cm_b(cm(object)) -- beware: does not use the cm stack
1452        before[#before+1] = function()
1453            context.MPLIBfigure(fg_name,prescript.fg_mask or "")
1454        end
1455        before[#before+1] = s_cm_e
1456        object.path = false
1457        object.grouped = true
1458    end
1459end
1460
1461-- color and transparency
1462
1463local value = Cs ( (
1464    (Carg(1) * C((1-P(","))^1)) / function(a,b) return f_f3(a * tonumber(b)) end
1465  + P(","))^1
1466)
1467
1468-- should be codeinjections
1469
1470local t_list = attributes.list[attributes.private('transparency')]
1471local c_list = attributes.list[attributes.private('color')]
1472
1473local remappers = {
1474    [1] = formatters["s=%s"],
1475    [3] = formatters["r=%s,g=%s,b=%s"],
1476    [4] = formatters["c=%s,m=%s,y=%s,k=%s"],
1477}
1478
1479local processlast = 0
1480local processhash = setmetatableindex(function(t,k)
1481    processlast = processlast + 1
1482    local v = formatters["mp_%s"](processlast)
1483    defineprocesscolor(v,k,true,true)
1484    t[k] = v
1485    return v
1486end)
1487
1488local function checked_transparency(alternative,transparency,before,after)
1489    alternative  = tonumber(alternative)  or 1
1490    transparency = tonumber(transparency) or 0
1491    before[#before+1] = formatters["/Tr%s gs"](registertransparency(nil,alternative,transparency,true))
1492    after [#after +1] = "/Tr0 gs" -- outertransparency
1493end
1494
1495local function tr_process(object,prescript,before,after)
1496    -- before can be shortcut to t
1497    local tr_alternative = prescript.tr_alternative
1498    if tr_alternative then
1499        checked_transparency(tr_alternative,prescript.tr_transparency,before,after)
1500    end
1501    local cs = object.color
1502    if cs and #cs > 0 then
1503        local c_b, c_a
1504        local sp_type = prescript.sp_type
1505-- inspect(cs)
1506-- inspect(prescript)
1507        if not sp_type then
1508            c_b, c_a = colorconverter(cs)
1509        else
1510            local sp_name = prescript.sp_name or "black"
1511            if sp_type == "spot" then
1512                local sp_value      = prescript.sp_value or "1"
1513                local components    = split(sp_value,":")
1514                local specification = remappers[#components]
1515                if specification then
1516                    specification = specification(unpack(components))
1517                else
1518                    specification = "s=0"
1519                end
1520                local sp_spec = processhash[specification]
1521                definespotcolor(sp_name,sp_spec,"p=1",true)
1522                sp_type = "named"
1523            elseif sp_type == "multitone" then -- (fractions of a multitone) don't work well in mupdf
1524                local sp_value = prescript.sp_value or "1"
1525                local sp_specs = { }
1526                local sp_list  = split(sp_value," ")
1527                for i=1,#sp_list do
1528                    local sp_value      = sp_list[i]
1529                    local components    = split(sp_value,":")
1530                    local specification = remappers[#components]
1531                    if specification then
1532                        specification = specification(unpack(components))
1533                    else
1534                        specification = "s=0"
1535                    end
1536                    local sp_spec = processhash[specification]
1537                    sp_specs[i] = formatters["%s=1"](sp_spec)
1538                end
1539                sp_specs = concat(sp_specs,",")
1540                definemultitonecolor(sp_name,sp_specs,"","")
1541                sp_type = "named"
1542            elseif sp_type == "named" then
1543--                cs = { 1 } -- factor 1
1544            end
1545            if sp_type == "named" then
1546                -- we might move this to another namespace .. also, named can be a spotcolor
1547                -- so we need to check for that too ... also we need to resolve indirect
1548                -- colors so we might need the second pass for this (draw dots with \MPcolor)
1549                if not tr_alternative then
1550                    -- todo: sp_name is not yet registered at this time
1551                    local t = t_list[sp_name] -- string or attribute
1552                    local v = t and transparencyvalue(t)
1553                    if v then
1554                        checked_transparency(v[1],v[2],before,after)
1555                    end
1556                end
1557                local c = c_list[sp_name] -- string or attribute
1558                local v = c and colorvalue(c)
1559                if v then
1560                    -- all=1 gray=2 rgb=3 cmyk=4
1561                    local colorspace = v[1]
1562                    local factor     = cs[1]
1563                    if colorspace == 2 then
1564                        local s = factor * v[2]
1565                        c_b, c_a = checked_color_pair(f_gray,s,s)
1566                    elseif colorspace == 3 then
1567                        local r = factor * v[3]
1568                        local g = factor * v[4]
1569                        local b = factor * v[5]
1570                        c_b, c_a = checked_color_pair(f_rgb,r,g,b,r,g,b)
1571                    elseif colorspace == 4 or colorspace == 1 then
1572                        local c = factor * v[6]
1573                        local m = factor * v[7]
1574                        local y = factor * v[8]
1575                        local k = factor * v[9]
1576                        c_b, c_a = checked_color_pair(f_cmyk,c,m,y,k,c,m,y,k)
1577                    elseif colorspace == 5 then
1578                        -- not all viewers show the fractions ok
1579                        local name  = v[10]
1580                        local value = split(v[13],",")
1581                        if factor ~= 1 then
1582                            for i=1,#value do
1583                                value[i] = f_scn(factor * (tonumber(value[i]) or 1))
1584                            end
1585                        end
1586                        value = concat(value," ")
1587                        c_b, c_a = checked_color_pair(f_spot,name,name,value,value)
1588                    else
1589                        local s = factor *v[2]
1590                        c_b, c_a = checked_color_pair(f_gray,s,s)
1591                    end
1592                end
1593            end
1594        end
1595        if c_a and c_b then
1596            before[#before+1] = c_b
1597            after [#after +1] = c_a
1598        end
1599    end
1600end
1601
1602-- layers (nasty: we need to keep the 'grouping' right
1603
1604local function la_process(object,prescript,before,after)
1605    local la_name = prescript.la_name
1606    if la_name then
1607        local codeinjections = backends.codeinjections
1608        before[#before+1] = codeinjections.startlayer(la_name)
1609        insert( after, 1,   codeinjections.stoplayer())
1610    end
1611end
1612
1613-- groups
1614
1615local function getcorners(path)
1616--     if #path == 1 then
1617--         return 0, 0, 0, 0
1618--     else
1619        local p1 = path[1]
1620        local p2 = path[2]
1621        local p3 = path[3]
1622        local p4 = path[4]
1623        return
1624            min(p1.x_coord,p2.x_coord,p3.x_coord,p4.x_coord),
1625            min(p1.y_coord,p2.y_coord,p3.y_coord,p4.y_coord),
1626            max(p1.x_coord,p2.x_coord,p3.x_coord,p4.x_coord),
1627            max(p1.y_coord,p2.y_coord,p3.y_coord,p4.y_coord)
1628--     end
1629end
1630
1631-- todo: just in place instead of calling out to tex
1632
1633local function gr_process(object,prescript,before,after)
1634    local gr_state = prescript.gr_state
1635    if not gr_state then
1636       return
1637    elseif gr_state == "start" then
1638        local gr_type = utilities.parsers.settings_to_set(prescript.gr_type)
1639        local llx, lly, urx, ury = getcorners(object.path)
1640        before[#before+1] = function()
1641            context.MPLIBstartgroup(
1642                gr_type.isolated and 1 or 0,
1643                gr_type.knockout and 1 or 0,
1644                llx, lly, urx, ury
1645            )
1646        end
1647    elseif gr_state == "stop" then
1648        after[#after+1] = function()
1649            context.MPLIBstopgroup()
1650        end
1651    end
1652    object.path    = false
1653    object.color   = false
1654    object.grouped = true
1655end
1656
1657-- states
1658
1659local function gs_process(object,prescript,before,after)
1660    local gs_type = prescript.gs_type
1661    if gs_type then
1662        object.detail = {
1663            type   = gs_type,
1664            action = prescript.gs_action,
1665            name   = prescript.gs_name,
1666        }
1667        -- just a signal that we need to do something .. will change
1668        before[#before+1] = function() end
1669        after [#after +1] = function() end
1670    end
1671 -- object.path    = false
1672 -- object.color   = false
1673 -- object.grouped = true
1674end
1675
1676-- patterns
1677
1678local pattern_index = 0
1679
1680local function pt_process(object,prescript,before,after)
1681    local pt_state = prescript.pt_state
1682    if not pt_state then
1683       return
1684    else
1685        local pt_action = prescript.pt_action
1686        if pt_state == "start" then
1687            local float = toboolean(prescript.pt_float) and 1 or 0
1688            local llx, lly, urx, ury = getcorners(object.path)
1689            if abs(llx) < 0.0001 then llx = 0 end
1690            if abs(lly) < 0.0001 then lly = 0 end
1691            if abs(urx) < 0.0001 then urx = 0 end
1692            if abs(ury) < 0.0001 then ury = 0 end
1693            before[#before+1] = function()
1694                if pt_action == "set" then
1695                    pattern_index = pattern_index + 1
1696                    context.MPLIBstartsetpattern(pattern_index, llx, lly, urx, ury, float)
1697                else
1698                    context.MPLIBstartgetpattern(pattern_index, llx, lly, urx, ury, float)
1699                end
1700            end
1701        elseif pt_state == "stop" then
1702            after[#after+1] = function()
1703                if pt_action == "set" then
1704                    context.MPLIBstopsetpattern()
1705                else
1706                    context.MPLIBstopgetpattern()
1707                end
1708            end
1709        end
1710    end
1711    object.path    = false
1712    object.color   = false
1713    object.grouped = true
1714end
1715
1716-- outlines
1717
1718local ot_reset, ot_process do
1719
1720    local outlinetexts = { } -- also in top data
1721
1722    ot_reset = function ()
1723        outlinetexts = { }
1724    end
1725
1726    local mp_index = 0
1727    local mp_kind  = ""
1728    local mp_str   = ""
1729
1730    function mp.mf_outline_text(index,str,kind)
1731        if not outlinetexts[index] then
1732            mp_index = index
1733            mp_kind  = kind
1734            mp_str   = str
1735            texrunlocal("mpoutlinetoks")
1736        end
1737    end
1738
1739    interfaces.implement {
1740        name    = "mpoutlinetoks",
1741        actions = function()
1742            context.MPLIBoutlinetext(mp_index,mp_kind,mp_str)
1743        end,
1744    }
1745
1746    implement {
1747        name      = "MPLIBconvertoutlinetext",
1748        arguments = { "integer", "string", "integer" },
1749        actions   = function(index,kind,box)
1750            local boxtomp = fonts.metapost.boxtomp
1751            if boxtomp then
1752                outlinetexts[index] = boxtomp(box,kind)
1753            else
1754                outlinetexts[index] = ""
1755            end
1756        end
1757    }
1758
1759    function mp.mf_get_outline_text(index) -- maybe we need a more private namespace
1760        mp.print(outlinetexts[index] or "draw origin;")
1761    end
1762
1763end
1764
1765-- a bit of a joke
1766
1767local an_process  do
1768
1769    local alternative = "actual" -- [ actual | alternative | none ]
1770
1771    directives.register("metapost.annotations", function(v)
1772        alternative = v
1773    end)
1774
1775    an_process = function(object,prescript,before,after)
1776        local text = prescript.an_text
1777        if text then
1778            local codeinjections = backends.codeinjections
1779            if alternative == true or alternative == "actual" then
1780                before[#before+1] = codeinjections.startactualtext(text)
1781                after [#after +1] = codeinjections.stopactualtext()
1782            elseif alternative == "alternative" then
1783                before[#before+1] = codeinjections.startalternativetext(text)
1784                after [#after +1] = codeinjections.stopalternativetext()
1785            end
1786        end
1787    end
1788
1789end
1790
1791-- mf_object=<string>
1792
1793do
1794
1795    local p1      = P("mf_object=")
1796    local p2      = lpeg.patterns.eol * p1
1797    local pattern = (1-p2)^0 * p2 + p1
1798
1799    function metapost.isobject(str)
1800        return pattern and str ~= "" and lpegmatch(p,str) and true or false
1801    end
1802
1803end
1804
1805local function installplugin(specification)
1806    local reset   = specification.reset
1807    local process = specification.process
1808    local object  = specification.object
1809    if reset then
1810        appendaction(resetteractions,"system",reset)
1811    end
1812    if process then
1813        appendaction(processoractions,"system",process)
1814    end
1815end
1816
1817metapost.installplugin = installplugin
1818
1819-- definitions (order matters but maybe we can just use a hash which is faster)
1820
1821installplugin { name = "outline",       process = ot_process, reset = ot_reset }
1822installplugin { name = "color",                               reset = cl_reset }
1823installplugin { name = "text",          process = tx_process, reset = tx_reset }
1824installplugin { name = "group",         process = gr_process  }
1825installplugin { name = "pattern",       process = pt_process  }
1826installplugin { name = "graphictext",   process = gt_process, reset = gt_reset }
1827installplugin { name = "shade",         process = sh_process  }
1828installplugin { name = "bitmap",        process = bm_process  }
1829installplugin { name = "box",           process = bx_process  }
1830installplugin { name = "position",      process = ps_process  }
1831installplugin { name = "figure",        process = fg_process  }
1832installplugin { name = "layer",         process = la_process  }
1833installplugin { name = "transparency",  process = tr_process  }
1834installplugin { name = "graphicstates", process = gs_process  }
1835installplugin { name = "annotation",    process = an_process  }
1836