From 37f444811d761b88ecd5910babbcac6269e7ac7c Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=B6rg=20Lehmann?= Date: Fri, 9 Apr 2004 19:16:19 +0000 Subject: [PATCH] more PDF work: beginnings of text support git-svn-id: https://pyx.svn.sourceforge.net/svnroot/pyx/trunk/pyx@1658 069f4177-920e-0410-937b-c2a4a81bcd90 --- pyx/canvas.py | 296 ++++++++++++++++++++++++++++++--------------------------- pyx/dvifile.py | 87 ++++++++++++----- pyx/prolog.py | 10 +- pyx/text.py | 4 + 4 files changed, 229 insertions(+), 168 deletions(-) diff --git a/pyx/canvas.py b/pyx/canvas.py index 4df94d6c..b47d639d 100644 --- a/pyx/canvas.py +++ b/pyx/canvas.py @@ -32,16 +32,23 @@ with their attributes. import sys, cStringIO, math, time import attr, base, bbox, deco, unit, prolog, style, trafo, version +try: + enumerate([]) +except NameError: + # fallback implementation for Python 2.2. and below + def enumerate(list): + return zip(xrange(len(list)), list) + # known paperformats as tuple (width, height) -_paperformats = { "A4" : ("210 t mm", "297 t mm"), - "A3" : ("297 t mm", "420 t mm"), - "A2" : ("420 t mm", "594 t mm"), - "A1" : ("594 t mm", "840 t mm"), - "A0" : ("840 t mm", "1188 t mm"), - "A0B" : ("910 t mm", "1370 t mm"), - "Letter" : ("8.5 t inch", "11 t inch"), - "Legal" : ("8.5 t inch", "14 t inch")} +_paperformats = { "A4" : (210 * unit.t_mm, 297 * unit.t_mm), + "A3" : (297 * unit.t_mm, 420 * unit.t_mm), + "A2" : (420 * unit.t_mm, 594 * unit.t_mm), + "A1" : (594 * unit.t_mm, 840 * unit.t_mm), + "A0" : (840 * unit.t_mm, 1188 * unit.t_mm), + "A0b" : (910 * unit.t_mm, 1370 * unit.t_mm), + "Letter" : (8.5 * unit.t_inch, 11 * unit.t_inch), + "Legal" : (8.5 * unit.t_inch, 14 * unit.t_inch)} # # clipping class @@ -165,7 +172,7 @@ class _canvas(base.PSCmd): def insert(self, PSOp, attrs=[]): """insert PSOp in the canvas. - If args are given, then insert a canvas containing PSOp applying args. + If attrss are given, a canvas containing the PSOp is inserted applying attrs. returns the PSOp @@ -330,6 +337,51 @@ class pattern(_canvas, attr.exclusiveattr, style.fillstyle): pattern.clear = attr.clearclass(pattern) +# helper function + +def calctrafo(abbox, paperformat, margin, rotated, fittosize): + """ calculate a trafo which rotates and fits a canvas with + bounding box abbox on the given paperformat with a margin on all + sides""" + margin = unit.length(margin) + atrafo = None # global transformation of canvas + + if rotated: + atrafo = trafo.rotate(90, *abbox.center()) + + if paperformat: + # center (optionally rotated) output on page + try: + paperwidth, paperheight = _paperformats[paperformat.capitalize()] + except KeyError: + raise KeyError, "unknown paperformat '%s'" % paperformat + + paperwidth -= 2*margin + paperheight -= 2*margin + + if not atrafo: atrafo = trafo.trafo() + + atrafo = atrafo.translated(margin + 0.5*(paperwidth - abbox.width()) - abbox.left(), + margin + 0.5*(paperheight - abbox.height()) - abbox.bottom()) + + if fittosize: + # scale output to pagesize - margins + if 2*margin > min(paperwidth, paperheight): + raise RuntimeError("Margins too broad for selected paperformat. Aborting.") + + if rotated: + sfactor = min(unit.topt(paperheight)/unit.topt(abbox.width()), + unit.topt(paperwidth)/unit.topt(abbox.height())) + else: + sfactor = min(unit.topt(paperwidth)/unit.topt(abbox.width()), + unit.topt(paperheight)/unit.topt(abbox.height())) + + atrafo = atrafo.scaled(sfactor, sfactor, margin + 0.5*paperwidth, margin + 0.5*paperheight) + elif fittosize: + raise ValueError("must specify paper size for fittosize") + + return atrafo + # # The main canvas class # @@ -366,48 +418,8 @@ class canvas(_canvas): abbox = bbox is not None and bbox or self.bbox() abbox.enlarge(bboxenlarge) - ctrafo = None # global transformation of canvas - - if rotated: - ctrafo = trafo.rotate_pt(90, - 0.5*(abbox.llx_pt+abbox.urx_pt), - 0.5*(abbox.lly_pt+abbox.ury_pt)) - - if paperformat: - # center (optionally rotated) output on page - try: - width, height = _paperformats[paperformat.capitalize()] - except KeyError: - raise KeyError, "unknown paperformat '%s'" % paperformat - width = unit.topt(width) - height = unit.topt(height) - - if not ctrafo: ctrafo=trafo.trafo() - - ctrafo = ctrafo.translated_pt(0.5*(width -(abbox.urx_pt-abbox.llx_pt))- - abbox.llx_pt, - 0.5*(height-(abbox.ury_pt-abbox.lly_pt))- - abbox.lly_pt) - - if fittosize: - # scale output to pagesize - margins - margin = unit.topt(margin) - if 2*margin > min(width, height): - raise RuntimeError("Margins too broad for selected paperformat. Aborting.") - - if rotated: - sfactor = min((height-2*margin)/(abbox.urx_pt-abbox.llx_pt), - (width-2*margin)/(abbox.ury_pt-abbox.lly_pt)) - else: - sfactor = min((width-2*margin)/(abbox.urx_pt-abbox.llx_pt), - (height-2*margin)/(abbox.ury_pt-abbox.lly_pt)) - - ctrafo = ctrafo.scaled_pt(sfactor, sfactor, 0.5*width, 0.5*height) - - - elif fittosize: - raise ValueError("must specify paper size for fittosize") - + ctrafo = calctrafo(abbox, paperformat, margin, rotated, fittosize) + # if there has been a global transformation, adjust the bounding box # accordingly if ctrafo: abbox.transform(ctrafo) @@ -435,7 +447,7 @@ class canvas(_canvas): file.write("%%EndProlog\n") - # again, if there has occured global transformation, apply it now + # apply a possible global transformation if ctrafo: ctrafo.outputPS(file) file.write("%f setlinewidth\n" % unit.topt(style.linewidth.normal)) @@ -447,7 +459,8 @@ class canvas(_canvas): file.write("%%Trailer\n") file.write("%%EOF\n") - def writePDFfile(self, filename, bbox=None, bboxenlarge="1 t pt"): + def writePDFfile(self, filename, paperformat=None, rotated=0, fittosize=0, margin="1 t cm", + bbox=None, bboxenlarge="1 t pt"): sys.stderr.write("*** PyX Warning: writePDFfile is experimental and supports only a subset of PyX's features\n") if filename[-4:]!=".pdf": @@ -461,6 +474,20 @@ class canvas(_canvas): abbox = bbox is not None and bbox or self.bbox() abbox.enlarge(bboxenlarge) + ctrafo = calctrafo(abbox, paperformat, margin, rotated, fittosize) + + # if there has been a global transformation, adjust the bounding box + # accordingly + if ctrafo: abbox.transform(ctrafo) + + mergedprolog = [] + + for pritem in self.prolog(): + for mpritem in mergedprolog: + if mpritem.merge(pritem) is None: break + else: + mergedprolog.append(pritem) + file.write("%%PDF-1.4\n%%%s%s%s%s\n" % (chr(195), chr(182), chr(195), chr(169))) reflist = [file.tell()] file.write("1 0 obj\n" @@ -492,7 +519,16 @@ class canvas(_canvas): "/Parent 3 0 R\n") abbox.outputPDF(file) file.write("/Contents 5 0 R\n" - "/Resources << /ProcSet 7 0 R >>\n" + "/Resources <<\n" + "/ProcSet 7 0 R\n") + + fontnr = 0 + for pritem in mergedprolog: + if isinstance(pritem, prolog.fontreencoding): + fontnr += 1 + file.write("/Font << /%s %d 0 R>>\n" % (pritem.fontname, fontnr+7)) + + file.write(">>\n" ">>\n" "endobj\n") reflist.append(file.tell()) @@ -500,7 +536,11 @@ class canvas(_canvas): "<< /Length 6 0 R >>\n" "stream\n") streamstartpos = file.tell() + + # apply a possible global transformation + if ctrafo: ctrafo.outputPDF(file) style.linewidth.normal.outputPDF(file) + self.outputPDF(file) streamendpos = file.tell() file.write("endstream\n" @@ -511,11 +551,27 @@ class canvas(_canvas): "endobj\n" % (streamendpos - streamstartpos)) reflist.append(file.tell()) file.write("7 0 obj\n" - "[/PDF]\n" + "[/PDF /Text]\n" "endobj\n") + + fontnr = 0 + for pritem in mergedprolog: + if isinstance(pritem, prolog.fontreencoding): + fontnr += 1 + reflist.append(file.tell()) + file.write("%d 0 obj\n" + "<<\n" + "/Type /Font\n" + "/Subtype /Type1\n" + "/Name /%s\n" + "/BaseFont /Helvetica\n" + "/Encoding /MacRomanEncoding\n" + ">>\n" + "endobj\n" % (fontnr+7, pritem.fontname)) + xrefpos = file.tell() file.write("xref\n" - "0 8\n") + "0 %d\n" % (len(reflist)+1)) file.write("0000000000 65535 f \n") for ref in reflist: file.write("%010i 00000 n \n" % ref) @@ -539,21 +595,52 @@ class canvas(_canvas): class page(canvas): - def __init__(self, attrs=[], texrunner=None, pagename=None, paperformat="a4", rotated=0, fittosize=0, margin="1 t cm"): + def __init__(self, attrs=[], texrunner=None, pagename=None, paperformat="a4", rotated=0, fittosize=0, + margin="1 t cm", bboxenlarge="1 t pt"): canvas.__init__(self, attrs, texrunner) self.pagename = pagename self.paperformat = paperformat.capitalize() self.rotated = rotated self.fittosize = fittosize self.margin = margin + self.bboxenlarge = bboxenlarge def bbox(self): - return bbox.bbox(0, 0, *_paperformats[self.paperformat]) + # the bounding box of a page is fixed by its format and an optional rotation + pbbox = bbox.bbox(0, 0, *_paperformats[self.paperformat]) + pbbox.enlarge(self.bboxenlarge) + if self.rotated: + pbbox.transform(trafo.rotate(90, *pbbox.center())) + return pbbox + + def outputPS(self, file): + file.write("%%%%PageMedia: %s\n" % self.paperformat) + file.write("%%%%PageOrientation: %s\n" % (self.rotated and "Landscape" or "Portrait")) + # file.write("%%%%PageBoundingBox: %d %d %d %d\n" % (math.floor(pbbox.llx_pt), math.floor(pbbox.lly_pt), + # math.ceil(pbbox.urx_pt), math.ceil(pbbox.ury_pt))) + + # page setup section + file.write("%%BeginPageSetup\n") + file.write("/pgsave save def\n") + # for scaling, we need the real bounding box of the page contents + pbbox = canvas.bbox(self) + pbbox.enlarge(self.bboxenlarge) + ptrafo = calctrafo(pbbox, self.paperformat, self.margin, self.rotated, self.fittosize) + if ptrafo: + ptrafo.outputPS(file) + file.write("%f setlinewidth\n" % unit.topt(style.linewidth.normal)) + file.write("%%EndPageSetup\n") + + # here comes the actual content + canvas.outputPS(self, file) + file.write("pgsave restore\n") + file.write("showpage\n") + # file.write("%%PageTrailer\n") class document: - """holds a collection of page and canvas instances which are output as pages of a document""" + """holds a collection of page instances which are output as pages of a document""" def __init__(self, pages=[]): self.pages = pages @@ -561,23 +648,8 @@ class document: def append(self, page): self.pages.append(page) - def writePSfile(self, filename, paperformat=None, rotated=0, - bbox=None, bboxenlarge="1 t pt"): - """write pages to PS file - - If paperformat is set to a known paperformat, the output will be centered on - the page. - - If rotated is set, the output will first be rotated by 90 degrees. - - If fittosize is set, then the output is scaled to the size of the - page (minus margin). In that case, the paperformat the specification - of the paperformat is obligatory. - - The bbox parameter overrides the automatic bounding box determination. - bboxenlarge may be used to enlarge the bbox of the canvas (or the - manually specified bbox). - """ + def writePSfile(self, filename): + """write pages to PS file """ if filename[-3:]!=".ps": filename = filename + ".ps" @@ -587,47 +659,13 @@ class document: except IOError: raise IOError("cannot open output file") - bboxes = [c.bbox().enlarged(bboxenlarge) for c in self.pages] - if bbox is None: - docbbox = None - for abbox in bboxes: - if docbbox is None: - docbbox = abbox - else: - docbbox += abbox - else: - docbbox = bbox - ctrafo = None # global transformation of canvas - - if rotated: - ctrafo = trafo.rotate_pt(90, - 0.5*(docbbox.llx_pt+docbbox.urx_pt), - 0.5*(docbbox.lly_pt+docbbox.ury_pt)) - - if paperformat: - # center (optionally rotated) output on page - try: - width, height = _paperformats[paperformat.capitalize()] - except KeyError: - raise KeyError, "unknown paperformat '%s'" % paperformat - width = unit.topt(width) - height = unit.topt(height) - - if not ctrafo: ctrafo = trafo.trafo() - - ctrafo = ctrafo.translated_pt(0.5*(width -(docbbox.urx_pt-docbbox.llx_pt))- - docbbox.llx_pt, - 0.5*(height-(docbbox.ury_pt-docbbox.lly_pt))- - docbbox.lly_pt) - - # if there has been a global transformation, adjust the bounding box - # accordingly - if ctrafo: - docbbox.transform(ctrafo) - for abbox in bboxes: - print abbox - abbox.transform(ctrafo) - print abbox + docbbox = None + for apage in self.pages: + pbbox = apage.bbox() + if docbbox is None: + docbbox = pbbox + else: + docbbox += pbbox # document header file.write("%!PS-Adobe-3.0\n") @@ -676,32 +714,12 @@ class document: # document setup section #file.write("%%BeginSetup\n") - #file.write("%%%%PaperSize: %s\n" % "A4") #file.write("%%EndSetup\n") # pages section - for apage, abbox, nr in zip(self.pages, bboxes, range(1, len(self.pages)+1)): - if isinstance(apage, page): - file.write("%%%%Page: %s %d\n" % (apage.pagename is None and str(nr) or apage.pagename , nr)) - file.write("%%%%PageMedia: %s\n" % apage.paperformat) - file.write("%%%%PageOrientation: %s\n" % (apage.rotated and "Landscape" or "Portrait")) - else: - file.write("%%%%Page: %d %d\n" % (nr , nr)) - file.write("%%%%PageBoundingBox: %d %d %d %d\n" % (math.floor(abbox.llx_pt), math.floor(abbox.lly_pt), - math.ceil(abbox.urx_pt), math.ceil(abbox.ury_pt))) - - # page setup section - file.write("%%BeginPageSetup\n") - file.write("/pgsave save def\n") - if ctrafo: ctrafo.outputPS(file) - file.write("%f setlinewidth\n" % unit.topt(style.linewidth.normal)) - file.write("%%EndPageSetup\n") - - # here comes the actual content + for nr, apage in enumerate(self.pages): + file.write("%%%%Page: %s %d\n" % (apage.pagename is None and str(nr) or apage.pagename , nr+1)) apage.outputPS(file) - file.write("pgsave restore\n") - file.write("showpage\n") - # file.write("%%PageTrailer\n") file.write("%%Trailer\n") file.write("%%EOF\n") diff --git a/pyx/dvifile.py b/pyx/dvifile.py index c66f5f77..ef8d9a15 100644 --- a/pyx/dvifile.py +++ b/pyx/dvifile.py @@ -401,6 +401,22 @@ _ReEncodeFont = prolog.definition("ReEncodeFont", """{ # PostScript font selection and output primitives # +class _begintextobject(base.PSOp): + def outputPS(self, file): + pass + + def outputPDF(self, file): + file.write("BT\n") + + +class _endtextobject(base.PSOp): + def outputPS(self, file): + pass + + def outputPDF(self, file): + file.write("ET\n") + + class _selectfont(base.PSOp): def __init__(self, name, size): self.name = name @@ -409,6 +425,9 @@ class _selectfont(base.PSOp): def outputPS(self, file): file.write("/%s %f selectfont\n" % (self.name, self.size)) + def outputPDF(self, file): + file.write("/%s %f Tf\n" % (self.name, self.size)) + class selectfont(_selectfont): def __init__(self, font): @@ -446,7 +465,7 @@ class _show(base.PSCmd): self.chars.append(char) def bbox(self): - return bbox._bbox(self.x, self.y-self.depth, self.x+self.width, self.y+self.height) + return bbox.bbox_pt(self.x, self.y-self.depth, self.x+self.width, self.y+self.height) def outputPS(self, file): outstring = "" @@ -456,7 +475,18 @@ class _show(base.PSCmd): else: ascii = "\\%03o" % char outstring += ascii - file.write("%f %f moveto (%s) show\n" % (self.x, self.y, outstring)) + file.write("%g %g moveto (%s) show\n" % (self.x, self.y, outstring)) + + def outputPDF(self, file): + outstring = "" + for char in self.chars: + if char > 32 and char < 127 and chr(char) not in "()[]<>\\": + ascii = "%s" % chr(char) + else: + ascii = "\\%03o" % char + outstring += ascii + # file.write("%f %f Td (%s) Tj\n" % (self.x, self.y, outstring)) + file.write("1 0 0 1 %f %f Tm (%s) Tj\n" % (self.x, self.y, outstring)) class fontmapping: @@ -765,9 +795,10 @@ class dvifile: # pointer to currently active page self.actpage = None - # currently active output: show instance being filled and actoutfont + # currently active output: show instance currently used and + # the corresponding type 1 font self.activeshow = None - self.actoutfont = None + self.activetype1font = None # stack for self.file, self.fonts and self.stack, needed for VF inclusion self.statestack = [] @@ -789,8 +820,24 @@ class dvifile: self.actpage.insert(self.activeshow) self.activeshow = None - def putrule(self, height, width, advancepos=1): + def begintext(self): + """ activate the font if is not yet active, closing a currently active + text object and flushing the output""" + if isinstance(self.activefont, type1font): + self.endtext() + if self.activetype1font != self.activefont and self.activefont: + self.actpage.insert(_begintextobject()) + self.actpage.insert(selectfont(self.activefont)) + self.activetype1font = self.activefont + + def endtext(self): self.flushout() + if self.activetype1font: + self.actpage.insert(_endtextobject()) + self.activetype1font = None + + def putrule(self, height, width, advancepos=1): + self.endtext() x1 = self.pos[_POS_H] * self.conv y1 = -self.pos[_POS_V] * self.conv w = width * self.conv @@ -857,14 +904,7 @@ class dvifile: fontnum, self.fonts[fontnum].name)) self.activefont = self.fonts[fontnum] - - # if the new font is a type 1 font and if it is not already the - # font being currently used for the output, we have to flush the - # output and insert a selectfont statement in the PS code - if isinstance(self.activefont, type1font) and self.actoutfont!=self.activefont: - self.flushout() - self.actpage.insert(selectfont(self.activefont)) - self.actoutfont = self.activefont + self.begintext() def definefont(self, cmdnr, num, c, q, d, fontname): # cmdnr: type of fontdef command (only used for debugging output) @@ -892,13 +932,6 @@ class dvifile: # print " (this font is magnified %d%%)" % round(scale/10) def special(self, s): - self.flushout() - - # it is in general not safe to continue using the currently active font because - # the specials may involve some gsave/grestore operations - # XXX: reset actoutfont only where strictly needed - self.actoutfont = None - x = self.pos[_POS_H] * self.conv y = -self.pos[_POS_V] * self.conv if self.debug: @@ -909,6 +942,11 @@ class dvifile: return else: raise RuntimeError("the special '%s' cannot be handled by PyX, aborting" % s) + + # it is in general not safe to continue using the currently active font because + # the specials may involve some gsave/grestore operations + self.endtext() + command, args = s[4:].split()[0], s[4:].split()[1:] if command=="color_begin": if args[0]=="cmyk": @@ -952,7 +990,7 @@ class dvifile: # construct kwargs for epsfile constructor epskwargs = {} epskwargs["filename"] = argdict["file"] - epskwargs["bbox"] = bbox._bbox(float(argdict["llx"]), float(argdict["lly"]), + epskwargs["bbox"] = bbox.bbox_pt(float(argdict["llx"]), float(argdict["lly"]), float(argdict["urx"]), float(argdict["ury"])) if argdict.has_key("width"): epskwargs["width"] = float(argdict["width"]) * unit.t_pt @@ -972,6 +1010,7 @@ class dvifile: self.actpage.markers[args[0]] = x * unit.t_pt, y * unit.t_pt else: raise RuntimeError("unknown PyX special '%s', aborting" % command) + self.begintext() # routines for pushing and popping different dvi chunks on the reader @@ -1070,7 +1109,7 @@ class dvifile: if cmd == _DVI_NOP: pass elif cmd == _DVI_BOP: - self.flushout() + # self.endtext() ispageid = [self.file.readuint32() for i in range(10)] #if ispageid[:3] != [ord("P"), ord("y"), ord("X")] or ispageid[4:] != [0, 0, 0, 0, 0, 0]: if pageid is not None and ispageid != pageid: @@ -1089,7 +1128,7 @@ class dvifile: self.actpage = actpage # XXX should be removed ... self.actpage.markers = {} self.pos = [0, 0, 0, 0, 0, 0] - self.actoutfont = None + self.activetype1font = None # Since we do not know which dvi pages the actual PS file contains later on, # we have to keep track of used char informations separately for each dvi page. @@ -1122,7 +1161,7 @@ class dvifile: elif cmd == _DVI_PUTRULE: self.putrule(afile.readint32(), afile.readint32(), 0) elif cmd == _DVI_EOP: - self.flushout() + self.endtext() if self.debug: print "%d: eop" % self.filepos print diff --git a/pyx/prolog.py b/pyx/prolog.py index 88f642cf..fd9c3c5c 100644 --- a/pyx/prolog.py +++ b/pyx/prolog.py @@ -82,10 +82,10 @@ class fontdefinition(prologitem): """ PostScript font definition included in the prolog """ - def __init__(self, name, filename, encfilename, usedchars): + def __init__(self, fontname, filename, encfilename, usedchars): """ include type 1 font defined by the following parameters - - name: PostScript FontName of font + - fontname: PostScript FontName of font - filename: name (without path) of file containing the font definition - encfilename: name (without path) of file containing used encoding of font or None (if no encoding file used) @@ -95,7 +95,7 @@ class fontdefinition(prologitem): # Note that here we only need the encoding for selecting the used glyphs! - self.name = name + self.fontname = fontname self.filename = filename self.encfilename = encfilename self.usedchars = usedchars @@ -103,7 +103,7 @@ class fontdefinition(prologitem): def merge(self, other): if not isinstance(other, fontdefinition): return other - if self.name==other.name and self.encfilename==other.encfilename: + if self.fontname==other.fontname and self.encfilename==other.encfilename: for i in range(len(self.usedchars)): self.usedchars[i] = self.usedchars[i] or other.usedchars[i] return None @@ -112,7 +112,7 @@ class fontdefinition(prologitem): def outputPS(self, file): if self.filename: - file.write("%%%%BeginFont: %s\n" % self.name) + file.write("%%%%BeginFont: %s\n" % self.fontname) file.write("%Included char codes:") for i in range(len(self.usedchars)): if self.usedchars[i]: diff --git a/pyx/text.py b/pyx/text.py index 55f20325..4537b405 100644 --- a/pyx/text.py +++ b/pyx/text.py @@ -649,6 +649,10 @@ class textbox(box.rect, canvas._canvas): self.ensuredvicanvas() canvas._canvas.outputPS(self, file) + def outputPDF(self, file): + self.ensuredvicanvas() + canvas._canvas.outputPDF(self, file) + def _cleantmp(texrunner): """get rid of temporary files -- 2.11.4.GIT