Tony T1

Q: 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:48 AM

Close

Q: Automator Watermark PDF Workflow

  • All replies
  • Helpful answers

Page 1 of 4 last Next
  • by VikingOSX,Helpful

    VikingOSX VikingOSX Oct 22, 2014 11:04 AM in response to Tony T1
    Level 7 (20,819 points)
    Mac OS X
    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

     

    Screen Shot 2014-10-22 at 2.00.11 PM.png

  • by Tony T1,

    Tony T1 Tony T1 Oct 22, 2014 11:13 AM in response to VikingOSX
    Level 6 (9,249 points)
    Mac OS X
    Oct 22, 2014 11:13 AM in response to VikingOSX

    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)

  • by VikingOSX,

    VikingOSX VikingOSX Oct 22, 2014 11:17 AM in response to Tony T1
    Level 7 (20,819 points)
    Mac OS X
    Oct 22, 2014 11:17 AM in response to Tony T1

    Looking at, but have not run, a PyObjC watermark solution right now.

  • by VikingOSX,

    VikingOSX VikingOSX Oct 22, 2014 12:15 PM in response to VikingOSX
    Level 7 (20,819 points)
    Mac OS X
    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
  • by VikingOSX,

    VikingOSX VikingOSX Oct 22, 2014 1:46 PM in response to Tony T1
    Level 7 (20,819 points)
    Mac OS X
    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.

     

    Screen Shot 2014-10-22 at 4.42.39 PM.png

     

    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())
    
  • by Frank Caggiano,

    Frank Caggiano Frank Caggiano Oct 22, 2014 2:57 PM in response to VikingOSX
    Level 7 (25,782 points)
    Oct 22, 2014 2:57 PM in response to VikingOSX

    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

     

    Screen Shot 2014-10-22 at 17.52.16.png

     

    It appears the script is throwing an error.  Not a python scriptor but one of you might be bale to see what trouble is.

  • by VikingOSX,

    VikingOSX VikingOSX Oct 22, 2014 4:11 PM in response to Frank Caggiano
    Level 7 (20,819 points)
    Mac OS X
    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.

  • by Tony T1,

    Tony T1 Tony T1 Oct 22, 2014 5:32 PM in response to VikingOSX
    Level 6 (9,249 points)
    Mac OS X
    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.

  • by VikingOSX,

    VikingOSX VikingOSX Oct 22, 2014 5:42 PM in response to Tony T1
    Level 7 (20,819 points)
    Mac OS X
    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

  • by Tony T1,

    Tony T1 Tony T1 Oct 22, 2014 7:19 PM in response to VikingOSX
    Level 6 (9,249 points)
    Mac OS X
    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)

  • by Hiroto,Solvedanswer

    Hiroto Hiroto Oct 23, 2014 3:11 AM in response to VikingOSX
    Level 5 (7,286 points)
    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

  • by Tony T1,

    Tony T1 Tony T1 Oct 23, 2014 6:39 AM in response to Hiroto
    Level 6 (9,249 points)
    Mac OS X
    Oct 23, 2014 6:39 AM in response to Hiroto

    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.

  • by VikingOSX,

    VikingOSX VikingOSX Oct 23, 2014 9:46 AM in response to Hiroto
    Level 7 (20,819 points)
    Mac OS X
    Oct 23, 2014 9:46 AM in response to Hiroto

    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)
    
  • by VikingOSX,

    VikingOSX VikingOSX Oct 23, 2014 2:17 PM in response to VikingOSX
    Level 7 (20,819 points)
    Mac OS X
    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.

Page 1 of 4 last Next