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