Crop a PDF Document

The script included below crops all pages of a PDF document and creates a new PDF with the cropped pages. To test this script, open it to a script editor and run.

The user is prompted to enter a crop rectangle, which is in points and consists of an x coordinate, y coordinate, width, and height. Just as a point of information, there are 72 points in an inch and 2.83 points in a millimeter.

It should be noted that this script does not crop a PDF page in the sense that a bitmap image can be cropped. It instead sets the PDF’s crop box, which generally defines the area of the PDF that will be displayed and printed by a PDF viewer application. This is what happens when a PDF is cropped by the Preview application.

-- revised 2022.11.19

use framework "Foundation"
use framework "Quartz"
use scripting additions

on main()
	set sourceFile to POSIX path of (choose file with prompt "Choose a PDF file to crop" of type {"pdf"})
	set sourceFile to current application's |NSURL|'s fileURLWithPath:sourceFile
	set theDocument to (current application's PDFDocument's alloc()'s initWithURL:sourceFile)
	
	set {x, y, w, h} to getBounds(sourceFile, theDocument)
	set cropRectangle to text returned of (display dialog "Enter the crop rectangle consisting of the x coordinate, y coordinate, width, and height." default answer (x as text) & " " & y & " " & w & " " & h with title "PDF Crop" buttons {"Cancel", "Crop"} cancel button 1 default button 2)
	set cropRectangle to current application's NSString's stringWithString:cropRectangle
	set cropRectangle to ((cropRectangle's componentsSeparatedByString:space)'s valueForKey:"integerValue") as list
	
	try
		cropPDF(sourceFile, theDocument, cropRectangle)
	on error
		display alert "The PDF could not be cropped" message "Normally this occurs when the PDF cannot be read or when the crop rectangle lacks necessary data" as critical
		error number -128
	end try
end main

on getBounds(sourceFile, theDocument)
	set aPage to (theDocument's pageAtIndex:0)
	set {{x, y}, {w, h}} to (aPage's boundsForBox:(current application's kPDFDisplayBoxMediaBox))
	return {x as integer, y as integer, w as integer, h as integer}
end getBounds

on cropPDF(sourceFile, theDocument, cropRectangle)
	set {a2, b2, c2, d2} to cropRectangle
	set sourceFolder to sourceFile's URLByDeletingLastPathComponent
	set sourceFileName to (sourceFile's URLByDeletingPathExtension())'s lastPathComponent()
	set targetFileName to sourceFileName's stringByAppendingString:" (cropped).pdf"
	set targetFile to sourceFolder's URLByAppendingPathComponent:targetFileName
	set sourcePageCount to theDocument's pageCount()
	
	repeat with i from 0 to (sourcePageCount - 1)
		set aPage to (theDocument's pageAtIndex:(i))
		set {{a1, b1}, {c1, d1}} to (aPage's boundsForBox:(current application's kPDFDisplayBoxMediaBox))
		set pageSize to {{a2, (d1 - b2 - d2)}, {c2, d2}}
		(aPage's setBounds:pageSize forBox:(current application's kPDFDisplayBoxCropBox))
		(theDocument's removePageAtIndex:i)
		(theDocument's insertPage:aPage atIndex:i)
	end repeat
	
	theDocument's writeToURL:targetFile
end cropPDF

main()

This script differs from the above in that it works on a PDF selected in a Finder window, allows the user to specify the pages to be cropped, and uses Shane’s Dialog Toolkit Plus script library, which can be downloaded from:

https://latenightsw.com/freeware/

This script was tested on Monterey only and will not work with every PDF. The dialog displayed by the script shows the bounds of the first page of the existing PDF, which can be helpful in setting the crop rectangle.

-- revised 2022.11.19

use framework "Foundation"
use framework "Quartz"
use script "Dialog Toolkit Plus" version "1.1.0"
use scripting additions

on main()
	tell application "Finder" to set sourceFiles to selection as alias list
	if (count sourceFiles) ≠ 1 then errorAlert("Multiple or no files selected")
	set sourceFile to POSIX path of item 1 of sourceFiles
	if sourceFile does not end with ".pdf" then errorAlert("The selected file does not have a PDF extension")
	set sourceFile to current application's |NSURL|'s fileURLWithPath:sourceFile
	set sourceDoc to (current application's PDFDocument's alloc()'s initWithURL:sourceFile)
	
	try
		set pageOneBounds to (sourceDoc's pageAtIndex:(0))'s boundsForBox:(current application's kPDFDisplayBoxMediaBox)
	on error
		errorAlert("The selected PDF could not be read")
	end try
	
	set {pageNumbers, cropRectangle} to displayDialog(pageOneBounds)
	
	set pageNumbers to current application's NSString's stringWithString:pageNumbers
	set pageNumbers to (pageNumbers's componentsSeparatedByString:space)
	checkNumbers(pageNumbers)
	set pageNumbers to pageNumbers's valueForKey:"integerValue"
	
	set cropRectangle to current application's NSString's stringWithString:cropRectangle
	set cropRectangle to (cropRectangle's componentsSeparatedByString:space)
	checkNumbers(cropRectangle)
	set cropRectangle to cropRectangle's valueForKey:"integerValue"
	
	try
		cropPDF(sourceFile, sourceDoc, pageNumbers, cropRectangle)
	on error
		errorAlert("The PDF could not be cropped. This normally results from invalid page numbers or crop rectangle.")
	end try
end main

on displayDialog(theBounds)
	set dialogWidth to 310
	set verticalSpace to 12
	set theBounds to "0 0 " & ((item 1 of item 2 of theBounds) as integer) & space & ((item 2 of item 2 of theBounds) as integer)
	set {theButtons, minWidth} to create buttons {"Cancel", "Crop"} cancel button 1 default button 2
	set {cropField, theTop} to create field theBounds placeholder text theBounds bottom 0 field width dialogWidth
	set {cropLabel, theTop} to create label "Enter the crop rectangle, consisting of its x and y point of origin and its width and height:" bottom theTop + verticalSpace max width dialogWidth control size regular size
	set {pagesField, theTop} to create field "0" placeholder text "" bottom (theTop + verticalSpace) field width dialogWidth
	set {pagesLabel, theTop} to create label "Enter space-separated page numbers or 0 to crop all pages:" bottom theTop + verticalSpace max width dialogWidth control size regular size
	set allControls to {cropField, cropLabel, pagesField, pagesLabel}
	set {buttonName, controlsResults} to display enhanced window "PDF Crop" buttons theButtons acc view width dialogWidth acc view height theTop acc view controls allControls initial position {} without align cancel button
	return {item 3, item 1} of controlsResults
end displayDialog

on checkNumbers(theArray) -- from Nigel
	set thePredicate to current application's NSPredicate's predicateWithFormat:"self MATCHES '[0-9]++'"
	set theResult to ((theArray's filteredArrayUsingPredicate:thePredicate)'s isEqualToArray:(theArray))
	if theResult as boolean is false then errorAlert("The page numbers or crop rectangle contained no or nonnumerical characters")
end checkNumbers

on cropPDF(sourceFile, sourceDoc, pageNumbers, cropRectangle)
	set {aC, bC, cC, dC} to cropRectangle
	
	set sourceFolder to sourceFile's URLByDeletingLastPathComponent
	set sourceFileName to (sourceFile's URLByDeletingPathExtension())'s lastPathComponent()
	set targetFileName to sourceFileName's stringByAppendingString:" (cropped).pdf"
	set targetFile to sourceFolder's URLByAppendingPathComponent:targetFileName
	set targetDoc to current application's PDFDocument's new()
	
	set sourcePageCount to sourceDoc's pageCount()
	set targetPageCount to 0
	
	if pageNumbers's isEqualToArray:{0} then
		set pageNumbers to current application's NSMutableArray's new()
		repeat with i from 1 to sourcePageCount
			(pageNumbers's addObject:i)
		end repeat
	end if
	
	repeat with i in pageNumbers
		set aPage to (sourceDoc's pageAtIndex:((i as integer) - 1))
		set {{aE, bE}, {cE, dE}} to (aPage's boundsForBox:(current application's kPDFDisplayBoxMediaBox))
		set pageSize to {{aC, (dE - bC - dC)}, {cC, dC}}
		(aPage's setBounds:pageSize forBox:(current application's kPDFDisplayBoxCropBox))
		(targetDoc's insertPage:aPage atIndex:targetPageCount)
		set targetPageCount to targetPageCount + 1
	end repeat
	
	targetDoc's writeToURL:targetFile
end cropPDF

on errorAlert(dialogMessage)
	display alert "An error has occurred" message dialogMessage as critical
	error number -128
end errorAlert

main()

I mentioned in post 1 that the above scripts do not actually crop images in a PDF and instead define the area of the PDF that will be displayed and printed by a PDF viewer application. With recent versions of macOS, the sips utility can be used for this purpose, and the image in the PDF (if there is one) is in fact cropped. However, sips only works on the first page of a PDF, so this approach is of limited utility.

I ran some tests and in all instances the crop width and height were 500 x 500.

PDF made from PNG image with ASObjC
original 448 KB 1920 x 2074 lossless
cropped 55 KB 500 x 500 lossy

PDF made from JPEG image with Finder
original 2.6 MB 4032 x 2268 lossy
cropped 46 KB 500 x 500 lossy

PDF made with Epson scanner in color document mode
original 295 KB 2550 x 3300 lossy
cropped 53 KB 500 x 500 lossy

A page of Shane’s ASObjC book (text only)
original 66 KB (contains no images) MediaBox [0 0 612 792]
cropped 52 KB 500 x 500 lossy MediaBox [0 0 240 240]

The script is:

set inPDF to POSIX path of (choose file of type {"pdf"})
set outPDF to POSIX path of (path to desktop) & "out.pdf"
do shell script "sips --cropOffset 1 1 --cropToHeightWidth 500 500 -o " & quoted form of outPDF & " " & quoted form of inPDF