Posts Tagged ‘iPhone’
In one of my latest applications, I wanted to allow users to
A) Turn text into a PDF file (multi-page document if needed)
B) Optionally set a password to protect this very PDF file
C) Set permissions to copy/print the PDF file
Since I haven’t played with PDF content creation on the iPhone/iPod Touch/iPad before, I fired up google and discovered THIS source. It was very helpful for understanding multi-page PDF creation. However there were two problems:
- The created PDF document ignored the chosen font and size.
- It did not contain information on how to password protect the PDF file.
After playing with this code for a while, I discovered a loop hole. If you “Push” your context, you can use the following method to draw a string with the correct font/size:
UIGraphicsPushContext(pdfContext);
[myNSString drawInRect:bounds withFont:[UIFont fontWithName:fontName size:fontSize]];
UIGraphicsPopContext();
Problem (1) was solved now and after reading Apple’s documentation on PDF file creation, I had an idea how password protection might work as well.
Ok, now that I had all the ingredients, it was time to get this thing up and running.
Before you read on, please keep in mind, that part of the code below has been thrown together, in order to get useful results real quick. I somehow got a feeling, that there is (must be) a more elegant way to do this.
Anyways, here we go…
I created 2 different methods:
1:
- (void) createPDF:(NSString *)fileName withContent:(NSString *)content forSize:(int)fontSize andFont:(NSString *)font andColor:(UIColor *)color:(BOOL)allowCopy:(BOOL)allowPrint:(NSString*)password;
This method accepts the following variables:
- fileName: the name we want to assign to our PDF file (e.g. “myPDF.pdf”).
- content: the text (string) we want to write to our PDF document.
- fontSize: size of our font (9, 10, 12, 14, 16, etc…)
- font: name of the font, we want to use (e.g. “Helvetica”).
- color: the color, we want to use (black, blue, etc…)
- allowCopy: YES/NO – whether we want to allow copy for this document
- allowPrint: YES/NO – whether we want to allow print for this document
- passoword: a user defined password to unlock content of the pdf file.
2:
- (NSString *)stringToDraw:(NSString*)fontName:(int)fontSize;
This method will be called from our method (1) and will return a NSString object. More on that later…
Before we take a closer look at method (1), we need to add the following lines to the .m file (right below #import lines), we’re working in:
//defines our default PDF page layout #define LEFT_MARGIN 25 #define RIGHT_MARGIN 25 #define TOP_MARGIN 35 #define BOTTOM_MARGIN 50 #define BOTTOM_FOOTER_MARGIN 32 #define DOC_WIDTH 595 #define DOC_HEIGHT 842
We also need a NSString object, that will hold some text for us later, a BOOL value, named “done” and a NSMutableArray. Simply create those in your viewDidLoad method or wherever you initialize your class:
//in .h
NSString *tempContentString;
NSMutableArray *textArray;
BOOL done;
@property (nonatomic, retain) NSString *tempContentString;
@property (nonatomic, retain) NSString *textArray;
//in .m
@synthesize tempString;
@synthesize textArray;
- (void)viewDidLoad {
tempContentString = [[NSString alloc] init];
textArray = [[NSMutableArray] alloc] init];
}
- (void)dealloc {
[tempContentString release];
[textArray release];
[super dealloc];
}
Now, let’s assume, that we have a UITextView element, named “myTextView” and this UITextView is filled with text.
Before we call method (1), we need to prepare a few things
- (void)preparePDFCreation {
NSString *fileName = @"myPDF.pdf"; //set whatever name you like
NSString *content = myTextView.text; //this is the UITextView, containing our text.
int fontSize = 20; //set your fontSize here
NSString *font = @"Papyrus"; //set your font name here (Helvetica, Georgia, etc...)
UIColor *myColor = [UIColor blackColor]; //set your color here (blueColor, brownColor, etc...)
NSString *password = @"test"; //set your password
//prepare saving our PDF file to documents directory
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *saveDirectory = [paths objectAtIndex:0];
NSString *saveFileName = fileName;
NSString *newFilePath = [saveDirectory stringByAppendingPathComponent:saveFileName];
tempContentString = content; //we assign our content (text we want to draw to our PDF file) to the tempContentString, we created earlier
BOOL pdfAllowCopy = YES; //allow/forbid copy
BOOL pdfAllowPrint = NO; //allow/forbid print
//important: if you forbid copy or print, you definitely need to set a password. Otherwise it won't work!
done = NO;
[textArray removeAllObjects]; //clean the textArray
[textArray setArray:[content componentsSeparatedByString:@" "]]; //split our text into single words...we will find out why we do this in method (2)
//Now, let's call our method (1)
[self createPDF:newFilePath withContent:tempContentString forSize:fontSize andFont:font andColor:myColor:pdfAllowCopy:pdfAllowPrinting:password];
}
- (void) createPDF:(NSString *)fileName withContent:(NSString *)content forSize:(int)fontSize andFont:(NSString *)font andColor:(UIColor *)color:(BOOL)allowCopy:(BOOL)allowPrint:(NSString*)password {
CGContextRef pdfContext; //our pdfContext
CFStringRef path;
CFURLRef url;
CFStringRef passwordString = (CFStringRef)password;
CGRect pageRect = CGRectMake(0, 0, DOC_WIDTH, DOC_HEIGHT);
CFMutableDictionaryRef myDictionary = NULL; //the dictionary, which will later contain some important meta data, like password, etc...
const char *filename = [fileName UTF8String];
// Create a CFString from the filename we provide to this method when we call it
path = CFStringCreateWithCString (NULL, filename,
kCFStringEncodingUTF8);
// Create a CFURL using the CFString we just defined
url = CFURLCreateWithFileSystemPath (NULL, path,
kCFURLPOSIXPathStyle, 0);
// This dictionary contains extra options mostly for 'signing' the PDF
myDictionary = CFDictionaryCreateMutable(NULL, 0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(myDictionary, kCGPDFContextTitle, (CFStringRef)mainDelegate.rootViewController.writeViewController.titleTextField.text);
CFDictionarySetValue(myDictionary, kCGPDFContextCreator, (CFStringRef)mainDelegate.rootViewController.writeViewController.authorTextField.text);
if (![password isEqualToString:@""]) CFDictionarySetValue(myDictionary, kCGPDFContextOwnerPassword, passwordString);
if (![password isEqualToString:@""]) CFDictionarySetValue(myDictionary, kCGPDFContextUserPassword, passwordString);
if (!allowCopy) CFDictionarySetValue(myDictionary, kCGPDFContextAllowsCopying, kCFBooleanFalse); //kCGPDFContextAllowsCopying is set to TRUE by default
if (!allowPrint) CFDictionarySetValue(myDictionary, kCGPDFContextAllowsPrinting, kCFBooleanFalse); //kCGPDFContextAllowsPrinting is set to TRUE by default
// Create our PDF Context with the CFURL, the CGRect we provide, and the above defined dictionary
pdfContext = CGPDFContextCreateWithURL (url, &pageRect, myDictionary);
// Cleanup our mess
CFRelease(myDictionary);
CFRelease(url);
//Now, this is a tricky part. We make use of a do - while loop in order to create as many pages as needed
do {
CGContextBeginPage (pdfContext, &pageRect); //begins a new PDF page
//create layout for our page
CGRect bounds = CGRectMake(LEFT_MARGIN,
TOP_MARGIN,
DOC_WIDTH - RIGHT_MARGIN - LEFT_MARGIN,
DOC_HEIGHT - TOP_MARGIN - BOTTOM_MARGIN);
UIGraphicsPushContext(pdfContext); //pushing the context, as explained at the beginning of this post
CGContextSaveGState(pdfContext);
CGContextTranslateCTM(pdfContext, 0, bounds.origin.y);
CGContextScaleCTM(pdfContext, 1, -1);
CGContextTranslateCTM(pdfContext, 0, -(bounds.origin.y + bounds.size.height));
if ([tempContentString length] > 0) [[self stringToDraw] drawInRect:bounds withFont:[UIFont fontWithName:font size:fontSize]]; //THIS IS THE NASTY PART
CGContextRestoreGState(pdfContext);
UIGraphicsPopContext();
CGContextEndPage (pdfContext); //ends the current page
}
while (!done);
// We are done with our context now, so we release it
CGContextRelease (pdfContext);
CFRelease(path);
}
Now, it’s getting messy. We’re about to explore the “stringToDraw” method. Before we go into detail, let me explain, what this method does.
When we draw text to our page, we have a huge problem. We do not know how much text will fit on a single page. This depends on length of the words used, font and size.
So, I figured, there must be a way to calculate this. And here is what I came up with:
Step by step…
- create a CGSize, named tempSize.
- create a CGSize, named theTextSize.
- use – sizeWithFont:constrainedToSize: method to determine, whether the text we want to draw, is too big for one page or not
- create if/else statements for results of (3)
Here is the complete method with documentation:
- (NSString *)stringToDraw:(NSString*)fontName:(int)fontSize {
CGSize tempSize;
CGSize theTextSize;
tempSize.width = DOC_WIDTH - RIGHT_MARGIN - LEFT_MARGIN; //define width of our document
tempSize.height = 10000000; //we use some unreal number, to make sure our text will definitely fit into this temporary "document"
//now we take the text we want to draw and use the method described in (3) to fit it on our temporary document
theTextSize = [tempContentString sizeWithFont: [UIFont fontWithName:fontName size:fontSize] constrainedToSize: tempSize];
//Depending on length of the text, it will fit on a single page or not.
if (theTextSize.height > DOC_HEIGHT - TOP_MARGIN - BOTTOM_MARGIN) { //the text we want to draw DOES NOT fit on a single page
BOOL pageFilled = NO;
int wordCount = 0;
float currentHeight = 0.0f;
float previousHeight = 0.0f;
NSString *returnString = [[[NSString alloc] init]autorelease];
NSString *tempReturnString = [[[NSString alloc] init]autorelease];
//Now, the following is a bit messy. However, it works
do { //repeat this loop, till our page is filled with words...
if ([textArray count] > wordCount + 1) { //if there are more words inside our textArray than the current word count + 1
returnString = (wordCount > 0) ? [returnString stringByAppendingString:[NSString stringWithFormat:@" %@", [textArray objectAtIndex:wordCount]]] : [NSString stringWithFormat:@"%@", [textArray objectAtIndex:wordCount]];
theTextSize = [returnString sizeWithFont: [UIFont fontWithName:font size:fontSize] constrainedToSize: CGSizeMake(DOC_WIDTH - RIGHT_MARGIN - LEFT_MARGIN, DOC_HEIGHT - TOP_MARGIN - BOTTOM_MARGIN)];
currentHeight = theTextSize.height;
if (theTextSize.height >= DOC_HEIGHT - TOP_MARGIN - BOTTOM_MARGIN) {
pageFilled = YES; // our page is now filled with words
}
if (currentHeight == previousHeight && currentHeight > 700.0f) { //this is a workaround. sometimes the above if statement fails and does not correctly detect a filled page. i noticed, that it happens most of the time, when using large fonts, like zapfino. so, what we do here, is simply check, if the height of our text stays the same when comparing two loops - hence "currentHeight" and "previousHeight". if that's the case and our text height is greater than 700 (look at if statement), we have a filled page.
pageFilled = YES; //page is now filled with words
wordCount --; //we subtract one word from our wordCount, because the filled page has not been correctly detected above
returnString = tempReturnString;
}
wordCount ++; //increase the wordCount
}
else {
pageFilled = YES; //we now have a correctly filled page
}
previousHeight = theTextSize.height; //set previousHeight, so we can compare it when running our next loop
tempReturnString = returnString;
}
while (!pageFilled); //repeat this loop until pageFilled = YES
for (int i = 0; i < wordCount; i++) { //remove all words, we put on our page from our textArray
[textArray removeObjectAtIndex:0];
}
if ([textArray count]) { //if there are words in our textArray
tempContentString = [textArray componentsJoinedByString:@" "]; //update our content string
}
else {
tempContentString = @"";
done = YES;
}
if ([returnString length] == 0) returnString = @" ";
return returnString;
}
else {
done = YES;
return tempContentString;
}
return tempContentString;
}
I’ve successfully implemented this code into my latest app and so far it did a great job. I managed to create PDF files with hundred pages or more in about 3-5 seconds on my iPad.
If you find this source helpful, manage to improve it or have questions/feedback, please do not hesitate to leave a comment.
[UPDATE: I've created a little demo project, showing this code in action. Grab it here.]
NSFileManager offers a convenient way to write images to and load them from the documents directory.
If you’re frequently doing that in your project, I suggest to wrap up NSFileManager support in three simple methods:
//saving an image
- (void)saveImage:(UIImage*)image:(NSString*)imageName {
NSData *imageData = UIImagePNGRepresentation(image); //convert image into .png format.
NSFileManager *fileManager = [NSFileManager defaultManager];//create instance of NSFileManager
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); //create an array and store result of our search for the documents directory in it
NSString *documentsDirectory = [paths objectAtIndex:0]; //create NSString object, that holds our exact path to the documents directory
NSString *fullPath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.png", imageName]]; //add our image to the path
[fileManager createFileAtPath:fullPath contents:imageData attributes:nil]; //finally save the path (image)
NSLog(@"image saved");
}
//removing an image
- (void)removeImage:(NSString*)fileName {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *fullPath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.png", fileName]];
[fileManager removeItemAtPath: fullPath error:NULL];
NSLog(@"image removed");
}
//loading an image
- (UIImage*)loadImage:(NSString*)imageName {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *fullPath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.png", imageName]];
return [UIImage imageWithContentsOfFile:fullPath];
}
Now, you can easily save an image like:
[self saveImage: myUIImage: @"myUIImageName"];
or load it like:
myUIImage = [self loadImage: @"myUIImageName"];
or remove it like:
[self removeImage: @"myUIImageName"];
[IMPORTANT: The information below is no longer up to date. Apple does now no longer allow you to use UIGetScreenImage(). Instead you need to use a different method. You can read about that on the official developer forum, located at developer.apple.com]
Not too long ago Apple decided to make on of their private APIs public. They now officially allow developers to make use of a very handy method:
+ (UIImage *)imageWithScreenContents;
Before they released it, grabbing screen contents could be a real nightmare. In some (rare) situations, it was pretty much impossible.
When you were using lots of openGL stuff in your project and wanted to grab the current state of your screen, you had to use glReadPixels();
This worked fine most of the time, but when you were dealing with transparency, it could make you pull your hair off
Anyways, those times are finally over.
To make use of this API, simply add the following lines to the .m file where you want to call it later:
//right below #import lines
CGImageRef UIGetScreenImage();
@interface UIImage (ScreenImage)
+ (UIImage *)imageWithScreenContents;
@end
@implementation UIImage (ScreenImage)
+ (UIImage *)imageWithScreenContents
{
CGImageRef cgScreen = UIGetScreenImage();
if (cgScreen) {
UIImage *result = [UIImage imageWithCGImage:cgScreen];
CGImageRelease(cgScreen);
return result;
}
return nil;
}
@end
Now you can easily do something like this:
- (void)saveScreenshotToPhotolibrary {
UIImageWriteToSavedPhotosAlbum([UIImage imageWithScreenContents], nil, nil, nil);
}
Congrats! You just saved an image with screen contents to your iPhone photo library.
