luatex-mplib.lua /size: 27 Kb    last modification: 2023-12-21 09:45
1if not modules then modules = { } end modules ['luatex-mplib'] = {
2    version   = 1.001,
3    comment   = "companion to luatex-mplib.tex",
4    author    = "Hans Hagen & Taco Hoekwater",
5    copyright = "ConTeXt Development Team",
6    license   = "public domain",
7}
8
9-- This module is a stripped down version of libraries that are used by ConTeXt. It
10-- can be used in other macro packages and/or serve as an example. Embedding in a
11-- macro package is upto others and normally boils down to inputting 'supp-mpl.tex'.
12
13if metapost and metapost.version then
14
15    -- Let's silently quit and make sure that no one loads it manually in
16    -- ConTeXt.
17
18else
19
20    local format, match, gsub = string.format, string.match, string.gsub
21    local concat = table.concat
22    local abs = math.abs
23
24    local mplib = require ('mplib')
25    local kpse  = require ('kpse')
26
27    -- We create a namespace and some variables to it. If a namespace is already
28    -- defined it wil not be initialized. This permits hooking in code beforehand.
29
30    -- We don't make a format automatically. After all, distributions might have
31    -- their own preferences and normally a format (mem) file will have some
32    -- special place in the TeX tree. Also, there can already be format files,
33    -- different memort settings and other nasty pitfalls that we don't want to
34    -- interfere with. If you want, you can define a function
35    --
36    --   metapost.make (name,mem_name)
37    --
38    -- that does the job.
39
40    metapost          = metapost or { }
41    metapost.version  = 1.00
42    metapost.showlog  = metapost.showlog or false
43    metapost.lastlog  = ""
44
45    -- A few helpers, taken from 'l-file.lua'.
46
47    local file = file or { }
48
49    function file.replacesuffix(filename, suffix)
50        return (string.gsub(filename,"%.[%a%d]+$","")) .. "." .. suffix
51    end
52
53    function file.stripsuffix(filename)
54        return (string.gsub(filename,"%.[%a%d]+$",""))
55    end
56
57    -- We use the KPSE library unless a finder is already defined.
58
59    local mpkpse = kpse.new("luatex","mpost")
60
61    metapost.finder = metapost.finder or function(name, mode, ftype)
62        if mode == "w" then
63            return name
64        else
65            return mpkpse:find_file(name,ftype)
66        end
67    end
68
69    -- You can use your own reported if needed, as long as it handles multiple
70    -- arguments and formatted strings.
71
72
73    metapost.report = metapost.report or function(...)
74        if logs.report then
75            logs.report("metapost",...)
76        else
77            texio.write(format("<mplib: %s>",format(...)))
78        end
79    end
80
81    -- The rest of this module is not documented. More info can be found in the
82    -- LuaTeX manual, articles in user group journals and the files that ship
83    -- with ConTeXt.
84
85    function metapost.resetlastlog()
86        metapost.lastlog = ""
87    end
88
89    local mplibone = tonumber(mplib.version()) <= 1.50
90
91    if mplibone then
92
93        metapost.make = metapost.make or function(name,mem_name,dump)
94            local t = os.clock()
95            local mpx = mplib.new {
96                ini_version = true,
97                find_file = metapost.finder,
98                job_name = file.stripsuffix(name)
99            }
100            mpx:execute(string.format("input %s ;",name))
101            if dump then
102                mpx:execute("dump ;")
103                metapost.report("format %s made and dumped for %s in %0.3f seconds",mem_name,name,os.clock()-t)
104            else
105                metapost.report("%s read in %0.3f seconds",name,os.clock()-t)
106            end
107            return mpx
108        end
109
110        function metapost.load(name)
111            local mem_name = file.replacesuffix(name,"mem")
112            local mpx = mplib.new {
113                ini_version = false,
114                mem_name = mem_name,
115                find_file = metapost.finder
116            }
117            if not mpx and type(metapost.make) == "function" then
118                -- when i have time i'll locate the format and dump
119                mpx = metapost.make(name,mem_name)
120            end
121            if mpx then
122                metapost.report("using format %s",mem_name,false)
123                return mpx, nil
124            else
125                return nil, { status = 99, error = "out of memory or invalid format" }
126            end
127        end
128
129    else
130
131        local preamble = [[
132            boolean mplib ; mplib := true ;
133            let dump = endinput ;
134            input %s ;
135        ]]
136
137        metapost.make = metapost.make or function()
138        end
139
140        local template = [[
141            \pdfoutput=1
142            \pdfpkresolution600
143            \pdfcompresslevel=9
144            %s\relax
145            \hsize=100in
146            \vsize=\hsize
147            \hoffset=-1in
148            \voffset=\hoffset
149            \topskip=0pt
150            \setbox0=\hbox{%s}\relax
151            \pageheight=\ht0
152            \pagewidth=\wd0
153            \box0
154            \bye
155        ]]
156
157        metapost.texrunner = "mtxrun --script plain"
158
159        local texruns = 0   -- per document
160        local texhash = { } -- per document
161
162        function metapost.maketext(mpd,str,what)
163            -- inefficient but one can always use metafun .. it's more a test
164            -- feature
165            local verbatimtex = mpd.verbatimtex
166            if not verbatimtex then
167                verbatimtex = { }
168                mpd.verbatimtex = verbatimtex
169            end
170            if what == 1 then
171                table.insert(verbatimtex,str)
172            else
173                local texcode = format(template,concat(verbatimtex,"\n"),str)
174                local texdone = texhash[texcode]
175                local jobname = tex.jobname
176                if not texdone then
177                    texruns = texruns + 1
178                    texdone = texruns
179                    texhash[texcode] = texdone
180                    local texname = format("%s-mplib-%s.tmp",jobname,texdone)
181                    local logname = format("%s-mplib-%s.log",jobname,texdone)
182                    local pdfname = format("%s-mplib-%s.pdf",jobname,texdone)
183                    io.savedata(texname,texcode)
184                    os.execute(format("%s %s",metapost.texrunner,texname))
185                    os.remove(texname)
186                    os.remove(logname)
187                end
188                return format('"image::%s-mplib-%s.pdf" infont defaultfont',jobname,texdone)
189            end
190        end
191
192        local function mpprint(buffer,...)
193            for i=1,select("#",...) do
194                local value = select(i,...)
195                if value ~= nil then
196                    local t = type(value)
197                    if t == "number" then
198                        buffer[#buffer+1] = format("%.16f",value)
199                    elseif t == "string" then
200                        buffer[#buffer+1] = value
201                    elseif t == "table" then
202                        buffer[#buffer+1] = "(" .. concat(value,",") .. ")"
203                    else -- boolean or whatever
204                        buffer[#buffer+1] = tostring(value)
205                    end
206                end
207            end
208        end
209
210        function metapost.runscript(mpd,code)
211            local code = loadstring(code)
212            if type(code) == "function" then
213                local buffer = { }
214                function metapost.print(...)
215                    mpprint(buffer,...)
216                end
217                code()
218             -- mpd.buffer = buffer -- for tracing
219                return concat(buffer,"")
220            end
221            return ""
222        end
223
224        local modes = {
225            scaled  = true,
226            decimal = true,
227            binary  = true,
228            double  = true,
229        }
230
231        function metapost.load(name,mode)
232            local mpd = {
233                buffer   = { },
234                verbatim = { }
235            }
236            local mpx = mplib.new {
237                ini_version = true,
238                find_file   = metapost.finder,
239                make_text   = function(...) return metapost.maketext (mpd,...) end,
240                run_script  = function(...) return metapost.runscript(mpd,...) end,
241                extensions  = 1,
242                math_mode   = mode and modes[mode] and mode or "scaled",
243            }
244            local result
245            if not mpx then
246                result = { status = 99, error = "out of memory"}
247            else
248                result = mpx:execute(format(preamble, file.replacesuffix(name,"mp")))
249            end
250            metapost.reporterror(result)
251            return mpx, result
252        end
253
254    end
255
256    function metapost.unload(mpx)
257        if mpx then
258            mpx:finish()
259        end
260    end
261
262    function metapost.reporterror(result)
263        if not result then
264            metapost.report("mp error: no result object returned")
265        elseif result.status > 0 then
266            local t, e, l = result.term, result.error, result.log
267            if t then
268                metapost.report("mp terminal: %s",t)
269            end
270            if e then
271                metapost.report("mp error: %s", e)
272            end
273            if not t and not e and l then
274                metapost.lastlog = metapost.lastlog .. "\n " .. l
275                metapost.report("mp log: %s",l)
276            else
277                metapost.report("mp error: unknown, no error, terminal or log messages")
278            end
279        else
280            return false
281        end
282        return true
283    end
284
285    function metapost.process(format,data,mode)
286        local converted, result = false, {}
287        local mpx = metapost.load(format,mode)
288        if mpx and data then
289            local result = mpx:execute(data)
290            if not result then
291                metapost.report("mp error: no result object returned")
292            elseif result.status > 0 then
293                metapost.report("mp error: %s",(result.term or "no-term") .. "\n" .. (result.error or "no-error"))
294            elseif metapost.showlog then
295                metapost.lastlog = metapost.lastlog .. "\n" .. result.term
296                metapost.report("mp info: %s",result.term or "no-term")
297            elseif result.fig then
298                converted = metapost.convert(result)
299            else
300                metapost.report("mp error: unknown error, maybe no beginfig/endfig")
301            end
302--             mpx:finish()
303--             mpx = nil
304        else
305           metapost.report("mp error: mem file not found")
306        end
307        return converted, result
308    end
309
310    local function getobjects(result,figure,f)
311        return figure:objects()
312    end
313
314    function metapost.convert(result,flusher)
315        metapost.flush(result,flusher)
316        return true -- done
317    end
318
319    -- We removed some message and tracing code. We might even remove the
320    -- flusher.
321
322    local function pdf_startfigure(n,llx,lly,urx,ury)
323        tex.sprint(format("\\startMPLIBtoPDF{%s}{%s}{%s}{%s}",llx,lly,urx,ury))
324    end
325
326    local function pdf_stopfigure()
327        tex.sprint("\\stopMPLIBtoPDF")
328    end
329
330    function pdf_literalcode(fmt,...) -- table
331        tex.sprint(format("\\MPLIBtoPDF{%s}",format(fmt,...)))
332    end
333
334    function pdf_textfigure(font,size,text,width,height,depth)
335        local how, what = match(text,"^(.-)::(.+)$")
336        if how == "image" then
337            tex.sprint(format("\\MPLIBpdftext{%s}{%s}",what,depth))
338        else
339         -- text = gsub(text,".","\\hbox{%1}") -- kerning happens in metapost
340         -- tex.sprint(format("\\MPLIBtextext{%s}{%s}{%s}{%s}",font,size,text,depth))
341            tex.sprint(format("\\MPLIBtextext{%s}{%s}{\\hpack{\\detokenize{%s}}}{%s}",font,size,text,depth))
342        end
343    end
344
345    local bend_tolerance = 131/65536
346
347    local rx, sx, sy, ry, tx, ty, divider = 1, 0, 0, 1, 0, 0, 1
348
349    local function pen_characteristics(object)
350        local t = mplib.pen_info(object)
351        rx, ry, sx, sy, tx, ty = t.rx, t.ry, t.sx, t.sy, t.tx, t.ty
352        divider = sx*sy - rx*ry
353        return not (sx==1 and rx==0 and ry==0 and sy==1 and tx==0 and ty==0), t.width
354    end
355
356    local function concatinated(px, py) -- no tx, ty here
357        return (sy*px-ry*py)/divider,(sx*py-rx*px)/divider
358    end
359
360    local function curved(ith,pth)
361        local d = pth.left_x - ith.right_x
362        if abs(ith.right_x - ith.x_coord - d) <= bend_tolerance and abs(pth.x_coord - pth.left_x - d) <= bend_tolerance then
363            d = pth.left_y - ith.right_y
364            if abs(ith.right_y - ith.y_coord - d) <= bend_tolerance and abs(pth.y_coord - pth.left_y - d) <= bend_tolerance then
365                return false
366            end
367        end
368        return true
369    end
370
371    local function flushnormalpath(path,open)
372        local pth, ith
373        for i=1,#path do
374            pth = path[i]
375            if not ith then
376                pdf_literalcode("%f %f m",pth.x_coord,pth.y_coord)
377            elseif curved(ith,pth) then
378                pdf_literalcode("%f %f %f %f %f %f c",ith.right_x,ith.right_y,pth.left_x,pth.left_y,pth.x_coord,pth.y_coord)
379            else
380                pdf_literalcode("%f %f l",pth.x_coord,pth.y_coord)
381            end
382            ith = pth
383        end
384        if not open then
385            local one = path[1]
386            if curved(pth,one) then
387                pdf_literalcode("%f %f %f %f %f %f c",pth.right_x,pth.right_y,one.left_x,one.left_y,one.x_coord,one.y_coord )
388            else
389                pdf_literalcode("%f %f l",one.x_coord,one.y_coord)
390            end
391        elseif #path == 1 then
392            -- special case .. draw point
393            local one = path[1]
394            pdf_literalcode("%f %f l",one.x_coord,one.y_coord)
395        end
396        return t
397    end
398
399    local function flushconcatpath(path,open)
400        pdf_literalcode("%f %f %f %f %f %f cm", sx, rx, ry, sy, tx ,ty)
401        local pth, ith
402        for i=1,#path do
403            pth = path[i]
404            if not ith then
405               pdf_literalcode("%f %f m",concatinated(pth.x_coord,pth.y_coord))
406            elseif curved(ith,pth) then
407                local a, b = concatinated(ith.right_x,ith.right_y)
408                local c, d = concatinated(pth.left_x,pth.left_y)
409                pdf_literalcode("%f %f %f %f %f %f c",a,b,c,d,concatinated(pth.x_coord, pth.y_coord))
410            else
411               pdf_literalcode("%f %f l",concatinated(pth.x_coord, pth.y_coord))
412            end
413            ith = pth
414        end
415        if not open then
416            local one = path[1]
417            if curved(pth,one) then
418                local a, b = concatinated(pth.right_x,pth.right_y)
419                local c, d = concatinated(one.left_x,one.left_y)
420                pdf_literalcode("%f %f %f %f %f %f c",a,b,c,d,concatinated(one.x_coord, one.y_coord))
421            else
422                pdf_literalcode("%f %f l",concatinated(one.x_coord,one.y_coord))
423            end
424        elseif #path == 1 then
425            -- special case .. draw point
426            local one = path[1]
427            pdf_literalcode("%f %f l",concatinated(one.x_coord,one.y_coord))
428        end
429        return t
430    end
431
432    -- Support for specials has been removed.
433
434    function metapost.flush(result,flusher)
435        if result then
436            local figures = result.fig
437            if figures then
438                for f=1, #figures do
439                    metapost.report("flushing figure %s",f)
440                    local figure = figures[f]
441                    local objects = getobjects(result,figure,f)
442                    local fignum = tonumber(match(figure:filename(),"([%d]+)$") or figure:charcode() or 0)
443                    local miterlimit, linecap, linejoin, dashed = -1, -1, -1, false
444                    local bbox = figure:boundingbox()
445                    local llx, lly, urx, ury = bbox[1], bbox[2], bbox[3], bbox[4] -- faster than unpack
446                    if urx < llx then
447                        -- invalid
448                        pdf_startfigure(fignum,0,0,0,0)
449                        pdf_stopfigure()
450                    else
451                        pdf_startfigure(fignum,llx,lly,urx,ury)
452                        pdf_literalcode("q")
453                        if objects then
454                            local savedpath = nil
455                            local savedhtap = nil
456                            for o=1,#objects do
457                                local object = objects[o]
458                                local objecttype = object.type
459                                if objecttype == "start_bounds" or objecttype == "stop_bounds" then
460                                    -- skip
461                                elseif objecttype == "start_clip" then
462                                    local evenodd = not object.istext and object.postscript == "evenodd"
463                                    pdf_literalcode("q")
464                                    flushnormalpath(object.path,t,false)
465                                    pdf_literalcode("W n")
466                                    pdf_literalcode(evenodd and "W* n" or "W n")
467                                elseif objecttype == "stop_clip" then
468                                    pdf_literalcode("Q")
469                                    miterlimit, linecap, linejoin, dashed = -1, -1, -1, false
470                                elseif objecttype == "special" then
471                                    -- not supported
472                                elseif objecttype == "text" then
473                                    local ot = object.transform -- 3,4,5,6,1,2
474                                    pdf_literalcode("q %f %f %f %f %f %f cm",ot[3],ot[4],ot[5],ot[6],ot[1],ot[2])
475                                    pdf_textfigure(object.font,object.dsize,object.text,object.width,object.height,object.depth)
476                                    pdf_literalcode("Q")
477                                else
478                                    local evenodd, collect, both = false, false, false
479                                    local postscript = object.postscript
480                                    if not object.istext then
481                                        if postscript == "evenodd" then
482                                            evenodd = true
483                                        elseif postscript == "collect" then
484                                            collect = true
485                                        elseif postscript == "both" then
486                                            both = true
487                                        elseif postscript == "eoboth" then
488                                            evenodd = true
489                                            both    = true
490                                        end
491                                    end
492                                    if collect then
493                                        if not savedpath then
494                                            savedpath = { object.path or false }
495                                            savedhtap = { object.htap or false }
496                                        else
497                                            savedpath[#savedpath+1] = object.path or false
498                                            savedhtap[#savedhtap+1] = object.htap or false
499                                        end
500                                    else
501                                        local cs = object.color
502                                        local cr = false
503                                        if cs and #cs > 0 then
504                                            cs, cr = metapost.colorconverter(cs)
505                                            pdf_literalcode(cs)
506                                        end
507                                        local ml = object.miterlimit
508                                        if ml and ml ~= miterlimit then
509                                            miterlimit = ml
510                                            pdf_literalcode("%f M",ml)
511                                        end
512                                        local lj = object.linejoin
513                                        if lj and lj ~= linejoin then
514                                            linejoin = lj
515                                            pdf_literalcode("%i j",lj)
516                                        end
517                                        local lc = object.linecap
518                                        if lc and lc ~= linecap then
519                                            linecap = lc
520                                            pdf_literalcode("%i J",lc)
521                                        end
522                                        local dl = object.dash
523                                        if dl then
524                                            local d = format("[%s] %i d",concat(dl.dashes or {}," "),dl.offset)
525                                            if d ~= dashed then
526                                                dashed = d
527                                                pdf_literalcode(dashed)
528                                            end
529                                        elseif dashed then
530                                           pdf_literalcode("[] 0 d")
531                                           dashed = false
532                                        end
533                                        local path = object.path
534                                        local transformed, penwidth = false, 1
535                                        local open = path and path[1].left_type and path[#path].right_type
536                                        local pen = object.pen
537                                        if pen then
538                                           if pen.type == 'elliptical' then
539                                                transformed, penwidth = pen_characteristics(object) -- boolean, value
540                                                pdf_literalcode("%f w",penwidth)
541                                                if objecttype == 'fill' then
542                                                    objecttype = 'both'
543                                                end
544                                           else -- calculated by mplib itself
545                                                objecttype = 'fill'
546                                           end
547                                        end
548                                        if transformed then
549                                            pdf_literalcode("q")
550                                        end
551                                        if path then
552                                            if savedpath then
553                                                for i=1,#savedpath do
554                                                    local path = savedpath[i]
555                                                    if transformed then
556                                                        flushconcatpath(path,open)
557                                                    else
558                                                        flushnormalpath(path,open)
559                                                    end
560                                                end
561                                                savedpath = nil
562                                            end
563                                            if transformed then
564                                                flushconcatpath(path,open)
565                                            else
566                                                flushnormalpath(path,open)
567                                            end
568                                            if objecttype == "fill" then
569                                                pdf_literalcode("h f")
570                                            elseif objecttype == "outline" then
571                                            if both then
572                                                pdf_literalcode(evenodd and "h B*" or "h B")
573                                            else
574                                                pdf_literalcode(open and "S" or "h S")
575                                            end
576                                            elseif objecttype == "both" then
577                                                pdf_literalcode(evenodd and "h B*" or "h B")
578                                            end
579                                        end
580                                        if transformed then
581                                            pdf_literalcode("Q")
582                                        end
583                                        local path = object.htap
584                                        if path then
585                                            if transformed then
586                                                pdf_literalcode("q")
587                                            end
588                                            if savedhtap then
589                                                for i=1,#savedhtap do
590                                                    local path = savedhtap[i]
591                                                    if transformed then
592                                                        flushconcatpath(path,open)
593                                                    else
594                                                        flushnormalpath(path,open)
595                                                    end
596                                                end
597                                                savedhtap = nil
598                                                evenodd   = true
599                                            end
600                                            if transformed then
601                                                flushconcatpath(path,open)
602                                            else
603                                                flushnormalpath(path,open)
604                                            end
605                                            if objecttype == "fill" then
606                                                pdf_literalcode("h f")
607                                            elseif objecttype == "outline" then
608                                                pdf_literalcode(evenodd and "h f*" or "h f")
609                                            elseif objecttype == "both" then
610                                                pdf_literalcode(evenodd and "h B*" or "h B")
611                                            end
612                                            if transformed then
613                                                pdf_literalcode("Q")
614                                            end
615                                        end
616                                        if cr then
617                                            pdf_literalcode(cr)
618                                        end
619                                    end
620                                end
621                           end
622                        end
623                        pdf_literalcode("Q")
624                        pdf_stopfigure()
625                    end
626                end
627            end
628        end
629    end
630
631    function metapost.colorconverter(cr)
632        local n = #cr
633        if n == 4 then
634            local c, m, y, k = cr[1], cr[2], cr[3], cr[4]
635            return format("%.3f %.3f %.3f %.3f k %.3f %.3f %.3f %.3f K",c,m,y,k,c,m,y,k), "0 g 0 G"
636        elseif n == 3 then
637            local r, g, b = cr[1], cr[2], cr[3]
638            return format("%.3f %.3f %.3f rg %.3f %.3f %.3f RG",r,g,b,r,g,b), "0 g 0 G"
639        else
640            local s = cr[1]
641            return format("%.3f g %.3f G",s,s), "0 g 0 G"
642        end
643    end
644
645end
646