From b1cc1b05c47385169fcca5dabcd0ed52a926b0a3 Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sun, 12 Mar 2023 12:15:50 +0100 Subject: [PATCH 01/22] Create a reportlab canvas without a file name It seems like we don't need the file name if we don't save the file, and we can also get the string from the reportlab canvas. This is closer to what we do now and doesn't require us to restructure a lot of the code. --- requirements.txt | 1 + src/pdf.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b35dbcf3..e696f2b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ setuptools wxPython lxml +reportlab pytest>=7.0 diff --git a/src/pdf.py b/src/pdf.py index 18ec71d0..e10807a6 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -1,5 +1,7 @@ from typing import Optional, List, Tuple, Dict +from reportlab.pdfgen.canvas import Canvas + import fontinfo import pml import util @@ -201,7 +203,7 @@ def __init__(self, doc: 'pml.Document'): # generate PDF document and return it as a string def generate(self) -> str: - #lsdjflksj = util.TimerDev("generate") + canvas = Canvas('') doc = self.doc # fast lookup of font information From 7611aa0250533c6307d723049444452708158a6d Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sun, 12 Mar 2023 13:40:47 +0100 Subject: [PATCH 02/22] Generate broken PDFs through reportlab by passing PDF instructions through We don't yet take any advantage of reportlab, and the PDFs are broken as reportlab creates its own instructions at the beginning and ignores some of the ones we pass to it --- src/characterreport.py | 4 ++- src/dialoguechart.py | 4 +-- src/gutil.py | 9 ++++-- src/locationreport.py | 4 ++- src/pdf.py | 68 +++++++++++++++++++++++------------------- src/scenereport.py | 4 ++- src/screenplay.py | 4 +-- src/scriptreport.py | 4 ++- 8 files changed, 61 insertions(+), 40 deletions(-) diff --git a/src/characterreport.py b/src/characterreport.py index fdb41ccf..257b488d 100644 --- a/src/characterreport.py +++ b/src/characterreport.py @@ -1,3 +1,5 @@ +from typing import AnyStr + import misc import pdf import pml @@ -86,7 +88,7 @@ def __init__(self, sp): def sum(self, name): return reduce(lambda tot, ci: tot + getattr(ci, name), self.cinfo, 0) - def generate(self) -> str: + def generate(self) -> AnyStr: tf = pml.TextFormatter(self.sp.cfg.paperWidth, self.sp.cfg.paperHeight, 20.0, 12) diff --git a/src/dialoguechart.py b/src/dialoguechart.py index 5ed5ece5..22df7351 100644 --- a/src/dialoguechart.py +++ b/src/dialoguechart.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, AnyStr import gutil import misc @@ -173,7 +173,7 @@ def __init__(self, sp, minLines): # spacing from one legend item to next self.legendSize = 4.0 - def generate(self, cbil: List[misc.CheckBoxItem]) -> str: + def generate(self, cbil: List[misc.CheckBoxItem]) -> AnyStr: doc = pml.Document(self.sp.cfg.paperHeight, self.sp.cfg.paperWidth) diff --git a/src/gutil.py b/src/gutil.py index c884c623..6b4b4c21 100644 --- a/src/gutil.py +++ b/src/gutil.py @@ -1,3 +1,5 @@ +from typing import AnyStr + import config from error import MiscError,TrelbyError import misc @@ -73,7 +75,7 @@ def btnDblClick(btn, func): # temporary file, first deleting all old temporary files, then opens PDF # viewer application. 'mainFrame' is used as a parent for message boxes in # case there are any errors. -def showTempPDF(pdfData: str, cfgGl: 'config.ConfigGlobal', mainFrame: wx.TopLevelWindow) -> None: +def showTempPDF(pdfData: AnyStr, cfgGl: 'config.ConfigGlobal', mainFrame: wx.TopLevelWindow) -> None: try: try: util.removeTempFiles(misc.tmpPrefix) @@ -82,7 +84,10 @@ def showTempPDF(pdfData: str, cfgGl: 'config.ConfigGlobal', mainFrame: wx.TopLev suffix = ".pdf") try: - os.write(fd, pdfData.encode("UTF-8")) + if isinstance(pdfData, str): + os.write(fd, pdfData.encode("UTF-8")) + else: + os.write(fd, pdfData) finally: os.close(fd) diff --git a/src/locationreport.py b/src/locationreport.py index fb5e3c7d..19f8a77f 100644 --- a/src/locationreport.py +++ b/src/locationreport.py @@ -1,3 +1,5 @@ +from typing import AnyStr + import misc import pdf import pml @@ -62,7 +64,7 @@ def sortFunc(o1, o2): for s in ["Speakers"]: self.inf.append(misc.CheckBoxItem(s)) - def generate(self) -> str: + def generate(self) -> AnyStr: tf = pml.TextFormatter(self.sp.cfg.paperWidth, self.sp.cfg.paperHeight, 15.0, 12) diff --git a/src/pdf.py b/src/pdf.py index e10807a6..2dfd83ba 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -1,4 +1,4 @@ -from typing import Optional, List, Tuple, Dict +from typing import Optional, List, Tuple, Dict, AnyStr from reportlab.pdfgen.canvas import Canvas @@ -14,7 +14,7 @@ } # users should only use this. -def generate(doc: 'pml.Document') -> str: +def generate(doc: 'pml.Document') -> AnyStr: tmp = PDFExporter(doc) return tmp.generate() @@ -188,11 +188,12 @@ def __init__(self, nr: int, data: str = ""): # when the object is written out (by the caller of write). self.xrefPos: int = -1 - # write object to output. - def write(self, output: 'util.String') -> None: - output += "%d 0 obj\n" % self.nr - output += self.data - output += "\nendobj\n" + # write object to canvas. + def write(self, canvas: Canvas) -> None: + code = "%d 0 obj\n" % self.nr + code += self.data + code += "\nendobj\n" + canvas.addLiteral(code) class PDFExporter: # see genWidths @@ -202,7 +203,7 @@ def __init__(self, doc: 'pml.Document'): self.doc: pml.Document = doc # generate PDF document and return it as a string - def generate(self) -> str: + def generate(self) -> AnyStr: canvas = Canvas('') doc = self.doc @@ -320,7 +321,11 @@ def generate(self) -> str: for i in range(len(doc.tocs)): self.genOutline(i) - return self.genPDF() + self.genPDF(canvas) + + data = canvas.getpdfdata() + + return data def createInfoObj(self) -> PDFObject: version = self.escapeStr(self.doc.version) @@ -416,44 +421,47 @@ def addObj(self, data: str = "") -> PDFObject: return obj - # write out object to 'output' - def writeObj(self, output: 'util.String', obj: PDFObject) -> None: - obj.xrefPos = len(output) - obj.write(output) + # write out object to 'canvas' + def writeObj(self, canvas: Canvas, obj: PDFObject) -> None: + obj.xrefPos = self.getPdfCodeLength(canvas._code) + obj.write(canvas) + + def getPdfCodeLength(self, canvasCodeList: List[str]): + length = 0 + for string in canvasCodeList: + length += len(string) + + return length # write a xref table entry to 'output', using position # 'pos, generation 'gen' and type 'typ'. - def writeXref(self, output: 'util.String', pos: int, gen: int = 0, typ: str = "n") -> None: - output += "%010d %05d %s \n" % (pos, gen, typ) + def writeXref(self, canvas: Canvas, pos: int, gen: int = 0, typ: str = "n") -> None: + canvas.addLiteral("%010d %05d %s \n" % (pos, gen, typ)) # generate PDF file and return it as a string - def genPDF(self) -> str: - data = util.String() - - data += "%PDF-1.5\n" + def genPDF(self, canvas: Canvas) -> None: + canvas.addLiteral("%PDF-1.5\n") for obj in self.objects: - self.writeObj(data, obj) + self.writeObj(canvas, obj) - xrefStartPos = len(data) + xrefStartPos = self.getPdfCodeLength(canvas._code) - data += "xref\n0 %d\n" % self.objectCnt - self.writeXref(data, 0, 65535, "f") + canvas.addLiteral("xref\n0 %d\n" % self.objectCnt) + self.writeXref(canvas, 0, 65535, "f") for obj in self.objects: - self.writeXref(data, obj.xrefPos) + self.writeXref(canvas, obj.xrefPos) - data += "\n" + canvas.addLiteral("\n") - data += ("trailer\n" + canvas.addLiteral(("trailer\n" "<< /Size %d\n" "/Root %d 0 R\n" "/Info %d 0 R\n>>\n" % ( - self.objectCnt, self.catalogObj.nr, self.infoObj.nr)) - - data += "startxref\n%d\n%%%%EOF\n" % xrefStartPos + self.objectCnt, self.catalogObj.nr, self.infoObj.nr))) - return str(data) + canvas.addLiteral("startxref\n%d\n%%%%EOF\n" % xrefStartPos) # get font number to use for given flags. also creates the PDF object # for the font if it does not yet exist. diff --git a/src/scenereport.py b/src/scenereport.py index 96d4a039..ed8dc1ac 100644 --- a/src/scenereport.py +++ b/src/scenereport.py @@ -1,3 +1,5 @@ +from typing import AnyStr + import misc import pdf import pml @@ -36,7 +38,7 @@ def __init__(self, sp): for s in ["Speakers"]: self.inf.append(misc.CheckBoxItem(s)) - def generate(self) -> str: + def generate(self) -> AnyStr: tf = pml.TextFormatter(self.sp.cfg.paperWidth, self.sp.cfg.paperHeight, 15.0, 12) diff --git a/src/screenplay.py b/src/screenplay.py index 7e84e4b5..e50d2d02 100644 --- a/src/screenplay.py +++ b/src/screenplay.py @@ -23,7 +23,7 @@ NOTE = 8 ACTBREAK = 9 -from typing import Tuple +from typing import Tuple, AnyStr import autocompletion import config @@ -826,7 +826,7 @@ def generateRTF(self): # 100% correct for the screenplay. isExport is True if this is an # "export to file" operation, False if we're just going to launch a # PDF viewer with the data. - def generatePDF(self, isExport: bool) -> str: + def generatePDF(self, isExport: bool) -> AnyStr: return pdf.generate(self.generatePML(isExport)) # Same arguments as generatePDF, but returns a PML document. diff --git a/src/scriptreport.py b/src/scriptreport.py index 46cdf805..2b8d83e4 100644 --- a/src/scriptreport.py +++ b/src/scriptreport.py @@ -1,3 +1,5 @@ +from typing import AnyStr + import characterreport import config import pdf @@ -12,7 +14,7 @@ def __init__(self, sp): self.sr = scenereport.SceneReport(sp) self.cr = characterreport.CharacterReport(sp) - def generate(self) -> str: + def generate(self) -> AnyStr: tf = pml.TextFormatter(self.sp.cfg.paperWidth, self.sp.cfg.paperHeight, 15.0, 12) From 4a56b8520d1ac5213ce72f0908b41bbc60d39aa4 Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sun, 12 Mar 2023 15:49:04 +0100 Subject: [PATCH 03/22] Remove manual xref-Table-generation When using reportlab, this doesn't have an effect anyway, as reportlab takes care of the xref table. It seems to ignore all xref related commands inserted manually. --- src/pdf.py | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/src/pdf.py b/src/pdf.py index 2dfd83ba..21abb6a2 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -184,10 +184,6 @@ def __init__(self, nr: int, data: str = ""): # all data between 'obj/endobj' tags, excluding newlines self.data: str = data - # start position of object, stored in the xref table. initialized - # when the object is written out (by the caller of write). - self.xrefPos: int = -1 - # write object to canvas. def write(self, canvas: Canvas) -> None: code = "%d 0 obj\n" % self.nr @@ -423,21 +419,8 @@ def addObj(self, data: str = "") -> PDFObject: # write out object to 'canvas' def writeObj(self, canvas: Canvas, obj: PDFObject) -> None: - obj.xrefPos = self.getPdfCodeLength(canvas._code) obj.write(canvas) - def getPdfCodeLength(self, canvasCodeList: List[str]): - length = 0 - for string in canvasCodeList: - length += len(string) - - return length - - # write a xref table entry to 'output', using position - # 'pos, generation 'gen' and type 'typ'. - def writeXref(self, canvas: Canvas, pos: int, gen: int = 0, typ: str = "n") -> None: - canvas.addLiteral("%010d %05d %s \n" % (pos, gen, typ)) - # generate PDF file and return it as a string def genPDF(self, canvas: Canvas) -> None: canvas.addLiteral("%PDF-1.5\n") @@ -445,24 +428,6 @@ def genPDF(self, canvas: Canvas) -> None: for obj in self.objects: self.writeObj(canvas, obj) - xrefStartPos = self.getPdfCodeLength(canvas._code) - - canvas.addLiteral("xref\n0 %d\n" % self.objectCnt) - self.writeXref(canvas, 0, 65535, "f") - - for obj in self.objects: - self.writeXref(canvas, obj.xrefPos) - - canvas.addLiteral("\n") - - canvas.addLiteral(("trailer\n" - "<< /Size %d\n" - "/Root %d 0 R\n" - "/Info %d 0 R\n>>\n" % ( - self.objectCnt, self.catalogObj.nr, self.infoObj.nr))) - - canvas.addLiteral("startxref\n%d\n%%%%EOF\n" % xrefStartPos) - # get font number to use for given flags. also creates the PDF object # for the font if it does not yet exist. def getFontNr(self, flags: int) -> int: From 740167cfe305633986155cc5ce84a88b2a6cbae6 Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sun, 12 Mar 2023 16:12:38 +0100 Subject: [PATCH 04/22] Fix PDF version in reportlab 1.5 was the version we previously generated, but the "literal" PDF instruction was ignored by reportlab --- src/pdf.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pdf.py b/src/pdf.py index 21abb6a2..1fc10831 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -200,7 +200,7 @@ def __init__(self, doc: 'pml.Document'): # generate PDF document and return it as a string def generate(self) -> AnyStr: - canvas = Canvas('') + canvas = Canvas('', pdfVersion=(1, 5)) doc = self.doc # fast lookup of font information @@ -423,8 +423,6 @@ def writeObj(self, canvas: Canvas, obj: PDFObject) -> None: # generate PDF file and return it as a string def genPDF(self, canvas: Canvas) -> None: - canvas.addLiteral("%PDF-1.5\n") - for obj in self.objects: self.writeObj(canvas, obj) From 2667f74ee48ab0dda7dd1678853308e9fe63fc02 Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sun, 12 Mar 2023 17:29:13 +0100 Subject: [PATCH 05/22] Port table of contents/"Outline" to reportlab --- src/pdf.py | 72 +++++++----------------------------------------------- src/pml.py | 3 --- 2 files changed, 9 insertions(+), 66 deletions(-) diff --git a/src/pdf.py b/src/pdf.py index 1fc10831..edbc2978 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -1,3 +1,4 @@ +import uuid from typing import Optional, List, Tuple, Dict, AnyStr from reportlab.pdfgen.canvas import Canvas @@ -31,9 +32,6 @@ def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe:'PDFE if not isinstance(pmlOp, pml.TextOp): raise Exception("PDFTextOp is only compatible with pml.TextOp, got "+type(pmlOp).__name__) - if pmlOp.toc: - pmlOp.toc.pageObjNr = pe.pageObjs[pageNr].nr - # we need to adjust y position since PDF uses baseline of text as # the y pos, but pml uses top of the text as y pos. The Adobe # standard Courier family font metrics give 157 units in 1/1000 @@ -236,38 +234,12 @@ def generate(self) -> AnyStr: pages: int = len(doc.pages) - self.catalogObj: PDFObject = self.addObj() self.infoObj: PDFObject = self.createInfoObj() pagesObj: PDFObject = self.addObj() # we only create this when needed, in genWidths self.widthsObj: Optional[PDFObject] = None - if doc.tocs: - self.outlinesObj: PDFObject = self.addObj() - - # each outline is a single PDF object - self.outLineObjs: List[PDFObject] = [] - - for i in range(len(doc.tocs)): - self.outLineObjs.append(self.addObj()) - - self.outlinesObj.data = ("<< /Type /Outlines\n" - "/Count %d\n" - "/First %d 0 R\n" - "/Last %d 0 R\n" - ">>" % (len(doc.tocs), - self.outLineObjs[0].nr, - self.outLineObjs[-1].nr)) - - outlinesStr = "/Outlines %d 0 R\n" % self.outlinesObj.nr - - if doc.showTOC: - outlinesStr += "/PageMode /UseOutlines\n" - - else: - outlinesStr = "" - # each page has two PDF objects: 1) a /Page object that links to # 2) a stream object that has the actual page contents. self.pageObjs: List[PDFObject] = [] @@ -282,13 +254,8 @@ def generate(self) -> AnyStr: self.pageContentObjs.append(self.addObj()) if doc.defPage != -1: - outlinesStr += "/OpenAction [%d 0 R /XYZ null null 0]\n" % ( - self.pageObjs[0].nr + doc.defPage * 2) - - self.catalogObj.data = ("<< /Type /Catalog\n" - "/Pages %d 0 R\n" - "%s" - ">>" % (pagesObj.nr, outlinesStr)) + canvas.addLiteral("/OpenAction [%d 0 R /XYZ null null 0]\n" % ( + self.pageObjs[0].nr + doc.defPage * 2)) # TODO: This probably doesn't work any more/couldn't be tested, as creating new pages doesn't work yet (and the output prints everything one one page) for i in range(pages): self.genPage(i) @@ -314,8 +281,12 @@ def generate(self) -> AnyStr: self.mm2points(doc.h), fontStr)) if doc.tocs: - for i in range(len(doc.tocs)): - self.genOutline(i) + for toc in doc.tocs: + bookmarkKey = uuid.uuid4().hex # we need a unique key to link the bookmark in toc – TODO: generate a more speaking one + canvas.bookmarkHorizontal(bookmarkKey, self.x(toc.op.x), self.y(toc.op.y)) + canvas.addOutlineEntry(toc.text, bookmarkKey) + if doc.showTOC: + canvas.showOutline() self.genPDF(canvas) @@ -361,31 +332,6 @@ def genPage(self, pageNr) -> None: self.pageContentObjs[pageNr].data = self.genStream(str(cont)) - # generate outline number 'i' - def genOutline(self, i) -> None: - toc = self.doc.tocs[i] - obj = self.outLineObjs[i] - - if i != (len(self.doc.tocs) - 1): - nextStr = "/Next %d 0 R\n" % (obj.nr + 1) - else: - nextStr = "" - - if i != 0: - prevStr = "/Prev %d 0 R\n" % (obj.nr - 1) - else: - prevStr = "" - - obj.data = ("<< /Parent %d 0 R\n" - "/Dest [%d 0 R /XYZ %f %f 0]\n" - "/Title (%s)\n" - "%s" - "%s" - ">>" % ( - self.outlinesObj.nr, toc.pageObjNr, self.x(toc.op.x), - self.y(toc.op.y), self.escapeStr(toc.text), - prevStr, nextStr)) - # generate a stream object's contents. 's' is all data between # 'stream/endstream' tags, excluding newlines. def genStream(self, s, isFontStream = False) -> str: diff --git a/src/pml.py b/src/pml.py index 9fa4a9b7..0debeb56 100644 --- a/src/pml.py +++ b/src/pml.py @@ -98,9 +98,6 @@ def __init__(self, text: str, op: 'TextOp'): # correct positioning information) self.op: TextOp = op - # the PDF object number of the page we point to - self.pageObjNr: int = -1 - # information about one PDF font class PDFFontInfo: def __init__(self, name: str, fontProgram: Optional[AnyStr]): From 3849d43460c569ee168da0494f1c5a9eb5bff2b0 Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sun, 12 Mar 2023 20:43:30 +0100 Subject: [PATCH 06/22] Port rendering of pages to reportlab --- src/pdf.py | 88 +++++++++++++++++------------------------------ src/pml.py | 5 --- src/screenplay.py | 1 - 3 files changed, 32 insertions(+), 62 deletions(-) diff --git a/src/pdf.py b/src/pdf.py index edbc2978..f5fe711b 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -198,8 +198,12 @@ def __init__(self, doc: 'pml.Document'): # generate PDF document and return it as a string def generate(self) -> AnyStr: - canvas = Canvas('', pdfVersion=(1, 5)) doc = self.doc + canvas = Canvas( + '', + pdfVersion=(1, 5), + pagesize=(self.mm2points(doc.w), self.mm2points(doc.h)), + ) # fast lookup of font information self.fonts: Dict[int, FontInfo] = { @@ -232,61 +236,47 @@ def generate(self) -> AnyStr: # xref table is an object of some kind or something... self.objectCnt: int = 1 - pages: int = len(doc.pages) - self.infoObj: PDFObject = self.createInfoObj() - pagesObj: PDFObject = self.addObj() # we only create this when needed, in genWidths self.widthsObj: Optional[PDFObject] = None - # each page has two PDF objects: 1) a /Page object that links to - # 2) a stream object that has the actual page contents. - self.pageObjs: List[PDFObject] = [] - self.pageContentObjs: List[PDFObject] = [] + if doc.defPage != -1: + # canvas.addLiteral("/OpenAction [%d 0 R /XYZ null null 0]\n" % (self.pageObjs[0].nr + doc.defPage * 2)) # this should make the PDF reader open the PDF at the desired page + # TODO: This doesn't seem to be easily doable with reportlab. /OpenAction is considered a security threat by some (as it allows executing JavaScript), so I think it's unlikely they'll add support. Also, this feature didn't work with many PDF viewers anyway; I tested Evince, Okular and pdf.js in Firefox, and they all didn't support it. So maybe, we should remove this feature entirely? + pass - for i in range(pages): - self.pageObjs.append(self.addObj("<< /Type /Page\n" - "/Parent %d 0 R\n" - "/Contents %d 0 R\n" - ">>" % (pagesObj.nr, - self.objectCnt + 1))) - self.pageContentObjs.append(self.addObj()) + numberOfPages: int = len(doc.pages) - if doc.defPage != -1: - canvas.addLiteral("/OpenAction [%d 0 R /XYZ null null 0]\n" % ( - self.pageObjs[0].nr + doc.defPage * 2)) # TODO: This probably doesn't work any more/couldn't be tested, as creating new pages doesn't work yet (and the output prints everything one one page) + # draw pages + for i in range(numberOfPages): + pg = self.doc.pages[i] + # content stream + cont = util.String() + self.currentFont: str = "" + for op in pg.ops: + op.pdfOp.draw(op, i, cont, self) - for i in range(pages): - self.genPage(i) + # create bookmark for table of contents if applicable TODO: move into pdfOp.draw() + if isinstance(op, pml.TextOp) and op.toc: + bookmarkKey = uuid.uuid4().hex # we need a unique key to link the bookmark in toc – TODO: generate a more speaking one + canvas.bookmarkHorizontal(bookmarkKey, self.x(op.x), self.y(op.y)) + canvas.addOutlineEntry(op.toc.text, bookmarkKey) - kids = util.String() - kids += "[" - for obj in self.pageObjs: - kids += "%d 0 R\n" % obj.nr - kids += "]" + canvas.addLiteral(self.genStream(str(cont))) + + if i < numberOfPages - 1: + canvas.showPage() + + if doc.showTOC: + canvas.showOutline() fontStr = "" for fi in self.fonts.values(): if fi.number != -1: fontStr += "/F%d %d 0 R " % (fi.number, fi.pdfObj.nr) - - pagesObj.data = ("<< /Type /Pages\n" - "/Kids %s\n" - "/Count %d\n" - "/MediaBox [0 0 %f %f]\n" - "/Resources << /Font <<\n" - "%s >> >>\n" - ">>" % (str(kids), pages, self.mm2points(doc.w), - self.mm2points(doc.h), fontStr)) - - if doc.tocs: - for toc in doc.tocs: - bookmarkKey = uuid.uuid4().hex # we need a unique key to link the bookmark in toc – TODO: generate a more speaking one - canvas.bookmarkHorizontal(bookmarkKey, self.x(toc.op.x), self.y(toc.op.y)) - canvas.addOutlineEntry(toc.text, bookmarkKey) - if doc.showTOC: - canvas.showOutline() + # The font string had been inserted into the /Pages object under "/Resources << /Font<<\n %s >> >>\n>>" + # TODO: find a new place for this information on the reportlab canvas self.genPDF(canvas) @@ -318,20 +308,6 @@ def genWidths(self) -> None: self.widthsObj = self.addObj(self.__class__._widthsStr) - # generate a single page - def genPage(self, pageNr) -> None: - pg = self.doc.pages[pageNr] - - # content stream - cont = util.String() - - self.currentFont: str = "" - - for op in pg.ops: - op.pdfOp.draw(op, pageNr, cont, self) - - self.pageContentObjs[pageNr].data = self.genStream(str(cont)) - # generate a stream object's contents. 's' is all data between # 'stream/endstream' tags, excluding newlines. def genStream(self, s, isFontStream = False) -> str: diff --git a/src/pml.py b/src/pml.py index 0debeb56..3f1de4e9 100644 --- a/src/pml.py +++ b/src/pml.py @@ -44,8 +44,6 @@ def __init__(self, w: float, h: float): self.pages: List[Page] = [] - self.tocs: List[TOCItem] = [] - # user-specified fonts, if any. key = 2 lowest bits of # TextOp.flags, value = pml.PDFFontInfo self.fonts: Dict[int, 'PDFFontInfo'] = {} @@ -67,9 +65,6 @@ def __init__(self, w: float, h: float): def add(self, page: 'Page') -> None: self.pages.append(page) - def addTOC(self, toc: 'TOCItem') -> None: - self.tocs.append(toc) - def addFont(self, style: int, pfi: 'PDFFontInfo') -> None: self.fonts[style] = pfi diff --git a/src/screenplay.py b/src/screenplay.py index e50d2d02..190a20c3 100644 --- a/src/screenplay.py +++ b/src/screenplay.py @@ -1037,7 +1037,6 @@ def generatePMLPage(self, pager, pageNr, forPDF, doExtra): s = text to.toc = pml.TOCItem(s, to) - pager.doc.addTOC(to.toc) if doExtra and cfg.pdfShowLineNumbers: pg.add(pml.TextOp("%02d" % (i - start + 1), From b234f9326fc0267a67538839e07079b2c9129670 Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sun, 12 Mar 2023 21:35:59 +0100 Subject: [PATCH 07/22] Port setting PDF information/metadata to reportlab --- src/pdf.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/pdf.py b/src/pdf.py index f5fe711b..cb1dac61 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -205,6 +205,13 @@ def generate(self) -> AnyStr: pagesize=(self.mm2points(doc.w), self.mm2points(doc.h)), ) + # set PDF info + version = self.escapeStr(self.doc.version) + canvas.setCreator('Trelby '+version) + canvas.setProducer('Trelby '+version) + if self.doc.uniqueId: + canvas.setKeywords(self.doc.uniqueId) + # fast lookup of font information self.fonts: Dict[int, FontInfo] = { pml.COURIER : FontInfo("Courier"), @@ -236,8 +243,6 @@ def generate(self) -> AnyStr: # xref table is an object of some kind or something... self.objectCnt: int = 1 - self.infoObj: PDFObject = self.createInfoObj() - # we only create this when needed, in genWidths self.widthsObj: Optional[PDFObject] = None @@ -284,19 +289,6 @@ def generate(self) -> AnyStr: return data - def createInfoObj(self) -> PDFObject: - version = self.escapeStr(self.doc.version) - - if self.doc.uniqueId: - extra = "/Keywords (%s)\n" % self.doc.uniqueId - else: - extra = "" - - return self.addObj("<< /Creator (Trelby %s)\n" - "/Producer (Trelby %s)\n" - "%s" - ">>" % (version, version, extra)) - # create a PDF object containing a 256-entry array for the widths of a # font, with all widths being 600 def genWidths(self) -> None: From c934be27a91553c381d1894053e5940ac8a3827d Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sun, 12 Mar 2023 22:16:54 +0100 Subject: [PATCH 08/22] Pass reportlabs canvas to PDFDrawOp Passing the canvas additionally to the output string, we can smoothly transition all operations from rendering to the output string to rendering to the reportlab canvas. A smooth transitions allows to test the effects of each change individually. Of course, we will later-on remove the output string parameter when it's not needed anymore. --- src/pdf.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pdf.py b/src/pdf.py index cb1dac61..ab1bbc36 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -24,11 +24,11 @@ class PDFDrawOp: # write PDF drawing operations corresponding to the PML object pmlOp # to output (util.String). pe = PDFExporter. - def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe:'PDFExporter') -> None: + def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas: Canvas) -> None: raise Exception("draw not implemented") class PDFTextOp(PDFDrawOp): - def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe:'PDFExporter') -> None: + def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas) -> None: if not isinstance(pmlOp, pml.TextOp): raise Exception("PDFTextOp is only compatible with pml.TextOp, got "+type(pmlOp).__name__) @@ -82,7 +82,7 @@ def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe:'PDFE "S\n" % (0.05 * pmlOp.size, x, undY, x + undLen, undY) class PDFLineOp(PDFDrawOp): - def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe:'PDFExporter'): + def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas): if not isinstance(pmlOp, pml.LineOp): raise Exception("PDFLineOp is only compatible with pml.LineOp, got "+type(pmlOp).__name__) @@ -107,7 +107,7 @@ def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe:'PDFE output += "S\n" class PDFRectOp(PDFDrawOp): - def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe:'PDFExporter') -> None: + def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas) -> None: if not isinstance(pmlOp, pml.RectOp): raise Exception("PDFRectOp is only compatible with pml.RectOp, got "+type(pmlOp).__name__) @@ -129,7 +129,7 @@ def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe:'PDFE print("Invalid fill type for RectOp") class PDFQuarterCircleOp(PDFDrawOp): - def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe:'PDFExporter') -> None: + def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas) -> None: if not isinstance(pmlOp, pml.QuarterCircleOp): raise Exception("PDFQuarterCircleOp is only compatible with pml.QuarterCircleOp, got "+type(pmlOp).__name__) @@ -156,7 +156,7 @@ def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe:'PDFE output += "S\n" class PDFArbitraryOp(PDFDrawOp): - def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe:'PDFExporter') -> None: + def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas) -> None: if not isinstance(pmlOp, pml.PDFOp): raise Exception("PDFArbitraryOp is only compatible with pml.PDFOp, got "+type(pmlOp).__name__) @@ -260,7 +260,7 @@ def generate(self) -> AnyStr: cont = util.String() self.currentFont: str = "" for op in pg.ops: - op.pdfOp.draw(op, i, cont, self) + op.pdfOp.draw(op, i, cont, self, canvas) # create bookmark for table of contents if applicable TODO: move into pdfOp.draw() if isinstance(op, pml.TextOp) and op.toc: From 78cc3365fd1f0cd476d31848af9e62e60cbfd41d Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sun, 12 Mar 2023 22:22:12 +0100 Subject: [PATCH 09/22] Port PDFQuarterCircleOp to reportlab canvas --- src/pdf.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pdf.py b/src/pdf.py index ab1bbc36..a425bc0a 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -144,16 +144,16 @@ def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDF # has a max. drift of 0.019608% which is 28% better. A = pmlOp.radius * 0.551915024494 - output += "%f w\n"\ - "%s m\n" % (pe.mm2points(pmlOp.width), - pe.xy((pmlOp.x - pmlOp.radius * sX, pmlOp.y))) - - output += "%f %f %f %f %f %f c\n" % ( - pe.x(pmlOp.x - pmlOp.radius * sX), pe.y(pmlOp.y - A * sY), - pe.x(pmlOp.x - A * sX), pe.y(pmlOp.y - pmlOp.radius * sY), - pe.x(pmlOp.x), pe.y(pmlOp.y - pmlOp.radius * sY)) - - output += "S\n" + canvas.setLineWidth(pe.mm2points(pmlOp.width)) + canvas.bezier( + pe.x(pmlOp.x - pmlOp.radius * sX), + pe.y(pmlOp.y), + pe.x(pmlOp.x - pmlOp.radius * sX), + pe.y(pmlOp.y - A * sY), + pe.x(pmlOp.x - A * sX), + pe.y(pmlOp.y - pmlOp.radius * sY), + pe.x(pmlOp.x), pe.y(pmlOp.y - pmlOp.radius * sY) + ) class PDFArbitraryOp(PDFDrawOp): def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas) -> None: From d54d9064766aa1d2ff1e314d2cf318a305fa6d1d Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sun, 12 Mar 2023 22:43:34 +0100 Subject: [PATCH 10/22] Port PDFArbitraryOp to reportlab canvas --- src/pdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pdf.py b/src/pdf.py index a425bc0a..34fbbe8e 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -160,7 +160,7 @@ def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDF if not isinstance(pmlOp, pml.PDFOp): raise Exception("PDFArbitraryOp is only compatible with pml.PDFOp, got "+type(pmlOp).__name__) - output += "%s\n" % pmlOp.cmds + canvas.addLiteral("%s\n" % pmlOp.cmds) # used for keeping track of used fonts class FontInfo: From 763b51e573bc49f7be70657a004403e6cca82a15 Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sun, 12 Mar 2023 22:44:26 +0100 Subject: [PATCH 11/22] Port PDFRectOp to reportlab canvas --- src/pdf.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/pdf.py b/src/pdf.py index 34fbbe8e..f838f733 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -112,21 +112,16 @@ def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDF raise Exception("PDFRectOp is only compatible with pml.RectOp, got "+type(pmlOp).__name__) if pmlOp.lw != -1: - output += "%f w\n" % pe.mm2points(pmlOp.lw) + canvas.setLineWidth(pe.mm2points(pmlOp.lw)) - output += "%f %f %f %f re\n" % ( + canvas.rect( pe.x(pmlOp.x), pe.y(pmlOp.y) - pe.mm2points(pmlOp.height), - pe.mm2points(pmlOp.width), pe.mm2points(pmlOp.height)) - - if pmlOp.fillType == pml.NO_FILL: - output += "S\n" - elif pmlOp.fillType == pml.FILL: - output += "f\n" - elif pmlOp.fillType == pml.STROKE_FILL: - output += "B\n" - else: - print("Invalid fill type for RectOp") + pe.mm2points(pmlOp.width), + pe.mm2points(pmlOp.height), + pmlOp.fillType == pml.NO_FILL or pmlOp.fillType == pml.STROKE_FILL, + pmlOp.fillType == pml.FILL or pmlOp.fillType == pml.STROKE_FILL + ) class PDFQuarterCircleOp(PDFDrawOp): def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas) -> None: From 474cda47e7ed026d872d8aa753142314fa3a227e Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sun, 12 Mar 2023 23:14:19 +0100 Subject: [PATCH 12/22] Port PDFLineOp to reportlab canvas --- src/pdf.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/pdf.py b/src/pdf.py index f838f733..03119060 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -86,25 +86,24 @@ def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDF if not isinstance(pmlOp, pml.LineOp): raise Exception("PDFLineOp is only compatible with pml.LineOp, got "+type(pmlOp).__name__) - p = pmlOp.points + points = pmlOp.points + numberOfPoints = len(points) - pc = len(p) - - if pc < 2: - print("LineOp contains only %d points" % pc) + if numberOfPoints < 2: + print("LineOp contains only %d points" % numberOfPoints) return - output += "%f w\n"\ - "%s m\n" % (pe.mm2points(pmlOp.width), pe.xy(p[0])) + canvas.setLineWidth(pe.mm2points(pmlOp.width)) - for i in range(1, pc): - output += "%s l\n" % (pe.xy(p[i])) + lines = [] + for i in range(0, numberOfPoints - 1): + lines.append(pe.xy(points[i]) + pe.xy(points[i+1])) if pmlOp.isClosed: - output += "s\n" - else: - output += "S\n" + lines.append(pe.xy(points[i+1]) + pe.xy(points[0])) + + canvas.lines(lines) class PDFRectOp(PDFDrawOp): def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas) -> None: @@ -432,10 +431,9 @@ def x(self, x: float) -> float: def y(self, y: float) -> float: return self.mm2points(self.doc.h - y) - # convert xy, which is (x, y) pair, into PDF coordinates, and format - # it as "%f %f", and return that. - def xy(self, xy: Tuple[float, float]) -> str: + # convert xy, which is (x, y) pair, into PDF coordinates + def xy(self, xy: Tuple[float, float]) -> Tuple[float, float]: x = self.x(xy[0]) y = self.y(xy[1]) - return "%f %f" % (x, y) + return (x, y) From dd085688e9df90faa077b17164e373980cbeafda Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Mon, 13 Mar 2023 01:37:41 +0100 Subject: [PATCH 13/22] Port PDFTextOp to reportlab canvas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was the last operation 🎉 --- src/pdf.py | 168 +++++++--------------------------------------- src/pml.py | 4 +- src/screenplay.py | 22 +----- 3 files changed, 31 insertions(+), 163 deletions(-) diff --git a/src/pdf.py b/src/pdf.py index 03119060..70b664ef 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -1,9 +1,10 @@ import uuid -from typing import Optional, List, Tuple, Dict, AnyStr +from typing import Optional, Tuple, Dict, AnyStr +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfgen.canvas import Canvas -import fontinfo import pml import util @@ -44,42 +45,34 @@ def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDF x = pe.x(pmlOp.x) y = pe.y(pmlOp.y) - 0.843 * pmlOp.size - newFont = "F%d %d" % (pe.getFontNr(pmlOp.flags), pmlOp.size) - if newFont != pe.currentFont: - output += "/%s Tf\n" % newFont - pe.currentFont = newFont + newFont = pe.getFontForFlags(pmlOp.flags) + canvas.setFont(newFont, pmlOp.size) if pmlOp.angle is not None: matrix = TRANSFORM_MATRIX.get(pmlOp.angle) if matrix: - output += "BT\n"\ + canvas.addLiteral("BT\n"\ "%f %f %f %f %f %f Tm\n"\ "(%s) Tj\n"\ "ET\n" % (matrix[0], matrix[1], matrix[2], matrix[3], - x, y, pe.escapeStr(pmlOp.text)) + x, y, pe.escapeStr(pmlOp.text))) # TODO: Doing this with addLiteral, non-latin characters won't work in watermarks. There must be a better way to do this with reportlab, that we do it this way is for historical reasons and because no one took the time to change it else: # unsupported angle, don't print it. pass else: - output += "BT\n"\ - "%f %f Td\n"\ - "(%s) Tj\n"\ - "ET\n" % (x, y, pe.escapeStr(pmlOp.text)) + canvas.drawString(x, y, pmlOp.text) if pmlOp.flags & pml.UNDERLINED: - undLen = fontinfo.getMetrics(pmlOp.flags).getTextWidth( - pmlOp.text, pmlOp.size) + undLen = canvas.stringWidth(pmlOp.text, newFont, pmlOp.size) # all standard PDF fonts have the underline line 100 units # below baseline with a thickness of 50 undY = y - 0.1 * pmlOp.size + canvas.setLineWidth(0.05 * pmlOp.size) - output += "%f w\n"\ - "%f %f m\n"\ - "%f %f l\n"\ - "S\n" % (0.05 * pmlOp.size, x, undY, x + undLen, undY) + canvas.line(x, undY, x + undLen, undY) class PDFLineOp(PDFDrawOp): def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas): @@ -227,19 +220,6 @@ def generate(self) -> AnyStr: FontInfo("Times-BoldItalic"), } - # list of PDFObjects - self.objects: List[PDFObject] = [] - - # number of fonts used - self.fontCnt: int = 0 - - # PDF object count. it starts at 1 because the 'f' thingy in the - # xref table is an object of some kind or something... - self.objectCnt: int = 1 - - # we only create this when needed, in genWidths - self.widthsObj: Optional[PDFObject] = None - if doc.defPage != -1: # canvas.addLiteral("/OpenAction [%d 0 R /XYZ null null 0]\n" % (self.pageObjs[0].nr + doc.defPage * 2)) # this should make the PDF reader open the PDF at the desired page # TODO: This doesn't seem to be easily doable with reportlab. /OpenAction is considered a security threat by some (as it allows executing JavaScript), so I think it's unlikely they'll add support. Also, this feature didn't work with many PDF viewers anyway; I tested Evince, Okular and pdf.js in Firefox, and they all didn't support it. So maybe, we should remove this feature entirely? @@ -252,7 +232,6 @@ def generate(self) -> AnyStr: pg = self.doc.pages[i] # content stream cont = util.String() - self.currentFont: str = "" for op in pg.ops: op.pdfOp.draw(op, i, cont, self, canvas) @@ -270,30 +249,10 @@ def generate(self) -> AnyStr: if doc.showTOC: canvas.showOutline() - fontStr = "" - for fi in self.fonts.values(): - if fi.number != -1: - fontStr += "/F%d %d 0 R " % (fi.number, fi.pdfObj.nr) - # The font string had been inserted into the /Pages object under "/Resources << /Font<<\n %s >> >>\n>>" - # TODO: find a new place for this information on the reportlab canvas - - self.genPDF(canvas) - data = canvas.getpdfdata() return data - # create a PDF object containing a 256-entry array for the widths of a - # font, with all widths being 600 - def genWidths(self) -> None: - if self.widthsObj: - return - - if not self.__class__._widthsStr: - self.__class__._widthsStr = "[%s]" % ("600 " * 256).rstrip() - - self.widthsObj = self.addObj(self.__class__._widthsStr) - # generate a stream object's contents. 's' is all data between # 'stream/endstream' tags, excluding newlines. def genStream(self, s, isFontStream = False) -> str: @@ -316,103 +275,28 @@ def genStream(self, s, isFontStream = False) -> str: "%s\n" "endstream" % (len(s), lenStr, filterStr, s)) - # add a new object and return it. 'data' is all data between - # 'obj/endobj' tags, excluding newlines. - def addObj(self, data: str = "") -> PDFObject: - obj = PDFObject(self.objectCnt, data) - self.objects.append(obj) - self.objectCnt += 1 - - return obj - - # write out object to 'canvas' - def writeObj(self, canvas: Canvas, obj: PDFObject) -> None: - obj.write(canvas) - # generate PDF file and return it as a string - def genPDF(self, canvas: Canvas) -> None: - for obj in self.objects: - self.writeObj(canvas, obj) - - # get font number to use for given flags. also creates the PDF object - # for the font if it does not yet exist. - def getFontNr(self, flags: int) -> int: + # get font name to use for given flags. also registers the font in reportlabs if it does not yet exist. + def getFontForFlags(self, flags: int) -> str: # the "& 15" gets rid of the underline flag - fi = self.fonts.get(flags & 15) + fontInfo = self.fonts.get(flags & 15) - if not fi: - print("PDF.getfontNr: invalid flags %d" % flags) + if not fontInfo: + raise Exception("PDF.getfontNr: invalid flags %d" % flags) - return 0 + # the "& 15" gets rid of the underline flag + customFontInfo = self.doc.fonts.get(flags & 15) - if fi.number == -1: - fi.number = self.fontCnt - self.fontCnt += 1 + if not customFontInfo: + return fontInfo.name - # the "& 15" gets rid of the underline flag - pfi = self.doc.fonts.get(flags & 15) + # Sadly, I don't know if this works, as setting custom fonts is broken currently and I couldn't test this + if not customFontInfo.name in pdfmetrics.getRegisteredFontNames(): + if not customFontInfo.fontFileName: + raise Exception('Font name %s is not known and no font file name provided' % customFontInfo.name) + pdfmetrics.registerFont(TTFont(customFontInfo.name, customFontInfo.fontFileName)) - if not pfi: - fi.pdfObj = self.addObj("<< /Type /Font\n" - "/Subtype /Type1\n" - "/BaseFont /%s\n" - "/Encoding /WinAnsiEncoding\n" - ">>" % fi.name) - else: - self.genWidths() - - fi.pdfObj = self.addObj("<< /Type /Font\n" - "/Subtype /TrueType\n" - "/BaseFont /%s\n" - "/Encoding /WinAnsiEncoding\n" - "/FirstChar 0\n" - "/LastChar 255\n" - "/Widths %d 0 R\n" - "/FontDescriptor %d 0 R\n" - ">>" % (pfi.name, self.widthsObj.nr, - self.objectCnt + 1)) - - fm = fontinfo.getMetrics(flags) - - if pfi.fontProgram: - fpStr = "/FontFile2 %d 0 R\n" % (self.objectCnt + 1) - else: - fpStr = "" - - # we use a %s format specifier for the italic angle since - # it sometimes contains integers, sometimes floating point - # values. - self.addObj("<< /Type /FontDescriptor\n" - "/FontName /%s\n" - "/FontWeight %d\n" - "/Flags %d\n" - "/FontBBox [%d %d %d %d]\n" - "/ItalicAngle %s\n" - "/Ascent %s\n" - "/Descent %s\n" - "/CapHeight %s\n" - "/StemV %s\n" - "/StemH %s\n" - "/XHeight %d\n" - "%s" - ">>" % (pfi.name, - fm.fontWeight, - fm.flags, - fm.bbox[0], fm.bbox[1], - fm.bbox[2], fm.bbox[3], - fm.italicAngle, - fm.ascent, - fm.descent, - fm.capHeight, - fm.stemV, - fm.stemH, - fm.xHeight, - fpStr)) - - if pfi.fontProgram: - self.addObj(self.genStream(pfi.fontProgram, True)) - - return fi.number + return customFontInfo.name # escape string def escapeStr(self, s: str) -> str: diff --git a/src/pml.py b/src/pml.py index 3f1de4e9..12a985df 100644 --- a/src/pml.py +++ b/src/pml.py @@ -95,14 +95,14 @@ def __init__(self, text: str, op: 'TextOp'): # information about one PDF font class PDFFontInfo: - def __init__(self, name: str, fontProgram: Optional[AnyStr]): + def __init__(self, name: str, fontFileName: Optional[str]): # name to use in generated PDF file ("CourierNew", "MyFontBold", # etc.). if empty, use the default PDF font. self.name: str = name # the font program (in practise, the contents of the .ttf file for # the font), or None, in which case the font is not embedded. - self.fontProgram: Optional[AnyStr] = fontProgram + self.fontFileName: Optional[str] = fontFileName # An abstract base class for all drawing operations. class DrawOp: diff --git a/src/screenplay.py b/src/screenplay.py index 190a20c3..00e23d55 100644 --- a/src/screenplay.py +++ b/src/screenplay.py @@ -852,30 +852,14 @@ def generatePML(self, isExport: bool) -> pml.Document: pf = self.cfg.getPDFFont(pfi) if pf.pdfName: - # TODO: it's nasty calling loadFile from here since it - # uses wxMessageBox. dialog stacking order is also wrong - # since we don't know the frame to give. so, we should - # remove references to wxMessageBox from util and instead - # pass in an ErrorHandlerObject to all functions that need - # it. then the GUI program can use a subclass of that that - # stores the frame pointer inside it, and testing - # framework / other non-interactive uses can use a version - # that logs errors to stderr / raises an exception / - # whatever. if pf.filename != "": - # we load at most 10 MB to avoid a denial-of-service - # attack by passing around scripts containing - # references to fonts with filenames like "/dev/zero" - # etc. no real font that I know of is this big so it - # shouldn't hurt. - fontProgram = util.loadFile(pf.filename, None, - 10 * 1024 * 1024) + fontFilename = pf.filename else: - fontProgram = None + fontFilename = None pager.doc.addFont(pf.style, - pml.PDFFontInfo(pf.pdfName, fontProgram)) + pml.PDFFontInfo(pf.pdfName, fontFilename)) return pager.doc From b71f28d41c08b023a7157d8f63c5466ddecfc504 Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Mon, 13 Mar 2023 01:38:29 +0100 Subject: [PATCH 14/22] Remove unnecessary escaping --- src/pdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pdf.py b/src/pdf.py index 70b664ef..fae7cce0 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -193,7 +193,7 @@ def generate(self) -> AnyStr: ) # set PDF info - version = self.escapeStr(self.doc.version) + version = self.doc.version canvas.setCreator('Trelby '+version) canvas.setProducer('Trelby '+version) if self.doc.uniqueId: From be69199221becc45f03e2e8816594135454ce421 Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Mon, 13 Mar 2023 01:45:38 +0100 Subject: [PATCH 15/22] Remove output-string parameter from PDFDrawOp --- src/pdf.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/pdf.py b/src/pdf.py index fae7cce0..433058bb 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -25,11 +25,11 @@ class PDFDrawOp: # write PDF drawing operations corresponding to the PML object pmlOp # to output (util.String). pe = PDFExporter. - def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas: Canvas) -> None: + def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, pe: 'PDFExporter', canvas: Canvas) -> None: raise Exception("draw not implemented") class PDFTextOp(PDFDrawOp): - def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas) -> None: + def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, pe: 'PDFExporter', canvas) -> None: if not isinstance(pmlOp, pml.TextOp): raise Exception("PDFTextOp is only compatible with pml.TextOp, got "+type(pmlOp).__name__) @@ -75,7 +75,7 @@ def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDF canvas.line(x, undY, x + undLen, undY) class PDFLineOp(PDFDrawOp): - def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas): + def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, pe: 'PDFExporter', canvas): if not isinstance(pmlOp, pml.LineOp): raise Exception("PDFLineOp is only compatible with pml.LineOp, got "+type(pmlOp).__name__) @@ -99,7 +99,7 @@ def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDF canvas.lines(lines) class PDFRectOp(PDFDrawOp): - def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas) -> None: + def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, pe: 'PDFExporter', canvas) -> None: if not isinstance(pmlOp, pml.RectOp): raise Exception("PDFRectOp is only compatible with pml.RectOp, got "+type(pmlOp).__name__) @@ -116,7 +116,7 @@ def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDF ) class PDFQuarterCircleOp(PDFDrawOp): - def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas) -> None: + def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, pe: 'PDFExporter', canvas) -> None: if not isinstance(pmlOp, pml.QuarterCircleOp): raise Exception("PDFQuarterCircleOp is only compatible with pml.QuarterCircleOp, got "+type(pmlOp).__name__) @@ -143,7 +143,7 @@ def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDF ) class PDFArbitraryOp(PDFDrawOp): - def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, output: 'util.String', pe: 'PDFExporter', canvas) -> None: + def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, pe: 'PDFExporter', canvas) -> None: if not isinstance(pmlOp, pml.PDFOp): raise Exception("PDFArbitraryOp is only compatible with pml.PDFOp, got "+type(pmlOp).__name__) @@ -230,10 +230,8 @@ def generate(self) -> AnyStr: # draw pages for i in range(numberOfPages): pg = self.doc.pages[i] - # content stream - cont = util.String() for op in pg.ops: - op.pdfOp.draw(op, i, cont, self, canvas) + op.pdfOp.draw(op, i, self, canvas) # create bookmark for table of contents if applicable TODO: move into pdfOp.draw() if isinstance(op, pml.TextOp) and op.toc: @@ -241,8 +239,6 @@ def generate(self) -> AnyStr: canvas.bookmarkHorizontal(bookmarkKey, self.x(op.x), self.y(op.y)) canvas.addOutlineEntry(op.toc.text, bookmarkKey) - canvas.addLiteral(self.genStream(str(cont))) - if i < numberOfPages - 1: canvas.showPage() From 7ffc986ce3f7094a1fd1fcf3df686aac943dd1f6 Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Mon, 13 Mar 2023 01:48:43 +0100 Subject: [PATCH 16/22] Move table of contents bookmark rendering to PDFTextOp --- src/pdf.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pdf.py b/src/pdf.py index 433058bb..a09fb89a 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -74,6 +74,12 @@ def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, pe: 'PDFExporter', canvas) -> N canvas.line(x, undY, x + undLen, undY) + # create bookmark for table of contents if applicable + if pmlOp.toc: + bookmarkKey = uuid.uuid4().hex # we need a unique key to link the bookmark in toc – TODO: generate a more speaking one + canvas.bookmarkHorizontal(bookmarkKey, pe.x(pmlOp.x), pe.y(pmlOp.y)) + canvas.addOutlineEntry(pmlOp.toc.text, bookmarkKey) + class PDFLineOp(PDFDrawOp): def draw(self, pmlOp: 'pml.DrawOp', pageNr: int, pe: 'PDFExporter', canvas): if not isinstance(pmlOp, pml.LineOp): @@ -233,12 +239,6 @@ def generate(self) -> AnyStr: for op in pg.ops: op.pdfOp.draw(op, i, self, canvas) - # create bookmark for table of contents if applicable TODO: move into pdfOp.draw() - if isinstance(op, pml.TextOp) and op.toc: - bookmarkKey = uuid.uuid4().hex # we need a unique key to link the bookmark in toc – TODO: generate a more speaking one - canvas.bookmarkHorizontal(bookmarkKey, self.x(op.x), self.y(op.y)) - canvas.addOutlineEntry(op.toc.text, bookmarkKey) - if i < numberOfPages - 1: canvas.showPage() From f703f83e9ac3aaa9020d4d1ff200eb6966713da4 Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Mon, 13 Mar 2023 01:50:11 +0100 Subject: [PATCH 17/22] Inline variable --- src/pdf.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pdf.py b/src/pdf.py index a09fb89a..546e9f2d 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -245,9 +245,7 @@ def generate(self) -> AnyStr: if doc.showTOC: canvas.showOutline() - data = canvas.getpdfdata() - - return data + return canvas.getpdfdata() # generate a stream object's contents. 's' is all data between # 'stream/endstream' tags, excluding newlines. From ee8dc43c31103b6e7ad2cc0d514a9787c61cac9d Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Mon, 13 Mar 2023 02:05:49 +0100 Subject: [PATCH 18/22] Don't set Helvetica as BaseFont Doesn't make a visible difference, but if you opened the PDF in a text editor before, you'd see Helvetica as an additional BaseFont --- src/pdf.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/pdf.py b/src/pdf.py index 546e9f2d..455a7743 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -188,6 +188,26 @@ class PDFExporter: def __init__(self, doc: 'pml.Document'): self.doc: pml.Document = doc + # fast lookup of font information + self.fonts: Dict[int, FontInfo] = { + pml.COURIER: FontInfo("Courier"), + pml.COURIER | pml.BOLD: FontInfo("Courier-Bold"), + pml.COURIER | pml.ITALIC: FontInfo("Courier-Oblique"), + pml.COURIER | pml.BOLD | pml.ITALIC: + FontInfo("Courier-BoldOblique"), + + pml.HELVETICA: FontInfo("Helvetica"), + pml.HELVETICA | pml.BOLD: FontInfo("Helvetica-Bold"), + pml.HELVETICA | pml.ITALIC: FontInfo("Helvetica-Oblique"), + pml.HELVETICA | pml.BOLD | pml.ITALIC: + FontInfo("Helvetica-BoldOblique"), + + pml.TIMES_ROMAN: FontInfo("Times-Roman"), + pml.TIMES_ROMAN | pml.BOLD: FontInfo("Times-Bold"), + pml.TIMES_ROMAN | pml.ITALIC: FontInfo("Times-Italic"), + pml.TIMES_ROMAN | pml.BOLD | pml.ITALIC: + FontInfo("Times-BoldItalic"), + } # generate PDF document and return it as a string def generate(self) -> AnyStr: @@ -196,6 +216,7 @@ def generate(self) -> AnyStr: '', pdfVersion=(1, 5), pagesize=(self.mm2points(doc.w), self.mm2points(doc.h)), + initialFontName=self.getFontForFlags(pml.NORMAL), ) # set PDF info @@ -205,26 +226,6 @@ def generate(self) -> AnyStr: if self.doc.uniqueId: canvas.setKeywords(self.doc.uniqueId) - # fast lookup of font information - self.fonts: Dict[int, FontInfo] = { - pml.COURIER : FontInfo("Courier"), - pml.COURIER | pml.BOLD: FontInfo("Courier-Bold"), - pml.COURIER | pml.ITALIC: FontInfo("Courier-Oblique"), - pml.COURIER | pml.BOLD | pml.ITALIC: - FontInfo("Courier-BoldOblique"), - - pml.HELVETICA : FontInfo("Helvetica"), - pml.HELVETICA | pml.BOLD: FontInfo("Helvetica-Bold"), - pml.HELVETICA | pml.ITALIC: FontInfo("Helvetica-Oblique"), - pml.HELVETICA | pml.BOLD | pml.ITALIC: - FontInfo("Helvetica-BoldOblique"), - - pml.TIMES_ROMAN : FontInfo("Times-Roman"), - pml.TIMES_ROMAN | pml.BOLD: FontInfo("Times-Bold"), - pml.TIMES_ROMAN | pml.ITALIC: FontInfo("Times-Italic"), - pml.TIMES_ROMAN | pml.BOLD | pml.ITALIC: - FontInfo("Times-BoldItalic"), - } if doc.defPage != -1: # canvas.addLiteral("/OpenAction [%d 0 R /XYZ null null 0]\n" % (self.pageObjs[0].nr + doc.defPage * 2)) # this should make the PDF reader open the PDF at the desired page From 290c4fd0060702179ef11ee7fff4427bf32e254e Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Fri, 7 Apr 2023 12:50:59 +0200 Subject: [PATCH 19/22] Improve error message when font can't be loaded --- src/pdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pdf.py b/src/pdf.py index d76bd62d..edb26fb6 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -288,7 +288,7 @@ def getFontForFlags(self, flags: int) -> str: # Sadly, I don't know if this works, as setting custom fonts is broken currently and I couldn't test this if not customFontInfo.name in pdfmetrics.getRegisteredFontNames(): if not customFontInfo.fontFileName: - raise Exception('Font name %s is not known and no font file name provided' % customFontInfo.name) + raise Exception('Font name "%s" is not known and no font file name provided. Please provide a file name for this font in the settings or use the default font.' % customFontInfo.name) pdfmetrics.registerFont(TTFont(customFontInfo.name, customFontInfo.fontFileName)) return customFontInfo.name From ec0a481db93619d50ee58627b9bce20035460074 Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sun, 17 Sep 2023 11:37:18 +0200 Subject: [PATCH 20/22] I know this works I can test this now, so that comment can be removed --- src/pdf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pdf.py b/src/pdf.py index edb26fb6..4ea81d89 100644 --- a/src/pdf.py +++ b/src/pdf.py @@ -285,7 +285,6 @@ def getFontForFlags(self, flags: int) -> str: if not customFontInfo: return fontInfo.name - # Sadly, I don't know if this works, as setting custom fonts is broken currently and I couldn't test this if not customFontInfo.name in pdfmetrics.getRegisteredFontNames(): if not customFontInfo.fontFileName: raise Exception('Font name "%s" is not known and no font file name provided. Please provide a file name for this font in the settings or use the default font.' % customFontInfo.name) From 1200ac3836b3c45ee2365124adc071bbc2cc529c Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sun, 17 Sep 2023 12:04:05 +0200 Subject: [PATCH 21/22] Fall back to default font when user has set font name without font file --- src/screenplay.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/screenplay.py b/src/screenplay.py index 64bcefea..db8b8b44 100644 --- a/src/screenplay.py +++ b/src/screenplay.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import wx # linebreak types @@ -848,18 +849,19 @@ def generatePML(self, isExport: bool) -> pml.Document: else: break + userHasSetFontNameWithoutFile = False + for pfi in self.cfg.getPDFFontIds(): pf = self.cfg.getPDFFont(pfi) - if pf.pdfName: - - if pf.filename != "": - fontFilename = pf.filename - else: - fontFilename = None - + if pf.pdfName and pf.filename != "": pager.doc.addFont(pf.style, - pml.PDFFontInfo(pf.pdfName, fontFilename)) + pml.PDFFontInfo(pf.pdfName, pf.filename)) + elif pf.pdfName: + userHasSetFontNameWithoutFile = True + + if userHasSetFontNameWithoutFile: + wx.MessageBox('Setting a font name without a font file is not possible in Trelby any more. Please adjust your settings in Script > Settings > Change > PDF/Fonts. Trelby will fall back to the default font for now.') return pager.doc From 23bf4535ff4f4ce9a975e664bdd95386551f5c3a Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sun, 17 Sep 2023 15:54:48 +0200 Subject: [PATCH 22/22] Adjust configuration dialogue to custom fonts without files being forbidden --- doc/manual.xml | 22 +++++++--------------- src/cfgdlg.py | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/doc/manual.xml b/doc/manual.xml index 52d1a7f9..fbcb65ea 100644 --- a/doc/manual.xml +++ b/doc/manual.xml @@ -1396,24 +1396,16 @@ Bold-Italic). Each font setting has two parts: a 'Name' field and a 'File' - field. The 'Name' field is the Postscript name of the font and is - used by the PDF viewer application to recognize the font to use. - Some examples of the names that would go here are 'AndaleMono', - 'CourierNewPSMT', and 'BitstreamVeraSansMono-Roman'. These names - are, in general, impossible for human beings to know for arbitrary - fonts. See the next paragraphs for ways around this. + field. The 'Name' field is the internal Postscript name of the font and + is used by the PDF viewer application to recognize the font to use. + The 'File' field is the filename of the TrueType font to use. If you fill in this field, the font file is embedded in the - generated PDF files. If you leave this field empty but fill in the - 'Name' field, the font file is not embedded in the PDF file, just - the 'Name' reference is used to indicate what font the PDF viewer - application should use. - - There are many factors affecting whether you should embed a - font or not. Embedding a font makes it much more likely that the - PDF file will display properly on other people's computers and/or - print properly. However, it will also increase the size of the PDF + generated PDF files. + + + Embedding a font will increase the size of the PDF files drastically, and not all fonts allow embedding in their license terms. diff --git a/src/cfgdlg.py b/src/cfgdlg.py index 94a76a16..283c6655 100644 --- a/src/cfgdlg.py +++ b/src/cfgdlg.py @@ -1401,10 +1401,8 @@ def __init__(self, parent, id, cfg): "Leave all the fields empty to use the default PDF Courier\n" "fonts. This is highly recommended.\n" "\n" - "Otherwise, fill in the font name (e.g. AndaleMono) to use\n" - "the specified TrueType font. If you want to embed the font\n" - "in the generated PDF files, fill in the font filename as well.\n" - "\n" + "Otherwise, fill in the the font filename to use\n" + "the specified TrueType font. \n" "See the manual for the full details.\n")) hsizer = wx.BoxSizer(wx.HORIZONTAL) @@ -1449,12 +1447,22 @@ def __init__(self, parent, id, cfg): # check that all embedded TrueType fonts are OK def checkForErrors(self): + userHasSetFontNameWithoutFile = False + for pfi in self.cfg.getPDFFontIds(): pf = self.cfg.getPDFFont(pfi) + if pf.name and not pf.filename: + userHasSetFontNameWithoutFile = True + if pf.filename: self.getFontPostscriptName(pf.filename) + if userHasSetFontNameWithoutFile: + wx.MessageBox( + 'You need to select a file when specifying a custom font. Select a file or remove the font name you provided.', + 'Error', wx.OK, cfgFrame) + def addEntry(self, name, descr, parent, sizer): sizer.Add(wx.StaticText(parent, -1, descr), 0, wx.ALIGN_CENTER_VERTICAL)