Files
IJSVG/source/IJSVG.m
T
2017-03-30 09:55:22 +01:00

739 lines
20 KiB
Objective-C

//
// IJSVGImage.m
// IconJar
//
// Created by Curtis Hard on 30/08/2014.
// Copyright (c) 2014 Curtis Hard. All rights reserved.
//
#import "IJSVG.h"
#import "IJSVGCache.h"
#import "IJSVGTransaction.h"
@implementation IJSVG
@synthesize fillColor;
@synthesize strokeColor;
@synthesize renderingBackingScaleHelper;
- (void)dealloc
{
[renderingBackingScaleHelper release], renderingBackingScaleHelper = nil;
[fillColor release], fillColor = nil;
[strokeColor release], strokeColor = nil;
[_group release], _group = nil;
[_layerTree release], _layerTree = nil;
[super dealloc];
}
+ (id)svgNamed:(NSString *)string
error:(NSError **)error
{
return [[self class] svgNamed:string
error:error
delegate:nil];
}
+ (id)svgNamed:(NSString *)string
{
return [[self class] svgNamed:string
error:nil];
}
+ (id)svgNamed:(NSString *)string
useCache:(BOOL)useCache
delegate:(id<IJSVGDelegate>)delegate
{
return [[self class] svgNamed:string
useCache:useCache
error:nil
delegate:delegate];
}
+ (id)svgNamed:(NSString *)string
delegate:(id<IJSVGDelegate>)delegate
{
return [[self class] svgNamed:string
error:nil
delegate:delegate];
}
+ (id)svgNamed:(NSString *)string
error:(NSError **)error
delegate:(id<IJSVGDelegate>)delegate
{
return [self svgNamed:string
useCache:YES
error:error
delegate:delegate];
}
+ (id)svgNamed:(NSString *)string
useCache:(BOOL)useCache
error:(NSError **)error
delegate:(id<IJSVGDelegate>)delegate
{
NSBundle * bundle = [NSBundle mainBundle];
NSString * str = nil;
NSString * ext = [string pathExtension];
if( ext == nil || ext.length == 0 ) {
ext = @"svg";
}
if( ( str = [bundle pathForResource:[string stringByDeletingPathExtension] ofType:ext] ) != nil ) {
return [[[self alloc] initWithFile:str
useCache:useCache
error:error
delegate:delegate] autorelease];
}
return nil;
}
- (id)initWithImage:(NSImage *)image
{
__block IJSVGGroupLayer * layer = nil;
__block IJSVGImageLayer * imageLayer = nil;
// make sure we obtain a lock, with whatever we do with layers!
IJSVGObtainTransactionLock(^{
// create the layers we require
layer = [[[IJSVGGroupLayer alloc] init] autorelease];
imageLayer = [[[IJSVGImageLayer alloc] initWithImage:image] autorelease];
[layer addSublayer:imageLayer];
}, NO);
// return the initialized SVG
return [self initWithSVGLayer:layer
viewBox:imageLayer.frame];
}
- (id)initWithSVGLayer:(IJSVGGroupLayer *)group
viewBox:(NSRect)viewBox
{
// this completely bypasses passing of files
if((self = [super init]) != nil) {
// keep the layer tree
_layerTree = [group retain];
_viewBox = viewBox;
// any setups
[self _setupBasicsFromAnyInitializer];
}
return self;
}
- (id)initWithFile:(NSString *)file
{
return [self initWithFile:file
delegate:nil];
}
- (id)initWithFile:(NSString *)file
useCache:(BOOL)useCache
{
return [self initWithFile:file
useCache:useCache
error:nil
delegate:nil];
}
- (id)initWithFile:(NSString *)file
useCache:(BOOL)useCache
error:(NSError **)error
delegate:(id<IJSVGDelegate>)delegate
{
return [self initWithFilePathURL:[NSURL fileURLWithPath:file]
useCache:useCache
error:error
delegate:delegate];
}
- (id)initWithFile:(NSString *)file
error:(NSError **)error
{
return [self initWithFile:file
error:error
delegate:nil];
}
- (id)initWithFile:(NSString *)file
delegate:(id<IJSVGDelegate>)delegate
{
return [self initWithFile:file
error:nil
delegate:delegate];
}
- (id)initWithFile:(NSString *)file
error:(NSError **)error
delegate:(id<IJSVGDelegate>)delegate
{
return [self initWithFilePathURL:[NSURL fileURLWithPath:file]
useCache:YES
error:error
delegate:delegate];
}
- (id)initWithFilePathURL:(NSURL *)aURL
{
return [self initWithFilePathURL:aURL
useCache:YES
error:nil
delegate:nil];
}
- (id)initWithFilePathURL:(NSURL *)aURL
error:(NSError **)error
{
return [self initWithFilePathURL:aURL
useCache:YES
error:error
delegate:nil];
}
- (id)initWithFilePathURL:(NSURL *)aURL
useCache:(BOOL)useCache
{
return [self initWithFilePathURL:aURL
useCache:useCache
error:nil
delegate:nil];
}
- (id)initWithFilePathURL:(NSURL *)aURL
delegate:(id<IJSVGDelegate>)delegate
{
return [self initWithFilePathURL:aURL
useCache:YES
error:nil
delegate:delegate];
}
- (id)initWithFilePathURL:(NSURL *)aURL
useCache:(BOOL)useCache
error:(NSError **)error
delegate:(id<IJSVGDelegate>)delegate
{
#ifndef __clang_analyzer__
// check the cache first
if( useCache && [IJSVGCache enabled] ) {
IJSVG * svg = nil;
if( ( svg = [IJSVGCache cachedSVGForFileURL:aURL] ) != nil ) {
// have to release, as this was called from an alloc..!
[self release];
return [svg retain];
}
}
// create the object
if( ( self = [super init] ) != nil ) {
NSError * anError = nil;
_delegate = delegate;
// this is a really quick check against the delegate
// for methods that exist
[self _checkDelegate];
// create the group
_group = [[IJSVGParser groupForFileURL:aURL
error:&anError
delegate:self] retain];
[self _setupBasicInfoFromGroup];
[self _setupBasicsFromAnyInitializer];
// something went wrong...
if( _group == nil ) {
if( error != NULL ) {
*error = anError;
}
[self release], self = nil;
return nil;
}
// cache the file
if( useCache && [IJSVGCache enabled] ) {
[IJSVGCache cacheSVG:self
fileURL:aURL];
}
}
#endif
return self;
}
- (id)initWithSVGString:(NSString *)string
{
return [self initWithSVGString:string
error:nil
delegate:nil];
}
- (id)initWithSVGString:(NSString *)string
error:(NSError **)error
{
return [self initWithSVGString:string
error:error
delegate:nil];
}
- (id)initWithSVGString:(NSString *)string
error:(NSError **)error
delegate:(id<IJSVGDelegate>)delegate
{
if((self = [super init]) != nil) {
// this is basically the same as init with URL just
// bypasses the loading of a file
NSError * anError = nil;
_delegate = delegate;
[self _checkDelegate];
// setup the parser
_group = [[IJSVGParser alloc] initWithSVGString:string
error:&anError
delegate:self];
[self _setupBasicInfoFromGroup];
[self _setupBasicsFromAnyInitializer];
// something went wrong :(
if(_group == nil) {
if(error != NULL) {
*error = anError;
}
[self release], self = nil;
return nil;
}
}
return self;
}
- (void)discardDOM
{
// if we discard, we can no longer create a tree, so lets create tree
// upfront before we kill anything
[self layer];
// now clear memory
[_group release], _group = nil;
}
- (void)_setupBasicInfoFromGroup
{
// store the viewbox
_viewBox = _group.viewBox;
_proposedViewSize = _group.proposedViewSize;
}
- (void)_setupBasicsFromAnyInitializer
{
_lastProposedBackingScale = 1.f;
}
- (NSString *)identifier
{
return _group.identifier;
}
- (void)_checkDelegate
{
_respondsTo.shouldHandleForeignObject = [_delegate respondsToSelector:@selector(svg:shouldHandleForeignObject:)];
_respondsTo.handleForeignObject = [_delegate respondsToSelector:@selector(svg:handleForeignObject:document:)];
_respondsTo.shouldHandleSubSVG = [_delegate respondsToSelector:@selector(svg:foundSubSVG:withSVGString:)];
}
- (NSRect)viewBox
{
return _viewBox;
}
- (BOOL)isFont
{
return [_group isFont];
}
- (NSArray *)glyphs
{
return [_group glyphs];
}
- (NSArray<IJSVG *> *)subSVGs:(BOOL)recursive
{
return [_group subSVGs:recursive];
}
- (NSImage *)imageWithSize:(NSSize)aSize
{
return [self imageWithSize:aSize
flipped:NO
error:nil];
}
- (NSImage *)imageWithSize:(NSSize)aSize
error:(NSError **)error;
{
return [self imageWithSize:aSize
flipped:NO
error:error];
}
- (NSImage *)imageWithSize:(NSSize)aSize
flipped:(BOOL)flipped
{
return [self imageWithSize:aSize
flipped:flipped
error:nil];
}
- (NSImage *)imageWithSize:(NSSize)aSize
flipped:(BOOL)flipped
error:(NSError **)error
{
NSImage * im = [[[NSImage alloc] initWithSize:aSize] autorelease];
[im lockFocus];
CGContextRef ref = [[NSGraphicsContext currentContext] graphicsPort];
CGContextSaveGState(ref);
if(flipped) {
CGContextTranslateCTM(ref, 0.f, aSize.height);
CGContextScaleCTM(ref, 1.f, -1.f);
}
[self drawAtPoint:NSMakePoint( 0.f, 0.f )
size:aSize
error:error];
CGContextRestoreGState(ref);
[im unlockFocus];
return im;
}
- (NSData *)PDFData
{
return [self PDFData:nil];
}
- (NSData *)PDFData:(NSError **)error
{
return [self PDFDataWithRect:(NSRect){
.origin=NSZeroPoint,
.size=_viewBox.size
} error:error];
}
- (NSData *)PDFDataWithRect:(NSRect)rect
{
return [self PDFDataWithRect:rect
error:nil];
}
- (NSData *)PDFDataWithRect:(NSRect)rect
error:(NSError **)error
{
// create the data for the PDF
NSMutableData * data = [[[NSMutableData alloc] init] autorelease];
// assign the data to the consumer
CGDataConsumerRef dataConsumer = CGDataConsumerCreateWithCFData((CFMutableDataRef)data);
const CGRect box = CGRectMake( rect.origin.x, rect.origin.y,
rect.size.width, rect.size.height );
// create the context
CGContextRef context = CGPDFContextCreate( dataConsumer, &box, NULL );
CGContextBeginPage( context, &box );
// the context is currently upside down, doh! flip it...
CGContextScaleCTM( context, 1, -1 );
CGContextTranslateCTM( context, 0, -box.size.height);
// draw the icon
[self _drawInRect:(NSRect)box
context:context
error:error];
CGContextEndPage(context);
//clean up
CGPDFContextClose(context);
CGContextRelease(context);
CGDataConsumerRelease(dataConsumer);
return data;
}
- (BOOL)drawAtPoint:(NSPoint)point
size:(NSSize)aSize
{
return [self drawAtPoint:point
size:aSize
error:nil];
}
- (BOOL)drawAtPoint:(NSPoint)point
size:(NSSize)aSize
error:(NSError **)error
{
return [self drawInRect:NSMakeRect( point.x, point.y, aSize.width, aSize.height )
error:error];
}
- (BOOL)drawInRect:(NSRect)rect
{
return [self drawInRect:rect
error:nil];
}
- (BOOL)drawInRect:(NSRect)rect
error:(NSError **)error
{
return [self _drawInRect:rect
context:[[NSGraphicsContext currentContext] graphicsPort]
error:error];
}
- (CGFloat)computeBackingScale:(CGFloat)actualScale
{
return (CGFloat)(_scale + actualScale);
}
- (NSRect)computeRectDrawingInRect:(NSRect)rect
isValid:(BOOL *)valid
{
// we also need to calculate the viewport so we can clip
// the drawing if needed
NSRect viewPort = NSZeroRect;
viewPort.origin.x = round(rect.size.width/2-(_proposedViewSize.width/2)*_clipScale);
viewPort.origin.y = round(rect.size.height/2-(_proposedViewSize.height/2)*_clipScale);;
viewPort.size.width = _proposedViewSize.width*_clipScale;
viewPort.size.height = _proposedViewSize.height*_clipScale;
// check the viewport
if( NSEqualRects( _viewBox, NSZeroRect )
|| _viewBox.size.width <= 0
|| _viewBox.size.height <= 0
|| NSEqualRects( NSZeroRect, viewPort)
|| CGRectIsEmpty(viewPort)
|| CGRectIsNull(viewPort)
|| viewPort.size.width <= 0
|| viewPort.size.height <= 0 )
{
*valid = NO;
return NSZeroRect;
}
*valid = YES;
return viewPort;
}
- (BOOL)_drawInRect:(NSRect)rect
context:(CGContextRef)ref
error:(NSError **)error
{
// prep for draw...
@synchronized (self) {
CGContextSaveGState(ref);
@try {
[self _beginDraw:rect];
// scale the whole drawing context, but first, we need
// to translate the context so its centered
CGFloat tX = round(rect.size.width/2-(_viewBox.size.width/2)*_scale);
CGFloat tY = round(rect.size.height/2-(_viewBox.size.height/2)*_scale);
// we also need to calculate the viewport so we can clip
// the drawing if needed
BOOL canDraw = NO;
NSRect viewPort = [self computeRectDrawingInRect:rect
isValid:&canDraw];
// check the viewport
if( !canDraw ) {
if( error != NULL ) {
*error = [[[NSError alloc] initWithDomain:IJSVGErrorDomain
code:IJSVGErrorDrawing
userInfo:nil] autorelease];
}
CGContextRestoreGState(ref);
return NO;
}
// clip to mask
CGContextClipToRect( ref, viewPort);
tX -= (_viewBox.origin.x*_scale);
tY -= (_viewBox.origin.y*_scale);
CGContextTranslateCTM( ref, tX, tY );
CGContextScaleCTM( ref, _scale, _scale );
// render the layer, its really important we lock
// the transaction when drawing
__block IJSVG * weakSelf = self;
IJSVGBeginTransactionLock();
// do we need to update the backing scales on the
// layers?
if(weakSelf.renderingBackingScaleHelper != nil) {
[weakSelf _askHelperForBackingScale];
}
// render the layers
[weakSelf.layer renderInContext:ref];
IJSVGEndTransactionLock();
}
@catch (NSException *exception) {
// just catch and give back a drawing error to the caller
if( error != NULL )
*error = [[[NSError alloc] initWithDomain:IJSVGErrorDomain
code:IJSVGErrorDrawing
userInfo:nil] autorelease];
}
@finally {
CGContextRestoreGState(ref);
}
}
return (error == nil);
}
- (void)_askHelperForBackingScale
{
CGFloat scale = (self.renderingBackingScaleHelper)();
if(scale < 1.f) {
scale = 1.f;
}
// dont do anything, nothing has changed, no point of iterating over
// every layer for no reason!
if(scale == _lastProposedBackingScale) {
return;
}
_lastProposedBackingScale = scale;
[self _recursivelySetScale:scale
forLayer:self.layer];
}
- (void)_recursivelySetScale:(CGFloat)scale
forLayer:(IJSVGLayer *)layer
{
// update the backing layer scale
if(layer.requiresBackingScaleHelp == YES) {
layer.backingScaleFactor = scale;
}
// find the next!
for(IJSVGLayer * aLayer in layer.sublayers) {
[self _recursivelySetScale:scale
forLayer:aLayer];
}
}
- (void)setFillColor:(NSColor *)aColor
{
if(fillColor != nil) {
[fillColor release], fillColor = nil;
}
fillColor = [aColor retain];
[_layerTree release], _layerTree = nil;
}
- (void)setStrokeColor:(NSColor *)aColor
{
if(strokeColor != nil) {
[strokeColor release], strokeColor = nil;
}
strokeColor = [aColor retain];
[_layerTree release], _layerTree = nil;
}
- (IJSVGLayer *)layerWithTree:(IJSVGLayerTree *)tree
{
// clear memory
if(_layerTree != nil) {
[_layerTree release], _layerTree = nil;
}
// force rebuild of the tree
IJSVGBeginTransactionLock();
_layerTree = [[tree layerForNode:_group] retain];
IJSVGEndTransactionLock();
return _layerTree;
}
- (IJSVGLayer *)layer
{
if(_layerTree != nil) {
return _layerTree;
}
// create the renderer and assign default values
// from this SVG object
IJSVGLayerTree * renderer = [[[IJSVGLayerTree alloc] init] autorelease];
renderer.viewBox = self.viewBox;
renderer.fillColor = self.fillColor;
renderer.strokeColor = self.strokeColor;
// return the rendered layer
return [self layerWithTree:renderer];
}
- (void)_beginDraw:(NSRect)rect
{
// in order to correctly fit the the SVG into the
// rect, we need to work out the ratio scale in order
// to transform the paths into our viewbox
NSSize dest = rect.size;
NSSize source = _viewBox.size;
_clipScale = MIN(dest.width/_proposedViewSize.width,
dest.height/_proposedViewSize.height);
// work out the actual scale based on the clip scale
CGFloat w = _proposedViewSize.width*_clipScale;
CGFloat h = _proposedViewSize.height*_clipScale;
_scale = MIN(w/source.width,h/source.height);
}
#pragma mark NSPasteboard
- (NSArray *)writableTypesForPasteboard:(NSPasteboard *)pasteboard
{
return @[NSPasteboardTypePDF];
}
- (id)pasteboardPropertyListForType:(NSString *)type
{
if( [type isEqualToString:NSPasteboardTypePDF] )
return [self PDFData];
return nil;
}
#pragma mark IJSVGParserDelegate
- (void)svgParser:(IJSVGParser *)svg
foundSubSVG:(IJSVG *)subSVG
withSVGString:(NSString *)string
{
if(_delegate != nil && _respondsTo.shouldHandleSubSVG == 1) {
[_delegate svg:self
foundSubSVG:subSVG
withSVGString:string];
}
}
- (BOOL)svgParser:(IJSVGParser *)parser
shouldHandleForeignObject:(IJSVGForeignObject *)foreignObject
{
if( _delegate != nil && _respondsTo.shouldHandleForeignObject == 1 ) {
return [_delegate svg:self shouldHandleForeignObject:foreignObject];
}
return NO;
}
- (void)svgParser:(IJSVGParser *)parser
handleForeignObject:(IJSVGForeignObject *)foreignObject
document:(NSXMLDocument *)document
{
if( _delegate != nil && _respondsTo.handleForeignObject == 1 ) {
[_delegate svg:self
handleForeignObject:foreignObject
document:document];
}
}
@end