/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Mozilla Corporation code. * * The Initial Developer of the Original Code is Mozilla Foundation. * Portions created by the Initial Developer are Copyright (C) 2010 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Vladimir Vukicevic * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #include "gfxUtils.h" #include "gfxContext.h" #include "gfxPlatform.h" #include "gfxDrawable.h" #include "nsRegion.h" #include "yuv_convert.h" #include "ycbcr_to_rgb565.h" #ifdef XP_WIN #include "gfxWindowsPlatform.h" #endif using namespace mozilla; using namespace mozilla::layers; using namespace mozilla::gfx; static PRUint8 sUnpremultiplyTable[256*256]; static PRUint8 sPremultiplyTable[256*256]; static bool sTablesInitialized = false; static const PRUint8 PremultiplyValue(PRUint8 a, PRUint8 v) { return sPremultiplyTable[a*256+v]; } static const PRUint8 UnpremultiplyValue(PRUint8 a, PRUint8 v) { return sUnpremultiplyTable[a*256+v]; } static void CalculateTables() { // It's important that the array be indexed first by alpha and then by rgb // value. When we unpremultiply a pixel, we're guaranteed to do three // lookups with the same alpha; indexing by alpha first makes it likely that // those three lookups will be close to one another in memory, thus // increasing the chance of a cache hit. // Unpremultiply table // a == 0 case for (PRUint32 c = 0; c <= 255; c++) { sUnpremultiplyTable[c] = c; } for (int a = 1; a <= 255; a++) { for (int c = 0; c <= 255; c++) { sUnpremultiplyTable[a*256+c] = (PRUint8)((c * 255) / a); } } // Premultiply table for (int a = 0; a <= 255; a++) { for (int c = 0; c <= 255; c++) { sPremultiplyTable[a*256+c] = (a * c + 254) / 255; } } sTablesInitialized = true; } void gfxUtils::PremultiplyImageSurface(gfxImageSurface *aSourceSurface, gfxImageSurface *aDestSurface) { if (!aDestSurface) aDestSurface = aSourceSurface; NS_ASSERTION(aSourceSurface->Format() == aDestSurface->Format() && aSourceSurface->Width() == aDestSurface->Width() && aSourceSurface->Height() == aDestSurface->Height() && aSourceSurface->Stride() == aDestSurface->Stride(), "Source and destination surfaces don't have identical characteristics"); NS_ASSERTION(aSourceSurface->Stride() == aSourceSurface->Width() * 4, "Source surface stride isn't tightly packed"); // Only premultiply ARGB32 if (aSourceSurface->Format() != gfxASurface::ImageFormatARGB32) { if (aDestSurface != aSourceSurface) { memcpy(aDestSurface->Data(), aSourceSurface->Data(), aSourceSurface->Stride() * aSourceSurface->Height()); } return; } if (!sTablesInitialized) CalculateTables(); PRUint8 *src = aSourceSurface->Data(); PRUint8 *dst = aDestSurface->Data(); PRUint32 dim = aSourceSurface->Width() * aSourceSurface->Height(); for (PRUint32 i = 0; i < dim; ++i) { #ifdef IS_LITTLE_ENDIAN PRUint8 b = *src++; PRUint8 g = *src++; PRUint8 r = *src++; PRUint8 a = *src++; *dst++ = PremultiplyValue(a, b); *dst++ = PremultiplyValue(a, g); *dst++ = PremultiplyValue(a, r); *dst++ = a; #else PRUint8 a = *src++; PRUint8 r = *src++; PRUint8 g = *src++; PRUint8 b = *src++; *dst++ = a; *dst++ = PremultiplyValue(a, r); *dst++ = PremultiplyValue(a, g); *dst++ = PremultiplyValue(a, b); #endif } } void gfxUtils::UnpremultiplyImageSurface(gfxImageSurface *aSourceSurface, gfxImageSurface *aDestSurface) { if (!aDestSurface) aDestSurface = aSourceSurface; NS_ASSERTION(aSourceSurface->Format() == aDestSurface->Format() && aSourceSurface->Width() == aDestSurface->Width() && aSourceSurface->Height() == aDestSurface->Height() && aSourceSurface->Stride() == aDestSurface->Stride(), "Source and destination surfaces don't have identical characteristics"); NS_ASSERTION(aSourceSurface->Stride() == aSourceSurface->Width() * 4, "Source surface stride isn't tightly packed"); // Only premultiply ARGB32 if (aSourceSurface->Format() != gfxASurface::ImageFormatARGB32) { if (aDestSurface != aSourceSurface) { memcpy(aDestSurface->Data(), aSourceSurface->Data(), aSourceSurface->Stride() * aSourceSurface->Height()); } return; } if (!sTablesInitialized) CalculateTables(); PRUint8 *src = aSourceSurface->Data(); PRUint8 *dst = aDestSurface->Data(); PRUint32 dim = aSourceSurface->Width() * aSourceSurface->Height(); for (PRUint32 i = 0; i < dim; ++i) { #ifdef IS_LITTLE_ENDIAN PRUint8 b = *src++; PRUint8 g = *src++; PRUint8 r = *src++; PRUint8 a = *src++; *dst++ = UnpremultiplyValue(a, b); *dst++ = UnpremultiplyValue(a, g); *dst++ = UnpremultiplyValue(a, r); *dst++ = a; #else PRUint8 a = *src++; PRUint8 r = *src++; PRUint8 g = *src++; PRUint8 b = *src++; *dst++ = a; *dst++ = UnpremultiplyValue(a, r); *dst++ = UnpremultiplyValue(a, g); *dst++ = UnpremultiplyValue(a, b); #endif } } static bool IsSafeImageTransformComponent(gfxFloat aValue) { return aValue >= -32768 && aValue <= 32767; } /** * This returns the fastest operator to use for solid surfaces which have no * alpha channel or their alpha channel is uniformly opaque. * This differs per render mode. */ static gfxContext::GraphicsOperator OptimalFillOperator() { #ifdef XP_WIN if (gfxWindowsPlatform::GetPlatform()->GetRenderMode() == gfxWindowsPlatform::RENDER_DIRECT2D) { // D2D -really- hates operator source. return gfxContext::OPERATOR_OVER; } else { #endif return gfxContext::OPERATOR_SOURCE; #ifdef XP_WIN } #endif } // EXTEND_PAD won't help us here; we have to create a temporary surface to hold // the subimage of pixels we're allowed to sample. static already_AddRefed CreateSamplingRestrictedDrawable(gfxDrawable* aDrawable, gfxContext* aContext, const gfxMatrix& aUserSpaceToImageSpace, const gfxRect& aSourceRect, const gfxRect& aSubimage, const gfxImageSurface::gfxImageFormat aFormat) { gfxRect userSpaceClipExtents = aContext->GetClipExtents(); // This isn't optimal --- if aContext has a rotation then GetClipExtents // will have to do a bounding-box computation, and TransformBounds might // too, so we could get a better result if we computed image space clip // extents in one go --- but it doesn't really matter and this is easier // to understand. gfxRect imageSpaceClipExtents = aUserSpaceToImageSpace.TransformBounds(userSpaceClipExtents); // Inflate by one pixel because bilinear filtering will sample at most // one pixel beyond the computed image pixel coordinate. imageSpaceClipExtents.Inflate(1.0); gfxRect needed = imageSpaceClipExtents.Intersect(aSourceRect); needed = needed.Intersect(aSubimage); needed.RoundOut(); // if 'needed' is empty, nothing will be drawn since aFill // must be entirely outside the clip region, so it doesn't // matter what we do here, but we should avoid trying to // create a zero-size surface. if (needed.IsEmpty()) return nsnull; gfxIntSize size(PRInt32(needed.Width()), PRInt32(needed.Height())); nsRefPtr temp = gfxPlatform::GetPlatform()->CreateOffscreenSurface(size, gfxASurface::ContentFromFormat(aFormat)); if (!temp || temp->CairoStatus()) return nsnull; nsRefPtr tmpCtx = new gfxContext(temp); tmpCtx->SetOperator(OptimalFillOperator()); aDrawable->Draw(tmpCtx, needed - needed.TopLeft(), true, gfxPattern::FILTER_FAST, gfxMatrix().Translate(needed.TopLeft())); nsRefPtr resultPattern = new gfxPattern(temp); if (!resultPattern) return nsnull; nsRefPtr drawable = new gfxSurfaceDrawable(temp, size, gfxMatrix().Translate(-needed.TopLeft())); return drawable.forget(); } // working around cairo/pixman bug (bug 364968) // Our device-space-to-image-space transform may not be acceptable to pixman. struct NS_STACK_CLASS AutoCairoPixmanBugWorkaround { AutoCairoPixmanBugWorkaround(gfxContext* aContext, const gfxMatrix& aDeviceSpaceToImageSpace, const gfxRect& aFill, const gfxASurface* aSurface) : mContext(aContext), mSucceeded(true), mPushedGroup(false) { // Quartz's limits for matrix are much larger than pixman if (!aSurface || aSurface->GetType() == gfxASurface::SurfaceTypeQuartz) return; if (!IsSafeImageTransformComponent(aDeviceSpaceToImageSpace.xx) || !IsSafeImageTransformComponent(aDeviceSpaceToImageSpace.xy) || !IsSafeImageTransformComponent(aDeviceSpaceToImageSpace.yx) || !IsSafeImageTransformComponent(aDeviceSpaceToImageSpace.yy)) { NS_WARNING("Scaling up too much, bailing out"); mSucceeded = false; return; } if (IsSafeImageTransformComponent(aDeviceSpaceToImageSpace.x0) && IsSafeImageTransformComponent(aDeviceSpaceToImageSpace.y0)) return; // We'll push a group, which will hopefully reduce our transform's // translation so it's in bounds. gfxMatrix currentMatrix = mContext->CurrentMatrix(); mContext->Save(); // Clip the rounded-out-to-device-pixels bounds of the // transformed fill area. This is the area for the group we // want to push. mContext->IdentityMatrix(); gfxRect bounds = currentMatrix.TransformBounds(aFill); bounds.RoundOut(); mContext->Clip(bounds); mContext->SetMatrix(currentMatrix); mContext->PushGroup(gfxASurface::CONTENT_COLOR_ALPHA); mContext->SetOperator(gfxContext::OPERATOR_OVER); mPushedGroup = true; } ~AutoCairoPixmanBugWorkaround() { if (mPushedGroup) { mContext->PopGroupToSource(); mContext->Paint(); mContext->Restore(); } } bool PushedGroup() { return mPushedGroup; } bool Succeeded() { return mSucceeded; } private: gfxContext* mContext; bool mSucceeded; bool mPushedGroup; }; static gfxMatrix DeviceToImageTransform(gfxContext* aContext, const gfxMatrix& aUserSpaceToImageSpace) { gfxFloat deviceX, deviceY; nsRefPtr currentTarget = aContext->CurrentSurface(&deviceX, &deviceY); gfxMatrix currentMatrix = aContext->CurrentMatrix(); gfxMatrix deviceToUser = gfxMatrix(currentMatrix).Invert(); deviceToUser.Translate(-gfxPoint(-deviceX, -deviceY)); return gfxMatrix(deviceToUser).Multiply(aUserSpaceToImageSpace); } /* static */ void gfxUtils::DrawPixelSnapped(gfxContext* aContext, gfxDrawable* aDrawable, const gfxMatrix& aUserSpaceToImageSpace, const gfxRect& aSubimage, const gfxRect& aSourceRect, const gfxRect& aImageRect, const gfxRect& aFill, const gfxImageSurface::gfxImageFormat aFormat, const gfxPattern::GraphicsFilter& aFilter) { bool doTile = !aImageRect.Contains(aSourceRect); nsRefPtr currentTarget = aContext->CurrentSurface(); gfxMatrix deviceSpaceToImageSpace = DeviceToImageTransform(aContext, aUserSpaceToImageSpace); AutoCairoPixmanBugWorkaround workaround(aContext, deviceSpaceToImageSpace, aFill, currentTarget); if (!workaround.Succeeded()) return; nsRefPtr drawable = aDrawable; // OK now, the hard part left is to account for the subimage sampling // restriction. If all the transforms involved are just integer // translations, then we assume no resampling will occur so there's // nothing to do. // XXX if only we had source-clipping in cairo! if (aContext->CurrentMatrix().HasNonIntegerTranslation() || aUserSpaceToImageSpace.HasNonIntegerTranslation()) { if (doTile || !aSubimage.Contains(aImageRect)) { nsRefPtr restrictedDrawable = CreateSamplingRestrictedDrawable(aDrawable, aContext, aUserSpaceToImageSpace, aSourceRect, aSubimage, aFormat); if (restrictedDrawable) { drawable.swap(restrictedDrawable); } } // We no longer need to tile: Either we never needed to, or we already // filled a surface with the tiled pattern; this surface can now be // drawn without tiling. doTile = false; } gfxContext::GraphicsOperator op = aContext->CurrentOperator(); if ((op == gfxContext::OPERATOR_OVER || workaround.PushedGroup()) && aFormat == gfxASurface::ImageFormatRGB24) { aContext->SetOperator(OptimalFillOperator()); } drawable->Draw(aContext, aFill, doTile, aFilter, aUserSpaceToImageSpace); aContext->SetOperator(op); } /* static */ int gfxUtils::ImageFormatToDepth(gfxASurface::gfxImageFormat aFormat) { switch (aFormat) { case gfxASurface::ImageFormatARGB32: return 32; case gfxASurface::ImageFormatRGB24: return 24; case gfxASurface::ImageFormatRGB16_565: return 16; default: break; } return 0; } static void PathFromRegionInternal(gfxContext* aContext, const nsIntRegion& aRegion, bool aSnap) { aContext->NewPath(); nsIntRegionRectIterator iter(aRegion); const nsIntRect* r; while ((r = iter.Next()) != nsnull) { aContext->Rectangle(gfxRect(r->x, r->y, r->width, r->height), aSnap); } } static void ClipToRegionInternal(gfxContext* aContext, const nsIntRegion& aRegion, bool aSnap) { PathFromRegionInternal(aContext, aRegion, aSnap); aContext->Clip(); } /*static*/ void gfxUtils::ClipToRegion(gfxContext* aContext, const nsIntRegion& aRegion) { ClipToRegionInternal(aContext, aRegion, false); } /*static*/ void gfxUtils::ClipToRegionSnapped(gfxContext* aContext, const nsIntRegion& aRegion) { ClipToRegionInternal(aContext, aRegion, true); } /*static*/ gfxFloat gfxUtils::ClampToScaleFactor(gfxFloat aVal) { // Arbitary scale factor limitation. We can increase this // for better scaling performance at the cost of worse // quality. static const gfxFloat kScaleResolution = 2; // Negative scaling is just a flip and irrelevant to // our resolution calculation. if (aVal < 0.0) { aVal = -aVal; } gfxFloat power = log(aVal)/log(kScaleResolution); // If power is within 1e-6 of an integer, round to nearest to // prevent floating point errors, otherwise round up to the // next integer value. if (fabs(power - NS_round(power)) < 1e-6) { power = NS_round(power); } else { power = ceil(power); } return pow(kScaleResolution, power); } /*static*/ void gfxUtils::PathFromRegion(gfxContext* aContext, const nsIntRegion& aRegion) { PathFromRegionInternal(aContext, aRegion, false); } /*static*/ void gfxUtils::PathFromRegionSnapped(gfxContext* aContext, const nsIntRegion& aRegion) { PathFromRegionInternal(aContext, aRegion, true); } bool gfxUtils::GfxRectToIntRect(const gfxRect& aIn, nsIntRect* aOut) { *aOut = nsIntRect(PRInt32(aIn.X()), PRInt32(aIn.Y()), PRInt32(aIn.Width()), PRInt32(aIn.Height())); return gfxRect(aOut->x, aOut->y, aOut->width, aOut->height).IsEqualEdges(aIn); } void gfxUtils::GetYCbCrToRGBDestFormatAndSize(const PlanarYCbCrImage::Data& aData, gfxASurface::gfxImageFormat& aSuggestedFormat, gfxIntSize& aSuggestedSize) { gfx::YUVType yuvtype = gfx::TypeFromSize(aData.mYSize.width, aData.mYSize.height, aData.mCbCrSize.width, aData.mCbCrSize.height); // 'prescale' is true if the scaling is to be done as part of the // YCbCr to RGB conversion rather than on the RGB data when rendered. bool prescale = aSuggestedSize.width > 0 && aSuggestedSize.height > 0 && aSuggestedSize != aData.mPicSize; if (aSuggestedFormat == gfxASurface::ImageFormatRGB16_565) { #if defined(HAVE_YCBCR_TO_RGB565) if (prescale && !gfx::IsScaleYCbCrToRGB565Fast(aData.mPicX, aData.mPicY, aData.mPicSize.width, aData.mPicSize.height, aSuggestedSize.width, aSuggestedSize.height, yuvtype, gfx::FILTER_BILINEAR) && gfx::IsConvertYCbCrToRGB565Fast(aData.mPicX, aData.mPicY, aData.mPicSize.width, aData.mPicSize.height, yuvtype)) { prescale = false; } #else // yuv2rgb16 function not available aSuggestedFormat = gfxASurface::ImageFormatRGB24; #endif } else if (aSuggestedFormat != gfxASurface::ImageFormatRGB24) { // No other formats are currently supported. aSuggestedFormat = gfxASurface::ImageFormatRGB24; } if (aSuggestedFormat == gfxASurface::ImageFormatRGB24) { /* ScaleYCbCrToRGB32 does not support a picture offset, nor 4:4:4 data. See bugs 639415 and 640073. */ if (aData.mPicX != 0 || aData.mPicY != 0 || yuvtype == gfx::YV24) prescale = false; } if (!prescale) { aSuggestedSize = aData.mPicSize; } } void gfxUtils::ConvertYCbCrToRGB(const PlanarYCbCrImage::Data& aData, const gfxASurface::gfxImageFormat& aDestFormat, const gfxIntSize& aDestSize, unsigned char* aDestBuffer, PRInt32 aStride) { gfx::YUVType yuvtype = gfx::TypeFromSize(aData.mYSize.width, aData.mYSize.height, aData.mCbCrSize.width, aData.mCbCrSize.height); // Convert from YCbCr to RGB now, scaling the image if needed. if (aDestSize != aData.mPicSize) { #if defined(HAVE_YCBCR_TO_RGB565) if (aDestFormat == gfxASurface::ImageFormatRGB16_565) { gfx::ScaleYCbCrToRGB565(aData.mYChannel, aData.mCbChannel, aData.mCrChannel, aDestBuffer, aData.mPicX, aData.mPicY, aData.mPicSize.width, aData.mPicSize.height, aDestSize.width, aDestSize.height, aData.mYStride, aData.mCbCrStride, aStride, yuvtype, gfx::FILTER_BILINEAR); } else #endif gfx::ScaleYCbCrToRGB32(aData.mYChannel, aData.mCbChannel, aData.mCrChannel, aDestBuffer, aData.mPicSize.width, aData.mPicSize.height, aDestSize.width, aDestSize.height, aData.mYStride, aData.mCbCrStride, aStride, yuvtype, gfx::ROTATE_0, gfx::FILTER_BILINEAR); } else { // no prescale #if defined(HAVE_YCBCR_TO_RGB565) if (aDestFormat == gfxASurface::ImageFormatRGB16_565) { gfx::ConvertYCbCrToRGB565(aData.mYChannel, aData.mCbChannel, aData.mCrChannel, aDestBuffer, aData.mPicX, aData.mPicY, aData.mPicSize.width, aData.mPicSize.height, aData.mYStride, aData.mCbCrStride, aStride, yuvtype); } else // aDestFormat != gfxASurface::ImageFormatRGB16_565 #endif gfx::ConvertYCbCrToRGB32(aData.mYChannel, aData.mCbChannel, aData.mCrChannel, aDestBuffer, aData.mPicX, aData.mPicY, aData.mPicSize.width, aData.mPicSize.height, aData.mYStride, aData.mCbCrStride, aStride, yuvtype); } } #ifdef MOZ_DUMP_PAINTING /* static */ void gfxUtils::WriteAsPNG(DrawTarget* aDT, const char* aFile) { aDT->Flush(); nsRefPtr surf = gfxPlatform::GetPlatform()->GetThebesSurfaceForDrawTarget(aDT); if (surf) { surf->WriteAsPNG(aFile); } else { NS_WARNING("Failed to get Thebes surface!"); } } /* static */ void gfxUtils::DumpAsDataURL(DrawTarget* aDT) { aDT->Flush(); nsRefPtr surf = gfxPlatform::GetPlatform()->GetThebesSurfaceForDrawTarget(aDT); if (surf) { surf->DumpAsDataURL(); } else { NS_WARNING("Failed to get Thebes surface!"); } } /* static */ void gfxUtils::CopyAsDataURL(DrawTarget* aDT) { aDT->Flush(); nsRefPtr surf = gfxPlatform::GetPlatform()->GetThebesSurfaceForDrawTarget(aDT); if (surf) { surf->CopyAsDataURL(); } else { NS_WARNING("Failed to get Thebes surface!"); } } #endif