Let’s say you’ve created a CGPath
in your code, and you’d like to save it to disk. It wasn’t immediately obvious to me how one might do this, but it turns out to be pretty easy with the use of the CGPathApply
function.
CGPathApply
The CGPathApply
function invokes a user-supplied callback for each “element” of a specified path. These “elements” are CGPathElement
structures; each contains an operator/type (one of 5) and an array of CGPoints
that modify the operator. Each operator maps intuitively to a CGPath…
function, which makes reconstitution easy.
Demo
Here’s some code for you. This class wraps a CGPath
, and provides basic load/save functionality. It’s been, ah, “lightly” tested, so proceed with caution. Whatever the particulars, the general approach seems sound.
#import <Foundation/Foundation.h>
@interface Path : NSObject
{
CGPathRef path;
}
- (CGPathRef)path;
- (void)setPath:(CGPathRef)newPath;
- (BOOL)saveToFile:(NSString*)fileName;
- (BOOL)loadFromFile:(NSString*)fileName;
@end
#import "Path.h"
static void saveApplier(void* info, const CGPathElement* element)
{
NSMutableArray* a = (NSMutableArray*) info;
int nPoints;
switch (element->type)
{
case kCGPathElementMoveToPoint:
nPoints = 1;
break;
case kCGPathElementAddLineToPoint:
nPoints = 1;
break;
case kCGPathElementAddQuadCurveToPoint:
nPoints = 2;
break;
case kCGPathElementAddCurveToPoint:
nPoints = 3;
break;
case kCGPathElementCloseSubpath:
nPoints = 0;
break;
default:
[a replaceObjectAtIndex:0 withObject:[NSNumber numberWithBool:NO]];
return;
}
NSNumber* type = [NSNumber numberWithInt:element->type];
NSData* points = [NSData dataWithBytes:element->points length:nPoints*sizeof(CGPoint)];
[a addObject:[NSDictionary dictionaryWithObjectsAndKeys:type,@"type",points,@"points",nil]];
}
@implementation Path
- (CGPathRef)path
{
return path;
}
- (void)setPath:(CGPathRef)newPath
{
if (path != newPath)
{
CGPathRelease(path);
path = CGPathRetain(newPath);
}
}
- (BOOL)saveToFile:(NSString*)fileName
{
// Convert path to an array
NSMutableArray* a = [NSMutableArray arrayWithObject:[NSNumber numberWithBool:YES]];
CGPathApply(path, a, saveApplier);
if (![[a objectAtIndex:0] boolValue])
{
return NO;
}
// Write path
return [a writeToFile:fileName atomically:YES];
}
- (BOOL)loadFromFile:(NSString*)fileName
{
// Read path
NSArray* a = [NSArray arrayWithContentsOfFile:fileName];
if (!a) return NO;
// Recreate (and store) path
CGMutablePathRef p = CGPathCreateMutable();
for (NSInteger i = 1, l = [a count]; i < l; i++)
{
NSDictionary* d = [a objectAtIndex:i];
CGPoint* points = (CGPoint*) [[d objectForKey:@"points"] bytes];
switch ([[d objectForKey:@"type"] intValue])
{
case kCGPathElementMoveToPoint:
CGPathMoveToPoint(p, NULL, points[0].x, points[0].y);
break;
case kCGPathElementAddLineToPoint:
CGPathAddLineToPoint(p, NULL, points[0].x, points[0].y);
break;
case kCGPathElementAddQuadCurveToPoint:
CGPathAddQuadCurveToPoint(p, NULL, points[0].x, points[0].y, points[1].x, points[1].y);
break;
case kCGPathElementAddCurveToPoint:
CGPathAddCurveToPoint(p, NULL, points[0].x, points[0].y, points[1].x, points[1].y, points[2].x, points[2].y);
break;
case kCGPathElementCloseSubpath:
CGPathCloseSubpath(p);
break;
default:
CGPathRelease(p);
return NO;
}
}
self.path = p;
CGPathRelease(p);
// Signal success
return YES;
}
- (void)dealloc
{
CGPathRelease(path);
[super dealloc];
}
@end
Format
The thing that bothers me the most about this code is the disk format of the CGPoint
arrays. I chose to stuff them into NSData
objects for simplicity and speed of implementation, but this yields an opaque file format. It would be better to store the CGPoints
as, say, arrays of dictionaries, or any equivalent arrangement which would render them human-readable and -editable.