diff --git a/classes/base.lua b/classes/base.lua index 0c35d26..c65e57f 100644 --- a/classes/base.lua +++ b/classes/base.lua @@ -84,9 +84,33 @@ end function class:setOptions (options) options = options or {} options.papersize = options.papersize or "a4" + -- BEGIN SILEX FULL BLEED AND PAGE SIZE + options.bleed = options.bleed or "0" + -- END SILEX FULL BLEED AND PAGE SIZE for option, value in pairs(options) do self.options[option] = value end + + -- BEGIN SILEX FULL BLEED AND PAGE SIZE + if not SILE.documentState.sheetSize then + SILE.documentState.sheetSize = { + SILE.documentState.paperSize[1], + SILE.documentState.paperSize[2] + } + end + 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 + -- END SILEX FULL BLEED AND PAGE SIZE end function class:declareOption (option, setter) @@ -120,6 +144,22 @@ function class:declareOptions () end return self.papersize end) + -- BEGIN SILEX FULL BLEED AND PAGE SIZE + self:declareOption("sheetsize", function (_, size) + if size then + self.sheetsize = size + SILE.documentState.sheetSize = SILE.papersize(size) + end + return self.sheetsize + 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 SILEX FULL BLEED AND PAGE SIZE end function class.declareSettings (_) diff --git a/outputters/base.lua b/outputters/base.lua new file mode 100644 index 0000000..570ae8b --- /dev/null +++ b/outputters/base.lua @@ -0,0 +1,63 @@ +local outputter = pl.class() +outputter.type = "outputter" +outputter._name = "base" + +function outputter._init () end + +function outputter.newPage () end + +function outputter.finish () end + +function outputter.getCursor () end + +function outputter.setCursor (_, _, _, _) end + +function outputter.setColor () end + +function outputter.pushColor () end + +function outputter.popColor () end + +function outputter.drawHbox (_, _, _) end + +function outputter.setFont (_, _) end + +function outputter.drawImage (_, _, _, _, _, _) end + +function outputter.getImageSize (_, _) end + +function outputter.drawSVG () end + +function outputter.drawRule (_, _, _, _, _) end + +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 + fname = SILE.outputFilename + elseif SILE.input.filenames[1] then + fname = pl.path.splitext(SILE.input.filenames[1]) + if self.extension then + fname = fname .. "." .. self.extension + end + end + if not fname then + SU.error("Cannot guess output filename without an input name") + end + return fname +end + +return outputter diff --git a/outputters/html.lua b/outputters/html.lua new file mode 100644 index 0000000..e743d2b --- /dev/null +++ b/outputters/html.lua @@ -0,0 +1,582 @@ +local base = require("outputters.base") +SILE.shaper = SILE.shapers.harfbuzz() + +local cursorX = 0 +local cursorY = 0 + +local started = false +local lastkey = false + +local debugfont = SILE.font.loadDefaults({ family = "Gentium Plus", language = "en", size = 10 }) + +local _dl = 0.5 + +local _debugfont +local _font + +-- local function _round (input) +-- -- LuaJIT 2.1 betas (and inheritors such as OpenResty and Moonjit) are biased +-- -- towards rounding 0.5 up to 1, all other Lua interpreters are biased +-- -- towards rounding such floating point numbers down. This hack shaves off +-- -- just enough to fix the bias so our test suite works across interpreters. +-- -- Note that even a true rounding function here will fail because the bias is +-- -- inherent to the floating point type. Also note we are erroring in favor of +-- -- the *less* common option beacuse the LuaJIT VMS are hopelessly broken +-- -- whereas normal LUA VMs can be cooerced. +-- if input > 0 then input = input + .00000000000001 end +-- if input < 0 then input = input - .00000000000001 end +-- return string.format("%.4f", input) +-- end + +local outputter = pl.class(base) +outputter._name = "html" +outputter.extension = "html" + +-- 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 + deltaX = (SILE.documentState.sheetSize[1] - SILE.documentState.paperSize[1]) / 2 + end + return x + deltaX +end +local function trueYCoord (y) + if not deltaY then + deltaY = (SILE.documentState.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 + +local _div = {} +local _fname +function outputter:_ensureInit () + if not started then + local w, h = SILE.documentState.sheetSize[1], SILE.documentState.sheetSize[2] + local fname = self:getOutputFilename() + _fname = fname + -- 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) + print("Writing HTML to " .. fname) + local fd, err = io.open(fname == "-" and "/dev/stdout" or fname, "w") + if not fd then return SU.error(err) end + self.fd = fd + self.fd:write(table.concat({ + "", + "", + "", + "", + "" .. "XXXX" .. "", + "", + "", + "", + "
", + + }, "\n")) + _div[#_div+1] = { x = 0, y = 0, w = w, h = h } + started = true + end +end + +function outputter:newPage () + self:_ensureInit() + self.fd:write('
\n') + self.fd:write("
") +end + +-- pdf stucture package needs a tie in here +function outputter._endHook (_) + -- FIXME: NOT IMPLEMENTED +end + +function outputter:finish () + self:_ensureInit() + self.fd:write("
\n") + self:_endHook() + self.fd:write("\n") + self.fd:close() + started = false + lastkey = nil +end + +function outputter.getCursor (_) + return cursorX, cursorY +end + +function outputter.setCursor (_, x, y, relative) + x = SU.cast("number", x) + y = SU.cast("number", y) + local offset = relative and { x = cursorX, y = cursorY } or { x = 0, y = 0 } + cursorX = offset.x + x + cursorY = offset.y + (relative and 0 or SILE.documentState.paperSize[2]) - y +end + +-- FIXME not called from the 0.14 code base!!! +function outputter:setColor (_) + self:_ensureInit() + SU.error("setColor not implemented") +end + +local cssColorStack = { "rgb(0,0,0)" } +local function cmykToRgb(c, m, y, k) + local r = 255 * (1 - c) * (1 - k) + local g = 255 * (1 - m) * (1 - k) + local b = 255 * (1 - y) * (1 - k) + return r, g, b +end + +function outputter:pushColor (color) + self:_ensureInit() + if color.r then + cssColorStack[#cssColorStack+1] = "rgb(" .. color.r * 255 .. ", " .. color.g * 255 .. ", " .. color.b * 255 .. ")" + elseif color.c then + local r, g, b = cmykToRgb(color.c, color.m, color.y, color.k) + cssColorStack[#cssColorStack+1] = "rgb(" .. r .. ", " .. g .. ", " .. b .. ")" + elseif color.l then + cssColorStack[#cssColorStack+1] = "rgb(" .. color.l * 255 .. ", " .. color.l * 255 .. ", " .. color.l * 255 .. ")" + end +end + +function outputter:popColor () + self:_ensureInit() + cssColorStack[#cssColorStack] = nil +end + +function outputter:_drawString (str, width, x_offset, y_offset) + local x, y = self:getCursor() + + x = x - _div[#_div].x + y = y - _div[#_div].y + local xt = trueXCoord(x+x_offset) + local yt = trueYCoord(y+y_offset) + self.fd:write('
' + .. str + ..'
') +end + +function outputter:drawHbox (value, width) + width = SU.cast("number", width) + self:_ensureInit() + if not value.glyphString then return end + -- Nodes which require kerning or have offsets to the glyph + -- position should be output a glyph at a time. We pass the + -- glyph advance from the htmx table, so that libtexpdf knows + -- how wide each glyph is. It uses this to then compute the + -- relative position between the pen after the glyph has been + -- painted (cursorX + glyphAdvance) and the next painting + -- position (cursorX + width - remember that the box's "width" + -- is actually the shaped x_advance). + if value.complex then + for i = 1, #value.items do + local item = value.items[i] + self:_drawString(item.text, item.glyphAdvance, item.x_offset or 0, item.y_offset or 0) + self:setCursor(item.width, 0, true) + end + else + self:_drawString(value.text, width, 0, 0) + self:setCursor(width, 0, true) + end +end + +function outputter:_withDebugFont (callback) + if not _debugfont then + _debugfont = self:setFont(debugfont) + end + local oldfont = _font + _font = _debugfont + callback() + _font = oldfont +end + +local function featuresToCss(features) + local css = "" + for feature in features:gmatch("([%+%-%w]+)") do + local state = "on" + local featureName = feature + if feature:sub(1, 1) == "+" then + featureName = feature:sub(2) + elseif feature:sub(1, 1) == "-" then + featureName = feature:sub(2) + state = "off" + end + css = css .. string.format("font-feature-settings: '%s' %s;", featureName, state) + end + return css +end + +local function fontToCss(font) + local props = { + "font-family: " .. font.family, + "font-size: " .. font.size .. "pt", + "font-weight: " .. font.weight, + } + if font.style and font.style:lower() == "italic" then + props[#props + 1] = "font-style: italic" + else + props[#props + 1] = "font-style: normal" + end + local css = table.concat(props, ";") + if font.features then + css = css .. ";" .. featuresToCss(font.features) + end + return css +end + +function outputter:setFont (options) + self:_ensureInit() + local key = SILE.font._key(options) + if lastkey and key == lastkey then return _font end + -- FIXME handle direction? + -- local font = SILE.font.cache(options, SILE.shaper.getFace) + -- if options.direction == "TTB" then + -- ??? + -- end + -- if SILE.typesetter.frame and SILE.typesetter.frame:writingDirection() == "TTB" then + -- ??? + -- else + -- ??? + -- end + local metrics = require("fontmetrics") + local face = SILE.font.cache(options, SILE.shaper.getFace) + local m = metrics.get_typographic_extents(face) + m.ascender = m.ascender * options.size + m.descender = m.descender * options.size + _font = { + css = fontToCss(options), + spec = options, + metrics = m + } + lastkey = key + return _font +end + +function outputter:drawImage (src, x, y, width, height, _) + x = SU.cast("number", x) + y = SU.cast("number", y) + width = SU.cast("number", width) + height = SU.cast("number", height) + x = trueXCoord(x) + y = trueYCoord(y) + + -- FIXME use bottom instead of top? + x = x - _div[#_div].x + y = y - _div[#_div].y + -- FIXME RELATIVE PATH HACK + -- FIXME escapes needed in the regex! + local dir = pl.path.dirname(_fname) + src = src:gsub("^"..dir.."/", ""):gsub("^/"..dir.."/", "") + self:_ensureInit() + self.fd:write('
') + self.fd:write("") + self.fd:write("
") +end + +function outputter.getImageSize (_, src, pageno) + local pdf = require("justenoughlibtexpdf") + local llx, lly, urx, ury, xresol, yresol = pdf.imagebbox(src, pageno or 1) + return (urx-llx), (ury-lly), xresol, yresol +end + +local function pathToSVG(path) -- FIXME broken, needs to be rewritten + local svgPath = "" + local i = 1 + local _, ep, operands + while i < #path do + _, ep, operands = path:find("(%g+%s+%g+)%s+m%s+", i) + if operands then + svgPath = svgPath .. "M" .. operands .. " " + i = ep + 1 + else + _, ep, operands = path:find("(%g+%s+%g+%s+%g+%s+%g+%s+%g+%s+%g+)%s+c%s+", i) + if operands then + svgPath = svgPath .. "C" .. operands .. " " + i = ep + 1 + else + break + end + end + end + if svgPath == "" then + SU.warn("Invalid path: " .. path) + end + return svgPath .. "Z" +end + + +function outputter:drawSVG (figure, x, y, width, height, scalefactor) + self:_ensureInit() + + local d = pathToSVG(figure) + + x = SU.cast("number", x) + y = SU.cast("number", y) + height = SU.cast("number", height) + width = SU.cast("number", width) + + self:setCursor(x, y) + x, y = self:getCursor() + + x = x - _div[#_div].x + y = y - _div[#_div].y + + x = trueXCoord(x) + y = trueYCoord(y) + + self.fd:write('
') + self.fd:write('') + self.fd:write('') + self.fd:write('') + self.fd:write("
") +end + +function outputter:drawRule (x, y, width, height) + x = SU.cast("number", x) + y = SU.cast("number", y) + width = SU.cast("number", width) + height = SU.cast("number", height) + self:_ensureInit() + x = trueXCoord(x) + y = trueYCoord(y) + + -- FIXME: Logic is wrong here + local paperY = SILE.documentState.sheetSize[2] + if not _div[#_div].rel then + y = paperY - y - height + else + y = paperY - y - height + x = x - _div[#_div].x + y = y - _div[#_div].y + end + if width < 0 then + x = x + width + width = -width + end + if height < 0 then + y = y + height + height = -height + end + self.fd:write("
") + self.fd:write('
') +end + +function outputter:debugFrame (frame) + self:_ensureInit() + self:pushColor({ r = 0.8, g = 0, b = 0 }) + self:drawRule(frame:left()-_dl/2, frame:top()-_dl/2, frame:width()+_dl, _dl) + self:drawRule(frame:left()-_dl/2, frame:top()-_dl/2, _dl, frame:height()+_dl) + self:drawRule(frame:right()-_dl/2, frame:top()-_dl/2, _dl, frame:height()+_dl) + self:drawRule(frame:left()-_dl/2, frame:bottom()-_dl/2, frame:width()+_dl, _dl) + -- FIXME NOT IMPLEMENTED + -- local stuff = SILE.shaper:createNnodes(frame.id, debugfont) + -- stuff = stuff[1].nodes[1].value.glyphString -- Horrible hack + -- local buf = {} + -- for i = 1, #stuff do + -- buf[i] = glyph2string(stuff[i]) + -- end + -- buf = table.concat(buf, "") + -- self:_withDebugFont(function () + -- self:setCursor(frame:left():tonumber() - _dl/2, frame:top():tonumber() + _dl/2) + -- self:_drawString(buf, 0, 0, 0) + -- end) + self:popColor() +end + +function outputter:debugHbox (hbox, scaledWidth) + self:_ensureInit() + self:pushColor({ r = 0.8, g = 0.3, b = 0.3 }) + local paperY = SILE.documentState.paperSize[2] + local x, y = self:getCursor() + y = paperY - y + self:drawRule(x-_dl/2, y-_dl/2-hbox.height, scaledWidth+_dl, _dl) + self:drawRule(x-_dl/2, y-hbox.height-_dl/2, _dl, hbox.height+hbox.depth+_dl) + self:drawRule(x-_dl/2, y-_dl/2, scaledWidth+_dl, _dl) + self:drawRule(x+scaledWidth-_dl/2, y-hbox.height-_dl/2, _dl, hbox.height+hbox.depth+_dl) + if hbox.depth > SILE.length(0) then + self:drawRule(x-_dl/2, y+hbox.depth-_dl/2, scaledWidth+_dl, _dl) + end + 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 paperY = SILE.documentState.sheetSize[2] + local x0 = trueXCoord(xorigin) + local y0 = paperY - trueYCoord(yorigin) + self:_ensureInit() + + local xt = x0 - _div[#_div].x + local yt = y0 - _div[#_div].y + + _div[#_div+1] = { x = x0, y = y0, rel = true } + + local style = { + position = "absolute", + left = xt .. "pt", + bottom = yt .. "pt", + transform = "scale(" .. xratio .. ", " .. yratio .. ")" + } + local s = "" + for k, v in pairs(style) do + s = s .. k .. ":" .. v .. ";" + end + + self.fd:write('
') + callback() + self.fd:write("
") + + _div[#_div] = nil +end + +function outputter:rotateFn (xorigin, yorigin, theta, callback) + xorigin = SU.cast("number", xorigin) + yorigin = SU.cast("number", yorigin) + local paperY = SILE.documentState.sheetSize[2] + local x0 = trueXCoord(xorigin) + local y0 = paperY - trueYCoord(yorigin) + self:_ensureInit() + + local xt = x0 - _div[#_div].x + local yt = y0 - _div[#_div].y + + _div[#_div+1] = { x = x0, y = y0, rel = true } + + local style = { + position = "absolute", + left = xt .. "pt", + bottom = yt .. "pt", -- + transform = "rotate(" .. -theta .. "rad)" + } + local s = "" + for k, v in pairs(style) do + s = s .. k .. ":" .. v .. ";" + end + + self.fd:write('
') + callback() + self.fd:write("
") + + _div[#_div] = nil +end + +-- Unstable link APIs + +function outputter:linkAnchor (x, y, name) + x = SU.cast("number", x) + y = SU.cast("number", y) + self:_ensureInit() + local x0 = trueXCoord(x) + local y0 = trueYCoord(y) + self:_ensureInit() + + local xt = x0 - _div[#_div].x + local yt = y0 - _div[#_div].y + + local style = { + position = "absolute", + left = xt .. "pt", + bottom = yt .. "pt", + } + local s = "" + for k, v in pairs(style) do + s = s .. k .. ":" .. v .. ";" + end + + self.fd:write('
') +end + +function outputter:enterLinkTarget (x0, y0, dest, options) + local target = options.external and dest or ("#" .. dest) + + x0 = trueXCoord(x0) + y0 = trueYCoord(y0) + self:_ensureInit() + + local xt = x0 - _div[#_div].x + local yt = y0 - _div[#_div].y + + _div[#_div+1] = { x = x0, y = y0, rel = true } + + local style = { + position = "absolute", + left = xt .. "pt", + bottom = yt .. "pt", + } + local s = "" + for k, v in pairs(style) do + s = s .. k .. ":" .. v .. ";" + end + + self.fd:write('
' + .. '') +end +function outputter:leaveLinkTarget (_, _, _, _, _, _) + self.fd:write("
") + _div[#_div] = nil +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 + end + self:_ensureInit() + -- FIXME: NOT IMPLEMENTED +end + +function outputter:setBookmark (_, _, _) -- dest, title, level + self:_ensureInit() + -- FIXME: NOT IMPLEMENTED +end + +return outputter diff --git a/outputters/libtexpdf.lua b/outputters/libtexpdf.lua new file mode 100644 index 0000000..e04f329 --- /dev/null +++ b/outputters/libtexpdf.lua @@ -0,0 +1,385 @@ +local base = require("outputters.base") +local pdf = require("justenoughlibtexpdf") + +local cursorX = 0 +local cursorY = 0 + +local started = false +local lastkey = false + +local debugfont = SILE.font.loadDefaults({ family = "Gentium Plus", language = "en", size = 10 }) + +local glyph2string = function (glyph) + return string.char(math.floor(glyph % 2^32 / 2^8)) .. string.char(glyph % 0x100) +end + +local _dl = 0.5 + +local _debugfont +local _font + +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 + deltaX = (SILE.documentState.sheetSize[1] - SILE.documentState.paperSize[1]) / 2 + end + return x + deltaX +end +local function trueYCoord (y) + if not deltaY then + deltaY = (SILE.documentState.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.sheetSize[1], SILE.documentState.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 + end +end + +function outputter:newPage () + self:_ensureInit() + pdf.endpage() + pdf.beginpage() +end + +-- pdf stucture package needs a tie in here +function outputter._endHook (_) +end + +function outputter:finish () + self:_ensureInit() + pdf.endpage() + self:_endHook() + pdf.finish() + started = false + lastkey = nil +end + +function outputter.getCursor (_) + return cursorX, cursorY +end + +function outputter.setCursor (_, x, y, relative) + x = SU.cast("number", x) + y = SU.cast("number", y) + local offset = relative and { x = cursorX, y = cursorY } or { x = 0, y = 0 } + cursorX = offset.x + x + cursorY = offset.y + (relative and 0 or SILE.documentState.paperSize[2]) - y +end + +function outputter:setColor (color) + self:_ensureInit() + if color.r then pdf.setcolor_rgb(color.r, color.g, color.b) end + if color.c then pdf.setcolor_cmyk(color.c, color.m, color.y, color.k) end + if color.l then pdf.setcolor_gray(color.l) end +end + +function outputter:pushColor (color) + self:_ensureInit() + if color.r then pdf.colorpush_rgb(color.r, color.g, color.b) end + if color.c then pdf.colorpush_cmyk(color.c, color.m, color.y, color.k) end + if color.l then pdf.colorpush_gray(color.l) end +end + +function outputter:popColor () + self:_ensureInit() + pdf.colorpop() +end + +function outputter:_drawString (str, width, x_offset, y_offset) + local x, y = self:getCursor() + pdf.colorpush_rgb(0,0,0) + pdf.colorpop() + pdf.setstring(trueXCoord(x+x_offset), trueYCoord(y+y_offset), str, string.len(str), _font, width) +end + +function outputter:drawHbox (value, width) + width = SU.cast("number", width) + self:_ensureInit() + if not value.glyphString then return end + -- Nodes which require kerning or have offsets to the glyph + -- position should be output a glyph at a time. We pass the + -- glyph advance from the htmx table, so that libtexpdf knows + -- how wide each glyph is. It uses this to then compute the + -- relative position between the pen after the glyph has been + -- painted (cursorX + glyphAdvance) and the next painting + -- position (cursorX + width - remember that the box's "width" + -- is actually the shaped x_advance). + if value.complex then + for i = 1, #value.items do + local item = value.items[i] + local buf = glyph2string(item.gid) + self:_drawString(buf, item.glyphAdvance, item.x_offset or 0, item.y_offset or 0) + self:setCursor(item.width, 0, true) + end + else + local buf = {} + for i = 1, #value.glyphString do + buf[i] = glyph2string(value.glyphString[i]) + end + buf = table.concat(buf, "") + self:_drawString(buf, width, 0, 0) + end +end + +function outputter:_withDebugFont (callback) + if not _debugfont then + _debugfont = self:setFont(debugfont) + end + local oldfont = _font + _font = _debugfont + callback() + _font = oldfont +end + +function outputter:setFont (options) + self:_ensureInit() + local key = SILE.font._key(options) + if lastkey and key == lastkey then return _font end + local font = SILE.font.cache(options, SILE.shaper.getFace) + if options.direction == "TTB" then + font.layout_dir = 1 + end + if SILE.typesetter.frame and SILE.typesetter.frame:writingDirection() == "TTB" then + pdf.setdirmode(1) + else + pdf.setdirmode(0) + end + _font = pdf.loadfont(font) + if _font < 0 then SU.error("Font loading error for " .. pl.pretty.write(options, "")) end + lastkey = key + return _font +end + +function outputter:drawImage (src, x, y, width, height, pageno) + x = SU.cast("number", x) + y = SU.cast("number", y) + width = SU.cast("number", width) + height = SU.cast("number", height) + self:_ensureInit() + pdf.drawimage(src, trueXCoord(x), trueYCoord(y), width, height, pageno or 1) +end + +function outputter:getImageSize (src, pageno) + self:_ensureInit() -- in case it's a PDF file + local llx, lly, urx, ury, xresol, yresol = pdf.imagebbox(src, pageno or 1) + return (urx-llx), (ury-lly), xresol, yresol +end + +function outputter:drawSVG (figure, x, y, _, height, scalefactor) + self:_ensureInit() + x = SU.cast("number", x) + y = SU.cast("number", y) + height = SU.cast("number", height) + pdf.add_content("q") + self:setCursor(x, y) + x, y = self:getCursor() + local newy = y - SILE.documentState.paperSize[2] / 2 + height - SILE.documentState.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 + +function outputter:drawRule (x, y, width, height) + x = SU.cast("number", x) + y = SU.cast("number", y) + width = SU.cast("number", width) + height = SU.cast("number", height) + self:_ensureInit() + local paperY = SILE.documentState.paperSize[2] + pdf.setrule(trueXCoord(x), trueYCoord(paperY - y - height), width, height) +end + +function outputter:debugFrame (frame) + self:_ensureInit() + self:pushColor({ r = 0.8, g = 0, b = 0 }) + self:drawRule(frame:left()-_dl/2, frame:top()-_dl/2, frame:width()+_dl, _dl) + self:drawRule(frame:left()-_dl/2, frame:top()-_dl/2, _dl, frame:height()+_dl) + self:drawRule(frame:right()-_dl/2, frame:top()-_dl/2, _dl, frame:height()+_dl) + self:drawRule(frame:left()-_dl/2, frame:bottom()-_dl/2, frame:width()+_dl, _dl) + -- self:drawRule(frame:left() + frame:width()/2 - 5, (frame:top() + frame:bottom())/2+5, 10, 10) + local stuff = SILE.shaper:createNnodes(frame.id, debugfont) + stuff = stuff[1].nodes[1].value.glyphString -- Horrible hack + local buf = {} + for i = 1, #stuff do + buf[i] = glyph2string(stuff[i]) + end + buf = table.concat(buf, "") + self:_withDebugFont(function () + self:setCursor(frame:left():tonumber() - _dl/2, frame:top():tonumber() + _dl/2) + self:_drawString(buf, 0, 0, 0) + end) + self:popColor() +end + +function outputter:debugHbox (hbox, scaledWidth) + self:_ensureInit() + self:pushColor({ r = 0.8, g = 0.3, b = 0.3 }) + local paperY = SILE.documentState.paperSize[2] + local x, y = self:getCursor() + y = paperY - y + self:drawRule(x-_dl/2, y-_dl/2-hbox.height, scaledWidth+_dl, _dl) + self:drawRule(x-_dl/2, y-hbox.height-_dl/2, _dl, hbox.height+hbox.depth+_dl) + self:drawRule(x-_dl/2, y-_dl/2, scaledWidth+_dl, _dl) + self:drawRule(x+scaledWidth-_dl/2, y-hbox.height-_dl/2, _dl, hbox.height+hbox.depth+_dl) + if hbox.depth > SILE.length(0) then + self:drawRule(x-_dl/2, y+hbox.depth-_dl/2, scaledWidth+_dl, _dl) + end + 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 (_, _, _, _) -- coords, 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 new file mode 100644 index 0000000..10e7a71 --- /dev/null +++ b/packages/background/init.lua @@ -0,0 +1,80 @@ +local base = require("packages.base") + +local package = pl.class(base) +package._name = "background" + +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.class:registerHook("newpage", outputBackground) +end + +function package:registerCommands () + + self:registerCommand("background", function (options, _) + 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 + 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] +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} +]] + +return package diff --git a/packages/cropmarks/init.lua b/packages/cropmarks/init.lua new file mode 100644 index 0000000..5135fae --- /dev/null +++ b/packages/cropmarks/init.lua @@ -0,0 +1,93 @@ +local base = require("packages.base") + +local package = pl.class(base) +package._name = "cropmarks" + +local outcounter = 1 + +local function outputMarks () + local page = SILE.getFrame("page") + -- 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" }) + 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("Migrating content is forbidden in crop header") + end + + 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 + +function package:_init () + base._init(self) + self:loadPackage("date") +end + +function package:registerCommands () + + self:registerCommand("cropmarks:header", function (_, _) + local info = SILE.masterFilename + .. " - " + .. self.class.packages.date:date({ format = "%x %X" }) + .. " - " .. outcounter + SILE.typesetter:typeset(info) + end) + + self:registerCommand("cropmarks:setup", function (_, _) + self.class:registerHook("endpage", outputMarks) + end) + + self:registerCommand("crop:setup", function (_, _) + SU.deprecated("crop:setup", "cropmarks:setup", "0.14.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 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. + +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{\cropmarks:header}. +\end{document} +]] + +return package diff --git a/packages/pdf/init.lua b/packages/pdf/init.lua new file mode 100644 index 0000000..2c41e08 --- /dev/null +++ b/packages/pdf/init.lua @@ -0,0 +1,128 @@ +-- +-- 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" + +function package:registerCommands () + + self:registerCommand("pdf:destination", function (options, _) + local name = SU.required(options, "name", "pdf:destination") + SILE.typesetter:pushHbox({ + outputYourself = function (_, typesetter, line) + local state = typesetter.frame.state + typesetter.frame:advancePageDirection(-line.height) + local x, y = state.cursorX, state.cursorY + typesetter.frame:advancePageDirection(line.height) + local _y = SILE.documentState.paperSize[2] - y + SILE.outputter:linkAnchor(x, _y, name) + end + }) + end) + + self:registerCommand("pdf:bookmark", function (options, _) + local dest = SU.required(options, "dest", "pdf:bookmark") + local title = SU.required(options, "title", "pdf:bookmark") + local level = SU.cast("integer", options.level or 1) + + SILE.outputter:setBookmark(dest, title, level) + 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 + SILE.typesetter:pushHbox({ + value = nil, + height = SILE.measurement(0), + width = SILE.measurement(0), + depth = SILE.measurement(0), + outputYourself = function (_, _, _) + pdf.add_content(content[1]) + end + }) + end) + + self:registerCommand("pdf:link", function (options, content) + local dest = SU.required(options, "dest", "pdf:link") + local external = SU.boolean(options.external, false) + local borderwidth = options.borderwidth and SU.cast("measurement", options.borderwidth):tonumber() or 0 + local bordercolor = SILE.color(options.bordercolor or "blue") + local borderoffset = SU.cast("measurement", options.borderoffset or "1pt"):tonumber() + local opts = { + external = external, + borderstyle = options.borderstyle, + bordercolor = bordercolor, + borderwidth = borderwidth, + borderoffset = borderoffset + } + + local x0, y0 + SILE.typesetter:pushHbox({ + value = nil, + height = 0, + width = 0, + depth = 0, + outputYourself = function (_, typesetter, _) + x0 = typesetter.frame.state.cursorX:tonumber() + y0 = (SILE.documentState.paperSize[2] - typesetter.frame.state.cursorY):tonumber() + SILE.outputter:enterLinkTarget(x0, y0, dest, opts) + end + }) + local hbox, hlist = SILE.typesetter:makeHbox(content) -- hack + SILE.typesetter:pushHbox(hbox) + SILE.typesetter:pushHbox({ + value = nil, + height = 0, + width = 0, + depth = 0, + outputYourself = function (_, typesetter, _) + 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, _) + local key = SU.required(options, "key", "pdf:metadata") + if options.val ~= nil then + SU.deprecated("\\pdf:metadata[…, val=…]", "\\pdf:metadata[…, value=…]", "0.12.0", "0.13.0") + end + local value = SU.required(options, "value", "pdf:metadata") + + SILE.outputter:setMetadata(key, value) + end) + +end + +package.documentation = [[ +\begin{document} +The \autodoc:package{pdf} package enables basic support for PDF links and table-of-contents entries. +It provides the four commands \autodoc:command{\pdf:destination}, \autodoc:command{\pdf:link}, \autodoc:command{\pdf:bookmark}, and \autodoc:command{\pdf:metadata}. + +The \autodoc:command{\pdf:destination} parameter creates a link target; it expects a parameter called \autodoc:parameter{name} to uniquely identify the target. +To create a link to that location in the document, use \autodoc:command{\pdf:link[dest=]{}}. + +The \autodoc:command{\pdf:link} command accepts several options defining its border style: a \autodoc:parameter{borderwidth} length setting the border width (defaults to \code{0}, meaning no border), a \autodoc:parameter{borderstyle} string (can be set to \code{underline} or \code{dashed}, otherwise a solid box), a \autodoc:parameter{bordercolor} color specification for this border (defaults to \code{blue}), and finally a \autodoc:parameter{borderoffset} length for adjusting the border with some vertical space above the content and below the baseline (defaults to \code{1pt}). +Note that PDF renderers may vary on how they honor these border styling features on link annotations. + +It also has an \autodoc:parameter{external} option for URL links, which is not intended to be used directly—refer to the \autodoc:package{url} package for more flexibility typesetting external links. + +To set arbitrary key-value metadata, use something like \autodoc:command{\pdf:metadata[key=Author, value=J. Smith]}. The PDF metadata field names are case-sensitive. Common keys include \code{Title}, \code{Author}, \code{Subject}, \code{Keywords}, \code{CreationDate}, and \code{ModDate}. +\end{document} +]] + +return package diff --git a/packages/rotate/init.lua b/packages/rotate/init.lua new file mode 100644 index 0000000..6aad18c --- /dev/null +++ b/packages/rotate/init.lua @@ -0,0 +1,136 @@ +local base = require("packages.base") + +local package = pl.class(base) +package._name = "rotate" + +local enter = function (self, _) + -- Probably broken, see: + -- https://github.com/sile-typesetter/sile/issues/427 + if not self.rotate then return end + 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 + 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, _) + if not self.rotate then return end + 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? +-- rect1 = Rectangle[{0, 0}, {w, h}] +-- {{xmin, xmax}, {ymin, ymax}} = Refine[RegionBounds[TransformedRegion[rect1, +-- RotationTransform[theta, {w/2,h/2}]]], +-- w > 0 && h > 0 && theta > 0 && theta < 2 Pi ] +-- PiecewiseExpand[xmax - xmin] + -- \[Piecewise] -w Cos[theta]-h Sin[theta] Sin[theta]<=0&&Cos[theta]<=0 + -- w Cos[theta]-h Sin[theta] Sin[theta]<=0&&Cos[theta]>0 + -- -w Cos[theta]+h Sin[theta] Sin[theta]>0&&Cos[theta]<=0 + -- w Cos[theta]+h Sin[theta] True + +local outputRotatedHbox = function (self, typesetter, line) + local origbox = self.value.orig + local theta = self.value.theta + + -- Find origin of untransformed hbox + 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 + +function package:_init () + base._init(self) + if SILE.typesetter and SILE.typesetter.frame then + enter(SILE.typesetter.frame, SILE.typesetter) + table.insert(SILE.typesetter.frame.leaveHooks, leave) + end + table.insert(SILE.framePrototype.enterHooks, enter) + table.insert(SILE.framePrototype.leaveHooks, leave) +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) + local h = origbox.height + origbox.depth + local w = origbox.width.length + local st = math.sin(theta) + local ct = math.cos(theta) + local height, width, depth + if st <= 0 and ct <= 0 then + width = -w * ct - h * st + height = 0.5*(h-h*ct-w*st) + depth = 0.5*(h+h*ct+w*st) + elseif st <=0 and ct > 0 then + width = w * ct - h * st + height = 0.5*(h+h*ct-w*st) + depth = 0.5*(h-h*ct+w*st) + elseif st > 0 and ct <= 0 then + width = -w * ct + h * st + height = 0.5*(h-h*ct+w*st) + depth = 0.5*(h+h*ct-w*st) + else + width = w * ct + h * st + height = 0.5*(h+h*ct+w*st) + depth = 0.5*(h-h*ct-w*st) + end + depth = -depth + if depth < SILE.length(0) then depth = SILE.length(0) end + SILE.typesetter:pushHbox({ + value = { orig = origbox, theta = theta}, + height = height, + width = width, + depth = depth, + outputYourself = outputRotatedHbox + }) + SILE.typesetter:pushHlist(hlist) + end) + +end + +package.documentation = [[ +\begin{document} +\use[module=packages.rotate] +The \autodoc:package{rotate} package allows you to rotate things. You can rotate entire +frames, by adding the \autodoc:parameter{rotate=} declaration to your frame declaration, +and you can rotate any content by issuing the command \autodoc:command{\rotate[angle=]{}}, +where the angle is measured in degrees. + +Content which is rotated is placed in a box and rotated. The height and width of +the rotated box is measured, and then put into the normal horizontal list for +typesetting. The effect is that space is reserved around the rotated content. +The best way to understand this is by example: here is some text rotated by +\rotate[angle=10]{ten}, \rotate[angle=20]{twenty}, and \rotate[angle=40]{forty} degrees. + +The previous line was produced by the following code: + +\begin[type=autodoc:codeblock]{raw} +here is some text rotated by +\rotate[angle=10]{ten}, \rotate[angle=20]{twenty}, and \rotate[angle=40]{forty} degrees. +\end{raw} +\end{document} +]] + +return package diff --git a/packages/scalebox/init.lua b/packages/scalebox/init.lua new file mode 100644 index 0000000..8ab331f --- /dev/null +++ b/packages/scalebox/init.lua @@ -0,0 +1,73 @@ +local base = require("packages.base") + +local package = pl.class(base) +package._name = "scalebox" + +function package:registerCommands () + + self:registerCommand("scalebox", function(options, content) + 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 + + local hbox, hlist = SILE.typesetter:makeHbox(content) + local xratio, yratio = SU.cast("number", options.xratio or 1), SU.cast("number", options.yratio or 1) + if xratio == 0 or yratio == 0 then + SU.error("Scaling ratio cannot be null") + end + + local W = hbox.width * math.abs(xratio) + local H, D + if yratio > 0 then + H = hbox.height * yratio + D = hbox.depth * yratio + else + H = hbox.depth * -yratio + D = hbox.height * -yratio + end + + SILE.typesetter:pushHbox({ + width = W, + height = H, + depth = D, + outputYourself = function(node, typesetter, line) + local outputWidth = SU.rationWidth(node.width, node.width, line.ratio) + local X = typesetter.frame.state.cursorX + local Y = typesetter.frame.state.cursorY + + if xratio < 0 then + typesetter.frame:advanceWritingDirection(-outputWidth) + end + 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) + end + }) + SILE.typesetter:pushHlist(hlist) + end, "Scale content by some horizontal and vertical ratios") + +end + +package.documentation = [[ +\begin{document} +The \autodoc:package{scalebox} package allows to scale any content by some horizontal +and vertical ratios, by issuing the command +\autodoc:command{\scalebox[xratio=, yratio=]{}}, +where the ratios are optional non-null numbers (defaulting to 1). +The content is placed in a box and scaled. + +Here is an \scalebox[xratio=0.75, yratio=1.25]{example}. + +The previous line was produced by the following code: + +\begin[type=autodoc:codeblock]{raw} +Here is an \scalebox[xratio=0.75, yratio=1.25]{example}. +\end{raw} +\end{document} +]] + +return package diff --git a/packages/url/init.lua b/packages/url/init.lua new file mode 100644 index 0000000..b754595 --- /dev/null +++ b/packages/url/init.lua @@ -0,0 +1,175 @@ +local base = require("packages.base") + +local package = pl.class(base) +package._name = "url" + +-- URL escape sequence, URL fragment: +local preferBreakBefore = "%#" +-- URL path elements, URL query arguments, acceptable extras: +local preferBreakAfter = ":/.;?&=!_-" +-- URL scheme: +local alwaysBreakAfter = ":" -- Must have only one character here! + +local escapeRegExpMinimal = function (str) + -- Minimalist = just what's needed for the above strings + return string.gsub(str, '([%.%?%-%%])', '%%%1') +end + +local breakPattern = "["..escapeRegExpMinimal(preferBreakBefore..preferBreakAfter..alwaysBreakAfter).."]" + +function package:_init () + base._init(self) + self:loadPackage("verbatim") + self:loadPackage("inputfilter") + self:loadPackage("pdf") +end + +function package.declareSettings (_) + + SILE.settings:declare({ + parameter = "url.linebreak.primaryPenalty", + type = "integer", + default = 100, + help = "Penalty for breaking lines in URLs at preferred breakpoints" + }) + + SILE.settings:declare({ + parameter = "url.linebreak.secondaryPenalty", + type = "integer", + default = 200, + help = "Penalty for breaking lines in URLs at tolerable breakpoints (should be higher than url.linebreak.primaryPenalty)" + }) + +end + +function package:registerCommands () + + self:registerCommand("href", function (options, content) + if options.src then + SILE.call("pdf:link", { dest = options.src, external = true, + borderwidth = options.borderwidth, + borderstyle = options.borderstyle, + bordercolor = options.bordercolor, + borderoffset = options.borderoffset }, + content) + else + options.src = content[1] + SILE.call("pdf:link", { dest = options.src, external = true, + borderwidth = options.borderwidth, + borderstyle = options.borderstyle, + bordercolor = options.bordercolor, + borderoffset = options.borderoffset }, + function (_, _) + SILE.call("url", { language = options.language }, content) + end) + end + end, "Inserts a PDF hyperlink.") + + local urlFilter = function (node, content, options) + if type(node) == "table" then return node end + local result = {} + for token in SU.gtoke(node, breakPattern) do + if token.string then + result[#result+1] = token.string + else + if string.find(preferBreakBefore, escapeRegExpMinimal(token.separator)) then + -- Accepts breaking before, and at the extreme worst after. + result[#result+1] = self.class.packages.inputfilter:createCommand( + content.pos, content.col, content.lno, + "penalty", { penalty = options.primaryPenalty } + ) + result[#result+1] = token.separator + result[#result+1] = self.class.packages.inputfilter:createCommand( + content.pos, content.col, content.lno, + "penalty", { penalty = options.worsePenalty } + ) + elseif token.separator == alwaysBreakAfter then + -- Accept breaking after (only). + result[#result+1] = token.separator + result[#result+1] = self.class.packages.inputfilter:createCommand( + content.pos, content.col, content.lno, + "penalty", { penalty = options.primaryPenalty } + ) + else + -- Accept breaking after, but tolerate breaking before. + result[#result+1] = self.class.packages.inputfilter:createCommand( + content.pos, content.col, content.lno, + "penalty", { penalty = options.secondaryPenalty } + ) + result[#result+1] = token.separator + result[#result+1] = self.class.packages.inputfilter:createCommand( + content.pos, content.col, content.lno, + "penalty", { penalty = options.primaryPenalty } + ) + end + end + end + return result + end + + self:registerCommand("url", function (options, content) + SILE.settings:temporarily(function () + local primaryPenalty = SILE.settings:get("url.linebreak.primaryPenalty") + local secondaryPenalty = SILE.settings:get("url.linebreak.secondaryPenalty") + local worsePenalty = primaryPenalty + secondaryPenalty + + if options.language then + SILE.languageSupport.loadLanguage(options.language) + if options.language == "fr" then + -- Trick the engine by declaring a "fake"" language that doesn't apply + -- the typographic rules for punctuations + SILE.hyphenator.languages["_fr_noSpacingRules"] = SILE.hyphenator.languages.fr + -- Not needed (the engine already defaults to SILE.nodeMakers.unicode if + -- the language is not found): + -- SILE.nodeMakers._fr_noSpacingRules = SILE.nodeMakers.unicode + SILE.settings:set("document.language", "_fr_noSpacingRules") + else + SILE.settings:set("document.language", options.language) + end + else + SILE.settings:set("document.language", 'und') + end + + local result = self.class.packages.inputfilter:transformContent(content, urlFilter, { + primaryPenalty = primaryPenalty, + secondaryPenalty = secondaryPenalty, + worsePenalty = worsePenalty + }) + SILE.call("urlstyle", {}, result) + end) + end, "Inserts penalties in an URL so it can be broken over multiple lines at appropriate places.") + + self:registerCommand("urlstyle", function (options, content) + SILE.call("code", options, content) + end, "Hook that may be redefined to change the styling of URLs") + +end + +package.documentation = [[ +\begin{document} +\use[module=packages.url] +This package enhances the typesetting of URLs in two ways. +First, it provides the \autodoc:command{\href[src=]{}} command which inserts PDF hyperlinks, \href[src=http://www.sile-typesetter.org/]{like this}. + +The \autodoc:command{\href} command accepts the same \autodoc:parameter{borderwidth}, \autodoc:parameter{bordercolor}, \autodoc:parameter{borderstyle}, and \autodoc:parameter{borderoffset} styling options as the \autodoc:command[check=false]{\pdf:link} command from the \autodoc:package{pdf} package, for instance \href[src=http://www.sile-typesetter.org/, borderwidth=0.4pt, bordercolor=blue, borderstyle=underline]{like this}. + +Nowadays, it is a common practice to have URLs in print articles (whether it is a good practice or not is yet \em{another} topic). +Therefore, the package also provides the \autodoc:command{\url} command, which will automatically insert breakpoints into unwieldy URLs like \url{https://github.com/sile-typesetter/sile-typesetter.github.io/tree/master/examples} so that they can be broken up over multiple lines. + +It allows line breaks after the colon, and before or after appropriate segments of an URL (path elements, query parts, fragments, etc.). +By default, the \autodoc:command{\url} command ignores the current language, as one would not want hyphenation to occur in URL segments. +If you have no other choice, however, you can pass it a \autodoc:parameter{language} option to enforce a language to be applied. +Note that if French (\code{fr}) is selected, the special typographic rules applying to punctuations in this language are disabled. + +To typeset a URL and also make it an active hyperlink, use the \autodoc:command{\href} command without the \autodoc:parameter{src} option, +but with the URL passed as argument. + +The breaks are controlled by two penalty settings: \autodoc:setting{url.linebreak.primaryPenalty} for preferred breakpoints and, for less acceptable but still tolerable breakpoints, \autodoc:setting{url.linebreak.secondaryPenalty}—its value should logically be higher than the previous one. + +The \autodoc:command{\urlstyle} command hook may be overridden to change the style of URLs. +By default, they are typeset as “code”. + +\end{document} +]] + +return package diff --git a/tests/gutenberg.png b/tests/gutenberg.png new file mode 100644 index 0000000..ecce70a Binary files /dev/null and b/tests/gutenberg.png differ diff --git a/tests/oldmanandbooks.svg b/tests/oldmanandbooks.svg new file mode 100644 index 0000000..e5b82be --- /dev/null +++ b/tests/oldmanandbooks.svg @@ -0,0 +1,46 @@ + + + + + + + + + image/svg+xml + + + + + Openclipart + + + Old Man and Books + 2013-09-27T06:46:27 + Originally there was some newspaper title obscuring this old man, so I put the effort into 'restoring' him before synthesizing this image. I think it worked, and now he can be old forever... ready for whatever wonderful clipart use you have for him! + https://openclipart.org/detail/183916/old-man-and-books-by-j4p4n-183916 + + + j4p4n + + + + + books + bookshelf + cobwebs + librarian + library + man + old + reading + + + + + + + + + + + \ No newline at end of file diff --git a/tests/stained-paper.jpg b/tests/stained-paper.jpg new file mode 100644 index 0000000..3be10a4 Binary files /dev/null and b/tests/stained-paper.jpg differ diff --git a/tests/test1.sil b/tests/test1.sil new file mode 100644 index 0000000..b6ea4e4 --- /dev/null +++ b/tests/test1.sil @@ -0,0 +1,52 @@ +\begin[papersize=6in x 9in, bleed=0.25in, sheetsize=a4]{document} +\use[module=packages/dropcaps] +\use[module=packages/lorem] +\use[module=packages/cropmarks] +\use[module=packages/background] +\use[module=packages/svg] +\use[module=packages/image] +\use[module=packages/scalebox] +\use[module=packages/rotate] +\use[module=packages/textsubsuper] +\use[module=packages/url] +\cropmarks:setup +\background[color=#e9d8ba, allpages=false] +\pdf:metadata[key=Title, value=Some title] +\pdf:metadata[key=Author, value=Some author] +\dropcap{T}his is paragraph 1. \lorem + +This is paragraph 2. +\pdf:bookmark[dest=link1, level=1, title=Link1] +\pdf:bookmark[dest=link2, level=2, title=Link2] + +\bigskip + +\dropcap{P}aragraph 3 here, large and in charge. + +Paragraph 4. Oh dear…\lorem + +\bigskip +Here is an \scalebox[xratio=0.75, yratio=1.25]{example}. + +here is some text rotated by +\rotate[angle=10]{ten}, \rotate[angle=20]{twenty}, and \rotate[angle=40]{forty} degrees. + +Some text with super\textsuperscript[fake=true]{super} and sub\textsubscript[fake=true]{sub}. + +\bigskip +\href[src=http://www.google.com, borderwidth=1pt]{This is a link to Google.} + +\pdf:link[dest=link1, borderwidth=1pt]{This is an internal link.} + +\supereject + +\background[src=stained-paper.jpg, allpages=false] + +\center{\svg[src=oldmanandbooks.svg, width=50%lw]} + +\center{\img[src=gutenberg.png, width=50%lw]} + +Here is my link target: \pdf:destination[name=link1]\lorem + +Another link target is \pdf:destination[name=link2]here +\end{document} diff --git a/typesetters/base.lua b/typesetters/base.lua index 0b09a59..05e313e 100644 --- a/typesetters/base.lua +++ b/typesetters/base.lua @@ -1126,16 +1126,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