2007-11-21 Eric Seidel <eric@webkit.org>
[WebKit-https.git] / WebKitTools / DumpRenderTree / mac / PixelDumpSupport.mm
1 /*
2  * Copyright (C) 2005, 2006, 2007 Apple, Inc.  All rights reserved.
3  *           (C) 2007 Graham Dennis (graham.dennis@gmail.com)
4  *           (C) 2007 Eric Seidel <eric@webkit.org>
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  *
10  * 1.  Redistributions of source code must retain the above copyright
11  *     notice, this list of conditions and the following disclaimer. 
12  * 2.  Redistributions in binary form must reproduce the above copyright
13  *     notice, this list of conditions and the following disclaimer in the
14  *     documentation and/or other materials provided with the distribution. 
15  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
16  *     its contributors may be used to endorse or promote products derived
17  *     from this software without specific prior written permission. 
18  *
19  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30  
31 #import "PixelDumpSupport.h"
32
33 #import "DumpRenderTree.h"
34 #import "LayoutTestController.h"
35
36 #import <AppKit/AppKit.h>
37 #import <ApplicationServices/ApplicationServices.h> // for CMSetDefaultProfileBySpace
38 #import <WebKit/WebKit.h>
39 #import <WebKit/WebDocumentPrivate.h>
40
41 #define COMMON_DIGEST_FOR_OPENSSL
42 #import <CommonCrypto/CommonDigest.h>               // for MD5 functions
43
44 static unsigned char* screenCaptureBuffer;
45
46 static CMProfileRef currentColorProfile = 0;
47 static CGColorSpaceRef sharedColorSpace;
48
49 void restoreColorSpace(int ignored)
50 {
51     // This is used as a signal handler, and thus the calls into ColorSync are unsafe
52     // But we might as well try to restore the user's color profile, we're going down anyway...
53     if (currentColorProfile) {
54         // This call is deprecated in Leopard, but there appears to be no replacement.
55         int error = CMSetDefaultProfileByUse(cmDisplayUse, currentColorProfile);
56         if (error)
57             fprintf(stderr, "Failed to retore previous color profile!  You may need to open System Preferences : Displays : Color and manually restore your color settings.  (Error: %i)", error);
58         currentColorProfile = 0;
59     }
60 }
61
62 static void setDefaultColorProfileToRGB()
63 {
64     CMProfileRef genericProfile = (CMProfileRef)[[NSColorSpace genericRGBColorSpace] colorSyncProfile];
65     CMProfileRef previousProfile;
66     int error = CMGetDefaultProfileByUse(cmDisplayUse, &previousProfile);
67     if (error) {
68         fprintf(stderr, "Failed to get current color profile.  I will not be able to restore your current profile, thus I'm not changing it.  Many pixel tests may fail as a result.  (Error: %i)\n", error);
69         return;
70     }
71     if (previousProfile == genericProfile)
72         return;
73     CFStringRef previousProfileName;
74     CFStringRef genericProfileName;
75     char previousProfileNameString[1024];
76     char genericProfileNameString[1024];
77     CMCopyProfileDescriptionString(previousProfile, &previousProfileName);
78     CMCopyProfileDescriptionString(genericProfile, &genericProfileName);
79     CFStringGetCString(previousProfileName, previousProfileNameString, sizeof(previousProfileNameString), kCFStringEncodingUTF8);
80     CFStringGetCString(genericProfileName, genericProfileNameString, sizeof(previousProfileNameString), kCFStringEncodingUTF8);
81     CFRelease(genericProfileName);
82     CFRelease(previousProfileName);
83     
84     fprintf(stderr, "\n\nWARNING: Temporarily changing your system color profile from \"%s\" to \"%s\".\n", previousProfileNameString, genericProfileNameString);
85     fprintf(stderr, "This allows the WebKit pixel-based regression tests to have consistent color values across all machines.\n");
86     fprintf(stderr, "The colors on your screen will change for the duration of the testing.\n\n");
87     
88     if ((error = CMSetDefaultProfileByUse(cmDisplayUse, genericProfile)))
89         fprintf(stderr, "Failed to set color profile to \"%s\"! Many pixel tests will fail as a result.  (Error: %i)",
90             genericProfileNameString, error);
91     else {
92         currentColorProfile = previousProfile;
93         signal(SIGINT, restoreColorSpace);
94         signal(SIGHUP, restoreColorSpace);
95         signal(SIGTERM, restoreColorSpace);
96     }
97 }
98
99 void initializeColorSpaceAndScreeBufferForPixelTests()
100 {
101     setDefaultColorProfileToRGB();
102     screenCaptureBuffer = (unsigned char *)malloc(maxViewHeight * maxViewWidth * 4);
103     sharedColorSpace = CGColorSpaceCreateDeviceRGB();
104 }
105
106 /* Hashes a bitmap and returns a text string for comparison and saving to a file */
107 static NSString *md5HashStringForBitmap(CGImageRef bitmap)
108 {
109     MD5_CTX md5Context;
110     unsigned char hash[16];
111     
112     unsigned bitsPerPixel = CGImageGetBitsPerPixel(bitmap);
113     assert(bitsPerPixel == 32); // ImageDiff assumes 32 bit RGBA, we must as well.
114     unsigned bytesPerPixel = bitsPerPixel / 8;
115     unsigned pixelsHigh = CGImageGetHeight(bitmap);
116     unsigned pixelsWide = CGImageGetWidth(bitmap);
117     unsigned bytesPerRow = CGImageGetBytesPerRow(bitmap);
118     assert(bytesPerRow >= (pixelsWide * bytesPerPixel));
119     
120     MD5_Init(&md5Context);
121     unsigned char *bitmapData = screenCaptureBuffer;
122     for (unsigned row = 0; row < pixelsHigh; row++) {
123         MD5_Update(&md5Context, bitmapData, pixelsWide * bytesPerPixel);
124         bitmapData += bytesPerRow;
125     }
126     MD5_Final(hash, &md5Context);
127     
128     char hex[33] = "";
129     for (int i = 0; i < 16; i++) {
130         snprintf(hex, 33, "%s%02x", hex, hash[i]);
131     }
132     
133     return [NSString stringWithUTF8String:hex];
134 }
135
136 void dumpWebViewAsPixelsAndCompareWithExpected(NSString* currentTest, bool forceAllTestsToDumpPixels)
137 {
138     if (!layoutTestController->dumpAsText() && !layoutTestController->dumpDOMAsWebArchive() && !layoutTestController->dumpSourceAsWebArchive()) {
139         // grab a bitmap from the view
140         WebView* view = [mainFrame webView];
141         NSSize webViewSize = [view frame].size;
142         
143         CGContextRef cgContext = CGBitmapContextCreate(screenCaptureBuffer, static_cast<size_t>(webViewSize.width), static_cast<size_t>(webViewSize.height), 8, static_cast<size_t>(webViewSize.width) * 4, sharedColorSpace, kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedLast);
144         
145         NSGraphicsContext* savedContext = [[[NSGraphicsContext currentContext] retain] autorelease];
146         NSGraphicsContext* nsContext = [NSGraphicsContext graphicsContextWithGraphicsPort:cgContext flipped:NO];
147         [NSGraphicsContext setCurrentContext:nsContext];
148         
149         if (!layoutTestController->testRepaint()) {
150             NSBitmapImageRep *imageRep;
151             [view displayIfNeeded];
152             [view lockFocus];
153             imageRep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:[view frame]];
154             [view unlockFocus];
155             [imageRep draw];
156             [imageRep release];
157         } else if (!layoutTestController->testRepaintSweepHorizontally()) {
158             NSRect line = NSMakeRect(0, 0, webViewSize.width, 1);
159             while (line.origin.y < webViewSize.height) {
160                 [view displayRectIgnoringOpacity:line inContext:nsContext];
161                 line.origin.y++;
162             }
163         } else {
164             NSRect column = NSMakeRect(0, 0, 1, webViewSize.height);
165             while (column.origin.x < webViewSize.width) {
166                 [view displayRectIgnoringOpacity:column inContext:nsContext];
167                 column.origin.x++;
168             }
169         }
170         if (layoutTestController->dumpSelectionRect()) {
171             NSView *documentView = [[mainFrame frameView] documentView];
172             if ([documentView conformsToProtocol:@protocol(WebDocumentSelection)]) {
173                 [[NSColor redColor] set];
174                 [NSBezierPath strokeRect:[documentView convertRect:[(id <WebDocumentSelection>)documentView selectionRect] fromView:nil]];
175             }
176         }
177         
178         [NSGraphicsContext setCurrentContext:savedContext];
179         
180         CGImageRef bitmapImage = CGBitmapContextCreateImage(cgContext);
181         CGContextRelease(cgContext);
182         
183         // compute the actual hash to compare to the expected image's hash
184         NSString *actualHash = md5HashStringForBitmap(bitmapImage);
185         printf("\nActualHash: %s\n", [actualHash UTF8String]);
186         
187         BOOL dumpImage;
188         if (forceAllTestsToDumpPixels)
189             dumpImage = YES;
190         else {
191             // FIXME: It's unfortunate that we hardcode the file naming scheme here.
192             // At one time, the perl script had all the knowledge about file layout.
193             // Some day we should restore that setup by passing in more parameters to this tool
194             // or returning more information from the tool to the perl script
195             NSString *baseTestPath = [currentTest stringByDeletingPathExtension];
196             NSString *baselineHashPath = [baseTestPath stringByAppendingString:@"-expected.checksum"];
197             NSString *baselineHash = [NSString stringWithContentsOfFile:baselineHashPath encoding:NSUTF8StringEncoding error:nil];
198             NSString *baselineImagePath = [baseTestPath stringByAppendingString:@"-expected.png"];
199             
200             printf("BaselineHash: %s\n", [baselineHash UTF8String]);
201             
202             /// send the image to stdout if the hash mismatches or if there's no file in the file system
203             dumpImage = ![baselineHash isEqualToString:actualHash] || access([baselineImagePath fileSystemRepresentation], F_OK) != 0;
204         }
205         
206         if (dumpImage) {
207             CFMutableDataRef imageData = CFDataCreateMutable(0, 0);
208             CGImageDestinationRef imageDest = CGImageDestinationCreateWithData(imageData, CFSTR("public.png"), 1, 0);
209             CGImageDestinationAddImage(imageDest, bitmapImage, 0);
210             CGImageDestinationFinalize(imageDest);
211             CFRelease(imageDest);
212             printf("Content-length: %lu\n", CFDataGetLength(imageData));
213             fwrite(CFDataGetBytePtr(imageData), 1, CFDataGetLength(imageData), stdout);
214             CFRelease(imageData);
215         }
216         
217         CGImageRelease(bitmapImage);
218     }
219     
220     printf("#EOF\n");
221 }