diff --git a/outputters/base.lua b/outputters/base.lua index 4888a06..570ae8b 100644 --- a/outputters/base.lua +++ b/outputters/base.lua @@ -36,7 +36,7 @@ function outputter.debugHbox (_, _, _) end function outputter.linkAnchor (_, _, _) end -- Unstable API -function outputter.enterLinkTarget (_, _, _) end -- Unstable API +function outputter.enterLinkTarget (_, _, _, _, _) end -- Unstable API function outputter.leaveLinkTarget (_, _, _, _, _, _, _) end -- Unstable API 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 index 2c63877..e04f329 100644 --- a/outputters/libtexpdf.lua +++ b/outputters/libtexpdf.lua @@ -324,7 +324,7 @@ local function borderStyle (style, width) return "/Border[0 0 " .. width .. "]" end -function outputter:enterLinkTarget (_, _) -- destination, options as argument +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. diff --git a/packages/pdf/init.lua b/packages/pdf/init.lua index 7a5b018..2c41e08 100644 --- a/packages/pdf/init.lua +++ b/packages/pdf/init.lua @@ -77,7 +77,7 @@ function package:registerCommands () outputYourself = function (_, typesetter, _) x0 = typesetter.frame.state.cursorX:tonumber() y0 = (SILE.documentState.paperSize[2] - typesetter.frame.state.cursorY):tonumber() - SILE.outputter:enterLinkTarget(dest, opts) + SILE.outputter:enterLinkTarget(x0, y0, dest, opts) end }) local hbox, hlist = SILE.typesetter:makeHbox(content) -- hack