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