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