diff --git a/Tests/fontPDF_data/input/OTF.otf b/Tests/fontPDF_data/input/OTF.otf new file mode 100644 index 000000000..e164e72f1 Binary files /dev/null and b/Tests/fontPDF_data/input/OTF.otf differ diff --git a/Tests/fontPDF_test.py b/Tests/fontPDF_test.py new file mode 100644 index 000000000..a130a9aba --- /dev/null +++ b/Tests/fontPDF_test.py @@ -0,0 +1,45 @@ +from __future__ import print_function, division, absolute_import + +import os + +from fontTools.ttLib import TTFont + +from afdko.Tools.SharedData.FDKScripts.fontPDF import (doTitle, FontPDFParams) +from afdko.Tools.SharedData.FDKScripts.otfPDF import txPDFFont +from afdko.Tools.SharedData.FDKScripts.pdfgen import Canvas + + +TOOL = 'fontPDF' +OTF_FONT = 'OTF.otf' + + +def _get_input_path(file_name): + return os.path.join(os.path.split(__file__)[0], TOOL + '_data', + 'input', file_name) + + +# ----- +# Tests +# ----- + +def test_doTitle_pageIncludeTitle_1(): + with TTFont(_get_input_path(OTF_FONT)) as otfont: + params = FontPDFParams() + assert params.pageIncludeTitle == 1 + pdfFont = txPDFFont(otfont, params) + rt_canvas = Canvas("pdf_file_path") + assert rt_canvas._code == [] + doTitle(rt_canvas, pdfFont, params, 1) + assert len(rt_canvas._code) + assert 'SourceSansPro-Black' in rt_canvas._code[1] + + +def test_doTitle_pageIncludeTitle_0(): + with TTFont(_get_input_path(OTF_FONT)) as otfont: + params = FontPDFParams() + params.pageIncludeTitle = 0 + pdfFont = txPDFFont(otfont, params) + rt_canvas = Canvas("pdf_file_path") + assert rt_canvas._code == [] + doTitle(rt_canvas, pdfFont, params, 1) + assert rt_canvas._code == [] diff --git a/afdko/Tools/SharedData/FDKScripts/fontPDF.py b/afdko/Tools/SharedData/FDKScripts/fontPDF.py index a9faaafc2..10f256d45 100644 --- a/afdko/Tools/SharedData/FDKScripts/fontPDF.py +++ b/afdko/Tools/SharedData/FDKScripts/fontPDF.py @@ -1,5 +1,5 @@ """ -fontPDF v1.24 Dec 4 2017. This module is not run stand-alone; it requires +fontPDF v1.25 May 15 2018. This module is not run stand-alone; it requires another module, such as ProofPDF.py, in order to collect the options, and call the MakePDF function. @@ -22,6 +22,17 @@ right, with the positive Y axis pointing down. """ +from __future__ import print_function, absolute_import + +import os +import re +import time + +from . import FDKUtils +from . import pdfgen +from . import pdfmetrics +from .pdfutils import LINEEND + __copyright__ = """Copyright 2014 Adobe Systems Incorporated (http://www.adobe.com/). All Rights Reserved. """ @@ -64,6 +75,7 @@ --pageRightMargin 36.0 # Integer. Point size --pageTitleFont Times-Bold # Text string. Font for title --pageTitleSize 14 # Integer. Point size used in title +--pageIncludeTitle 1 # 0 or 1. Whether to include a title on each page --fontsetGroupPtSize 14 # Integer. Point size for group header and PS name in fontsetplot. # Page layout attributes @@ -262,17 +274,6 @@ --pointLabelSize 12 #Change the size of the point label text. """ -import pdfdoc -import pdfgen -import pdfgeom -import pdfmetrics -import pdfutils -import time -import os -import re -from pdfutils import LINEEND -import FDKUtils - inch = INCH = 72 cm = CM = inch / 2.54 kShowMetaTag = "drawMeta_" @@ -320,6 +321,7 @@ def __init__(self): self.descenderSpace = None # The amout of space allowed for descenders. By default is Font BBox.ymin, but can be set by parameter. self.pageTitleFont = 'Times-Bold' # Font used for page titles. self.pageTitleSize = 14 # point size for page titles + self.pageIncludeTitle = 1 # include or not the page titles self.fontsetGroupPtSize = 14 # pt size for group header text font fontsetplot self.pointLabelFont = 'Helvetica' # Font used for all text in glyph tile. self.pointLabelSize = 16 # point size for all text in glyph tile. This is is relative to a glyph tile fo width kGlyphSquare; @@ -1252,7 +1254,8 @@ def drawMeta_RowFont(self, params): hintDir = rec[1] rowDir = rec[2] except (KeyError, TypeError): - rowFont = "RowFont: CID not in layout file" + hintDir = None + rowDir = None else: # it is name-keyed font that is not helpfully usiing cidXXXX names. Assume that it is in the # std development heirarchy. @@ -1271,7 +1274,6 @@ def drawMeta_WidthOnly(self, params): def getTitleHeight(params): - pageTitleFont = params.pageTitleFont pageTitleSize = params.pageTitleSize cur_y = params.pageSize[1] - (params.pageTopMargin + pageTitleSize) cur_y -= pageTitleSize*1.2 @@ -1302,14 +1304,17 @@ def doFontSetTitle(rt_canvas, params, numPages): def doTitle(rt_canvas, pdfFont, params, numGlyphs, numPages = None): pageTitleFont = params.pageTitleFont pageTitleSize = params.pageTitleSize + pageIncludeTitle = params.pageIncludeTitle # Set 0,0 to be at top right of page. - rt_canvas.setFont(pageTitleFont, pageTitleSize) + if pageIncludeTitle: + rt_canvas.setFont(pageTitleFont, pageTitleSize) title = "%s OT version %s " % (pdfFont.getPSName(), pdfFont.getOTVersion() ) rightMarginPos = params.pageSize[0]-params.pageRightMargin cur_y = params.pageSize[1] - (params.pageTopMargin + pageTitleSize) - rt_canvas.drawString(params.pageLeftMargin, cur_y, title) - rt_canvas.drawRightString(rightMarginPos, cur_y, time.asctime()) + if pageIncludeTitle: + rt_canvas.drawString(params.pageLeftMargin, cur_y, title) + rt_canvas.drawRightString(rightMarginPos, cur_y, time.asctime()) cur_y -= pageTitleSize*1.2 path = repr(params.rt_filePath) # Can be non-ASCII if numPages == None: @@ -1327,13 +1332,16 @@ def doTitle(rt_canvas, pdfFont, params, numGlyphs, numPages = None): if adjustedWidth: path = "..." + path[3:] - rt_canvas.drawString(params.pageLeftMargin, cur_y, path) - rt_canvas.drawRightString(rightMarginPos, cur_y, pageString) + if pageIncludeTitle: + rt_canvas.drawString(params.pageLeftMargin, cur_y, path) + rt_canvas.drawRightString(rightMarginPos, cur_y, pageString) cur_y -= pageTitleSize/2 - rt_canvas.setLineWidth(3) - rt_canvas.line(params.pageLeftMargin, cur_y, rightMarginPos, cur_y) + if pageIncludeTitle: + rt_canvas.setLineWidth(3) + rt_canvas.line(params.pageLeftMargin, cur_y, rightMarginPos, cur_y) #reset carefully afterwards - rt_canvas.setLineWidth(1) + if pageIncludeTitle: + rt_canvas.setLineWidth(1) return cur_y - pageTitleSize # Add some space below the title line. def getMetaDataHeight(params, fontYMin) : @@ -1471,24 +1479,20 @@ def getLayoutFromGPP(params, extraY, yTop): scale = scale1 numAcross = numAcross1 numDown = numDown1 - numGlyphs = numGlyphs1 else: scale = scale2 numAcross = numAcross2 numDown = numDown2 - numGlyphs = numGlyphs2 break elif (numGlyphs2 >= glyphsPerPage): scale = scale2 numAcross = numAcross2 numDown = numDown2 - numGlyphs = numGlyphs2 break elif (numGlyphs1 >= glyphsPerPage): scale = scale1 numAcross = numAcross1 numDown = numDown1 - numGlyphs = numGlyphs1 break if tryCount > 0: @@ -1615,7 +1619,7 @@ def StartProgress(self, startText = None): self.startTime = time.time() self.tickCount = 0 if startText: - print startText + print(startText) def DoProgress(self, tickCount): if tickCount and ((tickCount % self.kProgressBarTickStep) == 0): @@ -1625,10 +1629,11 @@ def DoProgress(self, tickCount): timeleft = int(perGlyph * (self.maxCount - tickCount)) minutesLeft = int(timeleft /60) secondsLeft = timeleft % 60 - print self.kText % (tickCount, self.maxCount, minutesLeft, secondsLeft) + print(self.kText % (tickCount, self.maxCount, minutesLeft, + secondsLeft)) def EndProgress(self): - print "Saving file..." + print("Saving file...") def makePDF(pdfFont, params, doProgressBar=True): @@ -1835,7 +1840,7 @@ def getFontDescriptorText(self): formatName = "/FontFile2" fontType = "/TrueType" else: - print "Font type not supported." + print("Font type not supported.") raise TypeError text = [] @@ -2173,7 +2178,8 @@ def makeProofPDF(pdfFont, params, doProgressBar=True): # Collect log file text, if any. if params.errorLogFilePath: if not os.path.isfile(params.errorLogFilePath): - print "Warning: log file %s does not exist or is not a file." % (repr(params.errorLogFilePath)) + print("Warning: log file %s does not exist or is not a file." % + repr(params.errorLogFilePath)) else: lf = file(params.errorLogFilePath, "rU") errorLines = lf.readlines() diff --git a/afdko/Tools/SharedData/FDKScripts/pdfdoc.py b/afdko/Tools/SharedData/FDKScripts/pdfdoc.py index 6dd316a88..b42daa699 100755 --- a/afdko/Tools/SharedData/FDKScripts/pdfdoc.py +++ b/afdko/Tools/SharedData/FDKScripts/pdfdoc.py @@ -1,11 +1,11 @@ #pdfdoc.py -""" -PDFgen is a library to generate PDF files containing text and graphics. It is the -foundation for a complete reporting solution in Python. +""" +PDFgen is a library to generate PDF files containing text and graphics. It is the +foundation for a complete reporting solution in Python. The module pdfdoc.py handles the 'outer structure' of PDF documents, ensuring that -all objects are properly cross-referenced and indexed to the nearest byte. The -'inner structure' - the page descriptions - are presumed to be generated before +all objects are properly cross-referenced and indexed to the nearest byte. The +'inner structure' - the page descriptions - are presumed to be generated before each page is saved. pdfgen.py calls this and provides a 'canvas' object to handle page marking operators. piddlePDF calls pdfgen and offers a high-level interface. @@ -14,15 +14,14 @@ Modified 7/25/2006 read rooberts. Added supported for embedding fonts. """ +from __future__ import print_function, absolute_import -import os -import sys import string +import sys import time -import tempfile -import cStringIO -from types import * -from math import sin, cos, pi, ceil + +from . import pdfutils +from .pdfutils import LINEEND # this constant needed in both Log = sys.stderr # reassign this if you don't want err output to console @@ -31,13 +30,6 @@ except: Log.write("zlib not available, page compression not available\n") - -from pdfgeom import bezierArc - -import pdfutils -from pdfutils import LINEEND # this constant needed in both -import pdfmetrics - ############################################################## # # Constants and declarations @@ -46,19 +38,23 @@ StandardEnglishFonts = [ - 'Courier', 'Courier-Bold', 'Courier-Oblique', 'Courier-BoldOblique', - 'Helvetica', 'Helvetica-Bold', 'Helvetica-Oblique', + 'Courier', 'Courier-Bold', 'Courier-Oblique', 'Courier-BoldOblique', + 'Helvetica', 'Helvetica-Bold', 'Helvetica-Oblique', 'Helvetica-BoldOblique', 'Times-Roman', 'Times-Bold', 'Times-Italic', 'Times-BoldItalic', 'Symbol','ZapfDingbats'] kDefaultEncoding = '/MacRoman' kDefaultFontType = "/Type1" -PDFError = 'PDFError' AFMDIR = '.' A4 = (595.27,841.89) #default page size + +class PDFError(KeyError): + pass + + class PDFDocument: """Responsible for linking and writing out the whole document. Builds up a list of objects using add(key, object). Each of these @@ -70,24 +66,24 @@ class PDFDocument: def __init__(self): self.objects = [] self.objectPositions = {} - + self.pages = [] self.pagepositions = [] - + # position 1 cat = PDFCatalog() cat.RefPages = 3 cat.RefOutlines = 2 self.add('Catalog', cat) - + # position 2 - outlines outl = PDFOutline() self.add('Outline', outl) - + # position 3 - pages collection self.PageCol = PDFPageCollection() self.add('PagesTreeRoot',self.PageCol) - + # mapping of Postscript font names to internal ones; # add all the standard built-in fonts. self.fonts = StandardEnglishFonts # list of PS names. MakeType1Fonts() @@ -106,14 +102,14 @@ def __init__(self): objectNumber = len(self.objects) self.fontMapping[psName+repr(encoding)] = [fontIndex, pdfFont, objectNumber, psName, encoding] self.fontdict = MakeFontDictionary(self.fontMapping) # This needs to be called again whenever a font is added. - - + + # position 17 - Info self.info = PDFInfo() #hang onto it! self.add('Info', self.info) self.infopos = len(self.objects) #1-based, this gives its position - - + + def add(self, key, obj): self.objectPositions[key] = len(self.objects) # its position self.objects.append(obj) @@ -125,27 +121,26 @@ def getPosition(self, key): cross-linking; an object can call self.doc.getPosition("Page001") to find out where the object keyed under "Page001" is stored.""" return self.objectPositions[key] - + def setTitle(self, title): "embeds in PDF file" self.info.title = title - + def setAuthor(self, author): "embedded in PDF file" self.info.author = author - + def setSubject(self, subject): "embeds in PDF file" self.info.subject = subject - - + def printXref(self): self.startxref = sys.stdout.tell() Log.write('xref\n') - Log.write("%s %s" % (0,len(self.objects) + 1) ) + Log.write("%s %s" % (0, len(self.objects) + 1)) Log.write('0000000000 65535 f') for pos in self.xref: - Log.write( '%0.10d 00000 n\n' % pos) + Log.write('%0.10d 00000 n\n' % pos) def writeXref(self, f): self.startxref = f.tell() @@ -155,12 +150,12 @@ def writeXref(self, f): for pos in self.xref: f.write('%0.10d 00000 n' % pos + LINEEND) - def printTrailer(self): - print 'trailer' - print '<< /Size %d /Root %d 0 R /Info %d 0 R>>' % (len(self.objects) + 1, 1, self.infopos) - print 'startxref' - print self.startxref + print('trailer') + print('<< /Size %d /Root %d 0 R /Info %d 0 R>>' % ( + len(self.objects) + 1, 1, self.infopos)) + print('startxref') + print(self.startxref) def writeTrailer(self, f): f.write('trailer' + LINEEND) @@ -192,48 +187,37 @@ def SaveToFileObject(self, fileobj): self.writeXref(f) self.writeTrailer(f) f.write('%%EOF') # no lineend needed on this one! - - # with the Mac, we need to tag the file in a special - #way so the system knows it is a PDF file. - #This supplied by Joe Strout - if os.name == 'mac': - import macfs - try: - macfs.FSSpec(filename).SetCreatorType('CARO','PDF ') - except: - pass - def printPDF(self): "prints it to standard output. Logs positions for doing trailer" - print "%PDF-1.0" - print "%\xed\xec\xb6\xbe" + print("%PDF-1.0") + print("%\xed\xec\xb6\xbe") i = 1 self.xref = [] for obj in self.objects: pos = sys.stdout.tell() self.xref.append(pos) - print i, '0 obj' + print(i, '0 obj') obj.printPDF() - print 'endobj' + print('endobj') i = i + 1 self.printXref() self.printTrailer() - print "%%EOF", + print("%%EOF",) def addPage(self, page): """adds page and stream at end. Maintains pages list""" #page.buildstream() pos = len(self.objects) # work out where added - + page.ParentPos = 3 #pages collection page.info = { 'parentpos':3, 'fontdict':self.fontdict, 'contentspos':pos + 2, } - - self.PageCol.PageList.append(pos+1) + + self.PageCol.PageList.append(pos+1) self.add('Page%06d'% len(self.PageCol.PageList), page) #self.objects.append(page) self.add('PageStream%06d'% len(self.PageCol.PageList), page.stream) @@ -264,36 +248,36 @@ def addFont(self, psname, encoding=kDefaultEncoding, clientCtx=None, getFontDesc getFontDescriptorItems is a client call back function that must return fdText, type, fontStream, fontStreamType - + fdText must be sttring containing a well-formed /FontDescriptor dict. In this string, the the reference to the embedded font stream must be as an indirect reference, with the object number being a string format , ie. /FontFile3 %s 0 R" This is is necessary ot allow the font stream object number ot be filled in later, by the line: fdText = fdText % objectNumber in MakeType1Font. - + type must tbe the PDF name for the font type, such as "/Type1" fontStream must be the actual font data to be embedded, not the entire PDF stream object. At the moment, it supports only CFF font data. - + fontStreamType is the value for the stream font SubType, e.g. /Type1C getEncodingInfo is a client call back function that must return firstChar, lastChar, widths - + firstChar is the index in the encoding array of the first glyph name which is not notdef lastChar is the index in the encoding array of the last glyph name whcih is not notdef widths is the width array of the encoded glyphs. It must contain a list of widths for the glyphs in the encoding array from firstChar to lastChar. - + The call to addFont will add the font descriptor object and the embedded stream object the first time it is called. If it is called again with the same PS name and encoding string, it will simply reference the font descriptor object and the embedded stream object that was already added, but will create a new font dict with a new widths and encoding dict. - + """ if self.hasFont(psname, encoding): return @@ -313,15 +297,15 @@ def getInternalFontName(self, psfontname, encoding=kDefaultEncoding): pdfFont = entry[1] return "/%s" % (pdfFont.keyname) except: - raise PDFError, "Font %s not available in document" % psfontname + raise PDFError("Font %s not available in document" % psfontname) def getAvailableFonts(self): - # There may be more + # There may be more entries = self.fontMapping.values() fontEntries = map(lambda entry: [entry[3], entry[4]], entries) # [psName, encoding] fontEntries.sort() return fontEntries - + def MakeType1Font(self, fontIndex, psName, encoding=kDefaultEncoding, clientCtx=None, getFontDescriptorItems=None, getEncodingInfo=None): "returns a font object" objectNumber = None @@ -333,7 +317,7 @@ def MakeType1Font(self, fontIndex, psName, encoding=kDefaultEncoding, clientCtx= if getEncodingInfo: firstChar, lastChar, widths = getEncodingInfo(clientCtx) try: - # check if we have already seen this + # check if we have already seen this objectNumber,type = self.fontDescriptors[psName] except KeyError: # Need to create a font descriptor object. @@ -349,10 +333,10 @@ def MakeType1Font(self, fontIndex, psName, encoding=kDefaultEncoding, clientCtx= self.fontDescriptors[psName] = (objectNumber, type) if type == "/Type0": font = PDFType0Font('F'+str(fontIndex), psName, encoding, objectNumber, type, firstChar, lastChar, widths) - else: + else: font = PDFType1Font('F'+str(fontIndex), psName, encoding, objectNumber, type, firstChar, lastChar, widths) return font - + ############################################################## # # Utilities @@ -363,7 +347,7 @@ class OutputGrabber: """At times we need to put something in the place of standard output. This grabs stdout, keeps the data, and releases stdout when done. - + NOT working well enough!""" def __init__(self): self.oldoutput = sys.stdout @@ -373,27 +357,27 @@ def __init__(self): def write(self, x): if not self.closed: self.data.append(x) - + def getData(self): return string.join(self.data) def close(self): sys.stdout = self.oldoutput self.closed = 1 - + def __del__(self): if not self.closed: self.close() - - + + def testOutputGrabber(): gr = OutputGrabber() for i in range(10): - print 'line',i + print('line', i) data = gr.getData() gr.close() - print 'Data...',data - + print('Data...', data) + ############################################################## # @@ -416,7 +400,7 @@ def save(self, file): file.write('% base PDF object' + LINEEND) def printPDF(self): self.save(sys.stdout) - + class PDFLiteral(PDFObject): " a ready-made one you wish to quote" @@ -463,13 +447,13 @@ def save(self, file): "/Subject (%s)", ">>" ], LINEEND - ) % ( - pdfutils._escape(self.title), - pdfutils._escape(self.author), - self.datestr, + ) % ( + pdfutils._escape(self.title), + pdfutils._escape(self.author), + self.datestr, pdfutils._escape(self.subject) ) + LINEEND) - + class PDFOutline(PDFObject): @@ -535,7 +519,7 @@ def setCompression(self, onoff=0): "Turns page compression on or off" assert onoff in [0,1], "Page compression options are 1=on, 2=off" self.stream.compression = onoff - + def save(self, file): self.info['pagewidth'] = self.pagewidth self.info['pageheight'] = self.pageheight @@ -550,9 +534,9 @@ def save(self, file): def clear(self): self.drawables = [] - + def setStream(self, data): - if type(data) is ListType: + if isinstance(data, list): data = string.join(data, LINEEND) self.stream.setStream(data) @@ -582,7 +566,7 @@ def save(self, file): data_to_write = self.data # the PDF length key should contain the length including # any extra LF pairs added by Print on DOS. - + #lines = len(string.split(self.data,'\n')) #length = len(self.data) + lines # one extra LF each length = len(data_to_write) + len(LINEEND) #AR 19980202 @@ -622,7 +606,7 @@ class PDFEmbddedFont(PDFStream): def __init__(self, fontStreamType): PDFStream.__init__(self) self.fontType = fontStreamType - + def save(self, file): #avoid crashes if they wrote nothing in the page if self.data == None: @@ -637,7 +621,7 @@ def save(self, file): data_to_write = self.data # the PDF length key should contain the length including # any extra LF pairs added by Print on DOS. - + #lines = len(string.split(self.data,'\n')) #length = len(self.data) + lines # one extra LF each length = len(data_to_write) + len(LINEEND) #AR 19980202 @@ -645,7 +629,7 @@ def save(self, file): fontStreamEntry = "" else: fontStreamEntry = "/Subtype %s" % (self.fontType) - + if self.compression: file.write('<< %s /Length %d /Filter [/ASCII85Decode /FlateDecode] >>' % (fontStreamEntry, length) + LINEEND) else: @@ -674,7 +658,7 @@ def __init__(self, key, psName, encoding=kDefaultEncoding, fontDescriptObjectNum "/Encoding %s" % (encoding), '>>'] self.template = LINEEND.join(textList) - + def save(self, file): file.write(self.template % (self.keyname, self.fontname) + LINEEND) @@ -685,7 +669,7 @@ class PDFType0Font(PDFType1Font): class PDFontDescriptor(PDFObject): def __init__(self, text): self.text = text - + def save(self, file): file.write(self.text + LINEEND) @@ -711,6 +695,6 @@ def MakeFontDictionary(fontMapping): dict = dict + '\t\t/%s %d 0 R ' % (pdfFont.keyname, objectPos) + LINEEND dict = dict + "\t\t>>" + LINEEND return dict - + if __name__ == '__main__': - print 'For test scripts, run test1.py to test6.py' + print('For test scripts, run test1.py to test6.py') diff --git a/afdko/Tools/SharedData/FDKScripts/pdfgen.py b/afdko/Tools/SharedData/FDKScripts/pdfgen.py index 6d3c59c8c..7b926a355 100755 --- a/afdko/Tools/SharedData/FDKScripts/pdfgen.py +++ b/afdko/Tools/SharedData/FDKScripts/pdfgen.py @@ -1,6 +1,6 @@ #pdfgen.py -""" -PDFgen is a library to generate PDF files containing text and graphics. It is the +""" +PDFgen is a library to generate PDF files containing text and graphics. It is the foundation for a complete reporting solution in Python. It is also the foundation for piddlePDF, the PDF back end for PIDDLE. @@ -16,7 +16,7 @@ copyright notice and this permission notice appear in supporting documentation, and that the name of Robinson Analytics not be used in advertising or publicity pertaining to distribution of the software -without specific, written prior permission. +without specific, written prior permission. ROBINSON ANALYTICS LTD. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, @@ -24,7 +24,7 @@ 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. +PERFORMANCE OF THIS SOFTWARE. Progress Reports: @@ -37,7 +37,7 @@ redistill. One limitation still - clipping to text paths is fine in Acrobat but not in Postscript (any level) - + 0.81,1999-10-13, AR: Adding RoundRect; changed all format strings to use %0.2f instead of %s, so we don't get exponentials in the output. @@ -49,22 +49,20 @@ """ # 1 ## 0.81 1999-10-13: -## ## ## +## +from __future__ import print_function, absolute_import + import os -import sys import string -import time -import tempfile import cStringIO -from types import * -from math import sin, cos, tan, pi, ceil +from math import sin, cos, tan, pi -import pdfutils -import pdfdoc -import pdfmetrics -import pdfgeom +from . import pdfdoc +from . import pdfgeom +from . import pdfmetrics +from . import pdfutils # Robert Kern # Constants for closing paths. @@ -115,36 +113,37 @@ def __init__(self,filename,pagesize=(595.27,841.89), bottomup = 1): self._currentPageHasImages = 1 self._pageTransitionString = '' - self._pageCompression = 1 #on by default - turn off when debugging! + self._pageCompression = 1 # on by default - turn off when debugging! self._pageNumber = 1 # keep a count - self._code = [] #where the current page's marking operators accumulate - - #PostScript has the origin at bottom left. It is easy to achieve a top- - #down coord system by translating to the top of the page and setting y - #scale to -1, but then text is inverted. So self.bottomup is used - #to also set the text matrix accordingly. You can now choose your - #drawing coordinates. + # where the current page's marking operators accumulate + self._code = [] + + # PostScript has the origin at bottom left. It is easy to achieve a + # top-down coord system by translating to the top of the page and + # setting y scale to -1, but then text is inverted. So self.bottomup + # is used to also set the text matrix accordingly. You can now choose + # your drawing coordinates. self.bottomup = bottomup if self.bottomup: - #set initial font - #self._preamble = 'BT /F9 12 Tf 14.4 TL ET' + # set initial font + # self._preamble = 'BT /F9 12 Tf 14.4 TL ET' self._preamble = '1 0 0 1 0 0 cm BT /F9 12 Tf 14.4 TL ET' else: - #switch coordinates, flip text and set font - #self._preamble = '1 0 0 -1 0 %0.4f cm BT /F9 12 Tf 14.4 TL ET' % self._pagesize[1] - self._preamble = '1 0 0 -1 0 %0.4f cm BT /F9 12 Tf 14.4 TL ET' % self._pagesize[1] + # switch coordinates, flip text and set font + self._preamble = ('1 0 0 -1 0 %0.4f cm BT /F9 12 Tf 14.4 TL ET' % + self._pagesize[1]) - #initial graphics state + # initial graphics state self._x = 0 self._y = 0 self._fontname = 'Times-Roman' self._fontsize = 12 - self._textMode = 0 #track if between BT/ET + self._textMode = 0 # track if between BT/ET self._leading = 14.4 self._currentMatrix = (1., 0., 0., 1., 0., 0.) - self._fillMode = 0 #even-odd - - #text state + self._fillMode = 0 # even-odd + + # text state self._charSpace = 0 self._wordSpace = 0 self._horizScale = 100 @@ -153,10 +152,10 @@ def __init__(self,filename,pagesize=(595.27,841.89), bottomup = 1): self._textLineMatrix = (1., 0., 0., 1., 0., 0.) self._textMatrix = (1., 0., 0., 1., 0., 0.) - # line drawing + # line drawing self._lineCap = 0 self._lineJoin = 0 - self._lineDash = None #not done + self._lineDash = None # not done self._lineWidth = 0 self._mitreLimit = 0 @@ -166,25 +165,25 @@ def __init__(self,filename,pagesize=(595.27,841.89), bottomup = 1): def _escape(self, s): """PDF escapes are like Python ones, but brackets need slashes before them too. Use Python's repr function and chop off the quotes first""" - #s = repr(s)[1:-1] + # s = repr(s)[1:-1] s = string.replace(s, '(','\(') s = string.replace(s, ')','\)') return s - #info functions - non-standard + # info functions - non-standard def setAuthor(self, author): self._doc.setAuthor(author) - + def setTitle(self, title): self._doc.setTitle(title) - + def setSubject(self, subject): self._doc.setSubject(subject) - + def pageHasData(self): "Info function - app can call it after showPage to see if it needs a save" return len(self._code) == 0 - + def showPage(self): """This is where the fun happens""" page = pdfdoc.PDFPage() @@ -193,30 +192,30 @@ def showPage(self): page.hasImages = self._currentPageHasImages page.pageTransitionString = self._pageTransitionString page.setCompression(self._pageCompression) - #print stream + # print stream page.setStream([self._preamble] + self._code) self._doc.addPage(page) - - #now get ready for the next one + + # now get ready for the next one self._pageNumber = self._pageNumber + 1 self._code = [] # ready for more... self._currentPageHasImages = 0 def getPageNumber(self): return self._pageNumber - + def save(self, filename=None, fileobj=None): """Saves the pdf document to fileobj or to file with name filename. If holding data, do a showPage() to save them having to.""" - - if len(self._code): - self.showPage() # what's the effect of multiple 'showPage's + + if len(self._code): + self.showPage() # what's the effect of multiple 'showPage's if fileobj: self._doc.SaveToFileObject(fileobj) elif filename: self._doc.SaveToFile(filename) - else: + else: self._doc.SaveToFile(self._filename) @@ -224,7 +223,7 @@ def setPageSize(self, size): """accepts a 2-tuple in points for paper size for this and subsequent pages""" self._pagesize = size - + def addLiteral(self, s, escaped=1): if escaped==0: @@ -277,7 +276,7 @@ def skew(self, alpha, beta): def saveState(self): """These need expanding to save/restore Python's state tracking too""" self._code.append('q') - + def restoreState(self): """These need expanding to save/restore Python's state tracking too""" self._code.append('Q') @@ -303,7 +302,7 @@ def restoreState(self): #--------first the line drawing methods----------------------- def line(self, x1,y1, x2,y2): - "As it says" + "As it says" self._code.append('n %0.4f %0.4f m %0.4f %0.4f l S' % (x1, y1, x2, y2)) def lines(self, linelist): @@ -337,7 +336,7 @@ def arc(self, x1,y1, x2,y2, startAng=0, extent=90): """Contributed to piddlePDF by Robert Kern, 28/7/99. Trimmed down by AR to remove color stuff for pdfgen.canvas and revert to positive coordinates. - + Draw a partial ellipse inscribed within the rectangle x1,y1,x2,y2, starting at startAng degrees and covering extent degrees. Angles start with 0 to the right (+x) and increase counter-clockwise. @@ -347,7 +346,7 @@ def arc(self, x1,y1, x2,y2, startAng=0, extent=90): Jim Fitzsimmon's TeX tutorial .""" pointList = pdfgeom.bezierArc(x1,y1, x2,y2, startAng, extent) - #move to first point + # move to first point self._code.append('n %0.4f %0.4f m' % pointList[0][:2]) for curve in pointList: self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' % curve[2:]) @@ -359,8 +358,8 @@ def rect(self, x, y, width, height, stroke=1, fill=0): "draws a rectangle" self._code.append('n %0.4f %0.4f %0.4f %0.4f re ' % (x, y, width, height) + PATH_OPS[stroke, fill, self._fillMode]) - - + + def ellipse(self, x1, y1, x2, y2, stroke=1, fill=0): """Uses bezierArc, which conveniently handles 360 degrees - nice touch Robert""" @@ -372,7 +371,7 @@ def ellipse(self, x1, y1, x2, y2, stroke=1, fill=0): #finish self._code.append(PATH_OPS[stroke, fill, self._fillMode]) - + def wedge(self, x1,y1, x2,y2, startAng, extent, stroke=1, fill=0): """Like arc, but connects to the centre of the ellipse. Most useful for pie charts and PacMan!""" @@ -380,7 +379,7 @@ def wedge(self, x1,y1, x2,y2, startAng, extent, stroke=1, fill=0): x_cen = (x1+x2)/2. y_cen = (y1+y2)/2. pointList = pdfgeom.bezierArc(x1,y1, x2,y2, startAng, extent) - + self._code.append('n %0.4f %0.4f m' % (x_cen, y_cen)) # Move the pen to the center of the rectangle self._code.append('%0.4f %0.4f l' % pointList[0][:2]) @@ -407,7 +406,7 @@ def roundRect(self, x, y, width, height, radius, stroke=1, fill=0): #to a circle. There are six relevant points on the x axis and y axis. #sketch them and it should all make sense! t = 0.4472 * radius - + x0 = x x1 = x0 + t x2 = x0 + radius @@ -430,18 +429,18 @@ def roundRect(self, x, y, width, height, radius, stroke=1, fill=0): self._code.append('%0.4f %0.4f l' % (x5, y3)) # right edge self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' % (x5, y4, x4, y5, x3, y5)) # top right - + self._code.append('%0.4f %0.4f l' % (x2, y5)) # top row self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' % (x1, y5, x0, y4, x0, y3)) # top left - + self._code.append('%0.4f %0.4f l' % (x0, y2)) # left edge self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' % (x0, y1, x1, y0, x2, y0)) # bottom left self._code.append('h') #close off, although it should be where it started anyway - - + + self._code.append(PATH_OPS[stroke, fill, self._fillMode]) ################################################## # @@ -474,7 +473,7 @@ def drawCentredString(self, x, y, text): t = self.beginText(x - 0.5*width, y) t.textLine(text) self.drawText(t) - + def getAvailableFonts(self): """Returns the list of PostScript font names available. Standard set now, but may grow in future with font embedding.""" @@ -484,8 +483,8 @@ def getAvailableFonts(self): def addFont(self, psfontname, encoding=None, clientCtx=None, getFontDescriptor=None, getEncodingInfo=None): self._doc.addFont(psfontname, encoding, clientCtx, getFontDescriptor, getEncodingInfo) # this function will not add the font or font decriptor if theu already exists. - - + + def setFont(self, psfontname, size, leading = None, encoding = None): """Sets the font. If leading not specified, defaults to 1.2 x font size. Raises a readable exception if an illegal font @@ -502,7 +501,7 @@ def setFont(self, psfontname, size, leading = None, encoding = None): def stringWidth(self, text, fontname, fontsize): "gets width of a string in the given font and size" return pdfmetrics.stringwidth(text, fontname) * 0.001 * fontsize - + # basic graphics modes def setLineWidth(self, width): self._lineWidth = width @@ -513,39 +512,39 @@ def setLineCap(self, mode): assert mode in (0,1,2), "Line caps allowed: 0=butt,1=round,2=square" self._lineCap = mode self._code.append('%d J' % mode) - + def setLineJoin(self, mode): """0=mitre, 1=round, 2=bevel""" assert mode in (0,1,2), "Line Joins allowed: 0=mitre, 1=round, 2=bevel" self._lineJoin = mode self._code.append('%d j' % mode) - + def setMiterLimit(self, limit): self._miterLimit = limit self._code.append('%0.4f M' % limit) def setDash(self, array=[], phase=0): """Two notations. pass two numbers, or an array and phase""" - if type(array) == IntType or type(array) == FloatType: + if isinstance(array, int) or isinstance(array, float): self._code.append('[%s %s] 0 d' % (array, phase)) - elif type(array) == ListType or type(Array) == TupleType: + elif isinstance(array, list) or isinstance(array, tuple): assert phase <= len(array), "setDash phase must be l.t.e. length of array" textarray = string.join(map(str, array)) self._code.append('[%s] %s d' % (textarray, phase)) - + def setFillColorRGB(self, r, g, b): self._fillColorRGB = (r, g, b) self._code.append('%0.4f %0.4f %0.4f rg' % (r,g,b)) - + def setStrokeColorRGB(self, r, g, b): self._strokeColorRGB = (r, g, b) self._code.append('%0.4f %0.4f %0.4f RG' % (r,g,b)) - - # path stuff - the separate path object builds it + + # path stuff - the separate path object builds it def beginPath(self): """Returns a fresh path object""" return PDFPathObject() - + def drawPath(self, aPath, stroke=1, fill=0): "Draw in the mode indicated" op = PATH_OPS[stroke, fill, self._fillMode] @@ -563,7 +562,7 @@ def beginText(self, x=0, y=0): def drawText(self, aTextObject): """Draws a text object""" self._code.append(aTextObject.getCode()) - + ###################################################### # # Image routines @@ -575,20 +574,15 @@ def drawInlineImage(self, image, x,y, width=None,height=None): Also allow file names as well as images. This allows a caching mechanism""" # print "drawInlineImage: x=%s, y=%s, width = %s, height=%s " % (x,y, width, height) - try: - import Image - except ImportError: - print 'Python Imaging Library not available' - return try: import zlib except ImportError: - print 'zlib not available' + print('zlib not available') return - + self._currentPageHasImages = 1 - if type(image) == StringType: + if isinstance(image, str): if os.path.splitext(image)[1] in ['.jpg', '.JPG']: #directly process JPEG files #open file, needs some error handling!! @@ -609,7 +603,7 @@ def drawInlineImage(self, image, x,y, width=None,height=None): imagedata.append('/BitsPerComponent 8') imagedata.append('/ColorSpace /%s' % colorSpace) imagedata.append('/Filter [ /ASCII85Decode /DCTDecode]') - imagedata.append('ID') + imagedata.append('ID') #write in blocks of (??) 60 characters per line to a list compressed = imageFile.read() encoded = pdfutils._AsciiBase85Encode(compressed) @@ -627,7 +621,7 @@ def drawInlineImage(self, image, x,y, width=None,height=None): imagedata = open(cachedname,'rb').readlines() #trim off newlines... imagedata = map(string.strip, imagedata) - + #parse line two for width, height words = string.split(imagedata[1]) imgwidth = string.atoi(words[1]) @@ -642,7 +636,7 @@ def drawInlineImage(self, image, x,y, width=None,height=None): # this describes what is in the image itself imagedata.append('/W %0.4f /H %0.4f /BPC 8 /CS /RGB /F [/A85 /Fl]' % (imgwidth, imgheight)) - imagedata.append('ID') + imagedata.append('ID') #use a flate filter and Ascii Base 85 to compress raw = myimage.tostring() @@ -663,7 +657,7 @@ def drawInlineImage(self, image, x,y, width=None,height=None): width = imgwidth if not height: height = imgheight - + # this says where and how big to draw it #self._code.append('ET') #self._code.append('q %0.4f 0 0 %0.4f %0.4f %0.4f cm' % (width, height, x, y+height)) @@ -716,7 +710,7 @@ def readJPEGInfo(self, image): if x[0] != 8: raise 'PDFError', ' JPEG must have 8 bits per component' y = struct.unpack('BB', image.read(2)) - height = (y[0] << 8) + y[1] + height = (y[0] << 8) + y[1] y = struct.unpack('BB', image.read(2)) width = (y[0] << 8) + y[1] y = struct.unpack('B', image.read(1)) @@ -729,7 +723,7 @@ def readJPEGInfo(self, image): #skip segments with parameters #read length and skip the data x = struct.unpack('BB', image.read(2)) - image.seek( (x[0] << 8) + x[1] - 2, 1) + image.seek( (x[0] << 8) + x[1] - 2, 1) def setPageCompression(self, onoff=1): """Possible values 1 or 0 (1 for 'on' is the default). @@ -738,9 +732,9 @@ def setPageCompression(self, onoff=1): This applies to all subsequent pages, or until setPageCompression() is next called.""" self._pageCompression = onoff - - def setPageTransition(self, effectname=None, duration=1, + + def setPageTransition(self, effectname=None, duration=1, direction=0,dimension='H',motion='I'): """PDF allows page transition effects for use when giving presentations. There are six possible effects. You can @@ -750,7 +744,7 @@ def setPageTransition(self, effectname=None, duration=1, direction_arg = [0,90,180,270] dimension_arg = ['H', 'V'] motion_arg = ['I','O'] (start at inside or outside) - + This table says which ones take which arguments: PageTransitionEffects = { @@ -766,18 +760,18 @@ def setPageTransition(self, effectname=None, duration=1, if not effectname: self._pageTransitionString = '' return - + #first check each optional argument has an allowed value if direction in [0,90,180,270]: direction_arg = '/Di /%d' % direction else: raise 'PDFError', ' directions allowed are 0,90,180,270' - + if dimension in ['H', 'V']: dimension_arg = '/Dm /%s' % dimension else: raise'PDFError','dimension values allowed are H and V' - + if motion in ['I','O']: motion_arg = '/M /%s' % motion else: @@ -800,16 +794,16 @@ def setPageTransition(self, effectname=None, duration=1, raise 'PDFError', 'Unknown Effect Name "%s"' % effectname self._pageTransitionString = '' return - - self._pageTransitionString = (('/Trans <>') - - - + + + class PDFPathObject: """Represents a graphic path. There are certain 'modes' to PDF drawing, and making a separate object to expose Path operations @@ -817,12 +811,12 @@ class PDFPathObject: the Canvas for a PDFPath with getNewPathObject(); moveto/lineto/ curveto wherever you want; add whole shapes; and then add it back into the canvas with one of the relevant operators. - + Path objects are probably not long, so we pack onto one line""" def __init__(self): self._code = [] self._code.append('n') #newpath - + def getCode(self): "pack onto one line; used internally" return string.join(self._code, ' ') @@ -832,7 +826,7 @@ def lineTo(self, x, y): self._code.append('%0.4f %0.4f l' % (x,y)) def curveTo(self, x1, y1, x2, y2, x3, y3): self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' % (x1, y1, x2, y2, x3, y3)) - + def arc(self, x1,y1, x2,y2, startAng=0, extent=90): """Contributed to piddlePDF by Robert Kern, 28/7/99. Draw a partial ellipse inscribed within the rectangle x1,y1,x2,y2, @@ -856,7 +850,7 @@ def arcTo(self, x1,y1, x2,y2, startAng=0, extent=90): self._code.append('%0.4f %0.4f l' % pointList[0][:2]) for curve in pointList: self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' % curve[2:]) - + def rect(self, x, y, width, height): """Adds a rectangle to the path""" @@ -868,7 +862,7 @@ def ellipse(self, x, y, width, height): self._code.append('%0.4f %0.4f m' % pointList[0][:2]) for curve in pointList: self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' % curve[2:]) - + def circle(self, x_cen, y_cen, r): """adds a circle to the path""" x1 = x_cen - r @@ -876,7 +870,7 @@ def circle(self, x_cen, y_cen, r): y1 = y_cen - r y2 = y_cen + r self.ellipse(x_cen - r, y_cen - r, x_cen + r, y_cen + r) - + def close(self): "draws a line back to where it started" self._code.append('h') @@ -900,15 +894,15 @@ def __init__(self, canvas, x=0,y=0): self._fontname = self._canvas._fontname self._fontsize = self._canvas._fontsize self._leading = self._canvas._leading - + self.setTextOrigin(x, y) - + def getCode(self): "pack onto one line; used internally" self._code.append('ET') return string.join(self._code, ' ') - def setTextOrigin(self, x, y): + def setTextOrigin(self, x, y): if self._canvas.bottomup: self._code.append('1 0 0 1 %0.4f %0.4f Tm' % (x, y)) #bottom up else: @@ -921,7 +915,7 @@ def setTextTransform(self, a, b, c, d, e, f): "Like setTextOrigin, but does rotation, scaling etc." # flip "y" coordinate for top down coordinate system -cwl # (1 0) (a b) ( a b) - # (0 -1) (c d) = (-c -d) + # (0 -1) (c d) = (-c -d) self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f Tm' % (a, b, -c, -d, e, f)) #top down #self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f Tm' % (a, b, c, d, e, f)) #bottom up @@ -994,7 +988,7 @@ def setTextRenderMode(self, mode): 5 = Stroke text and add to clipping path 6 = Fill then stroke and add to clipping path 7 = Add to clipping path""" - + assert mode in (0,1,2,3,4,5,6,7), "mode must be in (0,1,2,3,4,5,6,7)" self._textRenderMode = mode self._code.append('%d Tr' % mode) @@ -1012,7 +1006,7 @@ def setStrokeColorRGB(self, r, g, b): def setFillColorRGB(self, r, g, b): self._fillColorRGB = (r, g, b) self._code.append('%0.4f %0.4f %0.4f rg' % (r,g,b)) - + def textOut(self, text): "prints string at current point, text cursor moves across" @@ -1040,17 +1034,17 @@ def textLines(self, stuff, trim=1): since this may be indented, by default it trims whitespace off each line and from the beginning; set trim=0 to preserve whitespace.""" - if type(stuff) == StringType: + if isinstance(stuff, str): lines = string.split(string.strip(stuff), '\n') if trim==1: lines = map(string.strip,lines) - elif type(stuff) == ListType: + elif isinstance(stuff, list): lines = stuff - elif type(stuff) == TupleType: + elif isinstance(stuff, tuple): lines = stuff else: assert 1==0, "argument to textlines must be string,, list or tuple" - + for line in lines: escaped_text = self._canvas._escape(line) self._code.append('(%s) Tj T*' % escaped_text) @@ -1059,10 +1053,10 @@ def textLines(self, stuff, trim=1): else: self._y = self._y + self._leading self._x = self._x0 - - + + if __name__ == '__main__': - print 'For test scripts, run testpdfgen.py' + print('For test scripts, run testpdfgen.py')