Changes by Ben Lamonica and Eric Seidel, reviewed mostly by Eric and
authormjs <mjs@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 18 Aug 2005 05:59:07 +0000 (05:59 +0000)
committermjs <mjs@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 18 Aug 2005 05:59:07 +0000 (05:59 +0000)
somewhat by me, and also tweaked by me a little bit.

- better support for pixel-dumping
- use checksums of the images so the tests are fast
- change output format to make the tests run faster
- don't dump pixel results for tests that dump as text

        * DumpKCanvasTree/DumpKCanvasTree.m:
        (main):
        (dumpRenderTree):
        (md5HashStringForBitmap):
        (dumpPixelTests):
        (constrainSizeToMaximum):
        (getBitmapImageRepForSVGDocument):
        * DumpKCanvasTree/DumpKCanvasTree.xcodeproj/project.pbxproj:
        * DumpRenderTree/DumpRenderTree.m:
        (main):
        (dump):
        (dumpRenderTree):
        (md5HashStringForBitmap):
        * DumpRenderTree/DumpRenderTree.xcodeproj/project.pbxproj:
        * Scripts/run-webkit-tests:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@10249 268f45cc-cd09-0410-ab3c-d52691b4dbfc

WebKitTools/ChangeLog
WebKitTools/DumpKCanvasTree/DumpKCanvasTree.m
WebKitTools/DumpKCanvasTree/DumpKCanvasTree.xcodeproj/project.pbxproj
WebKitTools/DumpRenderTree/DumpRenderTree.m
WebKitTools/DumpRenderTree/DumpRenderTree.xcodeproj/project.pbxproj
WebKitTools/Scripts/run-webkit-tests

index f0227a8..b404083 100644 (file)
@@ -1,3 +1,38 @@
+2005-08-17  Maciej Stachowiak  <mjs@apple.com>
+
+        Changes by Ben Lamonica and Eric Seidel, reviewed mostly by Eric and
+       somewhat by me, and also tweaked by me a little bit.
+
+       - better support for pixel-dumping
+       - use checksums of the images so the tests are fast
+       - change output format to make the tests run faster
+       - don't dump pixel results for tests that dump as text
+
+        * DumpKCanvasTree/DumpKCanvasTree.m:
+        (main):
+        (dumpRenderTree):
+        (md5HashStringForBitmap):
+        (dumpPixelTests):
+        (constrainSizeToMaximum):
+        (getBitmapImageRepForSVGDocument):
+        * DumpKCanvasTree/DumpKCanvasTree.xcodeproj/project.pbxproj:
+        * DumpRenderTree/DumpRenderTree.m:
+        (main):
+        (dump):
+        (dumpRenderTree):
+        (md5HashStringForBitmap):
+        * DumpRenderTree/DumpRenderTree.xcodeproj/project.pbxproj:
+        * Scripts/run-webkit-tests:
+
+2005-08-17  Maciej Stachowiak  <mjs@apple.com>
+
+        Reviewed by Darin.
+
+       - hacked DumpRenderTree to make the scrollbars appear and disappear properly.
+
+        * DumpRenderTree/DumpRenderTree.m:
+        (main):
+
 2005-08-14  Oliver Hunt  <ojh16@student.canterbury.ac.nz>
 
         Reviewed and landed by Darin.
index f48dfec..91c36fe 100644 (file)
  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+#import <getopt.h>
 #import <WebCore+SVG/DrawDocumentPrivate.h>
+#import <WebCore+SVG/DrawView.h>
+
+#define COMMON_DIGEST_FOR_OPENSSL
+#import <CommonCrypto/CommonDigest.h>
+
+#define maxHeightOption     1
+#define maxWidthOption      2
 
 static void dumpRenderTree(const char *filename);
+void dumpPixelTests(DrawDocument *document, NSString *svgPath);
+static NSBitmapImageRep *getBitmapImageRepForSVGDocument(DrawDocument *document);
+NSString *md5HashStringForBitmap(NSBitmapImageRep *bitmap);
+
+/* global variables */
+int pixelTests = 0;  // run the pixel comparison tests?
+NSNumber *maxWidth = nil;
+NSNumber *maxHeight = nil;
+DrawView *view = nil;
 
 int main(int argc, const char *argv[])
 {
     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+    int ch = 0;
+
+    // define the commandline options that this program accepts
+    static struct option longopts[] = {
+        { "pixel-tests",    no_argument,        &pixelTests,    1               },
+        { "max-height",     required_argument,  NULL,           maxHeightOption },
+        { "max-width",      required_argument,  NULL,           maxWidthOption  },
+        { NULL,             0,                  NULL,           0               }
+    };
+    
+    // parse the options that the program receives
+    while ((ch = getopt_long(argc, (char * const *) argv, "", longopts, NULL)) != -1) {
+        switch (ch) {
+            case maxHeightOption:
+                maxHeight = [NSNumber numberWithInt:[[NSString stringWithUTF8String:optarg] intValue]];
+                break;
+            case maxWidthOption:
+                maxWidth = [NSNumber numberWithInt:[[NSString stringWithUTF8String:optarg] intValue]];
+                break;
+            case '?':   // unknown or ambiguous option
+            case ':':   // missing argument
+                exit(1);
+                break;
+        }
+    }
+
+    // remove the options that were parsed by getopt_long
+    argc -= optind;
+    argv += optind;
+
+    // if we are running the pixel comparison tests, instantiate our DrawView
+    if (pixelTests)
+        view = [[DrawView alloc] initWithFrame:NSZeroRect];
 
-    if (argc == 2 && strcmp(argv[1], "-") == 0) {
+    if (argc == 1 && strcmp(argv[0], "-") == 0) {
         char filenameBuffer[2048];
+
         while (fgets(filenameBuffer, sizeof(filenameBuffer), stdin)) {
             char *newLineCharacter = strchr(filenameBuffer, '\n');
             if (newLineCharacter) {
@@ -52,6 +103,7 @@ int main(int argc, const char *argv[])
         }
     }
 
+    [view release];
     [pool release];
     return 0;
 }
@@ -72,7 +124,89 @@ static void dumpRenderTree(const char *filename)
         puts("error");
     else
         fputs([result UTF8String], stdout);
+   
+    // generate the image and perform pixel tests 
+    if (pixelTests) {
+        fputs("#EOF\n", stdout);
+        dumpPixelTests(document, svgPath);
+    }
     
     [document release];
     [pool release];
 }
+
+NSString *md5HashStringForBitmap(NSBitmapImageRep *bitmap)
+{
+    MD5_CTX md5Context;
+    unsigned char hash[16];
+
+    MD5_Init(&md5Context);
+    MD5_Update(&md5Context, [bitmap bitmapData], [bitmap bytesPerPlane]);
+    MD5_Final(hash, &md5Context);
+    
+    char hex[33] = "";
+    for (int i = 0; i < 16; i++) {
+       snprintf(hex, 33, "%s%02x", hex, hash[i]);
+    }
+
+    return [NSString stringWithUTF8String:hex];
+}
+
+void dumpPixelTests(DrawDocument *document, NSString *svgPath)
+{
+    // Creates an image from the document object
+    NSBitmapImageRep *actualBitmap = getBitmapImageRepForSVGDocument(document);
+
+    // lookup the expected hash value for that image
+    NSString *baseTestPath = [svgPath stringByDeletingPathExtension];
+    NSString *baselineHashPath = [baseTestPath stringByAppendingString:@"-expected.checksum"];
+    NSString *baselineHash = [NSString stringWithContentsOfFile:baselineHashPath encoding:NSUTF8StringEncoding error:nil];
+
+    // compute the hash for the rendered image, print both.
+    NSString *actualHash = md5HashStringForBitmap(actualBitmap);
+    fprintf(stdout,"ActualHash: %s\n", [actualHash UTF8String]);
+    fprintf(stdout,"BaselineHash: %s\n", [baselineHash UTF8String]);
+    
+    // if the hashes don't match, then perform more pixel tests
+    if ([baselineHash isEqualToString:actualHash] == NO) {
+        // send the PNG compressed data through STDOUT
+        NSData *svgPNGData = [actualBitmap representationUsingType:NSPNGFileType properties:nil];
+        fprintf(stdout, "Content-length: %d\n", [svgPNGData length]);
+        fwrite([svgPNGData bytes], [svgPNGData length], 1, stdout);
+    }
+}
+
+static NSSize constrainSizeToMaximum(NSSize boundsSize)
+{
+    // Limit the maximum size and default the size to 500x500 if not specified
+    if (maxWidth && boundsSize.width > [maxWidth intValue]) {
+        // resize the height so that the aspect ratio remains the same
+        boundsSize.height = (boundsSize.height / boundsSize.width) * [maxWidth intValue];
+        boundsSize.width = [maxWidth intValue];
+    }
+    else if (boundsSize.width <= 0)
+        boundsSize.width = 500;
+        
+    if (maxHeight && boundsSize.height > [maxHeight intValue]) {
+        // resize the width so that the aspect ratio reamins the same
+        boundsSize.width = (boundsSize.width / boundsSize.height) * [maxHeight intValue];
+        boundsSize.height = [maxHeight intValue];
+    }
+    else if (boundsSize.height <= 0)
+        boundsSize.height = 500;
+    return boundsSize;
+}
+
+static NSBitmapImageRep *getBitmapImageRepForSVGDocument(DrawDocument *document)
+{
+    [view setDocument:document];
+    [view sizeToFitViewBox];
+    NSSize boundsSize = constrainSizeToMaximum([view bounds].size);
+    [view setFrameSize:boundsSize];
+    
+    // tell that view to render
+    NSBitmapImageRep *imageRep = [view bitmapImageRepForCachingDisplayInRect:[view bounds]];
+    [view cacheDisplayInRect:[view bounds] toBitmapImageRep:imageRep];
+
+    return imageRep;
+}
index b22f48b..fd9159f 100644 (file)
                A810DEDA08A40F3100333D98 /* Development */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
+                               GCC_C_LANGUAGE_STANDARD = gnu99;
+                               GCC_TREAT_WARNINGS_AS_ERRORS = YES;
+                               GCC_WARN_UNUSED_FUNCTION = YES;
+                               GCC_WARN_UNUSED_VARIABLE = YES;
                        };
                        name = Development;
                };
                A810DEDB08A40F3100333D98 /* Deployment */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
+                               GCC_C_LANGUAGE_STANDARD = gnu99;
+                               GCC_TREAT_WARNINGS_AS_ERRORS = YES;
+                               GCC_WARN_UNUSED_FUNCTION = YES;
+                               GCC_WARN_UNUSED_VARIABLE = YES;
                        };
                        name = Deployment;
                };
                A810DEDC08A40F3100333D98 /* Default */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
+                               GCC_C_LANGUAGE_STANDARD = gnu99;
+                               GCC_TREAT_WARNINGS_AS_ERRORS = YES;
+                               GCC_WARN_UNUSED_FUNCTION = YES;
+                               GCC_WARN_UNUSED_VARIABLE = YES;
                        };
                        name = Default;
                };
index d61fe90..2d40c1c 100644 (file)
@@ -40,6 +40,8 @@
 #import <WebKit/WebPreferences.h>
 #import <WebKit/WebView.h>
 
+#define COMMON_DIGEST_FOR_OPENSSL
+#import <CommonCrypto/CommonDigest.h>
 #import <getopt.h>
 
 @interface WaitUntilDoneDelegate : NSObject
@@ -52,6 +54,8 @@
 @end
 
 static void dumpRenderTree(const char *filename);
+NSString *md5HashStringForBitmap(NSBitmapImageRep *bitmap);
+
 
 static volatile BOOL done;
 static WebFrame *frame;
@@ -62,6 +66,7 @@ static BOOL dumpTitleChanges;
 static int dumpPixels = NO;
 static int dumpTree = YES;
 static BOOL printSeparators;
+static NSString *currentTest = nil;
 
 int main(int argc, const char *argv[])
 {
@@ -84,8 +89,7 @@ int main(int argc, const char *argv[])
     struct option options[] = {
         {"width", required_argument, NULL, 'w'},
         {"height", required_argument, NULL, 'h'},
-        {"bitmap", no_argument, &dumpPixels, YES},
-        {"nobitmap", no_argument, &dumpPixels, NO},
+        {"pixel-tests", no_argument, &dumpPixels, YES},
         {"tree", no_argument, &dumpTree, YES},
         {"notree", no_argument, &dumpTree, NO},
         {NULL, 0, NULL, 0}
@@ -132,21 +136,28 @@ int main(int argc, const char *argv[])
     [webView setUIDelegate:delegate];
     frame = [webView mainFrame];
     
+    // For reasons that are not entirely clear, the following pair of calls makes WebView handle its
+    // dynamic scrollbars properly. Without it, every frame will always have scrollbars.
+    NSBitmapImageRep *imageRep = [webView bitmapImageRepForCachingDisplayInRect:[webView bounds]];
+    [webView cacheDisplayInRect:[webView bounds] toBitmapImageRep:imageRep];
+
     if (argc == optind+1 && strcmp(argv[optind], "-") == 0) {
         char filenameBuffer[2048];
         printSeparators = YES;
         while (fgets(filenameBuffer, sizeof(filenameBuffer), stdin)) {
             char *newLineCharacter = strchr(filenameBuffer, '\n');
-            if (newLineCharacter) {
+            if (newLineCharacter)
                 *newLineCharacter = '\0';
-            }
+            
+            if (strlen(filenameBuffer) == 0)
+                continue;
+                
             dumpRenderTree(filenameBuffer);
             fflush(stdout);
         }
     } else {
-        int i;
         printSeparators = (optind < argc-1 || (dumpPixels && dumpTree));
-        for (i = optind; i != argc; ++i) {
+        for (int i = optind; i != argc; ++i) {
             dumpRenderTree(argv[i]);
         }
     }
@@ -171,32 +182,45 @@ static void dump(void)
     if (dumpTree) {
         if (dumpAsText) {
             DOMDocument *document = [frame DOMDocument];
-            if ([document isKindOfClass:[DOMHTMLDocument class]]) {
+            if ([document isKindOfClass:[DOMHTMLDocument class]])
                 result = [[[(DOMHTMLDocument *)document body] innerText] stringByAppendingString:@"\n"];
-            }
-        } else {
+        } else
             result = [frame renderTreeAsExternalRepresentation];
-        }
-        if (!result) {
+        
+        if (!result)
             puts("error");
-        } else {
+        else
             fputs([result UTF8String], stdout);
-        }
+        
         if (printSeparators)
             puts("#EOF");
     }
     
     if (dumpPixels) {
-        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
-        WebView *view = [frame webView];
-        NSBitmapImageRep *imageRep = [view bitmapImageRepForCachingDisplayInRect:[view frame]];
-        NSData *imageData;
-        [view cacheDisplayInRect:[view frame] toBitmapImageRep:imageRep];
-        imageData = [imageRep representationUsingType:NSPNGFileType properties:nil];
-        if (printSeparators)
-            printf("%d\n", [imageData length]);
-        fwrite([imageData bytes], 1, [imageData length], stdout);
-        [pool release];
+        if (!dumpAsText) {
+            NSString *baseTestPath = [currentTest stringByDeletingPathExtension];
+            NSString *baselineHashPath = [baseTestPath stringByAppendingString:@"-expected.checksum"];
+            NSString *baselineHash = [NSString stringWithContentsOfFile:baselineHashPath encoding:NSUTF8StringEncoding error:nil];
+            
+            // grab a bitmap from the view
+            WebView *view = [frame webView];
+            NSBitmapImageRep *imageRep = [view bitmapImageRepForCachingDisplayInRect:[view frame]];
+            [view cacheDisplayInRect:[view frame] toBitmapImageRep:imageRep];
+            
+            // has the actual hash to compare to the expected image's hash
+            NSString *actualHash = md5HashStringForBitmap(imageRep);
+            printf("\nActualHash: %s\n", [actualHash UTF8String]);
+            printf("BaselineHash: %s\n", [baselineHash UTF8String]);
+            
+            // if the hashes don't match, send image back to stdout for diff comparision
+            if ([baselineHash isEqualToString:actualHash] == NO) {            
+                NSData *imageData = [imageRep representationUsingType:NSPNGFileType properties:nil];
+                printf("Content-length: %d\n", [imageData length]);
+                fwrite([imageData bytes], 1, [imageData length], stdout);
+            }
+        }
+
+        printf("#EOF\n");
     }
 
     done = YES;
@@ -422,11 +446,13 @@ static void dump(void)
 
 static void dumpRenderTree(const char *filename)
 {
+
     CFStringRef filenameString = CFStringCreateWithCString(NULL, filename, kCFStringEncodingUTF8);
     if (filenameString == NULL) {
         fprintf(stderr, "can't parse filename as UTF-8\n");
         return;
     }
+
     CFURLRef URL = CFURLCreateWithFileSystemPath(NULL, filenameString, kCFURLPOSIXPathStyle, FALSE);
     if (URL == NULL) {
         fprintf(stderr, "can't turn %s into a CFURL\n", filename);
@@ -438,6 +464,7 @@ static void dumpRenderTree(const char *filename)
     waitToDump = NO;
     dumpAsText = NO;
     dumpTitleChanges = NO;
+    currentTest = (NSString *) filenameString;
 
     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
     [frame loadRequest:[NSURLRequest requestWithURL:(NSURL *)URL]];
@@ -449,3 +476,22 @@ static void dumpRenderTree(const char *filename)
     }
     [[frame webView] setSelectedDOMRange:nil affinity:NSSelectionAffinityDownstream];
 }
+
+/* Hashes a bitmap and returns a text string for comparison and saving to a file */
+NSString *md5HashStringForBitmap(NSBitmapImageRep *bitmap)
+{
+    MD5_CTX md5Context;
+    unsigned char hash[16];
+
+    MD5_Init(&md5Context);
+    MD5_Update(&md5Context, [bitmap bitmapData], [bitmap bytesPerPlane]);
+    MD5_Final(hash, &md5Context);
+    
+    char hex[33] = "";
+    for (int i = 0; i < 16; i++) {
+       snprintf(hex, 33, "%s%02x", hex, hash[i]);
+    }
+
+    return [NSString stringWithUTF8String:hex];
+}
+
index 40a3a70..39e53e4 100644 (file)
@@ -6,13 +6,44 @@
        objectVersion = 42;
        objects = {
 
+/* Begin PBXAggregateTarget section */
+               A84F608D08B1370600E9745F /* All */ = {
+                       isa = PBXAggregateTarget;
+                       buildConfigurationList = A84F609208B1371400E9745F /* Build configuration list for PBXAggregateTarget "All" */;
+                       buildPhases = (
+                       );
+                       buildSettings = {
+                               OPTIMIZATION_CFLAGS = "";
+                               OTHER_CFLAGS = "";
+                               OTHER_LDFLAGS = "";
+                               OTHER_REZFLAGS = "";
+                               PRODUCT_NAME = All;
+                               SECTORDER_FLAGS = "";
+                               WARNING_CFLAGS = (
+                                       "-Wmost",
+                                       "-Wno-four-char-constants",
+                                       "-Wno-unknown-pragmas",
+                               );
+                       };
+                       dependencies = (
+                               A84F609108B1370E00E9745F /* PBXTargetDependency */,
+                               A84F608F08B1370E00E9745F /* PBXTargetDependency */,
+                       );
+                       name = All;
+                       productName = All;
+               };
+/* End PBXAggregateTarget section */
+
 /* Begin PBXBuildFile section */
                9340994C08540CAE007F3BC8 /* DumpRenderTreePrefix.h in Headers */ = {isa = PBXBuildFile; fileRef = 32A70AAB03705E1F00C91783 /* DumpRenderTreePrefix.h */; };
                9340994E08540CAE007F3BC8 /* DumpRenderTree.m in Sources */ = {isa = PBXBuildFile; fileRef = 08FB7796FE84155DC02AAC07 /* DumpRenderTree.m */; settings = {ATTRIBUTES = (); }; };
-               9340995008540CAE007F3BC8 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08FB779EFE84155DC02AAC07 /* Foundation.framework */; };
                9340995108540CAE007F3BC8 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9335435F03D75502008635CE /* WebKit.framework */; };
-               9340995208540CAE007F3BC8 /* WebCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9335436503D7553D008635CE /* WebCore.framework */; };
-               9340995308540CAE007F3BC8 /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9335436903D75557008635CE /* JavaScriptCore.framework */; };
+               A817090008B163EF00CCB9FB /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A84F608908B136DA00E9745F /* Cocoa.framework */; };
+               A817090208B1643800CCB9FB /* WebCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A817090108B1643800CCB9FB /* WebCore.framework */; };
+               A817090408B164D300CCB9FB /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A817090308B164D300CCB9FB /* JavaScriptCore.framework */; };
+               A84F608A08B136DA00E9745F /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A84F608908B136DA00E9745F /* Cocoa.framework */; };
+               B5A752A008AF5CD400138E45 /* ImageDiff.m in Sources */ = {isa = PBXBuildFile; fileRef = B5A7525808AF4A3600138E45 /* ImageDiff.m */; };
+               B5A752A208AF5D1F00138E45 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A752A108AF5D1F00138E45 /* QuartzCore.framework */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXBuildStyle section */
                };
 /* End PBXBuildStyle section */
 
+/* Begin PBXContainerItemProxy section */
+               A84F608E08B1370E00E9745F /* PBXContainerItemProxy */ = {
+                       isa = PBXContainerItemProxy;
+                       containerPortal = 08FB7793FE84155DC02AAC07 /* Project object */;
+                       proxyType = 1;
+                       remoteGlobalIDString = B5A7525A08AF4A4A00138E45;
+                       remoteInfo = ImageDiff;
+               };
+               A84F609008B1370E00E9745F /* PBXContainerItemProxy */ = {
+                       isa = PBXContainerItemProxy;
+                       containerPortal = 08FB7793FE84155DC02AAC07 /* Project object */;
+                       proxyType = 1;
+                       remoteGlobalIDString = 9340994A08540CAE007F3BC8;
+                       remoteInfo = DumpRenderTree;
+               };
+/* End PBXContainerItemProxy section */
+
 /* Begin PBXFileReference section */
                08FB7796FE84155DC02AAC07 /* DumpRenderTree.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DumpRenderTree.m; sourceTree = "<group>"; };
-               08FB779EFE84155DC02AAC07 /* Foundation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = "<absolute>"; };
                32A70AAB03705E1F00C91783 /* DumpRenderTreePrefix.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DumpRenderTreePrefix.h; sourceTree = "<group>"; };
                9335435F03D75502008635CE /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = WebKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
-               9335436503D7553D008635CE /* WebCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = WebCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
-               9335436903D75557008635CE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = JavaScriptCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
                9340995408540CAF007F3BC8 /* DumpRenderTree */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = DumpRenderTree; sourceTree = BUILT_PRODUCTS_DIR; };
+               A817090108B1643800CCB9FB /* WebCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = WebCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+               A817090308B164D300CCB9FB /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = JavaScriptCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+               A84F608908B136DA00E9745F /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = "<absolute>"; };
+               B5A7525808AF4A3600138E45 /* ImageDiff.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = ImageDiff.m; sourceTree = "<group>"; };
+               B5A7526708AF4A4A00138E45 /* ImageDiff */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = ImageDiff; sourceTree = BUILT_PRODUCTS_DIR; };
+               B5A752A108AF5D1F00138E45 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = /System/Library/Frameworks/QuartzCore.framework; sourceTree = "<absolute>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
                        isa = PBXFrameworksBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
-                               9340995008540CAE007F3BC8 /* Foundation.framework in Frameworks */,
                                9340995108540CAE007F3BC8 /* WebKit.framework in Frameworks */,
-                               9340995208540CAE007F3BC8 /* WebCore.framework in Frameworks */,
-                               9340995308540CAE007F3BC8 /* JavaScriptCore.framework in Frameworks */,
+                               A84F608A08B136DA00E9745F /* Cocoa.framework in Frameworks */,
+                               A817090208B1643800CCB9FB /* WebCore.framework in Frameworks */,
+                               A817090408B164D300CCB9FB /* JavaScriptCore.framework in Frameworks */,
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
+               B5A7525F08AF4A4A00138E45 /* Frameworks */ = {
+                       isa = PBXFrameworksBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                               B5A752A208AF5D1F00138E45 /* QuartzCore.framework in Frameworks */,
+                               A817090008B163EF00CCB9FB /* Cocoa.framework in Frameworks */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
                08FB7794FE84155DC02AAC07 /* DumpRenderTree */ = {
                        isa = PBXGroup;
                        children = (
+                               B5A7525808AF4A3600138E45 /* ImageDiff.m */,
                                08FB7796FE84155DC02AAC07 /* DumpRenderTree.m */,
                                32A70AAB03705E1F00C91783 /* DumpRenderTreePrefix.h */,
-                               08FB779EFE84155DC02AAC07 /* Foundation.framework */,
-                               9335436903D75557008635CE /* JavaScriptCore.framework */,
-                               9335436503D7553D008635CE /* WebCore.framework */,
                                9335435F03D75502008635CE /* WebKit.framework */,
+                               A817090308B164D300CCB9FB /* JavaScriptCore.framework */,
+                               A817090108B1643800CCB9FB /* WebCore.framework */,
+                               B5A752A108AF5D1F00138E45 /* QuartzCore.framework */,
+                               A84F608908B136DA00E9745F /* Cocoa.framework */,
                                9340995508540CAF007F3BC8 /* Products */,
                        );
                        name = DumpRenderTree;
                        isa = PBXGroup;
                        children = (
                                9340995408540CAF007F3BC8 /* DumpRenderTree */,
+                               B5A7526708AF4A4A00138E45 /* ImageDiff */,
                        );
                        name = Products;
                        sourceTree = "<group>";
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
+               B5A7525B08AF4A4A00138E45 /* Headers */ = {
+                       isa = PBXHeadersBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
 /* End PBXHeadersBuildPhase section */
 
 /* Begin PBXNativeTarget section */
                        productReference = 9340995408540CAF007F3BC8 /* DumpRenderTree */;
                        productType = "com.apple.product-type.tool";
                };
+               B5A7525A08AF4A4A00138E45 /* ImageDiff */ = {
+                       isa = PBXNativeTarget;
+                       buildConfigurationList = B5A7526408AF4A4A00138E45 /* Build configuration list for PBXNativeTarget "ImageDiff" */;
+                       buildPhases = (
+                               B5A7525B08AF4A4A00138E45 /* Headers */,
+                               B5A7525D08AF4A4A00138E45 /* Sources */,
+                               B5A7525F08AF4A4A00138E45 /* Frameworks */,
+                       );
+                       buildRules = (
+                       );
+                       buildSettings = {
+                               GCC_PRECOMPILE_PREFIX_HEADER = YES;
+                               GCC_PREFIX_HEADER = DumpRenderTreePrefix.h;
+                               GCC_TREAT_WARNINGS_AS_ERRORS = YES;
+                               PRODUCT_NAME = DumpRenderTree;
+                               WARNING_CFLAGS = (
+                                       "-Wall",
+                                       "-W",
+                                       "-Wno-unused-parameter",
+                               );
+                       };
+                       dependencies = (
+                       );
+                       name = ImageDiff;
+                       productInstallPath = "$(HOME)/bin";
+                       productName = DumpRenderTree;
+                       productReference = B5A7526708AF4A4A00138E45 /* ImageDiff */;
+                       productType = "com.apple.product-type.tool";
+               };
 /* End PBXNativeTarget section */
 
 /* Begin PBXProject section */
                        productRefGroup = 9340995508540CAF007F3BC8 /* Products */;
                        projectDirPath = "";
                        targets = (
+                               A84F608D08B1370600E9745F /* All */,
                                9340994A08540CAE007F3BC8 /* DumpRenderTree */,
+                               B5A7525A08AF4A4A00138E45 /* ImageDiff */,
                        );
                };
 /* End PBXProject section */
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
+               B5A7525D08AF4A4A00138E45 /* Sources */ = {
+                       isa = PBXSourcesBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                               B5A752A008AF5CD400138E45 /* ImageDiff.m in Sources */,
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
 /* End PBXSourcesBuildPhase section */
 
+/* Begin PBXTargetDependency section */
+               A84F608F08B1370E00E9745F /* PBXTargetDependency */ = {
+                       isa = PBXTargetDependency;
+                       target = B5A7525A08AF4A4A00138E45 /* ImageDiff */;
+                       targetProxy = A84F608E08B1370E00E9745F /* PBXContainerItemProxy */;
+               };
+               A84F609108B1370E00E9745F /* PBXTargetDependency */ = {
+                       isa = PBXTargetDependency;
+                       target = 9340994A08540CAE007F3BC8 /* DumpRenderTree */;
+                       targetProxy = A84F609008B1370E00E9745F /* PBXContainerItemProxy */;
+               };
+/* End PBXTargetDependency section */
+
 /* Begin XCBuildConfiguration section */
                149C29C008902C6D008A9EFC /* Development */ = {
                        isa = XCBuildConfiguration;
                149C29C408902C6D008A9EFC /* Development */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
+                               GCC_C_LANGUAGE_STANDARD = gnu99;
                                GCC_OPTIMIZATION_LEVEL = 0;
                                GCC_PRECOMPILE_PREFIX_HEADER = YES;
                                GCC_TREAT_WARNINGS_AS_ERRORS = YES;
+                               GCC_WARN_UNUSED_FUNCTION = YES;
+                               GCC_WARN_UNUSED_VARIABLE = YES;
                                PREBINDING = NO;
                                WARNING_CFLAGS = (
                                        "-Wall",
                149C29C508902C6D008A9EFC /* Deployment */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
+                               GCC_C_LANGUAGE_STANDARD = gnu99;
                                GCC_PRECOMPILE_PREFIX_HEADER = YES;
                                GCC_TREAT_WARNINGS_AS_ERRORS = YES;
+                               GCC_WARN_UNUSED_FUNCTION = YES;
+                               GCC_WARN_UNUSED_VARIABLE = YES;
                                PREBINDING = NO;
                                WARNING_CFLAGS = (
                                        "-Wall",
                        };
                        name = Deployment;
                };
+               A84F609308B1371400E9745F /* Development */ = {
+                       isa = XCBuildConfiguration;
+                       buildSettings = {
+                               COPY_PHASE_STRIP = NO;
+                               GCC_GENERATE_DEBUGGING_SYMBOLS = YES;
+                               OPTIMIZATION_CFLAGS = "-O0";
+                               OTHER_CFLAGS = "";
+                               OTHER_LDFLAGS = "";
+                               OTHER_REZFLAGS = "";
+                               PRODUCT_NAME = All;
+                               SECTORDER_FLAGS = "";
+                               WARNING_CFLAGS = (
+                                       "-Wmost",
+                                       "-Wno-four-char-constants",
+                                       "-Wno-unknown-pragmas",
+                               );
+                       };
+                       name = Development;
+               };
+               A84F609408B1371400E9745F /* Deployment */ = {
+                       isa = XCBuildConfiguration;
+                       buildSettings = {
+                               COPY_PHASE_STRIP = YES;
+                               GCC_GENERATE_DEBUGGING_SYMBOLS = NO;
+                               OPTIMIZATION_CFLAGS = "-O0";
+                               OTHER_CFLAGS = "";
+                               OTHER_LDFLAGS = "";
+                               OTHER_REZFLAGS = "";
+                               PRODUCT_NAME = All;
+                               SECTORDER_FLAGS = "";
+                               WARNING_CFLAGS = (
+                                       "-Wmost",
+                                       "-Wno-four-char-constants",
+                                       "-Wno-unknown-pragmas",
+                               );
+                       };
+                       name = Deployment;
+               };
+               B5A7526508AF4A4A00138E45 /* Development */ = {
+                       isa = XCBuildConfiguration;
+                       buildSettings = {
+                               GCC_C_LANGUAGE_STANDARD = gnu99;
+                               GCC_PREFIX_HEADER = "";
+                               PRODUCT_NAME = ImageDiff;
+                       };
+                       name = Development;
+               };
+               B5A7526608AF4A4A00138E45 /* Deployment */ = {
+                       isa = XCBuildConfiguration;
+                       buildSettings = {
+                               GCC_C_LANGUAGE_STANDARD = gnu99;
+                               GCC_PREFIX_HEADER = "";
+                               PRODUCT_NAME = ImageDiff;
+                       };
+                       name = Deployment;
+               };
 /* End XCBuildConfiguration section */
 
 /* Begin XCConfigurationList section */
                        defaultConfigurationIsVisible = 0;
                        defaultConfigurationName = Deployment;
                };
+               A84F609208B1371400E9745F /* Build configuration list for PBXAggregateTarget "All" */ = {
+                       isa = XCConfigurationList;
+                       buildConfigurations = (
+                               A84F609308B1371400E9745F /* Development */,
+                               A84F609408B1371400E9745F /* Deployment */,
+                       );
+                       defaultConfigurationIsVisible = 0;
+               };
+               B5A7526408AF4A4A00138E45 /* Build configuration list for PBXNativeTarget "ImageDiff" */ = {
+                       isa = XCConfigurationList;
+                       buildConfigurations = (
+                               B5A7526508AF4A4A00138E45 /* Development */,
+                               B5A7526608AF4A4A00138E45 /* Deployment */,
+                       );
+                       defaultConfigurationIsVisible = 0;
+                       defaultConfigurationName = Deployment;
+               };
 /* End XCConfigurationList section */
        };
        rootObject = 08FB7793FE84155DC02AAC07 /* Project object */;
index 4d5eb53..41a2591 100755 (executable)
@@ -30,6 +30,7 @@
 
 use strict;
 use IPC::Open2;
+use Getopt::Long;
 use FindBin;
 use Cwd;
 use lib $FindBin::Bin;
@@ -54,27 +55,34 @@ my $productDir = productDir();
 chdirWebKit();
 
 # Argument handling
-my $testSVGs = 0;
-if (($#ARGV > -1) && ($ARGV[0] eq "--svg")) {
-    $testSVGs = 1;
-    shift @ARGV;
-}
+my $testSVGs = '';
+my $pixelTests = '';
+my $maxWidth = '';
+my $maxHeight = '';
+my $verbose = '';
+my $quiet = '';
+
+GetOptions('svg' => \$testSVGs, 
+    'pixel-tests' => \$pixelTests,
+    'max-width' => \$maxWidth,
+    'max-height' => \$maxHeight, 
+    'verbose' => \$verbose,
+    'quiet' => \$quiet);
 
-my $toolbuildscript = "build-dumprendertree";
-if ($testSVGs) {
-    $toolbuildscript = "build-dumpkcanvastree";
-}
+my $toolname = "DumpRenderTree";
 
-my $result = system "WebKitTools/Scripts/$toolbuildscript", @ARGV;
+my $result = system "WebKitTools/Scripts/build-dumprendertree", @ARGV;
 exit $result if $result;
-
-my $toolname = "DumpRenderTree";
 if ($testSVGs) {
+    my $result = system "WebKitTools/Scripts/build-dumpkcanvastree", @ARGV;
+    exit $result if $result;
     $toolname = "DumpKCanvasTree";
 }
 
 my $tool = "$productDir/$toolname";
+my $imageDiffTool = "$productDir/ImageDiff";
 die "can't find executable $toolname tool (looked in $productDir)\n" if !-x $tool;
+die "can't find executable $imageDiffTool tool (looked in $productDir)\n" if $pixelTests && !-x $imageDiffTool;
 
 if ($testSVGs) {
     checkSVGFrameworks();
@@ -128,30 +136,66 @@ die "no tests to run\n" if !@tests;
 
 my %counts;
 my %tests;
+my %imagesPresent;
 my $count;
+my @toolArgs;
+
+if ($pixelTests) {
+    push @toolArgs, "--pixel-tests";
+    if ($maxWidth) {
+        push @toolArgs, ("--width", $maxWidth);
+    }
+    if ($maxWidth) {
+        push @toolArgs, ("--height", $maxWidth);
+    }
+}
 
-open2(\*IN, \*OUT, $tool, "-") or die;
+push  @toolArgs, "-";
 
 $| = 1;
 
+open2(\*IN, \*OUT, $tool, @toolArgs) or die;
+
+if ($pixelTests) {
+    open2(\*DIFFIN, \*DIFFOUT, $imageDiffTool, "") or die "unable to open $imageDiffTool\n";
+}
+
+my $column = 0;
+my $lastDirectory = "";
+
 for my $test (@tests) {
     next if $test eq 'results.html';
     
     my $base = $test;
     $base =~ s/\.(html|xml|xhtml|svg)$//;
     
-    print "running $base test";
-    
+    if ($verbose) {
+        print "running $base test";
+    } elsif (!$quiet) {
+        my $dir = $base;
+        $dir =~ s|/[^/]+$||;
+        if ($dir ne $lastDirectory) {
+            print "\n" unless $column == 0;
+            print $dir . " ";
+            $lastDirectory = $dir;
+            $column = 0;
+        }
+
+        print ".";
+        $column++;
+    }
+
     my $result;
 
     print OUT "$testDirectory/$test\n";
 
     my $actual = "";
     while (<IN>) {
-        last if /#EOF$/;
+        last if /#EOF/;
         $actual .= $_;
     }
-    
+
     my $expected;
     if (open EXPECTED, "<", "$testDirectory/$base-expected.txt") {
         $expected = "";
@@ -160,29 +204,170 @@ for my $test (@tests) {
         }
         close EXPECTED;
     }
+
+    my $textDumpMatches = $expected && ($actual eq $expected);
+    my $actualHash = "";
+    my $expectedHash = "";
+    my $hashMatches = "";
+    my $actualPNG = "";
+    my $actualPNGSize = 0;
+    my $expectedPNG = "";
+    my $expectedPNGSize = 0;
+    my $diffPNG = "";
+    my $diffPercentage = "";
+    my $diffResult = "passed";
+    
+    if ($pixelTests) {
+        while (<IN>) {
+            last if /#EOF/;
+            if (/ActualHash: ([a-f0-9]{32})/) {
+                $actualHash = $1;
+            } elsif (/BaselineHash: ([a-f0-9]{32})/) {
+                $expectedHash = $1;
+            } elsif (/Content-length: (\d+)\s*/) {
+                $actualPNGSize = $1;
+                read(IN, $actualPNG, $actualPNGSize);
+            }
+        }
+
+        if ($hashMatches = ($expectedHash eq $actualHash)) {
+            $diffResult = "passed";
+        }
+
+        if (!$hashMatches && -f "$testDirectory/$base-expected.png" && $textDumpMatches) {
+            $expectedPNGSize = getFilesize("$testDirectory/$base-expected.png");
+            open EXPECTEDPNG, "$testDirectory/$base-expected.png";
+            read(EXPECTEDPNG, $expectedPNG, $expectedPNGSize);
+
+            print DIFFOUT "Content-length: $actualPNGSize\n";
+            print DIFFOUT $actualPNG;
+
+            print DIFFOUT "Content-length: $expectedPNGSize\n";
+            print DIFFOUT $expectedPNG;
+
+            while (<DIFFIN>) {
+               last if /^error/ || /^diff:/;
+                if (/Content-length: (\d+)\s*/) {
+                    read(DIFFIN, $diffPNG, $1);
+                }
+            }
+
+            if (/^diff: (.+)% (passed|failed)/) {
+                $diffPercentage = $1;
+                $diffResult = $2;
+            }
+        }
+    }
+
+    if ($pixelTests) {
+        if ($actualPNGSize != 0 && ! -f "$testDirectory/$base-expected.png") {
+            open EXPECTED, ">", "$testDirectory/$base-expected.png" or die "could not create $testDirectory/$base-expected.png\n";
+            print EXPECTED $actualPNG;
+            close EXPECTED;
+        }
+
+        # update the expected hash if the image diff said that there was no difference
+        if ($actualHash ne "" && ! -f "$testDirectory/$base-expected.checksum") {
+            open EXPECTED, ">", "$testDirectory/$base-expected.checksum" or die "could not create $testDirectory/$base-expected.checksum\n";
+            print EXPECTED $actualHash;
+            close EXPECTED;
+        }
+    }
+
     if (!defined $expected) {
-        print " -> new test\n";        
+        if ($verbose) {
+            print " -> new test\n";
+        }
         $result = "new";
         open EXPECTED, ">", "$testDirectory/$base-expected.txt" or die "could not create $testDirectory/$base-expected.txt\n";
         print EXPECTED $actual;
         close EXPECTED;
+
         unlink "$testResultsDirectory/$base-actual.txt";
         unlink "$testResultsDirectory/$base-diffs.txt";
-    } elsif ($actual eq $expected) {
-        print " -> succeeded\n";        
+    } elsif ($textDumpMatches && (!$pixelTests || ($pixelTests && $diffResult eq "passed"))) {
+        if ($verbose) {
+            print " -> succeeded\n";
+        }
         $result = "match";
         unlink "$testResultsDirectory/$base-actual.txt";
         unlink "$testResultsDirectory/$base-diffs.txt";
-    } else {
-        print " -> failed\n";        
+    } elsif (!$textDumpMatches || ($pixelTests && $diffResult ne "passed")) {
+        print "\n" unless $column == 0;
+        print "$test -> failed\n";
+        $column = 0;        
+
         $result = "mismatch";
+
         my $dir = "$testResultsDirectory/$base";
         $dir =~ s|/[^/]+$||;
         system "mkdir", "-p", $dir;
+
         open ACTUAL, ">", "$testResultsDirectory/$base-actual.txt" or die;
         print ACTUAL $actual;
         close ACTUAL;
+
         system "diff -u \"$testDirectory/$base-expected.txt\" \"$testResultsDirectory/$base-actual.txt\" > \"$testResultsDirectory/$base-diffs.txt\"";
+
+        if ($pixelTests && $diffPNG && $diffPNG ne "") {
+            $imagesPresent{$base} = 1;
+
+            open ACTUAL, ">", "$testResultsDirectory/$base-actual.png" or die;
+            print ACTUAL $actualPNG;
+            close ACTUAL;
+
+            open DIFF, ">", "$testResultsDirectory/$base-diffs.png" or die;
+            print DIFF $diffPNG;
+            close DIFF;
+
+            open DIFFHTML, ">$testResultsDirectory/$base-diffs.html" or die;
+            print DIFFHTML "<html>\n";
+            print DIFFHTML "<head>\n";
+            print DIFFHTML "<title>$base Image Compare</title>\n";
+            print DIFFHTML "<script language=\"Javascript\" type=\"text/javascript\">\n";
+            print DIFFHTML "var actualImageVisible = true;\n";
+            print DIFFHTML "function animateImage() {\n";
+            print DIFFHTML "    var image = document.getElementById(\"animatedImage\");\n";
+            print DIFFHTML "    var imageText = document.getElementById(\"imageText\");\n";
+            print DIFFHTML "    if (actualImageVisible) {\n";
+            print DIFFHTML "        image.src=\"$testDirectory/$base-expected.png\";\n";
+            print DIFFHTML "        imageText.innerHTML = \"Expected Image\";\n";
+            print DIFFHTML "        actualImageVisible = false;\n";
+            print DIFFHTML "    } else {\n";
+            print DIFFHTML "        image.src=\"$testResultsDirectory/$base-actual.png\";\n";
+            print DIFFHTML "        imageText.innerHTML = \"Actual Image\";\n";
+            print DIFFHTML "        actualImageVisible = true;\n";
+            print DIFFHTML "    }\n";
+            print DIFFHTML "    setTimeout('animateImage()',2000);\n";
+            print DIFFHTML "}\n";
+            print DIFFHTML "</script>\n";
+            print DIFFHTML "</head>\n";
+            print DIFFHTML "<body onLoad=\"animateImage();\">\n";
+            print DIFFHTML "<table>\n";
+            if ($diffPercentage) {
+                print DIFFHTML "<tr>\n";
+                print DIFFHTML "<td>Difference between images: <a href=\"$testResultsDirectory/$base-diffs.png\">$diffPercentage%</a></td>\n";
+                print DIFFHTML "</tr>\n";
+            }
+            print DIFFHTML "<tr>\n";
+            print DIFFHTML "<td id=\"imageText\" style=\"text-weight: bold;\">Actual Image</td>\n";
+            print DIFFHTML "</tr>\n";
+            print DIFFHTML "<tr>\n";
+            print DIFFHTML "<td><img src=\"$testResultsDirectory/$base-actual.png\" id=\"animatedImage\"></td>\n";
+            print DIFFHTML "</tr>\n";
+            print DIFFHTML "</table>\n";
+            print DIFFHTML "</body>\n";
+            print DIFFHTML "</html>\n";
+        }
+    } else {
+        $result = "fail";
+        print "\n" unless $column == 0;
+        print "$test -> crashed\n";
+        $column = 0;
+
+        close IN;
+        close OUT;
+        open2(\*IN, \*OUT, $tool, @toolArgs) or die;
     }
     
     $count += 1;
@@ -242,6 +427,12 @@ if ($counts{match} && $counts{match} == $count) {
             print HTML "<td><a href=\"$testDirectory/$base-expected.txt\">expected</a></td>\n";
             print HTML "<td><a href=\"$base-actual.txt\">actual</a></td>\n";
             print HTML "<td><a href=\"$base-diffs.txt\">diffs</a></td>\n";
+            if ($pixelTests) {
+                print HTML "<td><a href=\"$testDirectory/$base-expected.png\">expected image</a></td>\n";
+                if ($imagesPresent{$base}) {
+                    print HTML "<td><a href=\"$base-diffs.html\">image diffs</a></td>\n";
+                }
+            }
             print HTML "</tr>\n";
         }
         print HTML "</table>\n";
@@ -269,6 +460,9 @@ if ($counts{match} && $counts{match} == $count) {
             print HTML "<tr>\n";
             print HTML "<td><a href=\"$testDirectory/$test\">$base</a></td>\n";
             print HTML "<td><a href=\"$testDirectory/$base-expected.txt\">results</a></td>\n";
+            if ($pixelTests) {
+                print HTML "<td><a href=\"$testDirectory/$base-expected.png\">image</a></td>\n";
+            }
             print HTML "</tr>\n";
         }
         print HTML "</table>\n";
@@ -280,3 +474,13 @@ if ($counts{match} && $counts{match} == $count) {
     
     system "open", $testResults;
 }
+
+sub getFilesize
+{
+    my $filename = shift;
+    my @fileStats;
+
+    @fileStats = stat($filename);
+
+    $fileStats[7];
+}