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