----------------------------------------------------------------------- -- PDF exporter in Lua for SciTE -- Version 0.9.2, 20070805 -- -- Adapted from SciTE sources (scite/src/Exporters.cxx CVS 20040723) -- by Kein-Hong Man (This is a straightforward -- conversion and so I decline to claim it as my own.) -- -- Copyright 1998-2007 by Neil Hodgson -- All Rights Reserved -- -- Permission to use, copy, modify, and distribute this software and -- its documentation for any purpose and without fee is hereby granted, -- provided that the above copyright notice appear in all copies and -- that both that copyright notice and this permission notice appear in -- supporting documentation. -- -- NEIL HODGSON DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, -- INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN -- NO EVENT SHALL NEIL HODGSON BE LIABLE FOR ANY SPECIAL, INDIRECT OR -- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, -- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION -- WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -- ----------------------------------------------------------------------- -- USAGE -- -- * Please see SciTELuaExporters.html for notes on how to get this -- exporter up and running. -- * Requires SciTE_ExportBase.lua to be loaded first. -- * Has been tested casually with these lexers: basic text, props, -- lua and cpp giving byte-for-byte identical output. ----------------------------------------------------------------------- ----------------------------------------------------------------------- -- a simple check to alert of namespace collision, but allows different -- files to define their own functions in the exporters table ----------------------------------------------------------------------- if exporters then if exporters.SaveToPDF then error("SciTE_ExporterPDF: exporters.SaveToPDF already defined") end else exporters = {} end ----------------------------------------------------------------------- -- exporters:SaveToPDF -- -- Exports the document in the current window to a PDF format file. -- ----------------------------------------------------------------------- function exporters:SaveToPDF() local selBeg, selEnd = exportutil.GetSelPos() -- -- PDF Exporter. Status: Beta -- Contributed by Ahmad M. Zawawi -- Modifications by Darren Schroeder Feb 22, 2003; Philippe Lhoste 2003-10 -- Overhauled by Kein-Hong Man 2003-11 -- -- This exporter is meant to be small and simple; users are expected to -- use other methods for heavy-duty formatting. PDF elements marked with -- "PDF1.4Ref" states where in the PDF 1.4 Reference Spec (the PDF file of -- which is freely available from Adobe) the particular element can be found. -- -- Possible TODOs that will probably not be implemented: full styling, -- optimization, font substitution, compression, character set encoding. -- local PDF_TAB_DEFAULT = 8 local PDF_FONT_DEFAULT = 2 -- Helvetica local PDF_FONTSIZE_DEFAULT = 10 local PDF_SPACING_DEFAULT = 1.2 local PDF_HEIGHT_DEFAULT = 792 -- Letter local PDF_WIDTH_DEFAULT = 612 local PDF_MARGIN_DEFAULT = 72 -- 1.0" local PDF_ENCODING = "WinAnsiEncoding" local PDFfontNames = { "Courier", "Courier-Bold", "Courier-Oblique", "Courier-BoldOblique", "Helvetica", "Helvetica-Bold", "Helvetica-Oblique", "Helvetica-BoldOblique", "Times-Roman", "Times-Bold", "Times-Italic", "Times-BoldItalic", } -- ascender, descender aligns font origin point with page local PDFfontAscenders = { 629, 718, 699, } local PDFfontDescenders = { 157, 207, 217, } local PDFfontWidths = { 600, 0, 0, } -- This class conveniently handles the tracking of PDF objects -- so that the cross-reference table can be built (PDF1.4Ref(p39)) -- All writes to fp passes through a PDFObjectTracker object. -- PDFObjectTracker local oT = {} function oT:init(fp_) self.fp = fp_ self.offsetList = {} self.index = 1 end function oT:write(objectData) -- note binary write used, open with "wb" self.fp:write(objectData) end -- returns object number assigned to the supplied data function oT:add(objectData) -- save offset, then format and write object self.offsetList[self.index] = self.fp:seek() self:write(self.index) self:write(" 0 obj\n") self:write(objectData) self:write("endobj\n") self.index = self.index + 1 return self.index - 1 end -- builds xref table, returns file offset of xref table function oT:xref() -- xref start index and number of entries local xrefStart = self.fp:seek() self:write("xref\n0 ") self:write(self.index) -- a xref entry *must* be 20 bytes long (PDF1.4Ref(p64)) -- so extra space added; also the first entry is special self:write("\n0000000000 65535 f \n") for i = 1, self.index - 1 do local val = tostring(self.offsetList[i]) self:write(string.rep("0", 10 - #val)..val) self:write(" 00000 n \n") end return xrefStart end -- PDFRender local pr = {} -- Object to manage line and page rendering. Apart from startPDF, endPDF -- everything goes in via add() and nextLine() so that line formatting -- and pagination can be done properly. -- pr.style = {} pr.pageMargin = {} -- function pr:init() self.pageStarted = false self.pageCount = 0 end -- function pr:fontToPoints(thousandths) return self.fontSize * thousandths / 1000.0 end -- function pr:setStyle(oldStyle, style_) local styleNext = style_ if style_ == -1 then styleNext = self.styleCurrent end local buff = "" if styleNext ~= self.styleCurrent or style_ == -1 then if self.style[self.styleCurrent].font ~= self.style[styleNext].font or style_ == -1 then buff = string.format("/F%d %d Tf ", self.style[styleNext].font + 1, self.fontSize) end if self.style[self.styleCurrent].fore ~= self.style[styleNext].fore or style_ == -1 then buff = buff..self.style[styleNext].fore.."rg " end return buff end return oldStyle end -- function pr:startPDF() if self.fontSize <= 0 then self.fontSize = PDF_FONTSIZE_DEFAULT end -- leading is the term for distance between lines self.leading = self.fontSize * PDF_SPACING_DEFAULT -- sanity check for page size and margins local pageWidthMin = math.floor(self.leading) + self.pageMargin.left + self.pageMargin.right if self.pageWidth < pageWidthMin then self.pageWidth = pageWidthMin end local pageHeightMin = math.floor(self.leading) + self.pageMargin.top + self.pageMargin.bottom if self.pageHeight < pageHeightMin then self.pageHeight = pageHeightMin end -- start to write PDF file here (PDF1.4Ref(p63)) -- ASCII>127 characters to indicate binary-possible stream oT:write("%PDF-1.3\n%Η쏒\n") self.styleCurrent = exportutil.STYLE_DEFAULT -- build objects for font resources; note that font objects are -- *expected* to start from index 1 since they are the first objects -- to be inserted (PDF1.4Ref(p317)) for i = 1,4 do oT:add(string.format("<>\n", i, PDFfontNames[self.fontSet * 4 + i - 4])) end self.pageContentStart = oT.index end -- function pr:endPDF() if self.pageStarted then -- flush buffers self:endPage() end -- refer to all used or unused fonts for simplicity local resourceRef = oT:add("<> >>\n") -- create all the page objects (PDF1.4Ref(p88)) -- forward reference pages object; calculate its object number local pageObjectStart = oT.index local pagesRef = pageObjectStart + self.pageCount for i = 0, self.pageCount - 1 do oT:add(string.format("<>\n", pagesRef, self.pageWidth, self.pageHeight, self.pageContentStart + i, resourceRef)) end -- create page tree object (PDF1.4Ref(p86)) self.pageData = "<>\n", self.pageCount) oT:add(self.pageData) -- create catalog object (PDF1.4Ref(p83)) local catalogRef = oT:add(string.format("<>\n", pagesRef)) -- append the cross reference table (PDF1.4Ref(p64)) local xref = oT:xref() -- end the file with the trailer (PDF1.4Ref(p67)) oT:write(string.format("trailer\n<< /Size %d /Root %d 0 R\n>>".. "\nstartxref\n%d\n%%%%EOF\n", oT.index, catalogRef, xref)) end -- function pr:add(ch, style_) if not self.pageStarted then self:startPage() end -- get glyph width (TODO future non-monospace handling) local glyphWidth = self:fontToPoints(PDFfontWidths[self.fontSet]) self.xPos = self.xPos + glyphWidth -- if cannot fit into a line, flush, wrap to next line if self.xPos > self.pageWidth - self.pageMargin.right then self:nextLine() self.xPos = self.xPos + glyphWidth end -- if different style, then change to style if style_ ~= self.styleCurrent then self:flushSegment() -- output code (if needed) for new style self.segStyle = self:setStyle(self.segStyle, style_) self.stylePrev = self.styleCurrent self.styleCurrent = style_ end -- escape these characters if ch == ")" or ch == "(" or ch == "\\" then self.segment = self.segment.."\\" end if ch ~= " " then self.justWhiteSpace = false end self.segment = self.segment..ch -- add to segment data end -- function pr:flushSegment() if #self.segment > 0 then if self.justWhiteSpace then -- optimise self.styleCurrent = self.stylePrev else self.pageData = self.pageData..self.segStyle end self.pageData = self.pageData.."("..self.segment..")Tj\n" end self.segment = "" self.segStyle = "" self.justWhiteSpace = true end -- function pr:startPage() self.pageStarted = true self.firstLine = true self.pageCount = self.pageCount + 1 local fontAscender = self:fontToPoints(PDFfontAscenders[self.fontSet]) self.yPos = self.pageHeight - self.pageMargin.top - fontAscender -- start a new page self.pageData = string.format("BT 1 0 0 1 %d %d Tm\n", self.pageMargin.left, math.floor(self.yPos)) -- force setting of initial font, colour self.segStyle = self:setStyle(self.segStyle, -1) self.pageData = self.pageData..self.segStyle self.xPos = self.pageMargin.left self.segment = "" self:flushSegment() end -- function pr:endPage() self.pageStarted = false self:flushSegment() -- build actual text object; +3 is for "ET\n" -- PDF1.4Ref(p38) EOL marker preceding endstream not counted -- concatenate stream within the text object oT:add(string.format("<>\nstream\n%s".. "ET\nendstream\n", #self.pageData - 1 + 3, self.pageData)) end -- function pr:nextLine() if not self.pageStarted then self:startPage() end self.xPos = self.pageMargin.left self:flushSegment() -- PDF follows cartesian coords, subtract -> down self.yPos = self.yPos - self.leading local fontDescender = self:fontToPoints(PDFfontDescenders[self.fontSet]) if self.yPos < self.pageMargin.bottom + fontDescender then self:endPage() self:startPage() return end local buffer if self.firstLine then buffer = string.format("0 -%.1f TD\n", self.leading) self.firstLine = false else buffer = "T*\n" end self.pageData = self.pageData..buffer end editor:Colourise(0, -1) -- read exporter flags local tabSize = tonumber(props["tabsize"]) if not tabSize or tabSize <= 0 then tabSize = PDF_TAB_DEFAULT end -- read magnification value to add to default screen font size local propItem pr.fontSize = tonumber(props["export.pdf.magnification"]) or 0 -- set font family according to face name propItem = props["export.pdf.font"] pr.fontSet = PDF_FONT_DEFAULT; if propItem then if propItem == "Courier" then pr.fontSet = 1 elseif propItem == "Helvetica" then pr.fontSet = 2 elseif propItem == "Times" then pr.fontSet = 3 end end -- returns comma-separated fields in a property value as a list local function parseProp(prop) local propValue = props[prop] local propList = {} if not propValue or propValue == "" then return propList end for v in string.gfind(propValue, "([^,]+),?") do table.insert(propList, v) end return propList end -- page size: width, height propItem = parseProp("export.pdf.pagesize") pr.pageWidth = tonumber(propItem[1]) if not pr.pageWidth or pr.pageWidth < 0 then pr.pageWidth = PDF_WIDTH_DEFAULT end pr.pageHeight = tonumber(propItem[2]) if not pr.pageHeight or pr.pageHeight < 0 then pr.pageHeight = PDF_HEIGHT_DEFAULT end -- page margins: left, right, top, bottom propItem = parseProp("export.pdf.margins") pr.pageMargin.left = tonumber(propItem[1]) if not pr.pageMargin.left or pr.pageMargin.left < 0 then pr.pageMargin.left = PDF_MARGIN_DEFAULT end pr.pageMargin.right = tonumber(propItem[2]) if not pr.pageMargin.right or pr.pageMargin.right < 0 then pr.pageMargin.right = PDF_MARGIN_DEFAULT end pr.pageMargin.top = tonumber(propItem[3]) if not pr.pageMargin.top or pr.pageMargin.top < 0 then pr.pageMargin.top = PDF_MARGIN_DEFAULT end pr.pageMargin.bottom = tonumber(propItem[4]) if not pr.pageMargin.bottom or pr.pageMargin.bottom < 0 then pr.pageMargin.bottom = PDF_MARGIN_DEFAULT end -- collect all styles available for that 'language' -- or the default style if no language is available... stylemgr:setlexer(editor.Lexer) for i = 0, exportutil.STYLE_MAX do -- get keys prstyle = {} prstyle.font = 0 prstyle.fore = "" local sd = stylemgr:locate(i) if sd.specified ~= "" then if sd.italics then prstyle.font = prstyle.font + 2 end if sd.bold then prstyle.font = prstyle.font + 1 end if sd.fore then -- grab colour components (max string length produced = 18) local function colorElem(c) if c == 0 then return "0 " end -- optimise if c == 255 then return "1 " end -- 3 decimal places for enough dynamic range return string.format("%.3f ", c / 255) end prstyle.fore = colorElem(sd.fore.r).. colorElem(sd.fore.g).. colorElem(sd.fore.b) elseif i == exportutil.STYLE_DEFAULT then prstyle.fore = "0 0 0 " end -- grab font size from default style if i == exportutil.STYLE_DEFAULT then if sd.size > 0 then pr.fontSize = pr.fontSize + sd.size else pr.fontSize = PDF_FONTSIZE_DEFAULT end end end--if sd.specified pr.style[i] = prstyle end--for -- patch in default foregrounds for i = 0, exportutil.STYLE_MAX do if pr.style[i].fore == "" then pr.style[i].fore = pr.style[exportutil.STYLE_DEFAULT].fore end end local saveName = exportutil.exportfile(props["FileDir"], props["FileName"], "pdf") local fp = io.open(saveName, "wb") if not fp then error("exporters:SaveToPDF: could not save file \""..saveName.."\"") end if exportutil.VERBOSE ~= 0 then _ALERT("\nExporting to PDF, filepath: "..saveName) end -- initialise PDF rendering oT:init(fp) pr:init() pr:startPDF() -- do here all the writing -- local lengthDoc = editor.Length local lineIndex = 0 if selEnd == 0 then -- enable zero length docs pr:nextLine() else local i = selBeg while i < selEnd do local ch = exportutil.CharAt(i) local style = exportutil.StyleAt(i) if ch == "\t" then -- expand tabs local ts = tabSize - lineIndex % tabSize lineIndex = lineIndex + ts for j = 1, ts do pr:add(" ", style) -- add ts count of spaces end elseif ch == "\r" or ch == "\n" then if ch == "\r" and exportutil.CharAt(i + 1) == "\n" then i = i + 1 end -- close and begin a newline... pr:nextLine() lineIndex = 0 else -- write the character normally... pr:add(ch, style) lineIndex = lineIndex + 1 end--ch i = i + 1 end--while end--if -- write required stuff and close the PDF file pr:endPDF() fp:close() if exportutil.VERBOSE ~= 0 then exportutil.progress("... done.") end end -- end of script