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:
- 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.
- 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.
- 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.
- 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();
}