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 >

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.

Looks like no one’s replied in a while. To start the conversation again, simply ask a new question.

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)

Posted on Oct 22, 2014 9:43 AM

Reply
Question marked as Top-ranking reply

Posted on Oct 22, 2014 11:04 AM

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:

  • Watermark PDF Documents

    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)

  • Set Application for Files - Preview


Print > PDF > myDraft


User uploaded file

53 replies
Question marked as Top-ranking reply

Oct 22, 2014 11:04 AM in response to Tony T1

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:

  • Watermark PDF Documents

    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)

  • Set Application for Files - Preview


Print > PDF > myDraft


User uploaded file

Oct 22, 2014 12:15 PM in response to VikingOSX

Well Tony, this PyObjC code is a work in progress. I cleaned up all syntax issues, but:

  • The expected stamp file is hardcoded into the application, not up top as a constant.

    Replace confidential.pdf with your document (expects text on transparent background)

  • Apparently, one must intuitively know the rect content setting
  • The stamp is applied backwards \, not in the expected / orientation
  • Syntax: ConfidentialStamper.py Lorem.pdf

    Output: Lorem.watermarked.pdf

Oct 22, 2014 1:46 PM in response to Tony T1

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.


User uploaded file


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())

Oct 22, 2014 4:11 PM in response to Frank Caggiano

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.

Oct 22, 2014 5:32 PM in response to VikingOSX

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.

Oct 22, 2014 5:42 PM in response to Tony T1

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

Oct 22, 2014 7:19 PM in response to VikingOSX

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)

Oct 23, 2014 3:11 AM in response to VikingOSX

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

Oct 23, 2014 2:17 PM in response to VikingOSX

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.

Oct 23, 2014 2:28 PM in response to VikingOSX

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

Welcome to Apple Support Community
A forum where Apple customers help each other with their products. Get started with your Apple Account.