<rdar://problem/3991818> REGRESSION: Images scale while loading
[WebKit-https.git] / WebKit / WebCoreSupport.subproj / WebImageData.m
1 /*      
2         WebImageData.m
3         Copyright (c) 2004 Apple, Inc. All rights reserved.
4 */
5 #import <WebKit/WebAssertions.h>
6 #import <WebKit/WebGraphicsBridge.h>
7 #import <WebKit/WebImageData.h>
8 #import <WebKit/WebImageDecoder.h>
9 #import <WebKit/WebImageRenderer.h>
10 #import <WebKit/WebImageRendererFactory.h>
11 #import <WebKit/WebKitSystemBits.h>
12
13 #import <WebCore/WebCoreImageRenderer.h>
14
15 #import <CoreGraphics/CGContextPrivate.h>
16 #import <CoreGraphics/CGContextGState.h>
17 #import <CoreGraphics/CGColorSpacePrivate.h>
18
19 #ifdef USE_CGIMAGEREF
20
21 #import <ImageIO/CGImageSourcePrivate.h>
22
23 static CFDictionaryRef imageSourceOptions;
24
25 // Forward declarations of internal methods.
26 @interface WebImageData (WebInternal)
27 - (void)_commonTermination;
28 - (void)_invalidateImages;
29 - (void)_invalidateImageProperties;
30 - (int)_repetitionCount;
31 - (float)_frameDuration;
32 - (void)_stopAnimation;
33 - (void)_nextFrame;
34 - (CFDictionaryRef)_imageSourceOptions;
35 -(void)_createPDFWithData:(NSData *)data;
36 - (CGPDFDocumentRef)_PDFDocumentRef;
37 - (BOOL)_PDFDrawFromRect:(NSRect)srcRect toRect:(NSRect)dstRect operation:(CGCompositeOperation)op alpha:(float)alpha flipped:(BOOL)flipped context:(CGContextRef)context;
38 - (void)_cacheImages:(size_t)optionalIndex allImages:(BOOL)allImages;
39 @end
40
41
42 @implementation WebImageData
43
44 + (void)initialize
45 {
46     // Currently threaded decoding doesn't play well with the WebCore cache.  Until
47     // those issues are resolved threaded decoding is OFF by default, even on dual CPU
48     // machines.
49     //[WebImageRendererFactory setShouldUseThreadedDecoding:(WebNumberOfCPUs() >= 2 ? YES : NO)];
50     [WebImageRendererFactory setShouldUseThreadedDecoding:NO];
51 }
52
53 - init
54 {
55     self = [super init];
56     
57     if ([WebImageRendererFactory shouldUseThreadedDecoding])
58         decodeLock = [[NSLock alloc] init];
59
60     imageSource = CGImageSourceCreateIncremental ([self _imageSourceOptions]);
61     
62     return self;
63 }
64
65 - (void)setIsPDF:(BOOL)f
66 {
67     isPDF = f;
68 }
69
70 - (BOOL)isPDF
71 {
72     return isPDF;
73 }
74
75 - (void)_commonTermination
76 {
77     ASSERT (!frameTimer);
78     
79     [self _invalidateImages];
80     [self _invalidateImageProperties];
81         
82     if (fileProperties)
83         CFRelease (fileProperties);
84         
85     if (imageSource)
86         CFRelease (imageSource); 
87         
88     if (animatingRenderers)
89         CFRelease (animatingRenderers);
90
91     free (frameDurations);
92 }
93
94 - (void)dealloc
95 {
96     [_PDFDoc release];
97     [decodeLock release];
98
99     [self _commonTermination];
100
101     [super dealloc];
102 }
103
104 - (void)finalize
105 {
106     [self _commonTermination];
107     [super finalize];
108 }
109
110 - copyWithZone:(NSZone *)zone
111 {
112     WebImageData *copy;
113
114     copy = [[WebImageData alloc] init];
115     CFRetain (imageSource);
116     copy->imageSource = imageSource;
117     
118     return copy;
119 }
120
121
122 - (size_t)numberOfImages
123 {
124     if (imageSource)
125         return CGImageSourceGetCount(imageSource);
126     return 0;
127 }
128
129 - (size_t)currentFrame
130 {
131     return currentFrame;
132 }
133
134 - (void)_invalidateImages
135 {
136     if (images) {
137         size_t i;
138         for (i = 0; i < imagesSize; i++) {
139             if (images[i])
140                 CFRelease (images[i]);
141         }
142         free (images);
143         images = 0;
144         
145         isSolidColor = NO;
146         if( solidColor ) {
147             CFRelease(solidColor);
148             solidColor = NULL;
149         }
150     }
151 }
152
153 - (void)_invalidateImageProperties
154 {
155     size_t i;
156     for (i = 0; i < imagePropertiesSize; i++) {
157         if (imageProperties[i])
158             CFRelease (imageProperties[i]);
159     }
160     free (imageProperties);
161     imageProperties = 0;
162     imagePropertiesSize = 0;
163 }
164
165 - (CFDictionaryRef)_imageSourceOptions
166 {
167     if (!imageSourceOptions) {
168         const void * keys[2] = { kCGImageSourceShouldCache, kCGImageSourceShouldPreferRGB32 };
169         const void * values[2] = { kCFBooleanTrue, kCFBooleanTrue };
170         imageSourceOptions = CFDictionaryCreate (NULL, keys, values, 2, 
171                 &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
172     }
173     return imageSourceOptions;
174 }
175
176 - (CGImageRef)_noColorCorrectionImage:(CGImageRef)image withProperties:(CFDictionaryRef)props;
177 {
178     CGColorSpaceRef uncorrectedColorSpace = 0;
179     CGImageRef noColorCorrectionImage = 0;
180
181     if (!CFDictionaryGetValue (props, kCGImagePropertyProfileName)) {
182         CFStringRef colorModel = CFDictionaryGetValue (props, kCGImagePropertyColorModel);
183         
184         if (colorModel) {
185             if (CFStringCompare (colorModel, CFSTR("RGB"), 0) == kCFCompareEqualTo)
186                 uncorrectedColorSpace = CGColorSpaceCreateDisplayRGB();
187             else if (CFStringCompare (colorModel, CFSTR("Gray"), 0) == kCFCompareEqualTo)
188                 uncorrectedColorSpace = CGColorSpaceCreateDisplayGray();
189         }
190         
191         if (uncorrectedColorSpace) {
192             noColorCorrectionImage = CGImageCreateCopyWithColorSpace (image, uncorrectedColorSpace);
193             CFRelease (uncorrectedColorSpace);
194         }
195     }
196     return noColorCorrectionImage;
197 }
198             
199 - (CGImageRef)imageAtIndex:(size_t)index
200 {
201     if (index >= [self numberOfImages])
202         return 0;
203
204     if (!images || images[index] == 0){
205         [self _cacheImages:index allImages:NO];
206     }
207     
208     return images[index];
209 }
210
211 - (CFDictionaryRef)fileProperties
212 {
213     if (!fileProperties) {
214         fileProperties = CGImageSourceCopyProperties (imageSource, 0);
215     }
216     
217     return fileProperties;
218 }
219
220 - (CFDictionaryRef)propertiesAtIndex:(size_t)index
221 {
222     size_t num = [self numberOfImages];
223     
224     // Number of images changed!
225     if (imagePropertiesSize && num > imagePropertiesSize) {
226         // Clear cache.
227         [self _invalidateImageProperties];
228     }
229     
230     if (imageProperties == 0 && num) {
231         imageProperties = (CFDictionaryRef *)malloc (num * sizeof(CFDictionaryRef));
232         size_t i;
233         for (i = 0; i < num; i++) {
234 #if USE_DEPRECATED_IMAGESOURCE_API      
235             imageProperties[i] = CGImageSourceGetPropertiesAtIndex (imageSource, i, 0);
236             if (imageProperties[i])
237                 CFRetain (imageProperties[i]);
238 #else
239             imageProperties[i] = CGImageSourceCopyPropertiesAtIndex (imageSource, i, 0);
240 #endif
241         }
242         imagePropertiesSize = num;
243     }
244     
245     if (index < num) {
246         // If image properties are nil, try to get them again.  May have attempted to
247         // get them before enough data was available in the header.
248         if (imageProperties[index] == 0) {
249 #if USE_DEPRECATED_IMAGESOURCE_API      
250             imageProperties[index] = CGImageSourceGetPropertiesAtIndex (imageSource, index, 0);
251             if (imageProperties[index])
252                 CFRetain (imageProperties[index]);
253 #else
254             imageProperties[index] = CGImageSourceCopyPropertiesAtIndex (imageSource, index, 0);
255 #endif
256         }
257         
258         return imageProperties[index];
259     }
260         
261     return 0;
262 }
263
264 - (void)_checkSolidColor: (CGImageRef)image
265 {
266     isSolidColor = NO;
267     if( solidColor ) {
268         CFRelease(solidColor);
269         solidColor = NULL;
270     }
271     
272     // Currently we only check for solid color in the important special case of a 1x1 image
273     if( image && CGImageGetWidth(image)==1 && CGImageGetHeight(image)==1 ) {
274         float pixel[4]; // RGBA
275         CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
276         CGContextRef bmap = CGBitmapContextCreate(&pixel,1,1,8*sizeof(float),sizeof(pixel),space,
277                                                   kCGImageAlphaPremultipliedLast | kCGBitmapFloatComponents);
278         if( bmap ) {
279             CGContextSetCompositeOperation(bmap, kCGCompositeCopy);
280             CGRect dst = {{0,0},{1,1}};
281             CGContextDrawImage(bmap,dst,image);
282             if( pixel[3] > 0 )
283                 solidColor = CGColorCreate(space,pixel);
284             isSolidColor = YES;
285             CFRelease(bmap);
286             /*NSLog(@"WebImageData %p: 1x1 image has color {%f,%f,%f,%f} => %p",
287                   self,pixel[0],pixel[1],pixel[2],pixel[3],solidColor);*/
288         } else {
289             ERROR("Couldn't create CGBitmapContext");
290         }
291         CFRelease(space);
292     }
293 }
294
295 - (void)_cacheImages:(size_t)optionalIndex allImages:(BOOL)allImages
296 {
297     size_t i;
298
299     imagesSize = [self numberOfImages];
300     size_t from, to;
301     
302     if (allImages) {
303         from = 0;
304         to = imagesSize;
305     }
306     else {
307         from = optionalIndex;
308         to = optionalIndex+1;
309     }
310     for (i = from; i < to; i++) {
311         if (!images) {
312             images = (CGImageRef *)calloc (imagesSize, sizeof(CGImageRef *));
313         }
314
315         images[i] = CGImageSourceCreateImageAtIndex (imageSource, i, [self _imageSourceOptions]);
316     }
317     
318     if (from==0 && to>0) {
319         // Loaded image 0. Check whether it's a solid color:
320         [self _checkSolidColor: images[0]];
321     }
322 }
323
324 // Called from decoder thread.
325 - (void)decodeData:(CFDataRef)data isComplete:(BOOL)isComplete callback:(id)callback
326 {
327     [decodeLock lock];
328     
329     if (isPDF) {
330         if (isComplete) {
331             [self _createPDFWithData:(NSData *)data];
332         }
333     }
334     else {
335         // The work of decoding is actually triggered by image creation.
336         CGImageSourceUpdateData (imageSource, data, isComplete);
337         [self _invalidateImages];
338         [self _cacheImages:0 allImages:YES];
339     }
340         
341     [decodeLock unlock];
342
343     if (isPDF) {
344         if (isComplete && callback) {
345             if ([self _PDFDocumentRef]) {
346                 [WebImageDecoder decodeComplete:callback status:kCGImageStatusComplete];
347             }
348             else {
349                 [WebImageDecoder decodeComplete:callback status:kCGImageStatusInvalidData];
350             }
351         }
352     }
353     else {
354         // Use status from first image to trigger appropriate notification back to WebCore
355         // on main thread.
356         if (callback) {
357             CGImageSourceStatus imageStatus = CGImageSourceGetStatusAtIndex(imageSource, 0);
358             
359             // Lie about status.  If we have all the data, go ahead and say we're complete
360             // as long we are have at least some valid bands (i.e. >= kCGImageStatusIncomplete).
361             // We have to lie because CG incorrectly reports the status.
362             if (isComplete && imageStatus >= kCGImageStatusIncomplete)
363                 imageStatus = kCGImageStatusComplete;
364             
365             // Only send bad status if we've read the whole image.
366             if (isComplete || (!isComplete && imageStatus >= kCGImageStatusIncomplete))
367                 [WebImageDecoder decodeComplete:callback status:imageStatus];
368         }
369     }
370 }
371
372 - (BOOL)incrementalLoadWithBytes:(const void *)bytes length:(unsigned)length complete:(BOOL)isComplete callback:(id)callback
373 {
374 #ifdef kImageBytesCutoff
375     // This is a hack to help with testing display of partially-loaded images.
376     // To enable it, define kImageBytesCutoff to be a size smaller than that of the image files
377     // being loaded. They'll never finish loading.
378     if( length > kImageBytesCutoff ) {
379         length = kImageBytesCutoff;
380         isComplete = NO;
381     }
382 #endif
383     
384     CFDataRef data = CFDataCreate (NULL, bytes, length);
385     
386     if (callback) {
387         [WebImageDecoder performDecodeWithImage:self data:data isComplete:isComplete callback:callback];
388     }
389     else {
390         if (isPDF) {
391             if (isComplete) {
392                 [self _createPDFWithData:(NSData *)data];
393             }
394         }
395         else {
396             [self _invalidateImages];
397             CGImageSourceUpdateData (imageSource, data, isComplete);
398         }
399     }
400     
401     CFRelease (data);
402
403     return YES;
404 }
405
406 - (void)_fillSolidColorInRect:(CGRect)rect compositeOperation:(CGCompositeOperation)op context:(CGContextRef)aContext
407 {
408     /*NSLog(@"WebImageData %p: filling with color %p, in {%.0f,%.0f, %.0f x %.0f}",
409           self,solidColor,rect.origin.x,rect.origin.y,rect.size.width,rect.size.height);*/
410     if( solidColor ) {
411         CGContextSaveGState (aContext);
412         CGContextSetFillColorWithColor(aContext, solidColor);
413         CGContextSetCompositeOperation (aContext, op);
414         CGContextFillRect (aContext, rect);
415         CGContextRestoreGState (aContext);
416     }
417 }
418
419 - (void)drawImageAtIndex:(size_t)index inRect:(CGRect)ir fromRect:(CGRect)fr adjustedSize:(CGSize)adjustedSize compositeOperation:(CGCompositeOperation)op context:(CGContextRef)aContext;
420 {
421     if (isPDF) {
422         [self _PDFDrawFromRect:NSMakeRect(fr.origin.x, fr.origin.y, fr.size.width, fr.size.height)
423                 toRect:NSMakeRect(ir.origin.x, ir.origin.y, ir.size.width, ir.size.height)
424                 operation:op 
425                 alpha:1.0 
426                 flipped:YES
427                 context:aContext];
428     }
429     else {
430         [decodeLock lock];
431         
432         CGImageRef image = [self imageAtIndex:index];
433         
434         if (!image) {
435             [decodeLock unlock];
436             return;
437         }
438         
439         if( isSolidColor && index==0 ) {
440             [self _fillSolidColorInRect: ir compositeOperation: op context: aContext];
441
442         } else {
443             CGContextSaveGState (aContext);
444             
445             // Get the height (in adjusted, i.e. scaled, coords) of the portion of the image
446             // that is currently decoded.  This could be less that the actual height.
447             CGSize selfSize = [self size];                          // full image size, in pixels
448             float curHeight = CGImageGetHeight(image);              // height of loaded portion, in pixels
449             
450             if( curHeight < selfSize.height ) {
451                 adjustedSize.height *= curHeight / selfSize.height;
452
453                 // Is the amount of available bands less than what we need to draw?  If so,
454                 // we may have to clip 'fr' if it goes outside the available bounds.
455                 if( CGRectGetMaxY(fr) > adjustedSize.height ) {
456                     float frHeight = adjustedSize.height - fr.origin.y; // clip fr to available bounds
457                     if( frHeight <= 0 ) {
458                         [decodeLock unlock];
459                         return;                                             // clipped out entirely
460                     }
461                     ir.size.height *= (frHeight / fr.size.height);    // scale ir proportionally to fr
462                     fr.size.height = frHeight;
463                 }
464             }
465             
466             // Flip the coords.
467             CGContextSetCompositeOperation (aContext, op);
468             CGContextTranslateCTM (aContext, ir.origin.x, ir.origin.y);
469             CGContextScaleCTM (aContext, 1, -1);
470             CGContextTranslateCTM (aContext, 0, -ir.size.height);
471             
472             // Translated to origin, now draw at 0,0.
473             ir.origin.x = ir.origin.y = 0;
474             
475             // If we're drawing a sub portion of the image then create
476             // a image for the sub portion and draw that.
477             // Test using example site at http://www.meyerweb.com/eric/css/edge/complexspiral/demo.html
478             if (fr.size.width != adjustedSize.width || fr.size.height != adjustedSize.height) {
479                 // Convert ft to image pixel coords:
480                 float xscale = adjustedSize.width / selfSize.width;
481                 float yscale = adjustedSize.height / curHeight;     // yes, curHeight, not selfSize.height!
482                 fr.origin.x /= xscale;
483                 fr.origin.y /= yscale;
484                 fr.size.width /= xscale;
485                 fr.size.height /= yscale;
486                 
487                 image = CGImageCreateWithImageInRect (image, fr);
488                 if (image) {
489                     CGContextDrawImage (aContext, ir, image);
490                     CFRelease (image);
491                 }
492             }
493             // otherwise draw the whole image.
494             else { 
495                 CGContextDrawImage (aContext, ir, image);
496             }
497
498             CGContextRestoreGState (aContext);
499         }
500         
501         [decodeLock unlock];
502     }
503 }
504
505 - (void)drawImageAtIndex:(size_t)index inRect:(CGRect)ir fromRect:(CGRect)fr compositeOperation:(CGCompositeOperation)op context:(CGContextRef)aContext;
506 {    
507     [self drawImageAtIndex:index inRect:ir fromRect:fr adjustedSize:[self size] compositeOperation:op context:aContext];
508 }
509
510 static void drawPattern (void * info, CGContextRef context)
511 {
512     WebImageData *data = (WebImageData *)info;
513     
514     CGImageRef image = (CGImageRef)[data imageAtIndex:[data currentFrame]];
515     float w = CGImageGetWidth(image);
516     float h = CGImageGetHeight(image);
517     CGContextDrawImage (context, CGRectMake(0, [data size].height-h, w, h), image);    
518 }
519
520 CGPatternCallbacks patternCallbacks = { 0, drawPattern, NULL };
521
522 - (void)tileInRect:(CGRect)rect fromPoint:(CGPoint)point context:(CGContextRef)aContext
523 {
524     ASSERT (aContext);
525
526     [decodeLock lock];
527     
528     size_t frame = [self currentFrame];
529     CGImageRef image = [self imageAtIndex:frame];
530     if (!image) {
531         ERROR ("unable to find image");
532         [decodeLock unlock];
533         return;
534     }
535
536     if( frame == 0 && isSolidColor ) {
537         [self _fillSolidColorInRect: rect compositeOperation: kCGCompositeSover context: aContext];
538         
539     } else {
540         CGSize tileSize = [self size];
541         NSSize imageSize = NSMakeSize(CGImageGetWidth(image),CGImageGetHeight(image));
542         
543         // Check and see if a single draw of the image can cover the entire area we are supposed to tile.
544         NSRect oneTileRect;
545         oneTileRect.origin.x = rect.origin.x + fmodf(fmodf(-point.x, tileSize.width) - tileSize.width, tileSize.width);
546         oneTileRect.origin.y = rect.origin.y + fmodf(fmodf(-point.y, tileSize.height) - tileSize.height, tileSize.height);
547         oneTileRect.size = imageSize;
548
549         // If the single image draw covers the whole area, then just draw once.
550         if (NSContainsRect(oneTileRect, NSMakeRect(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height))) {
551             CGRect fromRect;
552
553             fromRect.origin.x = rect.origin.x - oneTileRect.origin.x;
554             fromRect.origin.y = rect.origin.y - oneTileRect.origin.y;
555             fromRect.size = rect.size;
556
557             [decodeLock unlock];
558             
559             [self drawImageAtIndex:[self currentFrame] inRect:rect fromRect:fromRect compositeOperation:kCGCompositeSover context:aContext];
560
561             return;
562         }
563
564         // Compute the appropriate phase relative to the top level view in the window.
565         // Conveniently, the oneTileRect we computed above has the appropriate origin.
566         NSPoint originInWindow = [[NSView focusView] convertPoint:oneTileRect.origin toView:nil];
567
568         // WebCore may translate the focus, and thus need an extra phase correction
569         NSPoint extraPhase = [[WebGraphicsBridge sharedBridge] additionalPatternPhase];
570         originInWindow.x += extraPhase.x;
571         originInWindow.y += extraPhase.y;
572         CGSize phase = CGSizeMake(fmodf(originInWindow.x, tileSize.width), fmodf(originInWindow.y, tileSize.height));
573
574         // Possible optimization:  We may want to cache the CGPatternRef    
575         CGPatternRef pattern = CGPatternCreate(self, CGRectMake (0, 0, imageSize.width, imageSize.height), CGAffineTransformIdentity, tileSize.width, tileSize.height, 
576             kCGPatternTilingConstantSpacing, true, &patternCallbacks);
577         if (pattern) {
578             CGContextSaveGState (aContext);
579
580             CGContextSetPatternPhase(aContext, phase);
581
582             CGColorSpaceRef patternSpace = CGColorSpaceCreatePattern(NULL);
583             CGContextSetFillColorSpace(aContext, patternSpace);
584             CGColorSpaceRelease(patternSpace);
585
586             float patternAlpha = 1;
587             CGContextSetFillPattern(aContext, pattern, &patternAlpha);
588
589             CGContextSetCompositeOperation (aContext, kCGCompositeSover);
590
591             CGContextFillRect (aContext, rect);
592
593             CGPatternRelease (pattern);
594
595             CGContextRestoreGState (aContext);
596         }
597         else {
598             ERROR ("unable to create pattern");
599         }
600     }
601     
602     [decodeLock unlock];
603 }
604
605 - (BOOL)isNull
606 {
607     if (imageSource)
608         return CGImageSourceGetStatus(imageSource) < kCGImageStatusReadingHeader;
609     return YES;
610 }
611
612 - (CGSize)size
613 {
614     float w = 0.f, h = 0.f;
615
616     if (isPDF) {
617         if (_PDFDoc) {
618             CGRect mediaBox = [_PDFDoc mediaBox];
619             return mediaBox.size;
620         }
621     }
622     else {
623         if (!haveSize) {
624             [decodeLock lock];
625             CFDictionaryRef properties = [self propertiesAtIndex:0];
626             if (properties) {
627             CFNumberRef num = CFDictionaryGetValue (properties, kCGImagePropertyPixelWidth);
628             if (num)
629                 CFNumberGetValue (num, kCFNumberFloat32Type, &w);
630             num = CFDictionaryGetValue (properties, kCGImagePropertyPixelHeight);
631             if (num)
632                 CFNumberGetValue (num, kCFNumberFloat32Type, &h);
633
634             size.width = w;
635             size.height = h;
636             
637             haveSize = YES;
638             }
639             [decodeLock unlock];
640         }
641     }
642     
643     return size;
644 }
645
646 - (float)_floatProperty:(CFStringRef)property type:(CFStringRef)type at:(size_t)i
647 {
648     [decodeLock lock];
649     
650     CFDictionaryRef properties = [self propertiesAtIndex:i];
651     if (!properties) {
652         [decodeLock unlock];
653         return 0.f;
654     }
655     
656     CFDictionaryRef typeProperties = CFDictionaryGetValue (properties, type);
657     if (!typeProperties) {
658         [decodeLock unlock];
659         return 0.f;
660     }
661     
662     CFNumberRef num = CFDictionaryGetValue (typeProperties, property);
663     if (!num) {
664         [decodeLock unlock];
665         return 0.f;
666     }
667     
668     float value = 0.f;
669     CFNumberGetValue (num, kCFNumberFloat32Type, &value);
670
671     [decodeLock unlock];
672     
673     return value;
674 }
675
676 - (float)_floatFileProperty:(CFStringRef)property type:(CFStringRef)type hasProperty:(BOOL *)hasProperty;
677 {
678     [decodeLock lock];
679     
680     *hasProperty = NO;
681     
682     CFDictionaryRef properties = [self fileProperties];
683     if (!properties) {
684         [decodeLock unlock];
685         return 0.f;
686     }
687     
688     if (type) {
689         properties = CFDictionaryGetValue (properties, type);
690         if (!properties) {
691             [decodeLock unlock];
692             return 0.f;
693         }
694     }
695     
696     CFNumberRef num = CFDictionaryGetValue (properties, property);
697     if (!num) {
698         [decodeLock unlock];
699         return 0.f;
700     }
701     
702     float value = 0.f;
703     CFNumberGetValue (num, kCFNumberFloat32Type, &value);
704
705     [decodeLock unlock];
706
707     *hasProperty = YES;
708     
709     return value;
710 }
711
712 #define MINIMUM_DURATION (1.0/30.0)
713
714 - (float)_frameDurationAt:(size_t)i
715 {
716     float duration = [self _floatProperty:kCGImagePropertyGIFDelayTime type:kCGImagePropertyGIFDictionary at:i];
717     if (duration < MINIMUM_DURATION) {
718         /*
719             Many annoying ads specify a 0 duration to make an image flash
720             as quickly as possible.  However a zero duration is faster than
721             the refresh rate.  We need to pick a minimum duration.
722             
723             Browsers handle the minimum time case differently.  IE seems to use something
724             close to 1/30th of a second.  Konqueror uses 0.  The ImageMagick library
725             uses 1/100th.  The units in the GIF specification are 1/100th of second.
726             We will use 1/30th of second as the minimum time.
727         */
728         duration = MINIMUM_DURATION;
729     }
730     return duration;
731 }
732
733 - (float)_frameDuration
734 {
735     size_t num = [self numberOfImages];
736     if (frameDurationsSize && num > frameDurationsSize) {
737         free (frameDurations);
738         frameDurations = 0;
739         frameDurationsSize = 0;
740     }
741     
742     if (!frameDurations) {
743         size_t i;
744
745         frameDurations = (float *)malloc (sizeof(float) * num);
746         for (i = 0; i < num; i++) {
747             frameDurations[i] = [self _frameDurationAt:i];
748         }
749         frameDurationsSize = num;
750     }
751     else if (frameDurations[currentFrame] == 0.f) {
752         frameDurations[currentFrame] = [self _frameDurationAt:currentFrame];
753     }
754
755     return frameDurations[currentFrame];
756 }
757
758 - (int)_repetitionCount
759 {
760     int count;
761     BOOL hasProperty;
762
763     // No property means loop once.
764     // A property with value 0 means loops forever.
765     count = [self _floatFileProperty:kCGImagePropertyGIFLoopCount type:kCGImagePropertyGIFDictionary hasProperty:&hasProperty];
766     if (!hasProperty)
767         count = -1;
768         
769     return count;
770 }
771
772 - (BOOL)isAnimationFinished
773 {
774     return animationFinished;
775 }
776
777 static NSMutableSet *activeAnimations;
778
779 + (void)stopAnimationsInView:(NSView *)aView
780 {
781     NSEnumerator *objectEnumerator = [activeAnimations objectEnumerator];
782     WebImageData *animation;
783     NSMutableSet *renderersToStop = nil;
784
785     // Determine all the renderers that are drawing animations in the view.
786     // A set of sets, one set of renderers for each image being animated
787     // in the view.  It is necessary to gather the all renderers to stop
788     // before actually stopping them because the process of stopping them
789     // will modify the active animations and animating renderer collections.
790     while ((animation = [objectEnumerator nextObject])) {
791         NSSet *renderersInView = (NSSet *)CFDictionaryGetValue (animation->animatingRenderers, aView);
792         if (renderersInView) {
793                         if (!renderersToStop)
794                                 renderersToStop = [[NSMutableSet alloc] init];
795             [renderersToStop unionSet:renderersInView];
796         }
797     }
798
799     // Now tell them all to stop drawing.
800     [renderersToStop makeObjectsPerformSelector:@selector(stopAnimation)];
801         [renderersToStop release];
802 }
803
804 - (void)addAnimatingRenderer:(WebImageRenderer *)r inView:(NSView *)view
805 {
806     if (!animatingRenderers)
807         animatingRenderers = CFDictionaryCreateMutable (NULL, 0, NULL, &kCFTypeDictionaryValueCallBacks);
808     
809     NSMutableSet *renderers = (NSMutableSet *)CFDictionaryGetValue (animatingRenderers, view);
810     if (!renderers) {
811         renderers = [[NSMutableSet alloc] init];
812         CFDictionaryAddValue(animatingRenderers, view, renderers);
813         [renderers release];
814     }
815             
816     [renderers addObject:r];
817
818     if (!activeAnimations)
819         activeAnimations = [[NSMutableSet alloc] init];
820     
821     [activeAnimations addObject:self];
822 }
823
824 - (void)removeAnimatingRenderer:(WebImageRenderer *)r
825 {
826     NSEnumerator *viewEnumerator = [(NSMutableDictionary *)animatingRenderers keyEnumerator];
827     NSView *view;
828     while ((view = [viewEnumerator nextObject])) {
829         NSMutableSet *renderers = (NSMutableSet *)CFDictionaryGetValue (animatingRenderers, view);
830         [renderers removeObject:r];
831         if ([renderers count] == 0) {
832             CFDictionaryRemoveValue (animatingRenderers, view);
833         }
834     }
835     
836     if (animatingRenderers && CFDictionaryGetCount(animatingRenderers) == 0) {
837         [activeAnimations removeObject:self];
838         [self _stopAnimation];
839     }
840 }
841
842 - (void)resetAnimation
843 {
844     [self _stopAnimation];
845     currentFrame = 0;
846     repetitionsComplete = 0;
847     animationFinished = NO;
848 }
849
850 - (void)_stopAnimation
851 {
852     // This timer is used to animate all occurences of this image.  Don't invalidate
853     // the timer unless all renderers have stopped drawing.
854     [frameTimer invalidate];
855     [frameTimer release];
856     frameTimer = nil;
857 }
858
859 - (void)_nextFrame:(id)context
860 {
861     // Release the timer that just fired.
862     [frameTimer release];
863     frameTimer = nil;
864     
865     currentFrame++;
866     if (currentFrame >= [self numberOfImages]) {
867         repetitionsComplete += 1;
868         if ([self _repetitionCount] && repetitionsComplete >= [self _repetitionCount]) {
869             animationFinished = YES;
870             currentFrame--;
871             return;
872         }
873         currentFrame = 0;
874     }
875     
876     NSEnumerator *viewEnumerator = [(NSMutableDictionary *)animatingRenderers keyEnumerator];
877     NSView *view;
878     while ((view = [viewEnumerator nextObject])) {
879         NSMutableSet *renderers = [(NSMutableDictionary *)animatingRenderers objectForKey:view];
880         WebImageRenderer *renderer;
881         NSEnumerator *rendererEnumerator = [renderers objectEnumerator];
882         while ((renderer = [rendererEnumerator nextObject])) {
883             [view setNeedsDisplayInRect:[renderer targetAnimationRect]];
884         }
885     }
886 }
887
888 - (BOOL)shouldAnimate
889 {
890     return [self numberOfImages] > 1 && ![self isAnimationFinished];
891 }
892
893 - (void)animate
894 {
895     if (frameTimer && [frameTimer isValid])
896         return;
897     frameTimer = [[NSTimer scheduledTimerWithTimeInterval:[self _frameDuration]
898                                                     target:self
899                                                   selector:@selector(_nextFrame:)
900                                                   userInfo:nil
901                                                    repeats:NO] retain];
902 }
903
904 -(void)_createPDFWithData:(NSData *)data
905 {
906     if (!_PDFDoc) {
907         _PDFDoc = [[WebPDFDocument alloc] initWithData:data];
908     }
909 }
910
911 - (CGPDFDocumentRef)_PDFDocumentRef
912 {
913     return [_PDFDoc documentRef];
914 }
915
916 - (void)_PDFDrawInContext:(CGContextRef)context
917 {
918     CGPDFDocumentRef document = [self _PDFDocumentRef];
919     if (document != NULL) {
920         CGRect       mediaBox = [_PDFDoc mediaBox];
921         
922         CGContextSaveGState(context);
923         // Rotate translate image into position according to doc properties.
924         [_PDFDoc adjustCTM:context];    
925
926         // Media box may have non-zero origin which we ignore. CGPDFDocumentShowPage pages start
927         // at 1, not 0.
928         CGContextDrawPDFDocument(context, CGRectMake(0, 0, mediaBox.size.width, mediaBox.size.height), document, 1);
929
930         CGContextRestoreGState(context);
931     }
932 }
933
934 - (BOOL)_PDFDrawFromRect:(NSRect)srcRect toRect:(NSRect)dstRect operation:(CGCompositeOperation)op alpha:(float)alpha flipped:(BOOL)flipped context:(CGContextRef)context
935 {
936     float hScale, vScale;
937
938     CGContextSaveGState(context);
939
940     CGContextSetCompositeOperation (context, op);
941
942     // Scale and translate so the document is rendered in the correct location.
943     hScale = dstRect.size.width  / srcRect.size.width;
944     vScale = dstRect.size.height / srcRect.size.height;
945
946     CGContextTranslateCTM (context, dstRect.origin.x - srcRect.origin.x * hScale, dstRect.origin.y - srcRect.origin.y * vScale);
947     CGContextScaleCTM (context, hScale, vScale);
948
949     // Reverse if flipped image.
950     if (flipped) {
951         CGContextScaleCTM(context, 1, -1);
952         CGContextTranslateCTM (context, 0, -dstRect.size.height);
953     }
954
955     // Clip to destination in case we are imaging part of the source only
956     CGContextClipToRect(context, CGRectIntegral(*(CGRect*)&srcRect));
957
958     // and draw
959     [self _PDFDrawInContext:context];
960
961     // done with our fancy transforms
962     CGContextRestoreGState(context);
963
964     return YES;
965 }
966
967 @end
968
969 #endif