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