Thoughts and examples about using the quartzfilter tool

As mentioned here before the quartzfilter tool hidden inside of Mac OS X can be of real help when it comes to PDF manipulation on the command line. It allows you to apply custom quartz filters created with the ColorSync Utility to your PDF or image files. Those quartz filters in turn can perform many useful tasks. They can reduce the file size by image compression, convert color to gray scale, increase lightness or add comments, just to mention a few possibilities.

The quartzfilter tool uses the following syntax:

/System/Library/Printers/Libraries/./quartzfilter inputfile filterpath outputfile

Do not use the same file path for the in- and output file, as this will result in an error. They must be different.

The example below would apply the «Sepia Tone» quartz filter located in /System/Library/Filters/ to a PDF file named «qfin.pdf» on my desktop and save the result in a PDF file named «qfout.pdf». The path to the quartz filter is quoted as it contains spaces:

/System/Library/Printers/Libraries/./quartzfilter /Users/martin/Desktop/qfin.pdf ‘/System/Library/Filters/Sepia Tone.qfilter’ /Users/martin/Desktop/qfout.pdf

Preinstalled quartz filters are located in /System/Library/Filters/, but your own custom quartz filters created with the ColorSync Utility are saved in your library folder under /Users/yourname/Library/Filters/.

So the quartzfilter tool let’s you apply preinstalled and your own custom quartz filters. Fine.

But what if I want to write a neat little AppleScript droplet for the Mac community, that would utilize the quartzfilter tool to batch-shrink countless dropped PDF or image files by a user-chosen image compression factor?

Which leads directly to the questions: What exactly is a quartz filter and can an application/script create its own custom quartz filters on-the-fly? It would be just useless/too error-prone to ask the user to create the custom quartz filter in ColorSync Utility for us first…

Unfortunately the ColorSync Utility itself is not scriptable with AppleScript, so this way for automatically creating custom quartz filters is blocked. But opening the «Reduce File Size» quartz filter file with my favourite free text editor - TextWrangler - revealed a surprise:

Quartz filters (or better: quartz filer files) are just pure XML/PLIST files!

Click here to view the XML representation of the «Reduce File Size» quartz filter

This means that an application or script can easily create its own custom quartz filters using an XML library or/and a template engine with default quartz filters. Even vanilla AppleScript can do it with the XML suite of the System Events Scripting Addition.

Therefor a PDF or image processing droplet could contain a copy of the above mentioned «Reduce File Size» quartz filter and change the value for the XML element «Compression Quality» according to a user-chosen factor. This way you could quickly batch-shrink PDF or image files with custom settings!

Just in case you are not yet convinced I invite you to download, test and inspect the sample droplet script below, which reduces the file size of dropped PDF documents according to a chosen compression quality:

Quartz-O-Shrink - Conveniently batch-shrink dropped PDF documents by a custom compression quality (ca. 41.2 KB)

The AppleScript was tested under Mac OS X 10.5.3 with Intel and PowerPC based Macs.

Important: Opening and saving the below script code in Script Editor won’t result in a usable AppleScript! That is because the AppleScript internally relies on a quartz filter template, which is located inside its Application bundle. Therefor please download the complete script here.


- created: 19.06.2008
-- tested on/with:
-- ¢ Intel & PowerPC based Macs
-- ¢ Mac OS X 10.5.3

-- This AppleScript reduces the file size of dropped PDF documents according to
-- a chosen compression quality. The script actually shrinks the PDF
-- documents by applying a custom quartz filter.

property mytitle : "Quartz-O-Shrink"

-- I am called when the user drops Finder items onto the script's icon
on open dropitems
	try
		-- searching the dropped Finder items for PDF documents
		-- PDF documents are identified by a '*.pdf' name extension
		set pdffilepaths to {}
		set pdffilenames to {}
		repeat with dropitem in dropitems
			set iteminfo to info for dropitem
			if name extension of iteminfo is in {"pdf"} then
				set pdffilepaths to pdffilepaths & (dropitem as Unicode text)
				set pdffilenames to pdffilenames & (name of iteminfo)
			end if
		end repeat
		-- no PDF documents found
		if pdffilepaths is {} then
			set errmsg to "You did not drop any PDF files onto me."
			my dsperrmsg(errmsg, "--")
			return
		end if
		-- asking the user to choose a compression quality
		set compqual to my choosecompqual()
		if compqual is missing value then
			return
		end if
		-- asking the user to choose an output folder where
		-- the shrinked PDF files are saved
		set outputfolderpath to my chooseoutputfolder()
		if outputfolderpath is missing value then
			return
		end if
		-- creating the custom quartz filter with the chosen compression quality
		set quartzfilterpath to my createquartzfilter(compqual)
		if quartzfilterpath is missing value then
			return
		end if
		-- applying the custom quartz filter to the dropped PDF documents
		set countpdffilepaths to length of pdffilepaths
		repeat with i from 1 to countpdffilepaths
			set pdffilepath to item i of pdffilepaths
			set pdffilename to item i of pdffilenames
			set unusedfilepath to my getunusedfilepath(outputfolderpath, pdffilename)
			set command to "/System/Library/Printers/Libraries/./quartzfilter " & quoted form of (POSIX path of pdffilepath) & space & quoted form of (POSIX path of quartzfilterpath) & space & quoted form of (POSIX path of unusedfilepath)
			do shell script command
		end repeat
	on error errmsg number errnum
		my dsperrmsg(errmsg, errnum)
	end try
end open

-- I am called when the user opens the script with a doubleclick
on run
	set welcomemsg to "I am an AppleScript droplet which reduces the file size of PDF documents." & return & return & "Therefor please drag and drop a bunch of PDF documents onto my icon."
	tell me
		activate
		display dialog welcomemsg buttons {"OK"} default button 1 with title mytitle with icon note
	end tell
end run

-- I am asking the user to choose a compression quality
on choosecompqual()
	set compquals to {"Best » low compression, high image quality", "1.0", "0.9", "0.8", "0.7", "0.6", "0.5", "0.4", "0.3", "0.2", "0.1", "0.0", "Least » high compression, low image quality"}
	choose from list compquals with prompt "Please choose a compression quality:" OK button name "Select" with title mytitle without multiple selections allowed and empty selection allowed
	set usrchoice to result
	if usrchoice is not false then
		set chosencompqual to item 1 of (usrchoice as list)
		if chosencompqual begins with "Best" or chosencompqual begins with "Least" then
			my choosecompqual()
		else
			return chosencompqual
		end if
	else
		return missing value
	end if
end choosecompqual

-- I am asking the user to specify an output folder for the shrinked PDF documents
-- I return the folder path or «missing value» in case the user cancels the dialog
on chooseoutputfolder()
	try
		set chosenfolder to choose folder with prompt "Please specify an output folder for the processed files:" without multiple selections allowed, showing package contents and invisibles
		return (chosenfolder as Unicode text)
	on error
		return missing value
	end try
end chooseoutputfolder

-- I am creating a new quartz filter containing the user chosen compression quality
-- I return the file path of the created quartz filter
on createquartzfilter(compqual)
	-- path to the quartz filter template located in the script bundle
	set tmpquartzfilterpath to (((path to me) as Unicode text) & "Contents:Resources:Reduce File Size.qfilter")
	set filecont to my readfromfile(tmpquartzfilterpath)
	-- reading the content of the quartz filter template failed :(
	if filecont is false then
		set errmsg to "Could not read the content of the quartz filter template supposed to be located at:" & return & return & tmpquartzfilterpath
		error errmsg
	end if
	-- replacing the placeholder with the chosen compression quality
	set newfilecont to my searchnreplace("$compqual$", compqual, filecont)
	-- creating our application support folder if it does not already exist
	set appsuppfolderpath to (((path to application support folder from user domain) as Unicode text) & mytitle & ":")
	try
		set appsuppfolderalias to appsuppfolderpath as alias
	on error
		do shell script "mkdir -p " & quoted form of (POSIX path of appsuppfolderpath)
	end try
	-- path to the new quartz filter containing the chosen compression quality
	set newquartzfilterpath to appsuppfolderpath & "Reduce File Size.qfilter"
	-- writing the modified template content to the new file path
	set writesuccess to my writetofile(newfilecont, newquartzfilterpath)
	if not writesuccess then
		set errmsg to "Could not write to file at:" & return & return & newquartzfilterpath
		error errmsg
	end if
	-- returning the file path to the newly created quartz filter
	return newquartzfilterpath
end createquartzfilter

-- I am returning an unused file path
-- example:
-- destfolderpath: «DandyDisk:Users:bender:Desktop:icnsfiles:»
-- filename: «example.pdf»
-- -> filepath: «DandyDisk:Users:bender:Desktop:icnsfiles:example.png»
-- if this filepath already exists:
-- -> filepath: «DandyDisk:Users:bender:Desktop:icnsfiles:example 1.png»
-- and so on...
on getunusedfilepath(destfolderpath, filename)
	set counter to 0
	repeat
		if counter < 1 then
			set unusedfilepath to destfolderpath & filename
		else
			set unusedfilepath to destfolderpath & ((characters 1 through -5 of filename) as Unicode text) & space & counter & ((characters -4 through -1 of filename) as Unicode text)
		end if
		try
			set unusedfilealias to unusedfilepath as alias
		on error
			exit repeat
		end try
		set counter to counter + 1
	end repeat
	return unusedfilepath
end getunusedfilepath

-- I am a very old search & replace function...
on searchnreplace(searchstr, replacestr, txt)
	considering case, diacriticals and punctuation
		if txt contains searchstr then
			set olddelims to AppleScript's text item delimiters
			set AppleScript's text item delimiters to {searchstr}
			set txtitems to text items of txt
			set AppleScript's text item delimiters to {replacestr}
			set txt to txtitems as Unicode text
			set AppleScript's text item delimiters to olddelims
		end if
	end considering
	return txt
end searchnreplace

-- I am reading and returning the content of a given file path
-- I return «false» in case the file could not be read
on readfromfile(filepath)
	try
		set openfile to open for access filepath
		set filecont to read openfile
		close access openfile
		return filecont
	on error
		try
			close access openfile
		end try
		return false
	end try
end readfromfile

-- I am writing given content to a given file path using UTF-8 text encoding (no BOM)
-- I return «false» in case the write attempt failed
on writetofile(cont, filepath)
	try
		set openfile to open for access filepath with write permission
		set eof of openfile to 0
		-- no BOM this time...
		-- set BOM_UTF8 to ((ASCII character 239) & (ASCII character 187) & (ASCII character 191))
		write cont to openfile as «class utf8»
		close access openfile
		return true
	on error
		try
			close access openfile
		end try
		return false
	end try
end writetofile

-- I am displaying error messages
on dsperrmsg(errmsg, errnum)
	tell me
		activate
		display dialog "Sorry, an error occured:" & return & return & errmsg & " (" & errnum & ")" buttons {"Never mind"} default button 1 with icon stop with title mytitle
	end tell
end dsperrmsg

Your hidden command seems to be missing in OS X Mountain Lion 10.8.5.
:frowning:

I would like to make a script that reduces the pixel size of images in PDF files using the built in OS X pdf engine.

This thread, and my own research, seem to indicate that something like this used to be possible from the OS X command line, but appears to no longer be there in 10.8 and above.

Any help would be appreciated.

If you’re running 10.9, you can use an AppleScriptObjC-based library to apply a QuartzFilter. Save this as an ASObjC-based .scptd file in your Script Libraries folder:

use framework "Foundation"
use framework "Quartz"

on filterPDF:pathToPDF newPath:newPDFPath
	set filterPath to current application's NSString's stringWithString:"/System/Library/Filters/Reduce File Size.qfilter"
	set filterURL to current application's NSURL's fileURLWithPath:filterPath
	set theFilter to current application's QuartzFilter's quartzFilterWithURL:filterURL
	set pdfURL to current application's NSURL's fileURLWithPath:pathToPDF
	set thePDFDoc to current application's PDFDocument's alloc()'s initWithURL:pdfURL
	set theOptions to {QuartzFilter:theFilter}
	thePDFDoc's writeToFile:newPDFPath withOptions:theOptions
end filterPDF:newPath:

Change the filter path to suit, or use the script above to create new versions of the filter, then call the script like this:

use theLib : script "<name of lib file>"

theLib's filterPDF:"/Users/shane/Desktop/Everyday AppleScriptObjC.pdf" newPath:"/Users/shane/Desktop/Everyday AppleScriptObjC-2.pdf"

I am sorry, I am unfamiliar with the variable:thisVarible structure you use in this code sample.

I did purchase your overview book on ASObjC, is there a few relevant pages I can go to read up on how to underhand this?

Thanks so much for your help.

Don

It’s known as interleaved syntax, and it was introduced in Mavericks. If you write this:

on doSomething_toSomething_(someArg, otherArg)

in 10.9 it will compile as:

on doSomething:someArg toSomething:otherArg

It’s just a different way of writing the same thing.

I found this tutorial which was very helpful on exactly how to deal with the Script Libraries folder, and was able to get everything in place.

http://macscripter.net/viewtopic.php?id=41638

Now, when I run a pdf though this, I get back false as the result, and no pdf is made.

I played around with the script in ASObjC Explorer 2 and got this error:

17:14:10.790 Can’t get POSIX path of class “ObjectWithFords”. (-1728)
17:14:10.790 – End – (~0.011s)

I’m not sure how ObjectWithFords gets involved. I just tried it in ASObjC Explorer 2 (without the use statements and as a normal handler):

0000.000 -- Start --
0000.001 [2] set filterPath to current application's NSString's stringWithString:"/System/Library/Filters/Reduce File Size.qfilter"
--> /System/Library/Filters/Reduce File Size.qfilter
0000.008 [3] set filterURL to current application's NSURL's fileURLWithPath:filterPath
--> file://localhost/System/Library/Filters/Reduce%20File%20Size.qfilter
0000.009 [4] set theFilter to current application's QuartzFilter's quartzFilterWithURL:filterURL
--> QuartzFilter:0x40010c020 private:0x400ea4100 name:Reduce File Size filter:0x40010c020 owner:0x0 URL:0x40037fbc0 properties:0x4011b42e0 qf:0x0 dict:0x0
0000.010 [5] set pdfURL to current application's NSURL's fileURLWithPath:pathToPDF
--> file://localhost/Users/shane/Desktop/Everyday%20AppleScriptObjC.pdf
0000.011 [6] set thePDFDoc to current application's PDFDocument's alloc()'s initWithURL:pdfURL
--> PDFDocument, number of pages: 85

0000.012 [7] set theOptions to {QuartzFilter:theFilter}
--> {QuartzFilter:«class ocid» id «data optr0000000020C0100004000000»}
0001.239 [8] thePDFDoc's writeToFile:newPDFPath withOptions:theOptions
--> 1
0001.240 [11] my filterPDF:"/Users/shane/Desktop/Everyday AppleScriptObjC.pdf" newPath:"/Users/shane/Desktop/Everyday AppleScriptObjC-2.pdf"
--> 1
0001.241 -- End -- (~1.241s)


I tried to use this today but am getting error “Unrecognized function writeToFile_withOptions_.” number -10000. Not sure what’s going wrong as I was able to alloc/init the PDF document class and have confirmed that the path is a string and the options are a dictionary. Any ideas?

Here’s the code I’m using to call the library script:


set inputFolder to (choose folder with prompt "Select a folder")
tell application "Finder"
	set newFolderName to "Compressed"
	if not (exists folder newFolderName of inputFolder) then
		make new folder at inputFolder with properties {name:newFolderName}
	end if
	set outputFolder to folder newFolderName of inputFolder
	set inputFiles to get every file of inputFolder as alias list
end tell

repeat with inputFile in inputFiles
	tell application "Finder" to set theName to name of inputFile
	set newPathToPDF to (outputFolder as text) & theName
	set newPathToPDF to quoted form of POSIX path of newPathToPDF
	set originalPathToPDF to quoted form of POSIX path of inputFile
	(script "ReduceFileSize2"'s filterPDF:originalPathToPDF newPath:newPathToPDF)
end repeat

Remove the “quoted form of” each time – you only need that for shell scripts.

Thanks - worked like a charm.

I was also getting error 4960 occasionally, but discovered from another of your posts that it was because of a bug in applescript and should only happen on the first compile.

Thanks again.

FWIW, it’s actually a bug in AppleScript Editor; if you use another editor (Script Debugger or my ASObjC Explorer) it doesn’t happen.