Create PDF from Image Files

The script included below adds image files in a selected folder to a PDF. The default image folder and dialog position can be set at the top of the displayDialog handler, and the amount of JPEG compression can be set at the top of the makePDF handler. This script uses Shane’s Dialog Toolkit Plus script library, which can be downloaded from:

It should be noted that recent versions of macOS have the native ability to add image files to a PDF. This script might be of use to those who are running older versions of macOS or who prefer batch processing of all image files in a folder.

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

on main()
	set {tiffFormat, dateSort, pdfWithImages, imageFolder} to displayDialog()
	set imageFolder to current application's |NSURL|'s fileURLWithPath:imageFolder
	set pdfFile to getPdfFile(imageFolder, pdfWithImages)
	set imageFiles to getImageFiles(imageFolder, dateSort)
	set fileCount to imageFiles's |count|()
	if fileCount = 0 then errorAlert("No image files were found in the selected folder")
	try
		makePDF(imageFiles, fileCount, pdfFile, tiffFormat)
	on error
		errorAlert("An error occurred while adding an image to the PDF")
	end try
	display notification "Images added to PDF: " & fileCount with title "PDF Created"
end main

on displayDialog()
	set dialogPosition to {} -- insert x and y coordinates if desired
	set defaultImageFolder to POSIX path of (path to home folder) -- change if desired
	set dialogWidth to 430
	set verticalSpace to 8
	set {theButtons, minWidth} to create buttons {"Cancel", "OK"} cancel button 1 default button 2
	set {tiffFormat, theTop, newWidth} to create checkbox "Convert images to TIFF format before adding them to the PDF" bottom verticalSpace max width dialogWidth
	set {dateSort, theTop, newWidth} to create checkbox "Add images to the PDF in order of their creation date" bottom theTop + verticalSpace max width dialogWidth
	set {pdfWithImages, theTop, newWidth} to create checkbox "Save the PDF in folder with images files" bottom theTop + verticalSpace max width dialogWidth
	set {optionsMessaage, theTop} to create label "The PDF will be saved on the desktop; images will be added to the PDF in the order of their file name; and images will be converted to JPEG format before being added to the PDF. These can be changed to:" bottom theTop + verticalSpace max width dialogWidth control size regular size
	set {imageFolder, theTop} to create path control defaultImageFolder bottom (theTop + verticalSpace) control width dialogWidth
	set {pathMessage, theTop} to create label "Choose or drag a folder containing image files to:" bottom theTop + verticalSpace max width dialogWidth control size regular size
	set theControls to {tiffFormat, dateSort, pdfWithImages, optionsMessaage, imageFolder, pathMessage}
	set {buttonName, controlsResults} to display enhanced window "Image to PDF" acc view width dialogWidth acc view height theTop acc view controls theControls initial position dialogPosition buttons theButtons without align cancel button
	if buttonName is "Cancel" then error number -128
	return {item 1, item 2, item 3, item 5} of controlsResults
end displayDialog

on getPdfFile(imageFolder, pdfWithImages)
	if pdfWithImages is false then
		set targetFolder to current application's NSHomeDirectory()
		set targetFolder to current application's |NSURL|'s fileURLWithPath:targetFolder
		set targetFolder to targetFolder's URLByAppendingPathComponent:"Desktop" isDirectory:true
	else
		set targetFolder to imageFolder
	end if
	set sourceFolderName to imageFolder's lastPathComponent()
	set pdfFile to sourceFolderName's stringByAppendingString:" Images.pdf"
	set pdfFile to (targetFolder's URLByAppendingPathComponent:pdfFile isDirectory:false)
	repeat with i from 1 to 100
		set fileExists to (pdfFile's checkResourceIsReachableAndReturnError:(missing value))
		if fileExists as boolean is false then return pdfFile
		set pdfFile to (sourceFolderName's stringByAppendingString:(" Images " & i & ".pdf"))
		set pdfFile to (targetFolder's URLByAppendingPathComponent:pdfFile isDirectory:false)
	end repeat
	errorAlert("The PDF file could not be created")
end getPdfFile

on getImageFiles(theFolder, dateSort)
	set theExtensions to {"jpg", "png", "tiff"}
	set fileManager to current application's NSFileManager's defaultManager()
	set {theResult, isDirectory} to (theFolder's getResourceValue:(reference) forKey:(current application's NSURLIsDirectoryKey) |error|:(missing value))
	if isDirectory as boolean is false then errorAlert("The image folder is not recognized as a folder")
	set folderContents to fileManager's contentsOfDirectoryAtURL:theFolder includingPropertiesForKeys:{} options:4 |error|:(missing value)
	set thePredicate to current application's NSPredicate's predicateWithFormat_("pathExtension.lowercaseString IN %@", theExtensions)
	set theFiles to (folderContents's filteredArrayUsingPredicate:thePredicate)
	if dateSort is false then
		set sortDescriptor to current application's NSSortDescriptor's sortDescriptorWithKey:"path" ascending:true selector:"localizedStandardCompare:"
		return theFiles's sortedArrayUsingDescriptors:{sortDescriptor}
	else
		set filePaths to current application's NSMutableArray's new()
		set theKeys to {current application's NSURLPathKey, current application's NSURLCreationDateKey}
		repeat with anItem in theFiles
			(filePaths's addObject:(anItem's resourceValuesForKeys:theKeys |error|:(missing value)))
		end repeat
		filePaths's sortUsingDescriptors:{current application's NSSortDescriptor's sortDescriptorWithKey:(current application's NSURLCreationDateKey) ascending:true}
		set filePaths to (filePaths's valueForKey:(current application's NSURLPathKey))
		set fileURLs to current application's NSMutableArray's new()
		repeat with anItem in filePaths
			set theURL to (current application's |NSURL|'s fileURLWithPath:anItem)
			(fileURLs's addObject:theURL)
		end repeat
		return fileURLs
	end if
end getImageFiles

on makePDF(theFiles, fileCount, pdfFile, tiffFormat)
	set jpegCompression to 0.9 -- 0.0 is maximum compression and 1.0 is no compression
	set pdfDoc to current application's PDFDocument's new()
	repeat with i from 0 to (fileCount - 1)
		set theFile to (theFiles's objectAtIndex:i)
		set imageRep to (current application's NSBitmapImageRep's imageRepWithContentsOfURL:theFile)
		if tiffFormat as boolean is true then
			set theData to (imageRep's representationUsingType:(current application's NSTIFFFileType) |properties|:{NSTIFFCompression:(current application's NSTIFFCompressionLZW)})
		else
			set theData to (imageRep's representationUsingType:(current application's NSJPEGFileType) |properties|:{NSImageCompressionFactor:jpegCompression})
		end if
		set theImage to (current application's NSImage's alloc()'s initWithData:theData)
		set thePDFPage to (current application's PDFPage's alloc's initWithImage:theImage)
		(pdfDoc's insertPage:thePDFPage atIndex:i)
	end repeat
	pdfDoc's writeToURL:pdfFile
end makePDF

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

main()

Forum members occasionally express their dislike for long scripts, and my script in post 1 certainly fits in that category. The following is an abbreviated version–it lacks any options and has no error correction but might be preferred for its brevity.

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

set imageFolder to (choose folder)
set pdfFile to POSIX path of ((path to desktop as text) & "PDF Images.pdf")
tell application "Finder" to set imageFiles to (every file in imageFolder whose name extension is in {"jpg", "png", "tiff"}) as alias list
set imageFiles to current application's NSArray's arrayWithArray:imageFiles
set pdfDoc to current application's PDFDocument's new()
repeat with i from 0 to ((imageFiles's |count|()) - 1)
	set imageRep to (current application's NSBitmapImageRep's imageRepWithContentsOfURL:(imageFiles's objectAtIndex:i))
	set theData to (imageRep's representationUsingType:(current application's NSJPEGFileType) |properties|:{NSImageCompressionFactor:0.9})
	set theImage to (current application's NSImage's alloc()'s initWithData:theData)
	set thePDFPage to (current application's PDFPage's alloc's initWithImage:theImage)
	(pdfDoc's insertPage:thePDFPage atIndex:i)
end repeat
pdfDoc's writeToFile:pdfFile

This script differs from those above in two respects. First, it works on image files selected in a Finder window. Second. it uses lossy compression (DCTDecode) with a JPG file and lossless compression (FlateDecode) otherwise. This permits high compression of JPEGs in a PDF without degrading the quality of a PNG or TIFF image in the same PDF.

-- revised 2022.11.19

use framework "AppKit" -- for NSImage
use framework "Foundation"
use framework "Quartz"
use scripting additions

on main()
	set pdfFile to POSIX path of (path to desktop) & "PDF Images.pdf"
	tell application "Finder" to set imageFiles to selection as alias list
	if imageFiles = {} then errorAlert("Finder window or file selection not found")
	try
		makePDF(imageFiles, pdfFile)
	on error
		errorAlert("The PDF could not be created. This often happens when a selected file cannot be read.")
	end try
end main

on makePDF(imageFiles, pdfFile)
	set imageFiles to current application's NSArray's arrayWithArray:imageFiles
	set pdfDoc to current application's PDFDocument's new()
	repeat with i from 0 to ((imageFiles's |count|()) - 1)
		set fileExtension to ((imageFiles's objectAtIndex:i)'s pathExtension())'s lowercaseString()
		set imageRep to (current application's NSBitmapImageRep's imageRepWithContentsOfURL:(imageFiles's objectAtIndex:i))
		if (fileExtension's isEqualToString:"jpg") then
			set theData to (imageRep's representationUsingType:(current application's NSJPEGFileType) |properties|:{NSImageCompressionFactor:0.8})
		else
			set theData to (imageRep's representationUsingType:(current application's NSTIFFFileType) |properties|:{NSTIFFCompression:(current application's NSTIFFCompressionLZW)})
		end if
		set theImage to (current application's NSImage's alloc()'s initWithData:theData)
		set thePDFPage to (current application's PDFPage's alloc's initWithImage:theImage)
		(pdfDoc's insertPage:thePDFPage atIndex:i)
	end repeat
	pdfDoc's writeToFile:pdfFile
end makePDF

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

main()

I revised my script in post 3 above to use a dialog to select image files and to make it easier for the user to set the JPEG compression level. I also ran some tests to determine the extent to which modifying JPEG compression changed file size. My test files were a screenshot in PNG format and a digital photo in JPEG format.

Screenshot - original - 647 KB
Screenshot - PDF at 0.0 compression - 691 KB
Screenshot - PDF at 1.0 compression - 691 KB

Digital photo - original - 5 MB
Digital photo - PDF at 0.0 compression - 984 KB
Digital photo - PDF at 1.0 compression - 9.5 MB

Screenshot and digital photo - PDF at 0.0 compression - 1.7 MB
Screenshot and digital photo - PDF at 1.0 compression - 10.1 MB

Just out of curiosity, I modified my script to force it to use JPEG format with maximum compression on both the screenshot and digital photo. As expected, the PDF file size was slightly smaller at 1.2 MB and the quality of the screenshot page of the PDF was somewhat degraded.

Just as a point of information, PDFs do not save images in JPEG, PNG, or any other standard format and instead use other lossy and lossless formats.

-- revised 2023.03.18

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

on main()
	set imageFiles to (choose file of type {"public.image"} with multiple selections allowed)
	set pdfFile to POSIX path of (path to desktop) & "PDF Images.pdf"
	try
		makePDF(imageFiles, pdfFile)
	on error
		display alert "The PDF could not be created" message "This often occurs when a selected file cannot be read." as critical
		error number -128
	end try
end main

on makePDF(imageFiles, pdfFile)
	set lossyCompression to "0.2" -- 0.0 is maximum compression and 1.0 is no compression
	set lossyExtensions to current application's NSArray's arrayWithArray:{"jpg"} -- user edit as desired
	set imageFiles to current application's NSArray's arrayWithArray:imageFiles
	set pdfDoc to current application's PDFDocument's new()
	repeat with i from 0 to ((imageFiles's |count|()) - 1)
		set fileExtension to ((imageFiles's objectAtIndex:i)'s pathExtension())'s lowercaseString()
		set imageRep to (current application's NSBitmapImageRep's imageRepWithContentsOfURL:(imageFiles's objectAtIndex:i))
		if (lossyExtensions's containsObject:fileExtension) as boolean is true then
			set theData to (imageRep's representationUsingType:(current application's NSJPEGFileType) |properties|:{NSImageCompressionFactor:(lossyCompression as real)})
		else
			set theData to (imageRep's representationUsingType:(current application's NSTIFFFileType) |properties|:{NSTIFFCompression:(current application's NSTIFFCompressionLZW)})
		end if
		set theImage to (current application's NSImage's alloc()'s initWithData:theData)
		set thePDFPage to (current application's PDFPage's alloc's initWithImage:theImage)
		(pdfDoc's insertPage:thePDFPage atIndex:i)
	end repeat
	pdfDoc's writeToFile:pdfFile
end makePDF

main()
1 Like

It was my intent that the scripts in posts 3 and 5 above would save JPEG images in the PDF in a lossy format (which would allow significant file-size reductions) and PNG and other images in a lossless format (which would maintain high quality). I’m fairly certain that doesn’t happen and instead that all image data is stored in the PDF in a lossy format, which is also what happens when the Finder is used to create a PDF from images.

Given the above, I modified my script to allow the user to choose different compression levels for different image formats. Thus, for example, the user might select high compression for JPEG images and limited compression for other images.

-- revised 2023.05.24

use framework "AppKit"
use framework "Foundation"
use framework "PDFKit"
use scripting additions

on main()
	set imageFiles to (choose file of type {"public.image"} with multiple selections allowed)
	set pdfFile to POSIX path of (path to desktop) & "PDF Images.pdf"
	try
		makePDF(imageFiles, pdfFile)
	on error
		display alert "The PDF could not be created" message "This often occurs when a selected file cannot be read." as critical
		error number -128
	end try
end main

on makePDF(imageFiles, pdfFile)
	set lowCompression to "0.8" -- 0.0 is maximum compression and 1.0 is no compression
	set highCompression to "0.2"
	set highCompressionExtensions to current application's NSArray's arrayWithArray:{"jpg", "jpeg"}
	set imageFiles to current application's NSArray's arrayWithArray:imageFiles
	set pdfDoc to current application's PDFDocument's new()
	repeat with i from 0 to ((imageFiles's |count|()) - 1)
		set fileExtension to ((imageFiles's objectAtIndex:i)'s pathExtension())'s lowercaseString()
		set imageRep to (current application's NSBitmapImageRep's imageRepWithContentsOfURL:(imageFiles's objectAtIndex:i))
		if (highCompressionExtensions's containsObject:fileExtension) as boolean is true then
			set theData to (imageRep's representationUsingType:(current application's NSJPEGFileType) |properties|:{NSImageCompressionFactor:(highCompression as real)})
		else
			set theData to (imageRep's representationUsingType:(current application's NSJPEGFileType) |properties|:{NSImageCompressionFactor:(lowCompression as real)})
		end if
		set theImage to (current application's NSImage's alloc()'s initWithData:theData)
		set thePDFPage to (current application's PDFPage's alloc's initWithImage:theImage)
		(pdfDoc's insertPage:thePDFPage atIndex:i)
	end repeat
	pdfDoc's writeToFile:pdfFile
end makePDF

main()

The following demonstrates the impact on PDF file size of different compression levels. The test images were a PNG screenshot of a 2-paragraph text file (281 KB) and a JPEG digital photo (5 MB). A setting of 0.0 is maximum compression and a setting of 1.0 is no compression.

PNG COMPRESSION - JPG COMPRESSION - PDF FILE SIZE
0.2 - 0.2 - 1.3 MB
0.8 - 0.2 - 1.6 MB
0.8 - 0.8 - 5.2 MB
unknown - unknown - 5.2 MB (created with Finder)

 
If you want really to add the image to any position of existing PDF’s first page, you can do something like this. It will work with any image, but for testing purposes select better some small image. With repeat loop on PDF pages you can add different images to different pages.
 

use scripting additions
use framework "Foundation"
use framework "AppKit"
use framework "Quartz"
use framework "QuartzCore"

set theDpi to 72
set aPDF to choose file of type "pdf"
set destPosixPath to (POSIX path of (path to desktop folder)) & "imageIncludedPDF.pdf"

-- Get PDF stuff
set aURL to (current application's |NSURL|'s fileURLWithPath:(POSIX path of aPDF))
set aPDFdoc to current application's PDFDocument's alloc()'s initWithURL:aURL

set thisPage to (aPDFdoc's pageAtIndex:0)
set pageSize to (thisPage's boundsForBox:(current application's kPDFDisplayBoxMediaBox))
set pageWidth to current application's NSWidth(pageSize)
set pageHeight to current application's NSHeight(pageSize)
set pixelWidth to (pageWidth * theDpi / 72) div 1
set pixelHeight to (pageHeight * theDpi / 72) div 1
-- make bitmaps
set theImageRep to (current application's NSPDFImageRep's imageRepWithData:(thisPage's dataRepresentation()))
set newRep to (current application's NSBitmapImageRep's alloc()'s initWithBitmapDataPlanes:(missing value) pixelsWide:pixelWidth pixelsHigh:pixelHeight bitsPerSample:8 samplesPerPixel:4 hasAlpha:yes isPlanar:false colorSpaceName:(current application's NSDeviceRGBColorSpace) bytesPerRow:0 bitsPerPixel:32)

-- do drawing
current application's NSGraphicsContext's saveGraphicsState()
(current application's NSGraphicsContext's setCurrentContext:(current application's NSGraphicsContext's graphicsContextWithBitmapImageRep:newRep))
(theImageRep's drawInRect:{origin:{x:0, y:0}, |size|:{width:pixelWidth, height:pixelHeight}} fromRect:(current application's NSZeroRect) operation:(current application's NSCompositeSourceOver) fraction:1.0 respectFlipped:false hints:(missing value))
-- do image drawing
set imagePath to POSIX path of (choose file of type "public.image")
set theImageRep to (current application's NSBitmapImageRep's imageRepWithContentsOfFile:imagePath)
set theHeight to theImageRep's pixelsHigh()
set theWidth to theImageRep's pixelsWide()
(theImageRep's drawInRect:{{0, 0}, {theWidth, theHeight}} fromRect:{{0, 0}, {theWidth, theHeight}} operation:(current application's NSCompositeSourceOver) fraction:1.0 respectFlipped:false hints:(missing value))
current application's NSGraphicsContext's restoreGraphicsState()

-- make PDF page from bitmap
(newRep's setSize:{pageWidth, pageHeight})
set theData to newRep's TIFFRepresentation()
set theImage to (current application's NSImage's alloc()'s initWithData:theData)
set newPage to (current application's PDFPage's alloc()'s initWithImage:theImage)
-- modify PDF doc 
(aPDFdoc's removePageAtIndex:0)
(aPDFdoc's insertPage:newPage atIndex:0)

-- save
set outNSURL to current application's |NSURL|'s fileURLWithPath:destPosixPath
aPDFdoc's writeToURL:outNSURL

 
NOTE: to draw the image from position {20,20} instead of {0,0}:

(theImageRep’s drawInRect:{{20, 20}, {theWidth, theHeight}} fromRect:{{0, 0}, {theWidth, theHeight}} operation:(current application’s NSCompositeSourceOver) fraction:1.0 respectFlipped:false hints:(missing value))

1 Like

@KniazidisR. Thanks for the script.

The title of this thread is a bad one. It should be “Create PDF from Image Files”, because that’s what all of my scripts do (I’ll ask that the title be changed). However, your script–which superimposes an image on an existing page of a PDF–is a great addition.

I just have to warn you that this script will rasterize the PDF page(s). It would be interesting to learn (from more experienced users) how to do the same (with AsObjC) without rasterizing the page. If it is possible of course.

Blockquote

You can do the following route:

Load PDFDocument into PDFView > addSubview to documentView > dataWithPDFInsideRect (of PDFView) > write data.

The addSubview stage allows you to add whatever you want to your PDF at the desired position and size (superimposing, of course).

I don’t have any useful working code to post here - especially not in ASOC - but you obviously have deep enough knowledge to apply this approach. It’s pretty straightforward.

P.S. The credit for this approach actually goes to David Chisnall and his book “Cocoa Programming” (2009).

1 Like

 
I tried to write such a script but failed. :sleepy: For some reason it creates an empty PDF:
 

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

set aPDF to choose file of type "com.adobe.pdf"
set destPosixPath to (POSIX path of (path to desktop folder)) & "imageIncludedPDF.pdf"

-- Get PDF stuff
set aURL to (current application's |NSURL|'s fileURLWithPath:(POSIX path of aPDF))
set aPDFdoc to current application's PDFDocument's alloc()'s initWithURL:aURL
set thisPage to (aPDFdoc's pageAtIndex:0)
set pageSize to (thisPage's boundsForBox:(current application's kPDFDisplayBoxMediaBox))
set PDFView to current application's PDFView's new()
PDFView's setDocument:aPDFdoc -- set PDFView document

set imagePath to POSIX path of (choose file of type "public.image")
set imageURL to (current application's |NSURL|'s fileURLWithPath:imagePath)
set anImage to (current application's NSImage's alloc()'s initWithContentsOfFile:imagePath)
set imagePage to (current application's PDFPage's alloc's initWithImage:anImage)
set imagePDF to current application's PDFDocument's new()
(imagePDF's insertPage:imagePage atIndex:0)
set imagePDFView to current application's PDFView's new()
imagePDFView's setDocument:imagePDF -- set subview document

PDFView's addSubview:imagePDFView -- add subview

set theData to PDFView's dataWithPDFInsideRect:pageSize -- the raw data
set outNSURL to current application's |NSURL|'s fileURLWithPath:destPosixPath
theData's writeToURL:outNSURL atomically:true

 

try to create NSImageView, setImage: your image, then use this in addSubview