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