Files
rdkit/rdkit/sping/PDF/pdfgen.py
gedeck bb71cd430d Remove deprecated string module functions (#1223)
* 1194: Review assignments of range in Python code

Task-Url: https://github.com/rdkit/rdkit/issues/1194
Either wrapped the range expression into a list or made sure that the
code is working with a range object.

* Removed use of deprecated string module functions
2017-02-05 08:17:45 +01:00

1044 lines
38 KiB
Python
Executable File

## Automatically adapted for numpy.oldnumeric Jun 27, 2008 by -c
#pdfgen.py
"""
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.
Documentation is a little slim right now; run then look at testpdfgen.py
to get a clue.
---------- Licence Terms (same as the Python license) -----------------
(C) Copyright Robinson Analytics 1998-1999.
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted, provided
that the above copyright notice appear in all copies and that both that
copyright notice and this permission notice appear in supporting
documentation, 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.
ROBINSON ANALYTICS LTD. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS,
IN NO EVENT SHALL ROBINSON ANALYTICS BE LIABLE FOR ANY SPECIAL, INDIRECT
OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
Progress Reports:
0.82, 1999-10-27, AR:
Fixed some bugs on printing to Postscript. Added 'Text Object'
analogous to Path Object to control entry and exit from text mode.
Much simpler clipping API. All verified to export Postscript and
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.
0.8,1999-10-07, AR: all changed!
2000-02-07: changed all %0.2f's to %0.4f in order to allow precise ploting of graphs that
range between 0 and 1 -cwl
"""
## 0.81 1999-10-13:
##
##
##
from __future__ import print_function
import os
import sys
import time
import tempfile
from io import StringIO
from types import *
from math import sin, cos, tan, pi, ceil
from . import pdfutils
from . import pdfdoc
from . import pdfmetrics
from . import pdfgeom
from rdkit.six import string_types
class PDFError(ValueError):
pass
# Robert Kern
# Constants for closing paths.
# May be useful if one changes 'arc' and 'rect' to take a
# default argument that tells how to close the path.
# That way we can draw filled shapes.
FILL_EVEN_ODD = 0
FILL_NON_ZERO = 1
#this is used by path-closing routines.
#map stroke, fill, fillmode -> operator
# fillmode: 1 = non-Zero (obviously), 0 = evenOdd
PATH_OPS = {(0, 0, FILL_EVEN_ODD): 'n', #no op
(0, 0, FILL_NON_ZERO): 'n', #no op
(1, 0, FILL_EVEN_ODD): 'S', #stroke only
(1, 0, FILL_NON_ZERO): 'S', #stroke only
(0, 1, FILL_EVEN_ODD): 'f*', #Fill only
(0, 1, FILL_NON_ZERO): 'f', #Fill only
(1, 1, FILL_EVEN_ODD): 'B*', #Stroke and Fill
(1, 1, FILL_NON_ZERO): 'B', #Stroke and Fill
}
close = 'h'
newpath = 'n'
stroke = 'S'
closeStroke = 's'
nzFill = 'f'
eoFill = 'f*'
fillStroke = 'B'
closeFillStroke = 'b'
eoFillStroke = 'B*'
closeEoFillStroke = 'b*'
class Canvas:
"""This is a low-level interface to the PDF file format. The plan is to
expose the whole pdfgen API through this. Its drawing functions should have a
one-to-one correspondence with PDF functionality. Unlike PIDDLE, it thinks
in terms of RGB values, Postscript font names, paths, and a 'current graphics
state'. Just started development at 5/9/99, not in use yet.
"""
def __init__(self, filename, pagesize=(595.27, 841.89), bottomup=1):
"""Most of the attributes are private - we will use set/get methods
as the preferred interface. Default page size is A4."""
self._filename = filename
self._doc = pdfdoc.PDFDocument()
self._pagesize = pagesize
self._currentPageHasImages = 1
self._pageTransitionString = ''
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.
self.bottomup = bottomup
if self.bottomup:
#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]
#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._leading = 14.4
self._currentMatrix = (1., 0., 0., 1., 0., 0.)
self._fillMode = 0 #even-odd
#text state
self._charSpace = 0
self._wordSpace = 0
self._horizScale = 100
self._textRenderMode = 0
self._rise = 0
self._textLineMatrix = (1., 0., 0., 1., 0., 0.)
self._textMatrix = (1., 0., 0., 1., 0., 0.)
# line drawing
self._lineCap = 0
self._lineJoin = 0
self._lineDash = None #not done
self._lineWidth = 0
self._mitreLimit = 0
self._fillColorRGB = (0, 0, 0)
self._strokeColorRGB = (0, 0, 0)
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 = s.replace('(', '\(')
s = s.replace(')', '\)')
return s
#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()
page.pagewidth = self._pagesize[0]
page.pageheight = self._pagesize[1]
page.hasImages = self._currentPageHasImages
page.pageTransitionString = self._pageTransitionString
page.setCompression(self._pageCompression)
#print stream
page.setStream([self._preamble] + self._code)
self._doc.addPage(page)
#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 fileobj:
self._doc.SaveToFileObject(fileobj)
elif filename:
self._doc.SaveToFile(filename)
else:
self._doc.SaveToFile(self._filename)
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:
s = self._escape(s)
self._code.append(s)
######################################################################
#
# coordinate transformations
#
######################################################################
def transform(self, a, b, c, d, e, f):
"""How can Python track this?"""
a0, b0, c0, d0, e0, f0 = self._currentMatrix
self._currentMatrix = (a0 * a + c0 * b, b0 * a + d0 * b, a0 * c + c0 * d, b0 * c + d0 * d,
a0 * e + c0 * f + e0, b0 * e + d0 * f + f0)
self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f cm' % (a, b, c, d, e, f))
def translate(self, dx, dy):
self.transform(1, 0, 0, 1, dx, dy)
def scale(self, x, y):
self.transform(x, 0, 0, y, 0, 0)
def rotate(self, theta):
"""Canvas.rotate(theta)
theta is in degrees."""
c = cos(theta * pi / 180)
s = sin(theta * pi / 180)
self.transform(c, s, -s, c, 0, 0)
def skew(self, alpha, beta):
tanAlpha = tan(alpha * pi / 180)
tanBeta = tan(beta * pi / 180)
self.transform(1, tanAlpha, tanBeta, 1, 0, 0)
######################################################################
#
# graphics state management
#
######################################################################
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')
###############################################################
#
# Drawing methods. These draw things directly without
# fiddling around with Path objects. We can add any geometry
# methods we wish as long as their meaning is precise and
# they are of general use.
#
# In general there are two patterns. Closed shapes
# have the pattern shape(self, args, stroke=1, fill=0);
# by default they draw an outline only. Line segments come
# in three flavours: line, bezier, arc (which is a segment
# of an elliptical arc, approximated by up to four bezier
# curves, one for each quadrant.
#
# In the case of lines, we provide a 'plural' to unroll
# the inner loop; it is useful for drawing big grids
################################################################
#--------first the line drawing methods-----------------------
def line(self, x1, y1, x2, y2):
"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):
"""As line(), but slightly more efficient for lots of them -
one stroke operation and one less function call"""
self._code.append('n')
for (x1, y1, x2, y2) in linelist:
self._code.append('%0.4f %0.4f m %0.4f %0.4f l' % (x1, y1, x2, y2))
self._code.append('S')
def grid(self, xlist, ylist):
"""Lays out a grid in current line style. Suuply list of
x an y positions."""
assert len(xlist) > 1, "x coordinate list must have 2+ items"
assert len(ylist) > 1, "y coordinate list must have 2+ items"
lines = []
y0, y1 = ylist[0], ylist[-1]
x0, x1 = xlist[0], xlist[-1]
for x in xlist:
lines.append(x, y0, x, y1)
for y in ylist:
lines.append(x0, y, x1, y)
self.lines(lines)
def bezier(self, x1, y1, x2, y2, x3, y3, x4, y4):
"Bezier curve with the four given control points"
self._code.append('n %0.4f %0.4f m %0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c S' %
(x1, y1, x2, y2, x3, y3, x4, y4))
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.
These should have x1<x2 and y1<y2.
The algorithm is an elliptical generalization of the formulae in
Jim Fitzsimmon's TeX tutorial <URL: http://www.tinaja.com/bezarc1.pdf>."""
pointList = pdfgeom.bezierArc(x1, y1, x2, y2, startAng, extent)
#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:])
# stroke
self._code.append('S')
#--------now the shape drawing methods-----------------------
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"""
pointList = pdfgeom.bezierArc(x1, y1, x2, y2, 0, 360)
#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:])
#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!"""
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])
for curve in pointList:
self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' % curve[2:])
# finish the wedge
self._code.append('%0.4f %0.4f l ' % (x_cen, y_cen))
# final operator
self._code.append(PATH_OPS[stroke, fill, self._fillMode])
def circle(self, x_cen, y_cen, r, stroke=1, fill=0):
"""special case of ellipse"""
x1 = x_cen - r
x2 = x_cen + r
y1 = y_cen - r
y2 = y_cen + r
self.ellipse(x1, y1, x2, y2, stroke, fill)
def roundRect(self, x, y, width, height, radius, stroke=1, fill=0):
"""Draws a rectangle with rounded corners. The corners are
approximately quadrants of a circle, with the given radius."""
#use a precomputed set of factors for the bezier approximation
#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
x3 = x0 + width - radius
x4 = x0 + width - t
x5 = x0 + width
y0 = y
y1 = y0 + t
y2 = y0 + radius
y3 = y0 + height - radius
y4 = y0 + height - t
y5 = y0 + height
self._code.append('n %0.4f %0.4f m' % (x2, y0))
self._code.append('%0.4f %0.4f l' % (x3, y0)) # bottom row
self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' %
(x4, y0, x5, y1, x5, y2)) # bottom right
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])
##################################################
#
# Text methods
#
# As with graphics, a separate object ensures that
# everything is bracketed between text operators.
# The methods below are a high-level convenience.
# use PDFTextObject for multi-line text.
##################################################
def drawString(self, x, y, text):
"""Draws a string in the current text styles."""
#we could inline this for speed if needed
t = self.beginText(x, y)
t.textLine(text)
self.drawText(t)
def drawRightString(self, x, y, text):
"""Draws a string right-aligned with the y coordinate"""
width = self.stringWidth(text, self._fontname, self._fontsize)
t = self.beginText(x - width, y)
t.textLine(text)
self.drawText(t)
def drawCentredString(self, x, y, text):
"""Draws a string right-aligned with the y coordinate. I
am British so the spelling is correct, OK?"""
width = self.stringWidth(text, self._fontname, self._fontsize)
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."""
fontnames = self._doc.getAvailableFonts()
fontnames.sort()
return fontnames
def setFont(self, psfontname, size, leading=None):
"""Sets the font. If leading not specified, defaults to 1.2 x
font size. Raises a readable exception if an illegal font
is supplied. Font names are case-sensitive! Keeps track
of font anme and size for metrics."""
self._fontname = psfontname
self._fontsize = size
pdffontname = self._doc.getInternalFontName(psfontname)
if leading is None:
leading = size * 1.2
self._leading = leading
self._code.append('BT %s %0.1f Tf %0.1f TL ET' % (pdffontname, size, leading))
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
self._code.append('%0.4f w' % width)
def setLineCap(self, mode):
"""0=butt,1=round,2=square"""
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 isinstance(array, (int, float)):
self._code.append('[%s %s] 0 d' % (array, phase))
elif isinstance(array, (list, tuple)):
assert phase <= len(array), "setDash phase must be l.t.e. length of array"
textarray = ' '.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
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]
self._code.append(aPath.getCode() + ' ' + op)
def clipPath(self, aPath, stroke=1, fill=0):
"clip as well as drawing"
op = PATH_OPS[stroke, fill, self._fillMode]
self._code.append(aPath.getCode() + ' W ' + op)
def beginText(self, x=0, y=0):
"""Returns a fresh text object"""
return PDFTextObject(self, x, y)
def drawText(self, aTextObject):
"""Draws a text object"""
self._code.append(aTextObject.getCode())
######################################################
#
# Image routines
#
######################################################
def drawInlineImage(self, image, x, y, width=None, height=None):
"""Draw a PIL Image into the specified rectangle. If width and
height are omitted, they are calculated from the image size.
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:
from PIL import Image
except ImportError:
print('Python Imaging Library not available')
return
try:
import zlib
except ImportError:
print('zlib not available')
return
self._currentPageHasImages = 1
if isinstance(image, string_types):
if os.path.splitext(image)[1] in ['.jpg', '.JPG']:
#directly process JPEG files
#open file, needs some error handling!!
imageFile = open(image, 'rb')
info = self.readJPEGInfo(imageFile)
imgwidth, imgheight = info[0], info[1]
if info[2] == 1:
colorSpace = 'DeviceGray'
elif info[2] == 3:
colorSpace = 'DeviceRGB'
else: #maybe should generate an error, is this right for CMYK?
colorSpace = 'DeviceCMYK'
imageFile.seek(0) #reset file pointer
imagedata = []
imagedata.append('BI') # begin image
# this describes what is in the image itself
imagedata.append('/Width %0.4f /Height %0.4f' % (info[0], info[1]))
imagedata.append('/BitsPerComponent 8')
imagedata.append('/ColorSpace /%s' % colorSpace)
imagedata.append('/Filter [ /ASCII85Decode /DCTDecode]')
imagedata.append('ID')
#write in blocks of (??) 60 characters per line to a list
compressed = imageFile.read()
encoded = pdfutils._AsciiBase85Encode(compressed)
outstream = StringIO(encoded)
dataline = outstream.read(60)
while dataline != "":
imagedata.append(dataline)
dataline = outstream.read(60)
imagedata.append('EI')
else:
if not pdfutils.cachedImageExists(image):
pdfutils.cacheImageFile(image)
#now we have one cached, slurp it in
cachedname = os.path.splitext(image)[0] + '.a85'
imagedata = open(cachedname, 'rb').readlines()
#trim off newlines...
imagedata = [s.strip() for s in imagedata]
#parse line two for width, height
words = imagedata[1].split()
imgwidth = int(words[1])
imgheight = int(words[3])
else:
#PIL Image
#work out all dimensions
myimage = image.convert('RGB')
imgwidth, imgheight = myimage.size
imagedata = []
imagedata.append('BI') # begin image
# 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')
#use a flate filter and Ascii Base 85 to compress
raw = getattr(myimage, 'tobytes', myimage.tostring)()
assert len(raw) == imgwidth * imgheight, "Wrong amount of data for image"
compressed = zlib.compress(raw) #this bit is very fast...
encoded = pdfutils._AsciiBase85Encode(compressed) #...sadly this isn't
#write in blocks of (??) 60 characters per line to a list
outstream = StringIO(encoded)
dataline = outstream.read(60)
while dataline != "":
imagedata.append(dataline)
dataline = outstream.read(60)
imagedata.append('EI')
#now build the PDF for the image.
if not width:
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))
if self.bottomup:
self._code.append('q %0.4f 0 0 %0.4f %0.4f %0.4f cm' % (width, height, x, y))
else:
# multiply height by (-1) to overcome flip in coordinate system -cwl
self._code.append('q %0.4f 0 0 %0.4f %0.4f %0.4f cm' % (width, -height, x, y + height))
self._code.extend(imagedata)
self._code.append('Q')
#self._code.append('BT')
#########################################################################
#
# JPEG processing code - contributed by Eric Johnson
#
#########################################################################
# Read data from the JPEG file. We should probably be using PIL to
# get this information for us -- but this way is more fun!
# Returns (width, height, color components) as a triple
# This is based on Thomas Merz's code from GhostScript (viewjpeg.ps)
def readJPEGInfo(self, image):
"Read width, height and number of components from JPEG file"
import struct
#Acceptable JPEG Markers:
# SROF0=baseline, SOF1=extended sequential or SOF2=progressive
validMarkers = [0xC0, 0xC1, 0xC2]
#JPEG markers without additional parameters
noParamMarkers = \
[ 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0x01 ]
#Unsupported JPEG Markers
unsupportedMarkers = \
[ 0xC3, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF ]
#read JPEG marker segments until we find SOFn marker or EOF
done = 0
while not done:
x = struct.unpack('B', image.read(1))
if x[0] == 0xFF: #found marker
x = struct.unpack('B', image.read(1))
#print "Marker: ", '%0.2x' % x[0]
#check marker type is acceptable and process it
if x[0] in validMarkers:
image.seek(2, 1) #skip segment length
x = struct.unpack('B', image.read(1)) #data precision
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]
y = struct.unpack('BB', image.read(2))
width = (y[0] << 8) + y[1]
y = struct.unpack('B', image.read(1))
color = y[0]
return width, height, color
done = 1
elif x[0] in unsupportedMarkers:
raise PDFError(' Unsupported JPEG marker: {%0.2x}'.format(x[0]))
elif x[0] not in noParamMarkers:
#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)
def setPageCompression(self, onoff=1):
"""Possible values 1 or 0 (1 for 'on' is the default).
If on, the page data will be compressed, leading to much
smaller files, but takes a little longer to create the files.
This applies to all subsequent pages, or until setPageCompression()
is next called."""
self._pageCompression = onoff
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
just guive the effect name, or supply more advanced options
to refine the way it works. There are three types of extra
argument permitted, and here are the allowed values:
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 = {
'Split': [direction_arg, motion_arg],
'Blinds': [dimension_arg],
'Box': [motion_arg],
'Wipe' : [direction_arg],
'Dissolve' : [],
'Glitter':[direction_arg]
}
Have fun!
"""
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:
raise PDFError('motion values allowed are I and O')
# this says which effects require which argument types from above
PageTransitionEffects = {
'Split': [direction_arg, motion_arg],
'Blinds': [dimension_arg],
'Box': [motion_arg],
'Wipe': [direction_arg],
'Dissolve': [],
'Glitter': [direction_arg]
}
try:
args = PageTransitionEffects[effectname]
except KeyError:
raise PDFError('Unknown Effect Name "{%s}"'.format(effectname))
self._pageTransitionString = ''
return
self._pageTransitionString = (
('/Trans <</D %d /S /%s ' % (duration, effectname)) + ' '.join(args) + ' >>')
class PDFPathObject:
"""Represents a graphic path. There are certain 'modes' to PDF
drawing, and making a separate object to expose Path operations
ensures they are completed with no run-time overhead. Ask
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 ' '.join(self._code)
def moveTo(self, x, y):
self._code.append('%0.4f %0.4f m' % (x, y))
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,
starting at startAng degrees and covering extent degrees. Angles
start with 0 to the right (+x) and increase counter-clockwise.
These should have x1<x2 and y1<y2.
The algorithm is an elliptical generalization of the formulae in
Jim Fitzsimmon's TeX tutorial <URL: http://www.tinaja.com/bezarc1.pdf>."""
pointList = pdfgeom.bezierArc(x1, y1, x2, y2, startAng, extent)
#move to first point
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 arcTo(self, x1, y1, x2, y2, startAng=0, extent=90):
"""Like arc, but draws a line from the current point to
the start if the start is not the current point."""
pointList = pdfgeom.bezierArc(x1, y1, x2, y2, startAng, extent)
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"""
self._code.append('%0.4f %0.4f %0.4f %0.4f re' % (x, y, width, height))
def ellipse(self, x, y, width, height):
"""adds an ellipse to the path"""
pointList = pdfgeom.bezierArc(x, y, x + width, y + height, 0, 360)
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
x2 = x_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')
class PDFTextObject:
"""PDF logically separates text and graphics drawing; you can
change the coordinate systems for text and graphics independently.
If you do drawings while in text mode, they appear in the right places
on the page in Acrobat Reader, bur when you export Postscript to
a printer the graphics appear relative to the text coordinate
system. I regard this as a bug in how Acrobat exports to PostScript,
but this is the workaround. It forces the user to separate text
and graphics. To output text, ask te canvas for a text object
with beginText(x, y). Do not construct one directly. It keeps
track of x and y coordinates relative to its origin."""
def __init__(self, canvas, x=0, y=0):
self._code = []
self._code.append('BT')
self._canvas = canvas #canvas sets this so it has access to size info
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 ' '.join(self._code)
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:
self._code.append('1 0 0 -1 %0.4f %0.4f Tm' % (x, y)) #top down
self._x = x
self._y = y
self._x0 = x #the margin
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)
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
#we only measure coords relative to present text matrix
self._x = e
self._y = f
def moveCursor(self, dx, dy):
"""Moves to a point dx, dy away from the start of the
current line - NOT from the current point! So if
you call it in mid-sentence, watch out."""
self._code.append('%s %s Td' % (dx, -dy))
def getCursor(self):
"""Returns current text position relative to the last origin."""
return (self._x, self._y)
def getX(self):
"""Returns current x position relative to the last origin."""
return self._x
def getY(self):
"""Returns current y position relative to the last origin."""
return self._y
def setFont(self, psfontname, size, leading=None):
"""Sets the font. If leading not specified, defaults to 1.2 x
font size. Raises a readable exception if an illegal font
is supplied. Font names are case-sensitive! Keeps track
of font anme and size for metrics."""
self._fontname = psfontname
self._fontsize = size
pdffontname = self._canvas._doc.getInternalFontName(psfontname)
if leading is None:
leading = size * 1.2
self._leading = leading
self._code.append('%s %0.1f Tf %0.1f TL' % (pdffontname, size, leading))
def setCharSpace(self, charSpace):
"""Adjusts inter-character spacing"""
self._charSpace = charSpace
self._code.append('%0.4f Tc' % charSpace)
def setWordSpace(self, wordSpace):
"""Adjust inter-word spacing. This can be used
to flush-justify text - you get the width of the
words, and add some space between them."""
self._wordSpace = wordSpace
self._code.append('%0.4f Tw' % wordSpace)
def setHorizScale(self, horizScale):
"Stretches text out horizontally"
self._horizScale = 100 + horizScale
self._code.append('%0.4f Tz' % horizScale)
def setLeading(self, leading):
"How far to move down at the end of a line."
self._leading = leading
self._code.append('%0.4f TL' % leading)
def setTextRenderMode(self, mode):
"""Set the text rendering mode.
0 = Fill text
1 = Stroke text
2 = Fill then stroke
3 = Invisible
4 = Fill text and add to clipping path
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)
def setRise(self, rise):
"Move text baseline up or down to allow superscrip/subscripts"
self._rise = rise
self._y = self._y - rise # + ? _textLineMatrix?
self._code.append('%0.4f Ts' % rise)
def setStrokeColorRGB(self, r, g, b):
self._strokeColorRGB = (r, g, b)
self._code.append('%0.4f %0.4f %0.4f RG' % (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"
text = self._canvas._escape(text)
self._x = self._x + self._canvas.stringWidth(text, self._fontname, self._fontsize)
self._code.append('(%s) Tj' % text)
def textLine(self, text=''):
"""prints string at current point, text cursor moves down.
Can work with no argument to simply move the cursor down."""
text = self._canvas._escape(text)
self._x = self._x0
if self._canvas.bottomup:
self._y = self._y - self._leading
else:
self._y = self._y + self._leading
self._code.append('(%s) Tj T*' % text)
def textLines(self, stuff, trim=1):
"""prints multi-line or newlined strings, moving down. One
comon use is to quote a multi-line block in your Python code;
since this may be indented, by default it trims whitespace
off each line and from the beginning; set trim=0 to preserve
whitespace."""
if isinstance(stuff, string_types):
lines = stuff.strip().split('\n')
if trim == 1:
lines = [s.strip() for s in lines]
elif isinstance(stuff, (tuple, list)):
lines = stuff
else:
raise ValueError("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)
if self._canvas.bottomup:
self._y = self._y - self._leading
else:
self._y = self._y + self._leading
self._x = self._x0
if __name__ == '__main__':
print('For test scripts, run testpdfgen.py')