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