Removing CGPDFDocument Passwords

Working with PDFs on mobile is a pain. On the bright side at least Apple provides us CGPDFDocument

On the bright side Apple does provide CGPDFDocument. If you’re on Android you are left to 3rd party alternatives such as iText and PDFBox.

However examples on how to use CGPDFDocument are limited to the bare basics. On a recent project, I though I had a pretty basic requirement. Provide the user with the ability to remove the password for a protected PDF file. How hard could this be? Unfortunately I wasn’t able to find any examples on how to do this so decided to put my own together.

The most important thing you need to know about working with CGPDFDocument is it is immutable. This means you can’t just call unlockWithPassword and update a property to remove the password.

This means you will need to first unlock the PDF, then create an entirely new document. If you are working with large files you will need to be careful about the memory you are using. Instruments will be your best friend.

Let’s walk through an example. First we need to load a PDF file into a Data object as shown below.

let pdfData = try Data(contentsOf: URL(fileURLWithPath: "Path to pdf"))

Next you need to unlock your PDF using the existing password. In the example below we create a helper function called unlock that takes the Data from our PDF file and returns an unlocked CGPDFDocument. If the method is unable to either cast the Data object or unlock using the provided password a nil is returned.

if let pdf = unlock(data: pdfData, password: "Hello World") {
print("You now have an unlocked CGPDFDocument")
}
func unlock(data: Data, password: String) -> CGPDFDocument? {
if let pdf = CGPDFDocument(CGDataProvider(data: data as CFData)!) {
guard pdf.isEncrypted == true else { return pdf }
guard pdf.unlockWithPassword("") == false else { return pdf }
if let cPasswordString = password.cString(using: String.Encoding.utf8) {
if pdf.unlockWithPassword(cPasswordString) {
return pdf
}
}
}
return nil
}
view raw unlock.swift hosted with ❤ by GitHub

We now have an unlocked CGPDFDocument object. Since this is immutable we need to use this as the source to create a new CGPDFDocument without a password. This is done by looping through the now unlocked CGPDFDocument and copying each CGPDFPage into a new CGPDFDocument. The copyPDFtoData helper function in the below example shows how the Core Graphics context is managed as part of the looping process. The end result is a Data object that is a copy of the PDF we started with, but without the password.

if let pdf = unlock(data: pdfData, password: "Hello World") {
print("You now have an unlocked CGPDFDocument")
print("Create a copy of the unlocked CGPDFDocument")
let pdfWithoutPassword = copyPDFtoData(pdf: pdf)
print("You how have a pdf without password information")
}
func copyPDFtoData(pdf: CGPDFDocument) -> Data {
let data = NSMutableData()
autoreleasepool {
let pageCount = pdf.numberOfPages
UIGraphicsBeginPDFContextToData(data, .zero, nil)
for index in 1...pageCount {
let page = pdf.page(at: index)
let pageRect = page?.getBoxRect(CGPDFBox.mediaBox)
UIGraphicsBeginPDFPageWithInfo(pageRect!, nil)
let ctx = UIGraphicsGetCurrentContext()
ctx?.interpolationQuality = .high
// Draw existing page
ctx!.saveGState()
ctx!.scaleBy(x: 1, y: -1)
ctx!.translateBy(x: 0, y: -(pageRect?.size.height)!)
ctx!.drawPDFPage(page!)
ctx!.restoreGState()
}
UIGraphicsEndPDFContext()
}
return data as Data
}

Keep in mind this isn’t an exact copy. The CGPDFDocument info and other meta data won’t be copied as part of this process. This can easily be added to the copyPDFtoData but was overkill for this example.

For convenience I’ve combined the above helper functions into a single class for easy maintenance and experimentation.

//
// PDF.swift
//
// Created by Ben Bahrenburg on 1/1/17.
// Copyright © 2017 bencoding.. All rights reserved.
//
import UIKit
open class PDFHelpers {
class open func unlock(data: Data, password: String) -> CGPDFDocument? {
if let pdf = CGPDFDocument(CGDataProvider(data: data as CFData)!) {
guard pdf.isEncrypted == true else { return pdf }
guard pdf.unlockWithPassword("") == false else { return pdf }
if let cPasswordString = password.cString(using: String.Encoding.utf8) {
if pdf.unlockWithPassword(cPasswordString) {
return pdf
}
}
}
return nil
}
class open func removePassword(data: Data, existingPDFPassword: String) throws -> Data? {
if let pdf = unlock(data: data, password: existingPDFPassword) {
let data = NSMutableData()
autoreleasepool {
let pageCount = pdf.numberOfPages
UIGraphicsBeginPDFContextToData(data, .zero, nil)
for index in 1...pageCount {
let page = pdf.page(at: index)
let pageRect = page?.getBoxRect(CGPDFBox.mediaBox)
UIGraphicsBeginPDFPageWithInfo(pageRect!, nil)
let ctx = UIGraphicsGetCurrentContext()
ctx?.interpolationQuality = .high
// Draw existing page
ctx!.saveGState()
ctx!.scaleBy(x: 1, y: -1)
ctx!.translateBy(x: 0, y: -(pageRect?.size.height)!)
ctx!.drawPDFPage(page!)
ctx!.restoreGState()
}
UIGraphicsEndPDFContext()
}
return data as Data
}
return nil
}
}

For more comprehensive PDF handling check out my PDFUtilities project.