Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Another approach to full bleed printing, paper/sheet size, and pdf refactoring #1853

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion classes/base.lua
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,15 @@ end
function class:setOptions (options)
options = options or {}
-- Classes that add options with dependencies should explicitly handle them, then exempt them from furthur processing.
-- The landscape option is handled explicitly before papersize, then the "rest" of options that are not interdependent.
-- The landscape and crop related options are handled explicitly before papersize, then the "rest" of options that are not interdependent.
self.options.landscape = SU.boolean(options.landscape, false)
options.landscape = nil
self.options.papersize = options.papersize or "a4"
options.papersize = nil
self.options.bleed = options.bleed or "0"
options.bleed = nil
self.options.sheetsize = options.sheetsize or nil
options.sheetsize = nil
for option, value in pairs(options) do
self.options[option] = value
end
Expand Down Expand Up @@ -127,6 +131,33 @@ function class:declareOptions ()
end
return self.papersize
end)
self:declareOption("sheetsize", function (_, size)
if size then
self.sheetsize = size
SILE.documentState.sheetSize = SILE.papersize(size, self.options.landscape)
if SILE.documentState.sheetSize[1] < SILE.documentState.paperSize[1]
or SILE.documentState.sheetSize[2] < SILE.documentState.paperSize[2] then
SU.error("Sheet size shall not be smaller than the paper size")
end
if SILE.documentState.sheetSize[1] < SILE.documentState.paperSize[1] + SILE.documentState.bleed then
SU.debug("frames", "Sheet size width augmented to take page bleed into account")
SILE.documentState.sheetSize[1] = SILE.documentState.paperSize[1] + SILE.documentState.bleed
end
if SILE.documentState.sheetSize[2] < SILE.documentState.paperSize[2] + SILE.documentState.bleed then
SU.debug("frames", "Sheet size height augmented to take page bleed into account")
SILE.documentState.sheetSize[2] = SILE.documentState.paperSize[2] + SILE.documentState.bleed
end
else
return self.sheetsize
end
end)
self:declareOption("bleed", function (_, dimen)
if dimen then
self.bleed = dimen
SILE.documentState.bleed = SU.cast("measurement", dimen):tonumber()
end
return self.bleed
end)
end

function class.declareSettings (_)
Expand Down
21 changes: 21 additions & 0 deletions documentation/c03-input.sil
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,27 @@ The orientation of the page is defined as "portrait" by default, but if you want
\begin[landscape=true]{document}
\end{raw}

\subsection{Full bleed printing}

When preparing a book for press printing, you may be asked by the professional printer to output the document on a larger sheet than your target paper, and to reserve a trim area around it.
This trick is often called “full bleed printing”.
Your document will be printed on an oversized sheet that will then be mechanically cut down to the target size.
You can specify the expected “trim” (or “bleed”) dimension, to be distributed evenly on all sides
of the document:

\code{papersize=\em{<paper size>}, bleed=\em{<measurement>}}.

For instance, a US trade book with an extra 0.125 inch bleed area can be specified by \code{papersize=6in x 9in, bleed=0.25in}.
The output paper size is then 6.25 per 9.25 inches, with the actual 6 per 9 inches inner content centered.

Some packages, such as \autodoc:package{background} and \autodoc:package{cropmarks}, ensure their content extends over the trim area and thus indeed “bleeds” off the sides of the page, so that artifacts such as blank lines are avoided when the sheets are cut, would they be trimmed slightly differently for some assembling or technical reasons.

Finally, there is also the case when the actual paper sheets available to you are larger than your target paper size, and yet you would want the output document to show properly centered:

\code{papersize=\em{<paper size>}, sheetsize=\em{<actual paper size>}}.

For instance, \code{papersize=6in x 9in, sheetsize=a4} produces an A4-dimensioned document, but with you content formatted as a 6 per 9 inches US trade book. You may, obviously, combine these options and also specify a bleed area.

\section{Ordinary text}

On the whole, ordinary text isn’t particularly interesting—it’s just typeset.
Expand Down
10 changes: 10 additions & 0 deletions outputters/base.lua
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ function outputter.debugFrame (_, _, _) end

function outputter.debugHbox (_, _, _) end

function outputter.linkAnchor (_, _, _) end -- Unstable API

function outputter.enterLinkTarget (_, _, _) end -- Unstable API

function outputter.leaveLinkTarget (_, _, _, _, _, _, _) end -- Unstable API

function outputter.setMetadata (_, _, _) end

function outputter.setBookmark (_, _, _) end

function outputter:getOutputFilename ()
local fname
if SILE.outputFilename then
Expand Down
170 changes: 164 additions & 6 deletions outputters/libtexpdf.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,36 @@ local outputter = pl.class(base)
outputter._name = "libtexpdf"
outputter.extension = "pdf"

-- N.B. Sometimes setCoord is called before the outputter has ensured initialization.
-- This ok for coordinates manipulation, at these points we know the page size.
local deltaX
local deltaY
local function trueXCoord (x)
if not deltaX then
local sheetSize = SILE.documentState.sheetSize or SILE.documentState.paperSize
deltaX = (sheetSize[1] - SILE.documentState.paperSize[1]) / 2
end
return x + deltaX
end
local function trueYCoord (y)
if not deltaY then
local sheetSize = SILE.documentState.sheetSize or SILE.documentState.paperSize
deltaY = (sheetSize[2] - SILE.documentState.paperSize[2]) / 2
end
return y + deltaY
end

-- The outputter init can't actually initialize output (as logical as it might
-- have seemed) because that requires a page size which we don't know yet.
-- function outputter:_init () end

function outputter:_ensureInit ()
if not started then
local w, h = SILE.documentState.paperSize[1], SILE.documentState.paperSize[2]
local sheetSize = SILE.documentState.sheetSize or SILE.documentState.paperSize
local w, h = sheetSize[1], sheetSize[2]
local fname = self:getOutputFilename()
-- Ideally we could want to set the PDF CropBox, BleedBox, TrimBox...
-- Our wrapper only manages the MediaBox at this point.
pdf.init(fname == "-" and "/dev/stdout" or fname, w, h, SILE.full_version)
pdf.beginpage()
started = true
Expand Down Expand Up @@ -90,7 +112,7 @@ function outputter:_drawString (str, width, x_offset, y_offset)
local x, y = self:getCursor()
pdf.colorpush_rgb(0,0,0)
pdf.colorpop()
pdf.setstring(x+x_offset, y+y_offset, str, string.len(str), _font, width)
pdf.setstring(trueXCoord(x+x_offset), trueYCoord(y+y_offset), str, string.len(str), _font, width)
end

function outputter:drawHbox (value, width)
Expand Down Expand Up @@ -157,7 +179,7 @@ function outputter:drawImage (src, x, y, width, height, pageno)
width = SU.cast("number", width)
height = SU.cast("number", height)
self:_ensureInit()
pdf.drawimage(src, x, y, width, height, pageno or 1)
pdf.drawimage(src, trueXCoord(x), trueYCoord(y), width, height, pageno or 1)
end

function outputter:getImageSize (src, pageno)
Expand All @@ -174,8 +196,9 @@ function outputter:drawSVG (figure, x, y, _, height, scalefactor)
pdf.add_content("q")
self:setCursor(x, y)
x, y = self:getCursor()
local newy = y - SILE.documentState.paperSize[2] + height
pdf.add_content(table.concat({ scalefactor, 0, 0, -scalefactor, x, newy, "cm" }, " "))
local sheetSize = SILE.documentState.sheetSize or SILE.documentState.paperSize
local newy = y - SILE.documentState.paperSize[2] / 2 + height - sheetSize[2] / 2
pdf.add_content(table.concat({ scalefactor, 0, 0, -scalefactor, trueXCoord(x), newy, "cm" }, " "))
pdf.add_content(figure)
pdf.add_content("Q")
end
Expand All @@ -187,7 +210,7 @@ function outputter:drawRule (x, y, width, height)
height = SU.cast("number", height)
self:_ensureInit()
local paperY = SILE.documentState.paperSize[2]
pdf.setrule(x, paperY - y - height, width, height)
pdf.setrule(trueXCoord(x), trueYCoord(paperY - y - height), width, height)
end

function outputter:debugFrame (frame)
Expand Down Expand Up @@ -228,4 +251,139 @@ function outputter:debugHbox (hbox, scaledWidth)
self:popColor()
end

-- The methods below are only implemented on outputters supporting these features.
-- In PDF, it relies on transformation matrices, but other backends may call
-- for a different strategy.
-- ! The API is unstable and subject to change. !

function outputter:scaleFn (xorigin, yorigin, xratio, yratio, callback)
xorigin = SU.cast("number", xorigin)
yorigin = SU.cast("number", yorigin)
local x0 = trueXCoord(xorigin)
local y0 = -trueYCoord(yorigin)
self:_ensureInit()
pdf:gsave()
pdf.setmatrix(1, 0, 0, 1, x0, y0)
pdf.setmatrix(xratio, 0, 0, yratio, 0, 0)
pdf.setmatrix(1, 0, 0, 1, -x0, -y0)
callback()
pdf:grestore()
end

function outputter:rotateFn (xorigin, yorigin, theta, callback)
xorigin = SU.cast("number", xorigin)
yorigin = SU.cast("number", yorigin)
local x0 = trueXCoord(xorigin)
local y0 = -trueYCoord(yorigin)
self:_ensureInit()
pdf:gsave()
pdf.setmatrix(1, 0, 0, 1, x0, y0)
pdf.setmatrix(math.cos(theta), math.sin(theta), -math.sin(theta), math.cos(theta), 0, 0)
pdf.setmatrix(1, 0, 0, 1, -x0, -y0)
callback()
pdf:grestore()
end

-- Other rotation unstable APIs

function outputter:enterFrameRotate (xa, xb, y, theta) -- Unstable API see rotate package
xa = SU.cast("number", xa)
xb = SU.cast("number", xb)
y = SU.cast("number", y)
-- Keep center point the same?
local cx0 = trueXCoord(xa)
local cx1 = trueXCoord(xb)
local cy = -trueYCoord(y)
self:_ensureInit()
pdf:gsave()
pdf.setmatrix(1, 0, 0, 1, cx1, cy)
pdf.setmatrix(math.cos(theta), math.sin(theta), -math.sin(theta), math.cos(theta), 0, 0)
pdf.setmatrix(1, 0, 0, 1, -cx0, -cy)
end

function outputter.leaveFrameRotate (_)
pdf:grestore()
end

-- Unstable link APIs

function outputter:linkAnchor (x, y, name)
x = SU.cast("number", x)
y = SU.cast("number", y)
self:_ensureInit()
pdf.destination(name, trueXCoord(x), trueYCoord(y))
end

local function borderColor (color)
if color then
if color.r then return "/C [" .. color.r .. " " .. color.g .. " " .. color.b .. "]" end
if color.c then return "/C [" .. color.c .. " " .. color.m .. " " .. color.y .. " " .. color.k .. "]" end
if color.l then return "/C [" .. color.l .. "]" end
end
return ""
end
local function borderStyle (style, width)
if style == "underline" then return "/BS<</Type/Border/S/U/W " .. width .. ">>" end
if style == "dashed" then return "/BS<</Type/Border/S/D/D[3 2]/W " .. width .. ">>" end
return "/Border[0 0 " .. width .. "]"
end

function outputter:enterLinkTarget (_, _) -- destination, options as argument
-- HACK:
-- Looking at the code, pdf.begin_annotation does nothing, and Simon wrote a comment
-- about tracking boxes. Unsure what he implied with this obscure statement.
-- Sure thing is that some backends may need the destination here, e.g. an HTML backend
-- would generate a <a href="#destination">, as well as the options possibly for styling
-- on the link opening?
self:_ensureInit()
pdf.begin_annotation()
end
function outputter.leaveLinkTarget (_, x0, y0, x1, y1, dest, opts)
local bordercolor = borderColor(opts.bordercolor)
local borderwidth = SU.cast("integer", opts.borderwidth)
local borderstyle = borderStyle(opts.borderstyle, borderwidth)
local target = opts.external and "/Type/Action/S/URI/URI" or "/S/GoTo/D"
local d = "<</Type/Annot/Subtype/Link" .. borderstyle .. bordercolor .. "/A<<" .. target .. "(" .. dest .. ")>>>>"
pdf.end_annotation(d,
trueXCoord(x0) , trueYCoord(y0 - opts.borderoffset),
trueXCoord(x1), trueYCoord(y1 + opts.borderoffset))
end

-- Bookmarks and metadata

local function validate_date (date)
return string.match(date, [[^D:%d+%s*-%s*%d%d%s*'%s*%d%d%s*'?$]]) ~= nil
end

function outputter:setMetadata (key, value)
if key == "Trapped" then
SU.warn("Skipping special metadata key \\Trapped")
return
end

if key == "ModDate" or key == "CreationDate" then
if not validate_date(value) then
SU.warn("Invalid date: " .. value)
return
end
else
-- see comment in on bookmark
value = SU.utf8_to_utf16be(value)
end
self:_ensureInit()
pdf.metadata(key, value)
end

function outputter:setBookmark (dest, title, level)
-- Added UTF8 to UTF16-BE conversion
-- For annotations and bookmarks, text strings must be encoded using
-- either PDFDocEncoding or UTF16-BE with a leading byte-order marker.
-- As PDFDocEncoding supports only limited character repertoire for
-- European languages, we use UTF-16BE for internationalization.
local ustr = SU.utf8_to_utf16be_hexencoded(title)
local d = "<</Title<" .. ustr .. ">/A<</S/GoTo/D(" .. dest .. ")>>>>"
self:_ensureInit()
pdf.bookmark(d, level)
end

return outputter
Loading
Loading