A comparison of QuickTime and CoreGraphics image scaling

Code and wisdom in this article have not been kept up-to-date. Use them at your own peril.

I recently used a photo management utility to scale some of my photos and I was less than thrilled with the results. After a brief exchange with the author, I learned that he uses NSGraphicContext image interpolation to scale the image. As a user, I really wanted to get better photos out of this process, and as a developer I started wondering if there’s anything he could do to improve his software short of writing a custom scaling algorithm. The answer turns out to be: when image quality is important, do not use CoreGraphics or AppKit to scale your images — use QuickTime instead.

I whipped up a small application that renders the same image in four different ways:

  1. Using QuickTime graphics importer to load and scale the image at the same time, then render the scaled image without additional scaling to a window.
  2. Using QuickTime graphics importer to load the image at its native size, then scale the image into a window using CoreGraphics high quality image interpolation.
  3. Using QuickTime graphics importer to load the image at its native size, then scale the image into a window using CoreGraphics low quality image interpolation.
  4. Using QuickTime graphics importer to load the image at its native size, then scale the image into a window using CoreGraphics without image interpolation.

At scaling factor of ½, the results were unremarkable. The CG image without interpolation was poor, and the remaining three were indistinguishable.

However, at scaling factors that are not powers of two, the difference between QD and high quality CG scaling was quite noticeable. QuickTime scaling produced the best output, and CG with no interpolation produced the worst. Ironically, CG with high quality scaling made the image look worse than CG with low quality scaling; the so-called high-quality scaling algorithm makes the image much too blurry.

The resulting images are shown in the movie below. Pay particular attention to the regions marked with red, green, and blue rectangles. The red rectangles show regions in which distortion of straight edges is particularly noticeable when scaled with CG and no interpolation. The green and blue rectangles show areas in which loss of detail is particularly noticeable when rendered with CG and high quality interpolation, compared to QuickTime. Finally, the rectangles themselves are a very good demonstration of what the different rendering methods do to sharp straight edges.

If you want to play with this yourself, the code is below. Build this as a Mac OS X Carbon application, and put a sample.jpg in the application’s Resources folder:

#include <Carbon/Carbon.h>
#include <CoreFoundation/CoreFoundation.h>
#include <QuickTime/QuickTime.h>

//
//  WARNING: This code is for demonstration purposes only. Do not use it verbatim, you’ll regret it.
//

int main()
{
    float   scaling = 8/float(9);
    bool    offset = false;

    FSRef         imageRef;
    CFURLGetFSRef(
        CFBundleCopyResourceURL(
            CFBundleGetMainBundle(),
            CFSTR("sample"),
            CFSTR("jpg"),
            0
        ),
        &imageRef
    );

    FSSpec        imageSpec;
    FSGetCatalogInfo(&imageRef, kFSCatInfoNone, 0, 0, &imageSpec, 0);

    ComponentInstance     importer;
    GetGraphicsImporterForFile(&imageSpec, &importer);

    Rect          fullImageBounds;
    GraphicsImportGetNaturalBounds(importer, &fullImageBounds);

    ImageDescriptionHandle        imageDesc =
        reinterpret_cast<ImageDescriptionHandle>(NewHandle(sizeof(ImageDescription)));

    GraphicsImportGetImageDescription(importer, &imageDesc);

    short   depth = ((**imageDesc).depth <= 16) ? 16 : 32;
    OSType  pixelFormat = (depth == 32) ? k32ARGBPixelFormat : k16BE555PixelFormat;

    GraphicsImportSetQuality(importer, codecLosslessQuality);

    // Load full image
    OffsetRect(&fullImageBounds, -fullImageBounds.left, -fullImageBounds.top);

    UInt32  fullRowStride = fullImageBounds.right * depth / 8;
    Ptr     fullImageBuffer = NewPtr(fullRowStride * fullImageBounds.bottom);

    GWorldPtr       fullImageWorld;
    QTNewGWorldFromPtr(&fullImageWorld, pixelFormat, &fullImageBounds, NULL, NULL, 0, fullImageBuffer, fullRowStride);
    GraphicsImportSetGWorld (importer, fullImageWorld, NULL);
    PixMapHandle          fullImagePixMap = GetGWorldPixMap(fullImageWorld);
    LockPixels(fullImagePixMap);
    GraphicsImportDraw(importer);

    CGDataProviderRef     fullImageDataProviderRef =
        CGDataProviderCreateWithData(
            NULL,
            GetPixBaseAddr(fullImagePixMap),
            fullImageBounds.bottom * GetPixRowBytes(fullImagePixMap),
            NULL
        );

    CGImageRef            fullImage =
        CGImageCreate(
            fullImageBounds.right, fullImageBounds.bottom,
            (**fullImagePixMap).cmpSize,
            (**fullImagePixMap).pixelSize,
            GetPixRowBytes(fullImagePixMap),
            CGColorSpaceCreateDeviceRGB(),
            kCGImageAlphaPremultipliedFirst,
            fullImageDataProviderRef,
            NULL,
            true,
            kCGRenderingIntentDefault
        );


    // Load scaled image
    Rect    scaledImageBounds = {
        0, 0, fullImageBounds.bottom * scaling, fullImageBounds.right * scaling
    };

    MatrixRecord  scaledMatrix;
    SetIdentityMatrix(&scaledMatrix);
    ScaleMatrix(&scaledMatrix, X2Fix(scaling), X2Fix(scaling), X2Fix(0), X2Fix(0));
    GraphicsImportSetMatrix(importer, &scaledMatrix);

    UInt32      scaledRowStride = scaledImageBounds.right * depth / 8;
    Ptr         scaledImageBuffer = NewPtr(scaledRowStride * scaledImageBounds.bottom);
    GWorldPtr   scaledImageWorld;
    QTNewGWorldFromPtr(&scaledImageWorld, pixelFormat, &scaledImageBounds, NULL, NULL, 0, scaledImageBuffer, scaledRowStride);
    GraphicsImportSetGWorld (importer, scaledImageWorld, NULL);
    PixMapHandle          scaledImagePixMap = GetGWorldPixMap(scaledImageWorld);
    LockPixels(scaledImagePixMap);
    GraphicsImportDraw(importer);

    CGDataProviderRef     scaledImageDataProviderRef =
        CGDataProviderCreateWithData(
            NULL,
            GetPixBaseAddr(scaledImagePixMap),
            scaledImageBounds.bottom * GetPixRowBytes(scaledImagePixMap),
            NULL
        );

    CGImageRef            scaledImage =
        CGImageCreate(
            scaledImageBounds.right, scaledImageBounds.bottom,
            (**scaledImagePixMap).cmpSize,
            (**scaledImagePixMap).pixelSize,
            GetPixRowBytes(scaledImagePixMap),
            CGColorSpaceCreateDeviceRGB(),
            kCGImageAlphaPremultipliedFirst,
            scaledImageDataProviderRef,
            NULL,
            true,
            kCGRenderingIntentDefault
        );

    CGContextRef context;

    // Create window with QT-scaled image
    Rect      qtBounds = {
        50,
        50,
        50 + scaledImageBounds.bottom,
        50 + scaledImageBounds.right
    };

    WindowRef     qtWindow;
    CreateNewWindow(
        kDocumentWindowClass,
        kWindowCollapseBoxAttribute | kWindowStandardHandlerAttribute | kWindowCompositingAttribute,
        &qtBounds,
        &qtWindow
    );
    SetWindowTitleWithCFString(qtWindow, CFSTR("QuickTime scaling"));
    ShowWindow(qtWindow);

    QDBeginCGContext(GetWindowPort(qtWindow), &context);
    CGContextSetInterpolationQuality(context, kCGInterpolationNone);
    CGContextDrawImage(context, CGRectMake(0, 0, scaledImageBounds.right, scaledImageBounds.bottom), scaledImage);
    CGContextFlush(context);
    QDEndCGContext(GetWindowPort(qtWindow), &context);


    // Create window with CG-scaled image (high quality interpolation)
    Rect      cgHighBounds = qtBounds;
    if (offset) {
        OffsetRect(&cgHighBounds, 50 + scaledImageBounds.right, 0);
    }

    WindowRef     cgHighWindow;
    CreateNewWindow(
        kDocumentWindowClass,
        kWindowCollapseBoxAttribute | kWindowStandardHandlerAttribute | kWindowCompositingAttribute,
        &cgHighBounds,
        &cgHighWindow
    );
    SetWindowTitleWithCFString(cgHighWindow, CFSTR("CoreGraphics high quality"));
    ShowWindow(cgHighWindow);

    QDBeginCGContext(GetWindowPort(cgHighWindow), &context);
    CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
    CGContextDrawImage(context, CGRectMake(0, 0, scaledImageBounds.right, scaledImageBounds.bottom), fullImage);
    CGContextFlush(context);
    QDEndCGContext(GetWindowPort(cgHighWindow), &context);

    // Create window with CG-scaled image (low quality interpolation)
    Rect      cgLowBounds = qtBounds;
    if (offset) {
        OffsetRect(&cgLowBounds, 0, 50 + scaledImageBounds.bottom);
    }

    WindowRef     cgLowWindow;
    CreateNewWindow(
        kDocumentWindowClass,
        kWindowCollapseBoxAttribute | kWindowStandardHandlerAttribute | kWindowCompositingAttribute,
        &cgLowBounds,
        &cgLowWindow
    );
    SetWindowTitleWithCFString(cgLowWindow, CFSTR("CoreGraphics low quality"));
    ShowWindow(cgLowWindow);

    QDBeginCGContext(GetWindowPort(cgLowWindow), &context);
    CGContextSetInterpolationQuality(context, kCGInterpolationLow);
    CGContextDrawImage(context, CGRectMake(0, 0, scaledImageBounds.right, scaledImageBounds.bottom), fullImage);
    CGContextFlush(context);
    QDEndCGContext(GetWindowPort(cgLowWindow), &context);

    // Create window with CG-scaled image (no interpolation)
    Rect      cgNoneBounds = qtBounds;
    if (offset) {
        OffsetRect(&cgNoneBounds, 50 + scaledImageBounds.right, 50 + scaledImageBounds.bottom);
    }

    WindowRef     cgNoneWindow;
    CreateNewWindow(
        kDocumentWindowClass,
        kWindowCollapseBoxAttribute | kWindowStandardHandlerAttribute | kWindowCompositingAttribute,
        &cgNoneBounds,
        &cgNoneWindow
    );
    SetWindowTitleWithCFString(cgNoneWindow, CFSTR("CoreGraphics without interpolation"));
    ShowWindow(cgNoneWindow);

    QDBeginCGContext(GetWindowPort(cgNoneWindow), &context);
    CGContextSetInterpolationQuality(context, kCGInterpolationNone);
    CGContextDrawImage(context, CGRectMake(0, 0, scaledImageBounds.right, scaledImageBounds.bottom), fullImage);
    CGContextFlush(context);
    QDEndCGContext(GetWindowPort(cgNoneWindow), &context);

    RunApplicationEventLoop();
}