I'm working with Objective Zip library in order to unzip files in iPhone.
All works fine excepts, that with text files there are not problem are uncompressed without problem and the file is correct. But with compressed png files are all corrupted. The sizes of the files are all equal to original file but are all corrupted.
This is the code:
-(void)installPackageFromZipFile:(NSString *)zipFile
{
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDir = [documentPaths objectAtIndex:0];
ZipFile *unzipFile= [[ZipFile alloc] initWithFileName:zipFile mode:ZipFileModeUnzip];
packageRegisterController *pckReg = [[packageRegisterController alloc] init];
[unzipFile goToFirstFileInZip];
NSArray *infos= [unzipFile listFileInZipInfos];
for (FileInZipInfo *info in infos)
{
NSLog([NSString stringWithFormat:#"File Found in Zip File- %# Size:%d", info.name, info.length]);
ZipReadStream *read = [unzipFile readCurrentFileInZip];
if (![pckReg detectIfFileExists:[documentsDir stringByAppendingPathComponent:info.name]])
{
NSMutableData *data = [[NSMutableData alloc] initWithLength:info.length];
int bytesRead = [read readDataWithBuffer:data];
[data writeToFile:[documentsDir stringByAppendingPathComponent:info.name] atomically:NO];
[read finishedReading];
[data release];
if ([[NSString stringWithFormat:#"%#",info.name] isEqualToString:#"TEMAMANIFEST.xml"])
{
if([self parseManifest:[documentsDir stringByAppendingPathComponent:info.name]])
if ([pckReg validateManifestId:self.temaToInstall.idManifest])
[self installManifest];
}
}
[unzipFile goToNextFileInZip];
}
[unzipFile close];
[unzipFile release];
}
This function decompress all files with good sizes and text files are ok, but not png.
Could someone help me?
Are you trying to view the images within the iPhone app, or are you zipping them, unzipping them on a Mac and trying to view them there?
PNGs are 'optimised' when built for iPhone and as a result aren't viewable on a mac without being 'unoptimised'.
When PNGs are copied to an iPhone app bundle they are sent through the pngcrush utility which takes the alpha channel value of the image and premultiplies it with the other colour channels, Red, Blue, and Green. The result is an un-viewable image on a Mac, but a speedy, easily renderable image on an iPhone.
This is done because the iPhone's graphics processor does not do alpha multiplying in hardware, but does it in software, which makes it slow. The pngcrush utility does all the alpha multiplying required during the build process so that all the images can be rendered by the hardware graphics processor really quickly.
Related
I have some png files in my iPhone app project. They work fine when I build for the simulator. But when I build for the device, suddenly every single png file generates the dreaded "while reading such-and-such.png pngcrush caught libpng error: ... Could not find file: ..."
As I say, everything builds and runs great with the simulator. It's only when I change the scheme to build for the device that I get the errors.
I tried cleaning and rebuilding.
I tried manually deleting the Products directory.
I tried restarting my system.
I tried using the files in a different project (same results there).
The only thing I have found that works is to open the files and resave them. However, this is a less than optimal solution because I have hundreds of PNG files all suffering from this issue. I would rather understand what the issue is so that I can fix it directly.
Any ideas?
It sounds as though you've got PNG files that were recompressed with Apple's rogue "pngcrush" Xcode program, which writes files that are not valid PNGs. Look for the string "CgBI" near the beginning of the file (starting at the 12th byte) where "IHDR" should be. There are applications (including the Apple version of "pngcrush") that can undo the problem.
Worked around this issue by writing a quick-and-dirty recursive file re-saver. I've verified that simply running this against my project directory fixes the 459 errors I was seeing. Here's the pertinent code in case it helps anyone.
- (IBAction) btnGo_Pressed:(id) sender {
// The path to search is specified by the user
NSString *path = self.txtPathToSearch.stringValue;
// Recursively find all files within it
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *subpaths = [fileManager subpathsOfDirectoryAtPath:path error:nil];
// Look for pngs
int totalImagesResaved = 0;
for (int j=0; j<[subpaths count]; j++) {
NSString *fullPath = [path stringByAppendingPathComponent:[subpaths objectAtIndex:j]];
// See if this path ends with a ".png"
if ([fullPath compare:#".png" options:NSCaseInsensitiveSearch range:NSMakeRange([fullPath length] - 4, 4)] == NSOrderedSame) {
// Got one. Now resave it as a png
NSImage *image = [[NSImage alloc] initWithContentsOfFile:fullPath];
[self saveImage:image asPngWithPath:fullPath];
totalImagesResaved++;
}
}
// Status report
NSAlert *alert = [NSAlert alertWithMessageText:#"Done" defaultButton:#"OK" alternateButton:nil otherButton:nil informativeTextWithFormat:#"Encountened %li paths. Resaved %i .pngs.", (unsigned long)[subpaths count], totalImagesResaved];
[alert runModal];
}
- (void) saveImage:(NSImage *) image asPngWithPath:(NSString *) path
{
// Cache the reduced image
NSData *imageData = [image TIFFRepresentation];
NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:imageData];
imageData = [imageRep representationUsingType:NSPNGFileType properties:nil];
[imageData writeToFile:path atomically:YES];
}
My app is using the NSDocumentDirectory to save images in it, I just wanna ask if its the safe way to save images(100 maximum). I have read several thread & questions with answers about it, though I dont know which to follow.Some say that its okay to save there. Some say I shouldnt use NSDocumentDirectory for saving, because it will be back-up by the iCloud. So where can I save it that when the user exit the app then run the app again, then images should still be there?. I dont know much about the tmp directory or cache directory. But if its either one of the 2 that I should use, How can I use them in my code here:
NSArray *paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask ,YES );
NSString *documentsDir = [paths objectAtIndex:0];
NSString *savedImagePath = [documentsDir stringByAppendingPathComponent:[NSString stringWithFormat:#"Images%d.png", i]];
ALAssetRepresentation *rep = [[info objectAtIndex: i] defaultRepresentation];
UIImage *image = [UIImage imageWithCGImage:[rep fullResolutionImage]];
//----resize the images
image = [self imageByScalingAndCroppingForSize:image toSize:CGSizeMake(256,256*image.size.height/image.size.width)];
NSData *imageData = UIImagePNGRepresentation(image);
[imageData writeToFile:savedImagePath atomically:YES];
Thank you so much for the help.
The tmp and cache directories are periodically cleaned up by iOS. If the images are for general use, use the camera roll as the other two answers suggest. However if these images are intended just for the scope of your app, you can still safely store them in the Documents directory, you just have to include an "exclude from iCloud backup" function call to each file after saving, in order to prevent Apple rejecting your app for using too much iCloud space. Of course there's a trade-off, disabling this means the user will lose their photos anyway should they delete the app or get another device(etc), but this caveat is preferable to not getting the App on the store at all.
To disable iCloud backup on a file, there's two methods for iOS versions > 5.0:
UPDATE! MERGED BOTH METHODS INTO A SINGLE FUNCTION THAT AUTOMATICALLY HANDLES iOS VERSION:
#include <sys/xattr.h> // Needed import for setting file attributes
+(BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)fileURL {
// First ensure the file actually exists
if (![[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]]) {
NSLog(#"File %# doesn't exist!",[fileURL path]);
return NO;
}
// Determine the iOS version to choose correct skipBackup method
NSString *currSysVer = [[UIDevice currentDevice] systemVersion];
if ([currSysVer isEqualToString:#"5.0.1"]) {
const char* filePath = [[fileURL path] fileSystemRepresentation];
const char* attrName = "com.apple.MobileBackup";
u_int8_t attrValue = 1;
int result = setxattr(filePath, attrName, &attrValue, sizeof(attrValue), 0, 0);
NSLog(#"Excluded '%#' from backup",fileURL);
return result == 0;
}
else if (&NSURLIsExcludedFromBackupKey) {
NSError *error = nil;
BOOL result = [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:&error];
if (result == NO) {
NSLog(#"Error excluding '%#' from backup. Error: %#",fileURL, error);
return NO;
}
else { // Succeeded
NSLog(#"Excluded '%#' from backup",fileURL);
return YES;
}
} else {
// iOS version is below 5.0, no need to do anything
return YES;
}
}
If your app must support 5.0, then unfortunately your only option is to save those photos in the Caches directory, which means they won't be backed up (this not causing an App Store rejection for that reason), but whenever the storage watchdog decides it's time to clean the Caches folder, you'll lose those photos. Not an ideal implementation at all, but such is the nature of the beast in 5.0, where Apple added in Backup exclusion as an afterthought.
EDIT: Forgot to answer the 'how to save to the tmp/cache directory' part of the question. If you do decide to go down that path:
Saving to tmp:
NSString *tempDir = NSTemporaryDirectory();
NSString *savedImagePath = [tempDir stringByAppendingPathComponent:[NSString stringWithFormat:#"Images%d.png", i]];
(note that this won't appear to have any effect in the simulator, but it works as expected on device)
Saving to Cache:
NSString *cacheDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES)lastObject];
NSString *savedImagePath = [cacheDir stringByAppendingPathComponent:[NSString stringWithFormat:#"Images%d.png", i]];
If you want the user to be able to use the images in other apps or view them along with their photos, use the photo album as Mike D suggest. If the files are something you generate locally for use with your app only, then you should probably use the documents directory. You can expose the documents directory to iTunes with the info.plist option "Application supports iTunes file sharing" which will allow the user to add or delete files through iTunes, but the files will not be exposed to other apps
You are saving scaled images so they are really only useful for your game. They are not going to be very large and will not take up much space. You could save them in the Library directory for the app and avoid the whole iCloud thing, as it doesn't sound like there is any reason to back them up. Also, saving the the Library avoid the possibility of the user deleting them, if for some other reason you have iTunes sharing turned on.
Update: code for saving to the app Library directory
- (void)saveSequences:(NSMutableDictionary*)sequences
{
NSArray *path = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *libDirectory = [path objectAtIndex:0];
NSString *settingsPath = [libDirectory stringByAppendingPathComponent:#"userSequences.plist"];
NSLog(#"settingsPath %#", settingsPath);
[sequences writeToFile:settingsPath atomically:YES];
}
// The code below gets the path to a named directory in the 'Documents' folder - and if it doesn't exist, creates it. Adjust it to use the Library path, if you decide to go that route.
- (NSString *)getDirectoryBySequenceName:(NSString *)sequenceName
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString * documentDirectory = [paths objectAtIndex:0];
NSString * sequenceDirectory = [documentDirectory stringByAppendingPathComponent:sequenceName];
NSError *error;
BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:sequenceDirectory
withIntermediateDirectories:YES
attributes:nil error:&error];
if (!success) {
NSLog(#"Error creating data path: %#", [error localizedDescription]);
}
return sequenceDirectory;
}
Depending on the purpose of your app, you could save it to the photos app (UIImageWriteToSavedPhotosAlbum(image, self, nil, nil) I think, Apple reference). Saving here, or in the documents directory (or any sub folder), will allow the user backup those images to iCloud or iTunes, if the user chooses too and/or if they have set up iCloud.
Since you state the images need to persist between launches, the temp or cache directory get emptied when the application is removed from memory, maybe sooner (the O/S decides).
More about the iOS file system.
I am attempting to save and load a UIImage to and from the iPhone documents directory after the image is picked from the iPhone Photo Library. It is able to do so, but for some reason, when I load the image, it rotates it 90 degrees counterclockwise. Here is my saveImage and loadImage methods:
Save Image:
- (void)saveImage: (UIImage*)image{
if (image != nil)
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString* path = [documentsDirectory stringByAppendingPathComponent:
[NSString stringWithString: #"lePhoto.png"] ];
//NSData* data = UIImagePNGRepresentation(image);
//[data writeToFile:path atomically:YES];
[UIImagePNGRepresentation(image) writeToFile:path atomically:YES];
}
}
Load Image:
- (NSData*)loadImage{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString* path = [documentsDirectory stringByAppendingPathComponent:
[NSString stringWithString: #"lePhoto.png"] ];
NSData *data = [[NSData alloc] initWithContentsOfFile:path];
//UIImage *image = [[UIImage alloc] initWithData:data]; //my second try
//UIImage* image = [UIImage imageWithContentsOfFile:path]; //first try
//return image;
return data;
[data release];
}
And I am now loading the image like this, to see if it would, for some crazy reason, solve my problem (it didn't, obviously):
UIImage* theImage = [[UIImage alloc] initWithData:[self loadImage]];
I have done some testing where I have two UIImageViews side-by-side, one on the left that doesn't save and load the image before display (it just straight-up shows the image once a photo is picked from the ImagePicker), and one on the right that displays AFTER the save load occurs. The one on the right is the only one rotating 90 degrees CCW; the non Save/Load picture is displaying correctly. This only occurs on the actual iPhone. The simulator shows both images correctly. I have spent many, many hours attempting to figure it out, but to no avail. Any ideas as to what could be causing this?
EDIT: It should also be known that the image always says it's oriented up, even when it obviously isn't! It only rotates after ALL of the code has been run. I've tried doing a force redraw in between my code to see if that changes things, but it doesn't (though it is possible I'm even doing that wrong. Who knows, anymore).
Also, it will not autorotate photos that have been screenshot from the iPhone. Only photos that have been taken by the camera. [I haven't tried downloaded photos.] It's the weirdest thing...
And if you're wondering exactly how I'm pulling my picture, here's the method that will at least shed light on what I'm doing (it's not every method needed to do this function, obviously, but it gives insight on how I've written my code):
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
theimageView.image = [info objectForKey:#"UIImagePickerControllerOriginalImage"];
[self saveImage:[info objectForKey:#"UIImagePickerControllerOriginalImage"]];
[picker dismissModalViewControllerAnimated:YES];
}
Any advice is greatly appreciated. And I do know this much: if there's a way to do something crazy to get some stupid, never-before-heard-of problem, chances are, I'll find it, apparently :P
Ok. I figured it out. Apparently when you save things with PNG representation, and not JPEG representation, the image orientation information is not saved with it. Because of this, every image loaded will default to showing orientation up. So, the easiest way is to do this:
NSData* data = UIImageJPEGRepresentation(image,0.0);
[data writeToFile:path atomically:YES];
instead of this:
[UIImagePNGRepresentation(image) writeToFile:path atomically:YES];
And, of course, changing all the picture names from .png to .jpg. The 0.0 part of the code above is to control Alpha levels, which is required of jpegs. Hope this helps people in the future!
I assume you're using the Assets Library Framework to get the image. If so, you'll want to get the orientation of the image in your results block, and manipulate the UIImage to adjust. I have something like the following:
ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset)
{
ALAssetRepresentation* rep = [asset defaultRepresentation];
_orientation = rep.orientation;
...
}
Aside: What a great type name, huh? Holy moly, does Objective-C need namespaces!
Thanks so much...
This code has worked and saved several hours of work for me.
NSData* data = UIImageJPEGRepresentation(image,0.0);
[data writeToFile:path atomically:YES];
in the place of
[UIImagePNGRepresentation(image) writeToFile:path atomically:YES];
We are caching images downloaded from our server. We get the data from an ASIHTTPRequest callback like this:
#pragma mark ASIHTTPRequest callback
-(void)imageDownloadFinished:(ASIHTTPRequest*)aRequest
{
NSString* fileName = aRequest.url.path.lastPathComponent;
[self imageDidArrive:[aRequest responseData] forFileName:fileName];
}
We write the image data to our local storage like this:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0] ;
NSString* folder = [[documentsDirectory stringByAppendingPathComponent:#"flook.images"] retain];
NSString* fileName = [folder stringByAppendingFormat:#"/%#", aBaseFilename];
BOOL writeSuccess = [anImageData writeToFile:fileName atomically:NO];
The downloaded images are always the expected size, around 45-85KB.
Later, we read images from our cache like this:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0] ;
NSString* folder = [[documentsDirectory stringByAppendingPathComponent:#"flook.images"] retain];
NSString* fileName = [folder stringByAppendingFormat:#"/%#", aBaseFilename];
image = [UIImage imageWithContentsOfFile:fileName];
Occasionally, the images returned from this cache read are much smaller because they are much more compressed - around 5-10KB. Has the OS done this to us?
Edit - it turns out that we are downloading the small images, so the issue isn't on the iPhone
If I'm reading your code correctly, you're using the NSData method writeToFile:atomically: to write to the file. That does an exact byte-for-byte write of the contents of the NSData object.
It appears that the NSData object is created directly from the contents of the HTTP response, so the answer is "no", there should not be any compression taking place.
We have the solution. When the phone is running on the 3G network, O2 kindly steps in and applies extra JPG compression to our images, so that they look extra horrible.
See this post on the UK 3G forum.
how do i get size of folder NSCachesDirectory i.e /Library/Cache. i want to know size of this folder so that i can eventually clear this.
thanks.
Edit: here is my code.
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:folderPath error:&error];
if (attributes != nil) {
if (fileSize = [attributes objectForKey:NSFileSize]) {
NSLog(#"size of :%# = %qi\n",folderPath, [fileSize unsignedLongLongValue]);
}
}
when i run this it gives my file size 768(dont know bytes or KB) and i check in finder it shows me folder size 168KB. i dont know whats wrong.
Something like the following should help get you started:
- (unsigned long long int) cacheFolderSize {
NSFileManager *_manager = [NSFileManager defaultManager];
NSArray *_cachePaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *_cacheDirectory = [_cachePaths objectAtIndex:0];
NSArray *_cacheFileList;
NSEnumerator *_cacheEnumerator;
NSString *_cacheFilePath;
unsigned long long int _cacheFolderSize = 0;
_cacheFileList = [_manager subpathsAtPath:_cacheDirectory];
_cacheEnumerator = [_cacheFileList objectEnumerator];
while (_cacheFilePath = [_cacheEnumerator nextObject]) {
NSDictionary *_cacheFileAttributes = [_manager fileAttributesAtPath:[_cacheDirectory stringByAppendingPathComponent:_cacheFilePath] traverseLink:YES];
_cacheFolderSize += [_cacheFileAttributes fileSize];
}
return _cacheFolderSize;
}
EDIT
The value returned will be in bytes: cf. http://developer.apple.com/mac/library/documentation/Cocoa/Reference/Foundation/Classes/NSFileManager_Class/Reference/Reference.html#//apple_ref/doc/c_ref/NSFileSize
Assuming you are running this in the Simulator, Finder is probably reporting usage of file blocks for those bytes. Those blocks will necessarily be larger than the file data itself. Read up on the HFS+ system to learn about blocks: http://en.wikipedia.org/wiki/HFS_Plus
I'm not sure what file system is used on the iPhone, or what the file block size will be on the device, so while the byte total will be the same, the actual disk usage may be different between Simulator and device.
Do you really mean /Library/Cache, or do you mean ~/Library/Cache (the application's cache directory). You generally have no control over the former, so I'll assume you mean the latter.
Use NSFileManager's -enumeratorAtPath: to walk the directory and use -attributesOfItemAtPath:error: to fetch the fileSize. I recommend doing this slowly on a background thread to avoid blocking your app.