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