mlib-run.lua /size: 20 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['mlib-run'] = {
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
9-- cmyk       -> done, native
10-- spot       -> done, but needs reworking (simpler)
11-- multitone  ->
12-- shade      -> partly done, todo: cm
13-- figure     -> done
14-- hyperlink  -> low priority, easy
15
16-- new * run
17-- or
18-- new * execute^1 * finish
19
20-- a*[b,c] == b + a * (c-b)
21
22--[[ldx--
23<p>The directional helpers and pen analysis are more or less translated from the
24<l n='c'/> code. It really helps that Taco know that source so well. Taco and I spent
25quite some time on speeding up the <l n='lua'/> and <l n='c'/> code. There is not
26much to gain, especially if one keeps in mind that when integrated in <l n='tex'/>
27only a part of the time is spent in <l n='metapost'/>. Of course an integrated
28approach is way faster than an external <l n='metapost'/> and processing time
29nears zero.</p>
30--ldx]]--
31
32local type, tostring, tonumber, next = type, tostring, tonumber, next
33local find, striplines = string.find, utilities.strings.striplines
34local concat, insert, remove = table.concat, table.insert, table.remove
35
36local emptystring = string.is_empty
37
38local trace_graphics   = false  trackers.register("metapost.graphics",   function(v) trace_graphics   = v end)
39local trace_tracingall = false  trackers.register("metapost.tracingall", function(v) trace_tracingall = v end)
40
41local report_metapost = logs.reporter("metapost")
42local texerrormessage = logs.texerrormessage
43
44local starttiming     = statistics.starttiming
45local stoptiming      = statistics.stoptiming
46
47local formatters      = string.formatters
48
49local mplib           = mplib
50metapost              = metapost or { }
51local metapost        = metapost
52
53metapost.showlog      = false
54metapost.lastlog      = ""
55metapost.texerrors    = false
56metapost.exectime     = metapost.exectime or { } -- hack
57metapost.nofruns      = 0
58
59local mpxformats      = { }
60local nofformats      = 0
61local mpxpreambles    = { }
62local mpxterminals    = { }
63local mpxextradata    = { }
64
65-- The flatten hack is needed because the library currently barks on \n\n and the
66-- collapse because mp cannot handle snippets due to grouping issues.
67
68-- todo: pass tables to executempx instead of preparing beforehand,
69-- as it's more efficient for the terminal
70
71local function flatten(source,target)
72    for i=1,#source do
73        local d = source[i]
74        if type(d) == "table" then
75            flatten(d,target)
76        elseif d and d ~= "" then
77            target[#target+1] = d
78        end
79    end
80    return target
81end
82
83local function prepareddata(data)
84    if data and data ~= "" then
85        if type(data) == "table" then
86            data = flatten(data,{ })
87            data = #data > 1 and concat(data,"\n") or data[1]
88        end
89        return data
90    end
91end
92
93local function executempx(mpx,data)
94    local terminal = mpxterminals[mpx]
95    if terminal then
96        terminal.writer(data)
97        data = ""
98    elseif type(data) == "table" then
99        data = prepareddata(data,collapse)
100    end
101    metapost.nofruns = metapost.nofruns + 1
102    return mpx:execute(data)
103end
104
105directives.register("mplib.texerrors",  function(v) metapost.texerrors = v end)
106trackers.register  ("metapost.showlog", function(v) metapost.showlog   = v end)
107
108function metapost.resetlastlog()
109    metapost.lastlog = ""
110end
111
112local new_instance = mplib.new
113local find_file    = mplib.finder
114
115function metapost.reporterror(result)
116    if not result then
117        report_metapost("error: no result object returned")
118        return true
119    elseif result.status == 0 then
120        return false
121    elseif mplib.realtimelogging then
122        return false -- we already reported
123    else
124        local t = result.term
125        local e = result.error
126        local l = result.log
127        local report = metapost.texerrors and texerrormessage or report_metapost
128        if t and t ~= "" then
129            report("mp error: %s",striplines(t))
130        end
131        if e == "" or e == "no-error" then
132            e = nil
133        end
134        if e then
135            report("mp error: %s",striplines(e))
136        end
137        if not t and not e and l then
138            metapost.lastlog = metapost.lastlog .. "\n" .. l
139            report_metapost("log: %s",l)
140        else
141            report_metapost("error: unknown, no error, terminal or log messages")
142        end
143        return true
144    end
145end
146
147local f_preamble = formatters [ [[
148    boolean mplib ; mplib := true ;
149    let dump = endinput ;
150    input "%s" ;
151    randomseed:=%s;
152]] ]
153
154local methods = {
155    double  = "double",
156    scaled  = "scaled",
157 -- binary  = "binary",
158    binary  = "double",
159    decimal = "decimal",
160    default = "scaled",
161}
162
163function metapost.runscript(code)
164    return ""
165end
166
167function metapost.scripterror(str)
168    report_metapost("script error: %s",str)
169end
170
171-- todo: random_seed
172
173local seed = nil
174
175function metapost.load(name,method)
176    starttiming(mplib)
177    if not seed then
178        seed = job.getrandomseed()
179        if seed <= 1 then
180            seed = seed % 1000
181        elseif seed > 4095 then
182            seed = seed % 4096
183        end
184    end
185    method = method and methods[method] or "scaled"
186    local mpx, terminal = new_instance {
187        ini_version  = true,
188        math_mode    = method,
189        run_script   = metapost.runscript,
190        script_error = metapost.scripterror,
191        make_text    = metapost.maketext,
192        extensions   = 1,
193     -- random_seed  = seed,
194        utf8_mode    = true,
195        text_mode    = true,
196    }
197    report_metapost("initializing number mode %a",method)
198    local result
199    if not mpx then
200        result = { status = 99, error = "out of memory"}
201    else
202        mpxterminals[mpx] = terminal
203        -- pushing permits advanced features
204        metapost.pushscriptrunner(mpx)
205        result = executempx(mpx,f_preamble(file.addsuffix(name,"mp"),seed))
206        metapost.popscriptrunner()
207    end
208    stoptiming(mplib)
209    metapost.reporterror(result)
210    return mpx, result
211end
212
213function metapost.checkformat(mpsinput,method)
214    local mpsinput  = mpsinput or "metafun"
215    local foundfile = ""
216    if file.suffix(mpsinput) ~= "" then
217        foundfile  = find_file(mpsinput) or ""
218    end
219 -- if foundfile == "" then
220 --     foundfile  = find_file(file.replacesuffix(mpsinput,"mpvi")) or ""
221 -- end
222    if CONTEXTLMTXMODE > 0 and foundfile == "" then
223        foundfile  = find_file(file.replacesuffix(mpsinput,"mpxl")) or ""
224    end
225    if foundfile == "" then
226        foundfile  = find_file(file.replacesuffix(mpsinput,"mpiv")) or ""
227    end
228    if foundfile == "" then
229        foundfile  = find_file(file.replacesuffix(mpsinput,"mp")) or ""
230    end
231    if foundfile == "" then
232        report_metapost("loading %a fails, format not found",mpsinput)
233    else
234        report_metapost("loading %a as %a using method %a",mpsinput,foundfile,method or "default")
235        local mpx, result = metapost.load(foundfile,method)
236        if mpx then
237            return mpx
238        else
239            report_metapost("error in loading %a",mpsinput)
240            metapost.reporterror(result)
241        end
242    end
243end
244
245function metapost.unload(mpx)
246    starttiming(mplib)
247    if mpx then
248        mpx:finish()
249    end
250    stoptiming(mplib)
251end
252
253metapost.defaultformat   = "metafun"
254metapost.defaultinstance = "metafun"
255metapost.defaultmethod   = "default"
256
257function metapost.getextradata(mpx)
258    return mpxextradata[mpx]
259end
260
261function metapost.pushformat(specification,f,m) -- was: instance, name, method
262    if type(specification) ~= "table" then
263        specification = {
264            instance = specification,
265            format   = f,
266            method   = m,
267        }
268    end
269    local instance    = specification.instance
270    local format      = specification.format
271    local method      = specification.method
272    local definitions = specification.definitions
273    local extensions  = specification.extensions
274    local preamble    = nil
275    if not instance or instance == "" then
276        instance = metapost.defaultinstance
277        specification.instance = instance
278    end
279    if not format or format == "" then
280        format = metapost.defaultformat
281        specification.format = format
282    end
283    if not method or method == "" then
284        method = metapost.defaultmethod
285        specification.method = method
286    end
287    if definitions and definitions ~= "" then
288        preamble = definitions
289    end
290    if extensions and extensions ~= "" then
291        if preamble then
292            preamble = preamble .. "\n" .. extensions
293        else
294            preamble = extensions
295        end
296    end
297    nofformats = nofformats + 1
298    local usedinstance = instance .. ":" .. nofformats
299    local mpx = mpxformats  [usedinstance]
300    local mpp = mpxpreambles[instance] or ""
301 -- report_metapost("push instance %a (%S)",usedinstance,mpx)
302    if preamble then
303        preamble = prepareddata(preamble)
304        mpp = mpp .. "\n" .. preamble
305        mpxpreambles[instance] = mpp
306    end
307    if not mpx then
308        report_metapost("initializing instance %a using format %a and method %a",usedinstance,format,method)
309        mpx = metapost.checkformat(format,method)
310        mpxformats  [usedinstance] = mpx
311        mpxextradata[mpx] = { }
312        if mpp ~= "" then
313            preamble = mpp
314        end
315    end
316    if preamble then
317        executempx(mpx,preamble)
318    end
319    specification.mpx = mpx
320    return mpx
321end
322
323-- luatex.wrapup(function()
324--     for k, mpx in next, mpxformats do
325--         mpx:finish()
326--     end
327-- end)
328
329function metapost.popformat()
330    nofformats = nofformats - 1
331end
332
333function metapost.reset(mpx)
334    if not mpx then
335        -- nothing
336    elseif type(mpx) == "string" then
337        if mpxformats[mpx] then
338            local instance = mpxformats[mpx]
339            instance:finish()
340            mpxterminals[mpx] = nil
341            mpxextradata[mpx] = nil
342            mpxformats  [mpx] = nil
343        end
344    else
345        for name, instance in next, mpxformats do
346            if instance == mpx then
347                mpx:finish()
348                mpxterminals[mpx] = nil
349                mpxextradata[mpx] = nil
350                mpxformats  [mpx] = nil
351                break
352            end
353        end
354    end
355end
356
357local mp_tra = { }
358local mp_tag = 0
359
360-- key/values
361
362do
363
364    local stack, top = { }, nil
365
366    function metapost.setvariable(k,v)
367        if top then
368            top[k] = v
369        else
370            metapost.variables[k] = v
371        end
372    end
373
374    function metapost.pushvariable(k)
375        local t = { }
376        if top then
377            insert(stack,top)
378            top[k] = t
379        else
380            metapost.variables[k] = t
381        end
382        top = t
383    end
384
385    function metapost.popvariable()
386        top = remove(stack)
387    end
388
389    local stack = { }
390
391    function metapost.pushvariables()
392        insert(stack,metapost.variables)
393        metapost.variables = { }
394    end
395
396    function metapost.popvariables()
397        metapost.variables = remove(stack) or metapost.variables
398    end
399
400end
401
402
403if not metapost.process then
404
405    function metapost.process(specification)
406        metapost.run(specification)
407    end
408
409end
410
411-- run, process, convert and flush all work with a specification with the
412-- following (often optional) fields
413--
414--     mpx          string or mp object
415--     data         string or table of strings
416--     flusher      table with flush methods
417--     askedfig     string ("all" etc) or number
418--     incontext    boolean
419--     plugmode     boolean
420
421local function makebeginbanner(specification)
422    return formatters["%% begin graphic: n=%s\n\n"](metapost.n)
423end
424
425local function makeendbanner(specification)
426    return "\n% end graphic\n\n"
427end
428
429function metapost.run(specification)
430    local mpx       = specification.mpx
431    local data      = specification.data
432    local converted = false
433    local result    = { }
434    local mpxdone   = type(mpx) == "string"
435    if mpxdone then
436        mpx = metapost.pushformat { instance = mpx, format = mpx }
437    end
438    if mpx and data then
439        local tra = nil
440        starttiming(metapost) -- why not at the outer level ...
441        metapost.variables = { } -- todo also push / pop
442        metapost.pushscriptrunner(mpx)
443        if trace_graphics then
444            tra = mp_tra[mpx]
445            if not tra then
446                mp_tag = mp_tag + 1
447                local jobname = tex.jobname
448                tra = {
449                    inp = io.open(formatters["%s-mplib-run-%03i.mp"] (jobname,mp_tag),"w"),
450                    log = io.open(formatters["%s-mplib-run-%03i.log"](jobname,mp_tag),"w"),
451                }
452                mp_tra[mpx] = tra
453            end
454            local banner = makebeginbanner(specification)
455            tra.inp:write(banner)
456            tra.log:write(banner)
457        end
458        local function process(d,i)
459            if d then
460                if trace_graphics then
461                    if i then
462                        tra.inp:write(formatters["\n%% begin snippet %s\n"](i))
463                    end
464                    if type(d) == "table" then
465                        for i=1,#d do
466                            tra.inp:write(d[i])
467                        end
468                    else
469                        tra.inp:write(d)
470                    end
471                    if i then
472                        tra.inp:write(formatters["\n%% end snippet %s\n"](i))
473                    end
474                end
475                starttiming(metapost.exectime)
476                result = executempx(mpx,d)
477                stoptiming(metapost.exectime)
478                if trace_graphics and result then
479                    local str = result.log or result.error
480                    if str and str ~= "" then
481                        tra.log:write(str)
482                    end
483                end
484                if not metapost.reporterror(result) then
485                    if metapost.showlog then
486                        -- make function and overload in lmtx
487                        local str = result.term ~= "" and result.term or "no terminal output"
488                        if not emptystring(str) then
489                            metapost.lastlog = metapost.lastlog .. "\n" .. str
490                            report_metapost("log: %s",str)
491                        end
492                    end
493                    if result.fig then
494                        converted = metapost.convert(specification,result)
495                    end
496                end
497            elseif i then
498                report_metapost("error: invalid graphic component %s",i)
499            else
500                report_metapost("error: invalid graphic")
501            end
502        end
503
504--         local data = prepareddata(data)
505        if type(data) == "table" then
506            if trace_tracingall then
507                executempx(mpx,"tracingall;")
508            end
509                process(data)
510--             for i=1,#data do
511--                 process(data[i],i)
512--             end
513        else
514            if trace_tracingall then
515                data = "tracingall;" .. data
516            end
517            process(data)
518        end
519        if trace_graphics then
520            local banner = makeendbanner(specification)
521            tra.inp:write(banner)
522            tra.log:write(banner)
523        end
524        stoptiming(metapost)
525        metapost.popscriptrunner(mpx)
526    end
527    if mpxdone then
528        metapost.popformat()
529    end
530    return converted, result
531end
532
533if not metapost.convert then
534
535    function metapost.convert()
536        report_metapost('warning: no converter set')
537    end
538
539end
540
541-- This will be redone as we no longer output svg of ps!
542
543-- function metapost.directrun(formatname,filename,outputformat,astable,mpdata)
544--     local fullname = file.addsuffix(filename,"mp")
545--     local data = mpdata or io.loaddata(fullname)
546--     if outputformat ~= "svg" then
547--         outputformat = "mps"
548--     end
549--     if not data then
550--         report_metapost("unknown file %a",filename)
551--     else
552--         local mpx = metapost.checkformat(formatname)
553--         if not mpx then
554--             report_metapost("unknown format %a",formatname)
555--         else
556--             report_metapost("processing %a",(mpdata and (filename or "data")) or fullname)
557--             local result = executempx(mpx,data)
558--             if not result then
559--                 report_metapost("error: no result object returned")
560--             elseif result.status > 0 then
561--                 report_metapost("error: %s",(result.term or "no-term") .. "\n" .. (result.error or "no-error"))
562--             else
563--                 if metapost.showlog then
564--                     metapost.lastlog = metapost.lastlog .. "\n" .. result.term
565--                     report_metapost("info: %s",result.term or "no-term")
566--                 end
567--                 local figures = result.fig
568--                 if figures then
569--                     local sorted = table.sortedkeys(figures)
570--                     if astable then
571--                         local result = { }
572--                         report_metapost("storing %s figures in table",#sorted)
573--                         for k=1,#sorted do
574--                             local v = sorted[k]
575--                             if outputformat == "mps" then
576--                                 result[v] = figures[v]:postscript()
577--                             else
578--                                 result[v] = figures[v]:svg() -- (3) for prologues
579--                             end
580--                         end
581--                         return result
582--                     else
583--                         local basename = file.removesuffix(file.basename(filename))
584--                         for k=1,#sorted do
585--                             local v = sorted[k]
586--                             local output
587--                             if outputformat == "mps" then
588--                                 output = figures[v]:postscript()
589--                             else
590--                                 output = figures[v]:svg() -- (3) for prologues
591--                             end
592--                             local outname = formatters["%s-%s.%s"](basename,v,outputformat)
593--                             report_metapost("saving %s bytes in %a",#output,outname)
594--                             io.savedata(outname,output)
595--                         end
596--                         return #sorted
597--                     end
598--                 end
599--             end
600--         end
601--     end
602-- end
603
604function metapost.directrun(formatname,filename,outputformat,astable,mpdata)
605    report_metapost("producing postscript and svg is no longer supported")
606end
607
608do
609
610    local result = { }
611    local width  = 0
612    local height = 0
613    local depth  = 0
614    local bbox   = { 0, 0, 0, 0 }
615
616    local flusher = {
617        startfigure = function(n,llx,lly,urx,ury)
618            result = { }
619            width  = urx - llx
620            height = ury
621            depth  = -lly
622            bbox   = { llx, lly, urx, ury }
623        end,
624        flushfigure = function(t)
625            local r = #result
626            for i=1,#t do
627                r = r + 1
628                result[r] = t[i]
629            end
630        end,
631        stopfigure = function()
632        end,
633    }
634
635    -- make table variant:
636
637    function metapost.simple(instance,code,useextensions,dontwrap)
638        -- can we pickup the instance ?
639        local mpx = metapost.pushformat {
640            instance = instance or "simplefun",
641            format   = "metafun", -- or: minifun
642            method   = "double",
643        }
644        metapost.process {
645            mpx        = mpx,
646            flusher    = flusher,
647            askedfig   = 1,
648            useplugins = useextensions,
649            data       = dontwrap and { code } or { "beginfig(1);", code, "endfig;" },
650            incontext  = false,
651        }
652        metapost.popformat()
653        if result then
654            local stream = concat(result," ")
655            result = { } -- nil -- cleanup .. weird, we can have a dangling q
656            return stream, width, height, depth, bbox
657        else
658            return "", 0, 0, 0, { 0, 0, 0, 0 }
659        end
660    end
661
662end
663
664local getstatistics = mplib.getstatistics or mplib.statistics
665
666function metapost.getstatistics(memonly)
667    if memonly then
668        local n, m = 0, 0
669        for name, mpx in next, mpxformats do
670            n = n + 1
671            m = m + getstatistics(mpx).memory
672        end
673        return n, m
674    else
675        local t = { }
676        for name, mpx in next, mpxformats do
677            t[name] = getstatistics(mpx)
678        end
679        return t
680    end
681end
682