Producing printers crop marks with MetaPost and LuaTeX nodes

Introduction

In this article I’ll show a technique for producing crop marks using the MetaPost library (MPlib) which is built into LuaTeX. There is a lot of ground to cover so I’ll try to focus on/summarise the most important/useful areas to prevent this article becoming far too long! It has already taken a few days to write it and prepare the graphics… I’m not going to attempt a tutorial on the MetaPost language because there are many excellent articles already written by people who are best qualified to produce that material. A great starting point is the TeX Users Group page on MetaPost.

SVG-enabled browser needed: This post uses inline SVG and SVG via in iframe, which may not work in all browsers.

Citing various sources: a few thanks are in order

Much of the material in this post is derived from existing work by members of the LuaTeX community, so I’d like to acknowledge those sources. Firstly, a huge thank you to Hans Hagen for creating the Lua code which converts the MPlib data to raw PDF code. It is a really excellent and hugely useful piece of code which is part of the ConTeXt distribution. In addition, I learnt a lot by reading the source code of luamplib by Hans Hagen, Taco Hoekwater, Elie Roux and Manuel Pégourié-Gonnard. I also discovered some code by Dohyun Kim which makes some very helpful additions to luamplib through which you can support the traditional btex … etex construct for including TeX within MetaPost code processed by MPlib (more of that in another post).

Pieces of the process

The key elements of the technique I’ll describe are:

  1. Embedding MetaPost code (to draw a crop mark) within your LuaTeX document.
  2. Converting the MetaPost output to PDF code (using the code from Hans Hagen’s ConTeXt).
  3. Generating pdf_literal nodes representing the crop marks (modification to luamplib).
  4. Placing the crop marks in the appropriate locations through an output routine.

What are crop marks?

Crop marks, also referred to as “printers marks”, “cut marks” or “trim marks”, are small graphics placed at the corners of a page to indicate the physical size of a final printed document pages. They are used during commercial printing activities, such as page imposition, colour separation, folding and trimming. The physical appearance of crop marks will vary depending on the application used to generate the pages but, of course, with LuaTeX and other TeX engines you are free to create your own designs. Advanced readers will be aware that for multi-colour separations (spot and 4 colour CMYK) the crop marks must appear on all plates but I’ll not cover that topic here. It would be fairly easy to do through injecting appropriate PDF code to set the colour space for the crop marks or using the Printer’s Mark Annotations feature of PDF.

The following graphic (produced with Inkscape) shows the general idea.

The following inline SVG graphic (produced directly by exporting from MPlib) shows the design of the crop mark we will be producing via MetaPost code.


The MetaPost code

I’ll readily admit that I’m an amateur when it comes to programming with MetaPost so due apologies to any experts reading the code :-). The idea here is that the MetaPost code is inline in the LuaTeX document and I’m using the process_input_buffer callback (see this post) to store the MetaPost code into a buffer which will processed via MPlib. The \startbuffer and \stopbuffer commands were described in an earlier article.

\def\startbuffer{\directlua{callback.register("process_input_buffer",dobuffer)}}
\def\stopbuffer{\directlua{callback.register("process_input_buffer",nil)}}
\startbuffer
beginfig(1);
numeric n,g,tl, alpha; 
pair dl,zc,zl,zr,zt,zb,db;
path c, t;
alpha:=0.75;
n:=2;
g:=72*(3/25.4);
tl:=(n+1)*g;
pickup pensquare scaled 0.5bp;
draw (0,g)--(0,tl);
draw (-g,0)--(-tl,0);
rd:=0.5*tl;
rc:=0.5*rd;
dl=(-(1+alpha)*rc,0);
db=(0,-(1+alpha)*rc);
zc=(-rd,rd);
zl=zc+dl;
zr=zc-dl;
zt=zc+db;
zb=zc-db;
t=zc+(0.9*rc,0)--zc-(0.9*rc,0);
c = fullcircle scaled 2rc shifted(-rd,rd);
pickup pensquare scaled 0.5bp;
draw zl--zr;
draw zt--zb;
fill c withcolor black;
pickup pensquare scaled 1bp;
draw t withcolor white;
draw t  rotatedabout(zc, 90) withcolor white;
endfig;
end;
\stopbuffer%

Very brief introduction to MPlib

As mentioned, MPlib is the library version on the MetaPost interpreter built into LuaTeX. Instead of using MetaPost as a standalone executable (e.g., mpost.exe) you access it through an API provided by the LuaTeX engine. The value of the integration of MetaPost with LuaTeX is well demonstrated by the truly stunning results achieved by Hans Hagen and the ConTeXt distribution. The ambitions of this article are rather more modest.

In outline the steps are as follows.

  1. You need to create a “finder” function which MPlib will call to locate any files it needs.
  2. Provide the finder function as one of the arguments to the API call mplib.new() which
    is responsible for creating an instance of the interpreter.
  3. Load the “format file” containing the macro package you want to use (e.g., plain.mp).
  4. Use the interpreter instance returned by mplib.new() to execure your code.
  5. Process the figure objects generated and returned by MPlib (if your code worked without error).

Here is some sample code for a finder function and creating an instance of the MetaPost interpreter.

-- finder function for MPlib
function finder(name, mode, ftype)
        local found 
                if mode=="w" then found = name else 
                        found = kpse.find_file(name,ftype) 
                end 
                if found then
                        print("MPlib finder: " .. name .. " -> " .. found) 
                end
        return found 
end 

-- create new interpreter instance
function newmp (memname)
        local preamble = "let dump = endinput ; input \%s ;"
        local mp = mplib.new {
            ini_version = true,
            find_file = finder,
        }
        mp:execute(string.format(preamble, memname))
        return mp
 end

MetaPost to PDF or SVG

One nice feature of MPlib is that it will automatically generate an SVG representation of the graphic produced from the MetaPost code, and that is how the inline SVG crop mark (above) was produced. The MPlib library will also PostScript code and an “object representation” of the graphic. The “object representation” can be parsed to convert the graphic into other data formats and this is how the PDF data is generated by the ConTeXt Lua code contained in luamplib. It runs over the collection of objects and converts them to the equivalent representation in PDF data/structures. I really admire that code and it’s great to have it available.

Re-using code and ideas in luamplib

Within the luamplib distribution is the core Lua code from ConTeXt which does the “heavy lifting” to convert MPlib data structures to PDF code. For the purposes of the work described in this article I re-used that Lua code and placed it into Lua file (mpnodes.lua) which can be downloaded here. To use it you’ll need to load it as a Lua module:

\directlua{require(“mpnodes.lua”)}

Using mpnodes.lua

The mpnodes.lua module contains functions which create a new instance of the MetaPost interpreter (via MPlib), execute MetaPost code and use the functions from ConTeXt to generate the PDF data. During the process of generating PDF data, the Lua code makes a number of calls to defined TeX macros (originally in luamplib.sty) which, in effect, pass the PDF data to the (Lua)TeX engine. The TeX macros of interest here are:

  • \def\mplibstarttoPDF#1#2#3#4{….}
  • \def\mplibtoPDF#1{…}
  • \def\mplibstoptoPDF{…}

As part of this implementation those macros were redefined to work with LuaTeX nodes. Examples will be provided later in the article.

Note: the mpnodes.lua module also contains other functions which I won’t describe: including work based on the code from Dohyun Kim which implements the btex … etex functionality of the standalone MetaPost interpreter. The MPlib version of MetaPost does not directly support the btex … etex construct; other methods have to be employed to include TeX code within MetaPost graphics processed by MPlib. Again, those methods are based on the pioneering work of the ConTeXt distribution. I’ll write about this in a future post.

Creating nodes to store crop marks

If you look at the figure above, which shows 4 crop marks on a page, you can see that you only need to create 1 crop mark graphic and then rotate it in increments of 90 degrees as you place it at the 4 corners. In the approach, described below, the PDF data generated from the MetaPost graphic is stored as LuaTeX pdf_literal nodes which are then drawn at each corner via the \output routine described below.

Enough description, here’s the code

Here’s the Plain-TeX-based code with inline comments.

\pdfoutput=1
\pdfcompresslevel=0
\hoffset-1in
\voffset-1in
\pdfpageheight=200mm
\pdfpagewidth=300mm
\vsize=100mm
\hsize=200mm
\topskip=0pt
\maxdepth=0pt

% This output routine centres \box255 horizontally and vertically
% on the PDF page and ships out 4 boxes (2000--2003) onto every page.
% These boxes contain the crop mark graphic, suitably rotated.
% The \output routine is described in the article text.

\output={\shipout\vbox to \pdfpageheight{%
\vfill%
\vbox to \vsize{\offinterlineskip%
\vfill%
\hbox to\pdfpagewidth{\hfill\hbox to \hsize{\copy2000\hfill\copy2001}\hfill}%
\hbox to \pdfpagewidth{\hfill\box255 \hfill}%
\hbox to \pdfpagewidth{\hfill\hbox to \hsize{\copy2003\hfill\copy2002}\hfill}%
\vfill}%
\vfill%
}%pdfpageheight
}%output

% Load the mpnodes.lua module

\directlua{require("mpnodes")}

% Here we redefine the macros in luamplib to 
% work with nodes. 


% \mplibstarttoPDF simply stores the coordinates of the bounding box of the MetaPost
% graphic in 4 TeX tokens, but they are not used in the code below.

\def\mplibstarttoPDF#1#2#3#4{%
\directlua{
		tex.toks[500]=#1
		tex.toks[501]=#2
		tex.toks[502]=#3
		tex.toks[503]=#4		 
%tex.print(tex.toks[500],tex.toks[501],tex.toks[502],tex.toks[503])
}}

% \mplibtoPDF stores each line of PDF data generated from Lua
% and builds it up into one long string. Note I am adding a carriage
% return to each line (string.char(10))

\def\mplibtoPDF#1{%
\directlua{
		buffy= buffy or ""
		buffy=buffy..string.char(10).."#1"
}}

% Here's the main work. Once the PDF data describing the crop mark has been collected 
% the final macro call made by the Lua code is to \mplibstoptoPDF. In this macro 
% we create 4 pdf_literal nodes which only differ by adding extra PDF data to rotate the  
% crop mark in increments of 90 degrees. The PDF data has been collected by \mplibtoPDF
% in a text string called "buffy".

\def\mplibstoptoPDF{%
\directlua{
	rad=math.rad
	sin=math.sin
	cos=math.cos

% Function to generate PDF transformation (rotation) matrices.
function rotate(angle)
	local d=string.format("q 0 0 m \%3.3f  \%3.3f  \%3.3f \%3.3f  0 0 cm ", 
	cos(rad(angle)), 
	sin(rad(angle)),
	-sin(rad(angle)), 
	cos(rad(angle)))
	return d
end

% The names of the pdf_literal nodes reflect their position on the page.
% Note: PDF rotations are counter-clockwise hence negative angles used
% in the code below.

pdftopleft = node.new("whatsit","pdf_literal")
pdftopleft.mode=0
pdftopleft.data=buffy

pdftopright = node.new("whatsit","pdf_literal")
pdftopright.mode=0

rot90=rotate(-90)
print(rot90)%..buffy.." Q")
pdftopright.data= rot90..buffy.." Q"

pdfbottomright = node.new("whatsit","pdf_literal") 
pdfbottomright.mode=0
rot180=rotate(180)
pdfbottomright.data=rot180..buffy.." Q"

pdfbottomleft = node.new("whatsit","pdf_literal") 
pdfbottomleft.mode=0
rot270=rotate(-270)
pdfbottomleft.data=rot270..buffy.." Q"

% We have our nodes now pack them into boxes for shipping
% out via the \output routine as \copy2000....\copy2003

tex.box[2000]= node.hpack(pdftopleft)
tex.box[2001]= node.hpack(pdftopright)
tex.box[2002]= node.hpack(pdfbottomright)
tex.box[2003]= node.hpack(pdfbottomleft)

}}

% Macro to create an instance of the MPlib interpreter and execute
% the MetaPost code collected by the \startbuffer and \stopbuffer macros.
% Lua code is stored in the mpnodes.lua module hence you prefix the functions
% with "mpnodes" to call them: mpnodes.function_name(...)

\def\runmpcode{%
\directlua{
%print(buffer)

%Error checking could be improved here...
mp=mpnodes.newmpx("plain")
res,err=mp:execute(buffer)
%print(res,err)
if res then 
	mpnodes.outputpdf(res) %core function to produce PDF data
		else
		print("No figures")
	end
}}

% Code to implement buffering the inline MetaPost code
\directlua{
function addline(line)
	line=line..string.char(10)
	%print("called with "..line)
	buffer = buffer..line
end}

% Code to implement buffering the inline MetaPost code
\directlua{
buffer=""
function dobuffer(line)
if string.match(line,"stopbuffer") then
	callback.register("process_input_buffer",nil)
	return ""
end
	addline(line)
%	print(line)
	return ""
end
}

% Code to implement buffering the inline MetaPost code through 
% process_input_buffer callback 
\def\startbuffer{\directlua{callback.register("process_input_buffer",dobuffer)}}
\def\stopbuffer{\directlua{callback.register("process_input_buffer",nil)}}
% Here's the inline MetaPost code
\startbuffer
beginfig(1);
numeric n,g,tl, alpha; 
pair dl,zc,zl,zr,zt,zb,db;
path c, t;
alpha:=0.75;
n:=2;
g:=72*(3/25.4);
tl:=(n+1)*g;
pickup pensquare scaled 0.5bp;
draw (0,g)--(0,tl);
draw (-g,0)--(-tl,0);
rd:=0.5*tl;
rc:=0.5*rd;
dl=(-(1+alpha)*rc,0);
db=(0,-(1+alpha)*rc);
zc=(-rd,rd);
zl=zc+dl;
zr=zc-dl;
zt=zc+db;
zb=zc-db;
t=zc+(0.9*rc,0)--zc-(0.9*rc,0);
c = fullcircle scaled 2rc shifted(-rd,rd);
pickup pensquare scaled 0.5bp;
draw zl--zr;
draw zt--zb;
fill c withcolor black;
pickup pensquare scaled 1bp;
draw t withcolor white;
draw t  rotatedabout(zc, 90) withcolor white;
endfig;
end;
\stopbuffer%
\runmpcode
\def\apar{Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet.."\par}%

\apar\apar\apar\apar\apar\apar\apar\apar\apar\apar\apar\apar
\bye

Sample PDF

Here’s an example PDF produced by the code above.

Have you spotted the flaw?

Readers who have a background in print production may have spotted a problem with the positioning of the crop marks and the text area defined by \box255: there is no white space between the text area and the crop marks, they are too close to the live text material. Can this be fixed? Yes, very easily. What you need to do is insert some vertical kerns to move the crop marks vertically, and offset the crop marks horizontally by adjusting the width of the horizontal boxes containing the crop marks. These actions will create offsets between the crops and the text area. Of course, these will have to be factored into any calculations for your page design but that should not be difficult. What this will mean is that the page size defined by the crop marks would then be:

  • width of page = \hsize + 2*(horizontal offset)
  • height of page = \vsize + 2*(vertical kern offset)

We’ll look at this below.

The output routine

A crucial part of the process is to assemble everything onto the final PDF page with the crop marks placed as required. In the following I’ll discuss a basic \output routine, that ignores many complications, such as inserts, but which could form the basis for your own experiments. It could, for example, be used as a starting point for with simpler documents such as business cards. I’ll also give an example of fixing the “crop marks offset” problem mentioned above.

So where do we start?

The \output routine is responsible for assembling the page components to achieve your desired page design and the way that the following output routine works is by wrapping \box255 in a series of \vboxes and \hboxes with flexible glues to centre \box255 on the page. The overall structure is an outer \vbox to the same size as the height of the PDF page followed by another \vbox to the value of \vsize. Very flexible (stretchy) vertical glue is used to fill the “space” above and below the inner \vbox, i.e., the space that needs to be filled due to the different heights of the two \vboxes (\pdfpageheight versus \vsize). This glue is responsible for vertical centring the inner \vbox.

Inside the second \vbox (the one to \vsize) we place a series of horizontal boxes (to a width of \pdfpagewidth) to contain the crop marks above and below \box255, plus the actual typeset content of \box255 itself. We use horizontal glue to centre everything… horizontally.

\output={\shipout\vbox to \pdfpageheight{%
\vfill%
\vbox to \vsize{\offinterlineskip%
\vfill%
\hbox to\pdfpagewidth{\hfill\hbox to \hsize{\copy2000\hfill\copy2001}\hfill}%
\hbox to \pdfpagewidth{\hfill\box255 \hfill}%
\hbox to \pdfpagewidth{\hfill\hbox to \hsize{\copy2003\hfill\copy2002}\hfill}%
\vfill}%
\vfill%
}%pdfpageheight
}%output

Placing the crop marks

Two of the three “\hboxes to \pdfpagewidth” contain yet another \hbox (of width \hsize) and the purpose of those is to place the crop marks above and below \box255.

\hbox to\pdfpagewidth{\hfill\hbox to \hsize{\copy2000\hfill\copy2001}\hfill}%
\hbox to \pdfpagewidth{\hfill\box255 \hfill}%
\hbox to \pdfpagewidth{\hfill\hbox to \hsize{\copy2003\hfill\copy2002}\hfill}%

Let’s take a look at the first one:

\hbox to\pdfpagewidth{\hfill\hbox to \hsize{\copy2000\hfill\copy2001}\hfill}%

The inner \hbox to \hsize{\copy2000\hfill\copy2001} contains an infinitely stretchable \hfill glue which streches to fill the \hbox. The key point is that the pdf_literal nodes from which the boxes 2000 and 2001 have zero width and so the \hfill glue pushes them to the far left and right of the containing \hbox.

If you look back at the Lua code which created the pdf_literal nodes:

pdftopleft = node.new(“whatsit”,”pdf_literal”)
pdftopleft.mode=0
pdftopleft.data=buffy

you’ll see that the “mode” is set to 0. This defines the origin for drawing them as the point on the page where they appear, which is just above and below \box255 thanks to the actions of the glue.

And finally, offsetting the crop marks

  • Vertically: one solution is simply to absorb some of the space which is occupied by the strechable glues, thus preventing the crop marks being pushed up against \box255.
  • Horizonally: adjust the size of the \hboxes containing the crop marks (make them wider).

Here’s one very quick example where we add 20pt offset vertically and increase the \hboxes containing the crop marks to 1.25 x \hsize. A proper solution would of course paramaterise everything.

\output={\shipout\vbox to \pdfpageheight{%
\vfill%
\vbox to \vsize{\offinterlineskip%
\vfill%
\hbox to\pdfpagewidth{\hfill\hbox to 1.25\hsize{\copy2000\hfill\copy2001}\hfill}%
\kern20pt%
\hbox to \pdfpagewidth{\hfill\box255 \hfill}%
\kern20pt%
\hbox to \pdfpagewidth{\hfill\hbox to 1.25\hsize{\copy2003\hfill\copy2002}\hfill}%
\vfill}%
\vfill%
}%pdfpageheight
}%output

And the resulting PDF:

Download mpnode.lua

You can download it here.