// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "skia/ext/skia_utils_mac.h" #import #include #include "base/logging.h" #include "base/mac/scoped_cftyperef.h" #include "base/mac/scoped_nsobject.h" #include "base/memory/scoped_ptr.h" #include "skia/ext/bitmap_platform_device_mac.h" #include "skia/ext/platform_canvas.h" #include "third_party/skia/include/core/SkRegion.h" #include "third_party/skia/include/utils/mac/SkCGUtils.h" namespace { // Draws an NSImage or an NSImageRep with a given size into a SkBitmap. SkBitmap NSImageOrNSImageRepToSkBitmapWithColorSpace( NSImage* image, NSImageRep* image_rep, NSSize size, bool is_opaque, CGColorSpaceRef color_space) { // Only image or image_rep should be provided, not both. DCHECK((image != 0) ^ (image_rep != 0)); SkBitmap bitmap; if (!bitmap.tryAllocN32Pixels(size.width, size.height, is_opaque)) return bitmap; // Return |bitmap| which should respond true to isNull(). void* data = bitmap.getPixels(); // Allocate a bitmap context with 4 components per pixel (BGRA). Apple // recommends these flags for improved CG performance. #define HAS_ARGB_SHIFTS(a, r, g, b) \ (SK_A32_SHIFT == (a) && SK_R32_SHIFT == (r) \ && SK_G32_SHIFT == (g) && SK_B32_SHIFT == (b)) #if defined(SK_CPU_LENDIAN) && HAS_ARGB_SHIFTS(24, 16, 8, 0) base::ScopedCFTypeRef context(CGBitmapContextCreate( data, size.width, size.height, 8, size.width * 4, color_space, kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host)); #else #error We require that Skia's and CoreGraphics's recommended \ image memory layout match. #endif #undef HAS_ARGB_SHIFTS // Something went really wrong. Best guess is that the bitmap data is invalid. DCHECK(context); [NSGraphicsContext saveGraphicsState]; NSGraphicsContext* context_cocoa = [NSGraphicsContext graphicsContextWithGraphicsPort:context flipped:NO]; [NSGraphicsContext setCurrentContext:context_cocoa]; NSRect drawRect = NSMakeRect(0, 0, size.width, size.height); if (image) { [image drawInRect:drawRect fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0]; } else { [image_rep drawInRect:drawRect fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0 respectFlipped:NO hints:nil]; } [NSGraphicsContext restoreGraphicsState]; return bitmap; } } // namespace namespace skia { CGAffineTransform SkMatrixToCGAffineTransform(const SkMatrix& matrix) { // CGAffineTransforms don't support perspective transforms, so make sure // we don't get those. DCHECK(matrix[SkMatrix::kMPersp0] == 0.0f); DCHECK(matrix[SkMatrix::kMPersp1] == 0.0f); DCHECK(matrix[SkMatrix::kMPersp2] == 1.0f); return CGAffineTransformMake(matrix[SkMatrix::kMScaleX], matrix[SkMatrix::kMSkewY], matrix[SkMatrix::kMSkewX], matrix[SkMatrix::kMScaleY], matrix[SkMatrix::kMTransX], matrix[SkMatrix::kMTransY]); } SkRect CGRectToSkRect(const CGRect& rect) { SkRect sk_rect = { rect.origin.x, rect.origin.y, CGRectGetMaxX(rect), CGRectGetMaxY(rect) }; return sk_rect; } CGRect SkIRectToCGRect(const SkIRect& rect) { CGRect cg_rect = { { rect.fLeft, rect.fTop }, { rect.fRight - rect.fLeft, rect.fBottom - rect.fTop } }; return cg_rect; } CGRect SkRectToCGRect(const SkRect& rect) { CGRect cg_rect = { { rect.fLeft, rect.fTop }, { rect.fRight - rect.fLeft, rect.fBottom - rect.fTop } }; return cg_rect; } // Converts CGColorRef to the ARGB layout Skia expects. SkColor CGColorRefToSkColor(CGColorRef color) { DCHECK(CGColorGetNumberOfComponents(color) == 4); const CGFloat* components = CGColorGetComponents(color); return SkColorSetARGB(SkScalarRoundToInt(255.0 * components[3]), // alpha SkScalarRoundToInt(255.0 * components[0]), // red SkScalarRoundToInt(255.0 * components[1]), // green SkScalarRoundToInt(255.0 * components[2])); // blue } // Converts ARGB to CGColorRef. CGColorRef CGColorCreateFromSkColor(SkColor color) { return CGColorCreateGenericRGB(SkColorGetR(color) / 255.0, SkColorGetG(color) / 255.0, SkColorGetB(color) / 255.0, SkColorGetA(color) / 255.0); } // Converts NSColor to ARGB SkColor NSDeviceColorToSkColor(NSColor* color) { DCHECK([color colorSpace] == [NSColorSpace genericRGBColorSpace] || [color colorSpace] == [NSColorSpace deviceRGBColorSpace]); CGFloat red, green, blue, alpha; color = [color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; [color getRed:&red green:&green blue:&blue alpha:&alpha]; return SkColorSetARGB(SkScalarRoundToInt(255.0 * alpha), SkScalarRoundToInt(255.0 * red), SkScalarRoundToInt(255.0 * green), SkScalarRoundToInt(255.0 * blue)); } // Converts ARGB to NSColor. NSColor* SkColorToCalibratedNSColor(SkColor color) { return [NSColor colorWithCalibratedRed:SkColorGetR(color) / 255.0 green:SkColorGetG(color) / 255.0 blue:SkColorGetB(color) / 255.0 alpha:SkColorGetA(color) / 255.0]; } NSColor* SkColorToDeviceNSColor(SkColor color) { return [NSColor colorWithDeviceRed:SkColorGetR(color) / 255.0 green:SkColorGetG(color) / 255.0 blue:SkColorGetB(color) / 255.0 alpha:SkColorGetA(color) / 255.0]; } NSColor* SkColorToSRGBNSColor(SkColor color) { const CGFloat components[] = { SkColorGetR(color) / 255.0, SkColorGetG(color) / 255.0, SkColorGetB(color) / 255.0, SkColorGetA(color) / 255.0 }; return [NSColor colorWithColorSpace:[NSColorSpace sRGBColorSpace] components:components count:4]; } SkBitmap CGImageToSkBitmap(CGImageRef image) { if (!image) return SkBitmap(); int width = CGImageGetWidth(image); int height = CGImageGetHeight(image); scoped_ptr device( skia::BitmapPlatformDevice::Create(NULL, width, height, false)); CGContextRef context = device->GetBitmapContext(); // We need to invert the y-axis of the canvas so that Core Graphics drawing // happens right-side up. Skia has an upper-left origin and CG has a lower- // left one. CGContextScaleCTM(context, 1.0, -1.0); CGContextTranslateCTM(context, 0, -height); // We want to copy transparent pixels from |image|, instead of blending it // onto uninitialized pixels. CGContextSetBlendMode(context, kCGBlendModeCopy); CGRect rect = CGRectMake(0, 0, width, height); CGContextDrawImage(context, rect, image); // Because |device| will be cleaned up and will take its pixels with it, we // copy it to the stack and return it. SkBitmap bitmap = device->accessBitmap(false); return bitmap; } SkBitmap NSImageToSkBitmapWithColorSpace( NSImage* image, bool is_opaque, CGColorSpaceRef color_space) { return NSImageOrNSImageRepToSkBitmapWithColorSpace( image, nil, [image size], is_opaque, color_space); } SkBitmap NSImageRepToSkBitmapWithColorSpace(NSImageRep* image_rep, NSSize size, bool is_opaque, CGColorSpaceRef color_space) { return NSImageOrNSImageRepToSkBitmapWithColorSpace( nil, image_rep, size, is_opaque, color_space); } NSBitmapImageRep* SkBitmapToNSBitmapImageRep(const SkBitmap& skiaBitmap) { base::ScopedCFTypeRef color_space( CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB)); return SkBitmapToNSBitmapImageRepWithColorSpace(skiaBitmap, color_space); } NSBitmapImageRep* SkBitmapToNSBitmapImageRepWithColorSpace( const SkBitmap& skiaBitmap, CGColorSpaceRef colorSpace) { // First convert SkBitmap to CGImageRef. base::ScopedCFTypeRef cgimage( SkCreateCGImageRefWithColorspace(skiaBitmap, colorSpace)); // Now convert to NSBitmapImageRep. base::scoped_nsobject bitmap( [[NSBitmapImageRep alloc] initWithCGImage:cgimage]); return [bitmap.release() autorelease]; } NSImage* SkBitmapToNSImageWithColorSpace(const SkBitmap& skiaBitmap, CGColorSpaceRef colorSpace) { if (skiaBitmap.isNull()) return nil; base::scoped_nsobject image([[NSImage alloc] init]); [image addRepresentation: SkBitmapToNSBitmapImageRepWithColorSpace(skiaBitmap, colorSpace)]; [image setSize:NSMakeSize(skiaBitmap.width(), skiaBitmap.height())]; return [image.release() autorelease]; } NSImage* SkBitmapToNSImage(const SkBitmap& skiaBitmap) { base::ScopedCFTypeRef colorSpace( CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB)); return SkBitmapToNSImageWithColorSpace(skiaBitmap, colorSpace.get()); } SkiaBitLocker::SkiaBitLocker(SkCanvas* canvas) : canvas_(canvas), userClipRectSpecified_(false), cgContext_(0), bitmapScaleFactor_(1), useDeviceBits_(false), bitmapIsDummy_(false) { } SkiaBitLocker::SkiaBitLocker(SkCanvas* canvas, const SkIRect& userClipRect, SkScalar bitmapScaleFactor) : canvas_(canvas), userClipRectSpecified_(true), cgContext_(0), bitmapScaleFactor_(bitmapScaleFactor), useDeviceBits_(false), bitmapIsDummy_(false) { canvas_->save(); canvas_->clipRect(SkRect::MakeFromIRect(userClipRect)); } SkiaBitLocker::~SkiaBitLocker() { releaseIfNeeded(); if (userClipRectSpecified_) canvas_->restore(); } SkIRect SkiaBitLocker::computeDirtyRect() { // If the user specified a clip region, assume that it was tight and that the // dirty rect is approximately the whole bitmap. if (userClipRectSpecified_) return SkIRect::MakeWH(offscreen_.width(), offscreen_.height()); // Find the bits that were drawn to. SkAutoLockPixels lockedPixels(offscreen_); const uint32_t* pixelBase = reinterpret_cast(offscreen_.getPixels()); int rowPixels = offscreen_.rowBytesAsPixels(); int width = offscreen_.width(); int height = offscreen_.height(); SkIRect bounds; bounds.fTop = 0; int x; int y = -1; const uint32_t* pixels = pixelBase; while (++y < height) { for (x = 0; x < width; ++x) { if (pixels[x]) { bounds.fTop = y; goto foundTop; } } pixels += rowPixels; } foundTop: bounds.fBottom = height; y = height; pixels = pixelBase + rowPixels * (y - 1); while (--y > bounds.fTop) { for (x = 0; x < width; ++x) { if (pixels[x]) { bounds.fBottom = y + 1; goto foundBottom; } } pixels -= rowPixels; } foundBottom: bounds.fLeft = 0; x = -1; while (++x < width) { pixels = pixelBase + rowPixels * bounds.fTop; for (y = bounds.fTop; y < bounds.fBottom; ++y) { if (pixels[x]) { bounds.fLeft = x; goto foundLeft; } pixels += rowPixels; } } foundLeft: bounds.fRight = width; x = width; while (--x > bounds.fLeft) { pixels = pixelBase + rowPixels * bounds.fTop; for (y = bounds.fTop; y < bounds.fBottom; ++y) { if (pixels[x]) { bounds.fRight = x + 1; goto foundRight; } pixels += rowPixels; } } foundRight: return bounds; } // This must be called to balance calls to cgContext void SkiaBitLocker::releaseIfNeeded() { if (!cgContext_) return; if (!useDeviceBits_ && !bitmapIsDummy_) { // Find the bits that were drawn to. SkIRect bounds = computeDirtyRect(); SkBitmap subset; if (!offscreen_.extractSubset(&subset, bounds)) { return; } subset.setImmutable(); // Prevents a defensive copy inside Skia. canvas_->save(); canvas_->setMatrix(SkMatrix::I()); // Reset back to device space. canvas_->translate(bounds.x() + bitmapOffset_.x(), bounds.y() + bitmapOffset_.y()); canvas_->scale(1.f / bitmapScaleFactor_, 1.f / bitmapScaleFactor_); canvas_->drawBitmap(subset, 0, 0); canvas_->restore(); } CGContextRelease(cgContext_); cgContext_ = 0; useDeviceBits_ = false; bitmapIsDummy_ = false; } CGContextRef SkiaBitLocker::cgContext() { SkIRect clip_bounds; if (!canvas_->getClipDeviceBounds(&clip_bounds)) { // If the clip is empty, then there is nothing to draw. The caller may // attempt to draw (to-be-clipped) results, so ensure there is a dummy // non-NULL CGContext to use. bitmapIsDummy_ = true; clip_bounds = SkIRect::MakeXYWH(0, 0, 1, 1); } SkBaseDevice* device = canvas_->getTopDevice(); DCHECK(device); if (!device) return 0; releaseIfNeeded(); // This flushes any prior bitmap use // remember the top/left, in case we need to compose this later bitmapOffset_.set(clip_bounds.x(), clip_bounds.y()); // Now make clip_bounds be relative to the current layer/device clip_bounds.offset(-device->getOrigin()); SkPixmap devicePixels; skia::GetWritablePixels(canvas_, &devicePixels); // Only draw directly if we have pixels, and we're only rect-clipped. // If not, we allocate an offscreen and draw into that, relying on the // compositing step to apply skia's clip. useDeviceBits_ = devicePixels.addr() && canvas_->isClipRect() && !bitmapIsDummy_; base::ScopedCFTypeRef colorSpace( CGColorSpaceCreateDeviceRGB()); int displayHeight; if (useDeviceBits_) { SkPixmap subset; bool result = devicePixels.extractSubset(&subset, clip_bounds); DCHECK(result); if (!result) return 0; displayHeight = subset.height(); cgContext_ = CGBitmapContextCreate(subset.writable_addr(), subset.width(), subset.height(), 8, subset.rowBytes(), colorSpace, kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst); } else { bool result = offscreen_.tryAllocN32Pixels( SkScalarCeilToInt(bitmapScaleFactor_ * clip_bounds.width()), SkScalarCeilToInt(bitmapScaleFactor_ * clip_bounds.height())); DCHECK(result); if (!result) return 0; offscreen_.eraseColor(0); displayHeight = offscreen_.height(); cgContext_ = CGBitmapContextCreate(offscreen_.getPixels(), offscreen_.width(), offscreen_.height(), 8, offscreen_.rowBytes(), colorSpace, kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst); } DCHECK(cgContext_); SkMatrix matrix = canvas_->getTotalMatrix(); matrix.postTranslate(-SkIntToScalar(bitmapOffset_.x()), -SkIntToScalar(bitmapOffset_.y())); matrix.postScale(bitmapScaleFactor_, -bitmapScaleFactor_); matrix.postTranslate(0, SkIntToScalar(displayHeight)); CGContextConcatCTM(cgContext_, SkMatrixToCGAffineTransform(matrix)); return cgContext_; } bool SkiaBitLocker::hasEmptyClipRegion() const { return canvas_->isClipEmpty(); } } // namespace skia