diff --git a/classes/base.lua b/classes/base.lua index 90c42066a..aab8cb138 100644 --- a/classes/base.lua +++ b/classes/base.lua @@ -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 @@ -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 (_) diff --git a/documentation/c03-input.sil b/documentation/c03-input.sil index 3151e7f94..a8a42f390 100644 --- a/documentation/c03-input.sil +++ b/documentation/c03-input.sil @@ -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{}, bleed=\em{}}. + +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{}, sheetsize=\em{}}. + +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. diff --git a/outputters/base.lua b/outputters/base.lua index a5ce0aacd..4888a0605 100644 --- a/outputters/base.lua +++ b/outputters/base.lua @@ -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 diff --git a/outputters/libtexpdf.lua b/outputters/libtexpdf.lua index 9e6ab8e03..7750c135a 100644 --- a/outputters/libtexpdf.lua +++ b/outputters/libtexpdf.lua @@ -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 @@ -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) @@ -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) @@ -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 @@ -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) @@ -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<>" end + if style == "dashed" then return "/BS<>" 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 , 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 = "<>>>" + 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 = "</A<>>>" + self:_ensureInit() + pdf.bookmark(d, level) +end + return outputter diff --git a/packages/background/init.lua b/packages/background/init.lua index 02e2fbd6a..10e7a7117 100644 --- a/packages/background/init.lua +++ b/packages/background/init.lua @@ -3,48 +3,77 @@ local base = require("packages.base") local package = pl.class(base) package._name = "background" -local outputBackground = function (color) - local page = SILE.getFrame("page") - local backgroundColor = SILE.color(color) - SILE.outputter:pushColor(backgroundColor) - SILE.outputter:drawRule(page:left(), page:top(), page:right(), page:bottom()) - SILE.outputter:popColor() +local background = {} + +local outputBackground = function () + local pagea = SILE.getFrame("page") + local offset = SILE.documentState.bleed / 2 + if type(background.bg) == "string" then + SILE.outputter:drawImage(background.bg, + pagea:left() - offset, pagea:top() - offset, + pagea:width() + 2 * offset, pagea:height() + 2 * offset) + elseif background.bg then + SILE.outputter:pushColor(background.bg) + SILE.outputter:drawRule( + pagea:left() - offset, pagea:top() - offset, + pagea:width() + 2 * offset, pagea:height() + 2 * offset) + SILE.outputter:popColor() + end + if not background.allpages then + background.bg = nil + end end function package:_init () base._init(self) - self:loadPackage("color") + self.class:registerHook("newpage", outputBackground) end function package:registerCommands () self:registerCommand("background", function (options, _) - options.color = options.color or "white" - options.allpages = options.allpages or true - outputBackground(options.color) - if options.allpages and options.allpages ~= "false" then - local oldNewPage = SILE.documentState.documentClass.newPage - SILE.documentState.documentClass.newPage = function (self_) - local page = oldNewPage(self_) - outputBackground(options.color) - return page - end + if SU.boolean(options.disable, false) then + -- This option is certainly better than enforcing a white color. + background.bg = nil + return + end + + local allpages = SU.boolean(options.allpages, true) + background.allpages = allpages + local color = options.color and SILE.color(options.color) + local src = options.src + if src then + background.bg = src and SILE.resolveFile(src) or SU.error("Couldn't find file "..src) + elseif color then + background.bg = color + else + SU.error("background requires a color or an image src parameter") end - end, "Draws a solid background color on pages after initialization.") + outputBackground(SILE.scratch.background) + end, "Output a solid background color or an image on pages after initialization.") end package.documentation = [[ \begin{document} \use[module=packages.background] -The \autodoc:package{background} package allows you to set the color of the canvas background (by drawing a solid color block the full size of the page on page initialization). -The package provides a \autodoc:command{\background} command which requires at least one parameter, \autodoc:parameter{color=}, and sets the background of the current and all following pages to that color. -If you want to set only the current page background different from the default, use the parameter \autodoc:parameter{allpages=false}. -The color specification in the same as specified in the \autodoc:package{color} package. +As its name implies, the \autodoc:package{background} package allows you to set the color of the page canvas background or to use a background image extending to the full page width and height. + +The package provides a \autodoc:command{\background} command which requires one of the following parameters: +\begin{itemize} +\item{\autodoc:parameter{color=} sets the background of the current and all following pages to that color. The color specification has the same syntax as specified in the \autodoc:package{color} package.} +\item{\autodoc:parameter{src=} sets the backgound of the current and all following pages to the specified image. The latter will be scaled to the target dimension.} +\end{itemize} + +The background extends to the page trim area (“page bleed”) if the latter is defined. +This is to ensure that it indeed “bleeds” off the sides of the page, so as to avoid thin white lignes on an otherwise full color page when the paper sheet is cut to dimension but some pages are trimmed slightly more than others. +If setting only the current page background different from the default is desired, an extra parameter \autodoc:parameter{allpages=false} can be passed. \background[color=#e9d8ba,allpages=false] So, for example, \autodoc:command{\background[color=#e9d8ba,allpages=false]} will set a sepia tone background on the current page. +The \autodoc:parameter{disable=true} parameter allows disabling the background on the following pages. +It may be useful when \autodoc:parameter{allpages} is active from a previous invocation. \end{document} ]] diff --git a/packages/cropmarks/init.lua b/packages/cropmarks/init.lua index a3c5af14f..2fd529f56 100644 --- a/packages/cropmarks/init.lua +++ b/packages/cropmarks/init.lua @@ -7,51 +7,45 @@ local outcounter = 1 local function outputMarks () local page = SILE.getFrame("page") - SILE.outputter:drawRule(page:left() - 10, page:top(), -10, 0.5) - SILE.outputter:drawRule(page:left(), page:top() - 10, 0.5, -10) - SILE.outputter:drawRule(page:right() + 10, page:top(), 10, 0.5) - SILE.outputter:drawRule(page:right(), page:top() - 10, 0.5, -10) - SILE.outputter:drawRule(page:left() - 10, page:bottom(), -10, 0.5) - SILE.outputter:drawRule(page:left(), page:bottom() + 10, 0.5, 10) - SILE.outputter:drawRule(page:right() + 10, page:bottom(), 10, 0.5) - SILE.outputter:drawRule(page:right(), page:bottom() + 10, 0.5, 10) + -- Length of crop mark bars + local cropsz = 20 + -- Ensure the crop marks stay outside the bleed area + local offset = math.max(10, SILE.documentState.bleed / 2) + + SILE.outputter:drawRule(page:left() - offset, page:top(), -cropsz, 0.5) + SILE.outputter:drawRule(page:left(), page:top() - offset, 0.5, -cropsz) + SILE.outputter:drawRule(page:right() + offset, page:top(), cropsz, 0.5) + SILE.outputter:drawRule(page:right(), page:top() - offset, 0.5, -cropsz) + SILE.outputter:drawRule(page:left() - offset, page:bottom(), -cropsz, 0.5) + SILE.outputter:drawRule(page:left() , page:bottom() + offset, 0.5, cropsz) + SILE.outputter:drawRule(page:right() + offset, page:bottom(), cropsz, 0.5) + SILE.outputter:drawRule(page:right(), page:bottom() + offset, 0.5, cropsz) local hbox, hlist = SILE.typesetter:makeHbox(function () SILE.settings:temporarily(function () SILE.call("noindent") SILE.call("font", { size="6pt" }) - SILE.call("crop:header") + if SILE.Commands["crop:header"] then + -- Deprecation shim: + -- If user redefined this command, still use it with a warning... + SU.deprecated("crop:header", "cropmarks:header", "0.15.0", "0.16.0") + SILE.call("crop:header") + else + SILE.call("cropmarks:header") + end end) end) if #hlist > 0 then - SU.error("Forbidden migrating content in crop header") + SU.error("Migrating content is forbidden in crop header") end - SILE.typesetter.frame.state.cursorX = page:left() + 10 - SILE.typesetter.frame.state.cursorY = page:top() - 13 + SILE.typesetter.frame.state.cursorX = page:left() + offset + SILE.typesetter.frame.state.cursorY = page:top() - offset - 4 outcounter = outcounter + 1 if hbox then - for i = 1, #(hbox.value) do hbox.value[i]:outputYourself(SILE.typesetter, { ratio = 1 }) end - end -end - -local function reconstrainFrameset (fs) - for n, f in pairs(fs) do - if n ~= "page" then - if f:isAbsoluteConstraint("right") then - f.constraints.right = "left(page) + (" .. f.constraints.right .. ")" - end - if f:isAbsoluteConstraint("left") then - f.constraints.left = "left(page) + (" .. f.constraints.left .. ")" - end - if f:isAbsoluteConstraint("top") then - f.constraints.top = "top(page) + (" .. f.constraints.top .. ")" - end - if f:isAbsoluteConstraint("bottom") then - f.constraints.bottom = "top(page) + (" .. f.constraints.bottom .. ")" - end - f:invalidate() + for i = 1, #(hbox.value) do + hbox.value[i]:outputYourself(SILE.typesetter, { ratio = 1 }) end end end @@ -63,53 +57,36 @@ end function package:registerCommands () - self:registerCommand("crop:header", function (_, _) - local info = SILE.input.filenames[1] .. " - " .. self.class:date("%x %X") .. " - " .. outcounter + self:registerCommand("cropmarks:header", function (_, _) + local info = SILE.input.filenames[1] + .. " - " + .. self.class.packages.date:date({ format = "%x %X" }) + .. " - " .. outcounter SILE.typesetter:typeset(info) end) - self:registerCommand("crop:setup", function (options, _) - local papersize = SU.required(options, "papersize", "setting up crop marks") - local landscape = SU.boolean(options.landscape, self.class.options.landscape) - local size = SILE.papersize(papersize, landscape) - local oldsize = SILE.documentState.paperSize - SILE.documentState.paperSize = size - local offsetx = ( SILE.documentState.paperSize[1] - oldsize[1] ) /2 - local offsety = ( SILE.documentState.paperSize[2] - oldsize[2] ) /2 - local page = SILE.getFrame("page") - page:constrain("right", page:right() + offsetx) - page:constrain("left", offsetx) - page:constrain("bottom", page:bottom() + offsety) - page:constrain("top", offsety) - if SILE.scratch.masters then - for _, v in pairs(SILE.scratch.masters) do - reconstrainFrameset(v.frames) - end - else - reconstrainFrameset(SILE.documentState.documentClass.pageTemplate.frames) - end - if SILE.typesetter.frame then SILE.typesetter.frame:init() end - local oldEndPage = SILE.documentState.documentClass.endPage - SILE.documentState.documentClass.endPage = function (self_) - oldEndPage(self_) - outputMarks() - end + self:registerCommand("cropmarks:setup", function (_, _) + self.class:registerHook("endpage", outputMarks) end) + self:registerCommand("crop:setup", function (_, _) + SU.deprecated("crop:setup", "cropmarks:setup", "0.15.10", "0.17.0") + SILE.call("cropmarks:setup") + end) end package.documentation = [[ \begin{document} -When preparing a document for printing, you may be asked by the printer to add crop marks. -This means that you need to output the document on a slightly larger page size than your target paper and add printer’s crop marks to show where the paper should be trimmed down to the correct size. -(This is to ensure that pages where the content “bleeds” off the side of the page are correctly cut.) +When preparing a document for printing, you may be asked by the printer add crop marks. +This means that you need to output the document on a slightly larger page size than your target paper and add crop marks to show where the paper sheet should be trimmed down to the correct size. -This package provides the \autodoc:command{\crop:setup} command which should be run early in your document file. -It takes one argument, \autodoc:parameter{papersize}, which is the true target paper size. -It place cropmarks around the true page content. +Actual paper size, true page content area and bleed/trim area can all be set via class options. +This package provides the \autodoc:command{\cropmarks:setup} command which should be run early in your document file. +It places crop marks around the true page content. +The crop marks are guaranteed to stay outside the bleed/trim area, when defined. It also adds a header at the top of the page with the filename, date and output sheet number. -You can customize this header by redefining \autodoc:command{\crop:header}. +You can customize this header by redefining \autodoc:command{\cropmarks:header}. \end{document} ]] diff --git a/packages/pdf/init.lua b/packages/pdf/init.lua index f6622a3bc..9a9e2f394 100644 --- a/packages/pdf/init.lua +++ b/packages/pdf/init.lua @@ -1,44 +1,17 @@ +-- +-- This package and its commands are perhaps ill-named: +-- Exception made of the pdf:literal command below, the concepts of links +-- (anchor, target), bookmarks, and metadata are not specific to PDF. +-- local base = require("packages.base") local package = pl.class(base) package._name = "pdf" -local pdf - -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<>" end - if style == "dashed" then return "/BS<>" end - return "/Border[0 0 " .. width .. "]" -end - -local function validate_date (date) - return string.match(date, [[^D:%d+%s*-%s*%d%d%s*'%s*%d%d%s*'?$]]) ~= nil -end - -function package:_init () - base._init(self) - pdf = require("justenoughlibtexpdf") - if SILE.outputter._name ~= "libtexpdf" then - SU.error("pdf package requires libtexpdf backend") - end -end - function package:registerCommands () self:registerCommand("pdf:destination", function (options, _) local name = SU.required(options, "name", "pdf:destination") - if type(SILE.outputter._ensureInit) == "function" then - SILE.outputter:_ensureInit() - end SILE.typesetter:pushHbox({ outputYourself = function (_, typesetter, line) local state = typesetter.frame.state @@ -46,7 +19,7 @@ function package:registerCommands () local x, y = state.cursorX, state.cursorY typesetter.frame:advancePageDirection(line.height) local _y = SILE.documentState.paperSize[2] - y - pdf.destination(name, x:tonumber(), _y:tonumber()) + SILE.outputter:linkAnchor(x, _y, name) end }) end) @@ -54,29 +27,26 @@ function package:registerCommands () self:registerCommand("pdf:bookmark", function (options, _) local dest = SU.required(options, "dest", "pdf:bookmark") local title = SU.required(options, "title", "pdf:bookmark") - local level = options.level or 1 - -- 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) - if type(SILE.outputter._ensureInit) == "function" then - SILE.outputter:_ensureInit() - end + local level = SU.cast("integer", options.level or 1) SILE.typesetter:pushHbox({ value = nil, height = SILE.measurement(0), width = SILE.measurement(0), depth = SILE.measurement(0), outputYourself = function () - local d = "</A<>>>" - pdf.bookmark(d, level) + SILE.outputter:setBookmark(dest, title, level) end }) end) self:registerCommand("pdf:literal", function (_, content) + -- NOTE: This method is used by the pdfstructure package and should + -- probably be moved elsewhere, so there's no attempt here to delegate + -- the low-level libtexpdf call to te outputter. + if SILE.outputter._name ~= "libtexpdf" then + SU.error("pdf package requires libtexpdf backend") + end + local pdf = require("justenoughlibtexpdf") if type(SILE.outputter._ensureInit) == "function" then SILE.outputter:_ensureInit() end @@ -93,43 +63,44 @@ function package:registerCommands () self:registerCommand("pdf:link", function (options, content) local dest = SU.required(options, "dest", "pdf:link") - local target = options.external and "/Type/Action/S/URI/URI" or "/S/GoTo/D" + local external = SU.boolean(options.external, false) local borderwidth = options.borderwidth and SU.cast("measurement", options.borderwidth):tonumber() or 0 - local bordercolor = borderColor(SILE.color(options.bordercolor or "blue")) + local bordercolor = SILE.color(options.bordercolor or "blue") local borderoffset = SU.cast("measurement", options.borderoffset or "1pt"):tonumber() - local borderstyle = borderStyle(options.borderstyle, borderwidth) - local llx, lly - if type(SILE.outputter._ensureInit) == "function" then - SILE.outputter:_ensureInit() - end + local opts = { + external = external, + borderstyle = options.borderstyle, + bordercolor = bordercolor, + borderwidth = borderwidth, + borderoffset = borderoffset + } + + local x0, y0 SILE.typesetter:pushHbox({ value = nil, - height = SILE.measurement(0), - width = SILE.measurement(0), - depth = SILE.measurement(0), + height = 0, + width = 0, + depth = 0, outputYourself = function (_, typesetter, _) - llx = typesetter.frame.state.cursorX:tonumber() - lly = (SILE.documentState.paperSize[2] - typesetter.frame.state.cursorY):tonumber() - pdf.begin_annotation() + x0 = typesetter.frame.state.cursorX:tonumber() + y0 = (SILE.documentState.paperSize[2] - typesetter.frame.state.cursorY):tonumber() + SILE.outputter:enterLinkTarget(dest, opts) end }) - local hbox, hlist = SILE.typesetter:makeHbox(content) -- hack SILE.typesetter:pushHbox(hbox) - SILE.typesetter:pushHlist(hlist) - SILE.typesetter:pushHbox({ value = nil, - height = SILE.measurement(0), - width = SILE.measurement(0), - depth = SILE.measurement(0), + height = 0, + width = 0, + depth = 0, outputYourself = function (_, typesetter, _) - local d = "<>>>" - local x = typesetter.frame.state.cursorX:tonumber() - local y = (SILE.documentState.paperSize[2] - typesetter.frame.state.cursorY + hbox.height):tonumber() - pdf.end_annotation(d, llx , lly - borderoffset, x, y + borderoffset) + local x1 = typesetter.frame.state.cursorX:tonumber() + local y1 = (SILE.documentState.paperSize[2] - typesetter.frame.state.cursorY + hbox.height):tonumber() + SILE.outputter:leaveLinkTarget(x0, y0, x1, y1, dest, opts) -- Unstable API end }) + SILE.typesetter:pushHlist(hlist) end) self:registerCommand("pdf:metadata", function (options, _) @@ -139,32 +110,7 @@ function package:registerCommands () end local value = SU.required(options, "value", "pdf:metadata") - 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 pdf:bookmark - value = SU.utf8_to_utf16be(value) - end - if type(SILE.outputter._ensureInit) == "function" then - SILE.outputter:_ensureInit() - end - SILE.typesetter:pushHbox({ - value = nil, - height = SILE.measurement(0), - width = SILE.measurement(0), - depth = SILE.measurement(0), - outputYourself = function (_, _, _) - pdf.metadata(key, value) - end - }) + SILE.outputter:setMetadata(key, value) end) end diff --git a/packages/rotate/init.lua b/packages/rotate/init.lua index 5982f0c2c..6aad18cca 100644 --- a/packages/rotate/init.lua +++ b/packages/rotate/init.lua @@ -3,23 +3,25 @@ local base = require("packages.base") local package = pl.class(base) package._name = "rotate" -local pdf = require("justenoughlibtexpdf") - local enter = function (self, _) + -- Probably broken, see: + -- https://github.com/sile-typesetter/sile/issues/427 if not self.rotate then return end - local x = -math.rad(self.rotate) + if not SILE.outputter.enterFrameRotate then + return SU.warn("Frame '".. self.id "' will not be rotated: backend '" .. SILE.outputter._name .. "' does not support rotation") + end + local theta = -math.rad(self.rotate) -- Keep center point the same - pdf:gsave() - local cx = self:left():tonumber() - local cy = -self:bottom():tonumber() - pdf.setmatrix(1, 0, 0, 1, cx + math.sin(x) * self:height():tonumber(), cy) - pdf.setmatrix(math.cos(x), math.sin(x), -math.sin(x), math.cos(x), 0, 0) - pdf.setmatrix(1, 0, 0, 1, -cx, -cy) + local x0 = self:left():tonumber() + local x1 = x0 + math.sin(theta) * self:height():tonumber() + local y0 = self:bottom():tonumber() + SILE.outputter:enterFrameRotate(x0, x1, y0, theta) -- Unstable API end -local leave = function(self, _) +local leave = function(self, _) if not self.rotate then return end - pdf:grestore() + if not SILE.outputter.enterFrameRotate then return end -- no enter no leave. + SILE.outputter:leaveFrameRotate() end -- What is the width, depth and height of a rectangle width w and height h rotated by angle theta? @@ -35,20 +37,20 @@ end local outputRotatedHbox = function (self, typesetter, line) local origbox = self.value.orig - local x = self.value.theta + local theta = self.value.theta + -- Find origin of untransformed hbox - local save = typesetter.frame.state.cursorX - typesetter.frame.state.cursorX = typesetter.frame.state.cursorX - (origbox.width.length-self.width)/2 - - local horigin = (typesetter.frame.state.cursorX + origbox.width.length / 2):tonumber() - local vorigin = -(typesetter.frame.state.cursorY - (origbox.height - origbox.depth) / 2):tonumber() - pdf:gsave() - pdf.setmatrix(1, 0, 0, 1, horigin, vorigin) - pdf.setmatrix(math.cos(x), math.sin(x), -math.sin(x), math.cos(x), 0, 0) - pdf.setmatrix(1, 0, 0, 1, -horigin, -vorigin) - origbox:outputYourself(typesetter, line) - pdf:grestore() - typesetter.frame.state.cursorX = save + local X = typesetter.frame.state.cursorX + local Y = typesetter.frame.state.cursorY + typesetter.frame.state.cursorX = X - (origbox.width.length-self.width)/2 + local horigin = X + origbox.width.length / 2 + local vorigin = Y - (origbox.height - origbox.depth) / 2 + + SILE.outputter:rotateFn(horigin, vorigin, theta, function () + origbox:outputYourself(typesetter, line) + end) + typesetter.frame.state.cursorX = X + typesetter.frame.state.cursorY = Y typesetter.frame:advanceWritingDirection(self.width) end @@ -65,6 +67,10 @@ end function package:registerCommands () self:registerCommand("rotate", function(options, content) + if not SILE.outputter.rotateFn then + SU.warn("Output will not be rotated: backend '" .. SILE.outputter._name .. "' does not support rotation") + return SILE.process(content) + end local angle = SU.required(options, "angle", "rotate command") local theta = -math.rad(angle) local origbox, hlist = SILE.typesetter:makeHbox(content) @@ -92,7 +98,6 @@ function package:registerCommands () end depth = -depth if depth < SILE.length(0) then depth = SILE.length(0) end - SILE.outputter:_ensureInit() SILE.typesetter:pushHbox({ value = { orig = origbox, theta = theta}, height = height, diff --git a/packages/scalebox/init.lua b/packages/scalebox/init.lua index f4d8375c2..8ab331fbb 100644 --- a/packages/scalebox/init.lua +++ b/packages/scalebox/init.lua @@ -6,12 +6,10 @@ package._name = "scalebox" function package:registerCommands () self:registerCommand("scalebox", function(options, content) - if SILE.outputter._name ~= "libtexpdf" then - SU.warn("Output will not be scaled: \\scalebox only works with the libtexpdf backend") + if not SILE.outputter.scaleFn then + SU.warn("Output will not be scaled: backend '" .. SILE.outputter._name .. "' does not support scaling") return SILE.process(content) end - SILE.outputter:_ensureInit() - local pdf = require("justenoughlibtexpdf") local hbox, hlist = SILE.typesetter:makeHbox(content) local xratio, yratio = SU.cast("number", options.xratio or 1), SU.cast("number", options.yratio or 1) @@ -37,18 +35,13 @@ function package:registerCommands () local outputWidth = SU.rationWidth(node.width, node.width, line.ratio) local X = typesetter.frame.state.cursorX local Y = typesetter.frame.state.cursorY - local x0 = X:tonumber() - local y0 = -Y:tonumber() if xratio < 0 then typesetter.frame:advanceWritingDirection(-outputWidth) end - 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) - hbox.outputYourself(hbox, typesetter, line) - pdf:grestore() + SILE.outputter:scaleFn(X, Y, xratio, yratio, function () + hbox:outputYourself(typesetter, line) + end) typesetter.frame.state.cursorX = X typesetter.frame.state.cursorY = Y typesetter.frame:advanceWritingDirection(outputWidth) diff --git a/packages/url/init.lua b/packages/url/init.lua index f30259445..b754595cc 100644 --- a/packages/url/init.lua +++ b/packages/url/init.lua @@ -3,8 +3,6 @@ local base = require("packages.base") local package = pl.class(base) package._name = "url" -local pdf - -- URL escape sequence, URL fragment: local preferBreakBefore = "%#" -- URL path elements, URL query arguments, acceptable extras: @@ -23,8 +21,7 @@ function package:_init () base._init(self) self:loadPackage("verbatim") self:loadPackage("inputfilter") - pdf = SILE.outputter._name == "libtexpdf" - if pdf then self:loadPackage("pdf") end + self:loadPackage("pdf") end function package.declareSettings (_) @@ -48,15 +45,6 @@ end function package:registerCommands () self:registerCommand("href", function (options, content) - if not pdf then - if options.src then - SILE.process(content) - else - SILE.call("url", { language = options.language }, content) - end - return -- DONE. - end - if options.src then SILE.call("pdf:link", { dest = options.src, external = true, borderwidth = options.borderwidth, diff --git a/tests/bug-337.expected b/tests/bug-337.expected index a21541759..ed8b90595 100644 --- a/tests/bug-337.expected +++ b/tests/bug-337.expected @@ -1,69 +1,68 @@ -Set paper size 297.6377985 419.5275636 +Set paper size 209.7637818 297.6377985 Begin page -Mx 43.9370 -My 67.1949 +My 6.2500 Set font Gentium Plus;10;400;;normal;;;LTR T 21 20 19 83 87 w=22.8174 (210pt) -Mx 69.4140 +Mx 25.4770 T 2258 w=4.1016 (×) -Mx 76.1752 +Mx 32.2382 T 21 28 27 83 87 w=22.8174 (298pt) -Draw line 33.9370 60.9449 -10.0000 0.5000 -Draw line 43.9370 50.9449 0.5000 -10.0000 -Draw line 263.7008 60.9449 10.0000 0.5000 -Draw line 253.7008 50.9449 0.5000 -10.0000 -Draw line 33.9370 358.5827 -10.0000 0.5000 -Draw line 43.9370 368.5827 0.5000 10.0000 -Draw line 263.7008 358.5827 10.0000 0.5000 -Draw line 253.7008 368.5827 0.5000 10.0000 -Mx 53.9370 -My 47.9449 +Draw line -10.0000 0.0000 -20.0000 0.5000 +Draw line 0.0000 -10.0000 0.5000 -20.0000 +Draw line 219.7638 0.0000 20.0000 0.5000 +Draw line 209.7638 -10.0000 0.5000 -20.0000 +Draw line -10.0000 297.6378 -20.0000 0.5000 +Draw line 0.0000 307.6378 0.5000 20.0000 +Draw line 219.7638 297.6378 20.0000 0.5000 +Draw line 209.7638 307.6378 0.5000 20.0000 +Mx 10.0000 +My -14.0000 Set font Gentium Plus;6;400;;normal;;;LTR T 87 72 86 87 86 w=11.5371 (tests) -Mx 65.4741 +Mx 21.5371 T 18 w=2.8154 (/) -Mx 68.2895 +Mx 24.3525 T 69 88 74 w=9.1523 (bug) -Mx 77.4419 +Mx 33.5049 T 16 w=2.0215 (-) -Mx 79.4634 +Mx 35.5264 T 22 22 26 w=8.4463 (337) -Mx 87.9097 +Mx 43.9727 T 17 w=1.3740 (.) -Mx 89.2837 +Mx 45.3467 T 86 76 79 w=5.5693 (sil) New page -Mx 43.9370 -My 67.1949 +Mx 0.0000 +My 6.2500 Set font Gentium Plus;10;400;;normal;;;LTR T 21 20 19 83 87 w=22.8174 (210pt) -Mx 69.4140 +Mx 25.4770 T 2258 w=4.1016 (×) -Mx 76.1752 +Mx 32.2382 T 21 28 27 83 87 w=22.8174 (298pt) -Draw line 33.9370 60.9449 -10.0000 0.5000 -Draw line 43.9370 50.9449 0.5000 -10.0000 -Draw line 263.7008 60.9449 10.0000 0.5000 -Draw line 253.7008 50.9449 0.5000 -10.0000 -Draw line 33.9370 358.5827 -10.0000 0.5000 -Draw line 43.9370 368.5827 0.5000 10.0000 -Draw line 263.7008 358.5827 10.0000 0.5000 -Draw line 253.7008 368.5827 0.5000 10.0000 -Mx 53.9370 -My 47.9449 +Draw line -10.0000 0.0000 -20.0000 0.5000 +Draw line 0.0000 -10.0000 0.5000 -20.0000 +Draw line 219.7638 0.0000 20.0000 0.5000 +Draw line 209.7638 -10.0000 0.5000 -20.0000 +Draw line -10.0000 297.6378 -20.0000 0.5000 +Draw line 0.0000 307.6378 0.5000 20.0000 +Draw line 219.7638 297.6378 20.0000 0.5000 +Draw line 209.7638 307.6378 0.5000 20.0000 +Mx 10.0000 +My -14.0000 Set font Gentium Plus;6;400;;normal;;;LTR T 87 72 86 87 86 w=11.5371 (tests) -Mx 65.4741 +Mx 21.5371 T 18 w=2.8154 (/) -Mx 68.2895 +Mx 24.3525 T 69 88 74 w=9.1523 (bug) -Mx 77.4419 +Mx 33.5049 T 16 w=2.0215 (-) -Mx 79.4634 +Mx 35.5264 T 22 22 26 w=8.4463 (337) -Mx 87.9097 +Mx 43.9727 T 17 w=1.3740 (.) -Mx 89.2837 +Mx 45.3467 T 86 76 79 w=5.5693 (sil) End page Finish diff --git a/tests/bug-337.sil b/tests/bug-337.sil index bab3d9f4c..5234876c9 100644 --- a/tests/bug-337.sil +++ b/tests/bug-337.sil @@ -1,8 +1,9 @@ -\begin[papersize=a7,class=book]{document} +\begin[papersize=a7,class=book,sheetsize=a6]{document} \use[module=packages.retrograde,target=v0.15.0] +\use[module=packages.color] \script[src=inc.bug-337] -\define[command=crop:header]{tests/bug-337.sil} -\crop:setup[papersize=a6] +\define[command=cropmarks:header]{tests/bug-337.sil} +\cropmarks:setup \nofolios \set[parameter=document.parindent,value=0] diff --git a/tests/bug-353.sil b/tests/bug-353.sil index 73944caf8..448668c47 100644 --- a/tests/bug-353.sil +++ b/tests/bug-353.sil @@ -1,6 +1,7 @@ \begin[papersize=a5]{document} \use[module=packages.retrograde,target=v0.15.0] \use[module=packages.background] +\use[module=packages.color] \nofolios \background[color=#e9d8ba] \color[color=#5a4129]{Sepia baby.} diff --git a/typesetters/base.lua b/typesetters/base.lua index 58dacadb4..3ae085fe1 100644 --- a/typesetters/base.lua +++ b/typesetters/base.lua @@ -1072,16 +1072,18 @@ function typesetter:makeHbox (content) local ox = atypesetter.frame.state.cursorX local oy = atypesetter.frame.state.cursorY SILE.outputter:setCursor(atypesetter.frame.state.cursorX, atypesetter.frame.state.cursorY) + SU.debug("hboxes", function () + -- setCursor is also invoked by the internal (wrapped) hboxes etc. + -- so we must show our debug box before outputting its content. + SILE.outputter:debugHbox(box, box:scaledWidth(line)) + return "Drew debug outline around hbox" + end) for _, node in ipairs(box.value) do node:outputYourself(atypesetter, line) end atypesetter.frame.state.cursorX = ox atypesetter.frame.state.cursorY = oy _post() - SU.debug("hboxes", function () - SILE.outputter:debugHbox(box, box:scaledWidth(line)) - return "Drew debug outline around hbox" - end) end }) return hbox, migratingNodes