Automator Watermark PDF Workflow
Seems with every major upgrade, Apple breaks the Automator Watermark PDF Workflow.
Can someone test this workflow in Yosemite so I know if the problem I'm having is with OS X 10.10.
OS X Yosemite (10.10)
Apple Intelligence now features Image Playground, Genmoji, Writing Tools enhancements, seamless support for ChatGPT, and visual intelligence.
Apple Intelligence has also begun language expansion with localized English support for Australia, Canada, Ireland, New Zealand, South Africa, and the U.K. Learn more >
Apple Intelligence has also begun language expansion with localized English support for Australia, Canada, Ireland, New Zealand, South Africa, and the U.K. Learn more >
You can make a difference in the Apple Support Community!
When you sign up with your Apple Account, you can provide valuable feedback to other community members by upvoting helpful replies and User Tips.
When you sign up with your Apple Account, you can provide valuable feedback to other community members by upvoting helpful replies and User Tips.
Seems with every major upgrade, Apple breaks the Automator Watermark PDF Workflow.
Can someone test this workflow in Yosemite so I know if the problem I'm having is with OS X 10.10.
OS X Yosemite (10.10)
I may agree with you. 😠 Can't get it to work as drag/drop application, with a rename after the watermark — or as a Printer plugin as below.
I create a Print plugin in Automator. Two actions:
Image: draft.jpg (will not allow you to select a draft.pdf with the chooser, but you can drag/drop one in the well - doesn't work either)
Print > PDF > myDraft
I may agree with you. 😠 Can't get it to work as drag/drop application, with a rename after the watermark — or as a Printer plugin as below.
I create a Print plugin in Automator. Two actions:
Image: draft.jpg (will not allow you to select a draft.pdf with the chooser, but you can drag/drop one in the well - doesn't work either)
Print > PDF > myDraft
I was afraid of that (I also tested with a simple Ask For Finder Items -> Watermark PDF Documents).
I couldn't find a fix.
Anyone know of an alternate method? (Ruby, Python, etc)
Looking at, but have not run, a PyObjC watermark solution right now.
Well Tony, this PyObjC code is a work in progress. I cleaned up all syntax issues, but:
Replace confidential.pdf with your document (expects text on transparent background)
Output: Lorem.watermarked.pdf
Tony,
Got the PyObjC ConfidentialStamper.py watermark utility to work, with some tweaks. Now have 45 degree Draft stamp on every page. Created a new letter-size stamp at 300 dpi with 50% transparency, and transparent background. Used this as the PDF stamp file. Here is the result when run from the Terminal. Hopefully, this image will not disappear into the host vortex.
Here is the syntax clean PyObjC source code, with two new constants that produced this result.
#!/usr/bin/python
"""
Add a watermark to all pages in a PDF document
"""
import sys
#import math
import os
from Quartz import *
from Foundation import *
STAMPFILE = 'Draft45-50.pdf'
ANGLE = -270.0
def usage(name):
print >> sys.stderr, "Usage %s [inputfile]" % (name,)
class MyPDFData (object):
pdfDoc = None
mediaRect = None
# This is a simple function to create a CFURLRef from
# a path to a file. The path can be relative to the
# current directory or an absolute path.
def createURL(path):
return CFURLCreateFromFileSystemRepresentation(None, path,
len(path), False)
# For the supplied URL and media box, create a PDF context
# that creates a PDF file at that URL and uses supplied rect
# as its document media box.
def myCreatePDFContext(url, mediaBox):
dict = {}
dict[kCGPDFContextCreator] = "PDF Stamper Application"
pdfContext = CGPDFContextCreateWithURL(url, mediaBox, dict)
return pdfContext
# For a URL corresponding to an existing PDF document on disk,
# create a CGPDFDocumentRef and obtain the media box of the first
# page.
def myCreatePDFSourceDocument(url):
myPDFData = MyPDFData()
myPDFData.pdfDoc = CGPDFDocumentCreateWithURL(url)
if myPDFData.pdfDoc is not None:
# NOTE: the original code uses CGPDFDocumentGetMediaBox, but that
# API is deprecated and doesn't work in Leopard.
page = CGPDFDocumentGetPage(myPDFData.pdfDoc, 1)
myPDFData.mediaRect = CGPDFPageGetBoxRect(page, kCGPDFMediaBox)
# Make the media rect origin at 0,0.
myPDFData.mediaRect.origin.x = myPDFData.mediaRect.origin.y = 0.0
return myPDFData
# Draw the source PDF document into the context and then draw the stamp
# PDF document on top of it. When drawing the stamp on top, place it
# along the diagonal from the lower left corner to the upper right
# corner and center its media rect to the center of that diagonal.
def StampWithPDFDocument(context, sourcePDFDoc, stampFileDoc, stampMediaRect):
numPages = CGPDFDocumentGetNumberOfPages(sourcePDFDoc)
# Loop over document pages and stamp each one appropriately.
for i in range(1, numPages+1):
# Use the page rectangle of each page from the source to compute
# the destination media box for each page and the location of
# the stamp.
# NOTE: the original code uses CGPDFDocumentGetMediaBox, but that
# API is deprecated and doesn't work in Leopard.
page = CGPDFDocumentGetPage(sourcePDFDoc, i)
pageRect = CGPDFPageGetBoxRect(page, kCGPDFMediaBox)
CGContextBeginPage(context, pageRect)
CGContextSaveGState(context)
# Clip to the media box of the page.
CGContextClipToRect(context, pageRect)
# First draw the content of the source document.
CGContextDrawPDFDocument(context, pageRect, sourcePDFDoc, i)
# Translate to center of destination rect, that is the center of
# the media box of content to draw on top of.
CGContextTranslateCTM(context,
pageRect.size.width/2, pageRect.size.height/2)
# Compute angle of the diagonal across the destination page.
# angle = math.atan(pageRect.size.height/pageRect.size.width)
#angle = math.acos(pageRect.size.height/pageRect.size.width)
# Rotate by an amount so that drawn content goes along a diagonal
# axis across the page.
#CGContextRotateCTM(context, angle)
CGContextRotateCTM(context, ANGLE)
# Move the origin so that the media box of the PDF to stamp
# is centered around center point of destination.
CGContextTranslateCTM(context, -stampMediaRect.size.width/2,
-stampMediaRect.size.height/2)
# Now draw the document to stamp with on top of original content.
CGContextDrawPDFDocument(context, stampMediaRect, stampFileDoc, 1)
CGContextRestoreGState(context)
CGContextEndPage(context)
# From an input PDF document and a PDF document whose contents you
# want to draw on top of the other, create a new PDF document
# containing all the pages of the input document with the first page
# of the "stamping" overlayed.
def createStampedFileWithFile(inURL, stampURL, outURL):
sourceFileData = myCreatePDFSourceDocument(inURL)
if sourceFileData.pdfDoc is None:
print >> sys.stderr, "Can't create PDFDocumentRef for source input file!"
return
stampFileData = myCreatePDFSourceDocument(stampURL)
if stampFileData.pdfDoc is None:
CGPDFDocumentRelease(sourceFileData.pdfDoc)
print >>sys.stderr, "Can't create PDFDocumentRef for file to stamp with!"
return
pdfContext = myCreatePDFContext(outURL, sourceFileData.mediaRect)
if pdfContext is None:
print >>sys.stderr, "Can't create PDFContext for output file!"
return
StampWithPDFDocument(pdfContext, sourceFileData.pdfDoc,
stampFileData.pdfDoc, stampFileData.mediaRect)
def main(args=None):
if args is None:
args = sys.argv
suffix = ".watermarked.pdf"
stampFileName = os.path.join(os.path.dirname(__file__), STAMPFILE)
if len(args) != 2:
usage(args[0])
return 1
inputFileName = args[1]
outputFileName = os.path.splitext(inputFileName)[0] + suffix
inURL = createURL(inputFileName)
if inURL is None:
print >>sys.stderr, "Couldn't create URL for input file!"
return 1
outURL = createURL(outputFileName)
if outURL is None:
print >>sys.stderr, "Couldn't create URL for output file!"
return 1
stampURL = createURL(stampFileName)
if stampURL is None:
print >>sys.stderr, "Couldn't create URL for stamping file!"
return 1
createStampedFileWithFile(inURL, stampURL, outURL)
return 0
if __name__ == "__main__":
sys.exit(main())
It appears that this is the python script that the Automater Actions runs
/System/Library/Automator/Watermark PDF Documents.action/Contents/Resources/tool.py
and according to the error I get from Automator
It appears the script is throwing an error. Not a python scriptor but one of you might be bale to see what trouble is.
Frank,
The tool.py file had a ton of Python syntax noise that I cleaned up. When I run it in the Terminal following precisely its command-line argument order, there appears to remain a consistent problem on line 31 with one of the Core Graphics methods, and a variable that was referenced before set. Will have to spend more cross-checking with the Dev docs. Maybe I can fix it.
I was trying to get the Python bit to work, because a UNIX executable living in MacOS that may also be a contributing factor to this misadventure.
I also have mac mini with OS X 10.7.5. The Workflow works fine on 10.7.5, but on that system, when I pull the tool.py and run from the command line (on OS X 10.7.5) I get the "UnboundLocalError: local variable 'scale' referenced before assignment" that you're getting (and the same error on OS X 10.10), so Automator is doing something to set the "scale" variable before running tool.py.
tool.py on 10.7.5 and 10.10 are essentially the same (Apple fixed a t flag to -t in 10.10), so I don't think that the problem with 10.10 is the python script.
In tool.py, add a scale = 100 assignment just after the angle variable in def main(argv) — that solves the variable referenced before assignment issue.
I use this syntax in the Terminal:
tool.py -i ~/input.pdf ~/watermark.png -o ~/output.pdf -t -a45 -p.5
I believe there is a PyObjC metadata issue with CGDataProviderCreateWithFilename(imagePath). It consistently bombs with ValueError: depythonifying 'pointer', got 'str'. I have read on stackoverflow, and elsewhere that there may be a problem with the PyObjC implementation defaulting to string, when it should be defaulting to a void pointer for this method. The cure is either on Apple's shoulders, or would require a significant reading investment in BridgeSupport metadata modifications. That ends my curiosity.
def createImage(imagePath):
image = None
provider = CGDataProviderCreateWithFilename(imagePath)
if provider:
imageSrc = CGImageSourceCreateWithDataProvider(provider, None)
if imageSrc:
image = CGImageSourceCreateImageAtIndex(imageSrc, 0, None)
if not image:
print "Cannot import the image from file %s" % imagePath
return image
I had tried that (with scale = 1.0) and got the same "ValueError: depythonifying 'pointer', got 'str'" error.
But as I said, I get the same error in terminal with OS X 10.7.5, yet it works within Automator (on 10.7.5), so there's something else in the Action that's resolving this issue (in 10.7.5) — but this does nothing to help in resolving the issue in 10.10. Looks like Apple needs to fix (I'll file a bug report)
Hello
Under 10.6.8, Watermark PDF Documents.action/Contents/Resources/tool.py works happily without any errors when invoked as follows with a.pdf and watermark.png in ~/desktop/test/ :
#!/bin/bash py='/System/Library/Automator/Watermark PDF Documents.action/Contents/Resources/tool.py' cd ~/desktop/test || exit args=( --input a.pdf --output a_wm.pdf --verbose --over --xOffset 0.0 --yOffset -150.0 --angle 300.0 --scale 0.3 --opacity 0.1 watermark.png ) "$py" "${args[@]}"
If the said error – ValueError: depythonifying 'pointer', got 'str' – is raised at the line:
provider = CGDataProviderCreateWithFilename(imagePath)
I'd think it is because imagePath is not a C string pointer which the function expects but a CFStringRef or something which is implicitly converted from python string. Since I don't get this error with pyobjc 2.2b3 & python 2.6 under 10.6.8, it is caused by something introduced in later versions.
Anyway, the statement in question may be replaced with the following statements if it helps:
url = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, imagePath, len(imagePath), False) provider = CGDataProviderCreateWithURL(url)
I modified the tool.py with these changes along with other minor fixes and run it successfully under 10.6.8. I'm not sure at all whether it works under later OSes as well. And even if it does, editing tool.py will require the Automator action to be re-codesigned.
Here's the revised tool.py.
#!/usr/bin/python # Watermark each page in a PDF document import sys #, os import getopt import math from Quartz.CoreGraphics import * from Quartz.ImageIO import * def drawWatermark(ctx, image, xOffset, yOffset, angle, scale, opacity): if image: imageWidth = CGImageGetWidth(image) imageHeight = CGImageGetHeight(image) imageBox = CGRectMake(0, 0, imageWidth, imageHeight) CGContextSaveGState(ctx) CGContextSetAlpha(ctx, opacity) CGContextTranslateCTM(ctx, xOffset, yOffset) CGContextScaleCTM(ctx, scale, scale) CGContextTranslateCTM(ctx, imageWidth / 2, imageHeight / 2) CGContextRotateCTM(ctx, angle * math.pi / 180) CGContextTranslateCTM(ctx, -imageWidth / 2, -imageHeight / 2) CGContextDrawImage(ctx, imageBox, image) CGContextRestoreGState(ctx) def createImage(imagePath): image = None # provider = CGDataProviderCreateWithFilename(imagePath) # FIXED: replaced by the following CGDataProviderCreateWithURL() url = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, imagePath, len(imagePath), False) provider = CGDataProviderCreateWithURL(url) if provider: imageSrc = CGImageSourceCreateWithDataProvider(provider, None) if imageSrc: image = CGImageSourceCreateImageAtIndex(imageSrc, 0, None) if not image: print "Cannot import the image from file %s" % imagePath return image def watermark(inputFile, watermarkFiles, outputFile, under, xOffset, yOffset, angle, scale, opacity, verbose): images = map(createImage, watermarkFiles) ctx = CGPDFContextCreateWithURL(CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, outputFile, len(outputFile), False), None, None) if ctx: pdf = CGPDFDocumentCreateWithURL(CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, inputFile, len(inputFile), False)) if pdf: for i in range(1, CGPDFDocumentGetNumberOfPages(pdf) + 1): image = images[i % len(images) - 1] page = CGPDFDocumentGetPage(pdf, i) if page: mediaBox = CGPDFPageGetBoxRect(page, kCGPDFMediaBox) if CGRectIsEmpty(mediaBox): mediaBox = None CGContextBeginPage(ctx, mediaBox) if under: drawWatermark(ctx, image, xOffset, yOffset, angle, scale, opacity) CGContextDrawPDFPage(ctx, page) if not under: drawWatermark(ctx, image, xOffset, yOffset, angle, scale, opacity) CGContextEndPage(ctx) del pdf CGPDFContextClose(ctx) del ctx def main(argv): verbose = False readFilename = None writeFilename = None under = False xOffset = 0.0 # FIXED: changed to float value yOffset = 0.0 # FIXED: changed to float value angle = 0.0 # FIXED: changed to float value scale = 1.0 # FIXED: added opacity = 1.0 # Parse the command line options try: options, args = getopt.getopt(argv, "vutx:y:a:p:s:i:o:", ["verbose", "under", "over", "xOffset=", "yOffset=", "angle=", "opacity=", "scale=", "input=", "output=", ]) except getopt.GetoptError: usage() sys.exit(2) for option, arg in options: print option, arg if option in ("-i", "--input") : if verbose: print "Reading pages from %s." % (arg) readFilename = arg elif option in ("-o", "--output") : if verbose: print "Setting %s as the output." % (arg) writeFilename = arg elif option in ("-v", "--verbose") : print "Verbose mode enabled." verbose = True elif option in ("-u", "--under"): print "watermark under PDF" under = True elif option in ("-t", "--over"): # FIXED: changed to "-t" from "t" print "watermark over PDF" under = False elif option in ("-x", "--xOffset"): xOffset = float(arg) elif option in ("-y", "--yOffset"): yOffset = float(arg) elif option in ("-a", "--angle"): angle = -float(arg) elif option in ("-s", "--scale"): scale = float(arg) elif option in ("-p", "--opacity"): opacity = float(arg) else: print "Unknown argument: %s" % (option) if (len(args) > 0): watermark(readFilename, args, writeFilename, under, xOffset, yOffset, angle, scale, opacity, verbose); else: shutil.copyfile(readFilename, writeFilename); def usage(): print "Usage: watermark --input <file> --output <file> <watermark files>..." if __name__ == "__main__": print sys.argv main(sys.argv[1:])
All the best,
H
Hiroto wrote:
I modified the tool.py with these changes along with other minor fixes and run it successfully under 10.6.8. I'm not sure at all whether it works under later OSes as well.
Thanks for the effort, but doesn't work in Terminal in OS X 10.10.
Hiroto,
In Terminal on 10.10, line 45 in watermark is bombing with 'TypeError: Object of type 'NoneType' has no len(). That is the only error, so far.
ctx = CGPDFContextCreateWithURL(CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, outputFile, len(outputFile), False), None, None)
I am learning more about this Python code. The correct command-line input for Hiroto's patch that does not produce any Python errors:
odin: ~$ tool2.py --angle 0.45 --over --input Lorem.pdf --output LoremD.pdf Draft.png
['./tool2.py', '--angle', '0.45', '--over', '--input', 'Lorem.pdf', '--output', 'LoremD.pdf', 'Draft.png']
--angle 0.45
--over
watermark over PDF
--input Lorem.pdf
--output LoremD.pdf
An output PDF is written — and silently ignores the Draft.png file — so there is no watermark applied.
Another interesting quirk with the workflow in Yosemite is that when you add the Watermark PDF Documents action to the workflow, in Mavericks the gray box to the left of the adjustments is filled with a pdf document from the actions resource folder, Bears.pdf, to enable you see what the watermark will look like. In Yosemite that dummy file is not loaded.
Automator Watermark PDF Workflow