forked from SVGKit/SVGKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSVGKImage.m
More file actions
814 lines (632 loc) · 30.3 KB
/
Copy pathSVGKImage.m
File metadata and controls
814 lines (632 loc) · 30.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
#import "SVGKImage.h"
#import "SVGDefsElement.h"
#import "SVGDescriptionElement.h"
#import "SVGKParser.h"
#import "SVGTitleElement.h"
#import "SVGPathElement.h"
#import "SVGUseElement.h"
#import "SVGClipPathElement.h"
#import "SVGSwitchElement.h"
#import <DOMiOS/NodeList+Mutable.h>
#import "SVGSVGElement_Mutable.h" // so that changing .size can change the SVG's .viewport
#import "SVGKParserSVG.h"
#import "SVGKSourceLocalFile.h" // for convenience constructors that load from filename
#import "SVGKSourceURL.h" // for convenience constructors that load from URL as string
#import "SVGKSourceNSData.h" // for convenience constructors that load from raw incoming NSData
#import "CALayer+RecursiveClone.h"
#import "SVGKExporterUIImage.h" // needed for .UIImage property
#if ENABLE_GLOBAL_IMAGE_CACHE_FOR_SVGKIMAGE_IMAGE_NAMED
@interface SVGKImageCacheLine : NSObject
@property(nonatomic) int numberOfInstances;
@property(nonatomic,retain) SVGKImage* mainInstance;
@end
@implementation SVGKImageCacheLine
@synthesize numberOfInstances;
@synthesize mainInstance;
@end
#endif
@interface SVGKImage ()
@property(nonatomic) CGSize internalSizeThatWasSetExplicitlyByUser;
@property (nonatomic, retain, readwrite) SVGKParseResult* parseErrorsAndWarnings;
@property (nonatomic, retain, readwrite) SVGKSource* source;
@property (nonatomic, retain, readwrite) SVGDocument* DOMDocument;
@property (nonatomic, retain, readwrite) SVGSVGElement* DOMTree; // needs renaming + (possibly) replacing by DOMDocument
@property (nonatomic, retain, readwrite) CALayer* CALayerTree;
#if ENABLE_GLOBAL_IMAGE_CACHE_FOR_SVGKIMAGE_IMAGE_NAMED
@property (nonatomic, retain, readwrite) NSString* nameUsedToInstantiate;
#endif
#pragma mark - UIImage methods cloned and re-implemented as SVG intelligent methods
//NOT DEFINED: what is the scale for a SVGKImage? @property(nonatomic,readwrite) CGFloat scale __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_4_0);
@end
#pragma mark - main class
@implementation SVGKImage
@synthesize DOMDocument, DOMTree, CALayerTree;
@synthesize scale = _scale;
@synthesize source;
@synthesize parseErrorsAndWarnings;
#if ENABLE_GLOBAL_IMAGE_CACHE_FOR_SVGKIMAGE_IMAGE_NAMED
@synthesize nameUsedToInstantiate = _nameUsedToInstantiate;
static NSMutableDictionary* globalSVGKImageCache;
#pragma mark - Respond to low-memory warnings by dumping the global static cache
+(void) initialize
{
if( self == [SVGKImage class]) // Have to protect against subclasses ADDITIONALLY calling this, as a "[super initialize] line
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarningOrBackgroundNotification:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarningOrBackgroundNotification:) name:UIApplicationDidEnterBackgroundNotification object:nil];
}
}
+(void) didReceiveMemoryWarningOrBackgroundNotification:(NSNotification*) notification
{
if ([globalSVGKImageCache count] == 0) return;
SVGKitLogWarn(@"[%@] Low-mem or background; purging cache of %lu SVGKImages...", self, (unsigned long)[globalSVGKImageCache count] );
[globalSVGKImageCache removeAllObjects]; // once they leave the cache, if they are no longer referred to, they should automatically dealloc
}
#endif
#pragma mark - Convenience initializers
+ (SVGKImage *)imageNamed:(NSString *)name
{
#if ENABLE_GLOBAL_IMAGE_CACHE_FOR_SVGKIMAGE_IMAGE_NAMED
if( globalSVGKImageCache == nil )
{
globalSVGKImageCache = [NSMutableDictionary new];
}
SVGKImageCacheLine* cacheLine = [globalSVGKImageCache valueForKey:name];
if( cacheLine != nil )
{
cacheLine.numberOfInstances ++;
return cacheLine.mainInstance;
}
#endif
SVGKSource* source = [SVGKSourceLocalFile internalSourceAnywhereInBundleUsingName:name];
/**
Key moment: init and parse the SVGKImage
*/
SVGKImage* result = [self imageWithSource:source];
#if ENABLE_GLOBAL_IMAGE_CACHE_FOR_SVGKIMAGE_IMAGE_NAMED
if( result != nil )
{
result->cameFromGlobalCache = TRUE;
result.nameUsedToInstantiate = name;
SVGKImageCacheLine* newCacheLine = [[[SVGKImageCacheLine alloc] init] autorelease];
newCacheLine.mainInstance = result;
[globalSVGKImageCache setValue:newCacheLine forKey:name];
}
else
{
NSLog(@"[%@] WARNING: not caching the output for new SVG image with name = %@, because it failed to load correctly", [self class], name );
}
#endif
return result;
}
+(SVGKParser *) imageAsynchronouslyNamed:(NSString *)name onCompletion:(SVGKImageAsynchronousLoadingDelegate)blockCompleted
{
return [self imageWithSource:[SVGKSourceLocalFile internalSourceAnywhereInBundleUsingName:name] onCompletion:blockCompleted];
}
+(SVGKParser *) imageWithSource:(SVGKSource *)source onCompletion:(SVGKImageAsynchronousLoadingDelegate)blockCompleted
{
#if ENABLE_GLOBAL_IMAGE_CACHE_FOR_SVGKIMAGE_IMAGE_NAMED
if( globalSVGKImageCache == nil )
{
globalSVGKImageCache = [NSMutableDictionary new];
}
SVGKImageCacheLine* cacheLine = [globalSVGKImageCache valueForKey:source.keyForAppleDictionaries];
if( cacheLine != nil )
{
cacheLine.numberOfInstances ++;
blockCompleted( cacheLine.mainInstance, /** (TODO: add a way for parse-results to chain each other, and say "I'm the cached version of this OTHER parseresult") original parse result: */ cacheLine.mainInstance.parseErrorsAndWarnings );
return nil;
}
#endif
/**
Key moment: init and parse the SVGKImage
*/
SVGKParser* parser = [SVGKParser newParserWithDefaultSVGKParserExtensions:source];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
SVGKParseResult* parsedSVG = [parser parseSynchronously];
SVGKImage* finalImage = [[[SVGKImage alloc] initWithParsedSVG:parsedSVG fromSource:source] autorelease];
#if ENABLE_GLOBAL_IMAGE_CACHE_FOR_SVGKIMAGE_IMAGE_NAMED
if( finalImage != nil )
{
finalImage->cameFromGlobalCache = TRUE;
finalImage.nameUsedToInstantiate = source.keyForAppleDictionaries;
SVGKImageCacheLine* newCacheLine = [[[SVGKImageCacheLine alloc] init] autorelease];
newCacheLine.mainInstance = finalImage;
[globalSVGKImageCache setValue:newCacheLine forKey:source.keyForAppleDictionaries];
}
else
{
NSLog(@"[%@] WARNING: not caching the output for new SVG image with source = %@, because it failed to load correctly", [self class], source );
}
#endif
blockCompleted( finalImage, parsedSVG );
});
return parser;
}
+ (SVGKImage*) imageWithContentsOfURL:(NSURL *)url {
NSParameterAssert(url != nil);
@synchronized(self) {
return [[[[self class] alloc] initWithContentsOfURL:url] autorelease];
}
}
+ (SVGKImage*) imageWithContentsOfFile:(NSString *)aPath {
@synchronized(self) {
return [[[[self class] alloc] initWithContentsOfFile:aPath] autorelease];
}
}
+ (SVGKImage*) imageWithSource:(SVGKSource *)newSource
{
NSParameterAssert(newSource != nil);
@synchronized(self) {
return [[[[self class] alloc] initWithSource:newSource] autorelease];
}
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
/** Remove and release (if appropriate) all cached render-output */
SVGKitLogVerbose(@"[%@] source data changed; de-caching cached data", [self class] );
self.CALayerTree = nil;
}
/**
Designated Initializer
*/
- (id)initWithParsedSVG:(SVGKParseResult *)parseResult fromSource:(SVGKSource*) parseSource
{
self = [super init];
if (self)
{
_internalSizeThatWasSetExplicitlyByUser = CGSizeZero; // mark it explicitly as "uninitialized" = this is important for the getSize method!
_scale = 0.0; // flags it as uninitialized (this is important to know later, when outputting rendered layers)
self.parseErrorsAndWarnings = parseResult;
if( parseErrorsAndWarnings.parsedDocument != nil )
{
self.DOMDocument = parseErrorsAndWarnings.parsedDocument;
self.DOMTree = DOMDocument.rootElement;
self.source = parseSource;
}
else
{
self.DOMDocument = nil;
self.DOMTree = nil;
}
if ( self.DOMDocument == nil )
{
SVGKitLogError(@"[%@] ERROR: failed to init SVGKImage with source = %@, returning nil from init methods. Parser warnings and errors = %@", [self class], parseSource, parseErrorsAndWarnings );
self = nil;
}
[self addObserver:self forKeyPath:@"DOMTree.viewport" options:NSKeyValueObservingOptionOld context:nil];
// [self.DOMTree addObserver:self forKeyPath:@"viewport" options:NSKeyValueObservingOptionOld context:nil];
}
return self;
}
- (id)initWithSource:(SVGKSource *)newSource {
NSAssert( newSource != nil, @"Attempted to init an SVGKImage using a nil SVGKSource");
self = [self initWithParsedSVG:[SVGKParser parseSourceUsingDefaultSVGKParser:newSource] fromSource:newSource];
return self;
}
- (id)initWithContentsOfFile:(NSString *)aPath {
NSParameterAssert(aPath != nil);
return [self initWithSource:[SVGKSourceLocalFile sourceFromFilename:aPath]];
}
- (id)initWithContentsOfURL:(NSURL *)url {
NSParameterAssert(url != nil);
return [self initWithSource:[SVGKSourceURL sourceFromURL:url]];
}
- (id)initWithData:(NSData *)data
{
NSParameterAssert(data != nil);
SVGKitLogWarn(@"Creating an SVG from raw data; this is not recommended: SVG requires knowledge of at least the URL where it came from (as it can contain relative file-links internally). You should use the method [SVGKImage initWithSource:] instead and specify an SVGKSource with more detail" );
return [self initWithSource:[SVGKSourceNSData sourceFromData:data URLForRelativeLinks:nil]];
}
- (void)dealloc
{
#if ENABLE_GLOBAL_IMAGE_CACHE_FOR_SVGKIMAGE_IMAGE_NAMED
if( self->cameFromGlobalCache )
{
SVGKImageCacheLine* cacheLine = [globalSVGKImageCache valueForKey:self.nameUsedToInstantiate];
cacheLine.numberOfInstances --;
if( cacheLine.numberOfInstances < 1 )
{
[globalSVGKImageCache removeObjectForKey:self.nameUsedToInstantiate];
}
}
#endif
//SOMETIMES CRASHES IN APPLE CODE, CAN'T WORK OUT WHY: [self removeObserver:self forKeyPath:@"DOMTree.viewport"];
@try {
[self removeObserver:self forKeyPath:@"DOMTree.viewport"];
}
@catch (NSException *exception) {
SVGKitLogError(@"Exception removing DOMTree.viewport observer");
}
self.source = nil;
self.parseErrorsAndWarnings = nil;
self.DOMDocument = nil;
self.DOMTree = nil;
self.CALayerTree = nil;
#if ENABLE_GLOBAL_IMAGE_CACHE_FOR_SVGKIMAGE_IMAGE_NAMED
self.nameUsedToInstantiate = nil;
#endif
[super dealloc];
}
//TODO mac alternatives to UIKit functions
#pragma mark - UIImage methods we reproduce to make it act like a UIImage
-(BOOL) hasSize
{
if( ! CGSizeEqualToSize(CGSizeZero, self.internalSizeThatWasSetExplicitlyByUser ) )
return true;
if( SVGRectIsInitialized( self.DOMTree.viewport ) )
return true;
if( SVGRectIsInitialized( self.DOMTree.viewBox ) )
return true;
return false;
}
-(CGSize)size
{
/**
c.f. http://t-machine.org/index.php/2013/04/13/svg-spec-missing-documentation-the-viewport-and-svg-width/
1. if we have an explicit size (something the user set), we return that; it overrides EVERYTHING else
2. otherwise ... if we have an INTERNAL viewport on the SVG, we return that
3. otherwise ... spec is UNDEFINED. If we have a viewbox, we return that (SVG spec defaults to 1 unit of viewbox = 1 pixel on screen)
4. otherwise ... spec is UNDEFINED. We have no viewbox, so we assume viewbox is "the bounding box of the entire SVG content, in SVG units", and use 3. above
*/
/* 1. if we have an explicit size (something the user set), we return that; it overrides EVERYTHING else */
if( ! CGSizeEqualToSize(CGSizeZero, self.internalSizeThatWasSetExplicitlyByUser ) )
{
return self.internalSizeThatWasSetExplicitlyByUser;
}
/* 2. otherwise ... if we have an INTERNAL viewport on the SVG, we return that */
if( SVGRectIsInitialized( self.DOMTree.viewport ) )
{
return CGSizeFromSVGRect( self.DOMTree.viewport );
}
/* Calculate a viewbox, either the explicit one from 3. above, or the implicit one from 4. above
*/
SVGRect effectiveViewbox;
if( ! SVGRectIsInitialized( self.DOMTree.viewBox ) )
{
/**
This is painful; the only way to calculate this is to recurse down the entire tree and find out the total extent
of every item - taking into account all local and global transforms, etc
We CANNOT USE the CALayerTree as a cheat to do this - because the CALayerTree itself uses the output of this method
to decide how large to output itself!
So, for now, we're going to NSAssert and crash, deliberately, until someone can write a better algorithm (without
editing the source of all the SVG* classes, this is quite a lot of work, I think)
*/
NSAssert(FALSE, @"Your SVG file has no internal size, and you have failed to specify a desired size. Therefore, we cannot give you a value for the 'image.size' property - you MUST use an SVG file that has a viewbox property, OR use an SVG file that defines an explicit svg width, OR provide a size of your own choosing (by setting image.size to a value) ... before you call this method" );
effectiveViewbox = SVGRectUninitialized();
}
else
effectiveViewbox = self.DOMTree.viewBox;
/* COMBINED TOGETHER:
3. otherwise ... spec is UNDEFINED. If we have a viewbox, we return that (SVG spec defaults to 1 unit of viewbox = 1 pixel on screen)
4. otherwise ... spec is UNDEFINED. We have no viewbox, so we assume viewbox is "the bounding box of the entire SVG content, in SVG units", and use 3. above
*/
return CGSizeFromSVGRect( effectiveViewbox );
}
-(void)setSize:(CGSize)newSize
{
self.internalSizeThatWasSetExplicitlyByUser = newSize;
if( ! SVGRectIsInitialized(self.DOMTree.viewBox) && !SVGRectIsInitialized( self.DOMTree.viewport ) )
{
NSLog(@"[%@] WARNING: you have set an explicit image size, but your SVG file has no explicit width or height AND no viewBox. This means the image will NOT BE SCALED - either add a viewBox to your SVG source file, or add an explicit svg width and height -- or: use the .scale method on this class (SVGKImage) instead to scale by desired amount", [self class]);
}
/** "size" is part of SVGKImage, not the SVG spec; we need to update the SVG spec size too (aka the ViewPort)
NB: in SVG world, the DOMTree.viewport is REQUIRED to be deleted if the "rendering agent" (i.e. this library)
uses a different value for viewport.
You can always re-calculate the "original" viewport by looking at self.DOMTree.width and self.DOMTree.height
*/
self.DOMTree.viewport = SVGRectMake(0,0,newSize.width,newSize.height); // implicitly resizes all the internal rendering of the SVG
/** invalidate all cached data that's dependent upon SVG's size */
self.CALayerTree = nil; // invalidate the cached copy
}
-(void)setScale:(CGFloat)newScale
{
NSAssert( self.DOMTree != nil, @"Can't set a scale before you've parsed an SVG file; scale is sometimes illegal, depending on the SVG file itself");
NSAssert( ! SVGRectIsInitialized( self.DOMTree.viewBox ), @"image.scale cannot be set because your SVG has an internal viewbox. To resize this SVG, you must instead call image.size = (a new size) to force the svg to scale itself up or down as appropriate");
_scale = newScale;
/** invalidate all cached data that's dependent upon SVG's size */
self.CALayerTree = nil; // invalidate the cached copy
}
-(UIImage *)UIImage
{
return [SVGKExporterUIImage exportAsUIImage:self antiAliased:TRUE curveFlatnessFactor:1.0f interpolationQuality:kCGInterpolationDefault]; // Apple defaults
}
// the these draw the image 'right side up' in the usual coordinate system with 'point' being the top-left.
- (void)drawAtPoint:(CGPoint)point // mode = kCGBlendModeNormal, alpha = 1.0
{
NSAssert( FALSE, @"Method unsupported / not yet implemented by SVGKit" );
}
#pragma mark - unsupported / unimplemented UIImage methods (should add as a feature)
- (void)drawAtPoint:(CGPoint)point blendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha
{
NSAssert( FALSE, @"Method unsupported / not yet implemented by SVGKit" );
}
- (void)drawInRect:(CGRect)rect // mode = kCGBlendModeNormal, alpha = 1.0
{
NSAssert( FALSE, @"Method unsupported / not yet implemented by SVGKit" );
}
- (void)drawInRect:(CGRect)rect blendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha
{
NSAssert( FALSE, @"Method unsupported / not yet implemented by SVGKit" );
}
- (void)drawAsPatternInRect:(CGRect)rect // draws the image as a CGPattern
// animated images. When set as UIImageView.image, animation will play in an infinite loop until removed. Drawing will render the first image
{
NSAssert( FALSE, @"Method unsupported / not yet implemented by SVGKit" );
}
#if TARGET_OS_IPHONE
+ (UIImage *)animatedImageNamed:(NSString *)name duration:(NSTimeInterval)duration // read sequnce of files with suffix starting at 0 or 1
{
NSAssert( FALSE, @"Method unsupported / not yet implemented by SVGKit" );
return nil;
}
+ (UIImage *)animatedResizableImageNamed:(NSString *)name capInsets:(UIEdgeInsets)capInsets duration:(NSTimeInterval)duration // squence of files
{
NSAssert( FALSE, @"Method unsupported / not yet implemented by SVGKit" );
return nil;
}
+ (UIImage *)animatedImageWithImages:(NSArray *)images duration:(NSTimeInterval)duration
{
NSAssert( FALSE, @"Method unsupported / not yet implemented by SVGKit" );
return nil;
}
#endif
#pragma mark - CALayer methods: generate the CALayerTree
- (CALayer *)layerWithIdentifier:(NSString *)identifier
{
return [self layerWithIdentifier:identifier layer:self.CALayerTree];
}
- (CALayer *)layerWithIdentifier:(NSString *)identifier layer:(CALayer *)layer {
if ([[layer valueForKey:kSVGElementIdentifier] isEqualToString:identifier]) {
return layer;
}
for (CALayer *child in layer.sublayers) {
CALayer *resultingLayer = [self layerWithIdentifier:identifier layer:child];
if (resultingLayer)
return resultingLayer;
}
return nil;
}
-(CALayer*) newCopyPositionedAbsoluteLayerWithIdentifier:(NSString *)identifier
{
NSAssert( identifier != nil, @"Requested the layer with NIL identifier - your calling method is broken and should check its arguments more carefully");
CALayer* originalLayer = [self layerWithIdentifier:identifier];
if( originalLayer == nil )
{
SVGKitLogError(@"[%@] ERROR: requested a clone of CALayer with id = %@, but there is no layer with that identifier in the parsed SVG layer stack", [self class], identifier );
return nil;
}
else
return [self newCopyPositionedAbsoluteOfLayer:originalLayer];
}
-(CALayer*) newCopyPositionedAbsoluteOfLayer:(CALayer *)originalLayer
{
return [self newCopyPositionedAbsoluteOfLayer:originalLayer withSubLayers:FALSE];
}
-(CALayer*) newCopyPositionedAbsoluteOfLayer:(CALayer *)originalLayer withSubLayers:(BOOL) recursive
{
/*CALayer* clonedLayer = [[[originalLayer class] alloc] init];
clonedLayer.frame = originalLayer.frame;
if( [originalLayer isKindOfClass:[CAShapeLayer class]] )
{
((CAShapeLayer*)clonedLayer).path = ((CAShapeLayer*)originalLayer).path;
((CAShapeLayer*)clonedLayer).lineCap = ((CAShapeLayer*)originalLayer).lineCap;
((CAShapeLayer*)clonedLayer).lineWidth = ((CAShapeLayer*)originalLayer).lineWidth;
((CAShapeLayer*)clonedLayer).strokeColor = ((CAShapeLayer*)originalLayer).strokeColor;
((CAShapeLayer*)clonedLayer).fillColor = ((CAShapeLayer*)originalLayer).fillColor;
}*/
CALayer* clonedLayer = recursive ? [originalLayer cloneRecursively] : [originalLayer cloneShallow];
if( clonedLayer == nil )
return nil;
else
{
/** CALayer has the magic valueForKey method */
NSString* layerID = [originalLayer valueForKey:kSVGElementIdentifier];
if( layerID != nil )
[clonedLayer setValue:layerID forKey:kSVGElementIdentifier];
CGRect lFrame = clonedLayer.frame;
CGFloat xOffset = 0.0;
CGFloat yOffset = 0.0;
CALayer* currentLayer = originalLayer;
if( currentLayer.superlayer == nil )
{
SVGKitLogWarn(@"AWOOGA: layer %@ has no superlayer!", originalLayer );
}
while( currentLayer.superlayer != nil )
{
//DEBUG: SVGKitLogVerbose(@"shifting (%2.2f, %2.2f) to accomodate offset of layer = %@ inside superlayer = %@", currentLayer.superlayer.frame.origin.x, currentLayer.superlayer.frame.origin.y, currentLayer, currentLayer.superlayer );
currentLayer = currentLayer.superlayer;
//DEBUG: SVGKitLogVerbose(@"...next superlayer in positioning absolute = %@, %@", currentLayer, NSStringFromCGRect(currentLayer.frame));
xOffset += currentLayer.frame.origin.x;
yOffset += currentLayer.frame.origin.y;
}
lFrame.origin = CGPointMake( lFrame.origin.x + xOffset, lFrame.origin.y + yOffset );
clonedLayer.frame = lFrame;
return [clonedLayer retain];
}
}
- (CALayer *)newLayerWithElement:(SVGElement <ConverterSVGToCALayer> *)element
{
CALayer *layer = [element newLayer];
layer.hidden = ![self isElementVisible:element];
//DEBUG: SVGKitLogVerbose(@"[%@] DEBUG: converted SVG element (class:%@) to CALayer (class:%@ frame:%@ pointer:%@) for id = %@", [self class], NSStringFromClass([element class]), NSStringFromClass([layer class]), NSStringFromCGRect( layer.frame ), layer, element.identifier);
NodeList* childNodes = element.childNodes;
Node* saveParentNode = nil;
/**
Special handling for <use> tags - they have to masquerade invisibly as the node they are referring to
*/
if( [element isKindOfClass:[SVGUseElement class]] )
{
SVGUseElement* useElement = (SVGUseElement*) element;
element = (SVGElement <ConverterSVGToCALayer> *)useElement.instanceRoot.correspondingElement;
saveParentNode = element.parentNode;
element.parentNode = useElement;
NodeList* nodeList = [[NodeList alloc] init];
[nodeList.internalArray addObject:element];
childNodes = [nodeList autorelease];
}
else
if ( [element isKindOfClass:[SVGSwitchElement class]] )
{
childNodes = [(SVGSwitchElement*) element visibleChildNodes];
}
/**
Special handling for clip-path; need to create their children
*/
NSString* clipPath = [element cascadedValueForStylableProperty:@"clip-path" inherit:NO];
if ( [clipPath hasPrefix:@"url"] )
{
NSRange idKeyRange = NSMakeRange(5, clipPath.length - 6);
NSString* _pathId = [clipPath substringWithRange:idKeyRange];
/** Replace the return layer with a special layer using the URL fill */
/** fetch the fill layer by URL using the DOM */
NSAssert( element.rootOfCurrentDocumentFragment != nil, @"This SVG shape has a URL clip-path type; it needs to search for that URL (%@) inside its nearest-ancestor <SVG> node, but the rootOfCurrentDocumentFragment reference was nil (suggests the parser failed, or the SVG file is corrupt)", _pathId );
SVGClipPathElement* clipPathElement = (SVGClipPathElement*) [element.rootOfCurrentDocumentFragment getElementById:_pathId];
NSAssert( clipPathElement != nil, @"This SVG shape has a URL clip-path (%@), but could not find an XML Node with that ID inside the DOM tree (suggests the parser failed, or the SVG file is corrupt)", _pathId );
CALayer *clipLayer = [clipPathElement newLayer];
for (SVGElement *child in clipPathElement.childNodes )
{
if ([child conformsToProtocol:@protocol(ConverterSVGToCALayer)]) {
CALayer *sublayer = [[self newLayerWithElement:(SVGElement<ConverterSVGToCALayer> *)child] autorelease];
if (!sublayer) {
continue;
}
[clipLayer addSublayer:sublayer];
}
}
[clipPathElement layoutLayer:clipLayer toMaskLayer:layer];
SVGKitLogWarn(@"DOESNT WORK, APPLE's API APPEARS BROKEN???? - About to mask layer frame (%@) with a mask of frame (%@)", NSStringFromCGRect(layer.frame), NSStringFromCGRect(clipLayer.frame));
layer.mask = clipLayer;
[clipLayer release]; // because it was created with a +1 retain count
}
/**
Generate child nodes and then re-layout
(parent may have to change its size to fit children)
*/
NSUInteger sublayerCount = 0;
for (SVGElement *child in childNodes )
{
if ([child conformsToProtocol:@protocol(ConverterSVGToCALayer)]) {
CALayer *sublayer = [[self newLayerWithElement:(SVGElement<ConverterSVGToCALayer> *)child] autorelease];
if (!sublayer) {
continue;
}
sublayerCount++;
[layer addSublayer:sublayer];
}
}
if (saveParentNode)
element.parentNode = saveParentNode;
/**
If none of the child nodes return a CALayer, we're safe to early-out here (and in fact we need to because
calling setNeedsDisplay on an image layer hides the image). We can't just check childNodes.count because
there may be some nodes like whitespace nodes for which we don't create layers.
*/
if ( sublayerCount < 1 ) {
return layer;
}
/** ...relayout */
/**
NOTE:
This call (layoutLayer:), and the fact that we call it directly on the "ConverterSVGToCALayer" instance,
is critical to ensuring that SVG <g> tags generate correctly sized/shaped/positioned CALayer's.
It is not used for any other class / SVG Element.
It's only needed by G elements because they have no explicit size, and their extent is defined by
"all the space occupied by my children"
If you refactor this method, or CALayer exporting, please make sure you keep the current behaviour. You can
test it by:
1. Make an SVG file with a G element wrapping some shape in the middle of screen
2. Load the file
3. Select the CALayer for the shape, and clone it (using the category for CAShape in this project)
4. add the clone to the screen, with its CALayer.position set to 0,0
5. If the code is correct, it will be positioned in top left corner.
6. If the code is broken, it will be positioned somewhere in the middle of the screen (probably directly on top of the one you cloned)
--- i.e. you've accidentally embedded the "relative position" into the "absolute position" of the CALayer
*/
[element layoutLayer:layer];
[layer setNeedsDisplay];
return layer;
}
-(CALayer *)newCALayerTree
{
if( self.DOMTree == nil )
return nil;
else
{
CALayer* newLayerTree = [self newLayerWithElement:self.DOMTree];
if( 0.0f != self.scale )
{
NSLog(@"[%@] WARNING: because you specified an image.scale (you SHOULD be using SVG viewbox or <svg width> instead!), we are changing the .anchorPoint and the .affineTransform of the returned CALayerTree. Apple's own libraries are EXTREMELY BUGGY if you hand them layers that have these variables changed (some of Apple's libraries completely ignore them, this is a major Known Bug that Apple hasn't fixed in many years). Proceed at your own risk, and warned!", [self class] );
/** Apple's bugs in CALayer are legion, and some have been around for almost 10 years...
When you set the affineTransform on a Layer, if you do not ALSO MANUALLY change the anchorpoint, Apple
renders the layer at the wrong co-ords.
*/
newLayerTree.anchorPoint = CGPointApplyAffineTransform( newLayerTree.anchorPoint, CGAffineTransformMakeScale(1.0f/self.scale, 1.0f/self.scale));
newLayerTree.affineTransform = CGAffineTransformMakeScale( self.scale, self.scale );
}
return newLayerTree;
}
}
-(CALayer *)CALayerTree
{
if( CALayerTree == nil )
{
SVGKitLogInfo(@"[%@] WARNING: no CALayer tree found, creating a new one (will cache it once generated)", [self class] );
NSDate* startTime = [NSDate date];
self.CALayerTree = [[self newCALayerTree] autorelease];
SVGKitLogInfo(@"[%@] ...time taken to convert from DOM to fresh CALayers: %2.3f seconds)", [self class], -1.0f * [startTime timeIntervalSinceNow] );
}
else
SVGKitLogVerbose(@"[%@] fetching CALayerTree: re-using cached CALayers (FREE))", [self class] );
return CALayerTree;
}
- (void) addSVGLayerTree:(CALayer*) layer withIdentifier:(NSString*) layerID toDictionary:(NSMutableDictionary*) layersByID
{
// TODO: consider removing this method: it caches the lookup of individual items in the CALayerTree. It's a performance boost, but is it enough to be worthwhile?
[layersByID setValue:layer forKey:layerID];
if ( [layer.sublayers count] < 1 )
{
return;
}
for (CALayer *subLayer in layer.sublayers)
{
NSString* subLayerID = [subLayer valueForKey:kSVGElementIdentifier];
if( subLayerID != nil )
{
SVGKitLogVerbose(@"[%@] element id: %@ => layer: %@", [self class], subLayerID, subLayer);
[self addSVGLayerTree:subLayer withIdentifier:subLayerID toDictionary:layersByID];
}
}
}
- (NSDictionary*) dictionaryOfLayers
{
// TODO: consider removing this method: it caches the lookup of individual items in the CALayerTree. It's a performance boost, but is it enough to be worthwhile?
NSMutableDictionary* layersByElementId = [NSMutableDictionary dictionary];
CALayer* rootLayer = self.CALayerTree;
[self addSVGLayerTree:rootLayer withIdentifier:self.DOMTree.identifier toDictionary:layersByElementId];
SVGKitLogVerbose(@"[%@] ROOT element id: %@ => layer: %@", [self class], self.DOMTree.identifier, rootLayer);
return layersByElementId;
}
#pragma mark - Useful bonus methods, will probably move to a different class at some point
-(void) scaleToFitInside:(CGSize) maxSize
{
NSAssert( [self hasSize], @"Cannot scale this image because the SVG file has infinite size. Either fix the SVG file, or set an explicit size you want it to be exported at (by calling .size = something on this SVGKImage instance");
float wScale = maxSize.width / self.size.width;
float hScale = maxSize.height / self.size.height;
float smallestScaleUp = MIN( wScale, hScale );
if( smallestScaleUp < 1.0f )
smallestScaleUp = MAX( wScale, hScale ); // instead of scaling-up the smallest, scale-down the largest
self.size = CGSizeApplyAffineTransform( self.size, CGAffineTransformMakeScale( smallestScaleUp, smallestScaleUp));
}
-(BOOL) isElementVisible:(SVGElement *) element
{
NSString *display = [element cascadedValueForStylableProperty:@"display" inherit:NO];
if( [display isEqualToString:@"none"] )
return NO;
NSString *visibility = [element cascadedValueForStylableProperty:@"visibility" inherit:NO];
if( [visibility isEqualToString:@"hidden"] )
return NO;
return YES;
}
@end