FunHouseDocument.m
/* |
File: FunHouseDocument.m |
Abstract: n/a |
Version: 2.1 |
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
*/ |
#import "FunHouseDocument.h" |
#import "EffectStack.h" |
#import "FunHouseWindowController.h" |
#import "ParameterView.h" |
#import "CoreImageView.h" |
#import "EffectStackController.h" |
#import <ApplicationServices/ApplicationServices.h> |
#import <Carbon/Carbon.h> |
#import "FunHouseApplication.h" |
@implementation FunHouseDocument |
// |
// fun house document code - we're a subclass of NSDocument |
// |
// this code uses NSFileWrapper to encode the fun house preset |
// check out the NSFileWrapper class - it allows you to support packages or regular files |
// to do so, you need to encode your data files as NSData first |
// this NSDocument subclass also uses ImageIO to encode and decode the JPEG and TIFF files to and from NSData. |
// |
// set this to 1 to disable save |
// to complete this, you must disconnext the save and save as menu items in MainMenu.nib |
#define SAVEDISABLED 0 |
- (id)init |
{ |
self = [super init]; |
if (self) |
{ |
// allocate the effect stack for a document here |
effectStack = [[EffectStack alloc] init]; |
fullScreen = NO; |
hasWindowDimensions = NO; |
} |
return self; |
} |
- (void)dealloc |
{ |
[[NSNotificationCenter defaultCenter] removeObserver:self]; |
// release the effect stack for the document here |
[effectStack release]; |
if (colorspace) |
CFRelease(colorspace); |
[super dealloc]; |
} |
// this sets up our (non-full-screen) window controller |
- (void)makeWindowControllers |
{ |
// create the window controller |
windowController = [[FunHouseWindowController allocWithZone:[self zone]] init]; |
[self addWindowController:windowController]; |
[windowController release]; |
} |
+ (NSArray *)writableTypes |
{ |
return [NSArray arrayWithObjects:@"Fun House Preset", @"JPEG File", @"TIFF File", nil]; |
} |
+ (BOOL)isNativeType:(NSString *)aType |
{ |
return [[[self class] writableTypes] containsObject:aType]; |
} |
- (BOOL)prepareSavePanel:(NSSavePanel *)sp |
{ |
// assign defaults for the save panel |
[sp setTitle:@"Save image"]; |
[sp setExtensionHidden:NO]; |
return YES; |
} |
// add '-1' to filename before extension |
- (NSString *)disambiguateFilename:(NSString *)filename |
{ |
NSString *base, *extension; |
base = [filename stringByDeletingPathExtension]; |
extension = [filename pathExtension]; |
return [[base stringByAppendingString:@"-1"] stringByAppendingPathExtension:extension]; |
} |
#if SAVEDISABLED |
// we implement this method in the NSDocument subclass specifically so we can disable the save and save as commands |
- (BOOL)isDocumentEdited |
{ |
// this is all that's required to make a document think it doesn't have to be saved |
return NO; |
} |
#endif |
// create JPEG data for a document, using ImageIO |
- (NSData *)jpegData:(NSError **)outError |
{ |
NSData *d; |
NSRect r; |
CIContext *context; |
// save the image |
// create a mutable data to store the JPEG data into |
CFMutableDataRef data = CFDataCreateMutable(kCFAllocatorDefault, 0); |
// create an image destination (ImageIO's way of saying we want to save to a file format) |
// note: public.jpeg denotes that we are saving to JPEG |
CGImageDestinationRef ref = CGImageDestinationCreateWithData(data, (CFStringRef)@"public.jpeg", 1, NULL); |
if (ref == NULL) |
{ |
printf("problems creating image destination\n"); |
if(data) |
CFRelease(data); |
if(ref) |
CFRelease(ref); |
if (outError) |
*outError = [NSError errorWithDomain:@"fun house errors" code:-10101 |
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"problems creating image destination for file save", NSLocalizedDescriptionKey, nil]]; |
return nil; |
} |
// get the window controller for this document |
FunHouseWindowController *con = [[self windowControllers] objectAtIndex:0]; |
// compute the rectangle that the file fits into - we use the bounds of the view in many cases |
if ([[con coreImageView] isScaled]) |
{ |
if ([effectStack layerCount] < 1 || ![[effectStack typeAtIndex:0] isEqualToString:@"image"]) |
// if there's no image (generator used instead or nothing is in the effect stack) we use the view bounds |
r = [[con coreImageView] bounds]; |
else |
{ |
// for the scaled image case, we use the actual image extents (too big to fit on screen) |
CGRect extent = [[effectStack imageAtIndex:0] extent]; |
r = NSMakeRect(extent.origin.x, extent.origin.y, extent.size.width, extent.size.height); |
} |
} |
else |
// image is not scaled by a view transform, use the view bounds |
r = [[con coreImageView] bounds]; |
// we make a CGImageRef for the view, based on the core image graph. Note: it gets rendered in createCGImage |
if (colorspace == nil) { |
CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); |
context = [CIContext contextWithCGContext:[[[[con coreImageView] window] graphicsContext] graphicsPort] |
options:[NSDictionary dictionaryWithObjectsAndKeys:(id)cs, kCIContextOutputColorSpace, nil]]; |
CGColorSpaceRelease(cs); |
} |
else { |
context = [CIContext contextWithCGContext:[[[[con coreImageView] window] graphicsContext] graphicsPort] |
options:[NSDictionary dictionaryWithObjectsAndKeys:(id)colorspace, kCIContextOutputColorSpace, nil]]; |
} |
CGImageRef iref = [context createCGImage:[effectStack coreImageResultForRect:r] fromRect:CGRectMake(r.origin.x, r.origin.y, r.size.width, r.size.height)]; |
// add image to the ImageIO destination (specify the image we want to save) |
CGImageDestinationAddImage(ref, iref, NULL); |
// finalize: this saves the image to the JPEG format as data |
if (!CGImageDestinationFinalize(ref)) |
{ |
printf("problems writing JPEG file\n"); |
CFRelease(data); |
CFRelease(ref); |
CGImageRelease(iref); |
if (outError) |
*outError = [NSError errorWithDomain:@"fun house errors" code:-10102 |
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"problems writing JPEG file for file save", NSLocalizedDescriptionKey, nil]]; |
return nil; |
} |
CFRelease(ref); |
CGImageRelease(iref); |
if (outError) |
*outError = nil; |
// return the data |
d = (NSData *)data; |
return [d autorelease]; |
} |
- (NSData *)tiffData:(NSError **)outError |
{ |
NSData *d; |
NSRect r; |
CIContext *context; |
// save the image |
// create a mutable data to store the TIFF data into |
CFMutableDataRef data = CFDataCreateMutable(kCFAllocatorDefault, 0); |
// create an image destination (ImageIO's way of saying we want to save to a file format) |
// note: public.tiff denotes that we are saving to TIFF |
CGImageDestinationRef ref = CGImageDestinationCreateWithData(data, (CFStringRef)@"public.tiff", 1, NULL); |
if (ref == NULL) |
{ |
printf("problems creating image destination\n"); |
CFRelease(data); |
if (outError) |
*outError = [NSError errorWithDomain:@"fun house errors" code:-10101 |
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"problems creating image destination for file save", NSLocalizedDescriptionKey, nil]]; |
return nil; |
} |
// get the window controller for this document |
FunHouseWindowController *con = [[self windowControllers] objectAtIndex:0]; |
// compute the rectangle that the file fits into - we use the bounds of the view in many cases |
if ([[con coreImageView] isScaled]) |
{ |
if ([effectStack layerCount] < 1 || ![[effectStack typeAtIndex:0] isEqualToString:@"image"]) |
// if there's no image (generator used instead or nothing is in the effect stack) we use the view bounds |
r = [[con coreImageView] bounds]; |
else |
{ |
// for the scaled image case, we use the actual image extents (too big to fit on screen) |
CGRect extent = [[effectStack imageAtIndex:0] extent]; |
r = NSMakeRect(extent.origin.x, extent.origin.y, extent.size.width, extent.size.height); |
} |
} |
else |
// image is not scaled by a view transform, use the view bounds |
r = [[con coreImageView] bounds]; |
// we make a CGImageRef for the view, based on the core image graph. Note: it gets rendered in createCGImage |
if (colorspace == nil) { |
CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); |
context = [CIContext contextWithCGContext:[[[[con coreImageView] window] graphicsContext] graphicsPort] |
options:[NSDictionary dictionaryWithObjectsAndKeys:(id)cs, kCIContextOutputColorSpace, nil]]; |
CGColorSpaceRelease(cs); |
} |
else { |
context = [CIContext contextWithCGContext:[[[[con coreImageView] window] graphicsContext] graphicsPort] |
options:[NSDictionary dictionaryWithObjectsAndKeys:(id)colorspace, kCIContextOutputColorSpace, nil]]; |
} |
CGImageRef iref = [context createCGImage:[effectStack coreImageResultForRect:r] fromRect:CGRectMake(r.origin.x, r.origin.y, r.size.width, r.size.height)]; |
// add image to the ImageIO destination (specify the image we want to save) |
CGImageDestinationAddImage(ref, iref, NULL); |
// finalize: this saves the image to the TIFF format as data |
if (!CGImageDestinationFinalize(ref)) |
{ |
printf("problems writing TIFF file\n"); |
if(data) |
CFRelease(data); |
CFRelease(ref); |
CGImageRelease(iref); |
if (outError) |
*outError = [NSError errorWithDomain:@"fun house errors" code:-10102 |
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"problems writing TIFF file for file save", NSLocalizedDescriptionKey, nil]]; |
return nil; |
} |
if(ref) |
CFRelease(ref); |
CGImageRelease(iref); |
if (outError) |
*outError = nil; |
// return the data |
d = (NSData *)data; |
return [d autorelease]; |
} |
// convert a CIImage to a CGImageRef - uses the context of the document's view |
- (CGImageRef)CIImageToCGImage:(CIImage *)im usingRect:(CGRect)r |
{ |
FunHouseWindowController *con = [[self windowControllers] objectAtIndex:0]; |
CGImageRef cgImage = [[[con coreImageView] context] createCGImage:im fromRect:r]; |
return (CGImageRef)[(id)cgImage autorelease];; |
} |
- (NSPrintOperation *)printOperationWithSettings:(NSDictionary *)printSettings error:(NSError **)outError |
{ |
FunHouseWindowController *con = [[self windowControllers] objectAtIndex:0]; |
return [NSPrintOperation printOperationWithView: [con coreImageView]]; |
} |
// save the document's effect stack as a preset bundle (package) |
// images are saved as actual files within the package with names ident1.tiff, ident2.tiff, etc. |
// the effect stack is saved as a text XML file, with the name file.xml |
// this means that core image data must be encoded as NSDictionary, NSArray, NSString and NSNumber items |
// anything that's not of that form (like CIVector, CIColor, etc.) must be re-encoded |
// this also has good code for converting CIImage to tiff file data |
- (NSFileWrapper *)fileWrapperForPreset:(NSError **)outError |
{ |
BOOL hasBackground; |
NSInteger i, count; |
NSString *type, *file, *key, *error, *name, *filename, *path; |
NSMutableDictionary *layerdict, *filedict, *values, *pathdict; |
NSMutableArray *layerarray; |
NSPoint offset; |
NSArray *inputKeys; |
NSDictionary *attr; |
NSEnumerator *enumerator, *e; |
id obj; |
CIFilter *filter; |
NSData *xmlData, *d; |
NSFileWrapper *fw; |
NSMutableDictionary *dict, *localdict; |
// create the preset bundle |
fw = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:[NSDictionary dictionary]]; |
count = [effectStack layerCount]; |
// first sort out all the image files used by this effect stack |
pathdict = [NSMutableDictionary dictionary]; |
// enumerate the document's effect stack layers |
for (i = 0; i < count; i++) |
{ |
type = [effectStack typeAtIndex:i]; |
if ([type isEqualToString:@"image"]) |
{ |
// image layer |
// get original image file path |
path = [effectStack imageFilePathAtIndex:i]; |
// make sure the file isn't being stored twice - we can reference the same file as many times as we want! |
if ([pathdict valueForKey:path] == nil) |
{ |
filename = [path lastPathComponent]; |
// make sure the last component is unique (there could be two files withg the same name from two directories) |
// we must assume they are different at this point |
e = [pathdict objectEnumerator]; |
while ((localdict = [e nextObject]) != nil) |
{ |
name = [localdict valueForKey:@"filename"]; |
if ([name isEqualToString:filename]) |
{ |
// handle collision |
filename = [self disambiguateFilename:filename]; |
// start over - check again! |
e = [pathdict objectEnumerator]; |
} |
} |
// set up the path to have a representation, by default its last component (e. g. tiger.jpg) |
[pathdict setValue: |
[NSMutableDictionary dictionaryWithObjectsAndKeys:filename, @"filename", |
[NSNumber numberWithBool:NO], @"storedToDisk", [effectStack imageFileDataAtIndex:i], @"data", nil] |
forKey:path]; |
} |
} |
else if ([type isEqualToString:@"filter"]) |
{ |
// filter layer |
filter = [effectStack filterAtIndex:i]; |
inputKeys = [filter inputKeys]; |
// decide if the filter has a background (it's a blend mode) |
enumerator = [inputKeys objectEnumerator]; |
hasBackground = NO; |
while ((key = [enumerator nextObject]) != nil) |
{ |
if ([key isEqualToString:@"inputBackgroundImage"]) |
{ |
hasBackground = YES; |
break; |
} |
} |
// now inspect all the input keys for the filter |
enumerator = [inputKeys objectEnumerator]; |
while ((key = [enumerator nextObject]) != nil) |
{ |
obj = [filter valueForKey:key]; |
if (obj != nil) |
{ |
if ([obj isKindOfClass:[CIImage class]]) |
{ |
// decide whether to skip this key (it gets filled in by something above it in the effect stack) |
if (hasBackground && [key isEqualToString:@"inputBackgroundImage"]) |
continue; // a blend mode; we chain on inputBackgroundImage |
if (!hasBackground && [key isEqualToString:@"inputImage"]) |
continue; // not a blend mode; we chain on inputImage |
// we have an image that fills in a filter image parameter |
// get its path |
path = [effectStack filterLayer:i imageFilePathValueForKey:key]; |
// make sure the file isn't being stored twice - we can reference the same file as many times as we want! |
if ([pathdict valueForKey:path] == nil) |
{ |
filename = [path lastPathComponent]; |
// make sure the last component is unique (there could be two files withg the same name from two directories) |
// we must assume they are different at this point |
e = [pathdict objectEnumerator]; |
while ((localdict = [e nextObject]) != nil) |
{ |
name = [localdict valueForKey:@"filename"]; |
if ([name isEqualToString:filename]) |
{ |
// handle collision |
filename = [self disambiguateFilename:filename]; |
// start over - check again! |
e = [pathdict objectEnumerator]; |
} |
} |
// set up the path to have a representation, by default its last component (e. g. tiger.jpg) |
[pathdict setValue: |
[NSMutableDictionary dictionaryWithObjectsAndKeys:filename, @"filename", |
[NSNumber numberWithBool:NO], @"storedToDisk", |
[effectStack filterLayer:i imageFileDataValueForKey:key], @"data", nil] |
forKey:path]; |
} |
} |
} |
} |
} |
} |
// make two structures: image file dictionary and layer array |
filedict = [NSMutableDictionary dictionaryWithCapacity:10]; |
layerarray = [NSMutableArray arrayWithCapacity:count]; |
[filedict setValue:layerarray forKey:@"layers"]; |
NSRect fr = [[windowController window] frame]; |
[filedict setValue:[NSNumber numberWithDouble:fr.size.width] forKey:@"windowWidth"]; |
[filedict setValue:[NSNumber numberWithDouble:fr.size.height] forKey:@"windowHeight"]; |
// enumerate the document's effect stack layers |
for (i = 0; i < count; i++) |
{ |
// create a dictionary for this layer and add it to the layer array |
layerdict = [NSMutableDictionary dictionaryWithCapacity:10]; |
[layerarray addObject:layerdict]; |
// dispatch on layer type |
type = [effectStack typeAtIndex:i]; |
if ([type isEqualToString:@"image"]) |
{ |
// image layer |
// fill dictionary for image layer |
[layerdict setValue:@"image" forKey:@"type"]; |
offset = [effectStack offsetAtIndex:i]; |
[layerdict setValue:[NSNumber numberWithDouble:offset.x] forKey:@"offsetX"]; |
[layerdict setValue:[NSNumber numberWithDouble:offset.y] forKey:@"offsetY"]; |
// create identifier for image |
path = [effectStack imageFilePathAtIndex:i]; |
// decide if this file has yet been output |
localdict = [pathdict valueForKey:path]; |
// get the filename within the bundle |
file = [localdict valueForKey:@"filename"]; |
// we only store it if it hasn't yet been stored |
if (![[localdict valueForKey:@"storedToDisk"] boolValue]) |
{ |
// mark it stored |
[localdict setValue:[NSNumber numberWithBool:YES] forKey:@"storedToDisk"]; |
// get original file as data |
//d = [NSData dataWithContentsOfFile:path]; |
d = [localdict valueForKey:@"data"]; |
// add the file to the file wrapper, given the NSData for the file |
[fw addRegularFileWithContents:d preferredFilename:file]; |
} |
// bind it to the layer dictionary |
[layerdict setValue:file forKey:@"file"]; |
} |
else if ([type isEqualToString:@"text"]) |
{ |
// text layer |
// fill dictionary for image layer |
[layerdict setValue:@"text" forKey:@"type"]; |
dict = [effectStack mutableDictionaryAtIndex:i]; |
[layerdict setValue:[dict valueForKey:@"string"] forKey:@"string"]; |
[layerdict setValue:[dict valueForKey:@"scale"] forKey:@"scale"]; |
[layerdict setValue:[dict valueForKey:@"offsetX"] forKey:@"offsetX"]; |
[layerdict setValue:[dict valueForKey:@"offsetY"] forKey:@"offsetY"]; |
[layerdict setValue:[dict valueForKey:@"font"] forKey:@"font"]; |
[layerdict setValue:[dict valueForKey:@"pointSize"] forKey:@"pointSize"]; |
[layerdict setValue:[dict valueForKey:@"colorRed"] forKey:@"colorRed"]; |
[layerdict setValue:[dict valueForKey:@"colorGreen"] forKey:@"colorGreen"]; |
[layerdict setValue:[dict valueForKey:@"colorBlue"] forKey:@"colorBlue"]; |
[layerdict setValue:[dict valueForKey:@"colorAlpha"] forKey:@"colorAlpha"]; |
[layerdict setValue:[dict valueForKey:@"shadowColorRed"] forKey:@"shadowColorRed"]; |
[layerdict setValue:[dict valueForKey:@"shadowColorGreen"] forKey:@"shadowColorGreen"]; |
[layerdict setValue:[dict valueForKey:@"shadowColorBlue"] forKey:@"shadowColorBlue"]; |
[layerdict setValue:[dict valueForKey:@"shadowColorAlpha"] forKey:@"shadowColorAlpha"]; |
[layerdict setValue:[dict valueForKey:@"shadowBlurRadius"] forKey:@"shadowBlurRadius"]; |
[layerdict setValue:[dict valueForKey:@"shadowBlurOffsetX"] forKey:@"shadowBlurOffsetX"]; |
[layerdict setValue:[dict valueForKey:@"shadowBlurOffsetY"] forKey:@"shadowBlurOffsetY"]; |
[layerdict setValue:[dict valueForKey:@"strikeThroughStyle"] forKey:@"strikeThroughStyle"]; |
[layerdict setValue:[dict valueForKey:@"underlineStyle"] forKey:@"underlineStyle"]; |
} |
else if ([type isEqualToString:@"filter"]) |
{ |
// filter layer |
// fill dictionary for filter layer |
filter = [effectStack filterAtIndex:i]; |
[layerdict setValue:@"filter" forKey:@"type"]; |
attr = [filter attributes]; |
inputKeys = [filter inputKeys]; |
// decide if the filter has a background (it's a blend mode) |
enumerator = [inputKeys objectEnumerator]; |
hasBackground = NO; |
while ((key = [enumerator nextObject]) != nil) |
{ |
if ([key isEqualToString:@"inputBackgroundImage"]) |
{ |
hasBackground = YES; |
break; |
} |
} |
[layerdict setValue:[attr valueForKey:kCIAttributeFilterName] forKey:@"classname"]; |
values = [NSMutableDictionary dictionaryWithCapacity:10]; |
[layerdict setValue:values forKey:@"values"]; |
// now inspect all the input keys for the filter |
enumerator = [inputKeys objectEnumerator]; |
while ((key = [enumerator nextObject]) != nil) |
{ |
obj = [filter valueForKey:key]; |
if (obj != nil) |
{ |
if ([obj isKindOfClass:[CIImage class]]) |
{ |
// decide whether to skip this key (it gets filled in by something above it in the effect stack) |
if (hasBackground && [key isEqualToString:@"inputBackgroundImage"]) |
continue; // a blend mode; we chain on inputBackgroundImage |
if (!hasBackground && [key isEqualToString:@"inputImage"]) |
continue; // not a blend mode; we chain on inputImage |
// we have an image that fills in a filter image parameter |
// create identifier for image |
path = [effectStack filterLayer:i imageFilePathValueForKey:key]; |
localdict = [pathdict valueForKey:path]; |
// get the filename within the bundle |
file = [localdict valueForKey:@"filename"]; |
// we only store it if it hasn't yet been stored |
if (![[localdict valueForKey:@"storedToDisk"] boolValue]) |
{ |
// mark it stored |
[localdict setValue:[NSNumber numberWithBool:YES] forKey:@"storedToDisk"]; |
// get original file as data |
// d = [NSData dataWithContentsOfFile:path]; |
d = [localdict valueForKey:@"data"]; |
// add the file to the file wrapper, given the NSData for the file |
[fw addRegularFileWithContents:d preferredFilename:file]; |
} |
// bind the filename within the bundle to the filter's key |
[values setValue:file forKey:key]; |
} |
else |
// if not an image, do a standard encoding into XML-compatible items |
[effectStack encodeValue:obj forKey:key intoDictionary:values]; |
} |
} |
} |
} |
// now create the top level XML file (as data, of course) |
xmlData = [NSPropertyListSerialization dataFromPropertyList:filedict format:NSPropertyListXMLFormat_v1_0 |
errorDescription:&error]; |
if (xmlData == nil) |
{ |
NSLog(@"%@", error); |
[error release]; |
if (outError) |
*outError = [NSError errorWithDomain:@"fun house errors" code:-10106 |
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"problems writing xml file for preset save", NSLocalizedDescriptionKey, nil]]; |
[fw release]; |
return nil; |
} |
// add the XML file (in NSData form) to the file wrapper |
[fw addRegularFileWithContents:xmlData preferredFilename:@"file.xml"]; |
if (outError) |
*outError = nil; |
// and the file wrapper's ready to store onto disk |
return [fw autorelease]; |
} |
// this is the high-level method that produces the NSFileWrapper for JPEG, TIFF, or fun house preset files |
- (NSFileWrapper *)fileWrapperOfType:(NSString *)typeName error:(NSError **)outError |
{ |
if ([typeName isEqualToString:@"JPEG File"]) |
return [[[NSFileWrapper alloc] initRegularFileWithContents:[self jpegData:outError]] autorelease]; |
if ([typeName isEqualToString:@"TIFF File"]) |
return [[[NSFileWrapper alloc] initRegularFileWithContents:[self tiffData:outError]] autorelease]; |
if ([typeName isEqualToString:@"Fun House Preset"]) |
return [self fileWrapperForPreset:outError]; |
if (outError) |
*outError = [NSError errorWithDomain:@"fun house errors" code:-10103 |
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"unrecognized file type for file save", NSLocalizedDescriptionKey, nil]]; |
return nil; |
} |
// this handles the read of a plain image file - it sets up the base image of the effect stack |
- (BOOL)readFromData:(NSData *)data ofType:(NSString *)aType error:(NSError **)outError |
{ |
CIImage *im; |
CGImageSourceRef ref; |
CGImageRef image; |
// use Core Image to convert the data - it uses the ImageIO library |
// get color space of image data |
if ([aType isEqualToString:@"OpenEXR File"]) |
ref = CGImageSourceCreateWithURL((CFURLRef)[self fileURL], nil); |
else |
ref = CGImageSourceCreateWithData((CFDataRef)data, nil); |
image = CGImageSourceCreateImageAtIndex(ref, 0, (CFDictionaryRef)[NSDictionary dictionaryWithObjectsAndKeys:(id)kCFBooleanTrue, |
(NSString *)kCGImageSourceShouldAllowFloat, nil]); |
colorspace = CGImageGetColorSpace(image); |
if(colorspace) |
CFRetain(colorspace); |
CFRelease(ref); |
if ([aType isEqualToString:@"OpenEXR File"]) |
im = [CIImage imageWithCGImage:image]; |
else |
im = [CIImage imageWithData:data]; |
if (image) |
CFRelease(image); |
[effectStack setBaseImage:im withFilename:[[[self fileURL] path] lastPathComponent] andImageFilePath:[[self fileURL] path]]; |
return YES; |
} |
// this is the high-level method required by the NSDocument subclass that reads in the document from an NSFileWrapper |
- (BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper ofType:(NSString *)typeName error:(NSError **)outError |
{ |
BOOL b; |
if ([fileWrapper isRegularFile]) |
b = [self readFromData:[fileWrapper regularFileContents] ofType:typeName error:outError]; |
else |
b = [self openPreset:fileWrapper error:outError]; |
[[self undoManager] removeAllActions]; |
return b; |
} |
// we implement this method in the subclass so that we can call removeAllActions (for undo) |
- (void)updateChangeCount:(NSDocumentChangeType)change |
{ |
// This clears the undo stack whenever we load or save. |
[super updateChangeCount:change]; |
if (change == NSChangeCleared) |
[[self undoManager] removeAllActions]; |
} |
// return this document's effect stack |
- (EffectStack *)effectStack |
{ |
return effectStack; |
} |
// this reads in a fun house preset from an NSFileWrapper |
- (BOOL)openPreset:(NSFileWrapper *)fileWrapper error:(NSError **)outError |
{ |
BOOL hasBackground; |
NSInteger i, count; |
NSDictionary *fw; |
NSData *xmldata; |
NSDictionary *filedict, *layerdict, *values, *attr, *parameter; |
NSArray *layers, *inputKeys; |
NSString *type, *file, *classname, *error, *key, *classstring, *path; |
NSPoint offset; |
NSNumber *num; |
CIImage *im; |
CIFilter *filter; |
NSPropertyListFormat format; |
NSEnumerator *enumerator; |
NSMutableDictionary *dict; |
// note: fileWrapper must be a dictionary by here |
fw = [fileWrapper fileWrappers]; |
// unpack dictionary |
xmldata = [[fw valueForKey:@"file.xml"] regularFileContents]; |
[effectStack removeAllLayers]; |
// read into internal data structure |
filedict = [NSPropertyListSerialization propertyListFromData:xmldata |
mutabilityOption:kCFPropertyListImmutable format:&format errorDescription:&error]; |
if (filedict == nil) |
{ |
*outError = [NSError errorWithDomain:@"fun house errors" code:-10105 |
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:error, NSLocalizedDescriptionKey, nil]]; |
[error release]; |
return NO; |
} |
// now unpack file dictionary |
num = [filedict valueForKey:@"windowWidth"]; |
if (num == nil) |
hasWindowDimensions = NO; |
else |
{ |
hasWindowDimensions = YES; |
wdWidth = [num doubleValue]; |
wdHeight = [[filedict valueForKey:@"windowHeight"] doubleValue]; |
} |
layers = [filedict valueForKey:@"layers"]; |
count = [layers count]; |
for (i = 0; i < count; i++) |
{ |
layerdict = [layers objectAtIndex:i]; |
type = [layerdict valueForKey:@"type"]; |
if ([type isEqualToString:@"image"]) |
{ |
// an image layer |
offset = NSMakePoint([[layerdict valueForKey:@"offsetX"] doubleValue], [[layerdict valueForKey:@"offsetY"] doubleValue]); |
file = [layerdict valueForKey:@"file"]; |
path = [[[[self fileURL] path] stringByAppendingString:@"/"] stringByAppendingString:file]; |
im = [[[CIImage alloc] initWithContentsOfURL:[NSURL fileURLWithPath:path]] autorelease]; |
// and insert the image layer into the effect stack |
[effectStack insertImageLayer:im withFilename:file atIndex:i]; |
[effectStack setImageLayer:i offset:offset]; |
[effectStack setImageLayer:i imageFilePath:path]; |
} |
else if ([type isEqualToString:@"text"]) |
{ |
// add the text layer to the effect stack |
[effectStack insertTextLayer:[layerdict valueForKey:@"string"] withImage:nil atIndex:i]; |
// rebuild the effect stack text layer's properties from the dictionary format saved to the XML file |
dict = [effectStack mutableDictionaryAtIndex:i]; |
[dict setValue:[layerdict valueForKey:@"offsetX"] forKey:@"offsetX"]; |
[dict setValue:[layerdict valueForKey:@"offsetY"] forKey:@"offsetY"]; |
[dict setValue:[layerdict valueForKey:@"scale"] forKey:@"scale"]; |
[dict setValue:[layerdict valueForKey:@"font"] forKey:@"font"]; |
[dict setValue:[layerdict valueForKey:@"pointSize"] forKey:@"pointSize"]; |
[dict setValue:[layerdict valueForKey:@"colorRed"] forKey:@"colorRed"]; |
[dict setValue:[layerdict valueForKey:@"colorGreen"] forKey:@"colorGreen"]; |
[dict setValue:[layerdict valueForKey:@"colorBlue"] forKey:@"colorBlue"]; |
[dict setValue:[layerdict valueForKey:@"colorAlpha"] forKey:@"colorAlpha"]; |
[dict setValue:[layerdict valueForKey:@"shadowColorRed"] forKey:@"shadowColorRed"]; |
[dict setValue:[layerdict valueForKey:@"shadowColorGreen"] forKey:@"shadowColorGreen"]; |
[dict setValue:[layerdict valueForKey:@"shadowColorBlue"] forKey:@"shadowColorBlue"]; |
[dict setValue:[layerdict valueForKey:@"shadowColorAlpha"] forKey:@"shadowColorAlpha"]; |
[dict setValue:[layerdict valueForKey:@"shadowBlurRadius"] forKey:@"shadowBlurRadius"]; |
[dict setValue:[layerdict valueForKey:@"shadowBlurOffsetX"] forKey:@"shadowBlurOffsetX"]; |
[dict setValue:[layerdict valueForKey:@"shadowBlurOffsetY"] forKey:@"shadowBlurOffsetY"]; |
[dict setValue:[layerdict valueForKey:@"strikeThroughStyle"] forKey:@"strikeThroughStyle"]; |
[dict setValue:[layerdict valueForKey:@"underlineStyle"] forKey:@"underlineStyle"]; |
} |
else if ([type isEqualToString:@"filter"]) |
{ |
// filter layer |
classname = [layerdict valueForKey:@"classname"]; |
values = [layerdict valueForKey:@"values"]; |
filter = [CIFilter filterWithName:classname]; |
// add the filter to the effect stack |
[effectStack insertFilterLayer:filter atIndex:i]; |
// get filter keys |
attr = [filter attributes]; |
inputKeys = [filter inputKeys]; |
// decide if the filter has a background (it's a blend mode) |
enumerator = [inputKeys objectEnumerator]; |
hasBackground = NO; |
while ((key = [enumerator nextObject]) != nil) |
{ |
if ([key isEqualToString:@"inputBackgroundImage"]) |
hasBackground = YES; |
} |
// now inspect all the input keys for the filter |
enumerator = [inputKeys objectEnumerator]; |
while ((key = [enumerator nextObject]) != nil) |
{ |
parameter = [attr objectForKey:key]; |
// decide whether to skip this key |
// we skip it if it's an image parameter that's provided by evaluation of the effect stack (a chained image from above) |
if (hasBackground && [key isEqualToString:@"inputBackgroundImage"]) |
continue; // a blend mode; we chain on inputBackgroundImage |
if (!hasBackground && [key isEqualToString:@"inputImage"]) |
continue; // not a blend mode; we chain on inputImage |
// otherwise try to unpack this key's value from values dictionary |
classstring = [parameter objectForKey:kCIAttributeClass]; |
if ([classstring isEqualToString:@"CIImage"]) |
{ |
// filter image parameter |
file = [values valueForKey:key]; |
path = [[[[self fileURL] path] stringByAppendingString:@"/"] stringByAppendingString:file]; |
im = [[[CIImage alloc] initWithContentsOfURL:[NSURL fileURLWithPath:path]] autorelease]; |
// and set up the filter's image value |
[filter setValue:im forKey:key]; |
[effectStack setFilterLayer:i imageFilePathValue:[[[[self fileURL] path] stringByAppendingString:@"/"] stringByAppendingString:file] |
forKey:key]; |
} |
else |
// set the filter's value (must decode complex parameters line CIVector, CIColor, etc.) |
[filter setValue:[effectStack decodedValueForKey:key ofClass:classstring fromDictionary:values] forKey:key]; |
} |
} |
} |
return YES; |
} |
// handle revert - basically do what NSDocument does, except that we have to update the effect stack to correspond |
- (IBAction)revertDocumentToSaved:(id)sender |
{ |
[super revertDocumentToSaved:sender]; |
// redo the effect stack to correspond |
[[EffectStackController sharedEffectStackController] updateLayout]; |
} |
// this handles the zoom to full screen menu item |
- (IBAction)zoomToFullScreenAction:(id)sender |
{ |
CoreImageView *v; |
if (fullScreen) |
{ |
// zoom away from full screen mode |
// put the menu bar and dock back |
[[NSApplication sharedApplication] setPresentationOptions:NSApplicationPresentationDefault]; |
// close the full screen window and its controller |
[fullScreenController close]; |
fullScreen = NO; |
// reset the core image view for the effect stack controller |
v = [windowController coreImageView]; |
[[EffectStackController sharedEffectStackController] setCoreImageView:v]; |
// and update the effect stack controller |
[[EffectStackController sharedEffectStackController] updateLayout]; |
// finally redisplay the window |
[v setNeedsDisplay:YES]; |
} |
else |
{ |
// zoom into full screen mode |
// add a new window controller, and thus a new window |
fullScreenController = [[FunHouseWindowController allocWithZone:[self zone]] initFullScreen]; |
[self addWindowController:fullScreenController]; |
// set the window up properly to be a full screen window (check out FunHouseWindowController.m) |
[fullScreenController prepFullScreenWindow]; |
v = [fullScreenController coreImageView]; |
// release it now that it's owned by the document |
[fullScreenController release]; |
fullScreen = YES; |
// point the effect stack controller to the right view in the right window |
[[EffectStackController sharedEffectStackController] setCoreImageView:v]; |
// update the effect stack controller |
[[EffectStackController sharedEffectStackController] updateLayout]; |
// recompute the view transform |
// note: the base image is zoomed to fit the full screen window |
if ([[effectStack typeAtIndex:0] isEqualToString:@"image"]) |
{ |
CGFloat scale, xscale, yscale, offsetX, offsetY; |
CGSize imagesize; |
NSSize screensize; |
// compute size of image |
imagesize = [[effectStack imageAtIndex:0] extent].size; |
// compute size of full screen window |
screensize = [[NSScreen mainScreen] frame].size; |
// decide scale factor now |
xscale = screensize.width / imagesize.width; |
yscale = screensize.height / imagesize.height; |
if (yscale < xscale) |
{ |
scale = yscale; |
offsetX = (screensize.width - imagesize.width * scale) * 0.5; |
offsetY = 0.0; |
} |
else |
{ |
scale = xscale; |
offsetX = 0.0; |
offsetY = (screensize.height - imagesize.height * scale) * 0.5; |
} |
// set the view transform for the core image view |
[v setViewTransformScale:scale]; |
[v setViewTransformOffsetX:offsetX andY:offsetY]; |
} |
// and call for a redisplay |
[v setNeedsDisplay:YES]; |
} |
// finally, set up the menu item to reflect the current state (actually, to reflect what the menu item will do) |
[((FunHouseApplication *)NSApp) setFullScreenMenuTitle:(BOOL)fullScreen]; |
} |
// handle the undo command |
- (void)undo |
{ |
CoreImageView *v; |
// undo (resets the object state) |
[[self undoManager] undo]; |
// get the right core image view pointer |
if (!fullScreen) |
v = [windowController coreImageView]; |
else |
v = [fullScreenController coreImageView]; |
// update the effect stack controller |
[[EffectStackController sharedEffectStackController] updateLayout]; |
// redisplay the core image view |
[v setNeedsDisplay:YES]; |
} |
// handle the redo command |
- (void)redo |
{ |
CoreImageView *v; |
// redo (sets the object state) |
[[self undoManager] redo]; |
// get the right core image view pointer |
if (!fullScreen) |
v = [windowController coreImageView]; |
else |
v = [fullScreenController coreImageView]; |
// update the effect stack controller |
[[EffectStackController sharedEffectStackController] updateLayout]; |
// redisplay the core image view |
[v setNeedsDisplay:YES]; |
} |
- (void)reconfigureWindowToSize:(NSSize)size andPath:(NSString *)path |
{ |
[self setFileURL:[NSURL fileURLWithPath:path]]; |
[windowController configureToSize:size andFilename:[path lastPathComponent]]; |
} |
- (BOOL)hasWindowDimensions |
{ |
return hasWindowDimensions; |
} |
- (CGFloat)windowWidth |
{ |
return wdWidth; |
} |
- (CGFloat)windowHeight |
{ |
return wdHeight; |
} |
@end |
Copyright © 2014 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2014-05-07