1if not modules then modules = { } end modules ['mlib-pdf'] = {
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
9local gsub = string.gsub
10local concat, insert, remove, sortedkeys = table.concat, table.insert, table.remove, table.sortedkeys
11local sqrt, round = math.sqrt, math.round
12local setmetatable, rawset, tostring, tonumber, type = setmetatable, rawset, tostring, tonumber, type
13local P, S, C, Ct, Cc, Cg, Cf, Carg = lpeg.P, lpeg.S, lpeg.C, lpeg.Ct, lpeg.Cc, lpeg.Cg, lpeg.Cf, lpeg.Carg
14local lpegmatch = lpeg.match
15local formatters = string.formatters
16
17local report_metapost = logs.reporter("metapost")
18
19local trace_variables = false trackers.register("metapost.variables",function(v) trace_variables = v end)
20
21local mplib = mplib
22local context = context
23
24local allocate = utilities.storage.allocate
25
26local peninfo = mplib.peninfo
27local getfields = mplib.getfields or mplib.fields
28
29local save_table = false
30local force_stroke = false
31
32metapost = metapost or { }
33local metapost = metapost
34
35metapost.flushers = metapost.flushers or { }
36local pdfflusher = { }
37metapost.flushers.pdf = pdfflusher
38
39metapost.n = 0
40
41local mpsliteral = nodes.pool.originliteral
42
43local f_f = formatters["%.6N"]
44local f_m = formatters["%.6N %.6N m"]
45local f_c = formatters["%.6N %.6N %.6N %.6N %.6N %.6N c"]
46local f_l = formatters["%.6N %.6N l"]
47local f_cm = formatters["%.6N %.6N %.6N %.6N %.6N %.6N cm"]
48local f_M = formatters["%.6N M"]
49local f_j = formatters["%i j"]
50local f_J = formatters["%i J"]
51local f_d = formatters["[%s] %.6N d"]
52local f_w = formatters["%.6N w"]
53local f_r = formatters["%.6N %.6N %.6N %.6N re"]
54
55directives.register("metapost.savetable",function(v)
56 if type(v) == "string" then
57 save_table = file.addsuffix(v,"mpl")
58 elseif v then
59 save_table = file.addsuffix(environment.jobname .. "-graphic","mpl")
60 else
61 save_table = false
62 end
63end)
64
65trackers.register("metapost.forcestroke",function(v)
66 force_stroke = v
67end)
68
69
70
71
72
73function metapost.convert(specification,result)
74 local flusher = specification.flusher
75 local askedfig = specification.askedfig
76 if save_table then
77 table.save(save_table,metapost.totable(result,1))
78 end
79 metapost.flush(specification,result)
80 return true
81end
82
83function pdfflusher.tocomment(message)
84 if message then
85 return formatters["%% mps graphic %s: %s"](metapost.n,message)
86 else
87 return formatters["%% mps graphic %s"](metapost.n)
88 end
89end
90
91function pdfflusher.startfigure(n,llx,lly,urx,ury,message,figure,plugmode)
92 metapost.n = metapost.n + 1
93 context.startMPLIBtoPDF(f_f(llx),f_f(lly),f_f(urx),f_f(ury),plugmode and "1" or "0")
94end
95
96function pdfflusher.stopfigure(message)
97 context.stopMPLIBtoPDF()
98end
99
100function pdfflusher.flushfigure(pdfliterals)
101 if #pdfliterals > 0 then
102 pdfliterals = concat(pdfliterals,"\n")
103 context(mpsliteral(pdfliterals))
104 end
105end
106
107function pdfflusher.textfigure(font,size,text,width,height,depth)
108 text = gsub(text,".","\\hbox{%1}")
109 context.MPtextext(font,size,text,0,-number.dimenfactors.bp*depth)
110end
111
112local rx, sx, sy, ry, tx, ty, divider = 1, 0, 0, 1, 0, 0, 1
113
114local function pen_characteristics(object)
115 local t = peninfo(object)
116 rx, ry, sx, sy, tx, ty = t.rx, t.ry, t.sx, t.sy, t.tx, t.ty
117 divider = sx*sy - rx*ry
118 return not (sx == 1 and rx == 0 and ry == 0 and sy == 1 and tx == 0 and ty == 0), t.width
119end
120
121local function mpconcat(px, py)
122 return (sy*px-ry*py)/divider,(sx*py-rx*px)/divider
123end
124
125local getbendtolerance = metapost.getbendtolerance
126local curved = metapost.hascurvature
127
128local function flushnormalpath(path,t,open,tolerance)
129 local pth, ith, nt
130 local length = #path
131 if t then
132 nt = #t
133 else
134 t = { }
135 nt = 0
136 end
137 if length == 4 and path.cycle then
138 local p1 = path[1]
139 local p2 = path[2]
140 local p3 = path[3]
141 local p4 = path[4]
142 if not p1.curved and not p2.curved and not p3.curved and not p4.curved then
143 local p1x = p1.x_coord
144 local p1y = p1.y_coord
145 local p3x = p3.x_coord
146 local p3y = p3.y_coord
147 if p1y == p2.y_coord and p1x == p4.x_coord and p2.x_coord == p3x and p3y == p4.y_coord then
148 nt = nt + 1
149 t[nt] = f_r(p1.x_coord,p1y,p3x-p1x,p3y-p1y)
150 path.rectangle = true
151 return
152 end
153 end
154 end
155 for i=1,length do
156 nt = nt + 1
157 pth = path[i]
158 if not ith or pth.state == 1 then
159 t[nt] = f_m(pth.x_coord,pth.y_coord)
160
161 elseif pth.curved then
162 t[nt] = f_c(ith.right_x,ith.right_y,pth.left_x,pth.left_y,pth.x_coord,pth.y_coord)
163 else
164 t[nt] = f_l(pth.x_coord,pth.y_coord)
165 end
166 ith = pth
167 end
168 if not open then
169 nt = nt + 1
170 local one = path[1]
171 local len = path[length]
172 if len.state == 2 then
173
174 elseif one.state == 1 then
175 t[nt] = f_m(one.x_coord,one.y_coord)
176
177 elseif one.curved then
178 t[nt] = f_c(pth.right_x,pth.right_y,one.left_x,one.left_y,one.x_coord,one.y_coord)
179 else
180 t[nt] = f_l(one.x_coord,one.y_coord)
181 end
182 elseif length == 1 then
183
184 local one = path[1]
185 nt = nt + 1
186 t[nt] = f_l(one.x_coord,one.y_coord)
187 end
188 return t
189end
190
191local function flushconcatpath(path,t,open,tolerance,transform)
192 local pth, ith, nt
193 local length = #path
194 if t then
195 nt = #t
196 else
197 t = { }
198 nt = 0
199 end
200 if transform then
201 nt = nt + 1
202 t[nt] = f_cm(sx,rx,ry,sy,tx,ty)
203 end
204 for i=1,length do
205 nt = nt + 1
206 pth = path[i]
207 if not ith or pth.state == 1 then
208 t[nt] = f_m(mpconcat(pth.x_coord,pth.y_coord))
209
210 elseif pth.curved then
211 local a, b = mpconcat(ith.right_x,ith.right_y)
212 local c, d = mpconcat(pth.left_x,pth.left_y)
213 t[nt] = f_c(a,b,c,d,mpconcat(pth.x_coord,pth.y_coord))
214 else
215 t[nt] = f_l(mpconcat(pth.x_coord, pth.y_coord))
216 end
217 ith = pth
218 end
219 if not open then
220 nt = nt + 1
221 local one = path[1]
222 local len = path[length]
223 if len.state == 2 then
224
225 elseif one.state == 1 then
226 t[nt] = f_m(one.x_coord,one.y_coord)
227
228 elseif one.curved then
229 local a, b = mpconcat(pth.right_x,pth.right_y)
230 local c, d = mpconcat(one.left_x,one.left_y)
231 t[nt] = f_c(a,b,c,d,mpconcat(one.x_coord, one.y_coord))
232 else
233 t[nt] = f_l(mpconcat(one.x_coord,one.y_coord))
234 end
235 elseif length == 1 then
236
237 nt = nt + 1
238 local one = path[1]
239 t[nt] = f_l(mpconcat(one.x_coord,one.y_coord))
240 end
241 return t
242end
243
244local function toboundingbox(path)
245 local size = #path
246 if size == 4 then
247 local pth = path[1]
248 local x = pth.x_coord
249 local y = pth.y_coord
250 local llx, lly, urx, ury = x, y, x, y
251 for i=2,size do
252 pth = path[i]
253 x = pth.x_coord
254 y = pth.y_coord
255 if x < llx then
256 llx = x
257 elseif x > urx then
258 urx = x
259 end
260 if y < lly then
261 lly = y
262 elseif y > ury then
263 ury = y
264 end
265 end
266 return { llx, lly, urx, ury }
267 else
268 return { 0, 0, 0, 0 }
269 end
270end
271
272function metapost.flushnormalpath(path, t, open, tolerance)
273 return flushnormalpath(path, t, open, tolerance or getbendtolerance())
274end
275
276
277
278
279
280
281
282
283
284
285
286local ignore = function() end
287
288local space = P(" ")
289local equal = P("=")
290local key = C((1-equal)^1) * equal
291local newline = S("\n\r")^1
292local number = (((1-space-newline)^1) / tonumber) * (space^0)
293
294local p_number = number
295local p_string = C((1-newline)^0)
296local p_boolean = P("false") * Cc(false) + P("true") * Cc(true)
297local p_set = Ct(number^1)
298local p_path = Ct(Ct(number * number^-5)^1)
299
300local variable =
301 P("1:") * p_number
302 + P("2:") * p_string
303 + P("3:") * p_boolean
304 + S("4568") * P(":") * p_set
305 + P("7:") * p_path
306
307local pattern_tab = Cf ( Carg(1) * (Cg(variable * newline^0)^0), rawset)
308
309local variable =
310 P("1:") * p_number
311 + P("2:") * p_string
312 + P("3:") * p_boolean
313 + S("4568") * P(":") * number^1
314 + P("7:") * (number * number^-5)^1
315
316local pattern_lst = (variable * newline^0)^0
317
318metapost.variables = { }
319metapost.properties = { }
320
321function metapost.untagvariable(str,variables)
322 if variables == false then
323 return lpegmatch(pattern_lst,str)
324 else
325 return lpegmatch(pattern_tab,str,1,variables or { })
326 end
327end
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345local stack = { }
346local nostacking = { 0 }
347
348local function pushproperties(figure)
349
350 local boundingbox = figure:boundingbox()
351 local slot = figure:charcode() or 0
352 local properties = {
353 llx = boundingbox[1],
354 lly = boundingbox[2],
355 urx = boundingbox[3],
356 ury = boundingbox[4],
357 slot = slot,
358 width = figure:width(),
359 height = figure:height(),
360 depth = figure:depth(),
361 italic = figure:italic(),
362 number = slot,
363 }
364 insert(stack,properties)
365 metapost.properties = properties
366 return properties
367end
368
369local function popproperties()
370 metapost.properties = remove(stack)
371end
372
373local optimizecolor = false
374
375directives.register("metapost.optimizecolor", function(v)
376 optimizecolor = v
377end)
378
379local function enhancedobject(original)
380 return setmetatable({ }, { __index = original })
381end
382
383function metapost.flush(specification,result)
384 if result then
385 local flusher = specification.flusher
386 local askedfig = specification.askedfig
387 local incontext = specification.incontext
388 local filtering = specification.filtering
389 local figures = result.fig
390 if figures then
391 flusher = flusher or pdfflusher
392local plugmode = not metapost.getbackendoption(specification.mpx,"noplugins")
393 local resetplugins = plugmode and metapost.resetplugins or ignore
394 local processplugins = plugmode and metapost.processplugins or ignore
395 local synchronizeplugins = plugmode and metapost.synchronizeplugins or ignore
396 local pluginactions = plugmode and metapost.pluginactions or ignore
397 local startfigure = flusher.startfigure
398 local stopfigure = flusher.stopfigure
399 local flushfigure = flusher.flushfigure
400 local textfigure = flusher.textfigure
401
402 local tocomment = flusher.tocomment
403 if type(filtering) ~= "table" or not next(filtering) then
404 filtering = false
405 end
406
407
408
409 for index=1,#figures do
410 local figure = figures[index]
411 local properties = pushproperties(figure)
412 if askedfig == "direct" or askedfig == "all" or askedfig == properties.number then
413 local stacking = figure:stacking()
414 local objects = figure:objects()
415 local tolerance = figure:tolerance() or getbendtolerance()
416 local result = { }
417 local miterlimit = -1
418 local linecap = -1
419 local linejoin = -1
420 local dashed = false
421 local linewidth = false
422 local llx = properties.llx
423 local lly = properties.lly
424 local urx = properties.urx
425 local ury = properties.ury
426 if urx < llx then
427
428 startfigure(properties.number,0,0,0,0,"invalid",figure,plugmode)
429 if tocomment then
430 result[#result+1] = tocomment("invalid")
431 end
432 stopfigure()
433 else
434
435
436
437 local groupstack = { }
438
439 local function processfigure()
440 if tocomment then
441 result[#result+1] = tocomment("begin")
442 end
443 result[#result+1] = "q"
444 if objects then
445
446 local savedpath = nil
447 local savedhtap = nil
448 if stacking then
449 stacking = { }
450 for o=1,#objects do
451 local stack = objects[o].stacking
452 if stack then
453 if filtering then
454 stacking[stack] = filtering[stack]
455 else
456 stacking[stack] = true
457 end
458 end
459 end
460 stacking = sortedkeys(stacking)
461 else
462 stacking = nostacking
463 end
464 for i=1,#stacking do
465 local stack = stacking[i]
466 for o=1,#objects do
467 local object = objects[o]
468 if stack == object.stacking then
469 local objecttype = object.type
470 if objecttype == "fill" or objecttype == "outline" then
471
472
473
474
475
476 local original = object
477 local object = { }
478 setmetatable(object, {
479 __index = original
480 })
481 local before,
482 after,
483 options = processplugins(object)
484 local evenodd = false
485 local collect = false
486 local both = false
487 local flush = false
488 local outline = force_outline
489 local envelope = false
490 local postscript = object.postscript
491 local tolerance = options and tonumber(options.tolerance) or tolerance
492
493 if postscript == "evenodd" then
494 evenodd = true
495 elseif postscript == "collect" then
496 collect = true
497 elseif postscript == "flush" then
498 flush = true
499 elseif postscript == "both" then
500 both = true
501 elseif postscript == "eoboth" then
502 evenodd = true
503 both = true
504 elseif postscript == "envelope" then
505 envelope = true
506 end
507
508
509 if flush and not savedpath then
510
511 elseif collect then
512 if not savedpath then
513 savedpath = { object.path or false }
514 savedhtap = { object.htap or false }
515 else
516 savedpath[#savedpath+1] = object.path or false
517 savedhtap[#savedhtap+1] = object.htap or false
518 end
519 else
520 local objecttype = object.type
521 if envelope then
522 dashed, linewidth = "", 1
523 end
524 if before then
525
526
527
528
529
530
531
532
533
534
535
536
537 if optimizecolor and after then
538 local r = #result
539 if result[r] == "0 g 0 G" and (after[1] == "0 g 0 G" or after[2] == "0 g 0 G") then
540 result[r] = nil
541 r = r - 1
542 end
543 if result[r] == "/Tr0 gs" and (after[1] == "/Tr0 gs" or after[2] == "/Tr0 gs") then
544 result[r] = nil
545 end
546 end
547
548 result = pluginactions(before,result,flushfigure)
549 end
550 local ml = object.miterlimit
551 if ml and ml ~= miterlimit then
552 miterlimit = ml
553 result[#result+1] = f_M(ml)
554 end
555 local lj = object.linejoin
556 if lj and lj ~= linejoin then
557 linejoin = lj
558 result[#result+1] = f_j(lj)
559 end
560 local lc = object.linecap
561 if lc and lc ~= linecap then
562 linecap = lc
563 result[#result+1] = f_J(lc)
564 end
565 if both then
566 if dashed ~= false then
567 result[#result+1] = "[] 0 d"
568 dashed = false
569 end
570 else
571 local dl = object.dash
572 if dl then
573 local d = f_d(concat(dl.dashes or {}," "),dl.offset)
574 if d ~= dashed then
575 dashed = d
576 result[#result+1] = d
577 end
578 elseif dashed ~= false then
579 result[#result+1] = "[] 0 d"
580 dashed = false
581 end
582 end
583 local path = object.path
584 local transformed = false
585 local penwidth = 1
586 local open = path and path[1].left_type and path[#path].right_type
587 local pen = object.pen
588 if pen then
589 if pen.type == "elliptical" or outline then
590 transformed, penwidth = pen_characteristics(original)
591 if penwidth ~= linewidth then
592 result[#result+1] = f_w(penwidth)
593 linewidth = penwidth
594 end
595 if objecttype == "fill" then
596 objecttype = "both"
597 end
598 else
599 objecttype = "fill"
600 end
601 end
602 if transformed then
603 result[#result+1] = "q"
604 end
605 if path then
606 if savedpath then
607 for i=1,#savedpath do
608 local path = savedpath[i]
609 local open = not path.cycle
610 if transformed then
611 flushconcatpath(path,result,open,tolerance,i==1)
612 else
613 flushnormalpath(path,result,open,tolerance)
614 end
615 end
616 savedpath = nil
617 end
618 if flush then
619
620 elseif transformed then
621 flushconcatpath(path,result,open,tolerance,true)
622 else
623 flushnormalpath(path,result,open,tolerance)
624 end
625 if path.rectangle then
626 if outline or envelope then
627 result[#result+1] = "S"
628 elseif objecttype == "fill" then
629 result[#result+1] = "f"
630 elseif objecttype == "outline" then
631 result[#result+1] = both and "B" or "S"
632 elseif objecttype == "both" then
633 result[#result+1] = "B"
634 end
635 elseif outline or envelope then
636 result[#result+1] = open and "S" or "h S"
637 elseif objecttype == "fill" then
638 result[#result+1] = evenodd and "h f*" or "h f"
639 elseif objecttype == "outline" then
640 if both then
641 result[#result+1] = evenodd and "h B*" or "h B"
642 else
643 result[#result+1] = open and "S" or "h S"
644 end
645 elseif objecttype == "both" then
646 result[#result+1] = evenodd and "h B*" or "h B"
647 end
648 end
649 if transformed then
650 result[#result+1] = "Q"
651 end
652 local path = object.htap
653 if path then
654 if transformed then
655 result[#result+1] = "q"
656 end
657 if savedhtap then
658 for i=1,#savedhtap do
659 local path = savedhtap[i]
660 local open = not path.cycle
661 if transformed then
662 flushconcatpath(path,result,open,tolerance,i==1)
663 else
664 flushnormalpath(path,result,open,tolerance)
665 end
666 end
667 savedhtap = nil
668 evenodd = true
669 end
670 if transformed then
671 flushconcatpath(path,result,open,tolerance,true)
672 else
673 flushnormalpath(path,result,open,tolerance)
674 end
675 if outline or envelope then
676 result[#result+1] = open and "S" or "h S"
677 elseif objecttype == "fill" then
678 result[#result+1] = evenodd and "h f*" or "h f"
679 elseif objecttype == "outline" then
680 result[#result+1] = open and "S" or "h S"
681 elseif objecttype == "both" then
682 result[#result+1] = evenodd and "h B*" or "h B"
683 end
684 if transformed then
685 result[#result+1] = "Q"
686 end
687 end
688 if after then
689 result = pluginactions(after,result,flushfigure)
690 end
691 end
692 if object.grouped then
693
694 miterlimit, linecap, linejoin, dashed, linewidth = -1, -1, -1, "", false
695 end
696 elseif objecttype == "start_clip" then
697
698 local evenodd = object.postscript == "evenodd"
699 result[#result+1] = "q"
700 flushnormalpath(object.path,result,false,tolerance)
701 result[#result+1] = evenodd and "W* n" or "W n"
702 elseif objecttype == "stop_clip" then
703 result[#result+1] = "Q"
704 miterlimit, linecap, linejoin, dashed, linewidth = -1, -1, -1, "", false
705 elseif objecttype == "start_bounds" or objecttype == "stop_bounds" then
706
707 elseif objecttype == "start_group" then
708 if lpdf.flushgroup then
709 local object = enhancedobject(object)
710 local before, after = processplugins(object)
711 local detail = object.detail
712 if detail or before then
713 result[#result+1] = "q"
714 result = pluginactions(before,result,flushfigure)
715 insert(groupstack, {
716 after = after,
717 result = result,
718 detail = detail,
719 bbox = toboundingbox(object.path),
720 })
721 result = { }
722 miterlimit, linecap, linejoin, dashed, linewidth = -1, -1, -1, "", false
723 else
724 insert(groupstack,false)
725 end
726 else
727 insert(groupstack,false)
728 end
729 elseif objecttype == "stop_group" then
730 local data = remove(groupstack)
731 if data then
732 local reference = lpdf.flushgroup(concat(result,"\r"),data.bbox,data.detail)
733
734 result = data.result
735 result[#result+1] = reference
736 result = pluginactions(data.after,result,flushfigure)
737 result[#result+1] = "Q"
738 miterlimit, linecap, linejoin, dashed, linewidth = -1, -1, -1, "", false
739 end
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754 end
755 end
756 end
757 end
758 end
759 result[#result+1] = "Q"
760 if tocomment then
761 result[#result+1] = tocomment("end")
762 end
763 flushfigure(result)
764 end
765 startfigure(properties.number,llx,lly,urx,ury,"begin",figure,plugmode)
766 if incontext then
767 context(function() processfigure() end)
768 else
769 processfigure()
770 end
771 stopfigure("end")
772 end
773 if askedfig ~= "all" then
774 break
775 end
776 end
777 popproperties()
778 end
779 resetplugins(result)
780 end
781 end
782end
783
784
785
786do
787
788 local result = { }
789
790 local flusher = {
791 startfigure = function()
792 result = { }
793 context.startnointerference()
794 end,
795 flushfigure = function(literals)
796 local n = #result
797 for i=1,#literals do
798 result[n+i] = literals[i]
799 end
800 end,
801 stopfigure = function()
802 context.stopnointerference()
803 end
804 }
805
806 local specification = {
807 flusher = flusher,
808
809 }
810
811 function metapost.pdfliterals(result)
812 metapost.flush(specification,result)
813 return result
814 end
815
816end
817
818function metapost.totable(result,askedfig)
819 local askedfig = askedfig or 1
820 local figure = result and result.fig and result.fig[1]
821 if figure then
822 local results = { }
823 local objects = figure:objects()
824 for o=1,#objects do
825 local object = objects[o]
826 local result = { }
827 local fields = getfields(object)
828 for f=1,#fields do
829 local field = fields[f]
830 result[field] = object[field]
831 end
832 results[o] = result
833 end
834 local boundingbox = figure:boundingbox()
835 return {
836 boundingbox = {
837 llx = boundingbox[1],
838 lly = boundingbox[2],
839 urx = boundingbox[3],
840 ury = boundingbox[4],
841 },
842 objects = results
843 }
844 else
845 return nil
846 end
847end
848 |