/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "DrawTargetCG.h" #include "SourceSurfaceCG.h" #include "Rect.h" #include "ScaledFontMac.h" #include "Tools.h" #include #include #include "QuartzSupport.h" using namespace std; //CG_EXTERN void CGContextSetCompositeOperation (CGContextRef, PrivateCGCompositeMode); // A private API that Cairo has been using for a long time CG_EXTERN void CGContextSetCTM(CGContextRef, CGAffineTransform); namespace mozilla { namespace gfx { static CGRect RectToCGRect(Rect r) { return CGRectMake(r.x, r.y, r.width, r.height); } static CGRect IntRectToCGRect(IntRect r) { return CGRectMake(r.x, r.y, r.width, r.height); } CGBlendMode ToBlendMode(CompositionOp op) { CGBlendMode mode; switch (op) { case OP_OVER: mode = kCGBlendModeNormal; break; case OP_ADD: mode = kCGBlendModePlusLighter; break; case OP_ATOP: mode = kCGBlendModeSourceAtop; break; case OP_OUT: mode = kCGBlendModeSourceOut; break; case OP_IN: mode = kCGBlendModeSourceIn; break; case OP_SOURCE: mode = kCGBlendModeCopy; break; case OP_DEST_IN: mode = kCGBlendModeDestinationIn; break; case OP_DEST_OUT: mode = kCGBlendModeDestinationOut; break; case OP_DEST_OVER: mode = kCGBlendModeDestinationOver; break; case OP_DEST_ATOP: mode = kCGBlendModeDestinationAtop; break; case OP_XOR: mode = kCGBlendModeXOR; break; case OP_MULTIPLY: mode = kCGBlendModeMultiply; break; case OP_SCREEN: mode = kCGBlendModeScreen; break; case OP_OVERLAY: mode = kCGBlendModeOverlay; break; case OP_DARKEN: mode = kCGBlendModeDarken; break; case OP_LIGHTEN: mode = kCGBlendModeLighten; break; case OP_COLOR_DODGE: mode = kCGBlendModeColorDodge; break; case OP_COLOR_BURN: mode = kCGBlendModeColorBurn; break; case OP_HARD_LIGHT: mode = kCGBlendModeHardLight; break; case OP_SOFT_LIGHT: mode = kCGBlendModeSoftLight; break; case OP_DIFFERENCE: mode = kCGBlendModeDifference; break; case OP_EXCLUSION: mode = kCGBlendModeExclusion; break; case OP_HUE: mode = kCGBlendModeHue; break; case OP_SATURATION: mode = kCGBlendModeSaturation; break; case OP_COLOR: mode = kCGBlendModeColor; break; case OP_LUMINOSITY: mode = kCGBlendModeLuminosity; break; /* case OP_CLEAR: mode = kCGBlendModeClear; break;*/ default: mode = kCGBlendModeNormal; } return mode; } static CGInterpolationQuality InterpolationQualityFromFilter(Filter aFilter) { switch (aFilter) { default: case FILTER_LINEAR: return kCGInterpolationLow; case FILTER_POINT: return kCGInterpolationNone; case FILTER_GOOD: return kCGInterpolationDefault; } } DrawTargetCG::DrawTargetCG() : mCg(nullptr), mSnapshot(nullptr) { } DrawTargetCG::~DrawTargetCG() { MarkChanged(); // We need to conditionally release these because Init can fail without initializing these. if (mColorSpace) CGColorSpaceRelease(mColorSpace); if (mCg) CGContextRelease(mCg); free(mData); } BackendType DrawTargetCG::GetType() const { // It may be worth spliting Bitmap and IOSurface DrawTarget // into seperate classes. if (GetContextType(mCg) == CG_CONTEXT_TYPE_IOSURFACE) { return BACKEND_COREGRAPHICS_ACCELERATED; } else { return BACKEND_COREGRAPHICS; } } TemporaryRef DrawTargetCG::Snapshot() { if (!mSnapshot) { if (GetContextType(mCg) == CG_CONTEXT_TYPE_IOSURFACE) { return new SourceSurfaceCGIOSurfaceContext(this); } else { mSnapshot = new SourceSurfaceCGBitmapContext(this); } } return mSnapshot; } TemporaryRef DrawTargetCG::CreateSimilarDrawTarget(const IntSize &aSize, SurfaceFormat aFormat) const { // XXX: in thebes we use CGLayers to do this kind of thing. It probably makes sense // to add that in somehow, but at a higher level RefPtr newTarget = new DrawTargetCG(); if (newTarget->Init(GetType(), aSize, aFormat)) { return newTarget; } else { return nullptr; } } TemporaryRef DrawTargetCG::CreateSourceSurfaceFromData(unsigned char *aData, const IntSize &aSize, int32_t aStride, SurfaceFormat aFormat) const { RefPtr newSurf = new SourceSurfaceCG(); if (!newSurf->InitFromData(aData, aSize, aStride, aFormat)) { return nullptr; } return newSurf; } static CGImageRef GetImageFromSourceSurface(SourceSurface *aSurface) { if (aSurface->GetType() == SURFACE_COREGRAPHICS_IMAGE) return static_cast(aSurface)->GetImage(); else if (aSurface->GetType() == SURFACE_COREGRAPHICS_CGCONTEXT) return static_cast(aSurface)->GetImage(); else if (aSurface->GetType() == SURFACE_DATA) return static_cast(aSurface)->GetImage(); abort(); } TemporaryRef DrawTargetCG::OptimizeSourceSurface(SourceSurface *aSurface) const { return nullptr; } class UnboundnessFixer { CGRect mClipBounds; CGLayerRef mLayer; CGContextRef mCg; public: UnboundnessFixer() : mCg(nullptr) {} CGContextRef Check(CGContextRef baseCg, CompositionOp blend) { if (!IsOperatorBoundByMask(blend)) { mClipBounds = CGContextGetClipBoundingBox(baseCg); // TransparencyLayers aren't blended using the blend mode so // we are forced to use CGLayers //XXX: The size here is in default user space units, of the layer relative to the graphics context. // is the clip bounds still correct if, for example, we have a scale applied to the context? mLayer = CGLayerCreateWithContext(baseCg, mClipBounds.size, nullptr); //XXX: if the size is 0x0 we get a nullptr CGContext back from GetContext mCg = CGLayerGetContext(mLayer); // CGContext's default to have the origin at the bottom left // so flip it to the top left and adjust for the origin // of the layer CGContextTranslateCTM(mCg, -mClipBounds.origin.x, mClipBounds.origin.y + mClipBounds.size.height); CGContextScaleCTM(mCg, 1, -1); return mCg; } else { return baseCg; } } void Fix(CGContextRef baseCg) { if (mCg) { CGContextTranslateCTM(baseCg, 0, mClipBounds.size.height); CGContextScaleCTM(baseCg, 1, -1); mClipBounds.origin.y *= -1; CGContextDrawLayerAtPoint(baseCg, mClipBounds.origin, mLayer); CGContextRelease(mCg); } } }; void DrawTargetCG::DrawSurface(SourceSurface *aSurface, const Rect &aDest, const Rect &aSource, const DrawSurfaceOptions &aSurfOptions, const DrawOptions &aDrawOptions) { MarkChanged(); CGImageRef image; CGImageRef subimage = nullptr; CGContextSaveGState(mCg); CGContextSetBlendMode(mCg, ToBlendMode(aDrawOptions.mCompositionOp)); UnboundnessFixer fixer; CGContextRef cg = fixer.Check(mCg, aDrawOptions.mCompositionOp); CGContextSetAlpha(cg, aDrawOptions.mAlpha); CGContextSetShouldAntialias(cg, aDrawOptions.mAntialiasMode != AA_NONE); CGContextConcatCTM(cg, GfxMatrixToCGAffineTransform(mTransform)); image = GetImageFromSourceSurface(aSurface); /* we have two options here: * - create a subimage -- this is slower * - fancy things with clip and different dest rects */ { subimage = CGImageCreateWithImageInRect(image, RectToCGRect(aSource)); image = subimage; } CGContextScaleCTM(cg, 1, -1); CGRect flippedRect = CGRectMake(aDest.x, -(aDest.y + aDest.height), aDest.width, aDest.height); CGContextSetInterpolationQuality(cg, InterpolationQualityFromFilter(aSurfOptions.mFilter)); CGContextDrawImage(cg, flippedRect, image); fixer.Fix(mCg); CGContextRestoreGState(mCg); CGImageRelease(subimage); } static CGColorRef ColorToCGColor(CGColorSpaceRef aColorSpace, const Color& aColor) { CGFloat components[4] = {aColor.r, aColor.g, aColor.b, aColor.a}; return CGColorCreate(aColorSpace, components); } class GradientStopsCG : public GradientStops { public: //XXX: The skia backend uses a vector and passes in aNumStops. It should do better GradientStopsCG(GradientStop* aStops, uint32_t aNumStops, ExtendMode aExtendMode) { mExtend = aExtendMode; if (aExtendMode == EXTEND_CLAMP) { //XXX: do the stops need to be in any particular order? // what should we do about the color space here? we certainly shouldn't be // recreating it all the time std::vector colors; std::vector offsets; colors.reserve(aNumStops*4); offsets.reserve(aNumStops); for (uint32_t i = 0; i < aNumStops; i++) { colors.push_back(aStops[i].color.r); colors.push_back(aStops[i].color.g); colors.push_back(aStops[i].color.b); colors.push_back(aStops[i].color.a); offsets.push_back(aStops[i].offset); } CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); mGradient = CGGradientCreateWithColorComponents(colorSpace, &colors.front(), &offsets.front(), aNumStops); CGColorSpaceRelease(colorSpace); } else { mGradient = nullptr; mStops.reserve(aNumStops); for (uint32_t i = 0; i < aNumStops; i++) { mStops.push_back(aStops[i]); } } } virtual ~GradientStopsCG() { if (mGradient) CGGradientRelease(mGradient); } // Will always report BACKEND_COREGRAPHICS, but it is compatible // with BACKEND_COREGRAPHICS_ACCELERATED BackendType GetBackendType() const { return BACKEND_COREGRAPHICS; } // XXX this should be a union CGGradientRef mGradient; std::vector mStops; ExtendMode mExtend; }; TemporaryRef DrawTargetCG::CreateGradientStops(GradientStop *aStops, uint32_t aNumStops, ExtendMode aExtendMode) const { return new GradientStopsCG(aStops, aNumStops, aExtendMode); } static void DrawLinearRepeatingGradient(CGContextRef cg, const LinearGradientPattern &aPattern, const CGRect &aExtents) { GradientStopsCG *stops = static_cast(aPattern.mStops.get()); CGPoint startPoint = { aPattern.mBegin.x, aPattern.mBegin.y }; CGPoint endPoint = { aPattern.mEnd.x, aPattern.mEnd.y }; // extend the gradient line in multiples of the existing length in both // directions until it crosses an edge of the extents box. double xDiff = aPattern.mEnd.x - aPattern.mBegin.x; double yDiff = aPattern.mEnd.y - aPattern.mBegin.y; int repeatCount = 1; // if we don't have a line then we can't extend it if (xDiff || yDiff) { while (startPoint.x > aExtents.origin.x && startPoint.y > aExtents.origin.y && startPoint.x < (aExtents.origin.x+aExtents.size.width) && startPoint.y < (aExtents.origin.y+aExtents.size.height)) { startPoint.x -= xDiff; startPoint.y -= yDiff; repeatCount++; } while (endPoint.x > aExtents.origin.x && endPoint.y > aExtents.origin.y && endPoint.x < (aExtents.origin.x+aExtents.size.width) && endPoint.y < (aExtents.origin.y+aExtents.size.height)) { endPoint.x += xDiff; endPoint.y += yDiff; repeatCount++; } } double scale = 1./repeatCount; std::vector colors; std::vector offsets; colors.reserve(stops->mStops.size()*repeatCount*4); offsets.reserve(stops->mStops.size()*repeatCount); for (int j = 0; j < repeatCount; j++) { for (uint32_t i = 0; i < stops->mStops.size(); i++) { colors.push_back(stops->mStops[i].color.r); colors.push_back(stops->mStops[i].color.g); colors.push_back(stops->mStops[i].color.b); colors.push_back(stops->mStops[i].color.a); offsets.push_back((stops->mStops[i].offset + j)*scale); } } CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, &colors.front(), &offsets.front(), repeatCount*stops->mStops.size()); CGColorSpaceRelease(colorSpace); CGContextDrawLinearGradient(cg, gradient, startPoint, endPoint, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); CGGradientRelease(gradient); } static CGPoint CGRectTopLeft(CGRect a) { return a.origin; } static CGPoint CGRectBottomLeft(CGRect a) { return CGPointMake(a.origin.x, a.origin.y + a.size.height); } static CGPoint CGRectTopRight(CGRect a) { return CGPointMake(a.origin.x + a.size.width, a.origin.y); } static CGPoint CGRectBottomRight(CGRect a) { return CGPointMake(a.origin.x + a.size.width, a.origin.y + a.size.height); } static CGFloat CGPointDistance(CGPoint a, CGPoint b) { return hypot(a.x-b.x, a.y-b.y); } static void DrawRadialRepeatingGradient(CGContextRef cg, const RadialGradientPattern &aPattern, const CGRect &aExtents) { GradientStopsCG *stops = static_cast(aPattern.mStops.get()); CGPoint startCenter = { aPattern.mCenter1.x, aPattern.mCenter1.y }; CGFloat startRadius = aPattern.mRadius1; CGPoint endCenter = { aPattern.mCenter2.x, aPattern.mCenter2.y }; CGFloat endRadius = aPattern.mRadius2; // find the maximum distance from endCenter to a corner of aExtents CGFloat minimumEndRadius = endRadius; minimumEndRadius = max(minimumEndRadius, CGPointDistance(endCenter, CGRectTopLeft(aExtents))); minimumEndRadius = max(minimumEndRadius, CGPointDistance(endCenter, CGRectBottomLeft(aExtents))); minimumEndRadius = max(minimumEndRadius, CGPointDistance(endCenter, CGRectTopRight(aExtents))); minimumEndRadius = max(minimumEndRadius, CGPointDistance(endCenter, CGRectBottomRight(aExtents))); CGFloat length = endRadius - startRadius; int repeatCount = 1; while (endRadius < minimumEndRadius) { endRadius += length; repeatCount++; } while (startRadius-length >= 0) { startRadius -= length; repeatCount++; } double scale = 1./repeatCount; std::vector colors; std::vector offsets; colors.reserve(stops->mStops.size()*repeatCount*4); offsets.reserve(stops->mStops.size()*repeatCount); for (int j = 0; j < repeatCount; j++) { for (uint32_t i = 0; i < stops->mStops.size(); i++) { colors.push_back(stops->mStops[i].color.r); colors.push_back(stops->mStops[i].color.g); colors.push_back(stops->mStops[i].color.b); colors.push_back(stops->mStops[i].color.a); offsets.push_back((stops->mStops[i].offset + j)*scale); } } CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, &colors.front(), &offsets.front(), repeatCount*stops->mStops.size()); CGColorSpaceRelease(colorSpace); //XXX: are there degenerate radial gradients that we should avoid drawing? CGContextDrawRadialGradient(cg, gradient, startCenter, startRadius, endCenter, endRadius, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); CGGradientRelease(gradient); } static void DrawGradient(CGContextRef cg, const Pattern &aPattern, const CGRect &aExtents) { if (aPattern.GetType() == PATTERN_LINEAR_GRADIENT) { const LinearGradientPattern& pat = static_cast(aPattern); GradientStopsCG *stops = static_cast(pat.mStops.get()); CGContextConcatCTM(cg, GfxMatrixToCGAffineTransform(pat.mMatrix)); if (stops->mExtend == EXTEND_CLAMP) { // XXX: we should take the m out of the properties of LinearGradientPatterns CGPoint startPoint = { pat.mBegin.x, pat.mBegin.y }; CGPoint endPoint = { pat.mEnd.x, pat.mEnd.y }; // Canvas spec states that we should avoid drawing degenerate gradients (XXX: should this be in common code?) //if (startPoint.x == endPoint.x && startPoint.y == endPoint.y) // return; CGContextDrawLinearGradient(cg, stops->mGradient, startPoint, endPoint, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); } else if (stops->mExtend == EXTEND_REPEAT) { DrawLinearRepeatingGradient(cg, pat, aExtents); } } else if (aPattern.GetType() == PATTERN_RADIAL_GRADIENT) { const RadialGradientPattern& pat = static_cast(aPattern); CGContextConcatCTM(cg, GfxMatrixToCGAffineTransform(pat.mMatrix)); GradientStopsCG *stops = static_cast(pat.mStops.get()); if (stops->mExtend == EXTEND_CLAMP) { // XXX: we should take the m out of the properties of RadialGradientPatterns CGPoint startCenter = { pat.mCenter1.x, pat.mCenter1.y }; CGFloat startRadius = pat.mRadius1; CGPoint endCenter = { pat.mCenter2.x, pat.mCenter2.y }; CGFloat endRadius = pat.mRadius2; //XXX: are there degenerate radial gradients that we should avoid drawing? CGContextDrawRadialGradient(cg, stops->mGradient, startCenter, startRadius, endCenter, endRadius, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); } else if (stops->mExtend == EXTEND_REPEAT) { DrawRadialRepeatingGradient(cg, pat, aExtents); } } else { assert(0); } } static void drawPattern(void *info, CGContextRef context) { CGImageRef image = static_cast(info); CGRect rect = {{0, 0}, {static_cast(CGImageGetWidth(image)), static_cast(CGImageGetHeight(image))}}; CGContextDrawImage(context, rect, image); } static void releaseInfo(void *info) { CGImageRef image = static_cast(info); CGImageRelease(image); } CGPatternCallbacks patternCallbacks = { 0, drawPattern, releaseInfo }; static bool isGradient(const Pattern &aPattern) { return aPattern.GetType() == PATTERN_LINEAR_GRADIENT || aPattern.GetType() == PATTERN_RADIAL_GRADIENT; } /* CoreGraphics patterns ignore the userspace transform so * we need to multiply it in */ static CGPatternRef CreateCGPattern(const Pattern &aPattern, CGAffineTransform aUserSpace) { const SurfacePattern& pat = static_cast(aPattern); // XXX: is .get correct here? CGImageRef image = GetImageFromSourceSurface(pat.mSurface.get()); CGFloat xStep, yStep; switch (pat.mExtendMode) { case EXTEND_CLAMP: // The 1 << 22 comes from Webkit see Pattern::createPlatformPattern() in PatternCG.cpp for more info xStep = static_cast(1 << 22); yStep = static_cast(1 << 22); break; case EXTEND_REFLECT: assert(0); case EXTEND_REPEAT: xStep = static_cast(CGImageGetWidth(image)); yStep = static_cast(CGImageGetHeight(image)); // webkit uses wkCGPatternCreateWithImageAndTransform a wrapper around CGPatternCreateWithImage2 // this is done to avoid pixel-cracking along pattern boundaries // (see https://bugs.webkit.org/show_bug.cgi?id=53055) // typedef enum { // wkPatternTilingNoDistortion, // wkPatternTilingConstantSpacingMinimalDistortion, // wkPatternTilingConstantSpacing // } wkPatternTiling; // extern CGPatternRef (*wkCGPatternCreateWithImageAndTransform)(CGImageRef, CGAffineTransform, int); } //XXX: We should be using CGContextDrawTiledImage when we can. Even though it // creates a pattern, it seems to go down a faster path than using a delegate // like we do below CGRect bounds = { {0, 0,}, {static_cast(CGImageGetWidth(image)), static_cast(CGImageGetHeight(image))} }; CGAffineTransform transform = CGAffineTransformConcat(CGAffineTransformConcat(CGAffineTransformMakeScale(1, -1), GfxMatrixToCGAffineTransform(pat.mMatrix)), aUserSpace); transform = CGAffineTransformTranslate(transform, 0, -static_cast(CGImageGetHeight(image))); return CGPatternCreate(CGImageRetain(image), bounds, transform, xStep, yStep, kCGPatternTilingConstantSpacing, true, &patternCallbacks); } static void SetFillFromPattern(CGContextRef cg, CGColorSpaceRef aColorSpace, const Pattern &aPattern) { assert(!isGradient(aPattern)); if (aPattern.GetType() == PATTERN_COLOR) { const Color& color = static_cast(aPattern).mColor; //XXX: we should cache colors CGColorRef cgcolor = ColorToCGColor(aColorSpace, color); CGContextSetFillColorWithColor(cg, cgcolor); CGColorRelease(cgcolor); } else if (aPattern.GetType() == PATTERN_SURFACE) { CGColorSpaceRef patternSpace; patternSpace = CGColorSpaceCreatePattern (nullptr); CGContextSetFillColorSpace(cg, patternSpace); CGColorSpaceRelease(patternSpace); CGPatternRef pattern = CreateCGPattern(aPattern, CGContextGetCTM(cg)); const SurfacePattern& pat = static_cast(aPattern); CGContextSetInterpolationQuality(cg, InterpolationQualityFromFilter(pat.mFilter)); CGFloat alpha = 1.; CGContextSetFillPattern(cg, pattern, &alpha); CGPatternRelease(pattern); } } static void SetStrokeFromPattern(CGContextRef cg, CGColorSpaceRef aColorSpace, const Pattern &aPattern) { assert(!isGradient(aPattern)); if (aPattern.GetType() == PATTERN_COLOR) { const Color& color = static_cast(aPattern).mColor; //XXX: we should cache colors CGColorRef cgcolor = ColorToCGColor(aColorSpace, color); CGContextSetStrokeColorWithColor(cg, cgcolor); CGColorRelease(cgcolor); } else if (aPattern.GetType() == PATTERN_SURFACE) { CGColorSpaceRef patternSpace; patternSpace = CGColorSpaceCreatePattern (nullptr); CGContextSetStrokeColorSpace(cg, patternSpace); CGColorSpaceRelease(patternSpace); CGPatternRef pattern = CreateCGPattern(aPattern, CGContextGetCTM(cg)); const SurfacePattern& pat = static_cast(aPattern); CGContextSetInterpolationQuality(cg, InterpolationQualityFromFilter(pat.mFilter)); CGFloat alpha = 1.; CGContextSetStrokePattern(cg, pattern, &alpha); CGPatternRelease(pattern); } } void DrawTargetCG::MaskSurface(const Pattern &aSource, SourceSurface *aMask, Point aOffset, const DrawOptions &aDrawOptions) { MarkChanged(); CGImageRef image; CGContextSaveGState(mCg); CGContextSetBlendMode(mCg, ToBlendMode(aDrawOptions.mCompositionOp)); UnboundnessFixer fixer; CGContextRef cg = fixer.Check(mCg, aDrawOptions.mCompositionOp); CGContextSetAlpha(cg, aDrawOptions.mAlpha); CGContextSetShouldAntialias(cg, aDrawOptions.mAntialiasMode != AA_NONE); CGContextConcatCTM(cg, GfxMatrixToCGAffineTransform(mTransform)); image = GetImageFromSourceSurface(aMask); // use a negative-y so that the mask image draws right ways up CGContextScaleCTM(cg, 1, -1); IntSize size = aMask->GetSize(); CGContextClipToMask(cg, CGRectMake(aOffset.x, -(aOffset.y + size.height), size.width, size.height), image); CGContextScaleCTM(cg, 1, -1); if (isGradient(aSource)) { // we shouldn't need to clip to an additional rectangle // as the cliping to the mask should be sufficient. DrawGradient(cg, aSource, CGRectMake(aOffset.x, aOffset.y, size.width, size.height)); } else { SetFillFromPattern(cg, mColorSpace, aSource); CGContextFillRect(cg, CGRectMake(aOffset.x, aOffset.y, size.width, size.height)); } fixer.Fix(mCg); CGContextRestoreGState(mCg); } void DrawTargetCG::FillRect(const Rect &aRect, const Pattern &aPattern, const DrawOptions &aDrawOptions) { MarkChanged(); CGContextSaveGState(mCg); UnboundnessFixer fixer; CGContextRef cg = fixer.Check(mCg, aDrawOptions.mCompositionOp); CGContextSetAlpha(mCg, aDrawOptions.mAlpha); CGContextSetShouldAntialias(cg, aDrawOptions.mAntialiasMode != AA_NONE); CGContextSetBlendMode(mCg, ToBlendMode(aDrawOptions.mCompositionOp)); CGContextConcatCTM(cg, GfxMatrixToCGAffineTransform(mTransform)); if (isGradient(aPattern)) { CGContextClipToRect(cg, RectToCGRect(aRect)); DrawGradient(cg, aPattern, RectToCGRect(aRect)); } else { if (aPattern.GetType() == PATTERN_SURFACE && static_cast(aPattern).mExtendMode != EXTEND_REPEAT) { // SetFillFromPattern can handle this case but using CGContextDrawImage // should give us better performance, better output, smaller PDF and // matches what cairo does. const SurfacePattern& pat = static_cast(aPattern); CGImageRef image = GetImageFromSourceSurface(pat.mSurface.get()); CGContextClipToRect(cg, RectToCGRect(aRect)); CGContextConcatCTM(cg, GfxMatrixToCGAffineTransform(pat.mMatrix)); CGContextTranslateCTM(cg, 0, CGImageGetHeight(image)); CGContextScaleCTM(cg, 1, -1); CGRect imageRect = CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)); CGContextSetInterpolationQuality(cg, InterpolationQualityFromFilter(pat.mFilter)); CGContextDrawImage(cg, imageRect, image); } else { SetFillFromPattern(cg, mColorSpace, aPattern); CGContextFillRect(cg, RectToCGRect(aRect)); } } fixer.Fix(mCg); CGContextRestoreGState(mCg); } void DrawTargetCG::StrokeLine(const Point &p1, const Point &p2, const Pattern &aPattern, const StrokeOptions &aStrokeOptions, const DrawOptions &aDrawOptions) { MarkChanged(); CGContextSaveGState(mCg); UnboundnessFixer fixer; CGContextRef cg = fixer.Check(mCg, aDrawOptions.mCompositionOp); CGContextSetAlpha(mCg, aDrawOptions.mAlpha); CGContextSetShouldAntialias(cg, aDrawOptions.mAntialiasMode != AA_NONE); CGContextSetBlendMode(mCg, ToBlendMode(aDrawOptions.mCompositionOp)); CGContextConcatCTM(cg, GfxMatrixToCGAffineTransform(mTransform)); CGContextBeginPath(cg); CGContextMoveToPoint(cg, p1.x, p1.y); CGContextAddLineToPoint(cg, p2.x, p2.y); SetStrokeOptions(cg, aStrokeOptions); if (isGradient(aPattern)) { CGContextReplacePathWithStrokedPath(cg); CGRect extents = CGContextGetPathBoundingBox(cg); //XXX: should we use EO clip here? CGContextClip(cg); DrawGradient(cg, aPattern, extents); } else { SetStrokeFromPattern(cg, mColorSpace, aPattern); CGContextStrokePath(cg); } fixer.Fix(mCg); CGContextRestoreGState(mCg); } void DrawTargetCG::StrokeRect(const Rect &aRect, const Pattern &aPattern, const StrokeOptions &aStrokeOptions, const DrawOptions &aDrawOptions) { MarkChanged(); CGContextSaveGState(mCg); UnboundnessFixer fixer; CGContextRef cg = fixer.Check(mCg, aDrawOptions.mCompositionOp); CGContextSetAlpha(mCg, aDrawOptions.mAlpha); CGContextSetShouldAntialias(cg, aDrawOptions.mAntialiasMode != AA_NONE); CGContextSetBlendMode(mCg, ToBlendMode(aDrawOptions.mCompositionOp)); CGContextConcatCTM(cg, GfxMatrixToCGAffineTransform(mTransform)); SetStrokeOptions(cg, aStrokeOptions); if (isGradient(aPattern)) { // There's no CGContextClipStrokeRect so we do it by hand CGContextBeginPath(cg); CGContextAddRect(cg, RectToCGRect(aRect)); CGContextReplacePathWithStrokedPath(cg); CGRect extents = CGContextGetPathBoundingBox(cg); //XXX: should we use EO clip here? CGContextClip(cg); DrawGradient(cg, aPattern, extents); } else { SetStrokeFromPattern(cg, mColorSpace, aPattern); CGContextStrokeRect(cg, RectToCGRect(aRect)); } fixer.Fix(mCg); CGContextRestoreGState(mCg); } void DrawTargetCG::ClearRect(const Rect &aRect) { MarkChanged(); CGContextSaveGState(mCg); CGContextConcatCTM(mCg, GfxMatrixToCGAffineTransform(mTransform)); CGContextClearRect(mCg, RectToCGRect(aRect)); CGContextRestoreGState(mCg); } void DrawTargetCG::Stroke(const Path *aPath, const Pattern &aPattern, const StrokeOptions &aStrokeOptions, const DrawOptions &aDrawOptions) { MarkChanged(); CGContextSaveGState(mCg); UnboundnessFixer fixer; CGContextRef cg = fixer.Check(mCg, aDrawOptions.mCompositionOp); CGContextSetAlpha(mCg, aDrawOptions.mAlpha); CGContextSetShouldAntialias(cg, aDrawOptions.mAntialiasMode != AA_NONE); CGContextSetBlendMode(mCg, ToBlendMode(aDrawOptions.mCompositionOp)); CGContextConcatCTM(cg, GfxMatrixToCGAffineTransform(mTransform)); CGContextBeginPath(cg); assert(aPath->GetBackendType() == BACKEND_COREGRAPHICS); const PathCG *cgPath = static_cast(aPath); CGContextAddPath(cg, cgPath->GetPath()); SetStrokeOptions(cg, aStrokeOptions); if (isGradient(aPattern)) { CGContextReplacePathWithStrokedPath(cg); CGRect extents = CGContextGetPathBoundingBox(cg); //XXX: should we use EO clip here? CGContextClip(cg); DrawGradient(cg, aPattern, extents); } else { // XXX: we could put fill mode into the path fill rule if we wanted SetStrokeFromPattern(cg, mColorSpace, aPattern); CGContextStrokePath(cg); } fixer.Fix(mCg); CGContextRestoreGState(mCg); } void DrawTargetCG::Fill(const Path *aPath, const Pattern &aPattern, const DrawOptions &aDrawOptions) { MarkChanged(); assert(aPath->GetBackendType() == BACKEND_COREGRAPHICS); CGContextSaveGState(mCg); CGContextSetBlendMode(mCg, ToBlendMode(aDrawOptions.mCompositionOp)); UnboundnessFixer fixer; CGContextRef cg = fixer.Check(mCg, aDrawOptions.mCompositionOp); CGContextSetAlpha(cg, aDrawOptions.mAlpha); CGContextSetShouldAntialias(cg, aDrawOptions.mAntialiasMode != AA_NONE); CGContextConcatCTM(cg, GfxMatrixToCGAffineTransform(mTransform)); CGContextBeginPath(cg); // XXX: we could put fill mode into the path fill rule if we wanted const PathCG *cgPath = static_cast(aPath); if (isGradient(aPattern)) { // setup a clip to draw the gradient through CGRect extents; if (CGPathIsEmpty(cgPath->GetPath())) { // Adding an empty path will cause us not to clip // so clip everything explicitly CGContextClipToRect(mCg, CGRectZero); extents = CGRectZero; } else { CGContextAddPath(cg, cgPath->GetPath()); extents = CGContextGetPathBoundingBox(cg); if (cgPath->GetFillRule() == FILL_EVEN_ODD) CGContextEOClip(mCg); else CGContextClip(mCg); } DrawGradient(cg, aPattern, extents); } else { CGContextAddPath(cg, cgPath->GetPath()); SetFillFromPattern(cg, mColorSpace, aPattern); if (cgPath->GetFillRule() == FILL_EVEN_ODD) CGContextEOFillPath(cg); else CGContextFillPath(cg); } fixer.Fix(mCg); CGContextRestoreGState(mCg); } CGRect ComputeGlyphsExtents(CGRect *bboxes, CGPoint *positions, CFIndex count, float scale) { CGFloat x1, x2, y1, y2; if (count < 1) return CGRectZero; x1 = bboxes[0].origin.x + positions[0].x; x2 = bboxes[0].origin.x + positions[0].x + scale*bboxes[0].size.width; y1 = bboxes[0].origin.y + positions[0].y; y2 = bboxes[0].origin.y + positions[0].y + scale*bboxes[0].size.height; // accumulate max and minimum coordinates for (int i = 1; i < count; i++) { x1 = min(x1, bboxes[i].origin.x + positions[i].x); y1 = min(y1, bboxes[i].origin.y + positions[i].y); x2 = max(x2, bboxes[i].origin.x + positions[i].x + scale*bboxes[i].size.width); y2 = max(y2, bboxes[i].origin.y + positions[i].y + scale*bboxes[i].size.height); } CGRect extents = {{x1, y1}, {x2-x1, y2-y1}}; return extents; } void DrawTargetCG::FillGlyphs(ScaledFont *aFont, const GlyphBuffer &aBuffer, const Pattern &aPattern, const DrawOptions &aDrawOptions, const GlyphRenderingOptions*) { MarkChanged(); assert(aBuffer.mNumGlyphs); CGContextSaveGState(mCg); CGContextSetBlendMode(mCg, ToBlendMode(aDrawOptions.mCompositionOp)); UnboundnessFixer fixer; CGContextRef cg = fixer.Check(mCg, aDrawOptions.mCompositionOp); CGContextSetAlpha(cg, aDrawOptions.mAlpha); CGContextSetShouldAntialias(cg, aDrawOptions.mAntialiasMode != AA_NONE); if (aDrawOptions.mAntialiasMode != AA_DEFAULT) { CGContextSetShouldSmoothFonts(cg, aDrawOptions.mAntialiasMode == AA_SUBPIXEL); } CGContextConcatCTM(cg, GfxMatrixToCGAffineTransform(mTransform)); ScaledFontMac* macFont = static_cast(aFont); //XXX: we should use a stack vector here when we have a class like that std::vector glyphs; std::vector positions; glyphs.resize(aBuffer.mNumGlyphs); positions.resize(aBuffer.mNumGlyphs); // Handle the flip CGAffineTransform matrix = CGAffineTransformMakeScale(1, -1); CGContextConcatCTM(cg, matrix); // CGContextSetTextMatrix works differently with kCGTextClip && kCGTextFill // It seems that it transforms the positions with TextFill and not with TextClip // Therefore we'll avoid it. See also: // http://cgit.freedesktop.org/cairo/commit/?id=9c0d761bfcdd28d52c83d74f46dd3c709ae0fa69 for (unsigned int i = 0; i < aBuffer.mNumGlyphs; i++) { glyphs[i] = aBuffer.mGlyphs[i].mIndex; // XXX: CGPointMake might not be inlined positions[i] = CGPointMake(aBuffer.mGlyphs[i].mPosition.x, -aBuffer.mGlyphs[i].mPosition.y); } //XXX: CGContextShowGlyphsAtPositions is 10.5+ for older versions use CGContextShowGlyphsWithAdvances if (isGradient(aPattern)) { CGContextSetTextDrawingMode(cg, kCGTextClip); CGRect extents; if (ScaledFontMac::CTFontDrawGlyphsPtr != nullptr) { CGRect *bboxes = new CGRect[aBuffer.mNumGlyphs]; CTFontGetBoundingRectsForGlyphs(macFont->mCTFont, kCTFontDefaultOrientation, &glyphs.front(), bboxes, aBuffer.mNumGlyphs); extents = ComputeGlyphsExtents(bboxes, &positions.front(), aBuffer.mNumGlyphs, 1.0f); ScaledFontMac::CTFontDrawGlyphsPtr(macFont->mCTFont, &glyphs.front(), &positions.front(), aBuffer.mNumGlyphs, cg); delete bboxes; } else { CGRect *bboxes = new CGRect[aBuffer.mNumGlyphs]; CGFontGetGlyphBBoxes(macFont->mFont, &glyphs.front(), aBuffer.mNumGlyphs, bboxes); extents = ComputeGlyphsExtents(bboxes, &positions.front(), aBuffer.mNumGlyphs, macFont->mSize); CGContextSetFont(cg, macFont->mFont); CGContextSetFontSize(cg, macFont->mSize); CGContextShowGlyphsAtPositions(cg, &glyphs.front(), &positions.front(), aBuffer.mNumGlyphs); delete bboxes; } DrawGradient(cg, aPattern, extents); } else { //XXX: with CoreGraphics we can stroke text directly instead of going // through GetPath. It would be nice to add support for using that CGContextSetTextDrawingMode(cg, kCGTextFill); SetFillFromPattern(cg, mColorSpace, aPattern); if (ScaledFontMac::CTFontDrawGlyphsPtr != nullptr) { ScaledFontMac::CTFontDrawGlyphsPtr(macFont->mCTFont, &glyphs.front(), &positions.front(), aBuffer.mNumGlyphs, cg); } else { CGContextSetFont(cg, macFont->mFont); CGContextSetFontSize(cg, macFont->mSize); CGContextShowGlyphsAtPositions(cg, &glyphs.front(), &positions.front(), aBuffer.mNumGlyphs); } } fixer.Fix(mCg); CGContextRestoreGState(cg); } extern "C" { void CGContextResetClip(CGContextRef); }; void DrawTargetCG::CopySurface(SourceSurface *aSurface, const IntRect& aSourceRect, const IntPoint &aDestination) { MarkChanged(); CGImageRef image; CGImageRef subimage = nullptr; if (aSurface->GetType() == SURFACE_COREGRAPHICS_IMAGE || aSurface->GetType() == SURFACE_COREGRAPHICS_CGCONTEXT) { image = GetImageFromSourceSurface(aSurface); /* we have two options here: * - create a subimage -- this is slower * - fancy things with clip and different dest rects */ { subimage = CGImageCreateWithImageInRect(image, IntRectToCGRect(aSourceRect)); image = subimage; } // XXX: it might be more efficient for us to do the copy directly if we have access to the bits CGContextSaveGState(mCg); // CopySurface ignores the clip, so we need to use private API to temporarily reset it CGContextResetClip(mCg); CGContextSetBlendMode(mCg, kCGBlendModeCopy); CGContextScaleCTM(mCg, 1, -1); CGRect flippedRect = CGRectMake(aDestination.x, -(aDestination.y + aSourceRect.height), aSourceRect.width, aSourceRect.height); CGContextDrawImage(mCg, flippedRect, image); CGContextRestoreGState(mCg); CGImageRelease(subimage); } } void DrawTargetCG::DrawSurfaceWithShadow(SourceSurface *aSurface, const Point &aDest, const Color &aColor, const Point &aOffset, Float aSigma, CompositionOp aOperator) { MarkChanged(); CGImageRef image; image = GetImageFromSourceSurface(aSurface); IntSize size = aSurface->GetSize(); CGContextSaveGState(mCg); //XXX do we need to do the fixup here? CGContextSetBlendMode(mCg, ToBlendMode(aOperator)); CGContextScaleCTM(mCg, 1, -1); CGRect flippedRect = CGRectMake(aDest.x, -(aDest.y + size.height), size.width, size.height); CGColorRef color = ColorToCGColor(mColorSpace, aColor); CGSize offset = {aOffset.x, -aOffset.y}; // CoreGraphics needs twice sigma as it's amount of blur CGContextSetShadowWithColor(mCg, offset, 2*aSigma, color); CGColorRelease(color); CGContextDrawImage(mCg, flippedRect, image); CGContextRestoreGState(mCg); } bool DrawTargetCG::Init(BackendType aType, unsigned char* aData, const IntSize &aSize, int32_t aStride, SurfaceFormat aFormat) { // XXX: we should come up with some consistent semantics for dealing // with zero area drawtargets if (aSize.width <= 0 || aSize.height <= 0 || // 32767 is the maximum size supported by cairo // we clamp to that to make it easier to interoperate aSize.width > 32767 || aSize.height > 32767) { mColorSpace = nullptr; mCg = nullptr; mData = nullptr; return false; } //XXX: handle SurfaceFormat //XXX: we'd be better off reusing the Colorspace across draw targets mColorSpace = CGColorSpaceCreateDeviceRGB(); if (aData == nullptr && aType != BACKEND_COREGRAPHICS_ACCELERATED) { // XXX: Currently, Init implicitly clears, that can often be a waste of time mData = calloc(aSize.height * aStride, 1); aData = static_cast(mData); } else { // mData == nullptr means DrawTargetCG doesn't own the image data and will not // delete it in the destructor mData = nullptr; } mSize = aSize; if (aType == BACKEND_COREGRAPHICS_ACCELERATED) { RefPtr ioSurface = MacIOSurface::CreateIOSurface(aSize.width, aSize.height); mCg = ioSurface->CreateIOSurfaceContext(); // If we don't have the symbol for 'CreateIOSurfaceContext' mCg will be null // and we will fallback to software below mData = nullptr; } mFormat = FORMAT_B8G8R8A8; if (!mCg || aType == BACKEND_COREGRAPHICS) { int bitsPerComponent = 8; CGBitmapInfo bitinfo; if (aFormat == FORMAT_A8) { if (mColorSpace) CGColorSpaceRelease(mColorSpace); mColorSpace = nullptr; bitinfo = kCGImageAlphaOnly; mFormat = FORMAT_A8; } else { bitinfo = kCGBitmapByteOrder32Host; if (aFormat == FORMAT_B8G8R8X8) { bitinfo |= kCGImageAlphaNoneSkipFirst; mFormat = aFormat; } else { bitinfo |= kCGImageAlphaPremultipliedFirst; } } // XXX: what should we do if this fails? mCg = CGBitmapContextCreate (aData, mSize.width, mSize.height, bitsPerComponent, aStride, mColorSpace, bitinfo); } assert(mCg); // CGContext's default to have the origin at the bottom left // so flip it to the top left CGContextTranslateCTM(mCg, 0, mSize.height); CGContextScaleCTM(mCg, 1, -1); // See Bug 722164 for performance details // Medium or higher quality lead to expensive interpolation // for canvas we want to use low quality interpolation // to have competitive performance with other canvas // implementation. // XXX: Create input parameter to control interpolation and // use the default for content. CGContextSetInterpolationQuality(mCg, kCGInterpolationLow); if (aType == BACKEND_COREGRAPHICS_ACCELERATED) { // The bitmap backend uses callac to clear, we can't do that without // reading back the surface. This should trigger something equivilent // to glClear. ClearRect(Rect(0, 0, mSize.width, mSize.height)); } return true; } void DrawTargetCG::Flush() { CGContextFlush(mCg); } bool DrawTargetCG::Init(CGContextRef cgContext, const IntSize &aSize) { // XXX: we should come up with some consistent semantics for dealing // with zero area drawtargets if (aSize.width == 0 || aSize.height == 0) { mColorSpace = nullptr; mCg = nullptr; mData = nullptr; return false; } //XXX: handle SurfaceFormat //XXX: we'd be better off reusing the Colorspace across draw targets mColorSpace = CGColorSpaceCreateDeviceRGB(); mSize = aSize; mCg = cgContext; CGContextRetain(mCg); mData = nullptr; assert(mCg); // CGContext's default to have the origin at the bottom left. // However, currently the only use of this function is to construct a // DrawTargetCG around a CGContextRef from a cairo quartz surface which // already has it's origin adjusted. // // CGContextTranslateCTM(mCg, 0, mSize.height); // CGContextScaleCTM(mCg, 1, -1); mFormat = FORMAT_B8G8R8A8; if (GetContextType(mCg) == CG_CONTEXT_TYPE_BITMAP) { CGColorSpaceRef colorspace; CGBitmapInfo bitinfo = CGBitmapContextGetBitmapInfo(mCg); colorspace = CGBitmapContextGetColorSpace (mCg); if (CGColorSpaceGetNumberOfComponents(colorspace) == 1) { mFormat = FORMAT_A8; } else if ((bitinfo & kCGBitmapAlphaInfoMask) == kCGImageAlphaNoneSkipFirst) { mFormat = FORMAT_B8G8R8X8; } } return true; } bool DrawTargetCG::Init(BackendType aType, const IntSize &aSize, SurfaceFormat &aFormat) { int stride = aSize.width*4; // Calling Init with aData == nullptr will allocate. return Init(aType, nullptr, aSize, stride, aFormat); } TemporaryRef DrawTargetCG::CreatePathBuilder(FillRule aFillRule) const { RefPtr pb = new PathBuilderCG(aFillRule); return pb; } void* DrawTargetCG::GetNativeSurface(NativeSurfaceType aType) { if ((aType == NATIVE_SURFACE_CGCONTEXT && GetContextType(mCg) == CG_CONTEXT_TYPE_BITMAP) || (aType == NATIVE_SURFACE_CGCONTEXT_ACCELERATED && GetContextType(mCg) == CG_CONTEXT_TYPE_IOSURFACE)) { return mCg; } else { return nullptr; } } void DrawTargetCG::Mask(const Pattern &aSource, const Pattern &aMask, const DrawOptions &aDrawOptions) { MarkChanged(); CGContextSaveGState(mCg); if (isGradient(aMask)) { assert(0); } else { if (aMask.GetType() == PATTERN_COLOR) { DrawOptions drawOptions(aDrawOptions); const Color& color = static_cast(aMask).mColor; drawOptions.mAlpha *= color.a; assert(0); // XXX: we need to get a rect that when transformed covers the entire surface //Rect //FillRect(rect, aSource, drawOptions); } else if (aMask.GetType() == PATTERN_SURFACE) { const SurfacePattern& pat = static_cast(aMask); CGImageRef mask = GetImageFromSourceSurface(pat.mSurface.get()); Rect rect(0,0, CGImageGetWidth(mask), CGImageGetHeight(mask)); // XXX: probably we need to do some flipping of the image or something CGContextClipToMask(mCg, RectToCGRect(rect), mask); FillRect(rect, aSource, aDrawOptions); } } CGContextRestoreGState(mCg); } void DrawTargetCG::PushClipRect(const Rect &aRect) { CGContextSaveGState(mCg); /* We go through a bit of trouble to temporarilly set the transform * while we add the path */ CGAffineTransform previousTransform = CGContextGetCTM(mCg); CGContextConcatCTM(mCg, GfxMatrixToCGAffineTransform(mTransform)); CGContextClipToRect(mCg, RectToCGRect(aRect)); CGContextSetCTM(mCg, previousTransform); } void DrawTargetCG::PushClip(const Path *aPath) { CGContextSaveGState(mCg); CGContextBeginPath(mCg); assert(aPath->GetBackendType() == BACKEND_COREGRAPHICS); const PathCG *cgPath = static_cast(aPath); // Weirdly, CoreGraphics clips empty paths as all shown // but emtpy rects as all clipped. We detect this situation and // workaround it appropriately if (CGPathIsEmpty(cgPath->GetPath())) { // XXX: should we return here? CGContextClipToRect(mCg, CGRectZero); } /* We go through a bit of trouble to temporarilly set the transform * while we add the path. XXX: this could be improved if we keep * the CTM as resident state on the DrawTarget. */ CGContextSaveGState(mCg); CGContextConcatCTM(mCg, GfxMatrixToCGAffineTransform(mTransform)); CGContextAddPath(mCg, cgPath->GetPath()); CGContextRestoreGState(mCg); if (cgPath->GetFillRule() == FILL_EVEN_ODD) CGContextEOClip(mCg); else CGContextClip(mCg); } void DrawTargetCG::PopClip() { CGContextRestoreGState(mCg); } void DrawTargetCG::MarkChanged() { if (mSnapshot) { if (mSnapshot->refCount() > 1) { // We only need to worry about snapshots that someone else knows about mSnapshot->DrawTargetWillChange(); } mSnapshot = nullptr; } } CGContextRef BorrowedCGContext::BorrowCGContextFromDrawTarget(DrawTarget *aDT) { if (aDT->GetType() == BACKEND_COREGRAPHICS || aDT->GetType() == BACKEND_COREGRAPHICS_ACCELERATED) { DrawTargetCG* cgDT = static_cast(aDT); cgDT->MarkChanged(); // swap out the context CGContextRef cg = cgDT->mCg; cgDT->mCg = nullptr; // save the state to make it easier for callers to avoid mucking with things CGContextSaveGState(cg); CGContextConcatCTM(cg, GfxMatrixToCGAffineTransform(cgDT->mTransform)); return cg; } return nullptr; } void BorrowedCGContext::ReturnCGContextToDrawTarget(DrawTarget *aDT, CGContextRef cg) { DrawTargetCG* cgDT = static_cast(aDT); CGContextRestoreGState(cg); cgDT->mCg = cg; } } }