You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2952 lines
106 KiB
2952 lines
106 KiB
from __future__ import division
|
|
from fitz import *
|
|
import math
|
|
import os
|
|
import warnings
|
|
import io
|
|
|
|
"""
|
|
The following is a collection of functions to extend PyMupdf.
|
|
"""
|
|
|
|
def showPDFpage(
|
|
page,
|
|
rect,
|
|
src,
|
|
pno=0,
|
|
overlay=True,
|
|
keep_proportion=True,
|
|
rotate=0,
|
|
reuse_xref=0,
|
|
clip=None,
|
|
):
|
|
"""Show page number 'pno' of PDF 'src' in rectangle 'rect'.
|
|
|
|
Args:
|
|
rect: (rect-like) where to place the source image
|
|
src: (document) source PDF
|
|
pno: (int) source page number
|
|
overlay: (bool) put in foreground
|
|
keep_proportion: (bool) do not change width-height-ratio
|
|
rotate: (int) degrees (multiple of 90)
|
|
clip: (rect-like) part of source page rectangle
|
|
Returns:
|
|
xref of inserted object (for reuse)
|
|
"""
|
|
|
|
def calc_matrix(sr, tr, keep=True, rotate=0):
|
|
""" Calculate transformation matrix from source to target rect.
|
|
|
|
Notes:
|
|
The product of four matrices in this sequence: (1) translate correct
|
|
source corner to origin, (2) rotate, (3) scale, (4) translate to
|
|
target's top-left corner.
|
|
Args:
|
|
sr: source rect in PDF (!) coordinate system
|
|
tr: target rect in PDF coordinate system
|
|
keep: whether to keep source ratio of width to height
|
|
rotate: rotation angle in degrees
|
|
Returns:
|
|
Transformation matrix.
|
|
"""
|
|
# calc center point of source rect
|
|
smp = Point((sr.x1 + sr.x0) / 2., (sr.y1 + sr.y0) / 2.)
|
|
# calc center point of target rect
|
|
tmp = Point((tr.x1 + tr.x0) / 2., (tr.y1 + tr.y0) / 2.)
|
|
|
|
rot = Matrix(rotate) # rotation matrix
|
|
|
|
# m moves to (0, 0), then rotates
|
|
m = Matrix(1, 0, 0, 1, -smp.x, -smp.y) * rot
|
|
|
|
sr1 = sr * m # resulting source rect to calculate scale factors
|
|
|
|
fw = tr.width / sr1.width # scale the width
|
|
fh = tr.height / sr1.height # scale the height
|
|
if keep:
|
|
fw = fh = min(fw, fh) # take min if keeping aspect ratio
|
|
|
|
m *= Matrix(fw, fh) # concat scale matrix
|
|
m *= Matrix(1, 0, 0, 1, tmp.x, tmp.y) # concat move to target center
|
|
return m
|
|
|
|
CheckParent(page)
|
|
doc = page.parent
|
|
|
|
if not doc.isPDF or not src.isPDF:
|
|
raise ValueError("not a PDF")
|
|
|
|
rect = page.rect & rect # intersect with page rectangle
|
|
if rect.isEmpty or rect.isInfinite:
|
|
raise ValueError("rect must be finite and not empty")
|
|
|
|
if reuse_xref > 0:
|
|
warnings.warn("ignoring 'reuse_xref'", DeprecationWarning)
|
|
|
|
while pno < 0: # support negative page numbers
|
|
pno += len(src)
|
|
src_page = src[pno] # load ource page
|
|
if len(src_page._getContents()) == 0:
|
|
raise ValueError("nothing to show - source page empty")
|
|
|
|
tar_rect = rect * ~page._getTransformation() # target rect in PDF coordinates
|
|
|
|
src_rect = src_page.rect if not clip else src_page.rect & clip # source rect
|
|
if src_rect.isEmpty or src_rect.isInfinite:
|
|
raise ValueError("clip must be finite and not empty")
|
|
src_rect = src_rect * ~src_page._getTransformation() # ... in PDF coord
|
|
|
|
matrix = calc_matrix(src_rect, tar_rect, keep=keep_proportion, rotate=rotate)
|
|
|
|
# list of existing /Form /XObjects
|
|
ilst = [i[1] for i in doc._getPageInfo(page.number, 3)]
|
|
|
|
# create a name that is not in that list
|
|
n = "fzFrm"
|
|
i = 0
|
|
_imgname = n + "0"
|
|
while _imgname in ilst:
|
|
i += 1
|
|
_imgname = n + str(i)
|
|
|
|
isrc = src._graft_id # used as key for graftmaps
|
|
if doc._graft_id == isrc:
|
|
raise ValueError("source document must not equal target")
|
|
|
|
# check if we have already copied objects from this source doc
|
|
if isrc in doc.Graftmaps: # yes: use the old graftmap
|
|
gmap = doc.Graftmaps[isrc]
|
|
else: # no: make a new graftmap
|
|
gmap = Graftmap(doc)
|
|
doc.Graftmaps[isrc] = gmap
|
|
|
|
# take note of generated xref for automatic reuse
|
|
pno_id = (isrc, pno) # id of src[pno]
|
|
xref = doc.ShownPages.get(pno_id, 0)
|
|
|
|
xref = page._showPDFpage(
|
|
src_page,
|
|
overlay=overlay,
|
|
matrix=matrix,
|
|
xref=xref,
|
|
clip=src_rect,
|
|
graftmap=gmap,
|
|
_imgname=_imgname,
|
|
)
|
|
doc.ShownPages[pno_id] = xref
|
|
|
|
return xref
|
|
|
|
|
|
def insertImage(page, rect, filename=None, pixmap=None, stream=None, rotate=0,
|
|
keep_proportion = True,
|
|
overlay=True):
|
|
"""Insert an image in a rectangle on the current page.
|
|
|
|
Notes:
|
|
Exactly one of filename, pixmap or stream must be provided.
|
|
Args:
|
|
rect: (rect-like) where to place the source image
|
|
filename: (str) name of an image file
|
|
pixmap: (obj) a Pixmap object
|
|
stream: (bytes) an image in memory
|
|
rotate: (int) degrees (multiple of 90)
|
|
keep_proportion: (bool) whether to maintain aspect ratio
|
|
overlay: (bool) put in foreground
|
|
"""
|
|
|
|
def calc_matrix(fw, fh, tr, rotate=0):
|
|
""" Calculate transformation matrix for image insertion.
|
|
|
|
Notes:
|
|
The image will preserve its aspect ratio if and only if arguments
|
|
fw, fh are both equal to 1.
|
|
Args:
|
|
fw, fh: width / height ratio factors of image - floats in (0,1].
|
|
At least one of them (corresponding to the longer side) is equal to 1.
|
|
tr: target rect in PDF coordinates
|
|
rotate: rotation angle in degrees
|
|
Returns:
|
|
Transformation matrix.
|
|
"""
|
|
# center point of target rect
|
|
tmp = Point((tr.x1 + tr.x0) / 2., (tr.y1 + tr.y0) / 2.)
|
|
|
|
rot = Matrix(rotate) # rotation matrix
|
|
|
|
# matrix m moves image center to (0, 0), then rotates
|
|
m = Matrix(1, 0, 0, 1, -0.5, -0.5) * rot
|
|
|
|
#sr1 = sr * m # resulting image rect
|
|
|
|
# --------------------------------------------------------------------
|
|
# calculate the scale matrix
|
|
# --------------------------------------------------------------------
|
|
small = min(fw, fh) # factor of the smaller side
|
|
|
|
if rotate not in (0, 180):
|
|
fw, fh = fh, fw # width / height exchange their roles
|
|
|
|
if fw < 1: # portrait
|
|
if tr.width / fw > tr.height / fh:
|
|
w = tr.height * small
|
|
h = tr.height
|
|
else:
|
|
w = tr.width
|
|
h = tr.width / small
|
|
|
|
elif fw != fh: # landscape
|
|
if tr.width / fw > tr.height / fh:
|
|
w = tr.height / small
|
|
h = tr.height
|
|
else:
|
|
w = tr.width
|
|
h = tr.width * small
|
|
|
|
else: # (treated as) equal sided
|
|
w = tr.width
|
|
h = tr.height
|
|
|
|
m *= Matrix(w, h) # concat scale matrix
|
|
|
|
m *= Matrix(1, 0, 0, 1, tmp.x, tmp.y) # concat move to target center
|
|
|
|
return m
|
|
# -------------------------------------------------------------------------
|
|
|
|
CheckParent(page)
|
|
doc = page.parent
|
|
if not doc.isPDF:
|
|
raise ValueError("not a PDF")
|
|
if bool(filename) + bool(stream) + bool(pixmap) != 1:
|
|
raise ValueError("need exactly one of filename, pixmap, stream")
|
|
|
|
if filename and not os.path.exists(filename):
|
|
raise FileNotFoundError("No such file: '%s'" % filename)
|
|
elif stream and type(stream) not in (bytes, bytearray, io.BytesIO):
|
|
raise ValueError("stream must be bytes-like or BytesIO")
|
|
elif pixmap and type(pixmap) is not Pixmap:
|
|
raise ValueError("pixmap must be a Pixmap")
|
|
|
|
while rotate < 0:
|
|
rotate += 360
|
|
while rotate > 360:
|
|
rotate -= 360
|
|
if rotate not in (0, 90, 180, 270):
|
|
raise ValueError("bad rotate value")
|
|
|
|
r = page.rect & rect
|
|
if r.isEmpty or r.isInfinite:
|
|
raise ValueError("rect must be finite and not empty")
|
|
|
|
_imgpointer = None
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Calculate the matrix for image insertion.
|
|
# -------------------------------------------------------------------------
|
|
# If aspect ratio must be kept, we need to know image width and height.
|
|
# Easy for pixmaps. For file and stream cases, we make an fz_image and
|
|
# take those values from it. In this case, we also hand the fz_image over
|
|
# to the actual C-level function (_imgpointer), and set all other
|
|
# parameters to None.
|
|
# -------------------------------------------------------------------------
|
|
if keep_proportion is True: # for this we need the image dimension
|
|
if pixmap: # this is the easy case
|
|
w = pixmap.width
|
|
h = pixmap.height
|
|
|
|
elif stream: # use tool to access the information
|
|
# we also pass through the generated fz_image address
|
|
if type(stream) is io.BytesIO:
|
|
stream = stream.getvalue()
|
|
img_prof = TOOLS.image_profile(stream, keep_image=True)
|
|
w, h = img_prof["width"], img_prof["height"]
|
|
stream = None # make sure this arg is NOT used
|
|
_imgpointer = img_prof["image"] # pointer to fz_image
|
|
|
|
else: # worst case: must read the file
|
|
img = open(filename, "rb")
|
|
stream = img.read()
|
|
img_prof = TOOLS.image_profile(stream, keep_image=True)
|
|
w, h = img_prof["width"], img_prof["height"]
|
|
stream = None # make sure this arg is NOT used
|
|
filename = None # make sure this arg is NOT used
|
|
img.close() # close image file
|
|
_imgpointer = img_prof["image"] # pointer to fz_image
|
|
|
|
maxf = max(w, h)
|
|
fw = w / maxf
|
|
fh = h / maxf
|
|
else:
|
|
fw = fh = 1.0
|
|
|
|
clip = r * ~page._getTransformation() # target rect in PDF coordinates
|
|
|
|
matrix = calc_matrix(fw, fh, clip, rotate=rotate) # calculate matrix
|
|
|
|
# Create a unique image reference name. First make existing names list.
|
|
ilst = [i[7] for i in doc.getPageImageList(page.number)] # existing names
|
|
n = "fzImg" # 'fitz image'
|
|
i = 0
|
|
_imgname = n + "0" # first name candidate
|
|
while _imgname in ilst:
|
|
i += 1
|
|
_imgname = n + str(i) # try new name
|
|
|
|
page._insertImage(
|
|
filename=filename, # image in file
|
|
pixmap=pixmap, # image in pixmap
|
|
stream=stream, # image in memory
|
|
matrix=matrix, # generated matrix
|
|
overlay=overlay,
|
|
_imgname=_imgname, # generated PDF resource name
|
|
_imgpointer=_imgpointer, # address of fz_image
|
|
)
|
|
|
|
|
|
def getImageBbox(page, img):
|
|
"""Calculate the rectangle (bbox) of a PDF image.
|
|
|
|
Args:
|
|
:page: the PyMuPDF page object
|
|
:img: a list item from doc.getPageImageList(page.number)
|
|
|
|
Returns:
|
|
The bbox (fitz.Rect) of the image.
|
|
|
|
Notes:
|
|
This function can be used to find a connection between images returned
|
|
by page.getText("dict") and the images referenced in the list
|
|
page.getImageList().
|
|
"""
|
|
|
|
def lookup_matrix(page, imgname):
|
|
"""Return the transformation matrix for an image name.
|
|
|
|
Args:
|
|
:page: the PyMuPDF page object
|
|
:imgname: the image reference name, must equal the name in the
|
|
list doc.getPageImageList(page.number).
|
|
Returns:
|
|
concatenated matrices preceeding the image invocation.
|
|
|
|
Notes:
|
|
We are looking up "/imgname Do" in the concatenated /contents of the
|
|
page first. If not found, also look it up in the streams of any
|
|
Form XObjects of the page. If still not found, return the zero matrix.
|
|
"""
|
|
doc = page.parent # get the PDF document
|
|
if not doc.isPDF:
|
|
raise ValueError("not PDF")
|
|
|
|
page._cleanContents() # sanitize image invocation matrices
|
|
xref = page._getContents()[0] # the (only) contents object
|
|
cont = doc._getXrefStream(xref) # the contents object
|
|
cont = cont.replace(b"/", b" /") # prepend slashes with a space
|
|
# split this, ignoring white spaces
|
|
cont = cont.split()
|
|
|
|
imgnm = bytes("/" + imgname, "utf8")
|
|
if imgnm in cont:
|
|
idx = cont.index(imgnm) # the image name is found here
|
|
else: # not in page /contents, so look in Form XObjects
|
|
cont = None
|
|
xreflist = doc._getPageInfo(page.number, 3) # XObject xrefs
|
|
for item in xreflist:
|
|
cont = doc._getXrefStream(item[0]).split()
|
|
if imgnm not in cont:
|
|
cont = None
|
|
continue
|
|
idx = cont.index(imgnm) # image name found here
|
|
break
|
|
|
|
if cont is None: # safeguard against inconsistencies
|
|
return fitz.Matrix()
|
|
|
|
# list of matrices preceeding image invocation command.
|
|
# not really required, because clean contents has concatenated those
|
|
mat_list = []
|
|
while idx >= 0: # start value is "/Image Do" location
|
|
if cont[idx] == b"q": # finished at leading stacking command
|
|
break
|
|
if cont[idx] == b"cm": # encountered a matrix command
|
|
mat = cont[idx - 6 : idx] # list of the 6 matrix values
|
|
l = list(map(float, mat)) # make them floats
|
|
mat_list.append(fitz.Matrix(l)) # append fitz matrix
|
|
idx -= 6 # step backwards 6 entries
|
|
else:
|
|
idx -= 1 # step backwards
|
|
mat = fitz.Matrix(1, 1) # concatenate encountered matrices to this one
|
|
for m in reversed(mat_list):
|
|
mat *= m
|
|
l = len(mat_list)
|
|
if l == 0: # safeguard against unusual situations
|
|
return fitz.Matrix() # the zero matrix
|
|
return m
|
|
|
|
if type(img) in (list, tuple):
|
|
imgname = img[7]
|
|
else:
|
|
imgname = img
|
|
mat = lookup_matrix(page, imgname)
|
|
if not bool(mat):
|
|
raise ValueError("image not found")
|
|
|
|
ctm = page._getTransformation() # page transformation matrix
|
|
mat.preScale(1, -1) # fiddle the matrix
|
|
mat.preTranslate(0, -1) # fiddle the matrix
|
|
r = fitz.Rect(0, 0, 1, 1) * mat # the bbox in PDF coordinates
|
|
return r * ctm # the bbox in MuPDF coordinates
|
|
|
|
|
|
def searchFor(page, text, hit_max = 16, quads = False):
|
|
""" Search for a string on a page.
|
|
|
|
Args:
|
|
text: string to be searched for
|
|
hit_max: maximum hits
|
|
quads: return quads instead of rectangles
|
|
Returns:
|
|
a list of rectangles or quads, each containing one occurrence.
|
|
"""
|
|
CheckParent(page)
|
|
dl = page.getDisplayList() # create DisplayList
|
|
tp = dl.getTextPage() # create TextPage
|
|
# return list of hitting reactangles
|
|
rlist = tp.search(text, hit_max = hit_max, quads = quads)
|
|
dl = None
|
|
tp = None
|
|
return rlist
|
|
|
|
|
|
def searchPageFor(doc, pno, text, hit_max=16, quads=False):
|
|
""" Search for a string on a page.
|
|
|
|
Args:
|
|
pno: page number
|
|
text: string to be searched for
|
|
hit_max: maximum hits
|
|
quads: return quads instead of rectangles
|
|
Returns:
|
|
a list of rectangles or quads, each containing an occurrence.
|
|
"""
|
|
|
|
return doc[pno].searchFor(text, hit_max = hit_max, quads = quads)
|
|
|
|
|
|
def getTextBlocks(page, flags=None):
|
|
"""Return the text blocks on a page.
|
|
|
|
Notes:
|
|
Lines in a block are concatenated with line breaks.
|
|
Args:
|
|
flags: (int) control the amount of data parsed into the textpage.
|
|
Returns:
|
|
A list of the blocks. Each item contains the containing rectangle coordinates,
|
|
text lines, block type and running block number.
|
|
"""
|
|
CheckParent(page)
|
|
dl = page.getDisplayList()
|
|
if flags is None:
|
|
flags = TEXT_PRESERVE_LIGATURES | TEXT_INHIBIT_SPACES
|
|
tp = dl.getTextPage(flags)
|
|
l = []
|
|
tp._extractTextBlocks_AsList(l)
|
|
del tp
|
|
del dl
|
|
return l
|
|
|
|
def getTextWords(page, flags=None):
|
|
"""Return the text words as a list with the bbox for each word.
|
|
|
|
Args:
|
|
flags: (int) control the amount of data parsed into the textpage.
|
|
"""
|
|
CheckParent(page)
|
|
dl = page.getDisplayList()
|
|
if flags is None:
|
|
flags = TEXT_PRESERVE_LIGATURES | TEXT_INHIBIT_SPACES
|
|
tp = dl.getTextPage(flags)
|
|
l = []
|
|
tp._extractTextWords_AsList(l)
|
|
del dl
|
|
del tp
|
|
return l
|
|
|
|
def getText(page, output="text", flags=None):
|
|
""" Extract a document page's text.
|
|
|
|
Args:
|
|
output: (str) text, html, dict, json, rawdict, xhtml or xml.
|
|
|
|
Returns:
|
|
the output of TextPage methods extractText, extractHTML, extractDICT, extractJSON, extractRAWDICT, extractXHTML or etractXML respectively. Default and misspelling choice is "text".
|
|
"""
|
|
CheckParent(page)
|
|
dl = page.getDisplayList()
|
|
# available output types
|
|
formats = ("text", "html", "json", "xml", "xhtml", "dict", "rawdict")
|
|
output = output.lower()
|
|
if output not in formats:
|
|
output = "text"
|
|
# choose which of them also include images in the TextPage
|
|
images = (0, 1, 1, 0, 1, 1, 1) # controls image inclusion in text page
|
|
f = formats.index(output)
|
|
if flags is None:
|
|
flags = TEXT_PRESERVE_LIGATURES | TEXT_PRESERVE_WHITESPACE | TEXT_INHIBIT_SPACES
|
|
if images[f] == 1:
|
|
flags |= TEXT_PRESERVE_IMAGES
|
|
|
|
tp = dl.getTextPage(flags) # TextPage with or without images
|
|
|
|
if f == 2:
|
|
t = tp.extractJSON()
|
|
elif f == 5:
|
|
t = tp.extractDICT()
|
|
elif f == 6:
|
|
t = tp.extractRAWDICT()
|
|
else:
|
|
t = tp._extractText(f)
|
|
|
|
del dl
|
|
del tp
|
|
return t
|
|
|
|
|
|
def getPageText(doc, pno, output="text"):
|
|
""" Extract a document page's text by page number.
|
|
|
|
Notes:
|
|
Convenience function calling page.getText().
|
|
Args:
|
|
pno: page number
|
|
output: (str) text, html, dict, json, rawdict, xhtml or xml.
|
|
Returns:
|
|
output from page.TextPage().
|
|
"""
|
|
return doc[pno].getText(output)
|
|
|
|
def getPixmap(page,
|
|
matrix=None,
|
|
colorspace=csRGB,
|
|
clip=None,
|
|
alpha=False,
|
|
annots=True,
|
|
):
|
|
"""Create pixmap of page.
|
|
|
|
Args:
|
|
matrix: Matrix for transformation (default: Identity).
|
|
colorspace: (str/Colorspace) rgb, rgb, gray - case ignored, default csRGB.
|
|
clip: (irect-like) restrict rendering to this area.
|
|
alpha: (bool) include alpha channel
|
|
"""
|
|
CheckParent(page)
|
|
|
|
# determine required colorspace
|
|
cs = colorspace
|
|
if type(colorspace) is str:
|
|
if colorspace.upper() == "GRAY":
|
|
cs = csGRAY
|
|
elif colorspace.upper() == "CMYK":
|
|
cs = csCMYK
|
|
else:
|
|
cs = csRGB
|
|
if cs.n not in (1,3,4):
|
|
raise ValueError("unsupported colorspace")
|
|
|
|
dl = page.getDisplayList(annots) # create DisplayList
|
|
if clip:
|
|
scissor = Rect(clip)
|
|
else:
|
|
scissor = None
|
|
pix = dl.getPixmap(matrix=matrix,
|
|
colorspace=cs,
|
|
alpha=alpha,
|
|
clip=scissor,
|
|
)
|
|
del dl
|
|
return pix
|
|
|
|
def getPagePixmap(doc,
|
|
pno,
|
|
matrix=None,
|
|
colorspace=csRGB,
|
|
clip=None,
|
|
alpha=False,
|
|
annots=True,
|
|
):
|
|
"""Create pixmap of document page by page number.
|
|
|
|
Notes:
|
|
Convenience function calling page.getPixmap.
|
|
Args:
|
|
pno: (int) page number
|
|
matrix: Matrix for transformation (default: Identity).
|
|
colorspace: (str,Colorspace) rgb, rgb, gray - case ignored, default csRGB.
|
|
clip: (irect-like) restrict rendering to this area.
|
|
alpha: (bool) include alpha channel
|
|
annots: (bool) also render annotations
|
|
"""
|
|
return doc[pno].getPixmap(matrix=matrix,
|
|
colorspace=colorspace,
|
|
clip=clip,
|
|
alpha=alpha,
|
|
annots=annots,
|
|
)
|
|
|
|
def getLinkDict(ln):
|
|
nl = {"kind": ln.dest.kind, "xref": 0}
|
|
try:
|
|
nl["from"] = ln.rect
|
|
except:
|
|
pass
|
|
pnt = Point(0, 0)
|
|
if ln.dest.flags & LINK_FLAG_L_VALID:
|
|
pnt.x = ln.dest.lt.x
|
|
if ln.dest.flags & LINK_FLAG_T_VALID:
|
|
pnt.y = ln.dest.lt.y
|
|
|
|
if ln.dest.kind == LINK_URI:
|
|
nl["uri"] = ln.dest.uri
|
|
|
|
elif ln.dest.kind == LINK_GOTO:
|
|
nl["page"] = ln.dest.page
|
|
nl["to"] = pnt
|
|
if ln.dest.flags & LINK_FLAG_R_IS_ZOOM:
|
|
nl["zoom"] = ln.dest.rb.x
|
|
else:
|
|
nl["zoom"] = 0.0
|
|
|
|
elif ln.dest.kind == LINK_GOTOR:
|
|
nl["file"] = ln.dest.fileSpec.replace("\\", "/")
|
|
nl["page"] = ln.dest.page
|
|
if ln.dest.page < 0:
|
|
nl["to"] = ln.dest.dest
|
|
else:
|
|
nl["to"] = pnt
|
|
if ln.dest.flags & LINK_FLAG_R_IS_ZOOM:
|
|
nl["zoom"] = ln.dest.rb.x
|
|
else:
|
|
nl["zoom"] = 0.0
|
|
|
|
elif ln.dest.kind == LINK_LAUNCH:
|
|
nl["file"] = ln.dest.fileSpec.replace("\\", "/")
|
|
|
|
elif ln.dest.kind == LINK_NAMED:
|
|
nl["name"] = ln.dest.named
|
|
|
|
else:
|
|
nl["page"] = ln.dest.page
|
|
|
|
return nl
|
|
|
|
def getLinks(page):
|
|
"""Create a list of all links contained in a PDF page.
|
|
|
|
Notes:
|
|
see PyMuPDF ducmentation for details.
|
|
"""
|
|
|
|
CheckParent(page)
|
|
ln = page.firstLink
|
|
links = []
|
|
while ln:
|
|
nl = getLinkDict(ln)
|
|
#if nl["kind"] == LINK_GOTO:
|
|
# if type(nl["to"]) is Point and nl["page"] >= 0:
|
|
# doc = page.parent
|
|
# target_page = doc[nl["page"]]
|
|
# ctm = target_page._getTransformation()
|
|
# point = nl["to"] * ctm
|
|
# nl["to"] = point
|
|
links.append(nl)
|
|
ln = ln.next
|
|
if len(links) > 0:
|
|
linkxrefs = page._getLinkXrefs()
|
|
if len(linkxrefs) == len(links):
|
|
for i in range(len(linkxrefs)):
|
|
links[i]["xref"] = linkxrefs[i]
|
|
return links
|
|
|
|
def getToC(doc, simple = True):
|
|
"""Create a table of contents.
|
|
|
|
Args:
|
|
simple: a bool to control output. Returns a list, where each entry consists of outline level, title, page number and link destination (if simple = False). For details see PyMuPDF's documentation.
|
|
"""
|
|
|
|
def recurse(olItem, liste, lvl):
|
|
'''Recursively follow the outline item chain and record item information in a list.'''
|
|
while olItem:
|
|
if olItem.title:
|
|
title = olItem.title
|
|
else:
|
|
title = " "
|
|
|
|
if not olItem.isExternal:
|
|
if olItem.uri:
|
|
page = olItem.page + 1
|
|
else:
|
|
page = -1
|
|
else:
|
|
page = -1
|
|
|
|
if not simple:
|
|
link = getLinkDict(olItem)
|
|
liste.append([lvl, title, page, link])
|
|
else:
|
|
liste.append([lvl, title, page])
|
|
|
|
if olItem.down:
|
|
liste = recurse(olItem.down, liste, lvl+1)
|
|
olItem = olItem.next
|
|
return liste
|
|
|
|
# check if document is open and not encrypted
|
|
if doc.isClosed:
|
|
raise ValueError("document closed")
|
|
doc.initData()
|
|
olItem = doc.outline
|
|
|
|
if not olItem: return []
|
|
lvl = 1
|
|
liste = []
|
|
return recurse(olItem, liste, lvl)
|
|
|
|
def getRectArea(*args):
|
|
"""Calculate area of rectangle.\nparameter is one of 'px' (default), 'in', 'cm', or 'mm'."""
|
|
rect = args[0]
|
|
if len(args) > 1:
|
|
unit = args[1]
|
|
else:
|
|
unit = "px"
|
|
u = {"px": (1,1), "in": (1.,72.), "cm": (2.54, 72.), "mm": (25.4, 72.)}
|
|
f = (u[unit][0] / u[unit][1])**2
|
|
return f * rect.width * rect.height
|
|
|
|
def setMetadata(doc, m):
|
|
"""Set a PDF's metadata (/Info dictionary)\nm: dictionary like doc.metadata'."""
|
|
if doc.isClosed or doc.isEncrypted:
|
|
raise ValueError("document closed or encrypted")
|
|
if type(m) is not dict:
|
|
raise ValueError("arg2 must be a dictionary")
|
|
for k in m.keys():
|
|
if not k in ("author", "producer", "creator", "title", "format",
|
|
"encryption", "creationDate", "modDate", "subject",
|
|
"keywords"):
|
|
raise ValueError("invalid dictionary key: " + k)
|
|
d = "<</Author"
|
|
d += getPDFstr(m.get("author", "none"))
|
|
d += "/CreationDate"
|
|
d += getPDFstr(m.get("creationDate", "none"))
|
|
d += "/Creator"
|
|
d += getPDFstr(m.get("creator", "none"))
|
|
d += "/Keywords"
|
|
d += getPDFstr(m.get("keywords", "none"))
|
|
d += "/ModDate"
|
|
d += getPDFstr(m.get("modDate", "none"))
|
|
d += "/Producer"
|
|
d += getPDFstr(m.get("producer", "none"))
|
|
d += "/Subject"
|
|
d += getPDFstr(m.get("subject", "none"))
|
|
d += "/Title"
|
|
d += getPDFstr(m.get("title", "none"))
|
|
d += ">>"
|
|
doc._setMetadata(d)
|
|
doc.initData()
|
|
return
|
|
|
|
def getDestStr(xref, ddict):
|
|
""" Calculate the PDF action string.
|
|
|
|
Notes:
|
|
Supports Link annotations and outline items (bookmarks).
|
|
"""
|
|
if not ddict:
|
|
return ""
|
|
str_goto = "/A<</S/GoTo/D[%i 0 R/XYZ %g %g %i]>>"
|
|
str_gotor1 = "/A<</S/GoToR/D[%s /XYZ %s %s %s]/F<</F%s/UF%s/Type/Filespec>>>>"
|
|
str_gotor2 = "/A<</S/GoToR/D%s/F<</F%s/UF%s/Type/Filespec>>>>"
|
|
str_launch = "/A<</S/Launch/F<</F%s/UF%s/Type/Filespec>>>>"
|
|
str_uri = "/A<</S/URI/URI%s>>"
|
|
|
|
if type(ddict) in (int, float):
|
|
dest = str_goto % (xref, 0, ddict, 0)
|
|
return dest
|
|
d_kind = ddict.get("kind", LINK_NONE)
|
|
|
|
if d_kind == LINK_NONE:
|
|
return ""
|
|
|
|
if ddict["kind"] == LINK_GOTO:
|
|
d_zoom = ddict.get("zoom", 0)
|
|
to = ddict.get("to", Point(0, 0))
|
|
d_left, d_top = to
|
|
dest = str_goto % (xref, d_left, d_top, d_zoom)
|
|
return dest
|
|
|
|
if ddict["kind"] == LINK_URI:
|
|
dest = str_uri % (getPDFstr(ddict["uri"]),)
|
|
return dest
|
|
|
|
if ddict["kind"] == LINK_LAUNCH:
|
|
fspec = getPDFstr(ddict["file"])
|
|
dest = str_launch % (fspec, fspec)
|
|
return dest
|
|
|
|
if ddict["kind"] == LINK_GOTOR and ddict["page"] < 0:
|
|
fspec = getPDFstr(ddict["file"])
|
|
dest = str_gotor2 % (getPDFstr(ddict["to"]), fspec, fspec)
|
|
return dest
|
|
|
|
if ddict["kind"] == LINK_GOTOR and ddict["page"] >= 0:
|
|
fspec = getPDFstr(ddict["file"])
|
|
dest = str_gotor1 % (ddict["page"], ddict["to"].x, ddict["to"].y,
|
|
ddict["zoom"], fspec, fspec)
|
|
return dest
|
|
|
|
return ""
|
|
|
|
def setToC(doc, toc):
|
|
'''Create new outline tree (table of contents)\ntoc: a Python list of lists. Each entry must contain level, title, page and optionally top margin on the page.'''
|
|
if doc.isClosed or doc.isEncrypted:
|
|
raise ValueError("document closed or encrypted")
|
|
if not doc.isPDF:
|
|
raise ValueError("not a PDF")
|
|
toclen = len(toc)
|
|
# check toc validity ------------------------------------------------------
|
|
if type(toc) is not list:
|
|
raise ValueError("arg2 must be a list")
|
|
if toclen == 0:
|
|
return len(doc._delToC())
|
|
pageCount = len(doc)
|
|
t0 = toc[0]
|
|
if type(t0) is not list:
|
|
raise ValueError("arg2 must contain lists of 3 or 4 items")
|
|
if t0[0] != 1:
|
|
raise ValueError("hierarchy level of item 0 must be 1")
|
|
for i in list(range(toclen-1)):
|
|
t1 = toc[i]
|
|
t2 = toc[i+1]
|
|
if not -1 <= t1[2] <= pageCount:
|
|
raise ValueError("row %i:page number out of range" % i)
|
|
if (type(t2) is not list) or len(t2) < 3 or len(t2) > 4:
|
|
raise ValueError("arg2 must contain lists of 3 or 4 items")
|
|
if (type(t2[0]) is not int) or t2[0] < 1:
|
|
raise ValueError("hierarchy levels must be int > 0")
|
|
if t2[0] > t1[0] + 1:
|
|
raise ValueError("row %i: hierarchy step is > 1" % i)
|
|
# no formal errors in toc --------------------------------------------------
|
|
|
|
old_xrefs = doc._delToC() # del old outlines, get xref numbers
|
|
old_xrefs = [] # force creation of new xrefs
|
|
# prepare table of xrefs for new bookmarks
|
|
xref = [0] + old_xrefs
|
|
xref[0] = doc._getOLRootNumber() # entry zero is outline root xref#
|
|
if toclen > len(old_xrefs): # too few old xrefs?
|
|
for i in range((toclen - len(old_xrefs))):
|
|
xref.append(doc._getNewXref()) # acquire new ones
|
|
|
|
lvltab = {0:0} # to store last entry per hierarchy level
|
|
|
|
#==============================================================================
|
|
# contains new outline objects as strings - first one is outline root
|
|
#==============================================================================
|
|
olitems = [{"count":0, "first":-1, "last":-1, "xref":xref[0]}]
|
|
#==============================================================================
|
|
# build olitems as a list of PDF-like connnected dictionaries
|
|
#==============================================================================
|
|
for i in range(toclen):
|
|
o = toc[i]
|
|
lvl = o[0] # level
|
|
title = getPDFstr(o[1]) # titel
|
|
pno = min(doc.pageCount - 1, max(0, o[2] - 1)) # page number
|
|
page = doc[pno] # load the page
|
|
ictm = ~page._getTransformation() # get inverse transformation matrix
|
|
top = Point(72, 36) * ictm # default top location
|
|
dest_dict = {"to": top, "kind": LINK_GOTO} # fall back target
|
|
if o[2] < 0:
|
|
dest_dict["kind"] = LINK_NONE
|
|
if len(o) > 3: # some target is specified
|
|
if type(o[3]) in (int, float): # if number, make a point from it
|
|
dest_dict["to"] = Point(72, o[3]) * ictm
|
|
else: # if something else, make sure we have a dict
|
|
dest_dict = o[3] if type(o[3]) is dict else dest_dict
|
|
if "to" not in dest_dict: # target point not in dict?
|
|
dest_dict["to"] = top # put default in
|
|
else: # transform target to PDF coordinates
|
|
point = dest_dict["to"] * ictm
|
|
dest_dict["to"] = point
|
|
d = {}
|
|
d["first"] = -1
|
|
d["count"] = 0
|
|
d["last"] = -1
|
|
d["prev"] = -1
|
|
d["next"] = -1
|
|
d["dest"] = getDestStr(page.xref, dest_dict)
|
|
d["top"] = dest_dict["to"]
|
|
d["title"] = title
|
|
d["parent"] = lvltab[lvl-1]
|
|
d["xref"] = xref[i+1]
|
|
lvltab[lvl] = i+1
|
|
parent = olitems[lvltab[lvl-1]]
|
|
parent["count"] += 1
|
|
|
|
if parent["first"] == -1:
|
|
parent["first"] = i+1
|
|
parent["last"] = i+1
|
|
else:
|
|
d["prev"] = parent["last"]
|
|
prev = olitems[parent["last"]]
|
|
prev["next"] = i+1
|
|
parent["last"] = i+1
|
|
olitems.append(d)
|
|
|
|
#==============================================================================
|
|
# now create each ol item as a string and insert it in the PDF
|
|
#==============================================================================
|
|
for i, ol in enumerate(olitems):
|
|
txt = "<<"
|
|
if ol["count"] > 0:
|
|
if i > 0:
|
|
txt += "/Count -%i" % ol["count"]
|
|
else:
|
|
txt += "/Count %i" % ol["count"]
|
|
try:
|
|
txt += ol["dest"]
|
|
except: pass
|
|
try:
|
|
if ol["first"] > -1:
|
|
txt += "/First %i 0 R" % xref[ol["first"]]
|
|
except: pass
|
|
try:
|
|
if ol["last"] > -1:
|
|
txt += "/Last %i 0 R" % xref[ol["last"]]
|
|
except: pass
|
|
try:
|
|
if ol["next"] > -1:
|
|
txt += "/Next %i 0 R" % xref[ol["next"]]
|
|
except: pass
|
|
try:
|
|
if ol["parent"] > -1:
|
|
txt += "/Parent %i 0 R" % xref[ol["parent"]]
|
|
except: pass
|
|
try:
|
|
if ol["prev"] > -1:
|
|
txt += "/Prev %i 0 R" % xref[ol["prev"]]
|
|
except: pass
|
|
try:
|
|
txt += "/Title" + ol["title"]
|
|
except: pass
|
|
if i == 0: # special: this is the outline root
|
|
txt += "/Type/Outlines"
|
|
txt += ">>"
|
|
doc._updateObject(xref[i], txt) # insert the PDF object
|
|
|
|
doc.initData()
|
|
return toclen
|
|
|
|
def do_links(doc1, doc2, from_page = -1, to_page = -1, start_at = -1):
|
|
'''Insert links contained in copied page range into destination PDF.
|
|
Parameter values **must** equal those of method insertPDF() - which must have been previously executed.'''
|
|
#--------------------------------------------------------------------------
|
|
# define skeletons for /Annots object texts
|
|
#--------------------------------------------------------------------------
|
|
annot_goto = "<</A<</S/GoTo/D[%i 0 R /XYZ %g %g 0]>>/Rect[%s]/Subtype/Link>>"
|
|
|
|
annot_gotor = "<</A<</S/GoToR/D[%i /XYZ %g %g 0]/F<</F(%s)/UF(%s)/Type/Filespec>>>>/Rect[%s]/Subtype/Link>>"
|
|
|
|
annot_gotor_n = "<</A<</S/GoToR/D(%s)/F(%s)>>/Rect[%s]/Subtype/Link>>"
|
|
|
|
annot_launch = "<</A<</S/Launch/F<</F(%s)/UF(%s)/Type/Filespec>>>>/Rect[%s]/Subtype/Link>>"
|
|
|
|
annot_uri = "<</A<</S/URI/URI(%s)>>/Rect[%s]/Subtype/Link>>"
|
|
|
|
#--------------------------------------------------------------------------
|
|
# internal function to create the actual "/Annots" object string
|
|
#--------------------------------------------------------------------------
|
|
def cre_annot(lnk, xref_dst, pno_src, ctm):
|
|
"""Create annotation object string for a passed-in link.
|
|
"""
|
|
|
|
r = lnk["from"] * ctm # rect in PDF coordinates
|
|
rect = "%g %g %g %g" % tuple(r)
|
|
if lnk["kind"] == LINK_GOTO:
|
|
txt = annot_goto
|
|
idx = pno_src.index(lnk["page"])
|
|
p = lnk["to"] * ctm # target point in PDF coordinates
|
|
annot = txt % (xref_dst[idx], p.x, p.y, rect)
|
|
|
|
elif lnk["kind"] == LINK_GOTOR:
|
|
if lnk["page"] >= 0:
|
|
txt = annot_gotor
|
|
pnt = lnk.get("to", Point(0, 0)) # destination point
|
|
if type(pnt) is not Point:
|
|
pnt = Point(0, 0)
|
|
annot = txt % (lnk["page"], pnt.x, pnt.y,
|
|
lnk["file"], lnk["file"], rect)
|
|
else:
|
|
txt = annot_gotor_n
|
|
to = getPDFstr(lnk["to"])
|
|
to = to[1:-1]
|
|
f = lnk["file"]
|
|
annot = txt % (to, f, rect)
|
|
|
|
elif lnk["kind"] == LINK_LAUNCH:
|
|
txt = annot_launch
|
|
annot = txt % (lnk["file"], lnk["file"], rect)
|
|
|
|
elif lnk["kind"] == LINK_URI:
|
|
txt = annot_uri
|
|
annot = txt % (lnk["uri"], rect)
|
|
|
|
else:
|
|
annot = ""
|
|
|
|
return annot
|
|
#--------------------------------------------------------------------------
|
|
|
|
# validate & normalize parameters
|
|
if from_page < 0:
|
|
fp = 0
|
|
elif from_page >= doc2.pageCount:
|
|
fp = doc2.pageCount - 1
|
|
else:
|
|
fp = from_page
|
|
|
|
if to_page < 0 or to_page >= doc2.pageCount:
|
|
tp = doc2.pageCount - 1
|
|
else:
|
|
tp = to_page
|
|
|
|
if start_at < 0:
|
|
raise ValueError("'start_at' must be >= 0")
|
|
sa = start_at
|
|
|
|
incr = 1 if fp <= tp else -1 # page range could be reversed
|
|
|
|
# lists of source / destination page numbers
|
|
pno_src = list(range(fp, tp + incr, incr))
|
|
pno_dst = [sa + i for i in range(len(pno_src))]
|
|
|
|
# lists of source / destination page xrefs
|
|
xref_src = []
|
|
xref_dst = []
|
|
for i in range(len(pno_src)):
|
|
p_src = pno_src[i]
|
|
p_dst = pno_dst[i]
|
|
old_xref = doc2._getPageObjNumber(p_src)[0]
|
|
new_xref = doc1._getPageObjNumber(p_dst)[0]
|
|
xref_src.append(old_xref)
|
|
xref_dst.append(new_xref)
|
|
|
|
# create the links for each copied page in destination PDF
|
|
for i in range(len(xref_src)):
|
|
page_src = doc2[pno_src[i]] # load source page
|
|
links = page_src.getLinks() # get all its links
|
|
if len(links) == 0: # no links there
|
|
page_src = None
|
|
continue
|
|
ctm = ~page_src._getTransformation() # calc page transformation matrix
|
|
page_dst = doc1[pno_dst[i]] # load destination page
|
|
link_tab = [] # store all link definitions here
|
|
for l in links:
|
|
if l["kind"] == LINK_GOTO and (l["page"] not in pno_src):
|
|
continue # GOTO link target not in copied pages
|
|
annot_text = cre_annot(l, xref_dst, pno_src, ctm)
|
|
if not annot_text:
|
|
print("cannot create /Annot for kind: " + str(l["kind"]))
|
|
else:
|
|
link_tab.append(annot_text)
|
|
if len(link_tab) > 0:
|
|
page_dst._addAnnot_FromString(link_tab)
|
|
page_dst = None
|
|
page_src = None
|
|
return
|
|
|
|
def getLinkText(page, lnk):
|
|
#--------------------------------------------------------------------------
|
|
# define skeletons for /Annots object texts
|
|
#--------------------------------------------------------------------------
|
|
annot_goto = "<</A<</S/GoTo/D[%i 0 R/XYZ %g %g 0]>>/Rect[%s]/Subtype/Link>>"
|
|
|
|
annot_goto_n = "<</A<</S/GoTo/D%s>>/Rect[%s]/Subtype/Link>>"
|
|
|
|
annot_gotor = '''<</A<</S/GoToR/D[%i /XYZ %g %g 0]/F<</F(%s)/UF(%s)/Type/Filespec
|
|
>>>>/Rect[%s]/Subtype/Link>>'''
|
|
|
|
annot_gotor_n = "<</A<</S/GoToR/D%s/F(%s)>>/Rect[%s]/Subtype/Link>>"
|
|
|
|
annot_launch = '''<</A<</S/Launch/F<</F(%s)/UF(%s)/Type/Filespec>>
|
|
>>/Rect[%s]/Subtype/Link>>'''
|
|
|
|
annot_uri = "<</A<</S/URI/URI(%s)>>/Rect[%s]/Subtype/Link>>"
|
|
|
|
annot_named = "<</A<</S/Named/N/%s/Type/Action>>/Rect[%s]/Subtype/Link>>"
|
|
|
|
ctm = page._getTransformation()
|
|
ictm = ~ctm
|
|
r = lnk["from"]
|
|
height = page.rect.height
|
|
rect = "%g %g %g %g" % tuple(r * ictm)
|
|
|
|
annot = ""
|
|
if lnk["kind"] == LINK_GOTO:
|
|
if lnk["page"] >= 0:
|
|
txt = annot_goto
|
|
pno = lnk["page"]
|
|
xref = page.parent._getPageXref(pno)[0]
|
|
pnt = lnk.get("to", Point(0, 0)) # destination point
|
|
ipnt = pnt * ictm
|
|
annot = txt % (xref, ipnt.x, ipnt.y, rect)
|
|
else:
|
|
txt = annot_goto_n
|
|
annot = txt % (getPDFstr(lnk["to"]), rect)
|
|
|
|
elif lnk["kind"] == LINK_GOTOR:
|
|
if lnk["page"] >= 0:
|
|
txt = annot_gotor
|
|
pnt = lnk.get("to", Point(0, 0)) # destination point
|
|
if type(pnt) is not Point:
|
|
pnt = Point(0, 0)
|
|
annot = txt % (lnk["page"], pnt.x, pnt.y,
|
|
lnk["file"], lnk["file"], rect)
|
|
else:
|
|
txt = annot_gotor_n
|
|
annot = txt % (getPDFstr(lnk["to"]), lnk["file"], rect)
|
|
|
|
elif lnk["kind"] == LINK_LAUNCH:
|
|
txt = annot_launch
|
|
annot = txt % (lnk["file"], lnk["file"], rect)
|
|
|
|
elif lnk["kind"] == LINK_URI:
|
|
txt = annot_uri
|
|
annot = txt % (lnk["uri"], rect)
|
|
|
|
elif lnk["kind"] == LINK_NAMED:
|
|
txt = annot_named
|
|
annot = txt % (lnk["name"], rect)
|
|
|
|
return annot
|
|
|
|
def updateLink(page, lnk):
|
|
""" Update a link on the current page. """
|
|
CheckParent(page)
|
|
annot = getLinkText(page, lnk)
|
|
if annot == "":
|
|
raise ValueError("link kind not supported")
|
|
|
|
page.parent._updateObject(lnk["xref"], annot, page = page)
|
|
return
|
|
|
|
def insertLink(page, lnk, mark = True):
|
|
""" Insert a new link for the current page. """
|
|
CheckParent(page)
|
|
annot = getLinkText(page, lnk)
|
|
if annot == "":
|
|
raise ValueError("link kind not supported")
|
|
|
|
page._addAnnot_FromString([annot])
|
|
return
|
|
|
|
def insertTextbox(page, rect, buffer,
|
|
fontname="helv",
|
|
fontfile=None,
|
|
set_simple=0,
|
|
encoding=0,
|
|
fontsize=11,
|
|
color=None,
|
|
fill=None,
|
|
expandtabs=1,
|
|
align=0,
|
|
rotate=0,
|
|
render_mode=0,
|
|
border_width=1,
|
|
morph=None,
|
|
overlay=True):
|
|
""" Insert text into a given rectangle.
|
|
|
|
Notes:
|
|
Creates a Shape object, uses its same-named method and commits it.
|
|
Parameters:
|
|
rect: (rect-like) area to use for text.
|
|
buffer: text to be inserted
|
|
fontname: a Base-14 font, font name or '/name'
|
|
fontfile: name of a font file
|
|
fontsize: font size
|
|
color: RGB color triple
|
|
expandtabs: handles tabulators with string function
|
|
align: left, center, right, justified
|
|
rotate: 0, 90, 180, or 270 degrees
|
|
morph: morph box with a matrix and a pivotal point
|
|
overlay: put text in foreground or background
|
|
Returns:
|
|
unused or deficit rectangle area (float)
|
|
"""
|
|
img = page.newShape()
|
|
rc = img.insertTextbox(rect, buffer,
|
|
fontsize=fontsize,
|
|
fontname=fontname,
|
|
fontfile=fontfile,
|
|
set_simple=set_simple,
|
|
encoding=encoding,
|
|
color=color,
|
|
fill=fill,
|
|
expandtabs=expandtabs,
|
|
render_mode=render_mode,
|
|
border_width=border_width,
|
|
align=align,
|
|
rotate=rotate,
|
|
morph=morph)
|
|
if rc >= 0:
|
|
img.commit(overlay)
|
|
return rc
|
|
|
|
def insertText(page, point, text,
|
|
fontsize=11,
|
|
fontname="helv",
|
|
fontfile=None,
|
|
set_simple=0,
|
|
encoding=0,
|
|
color=None,
|
|
fill=None,
|
|
border_width=1,
|
|
render_mode=0,
|
|
rotate=0,
|
|
morph=None,
|
|
overlay=True):
|
|
|
|
img = page.newShape()
|
|
rc = img.insertText(point, text,
|
|
fontsize=fontsize,
|
|
fontname=fontname,
|
|
fontfile=fontfile,
|
|
set_simple=set_simple,
|
|
encoding=encoding,
|
|
color=color,
|
|
fill=fill,
|
|
border_width=border_width,
|
|
render_mode=render_mode,
|
|
rotate=rotate,
|
|
morph=morph)
|
|
if rc >= 0:
|
|
img.commit(overlay)
|
|
return rc
|
|
|
|
def newPage(doc, pno=-1, width=595, height=842):
|
|
"""Create and return a new page object.
|
|
"""
|
|
doc._newPage(pno, width=width, height=height)
|
|
return doc[pno]
|
|
|
|
def insertPage(
|
|
doc,
|
|
pno,
|
|
text=None,
|
|
fontsize=11,
|
|
width=595,
|
|
height=842,
|
|
fontname="helv",
|
|
fontfile=None,
|
|
color=None,
|
|
):
|
|
""" Create a new PDF page and insert some text.
|
|
|
|
Notes:
|
|
Function combining Document.newPage() and Page.insertText().
|
|
For parameter details see these methods.
|
|
"""
|
|
page = doc.newPage(pno=pno, width=width, height=height)
|
|
if not bool(text):
|
|
return 0
|
|
rc = page.insertText(
|
|
(50, 72),
|
|
text,
|
|
fontsize=fontsize,
|
|
fontname=fontname,
|
|
fontfile=fontfile,
|
|
color=color,
|
|
)
|
|
return rc
|
|
|
|
def drawLine(page, p1, p2, color=None, dashes=None, width=1, lineCap=0, lineJoin=0, overlay=True, morph=None, roundcap=None):
|
|
"""Draw a line from point p1 to point p2.
|
|
"""
|
|
img = page.newShape()
|
|
p = img.drawLine(Point(p1), Point(p2))
|
|
img.finish(color=color, dashes=dashes, width=width, closePath=False,
|
|
lineCap=lineCap, lineJoin=lineJoin, morph=morph, roundCap=roundcap)
|
|
img.commit(overlay)
|
|
|
|
return p
|
|
|
|
def drawSquiggle(page, p1, p2, breadth = 2, color=None, dashes=None,
|
|
width=1, lineCap=0, lineJoin=0, overlay=True, morph=None, roundCap=None):
|
|
"""Draw a squiggly line from point p1 to point p2.
|
|
"""
|
|
img = page.newShape()
|
|
p = img.drawSquiggle(Point(p1), Point(p2), breadth = breadth)
|
|
img.finish(color=color, dashes=dashes, width=width, closePath=False,
|
|
lineCap=lineCap, lineJoin=lineJoin, morph=morph, roundCap=roundCap)
|
|
img.commit(overlay)
|
|
|
|
return p
|
|
|
|
def drawZigzag(page, p1, p2, breadth = 2, color=None, dashes=None,
|
|
width=1, lineCap=0, lineJoin=0, overlay=True, morph=None, roundCap=None):
|
|
"""Draw a zigzag line from point p1 to point p2.
|
|
"""
|
|
img = page.newShape()
|
|
p = img.drawZigzag(Point(p1), Point(p2), breadth = breadth)
|
|
img.finish(color=color, dashes=dashes, width=width, closePath=False,
|
|
lineCap=lineCap, lineJoin=lineJoin, morph=morph, roundCap=roundCap)
|
|
img.commit(overlay)
|
|
|
|
return p
|
|
|
|
def drawRect(page, rect, color=None, fill=None, dashes=None,
|
|
width=1, lineCap=0, lineJoin=0, morph=None, roundCap=None, overlay=True):
|
|
"""Draw a rectangle.
|
|
"""
|
|
img = page.newShape()
|
|
Q = img.drawRect(Rect(rect))
|
|
img.finish(color=color, fill=fill, dashes=dashes, width=width,
|
|
lineCap=lineCap, lineJoin=lineJoin, morph=morph, roundCap=roundCap)
|
|
img.commit(overlay)
|
|
|
|
return Q
|
|
|
|
def drawQuad(page, quad, color=None, fill=None, dashes=None,
|
|
width=1, lineCap=0, lineJoin=0, morph=None, roundCap=None, overlay=True):
|
|
"""Draw a quadrilateral.
|
|
"""
|
|
img = page.newShape()
|
|
Q = img.drawQuad(Quad(quad))
|
|
img.finish(color=color, fill=fill, dashes=dashes, width=width,
|
|
lineCap=lineCap, lineJoin=lineJoin, morph=morph, roundCap=roundCap)
|
|
img.commit(overlay)
|
|
|
|
return Q
|
|
|
|
def drawPolyline(page, points, color=None, fill=None, dashes=None,
|
|
width=1, morph=None, lineCap=0, lineJoin=0, roundCap=None, overlay=True,
|
|
closePath=False):
|
|
"""Draw multiple connected line segments.
|
|
"""
|
|
img = page.newShape()
|
|
Q = img.drawPolyline(points)
|
|
img.finish(color=color, fill=fill, dashes=dashes, width=width,
|
|
lineCap=lineCap, lineJoin=lineJoin, morph=morph, roundCap=roundCap, closePath=closePath)
|
|
img.commit(overlay)
|
|
|
|
return Q
|
|
|
|
def drawCircle(page, center, radius, color=None, fill=None,
|
|
morph=None, dashes=None, width=1,
|
|
lineCap=0, lineJoin=0, roundCap=None, overlay=True):
|
|
"""Draw a circle given its center and radius.
|
|
"""
|
|
img = page.newShape()
|
|
Q = img.drawCircle(Point(center), radius)
|
|
img.finish(color=color, fill=fill, dashes=dashes, width=width,
|
|
lineCap=lineCap, lineJoin=lineJoin, morph=morph, roundCap=roundCap)
|
|
img.commit(overlay)
|
|
return Q
|
|
|
|
def drawOval(page, rect, color=None, fill=None, dashes=None,
|
|
morph=None,roundCap=None,
|
|
width=1, lineCap=0, lineJoin=0, overlay=True):
|
|
"""Draw an oval given its containing rectangle or quad.
|
|
"""
|
|
img = page.newShape()
|
|
Q = img.drawOval(rect)
|
|
img.finish(color=color, fill=fill, dashes=dashes, width=width,
|
|
lineCap=lineCap, lineJoin=lineJoin, morph=morph, roundCap=roundCap)
|
|
img.commit(overlay)
|
|
|
|
return Q
|
|
|
|
def drawCurve(page, p1, p2, p3, color=None, fill=None, dashes=None,
|
|
width=1, morph=None, roundCap=None, closePath=False,
|
|
lineCap=0, lineJoin=0, overlay=True):
|
|
"""Draw a special Bezier curve from p1 to p3, generating control points on lines p1 to p2 and p2 to p3.
|
|
"""
|
|
img = page.newShape()
|
|
Q = img.drawCurve(Point(p1), Point(p2), Point(p3))
|
|
img.finish(color=color, fill=fill, dashes=dashes, width=width,
|
|
lineCap=lineCap, lineJoin=lineJoin, morph=morph, roundCap=roundCap, closePath=closePath)
|
|
img.commit(overlay)
|
|
|
|
return Q
|
|
|
|
def drawBezier(page, p1, p2, p3, p4, color=None, fill=None,
|
|
dashes=None, width=1, morph=None, roundCap=None,
|
|
closePath=False, lineCap=0, lineJoin=0, overlay=True):
|
|
"""Draw a general cubic Bezier curve from p1 to p4 using control points p2 and p3.
|
|
"""
|
|
img = page.newShape()
|
|
Q = img.drawBezier(Point(p1), Point(p2), Point(p3), Point(p4))
|
|
img.finish(color=color, fill=fill, dashes=dashes, width=width,
|
|
lineCap=lineCap, lineJoin=lineJoin, morph=morph, roundCap=roundCap, closePath=closePath)
|
|
img.commit(overlay)
|
|
|
|
return Q
|
|
|
|
def drawSector(page, center, point, beta, color=None, fill=None,
|
|
dashes=None, fullSector=True, morph=None, roundCap=None,
|
|
width=1, closePath=False, lineCap=0, lineJoin=0, overlay=True):
|
|
""" Draw a circle sector given circle center, one arc end point and the angle of the arc.
|
|
|
|
Parameters:
|
|
center -- center of circle
|
|
point -- arc end point
|
|
beta -- angle of arc (degrees)
|
|
fullSector -- connect arc ends with center
|
|
"""
|
|
img = page.newShape()
|
|
Q = img.drawSector(Point(center), Point(point), beta, fullSector=fullSector)
|
|
img.finish(color=color, fill=fill, dashes=dashes, width=width,
|
|
lineCap=lineCap, lineJoin=lineJoin, morph=morph, roundCap=roundCap, closePath=closePath)
|
|
img.commit(overlay)
|
|
|
|
return Q
|
|
|
|
#----------------------------------------------------------------------
|
|
# Name: wx.lib.colourdb.py
|
|
# Purpose: Adds a bunch of colour names and RGB values to the
|
|
# colour database so they can be found by name
|
|
#
|
|
# Author: Robin Dunn
|
|
#
|
|
# Created: 13-March-2001
|
|
# Copyright: (c) 2001-2017 by Total Control Software
|
|
# Licence: wxWindows license
|
|
# Tags: phoenix-port, unittest, documented
|
|
#----------------------------------------------------------------------
|
|
|
|
def getColorList():
|
|
"""
|
|
Returns a list of just the colour names used by this module.
|
|
:rtype: list of strings
|
|
"""
|
|
|
|
return [ x[0] for x in getColorInfoList() ]
|
|
|
|
|
|
def getColorInfoList():
|
|
"""
|
|
Returns the list of colour name/value tuples used by this module.
|
|
:rtype: list of tuples
|
|
"""
|
|
|
|
return [
|
|
("ALICEBLUE", 240, 248, 255),
|
|
("ANTIQUEWHITE", 250, 235, 215),
|
|
("ANTIQUEWHITE1", 255, 239, 219),
|
|
("ANTIQUEWHITE2", 238, 223, 204),
|
|
("ANTIQUEWHITE3", 205, 192, 176),
|
|
("ANTIQUEWHITE4", 139, 131, 120),
|
|
("AQUAMARINE", 127, 255, 212),
|
|
("AQUAMARINE1", 127, 255, 212),
|
|
("AQUAMARINE2", 118, 238, 198),
|
|
("AQUAMARINE3", 102, 205, 170),
|
|
("AQUAMARINE4", 69, 139, 116),
|
|
("AZURE", 240, 255, 255),
|
|
("AZURE1", 240, 255, 255),
|
|
("AZURE2", 224, 238, 238),
|
|
("AZURE3", 193, 205, 205),
|
|
("AZURE4", 131, 139, 139),
|
|
("BEIGE", 245, 245, 220),
|
|
("BISQUE", 255, 228, 196),
|
|
("BISQUE1", 255, 228, 196),
|
|
("BISQUE2", 238, 213, 183),
|
|
("BISQUE3", 205, 183, 158),
|
|
("BISQUE4", 139, 125, 107),
|
|
("BLACK", 0, 0, 0),
|
|
("BLANCHEDALMOND", 255, 235, 205),
|
|
("BLUE", 0, 0, 255),
|
|
("BLUE1", 0, 0, 255),
|
|
("BLUE2", 0, 0, 238),
|
|
("BLUE3", 0, 0, 205),
|
|
("BLUE4", 0, 0, 139),
|
|
("BLUEVIOLET", 138, 43, 226),
|
|
("BROWN", 165, 42, 42),
|
|
("BROWN1", 255, 64, 64),
|
|
("BROWN2", 238, 59, 59),
|
|
("BROWN3", 205, 51, 51),
|
|
("BROWN4", 139, 35, 35),
|
|
("BURLYWOOD", 222, 184, 135),
|
|
("BURLYWOOD1", 255, 211, 155),
|
|
("BURLYWOOD2", 238, 197, 145),
|
|
("BURLYWOOD3", 205, 170, 125),
|
|
("BURLYWOOD4", 139, 115, 85),
|
|
("CADETBLUE", 95, 158, 160),
|
|
("CADETBLUE1", 152, 245, 255),
|
|
("CADETBLUE2", 142, 229, 238),
|
|
("CADETBLUE3", 122, 197, 205),
|
|
("CADETBLUE4", 83, 134, 139),
|
|
("CHARTREUSE", 127, 255, 0),
|
|
("CHARTREUSE1", 127, 255, 0),
|
|
("CHARTREUSE2", 118, 238, 0),
|
|
("CHARTREUSE3", 102, 205, 0),
|
|
("CHARTREUSE4", 69, 139, 0),
|
|
("CHOCOLATE", 210, 105, 30),
|
|
("CHOCOLATE1", 255, 127, 36),
|
|
("CHOCOLATE2", 238, 118, 33),
|
|
("CHOCOLATE3", 205, 102, 29),
|
|
("CHOCOLATE4", 139, 69, 19),
|
|
("COFFEE", 156, 79, 0),
|
|
("CORAL", 255, 127, 80),
|
|
("CORAL1", 255, 114, 86),
|
|
("CORAL2", 238, 106, 80),
|
|
("CORAL3", 205, 91, 69),
|
|
("CORAL4", 139, 62, 47),
|
|
("CORNFLOWERBLUE", 100, 149, 237),
|
|
("CORNSILK", 255, 248, 220),
|
|
("CORNSILK1", 255, 248, 220),
|
|
("CORNSILK2", 238, 232, 205),
|
|
("CORNSILK3", 205, 200, 177),
|
|
("CORNSILK4", 139, 136, 120),
|
|
("CYAN", 0, 255, 255),
|
|
("CYAN1", 0, 255, 255),
|
|
("CYAN2", 0, 238, 238),
|
|
("CYAN3", 0, 205, 205),
|
|
("CYAN4", 0, 139, 139),
|
|
("DARKBLUE", 0, 0, 139),
|
|
("DARKCYAN", 0, 139, 139),
|
|
("DARKGOLDENROD", 184, 134, 11),
|
|
("DARKGOLDENROD1", 255, 185, 15),
|
|
("DARKGOLDENROD2", 238, 173, 14),
|
|
("DARKGOLDENROD3", 205, 149, 12),
|
|
("DARKGOLDENROD4", 139, 101, 8),
|
|
("DARKGREEN", 0, 100, 0),
|
|
("DARKGRAY", 169, 169, 169),
|
|
("DARKKHAKI", 189, 183, 107),
|
|
("DARKMAGENTA", 139, 0, 139),
|
|
("DARKOLIVEGREEN", 85, 107, 47),
|
|
("DARKOLIVEGREEN1", 202, 255, 112),
|
|
("DARKOLIVEGREEN2", 188, 238, 104),
|
|
("DARKOLIVEGREEN3", 162, 205, 90),
|
|
("DARKOLIVEGREEN4", 110, 139, 61),
|
|
("DARKORANGE", 255, 140, 0),
|
|
("DARKORANGE1", 255, 127, 0),
|
|
("DARKORANGE2", 238, 118, 0),
|
|
("DARKORANGE3", 205, 102, 0),
|
|
("DARKORANGE4", 139, 69, 0),
|
|
("DARKORCHID", 153, 50, 204),
|
|
("DARKORCHID1", 191, 62, 255),
|
|
("DARKORCHID2", 178, 58, 238),
|
|
("DARKORCHID3", 154, 50, 205),
|
|
("DARKORCHID4", 104, 34, 139),
|
|
("DARKRED", 139, 0, 0),
|
|
("DARKSALMON", 233, 150, 122),
|
|
("DARKSEAGREEN", 143, 188, 143),
|
|
("DARKSEAGREEN1", 193, 255, 193),
|
|
("DARKSEAGREEN2", 180, 238, 180),
|
|
("DARKSEAGREEN3", 155, 205, 155),
|
|
("DARKSEAGREEN4", 105, 139, 105),
|
|
("DARKSLATEBLUE", 72, 61, 139),
|
|
("DARKSLATEGRAY", 47, 79, 79),
|
|
("DARKTURQUOISE", 0, 206, 209),
|
|
("DARKVIOLET", 148, 0, 211),
|
|
("DEEPPINK", 255, 20, 147),
|
|
("DEEPPINK1", 255, 20, 147),
|
|
("DEEPPINK2", 238, 18, 137),
|
|
("DEEPPINK3", 205, 16, 118),
|
|
("DEEPPINK4", 139, 10, 80),
|
|
("DEEPSKYBLUE", 0, 191, 255),
|
|
("DEEPSKYBLUE1", 0, 191, 255),
|
|
("DEEPSKYBLUE2", 0, 178, 238),
|
|
("DEEPSKYBLUE3", 0, 154, 205),
|
|
("DEEPSKYBLUE4", 0, 104, 139),
|
|
("DIMGRAY", 105, 105, 105),
|
|
("DODGERBLUE", 30, 144, 255),
|
|
("DODGERBLUE1", 30, 144, 255),
|
|
("DODGERBLUE2", 28, 134, 238),
|
|
("DODGERBLUE3", 24, 116, 205),
|
|
("DODGERBLUE4", 16, 78, 139),
|
|
("FIREBRICK", 178, 34, 34),
|
|
("FIREBRICK1", 255, 48, 48),
|
|
("FIREBRICK2", 238, 44, 44),
|
|
("FIREBRICK3", 205, 38, 38),
|
|
("FIREBRICK4", 139, 26, 26),
|
|
("FLORALWHITE", 255, 250, 240),
|
|
("FORESTGREEN", 34, 139, 34),
|
|
("GAINSBORO", 220, 220, 220),
|
|
("GHOSTWHITE", 248, 248, 255),
|
|
("GOLD", 255, 215, 0),
|
|
("GOLD1", 255, 215, 0),
|
|
("GOLD2", 238, 201, 0),
|
|
("GOLD3", 205, 173, 0),
|
|
("GOLD4", 139, 117, 0),
|
|
("GOLDENROD", 218, 165, 32),
|
|
("GOLDENROD1", 255, 193, 37),
|
|
("GOLDENROD2", 238, 180, 34),
|
|
("GOLDENROD3", 205, 155, 29),
|
|
("GOLDENROD4", 139, 105, 20),
|
|
("GREEN YELLOW", 173, 255, 47),
|
|
("GREEN", 0, 255, 0),
|
|
("GREEN1", 0, 255, 0),
|
|
("GREEN2", 0, 238, 0),
|
|
("GREEN3", 0, 205, 0),
|
|
("GREEN4", 0, 139, 0),
|
|
("GREENYELLOW", 173, 255, 47),
|
|
("GRAY", 190, 190, 190),
|
|
("GRAY0", 0, 0, 0),
|
|
("GRAY1", 3, 3, 3),
|
|
("GRAY10", 26, 26, 26),
|
|
("GRAY100", 255, 255, 255),
|
|
("GRAY11", 28, 28, 28),
|
|
("GRAY12", 31, 31, 31),
|
|
("GRAY13", 33, 33, 33),
|
|
("GRAY14", 36, 36, 36),
|
|
("GRAY15", 38, 38, 38),
|
|
("GRAY16", 41, 41, 41),
|
|
("GRAY17", 43, 43, 43),
|
|
("GRAY18", 46, 46, 46),
|
|
("GRAY19", 48, 48, 48),
|
|
("GRAY2", 5, 5, 5),
|
|
("GRAY20", 51, 51, 51),
|
|
("GRAY21", 54, 54, 54),
|
|
("GRAY22", 56, 56, 56),
|
|
("GRAY23", 59, 59, 59),
|
|
("GRAY24", 61, 61, 61),
|
|
("GRAY25", 64, 64, 64),
|
|
("GRAY26", 66, 66, 66),
|
|
("GRAY27", 69, 69, 69),
|
|
("GRAY28", 71, 71, 71),
|
|
("GRAY29", 74, 74, 74),
|
|
("GRAY3", 8, 8, 8),
|
|
("GRAY30", 77, 77, 77),
|
|
("GRAY31", 79, 79, 79),
|
|
("GRAY32", 82, 82, 82),
|
|
("GRAY33", 84, 84, 84),
|
|
("GRAY34", 87, 87, 87),
|
|
("GRAY35", 89, 89, 89),
|
|
("GRAY36", 92, 92, 92),
|
|
("GRAY37", 94, 94, 94),
|
|
("GRAY38", 97, 97, 97),
|
|
("GRAY39", 99, 99, 99),
|
|
("GRAY4", 10, 10, 10),
|
|
("GRAY40", 102, 102, 102),
|
|
("GRAY41", 105, 105, 105),
|
|
("GRAY42", 107, 107, 107),
|
|
("GRAY43", 110, 110, 110),
|
|
("GRAY44", 112, 112, 112),
|
|
("GRAY45", 115, 115, 115),
|
|
("GRAY46", 117, 117, 117),
|
|
("GRAY47", 120, 120, 120),
|
|
("GRAY48", 122, 122, 122),
|
|
("GRAY49", 125, 125, 125),
|
|
("GRAY5", 13, 13, 13),
|
|
("GRAY50", 127, 127, 127),
|
|
("GRAY51", 130, 130, 130),
|
|
("GRAY52", 133, 133, 133),
|
|
("GRAY53", 135, 135, 135),
|
|
("GRAY54", 138, 138, 138),
|
|
("GRAY55", 140, 140, 140),
|
|
("GRAY56", 143, 143, 143),
|
|
("GRAY57", 145, 145, 145),
|
|
("GRAY58", 148, 148, 148),
|
|
("GRAY59", 150, 150, 150),
|
|
("GRAY6", 15, 15, 15),
|
|
("GRAY60", 153, 153, 153),
|
|
("GRAY61", 156, 156, 156),
|
|
("GRAY62", 158, 158, 158),
|
|
("GRAY63", 161, 161, 161),
|
|
("GRAY64", 163, 163, 163),
|
|
("GRAY65", 166, 166, 166),
|
|
("GRAY66", 168, 168, 168),
|
|
("GRAY67", 171, 171, 171),
|
|
("GRAY68", 173, 173, 173),
|
|
("GRAY69", 176, 176, 176),
|
|
("GRAY7", 18, 18, 18),
|
|
("GRAY70", 179, 179, 179),
|
|
("GRAY71", 181, 181, 181),
|
|
("GRAY72", 184, 184, 184),
|
|
("GRAY73", 186, 186, 186),
|
|
("GRAY74", 189, 189, 189),
|
|
("GRAY75", 191, 191, 191),
|
|
("GRAY76", 194, 194, 194),
|
|
("GRAY77", 196, 196, 196),
|
|
("GRAY78", 199, 199, 199),
|
|
("GRAY79", 201, 201, 201),
|
|
("GRAY8", 20, 20, 20),
|
|
("GRAY80", 204, 204, 204),
|
|
("GRAY81", 207, 207, 207),
|
|
("GRAY82", 209, 209, 209),
|
|
("GRAY83", 212, 212, 212),
|
|
("GRAY84", 214, 214, 214),
|
|
("GRAY85", 217, 217, 217),
|
|
("GRAY86", 219, 219, 219),
|
|
("GRAY87", 222, 222, 222),
|
|
("GRAY88", 224, 224, 224),
|
|
("GRAY89", 227, 227, 227),
|
|
("GRAY9", 23, 23, 23),
|
|
("GRAY90", 229, 229, 229),
|
|
("GRAY91", 232, 232, 232),
|
|
("GRAY92", 235, 235, 235),
|
|
("GRAY93", 237, 237, 237),
|
|
("GRAY94", 240, 240, 240),
|
|
("GRAY95", 242, 242, 242),
|
|
("GRAY96", 245, 245, 245),
|
|
("GRAY97", 247, 247, 247),
|
|
("GRAY98", 250, 250, 250),
|
|
("GRAY99", 252, 252, 252),
|
|
("HONEYDEW", 240, 255, 240),
|
|
("HONEYDEW1", 240, 255, 240),
|
|
("HONEYDEW2", 224, 238, 224),
|
|
("HONEYDEW3", 193, 205, 193),
|
|
("HONEYDEW4", 131, 139, 131),
|
|
("HOTPINK", 255, 105, 180),
|
|
("HOTPINK1", 255, 110, 180),
|
|
("HOTPINK2", 238, 106, 167),
|
|
("HOTPINK3", 205, 96, 144),
|
|
("HOTPINK4", 139, 58, 98),
|
|
("INDIANRED", 205, 92, 92),
|
|
("INDIANRED1", 255, 106, 106),
|
|
("INDIANRED2", 238, 99, 99),
|
|
("INDIANRED3", 205, 85, 85),
|
|
("INDIANRED4", 139, 58, 58),
|
|
("IVORY", 255, 255, 240),
|
|
("IVORY1", 255, 255, 240),
|
|
("IVORY2", 238, 238, 224),
|
|
("IVORY3", 205, 205, 193),
|
|
("IVORY4", 139, 139, 131),
|
|
("KHAKI", 240, 230, 140),
|
|
("KHAKI1", 255, 246, 143),
|
|
("KHAKI2", 238, 230, 133),
|
|
("KHAKI3", 205, 198, 115),
|
|
("KHAKI4", 139, 134, 78),
|
|
("LAVENDER", 230, 230, 250),
|
|
("LAVENDERBLUSH", 255, 240, 245),
|
|
("LAVENDERBLUSH1", 255, 240, 245),
|
|
("LAVENDERBLUSH2", 238, 224, 229),
|
|
("LAVENDERBLUSH3", 205, 193, 197),
|
|
("LAVENDERBLUSH4", 139, 131, 134),
|
|
("LAWNGREEN", 124, 252, 0),
|
|
("LEMONCHIFFON", 255, 250, 205),
|
|
("LEMONCHIFFON1", 255, 250, 205),
|
|
("LEMONCHIFFON2", 238, 233, 191),
|
|
("LEMONCHIFFON3", 205, 201, 165),
|
|
("LEMONCHIFFON4", 139, 137, 112),
|
|
("LIGHTBLUE", 173, 216, 230),
|
|
("LIGHTBLUE1", 191, 239, 255),
|
|
("LIGHTBLUE2", 178, 223, 238),
|
|
("LIGHTBLUE3", 154, 192, 205),
|
|
("LIGHTBLUE4", 104, 131, 139),
|
|
("LIGHTCORAL", 240, 128, 128),
|
|
("LIGHTCYAN", 224, 255, 255),
|
|
("LIGHTCYAN1", 224, 255, 255),
|
|
("LIGHTCYAN2", 209, 238, 238),
|
|
("LIGHTCYAN3", 180, 205, 205),
|
|
("LIGHTCYAN4", 122, 139, 139),
|
|
("LIGHTGOLDENROD", 238, 221, 130),
|
|
("LIGHTGOLDENROD1", 255, 236, 139),
|
|
("LIGHTGOLDENROD2", 238, 220, 130),
|
|
("LIGHTGOLDENROD3", 205, 190, 112),
|
|
("LIGHTGOLDENROD4", 139, 129, 76),
|
|
("LIGHTGOLDENRODYELLOW", 250, 250, 210),
|
|
("LIGHTGREEN", 144, 238, 144),
|
|
("LIGHTGRAY", 211, 211, 211),
|
|
("LIGHTPINK", 255, 182, 193),
|
|
("LIGHTPINK1", 255, 174, 185),
|
|
("LIGHTPINK2", 238, 162, 173),
|
|
("LIGHTPINK3", 205, 140, 149),
|
|
("LIGHTPINK4", 139, 95, 101),
|
|
("LIGHTSALMON", 255, 160, 122),
|
|
("LIGHTSALMON1", 255, 160, 122),
|
|
("LIGHTSALMON2", 238, 149, 114),
|
|
("LIGHTSALMON3", 205, 129, 98),
|
|
("LIGHTSALMON4", 139, 87, 66),
|
|
("LIGHTSEAGREEN", 32, 178, 170),
|
|
("LIGHTSKYBLUE", 135, 206, 250),
|
|
("LIGHTSKYBLUE1", 176, 226, 255),
|
|
("LIGHTSKYBLUE2", 164, 211, 238),
|
|
("LIGHTSKYBLUE3", 141, 182, 205),
|
|
("LIGHTSKYBLUE4", 96, 123, 139),
|
|
("LIGHTSLATEBLUE", 132, 112, 255),
|
|
("LIGHTSLATEGRAY", 119, 136, 153),
|
|
("LIGHTSTEELBLUE", 176, 196, 222),
|
|
("LIGHTSTEELBLUE1", 202, 225, 255),
|
|
("LIGHTSTEELBLUE2", 188, 210, 238),
|
|
("LIGHTSTEELBLUE3", 162, 181, 205),
|
|
("LIGHTSTEELBLUE4", 110, 123, 139),
|
|
("LIGHTYELLOW", 255, 255, 224),
|
|
("LIGHTYELLOW1", 255, 255, 224),
|
|
("LIGHTYELLOW2", 238, 238, 209),
|
|
("LIGHTYELLOW3", 205, 205, 180),
|
|
("LIGHTYELLOW4", 139, 139, 122),
|
|
("LIMEGREEN", 50, 205, 50),
|
|
("LINEN", 250, 240, 230),
|
|
("MAGENTA", 255, 0, 255),
|
|
("MAGENTA1", 255, 0, 255),
|
|
("MAGENTA2", 238, 0, 238),
|
|
("MAGENTA3", 205, 0, 205),
|
|
("MAGENTA4", 139, 0, 139),
|
|
("MAROON", 176, 48, 96),
|
|
("MAROON1", 255, 52, 179),
|
|
("MAROON2", 238, 48, 167),
|
|
("MAROON3", 205, 41, 144),
|
|
("MAROON4", 139, 28, 98),
|
|
("MEDIUMAQUAMARINE", 102, 205, 170),
|
|
("MEDIUMBLUE", 0, 0, 205),
|
|
("MEDIUMORCHID", 186, 85, 211),
|
|
("MEDIUMORCHID1", 224, 102, 255),
|
|
("MEDIUMORCHID2", 209, 95, 238),
|
|
("MEDIUMORCHID3", 180, 82, 205),
|
|
("MEDIUMORCHID4", 122, 55, 139),
|
|
("MEDIUMPURPLE", 147, 112, 219),
|
|
("MEDIUMPURPLE1", 171, 130, 255),
|
|
("MEDIUMPURPLE2", 159, 121, 238),
|
|
("MEDIUMPURPLE3", 137, 104, 205),
|
|
("MEDIUMPURPLE4", 93, 71, 139),
|
|
("MEDIUMSEAGREEN", 60, 179, 113),
|
|
("MEDIUMSLATEBLUE", 123, 104, 238),
|
|
("MEDIUMSPRINGGREEN", 0, 250, 154),
|
|
("MEDIUMTURQUOISE", 72, 209, 204),
|
|
("MEDIUMVIOLETRED", 199, 21, 133),
|
|
("MIDNIGHTBLUE", 25, 25, 112),
|
|
("MINTCREAM", 245, 255, 250),
|
|
("MISTYROSE", 255, 228, 225),
|
|
("MISTYROSE1", 255, 228, 225),
|
|
("MISTYROSE2", 238, 213, 210),
|
|
("MISTYROSE3", 205, 183, 181),
|
|
("MISTYROSE4", 139, 125, 123),
|
|
("MOCCASIN", 255, 228, 181),
|
|
("MUPDFBLUE", 37, 114, 172),
|
|
("NAVAJOWHITE", 255, 222, 173),
|
|
("NAVAJOWHITE1", 255, 222, 173),
|
|
("NAVAJOWHITE2", 238, 207, 161),
|
|
("NAVAJOWHITE3", 205, 179, 139),
|
|
("NAVAJOWHITE4", 139, 121, 94),
|
|
("NAVY", 0, 0, 128),
|
|
("NAVYBLUE", 0, 0, 128),
|
|
("OLDLACE", 253, 245, 230),
|
|
("OLIVEDRAB", 107, 142, 35),
|
|
("OLIVEDRAB1", 192, 255, 62),
|
|
("OLIVEDRAB2", 179, 238, 58),
|
|
("OLIVEDRAB3", 154, 205, 50),
|
|
("OLIVEDRAB4", 105, 139, 34),
|
|
("ORANGE", 255, 165, 0),
|
|
("ORANGE1", 255, 165, 0),
|
|
("ORANGE2", 238, 154, 0),
|
|
("ORANGE3", 205, 133, 0),
|
|
("ORANGE4", 139, 90, 0),
|
|
("ORANGERED", 255, 69, 0),
|
|
("ORANGERED1", 255, 69, 0),
|
|
("ORANGERED2", 238, 64, 0),
|
|
("ORANGERED3", 205, 55, 0),
|
|
("ORANGERED4", 139, 37, 0),
|
|
("ORCHID", 218, 112, 214),
|
|
("ORCHID1", 255, 131, 250),
|
|
("ORCHID2", 238, 122, 233),
|
|
("ORCHID3", 205, 105, 201),
|
|
("ORCHID4", 139, 71, 137),
|
|
("PALEGOLDENROD", 238, 232, 170),
|
|
("PALEGREEN", 152, 251, 152),
|
|
("PALEGREEN1", 154, 255, 154),
|
|
("PALEGREEN2", 144, 238, 144),
|
|
("PALEGREEN3", 124, 205, 124),
|
|
("PALEGREEN4", 84, 139, 84),
|
|
("PALETURQUOISE", 175, 238, 238),
|
|
("PALETURQUOISE1", 187, 255, 255),
|
|
("PALETURQUOISE2", 174, 238, 238),
|
|
("PALETURQUOISE3", 150, 205, 205),
|
|
("PALETURQUOISE4", 102, 139, 139),
|
|
("PALEVIOLETRED", 219, 112, 147),
|
|
("PALEVIOLETRED1", 255, 130, 171),
|
|
("PALEVIOLETRED2", 238, 121, 159),
|
|
("PALEVIOLETRED3", 205, 104, 137),
|
|
("PALEVIOLETRED4", 139, 71, 93),
|
|
("PAPAYAWHIP", 255, 239, 213),
|
|
("PEACHPUFF", 255, 218, 185),
|
|
("PEACHPUFF1", 255, 218, 185),
|
|
("PEACHPUFF2", 238, 203, 173),
|
|
("PEACHPUFF3", 205, 175, 149),
|
|
("PEACHPUFF4", 139, 119, 101),
|
|
("PERU", 205, 133, 63),
|
|
("PINK", 255, 192, 203),
|
|
("PINK1", 255, 181, 197),
|
|
("PINK2", 238, 169, 184),
|
|
("PINK3", 205, 145, 158),
|
|
("PINK4", 139, 99, 108),
|
|
("PLUM", 221, 160, 221),
|
|
("PLUM1", 255, 187, 255),
|
|
("PLUM2", 238, 174, 238),
|
|
("PLUM3", 205, 150, 205),
|
|
("PLUM4", 139, 102, 139),
|
|
("POWDERBLUE", 176, 224, 230),
|
|
("PURPLE", 160, 32, 240),
|
|
("PURPLE1", 155, 48, 255),
|
|
("PURPLE2", 145, 44, 238),
|
|
("PURPLE3", 125, 38, 205),
|
|
("PURPLE4", 85, 26, 139),
|
|
("PY_COLOR", 240, 255, 210),
|
|
("RED", 255, 0, 0),
|
|
("RED1", 255, 0, 0),
|
|
("RED2", 238, 0, 0),
|
|
("RED3", 205, 0, 0),
|
|
("RED4", 139, 0, 0),
|
|
("ROSYBROWN", 188, 143, 143),
|
|
("ROSYBROWN1", 255, 193, 193),
|
|
("ROSYBROWN2", 238, 180, 180),
|
|
("ROSYBROWN3", 205, 155, 155),
|
|
("ROSYBROWN4", 139, 105, 105),
|
|
("ROYALBLUE", 65, 105, 225),
|
|
("ROYALBLUE1", 72, 118, 255),
|
|
("ROYALBLUE2", 67, 110, 238),
|
|
("ROYALBLUE3", 58, 95, 205),
|
|
("ROYALBLUE4", 39, 64, 139),
|
|
("SADDLEBROWN", 139, 69, 19),
|
|
("SALMON", 250, 128, 114),
|
|
("SALMON1", 255, 140, 105),
|
|
("SALMON2", 238, 130, 98),
|
|
("SALMON3", 205, 112, 84),
|
|
("SALMON4", 139, 76, 57),
|
|
("SANDYBROWN", 244, 164, 96),
|
|
("SEAGREEN", 46, 139, 87),
|
|
("SEAGREEN1", 84, 255, 159),
|
|
("SEAGREEN2", 78, 238, 148),
|
|
("SEAGREEN3", 67, 205, 128),
|
|
("SEAGREEN4", 46, 139, 87),
|
|
("SEASHELL", 255, 245, 238),
|
|
("SEASHELL1", 255, 245, 238),
|
|
("SEASHELL2", 238, 229, 222),
|
|
("SEASHELL3", 205, 197, 191),
|
|
("SEASHELL4", 139, 134, 130),
|
|
("SIENNA", 160, 82, 45),
|
|
("SIENNA1", 255, 130, 71),
|
|
("SIENNA2", 238, 121, 66),
|
|
("SIENNA3", 205, 104, 57),
|
|
("SIENNA4", 139, 71, 38),
|
|
("SKYBLUE", 135, 206, 235),
|
|
("SKYBLUE1", 135, 206, 255),
|
|
("SKYBLUE2", 126, 192, 238),
|
|
("SKYBLUE3", 108, 166, 205),
|
|
("SKYBLUE4", 74, 112, 139),
|
|
("SLATEBLUE", 106, 90, 205),
|
|
("SLATEBLUE1", 131, 111, 255),
|
|
("SLATEBLUE2", 122, 103, 238),
|
|
("SLATEBLUE3", 105, 89, 205),
|
|
("SLATEBLUE4", 71, 60, 139),
|
|
("SLATEGRAY", 112, 128, 144),
|
|
("SNOW", 255, 250, 250),
|
|
("SNOW1", 255, 250, 250),
|
|
("SNOW2", 238, 233, 233),
|
|
("SNOW3", 205, 201, 201),
|
|
("SNOW4", 139, 137, 137),
|
|
("SPRINGGREEN", 0, 255, 127),
|
|
("SPRINGGREEN1", 0, 255, 127),
|
|
("SPRINGGREEN2", 0, 238, 118),
|
|
("SPRINGGREEN3", 0, 205, 102),
|
|
("SPRINGGREEN4", 0, 139, 69),
|
|
("STEELBLUE", 70, 130, 180),
|
|
("STEELBLUE1", 99, 184, 255),
|
|
("STEELBLUE2", 92, 172, 238),
|
|
("STEELBLUE3", 79, 148, 205),
|
|
("STEELBLUE4", 54, 100, 139),
|
|
("TAN", 210, 180, 140),
|
|
("TAN1", 255, 165, 79),
|
|
("TAN2", 238, 154, 73),
|
|
("TAN3", 205, 133, 63),
|
|
("TAN4", 139, 90, 43),
|
|
("THISTLE", 216, 191, 216),
|
|
("THISTLE1", 255, 225, 255),
|
|
("THISTLE2", 238, 210, 238),
|
|
("THISTLE3", 205, 181, 205),
|
|
("THISTLE4", 139, 123, 139),
|
|
("TOMATO", 255, 99, 71),
|
|
("TOMATO1", 255, 99, 71),
|
|
("TOMATO2", 238, 92, 66),
|
|
("TOMATO3", 205, 79, 57),
|
|
("TOMATO4", 139, 54, 38),
|
|
("TURQUOISE", 64, 224, 208),
|
|
("TURQUOISE1", 0, 245, 255),
|
|
("TURQUOISE2", 0, 229, 238),
|
|
("TURQUOISE3", 0, 197, 205),
|
|
("TURQUOISE4", 0, 134, 139),
|
|
("VIOLET", 238, 130, 238),
|
|
("VIOLETRED", 208, 32, 144),
|
|
("VIOLETRED1", 255, 62, 150),
|
|
("VIOLETRED2", 238, 58, 140),
|
|
("VIOLETRED3", 205, 50, 120),
|
|
("VIOLETRED4", 139, 34, 82),
|
|
("WHEAT", 245, 222, 179),
|
|
("WHEAT1", 255, 231, 186),
|
|
("WHEAT2", 238, 216, 174),
|
|
("WHEAT3", 205, 186, 150),
|
|
("WHEAT4", 139, 126, 102),
|
|
("WHITE", 255, 255, 255),
|
|
("WHITESMOKE", 245, 245, 245),
|
|
("YELLOW", 255, 255, 0),
|
|
("YELLOW1", 255, 255, 0),
|
|
("YELLOW2", 238, 238, 0),
|
|
("YELLOW3", 205, 205, 0),
|
|
("YELLOW4", 139, 139, 0),
|
|
("YELLOWGREEN", 154, 205, 50),
|
|
]
|
|
|
|
def getColor(name):
|
|
"""Retrieve RGB color in PDF format by name.
|
|
|
|
Returns:
|
|
a triple of floats in range 0 to 1. In case of name-not-found, "white" is returned.
|
|
"""
|
|
try:
|
|
c = getColorInfoList()[getColorList().index(name.upper())]
|
|
return (c[1] / 255., c[2] / 255., c[3] / 255.)
|
|
except:
|
|
return (1, 1, 1)
|
|
|
|
def getColorHSV(name):
|
|
"""Retrieve the hue, saturation, value triple of a color name.
|
|
|
|
Returns:
|
|
a triple (degree, percent, percent). If not found (-1, -1, -1) is returned.
|
|
"""
|
|
try:
|
|
x = getColorInfoList()[getColorList().index(name.upper())]
|
|
except:
|
|
return (-1, -1, -1)
|
|
|
|
r = x[1] / 255.
|
|
g = x[2] / 255.
|
|
b = x[3] / 255.
|
|
cmax = max(r, g, b)
|
|
V = round(cmax * 100, 1)
|
|
cmin = min(r, g, b)
|
|
delta = cmax - cmin
|
|
if delta == 0:
|
|
hue = 0
|
|
elif cmax == r:
|
|
hue = 60. * (((g - b)/delta) % 6)
|
|
elif cmax == g:
|
|
hue = 60. * (((b - r)/delta) + 2)
|
|
else:
|
|
hue = 60. * (((r - g)/delta) + 4)
|
|
|
|
H = int(round(hue))
|
|
|
|
if cmax == 0:
|
|
sat = 0
|
|
else:
|
|
sat = delta / cmax
|
|
S = int(round(sat * 100))
|
|
|
|
return (H, S, V)
|
|
|
|
|
|
def getCharWidths(doc, xref, limit = 256, idx = 0):
|
|
"""Get list of glyph information of a font.
|
|
|
|
Notes:
|
|
Must be provided by its XREF number. If we already dealt with the
|
|
font, it will be recorded in doc.FontInfos. Otherwise we insert an
|
|
entry there.
|
|
Finally we return the glyphs for the font. This is a list of
|
|
(glyph, width) where glyph is an integer controlling the char
|
|
appearance, and width is a float controlling the char's spacing:
|
|
width * fontsize is the actual space.
|
|
For 'simple' fonts, glyph == ord(char) will usually be true.
|
|
Exceptions are 'Symbol' and 'ZapfDingbats'. We are providing data for these directly here.
|
|
"""
|
|
fontinfo = CheckFontInfo(doc, xref)
|
|
if fontinfo is None: # not recorded yet: create it
|
|
name, ext, stype, _ = doc.extractFont(xref, info_only = True)
|
|
fontdict = {"name": name, "type": stype, "ext": ext}
|
|
|
|
if ext == "":
|
|
raise ValueError("xref is not a font")
|
|
|
|
# check for 'simple' fonts
|
|
if stype in ("Type1", "MMType1", "TrueType"):
|
|
simple = True
|
|
else:
|
|
simple = False
|
|
|
|
# check for CJK fonts
|
|
if name in ("Fangti", "Ming"):
|
|
ordering = 0
|
|
elif name in ("Heiti", "Song"):
|
|
ordering = 1
|
|
elif name in ("Gothic", "Mincho"):
|
|
ordering = 2
|
|
elif name in ("Dotum", "Batang"):
|
|
ordering = 3
|
|
else:
|
|
ordering = -1
|
|
|
|
fontdict["simple"] = simple
|
|
|
|
if name == "ZapfDingbats":
|
|
glyphs = zapf_glyphs
|
|
elif name == "Symbol":
|
|
glyphs = symbol_glyphs
|
|
else:
|
|
glyphs = None
|
|
|
|
fontdict["glyphs"] = glyphs
|
|
fontdict["ordering"] = ordering
|
|
fontinfo = [xref, fontdict]
|
|
doc.FontInfos.append(fontinfo)
|
|
else:
|
|
fontdict = fontinfo[1]
|
|
glyphs = fontdict["glyphs"]
|
|
simple = fontdict["simple"]
|
|
ordering = fontdict["ordering"]
|
|
|
|
if glyphs is None:
|
|
oldlimit = 0
|
|
else:
|
|
oldlimit = len(glyphs)
|
|
|
|
mylimit = max(256, limit)
|
|
|
|
if mylimit <= oldlimit:
|
|
return glyphs
|
|
|
|
if ordering < 0: # not a CJK font
|
|
glyphs = doc._getCharWidths(xref, fontdict["name"],
|
|
fontdict["ext"],
|
|
fontdict["ordering"],
|
|
mylimit, idx)
|
|
else: # CJK fonts use char codes and width = 1
|
|
glyphs = None
|
|
|
|
fontdict["glyphs"] = glyphs
|
|
fontinfo[1] = fontdict
|
|
UpdateFontInfo(doc, fontinfo)
|
|
|
|
return glyphs
|
|
|
|
class Shape(object):
|
|
"""Create a new shape.
|
|
"""
|
|
|
|
@staticmethod
|
|
def horizontal_angle(C, P):
|
|
"""Return the angle to the horizontal for the connection from C to P.
|
|
This uses the arcus sine function and resolves its inherent ambiguity by
|
|
looking up in which quadrant vector S = P - C is located.
|
|
"""
|
|
S = Point(P - C).unit # unit vector 'C' -> 'P'
|
|
alfa = math.asin(abs(S.y)) # absolute angle from horizontal
|
|
if S.x < 0: # make arcsin result unique
|
|
if S.y <= 0: # bottom-left
|
|
alfa = -(math.pi - alfa)
|
|
else: # top-left
|
|
alfa = math.pi - alfa
|
|
else:
|
|
if S.y >= 0: # top-right
|
|
pass
|
|
else: # bottom-right
|
|
alfa = - alfa
|
|
return alfa
|
|
|
|
def __init__(self, page):
|
|
CheckParent(page)
|
|
self.page = page
|
|
self.doc = page.parent
|
|
if not self.doc.isPDF:
|
|
raise ValueError("not a PDF")
|
|
self.height = page.MediaBoxSize.y
|
|
self.width = page.MediaBoxSize.x
|
|
self.x = page.CropBoxPosition.x
|
|
self.y = page.CropBoxPosition.y
|
|
self.pctm = page._getTransformation() # page transf. matrix
|
|
self.ipctm = ~self.pctm # inverted transf. matrix
|
|
self.draw_cont = ""
|
|
self.text_cont = ""
|
|
self.totalcont = ""
|
|
self.lastPoint = None
|
|
self.rect = None
|
|
|
|
def updateRect(self, x):
|
|
if self.rect is None:
|
|
if len(x) == 2:
|
|
self.rect = Rect(x, x)
|
|
else:
|
|
self.rect = Rect(x)
|
|
|
|
else:
|
|
if len(x) == 2:
|
|
x = Point(x)
|
|
self.rect.x0 = min(self.rect.x0, x.x)
|
|
self.rect.y0 = min(self.rect.y0, x.y)
|
|
self.rect.x1 = max(self.rect.x1, x.x)
|
|
self.rect.y1 = max(self.rect.y1, x.y)
|
|
else:
|
|
x = Rect(x)
|
|
self.rect.x0 = min(self.rect.x0, x.x0)
|
|
self.rect.y0 = min(self.rect.y0, x.y0)
|
|
self.rect.x1 = max(self.rect.x1, x.x1)
|
|
self.rect.y1 = max(self.rect.y1, x.y1)
|
|
|
|
def drawLine(self, p1, p2):
|
|
"""Draw a line between two points.
|
|
"""
|
|
p1 = Point(p1)
|
|
p2 = Point(p2)
|
|
if not (self.lastPoint == p1):
|
|
self.draw_cont += "%g %g m\n" % JM_TUPLE(p1 * self.ipctm)
|
|
self.lastPoint = p1
|
|
self.updateRect(p1)
|
|
|
|
self.draw_cont += "%g %g l\n" % JM_TUPLE(p2 * self.ipctm)
|
|
self.updateRect(p2)
|
|
self.lastPoint = p2
|
|
return self.lastPoint
|
|
|
|
def drawPolyline(self, points):
|
|
"""Draw several connected line segments.
|
|
"""
|
|
for i, p in enumerate(points):
|
|
if i == 0:
|
|
if not (self.lastPoint == Point(p)):
|
|
self.draw_cont += "%g %g m\n" % JM_TUPLE(Point(p) * self.ipctm)
|
|
self.lastPoint = Point(p)
|
|
else:
|
|
self.draw_cont += "%g %g l\n" % JM_TUPLE(Point(p) * self.ipctm)
|
|
self.updateRect(p)
|
|
|
|
self.lastPoint = Point(points[-1])
|
|
return self.lastPoint
|
|
|
|
def drawBezier(self, p1, p2, p3, p4):
|
|
"""Draw a standard cubic Bezier curve.
|
|
"""
|
|
p1 = Point(p1)
|
|
p2 = Point(p2)
|
|
p3 = Point(p3)
|
|
p4 = Point(p4)
|
|
if not (self.lastPoint == p1):
|
|
self.draw_cont += "%g %g m\n" % JM_TUPLE(p1 * self.ipctm)
|
|
self.draw_cont += "%g %g %g %g %g %g c\n" % JM_TUPLE(list(p2 * self.ipctm) + \
|
|
list(p3 * self.ipctm) + \
|
|
list(p4 * self.ipctm))
|
|
self.updateRect(p1)
|
|
self.updateRect(p2)
|
|
self.updateRect(p3)
|
|
self.updateRect(p4)
|
|
self.lastPoint = p4
|
|
return self.lastPoint
|
|
|
|
def drawOval(self, tetra):
|
|
"""Draw an ellipse inside a tetrapod.
|
|
"""
|
|
if len(tetra) != 4:
|
|
raise ValueError("invalid arg length")
|
|
if hasattr(tetra[0], "__float__"):
|
|
q = Rect(tetra).quad
|
|
else:
|
|
q = Quad(tetra)
|
|
|
|
mt = q.ul + (q.ur - q.ul) * 0.5
|
|
mr = q.ur + (q.lr - q.ur) * 0.5
|
|
mb = q.ll + (q.lr - q.ll) * 0.5
|
|
ml = q.ul + (q.ll - q.ul) * 0.5
|
|
if not (self.lastPoint == ml):
|
|
self.draw_cont += "%g %g m\n" % JM_TUPLE(ml * self.ipctm)
|
|
self.lastPoint = ml
|
|
self.drawCurve(ml, q.ll, mb)
|
|
self.drawCurve(mb, q.lr, mr)
|
|
self.drawCurve(mr, q.ur, mt)
|
|
self.drawCurve(mt, q.ul, ml)
|
|
self.updateRect(q.rect)
|
|
self.lastPoint = ml
|
|
return self.lastPoint
|
|
|
|
def drawCircle(self, center, radius):
|
|
"""Draw a circle given its center and radius.
|
|
"""
|
|
if not radius > EPSILON:
|
|
raise ValueError("radius must be postive")
|
|
center = Point(center)
|
|
p1 = center - (radius, 0)
|
|
return self.drawSector(center, p1, 360, fullSector=False)
|
|
|
|
def drawCurve(self, p1, p2, p3):
|
|
"""Draw a curve between points using one control point.
|
|
"""
|
|
kappa = 0.55228474983
|
|
p1 = Point(p1)
|
|
p2 = Point(p2)
|
|
p3 = Point(p3)
|
|
k1 = p1 + (p2 - p1) * kappa
|
|
k2 = p3 + (p2 - p3) * kappa
|
|
return self.drawBezier(p1, k1, k2, p3)
|
|
|
|
def drawSector(self, center, point, beta, fullSector=True):
|
|
"""Draw a circle sector.
|
|
"""
|
|
center = Point(center)
|
|
point = Point(point)
|
|
l3 = "%g %g m\n"
|
|
l4 = "%g %g %g %g %g %g c\n"
|
|
l5 = "%g %g l\n"
|
|
betar = math.radians(-beta)
|
|
w360 = math.radians(math.copysign(360, betar)) * (-1)
|
|
w90 = math.radians(math.copysign(90, betar))
|
|
w45 = w90 / 2
|
|
while abs(betar) > 2 * math.pi:
|
|
betar += w360 # bring angle below 360 degrees
|
|
if not (self.lastPoint == point):
|
|
self.draw_cont += l3 % JM_TUPLE(point * self.ipctm)
|
|
self.lastPoint = point
|
|
Q = Point(0, 0) # just make sure it exists
|
|
C = center
|
|
P = point
|
|
S = P - C # vector 'center' -> 'point'
|
|
rad = abs(S) # circle radius
|
|
|
|
if not rad > EPSILON:
|
|
raise ValueError("radius must be positive")
|
|
|
|
alfa = self.horizontal_angle(center, point)
|
|
while abs(betar) > abs(w90): # draw 90 degree arcs
|
|
q1 = C.x + math.cos(alfa + w90) * rad
|
|
q2 = C.y + math.sin(alfa + w90) * rad
|
|
Q = Point(q1, q2) # the arc's end point
|
|
r1 = C.x + math.cos(alfa + w45) * rad / math.cos(w45)
|
|
r2 = C.y + math.sin(alfa + w45) * rad / math.cos(w45)
|
|
R = Point(r1, r2) # crossing point of tangents
|
|
kappah = (1 - math.cos(w45)) * 4 / 3 / abs(R - Q)
|
|
kappa = kappah * abs(P - Q)
|
|
cp1 = P + (R - P) * kappa # control point 1
|
|
cp2 = Q + (R - Q) * kappa # control point 2
|
|
self.draw_cont += l4 % JM_TUPLE(list(cp1 * self.ipctm) + \
|
|
list(cp2 * self.ipctm) + \
|
|
list(Q * self.ipctm))
|
|
|
|
betar -= w90 # reduce parm angle by 90 deg
|
|
alfa += w90 # advance start angle by 90 deg
|
|
P = Q # advance to arc end point
|
|
# draw (remaining) arc
|
|
if abs(betar) > 1e-3: # significant degrees left?
|
|
beta2 = betar / 2
|
|
q1 = C.x + math.cos(alfa + betar) * rad
|
|
q2 = C.y + math.sin(alfa + betar) * rad
|
|
Q = Point(q1, q2) # the arc's end point
|
|
r1 = C.x + math.cos(alfa + beta2) * rad / math.cos(beta2)
|
|
r2 = C.y + math.sin(alfa + beta2) * rad / math.cos(beta2)
|
|
R = Point(r1, r2) # crossing point of tangents
|
|
# kappa height is 4/3 of segment height
|
|
kappah = (1 - math.cos(beta2)) * 4 / 3 / abs(R - Q) # kappa height
|
|
kappa = kappah * abs(P - Q) / (1 - math.cos(betar))
|
|
cp1 = P + (R - P) * kappa # control point 1
|
|
cp2 = Q + (R - Q) * kappa # control point 2
|
|
self.draw_cont += l4 % JM_TUPLE(list(cp1 * self.ipctm) + \
|
|
list(cp2 * self.ipctm) + \
|
|
list(Q * self.ipctm))
|
|
if fullSector:
|
|
self.draw_cont += l3 % JM_TUPLE(point * self.ipctm)
|
|
self.draw_cont += l5 % JM_TUPLE(center * self.ipctm)
|
|
self.draw_cont += l5 % JM_TUPLE(Q * self.ipctm)
|
|
self.lastPoint = Q
|
|
return self.lastPoint
|
|
|
|
def drawRect(self, rect):
|
|
"""Draw a rectangle.
|
|
"""
|
|
r = Rect(rect)
|
|
self.draw_cont += "%g %g %g %g re\n" % JM_TUPLE(list(r.bl * self.ipctm) + \
|
|
[r.width, r.height])
|
|
self.updateRect(r)
|
|
self.lastPoint = r.tl
|
|
return self.lastPoint
|
|
|
|
def drawQuad(self, quad):
|
|
"""Draw a Quad.
|
|
"""
|
|
q = Quad(quad)
|
|
return self.drawPolyline([q.ul, q.ll, q.lr, q.ur, q.ul])
|
|
|
|
def drawZigzag(self, p1, p2, breadth = 2):
|
|
"""Draw a zig-zagged line from p1 to p2.
|
|
"""
|
|
p1 = Point(p1)
|
|
p2 = Point(p2)
|
|
S = p2 - p1 # vector start - end
|
|
rad = abs(S) # distance of points
|
|
cnt = 4 * int(round(rad / (4 * breadth), 0)) # always take full phases
|
|
if cnt < 4:
|
|
raise ValueError("points too close")
|
|
mb = rad / cnt # revised breadth
|
|
matrix = TOOLS._hor_matrix(p1, p2) # normalize line to x-axis
|
|
i_mat = ~matrix # get original position
|
|
points = [] # stores edges
|
|
for i in range (1, cnt):
|
|
if i % 4 == 1: # point "above" connection
|
|
p = Point(i, -1) * mb
|
|
elif i % 4 == 3: # point "below" connection
|
|
p = Point(i, 1) * mb
|
|
else: # ignore others
|
|
continue
|
|
points.append(p * i_mat)
|
|
self.drawPolyline([p1] + points + [p2]) # add start and end points
|
|
return p2
|
|
|
|
def drawSquiggle(self, p1, p2, breadth = 2):
|
|
"""Draw a squiggly line from p1 to p2.
|
|
"""
|
|
p1 = Point(p1)
|
|
p2 = Point(p2)
|
|
S = p2 - p1 # vector start - end
|
|
rad = abs(S) # distance of points
|
|
cnt = 4 * int(round(rad / (4 * breadth), 0)) # always take full phases
|
|
if cnt < 4:
|
|
raise ValueError("points too close")
|
|
mb = rad / cnt # revised breadth
|
|
matrix = TOOLS._hor_matrix(p1, p2) # normalize line to x-axis
|
|
i_mat = ~matrix # get original position
|
|
k = 2.4142135623765633 # y of drawCurve helper point
|
|
|
|
points = [] # stores edges
|
|
for i in range (1, cnt):
|
|
if i % 4 == 1: # point "above" connection
|
|
p = Point(i, -k) * mb
|
|
elif i % 4 == 3: # point "below" connection
|
|
p = Point(i, k) * mb
|
|
else: # else on connection line
|
|
p = Point(i, 0) * mb
|
|
points.append(p * i_mat)
|
|
|
|
points = [p1] + points + [p2]
|
|
cnt = len(points)
|
|
i = 0
|
|
while i + 2 < cnt:
|
|
self.drawCurve(points[i], points[i+1], points[i+2])
|
|
i += 2
|
|
return p2
|
|
|
|
#==============================================================================
|
|
# Shape.insertText
|
|
#==============================================================================
|
|
def insertText(self, point, buffer,
|
|
fontsize=11,
|
|
fontname="helv",
|
|
fontfile=None,
|
|
set_simple=0,
|
|
encoding=0,
|
|
color=None,
|
|
fill=None,
|
|
render_mode=0,
|
|
border_width=1,
|
|
rotate=0,
|
|
morph=None):
|
|
|
|
# ensure 'text' is a list of strings, worth dealing with
|
|
if not bool(buffer): return 0
|
|
|
|
if type(buffer) not in (list, tuple):
|
|
text = buffer.splitlines()
|
|
else:
|
|
text = buffer
|
|
|
|
if not len(text) > 0:
|
|
return 0
|
|
|
|
point = Point(point)
|
|
try:
|
|
maxcode = max([ord(c) for c in " ".join(text)])
|
|
except:
|
|
return 0
|
|
|
|
# ensure valid 'fontname'
|
|
fname = fontname
|
|
if fname.startswith("/"):
|
|
fname = fname[1:]
|
|
|
|
xref = self.page.insertFont(fontname=fname,
|
|
fontfile=fontfile,
|
|
encoding=encoding,
|
|
set_simple=set_simple,
|
|
)
|
|
fontinfo = CheckFontInfo(self.doc, xref)
|
|
|
|
fontdict = fontinfo[1]
|
|
ordering = fontdict["ordering"]
|
|
simple = fontdict["simple"]
|
|
bfname = fontdict["name"]
|
|
if maxcode > 255:
|
|
glyphs = self.doc.getCharWidths(xref, maxcode + 1)
|
|
else:
|
|
glyphs = fontdict["glyphs"]
|
|
|
|
tab = []
|
|
for t in text:
|
|
if simple and bfname not in ("Symbol", "ZapfDingbats"):
|
|
g = None
|
|
else:
|
|
g = glyphs
|
|
tab.append(getTJstr(t, g, simple, ordering))
|
|
text = tab
|
|
|
|
color_str = ColorCode(color, "c")
|
|
fill_str = ColorCode(fill, "f")
|
|
if fill is None and render_mode == 0: # ensure fill color when 0 Tr
|
|
fill = color
|
|
fill_str = ColorCode(color, "f")
|
|
|
|
morphing = CheckMorph(morph)
|
|
rot = rotate
|
|
if rot % 90 != 0:
|
|
raise ValueError("rotate not multiple of 90")
|
|
|
|
while rot < 0: rot += 360
|
|
rot = rot % 360 # text rotate = 0, 90, 270, 180
|
|
|
|
templ1 = "\nq BT\n%s1 0 0 1 %g %g Tm /%s %g Tf "
|
|
templ2 = "TJ\n0 -%g TD\n"
|
|
cmp90 = "0 1 -1 0 0 0 cm\n" # rotates 90 deg counter-clockwise
|
|
cmm90 = "0 -1 1 0 0 0 cm\n" # rotates 90 deg clockwise
|
|
cm180 = "-1 0 0 -1 0 0 cm\n" # rotates by 180 deg.
|
|
height = self.height
|
|
width = self.width
|
|
lheight = fontsize * 1.2 # line height
|
|
# setting up for standard rotation directions
|
|
# case rotate = 0
|
|
if morphing:
|
|
m1 = Matrix(1, 0, 0, 1, morph[0].x + self.x,
|
|
height - morph[0].y - self.y)
|
|
mat = ~m1 * morph[1] * m1
|
|
cm = "%g %g %g %g %g %g cm\n" % JM_TUPLE(mat)
|
|
else:
|
|
cm = ""
|
|
top = height - point.y - self.y # start of 1st char
|
|
left = point.x + self.x # start of 1. char
|
|
space = top # space available
|
|
headroom = point.y + self.y # distance to page border
|
|
if rot == 90:
|
|
left = height - point.y - self.y
|
|
top = -point.x - self.x
|
|
cm += cmp90
|
|
space = width - abs(top)
|
|
headroom = point.x + self.x
|
|
|
|
elif rot == 270:
|
|
left = -height + point.y + self.y
|
|
top = point.x + self.x
|
|
cm += cmm90
|
|
space = abs(top)
|
|
headroom = width - point.x - self.x
|
|
|
|
elif rot == 180:
|
|
left = -point.x - self.x
|
|
top = -height + point.y + self.y
|
|
cm += cm180
|
|
space = abs(point.y + self.y)
|
|
headroom = height - point.y - self.y
|
|
|
|
if headroom < fontsize: # at least 1 full line space required!
|
|
raise ValueError("text starts outside page")
|
|
|
|
nres = templ1 % (cm, left, top, fname, fontsize)
|
|
if render_mode > 0:
|
|
nres += "%i Tr " % render_mode
|
|
if border_width != 1:
|
|
nres += "%g w " % border_width
|
|
if color is not None:
|
|
nres += color_str
|
|
if fill is not None:
|
|
nres += fill_str
|
|
|
|
# =========================================================================
|
|
# start text insertion
|
|
# =========================================================================
|
|
nres += text[0]
|
|
nlines = 1 # set output line counter
|
|
nres += templ2 % lheight # line 1
|
|
for i in range(1, len(text)):
|
|
if space < lheight:
|
|
break # no space left on page
|
|
if i > 1:
|
|
nres += "\nT* "
|
|
nres += text[i] + templ2[:2]
|
|
space -= lheight
|
|
nlines += 1
|
|
|
|
nres += " ET Q\n"
|
|
|
|
# =========================================================================
|
|
# end of text insertion
|
|
# =========================================================================
|
|
# update the /Contents object
|
|
self.text_cont += nres
|
|
return nlines
|
|
|
|
#==============================================================================
|
|
# Shape.insertTextbox
|
|
#==============================================================================
|
|
def insertTextbox(self, rect, buffer,
|
|
fontname="helv",
|
|
fontfile=None,
|
|
fontsize=11,
|
|
set_simple=0,
|
|
encoding=0,
|
|
color=None,
|
|
fill=None,
|
|
expandtabs=1,
|
|
border_width=1,
|
|
align=0,
|
|
render_mode=0,
|
|
rotate=0,
|
|
morph=None):
|
|
""" Insert text into a given rectangle.
|
|
|
|
Args:
|
|
rect -- the textbox to fill
|
|
buffer -- text to be inserted
|
|
fontname -- a Base-14 font, font name or '/name'
|
|
fontfile -- name of a font file
|
|
fontsize -- font size
|
|
color -- RGB stroke color triple
|
|
fill -- RGB fill color triple
|
|
render_mode -- text rendering control
|
|
border_width -- thickness of glyph borders
|
|
expandtabs -- handles tabulators with string function
|
|
align -- left, center, right, justified
|
|
rotate -- 0, 90, 180, or 270 degrees
|
|
morph -- morph box with a matrix and a pivotal point
|
|
Returns:
|
|
unused or deficit rectangle area (float)
|
|
"""
|
|
rect = Rect(rect)
|
|
if rect.isEmpty or rect.isInfinite:
|
|
raise ValueError("text box must be finite and not empty")
|
|
|
|
color_str = ColorCode(color, "c")
|
|
fill_str = ColorCode(fill, "f")
|
|
if fill is None and render_mode == 0: # ensure fill color for 0 Tr
|
|
fill = color
|
|
fill_str = ColorCode(color, "f")
|
|
|
|
if rotate % 90 != 0:
|
|
raise ValueError("rotate must be multiple of 90")
|
|
|
|
rot = rotate
|
|
while rot < 0: rot += 360
|
|
rot = rot % 360
|
|
|
|
# is buffer worth of dealing with?
|
|
if not bool(buffer):
|
|
return rect.height if rot in (0, 180) else rect.width
|
|
|
|
cmp90 = "0 1 -1 0 0 0 cm\n" # rotates counter-clockwise
|
|
cmm90 = "0 -1 1 0 0 0 cm\n" # rotates clockwise
|
|
cm180 = "-1 0 0 -1 0 0 cm\n" # rotates by 180 deg.
|
|
height = self.height
|
|
|
|
fname = fontname
|
|
if fname.startswith("/"):
|
|
fname = fname[1:]
|
|
|
|
xref = self.page.insertFont(fontname=fname,
|
|
fontfile=fontfile,
|
|
encoding=encoding,
|
|
set_simple=set_simple,
|
|
)
|
|
fontinfo = CheckFontInfo(self.doc, xref)
|
|
|
|
fontdict = fontinfo[1]
|
|
ordering = fontdict["ordering"]
|
|
simple = fontdict["simple"]
|
|
glyphs = fontdict["glyphs"]
|
|
bfname = fontdict["name"]
|
|
|
|
# create a list from buffer, split into its lines
|
|
if type(buffer) in (list, tuple):
|
|
t0 = "\n".join(buffer)
|
|
else:
|
|
t0 = buffer
|
|
|
|
maxcode = max([ord(c) for c in t0])
|
|
# replace invalid char codes for simple fonts
|
|
if simple and maxcode > 255:
|
|
t0 = "".join([c if ord(c)<256 else "?" for c in t0])
|
|
|
|
t0 = t0.splitlines()
|
|
|
|
glyphs = self.doc.getCharWidths(xref, maxcode + 1)
|
|
if simple and bfname not in ("Symbol", "ZapfDingbats"):
|
|
tj_glyphs = None
|
|
else:
|
|
tj_glyphs = glyphs
|
|
|
|
|
|
#----------------------------------------------------------------------
|
|
# calculate pixel length of a string
|
|
#----------------------------------------------------------------------
|
|
def pixlen(x):
|
|
"""Calculate pixel length of x."""
|
|
if ordering < 0:
|
|
return sum([glyphs[ord(c)][1] for c in x]) * fontsize
|
|
else:
|
|
return len(x) * fontsize
|
|
#----------------------------------------------------------------------
|
|
|
|
if ordering < 0:
|
|
blen = glyphs[32][1] * fontsize # pixel size of space character
|
|
else:
|
|
blen = fontsize
|
|
|
|
text = "" # output buffer
|
|
lheight = fontsize * 1.2 # line height
|
|
if CheckMorph(morph):
|
|
m1 = Matrix(1, 0, 0, 1, morph[0].x + self.x,
|
|
self.height - morph[0].y - self.y)
|
|
mat = ~m1 * morph[1] * m1
|
|
cm = "%g %g %g %g %g %g cm\n" % JM_TUPLE(mat)
|
|
else:
|
|
cm = ""
|
|
|
|
#---------------------------------------------------------------------------
|
|
# adjust for text orientation / rotation
|
|
#---------------------------------------------------------------------------
|
|
progr = 1 # direction of line progress
|
|
c_pnt = Point(0, fontsize) # used for line progress
|
|
if rot == 0: # normal orientation
|
|
point = rect.tl + c_pnt # line 1 is 'fontsize' below top
|
|
pos = point.y + self.y # y of first line
|
|
maxwidth = rect.width # pixels available in one line
|
|
maxpos = rect.y1 + self.y # lines must not be below this
|
|
|
|
elif rot == 90: # rotate counter clockwise
|
|
c_pnt = Point(fontsize, 0) # progress in x-direction
|
|
point = rect.bl + c_pnt # line 1 'fontsize' away from left
|
|
pos = point.x + self.x # position of first line
|
|
maxwidth = rect.height # pixels available in one line
|
|
maxpos = rect.x1 + self.x # lines must not be right of this
|
|
cm += cmp90
|
|
|
|
elif rot == 180: # text upside down
|
|
c_pnt = -Point(0, fontsize) # progress upwards in y direction
|
|
point = rect.br + c_pnt # line 1 'fontsize' above bottom
|
|
pos = point.y + self.y # position of first line
|
|
maxwidth = rect.width # pixels available in one line
|
|
progr = -1 # subtract lheight for next line
|
|
maxpos = rect.y0 + self.y # lines must not be above this
|
|
cm += cm180
|
|
|
|
else: # rotate clockwise (270 or -90)
|
|
c_pnt = -Point(fontsize, 0) # progress from right to left
|
|
point = rect.tr + c_pnt # line 1 'fontsize' left of right
|
|
pos = point.x + self.x # position of first line
|
|
maxwidth = rect.height # pixels available in one line
|
|
progr = -1 # subtract lheight for next line
|
|
maxpos = rect.x0 + self.x # lines must not left of this
|
|
cm += cmm90
|
|
|
|
#=======================================================================
|
|
# line loop
|
|
#=======================================================================
|
|
just_tab = [] # 'justify' indicators per line
|
|
|
|
for i, line in enumerate(t0):
|
|
line_t = line.expandtabs(expandtabs).split(" ") # split into words
|
|
lbuff = "" # init line buffer
|
|
rest = maxwidth # available line pixels
|
|
#===================================================================
|
|
# word loop
|
|
#===================================================================
|
|
for word in line_t:
|
|
pl_w = pixlen(word) # pixel len of word
|
|
if rest >= pl_w: # will it fit on the line?
|
|
lbuff += word + " " # yes, and append word
|
|
rest -= (pl_w + blen) # update available line space
|
|
continue
|
|
# word won't fit - output line (if not empty)
|
|
if len(lbuff) > 0:
|
|
lbuff = lbuff.rstrip() + "\n" # line full, append line break
|
|
text += lbuff # append to total text
|
|
pos += lheight * progr # increase line position
|
|
just_tab.append(True) # line is justify candidate
|
|
lbuff = "" # re-init line buffer
|
|
rest = maxwidth # re-init avail. space
|
|
if pl_w <= maxwidth: # word shorter than 1 line?
|
|
lbuff = word + " " # start the line with it
|
|
rest = maxwidth - pl_w - blen # update free space
|
|
continue
|
|
# long word: split across multiple lines - char by char ...
|
|
if len(just_tab) > 0:
|
|
just_tab[-1] = False # reset justify indicator
|
|
for c in word:
|
|
if pixlen(lbuff) <= maxwidth - pixlen(c):
|
|
lbuff += c
|
|
else: # line full
|
|
lbuff += "\n" # close line
|
|
text += lbuff # append to text
|
|
pos += lheight * progr # increase line position
|
|
just_tab.append(False) # do not justify line
|
|
lbuff = c # start new line with this char
|
|
lbuff += " " # finish long word
|
|
rest = maxwidth - pixlen(lbuff) # long word stored
|
|
|
|
if lbuff != "": # unprocessed line content?
|
|
text += lbuff.rstrip() # append to text
|
|
just_tab.append(False) # do not justify line
|
|
if i < len(t0) - 1: # not the last line?
|
|
text += "\n" # insert line break
|
|
pos += lheight * progr # increase line position
|
|
|
|
more = (pos - maxpos) * progr # difference to rect size limit
|
|
|
|
if more > EPSILON: # landed too much outside rect
|
|
return (-1) * more # return deficit, don't output
|
|
|
|
more = abs(more)
|
|
if more < EPSILON:
|
|
more = 0 # don't bother with epsilons
|
|
nres = "\nq BT\n" + cm # initialize output buffer
|
|
templ = "1 0 0 1 %g %g Tm /%s %g Tf "
|
|
# center, right, justify: output each line with its own specifics
|
|
spacing = 0
|
|
text_t = text.splitlines() # split text in lines again
|
|
for i, t in enumerate(text_t):
|
|
pl = maxwidth - pixlen(t) # length of empty line part
|
|
pnt = point + c_pnt * (i * 1.2) # text start of line
|
|
if align == 1: # center: right shift by half width
|
|
if rot in (0, 180):
|
|
pnt = pnt + Point(pl / 2, 0) * progr
|
|
else:
|
|
pnt = pnt - Point(0, pl / 2) * progr
|
|
elif align == 2: # right: right shift by full width
|
|
if rot in (0, 180):
|
|
pnt = pnt + Point(pl, 0) * progr
|
|
else:
|
|
pnt = pnt - Point(0, pl) * progr
|
|
elif align == 3: # justify
|
|
spaces = t.count(" ") # number of spaces in line
|
|
if spaces > 0 and just_tab[i]: # if any, and we may justify
|
|
spacing = pl / spaces # make every space this much larger
|
|
else:
|
|
spacing = 0 # keep normal space length
|
|
top = height - pnt.y - self.y
|
|
left = pnt.x + self.x
|
|
if rot == 90:
|
|
left = height - pnt.y - self.y
|
|
top = -pnt.x - self.x
|
|
elif rot == 270:
|
|
left = -height + pnt.y + self.y
|
|
top = pnt.x + self.x
|
|
elif rot == 180:
|
|
left = -pnt.x - self.x
|
|
top = -height + pnt.y + self.y
|
|
|
|
nres += templ % (left, top, fname, fontsize)
|
|
if render_mode > 0:
|
|
nres += "%i Tr " % render_mode
|
|
if spacing != 0:
|
|
nres += "%g Tw " % spacing
|
|
if color is not None:
|
|
nres += color_str
|
|
if fill is not None:
|
|
nres += fill_str
|
|
if border_width != 1:
|
|
nres += "%g w " % border_width
|
|
nres += "%sTJ\n" % getTJstr(t, tj_glyphs, simple, ordering)
|
|
|
|
nres += "ET Q\n"
|
|
|
|
self.text_cont += nres
|
|
self.updateRect(rect)
|
|
return more
|
|
|
|
def finish(
|
|
self,
|
|
width=1,
|
|
color=None,
|
|
fill=None,
|
|
lineCap=0,
|
|
lineJoin=0,
|
|
roundCap=None,
|
|
dashes=None,
|
|
even_odd=False,
|
|
morph=None,
|
|
closePath=True
|
|
):
|
|
"""Finish the current drawing segment.
|
|
|
|
Notes:
|
|
Apply stroke and fill colors, dashes, line style and width, or
|
|
morphing. Also determines whether any open path should be closed
|
|
by a connecting line to its start point.
|
|
"""
|
|
if self.draw_cont == "": # treat empty contents as no-op
|
|
return
|
|
if roundCap is not None:
|
|
warnings.warn("roundCap is replaced by lineCap / lineJoin", DeprecationWarning)
|
|
lineCap = lineJoin = roundCap
|
|
|
|
if width == 0: # border color makes no sense then
|
|
color = None
|
|
elif color is None: # vice versa
|
|
width = 0
|
|
color_str = ColorCode(color, "c") # ensure proper color string
|
|
fill_str = ColorCode(fill, "f") # ensure proper fill string
|
|
|
|
if width not in (0, 1):
|
|
self.draw_cont += "%g w\n" % width
|
|
|
|
if lineCap + lineJoin > 0:
|
|
self.draw_cont += "%i J %i j\n" % (lineCap, lineJoin)
|
|
|
|
if dashes is not None and len(dashes) > 0:
|
|
self.draw_cont += "%s d\n" % dashes
|
|
|
|
if closePath:
|
|
self.draw_cont += "h\n"
|
|
self.lastPoint = None
|
|
|
|
if color is not None:
|
|
self.draw_cont += color_str
|
|
|
|
if fill is not None:
|
|
self.draw_cont += fill_str
|
|
if color is not None:
|
|
if not even_odd:
|
|
self.draw_cont += "B\n"
|
|
else:
|
|
self.draw_cont += "B*\n"
|
|
else:
|
|
if not even_odd:
|
|
self.draw_cont += "f\n"
|
|
else:
|
|
self.draw_cont += "f*\n"
|
|
else:
|
|
self.draw_cont += "S\n"
|
|
|
|
if CheckMorph(morph):
|
|
m1 = Matrix(1, 0, 0, 1, morph[0].x + self.x,
|
|
self.height - morph[0].y - self.y)
|
|
mat = ~m1 * morph[1] * m1
|
|
self.draw_cont = "%g %g %g %g %g %g cm\n" % JM_TUPLE(mat) + self.draw_cont
|
|
|
|
self.totalcont += "\nq\n" + self.draw_cont + "Q\n"
|
|
self.draw_cont = ""
|
|
self.lastPoint = None
|
|
return
|
|
|
|
def commit(self, overlay=True):
|
|
"""Update the page's /Contents object with Shape data. The argument controls whether data appear in foreground (default) or background.
|
|
"""
|
|
CheckParent(self.page) # doc may have died meanwhile
|
|
self.totalcont += self.text_cont
|
|
|
|
if not fitz_py2: # need bytes if Python > 2
|
|
self.totalcont = bytes(self.totalcont, "utf-8")
|
|
|
|
# make /Contents object with dummy stream
|
|
xref = TOOLS._insert_contents(self.page, b" ", overlay)
|
|
# update it with potential compression
|
|
self.doc._updateStream(xref, self.totalcont)
|
|
|
|
self.lastPoint = None # clean up ...
|
|
self.rect = None #
|
|
self.draw_cont = "" # for possible ...
|
|
self.text_cont = "" # ...
|
|
self.totalcont = "" # re-use
|
|
return
|