-
Notifications
You must be signed in to change notification settings - Fork 804
Add support+tests for clipping context rendering to an image mask. #1528
Changes from all commits
be87eb9
a5aaef0
a706b86
317c838
85caf73
b8a1d7f
d1bf8ca
9c21411
b6ae3fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -103,8 +103,9 @@ | |
| CGSize shadowOffset{ 0, 0 }; | ||
| CGFloat shadowBlur{ 0 }; | ||
|
|
||
| // Clipping | ||
| // Clipping + Masking | ||
| ComPtr<ID2D1Geometry> clippingGeometry; | ||
| ComPtr<ID2D1BitmapBrush> opacityBrush; | ||
|
|
||
| inline void ComputeStrokeStyle(ID2D1DeviceContext* deviceContext) { | ||
| if (strokeStyle) { | ||
|
|
@@ -345,6 +346,8 @@ HRESULT DrawToCommandList(_CGCoordinateMode coordinateMode, | |
| Lambda&& drawLambda); | ||
| HRESULT DrawGeometry(_CGCoordinateMode coordinateMode, ID2D1Geometry* pGeometry, CGPathDrawingMode drawMode); | ||
| HRESULT DrawImage(ID2D1Image* image); | ||
| HRESULT ClipToD2DMaskBitmap(ID2D1Bitmap* bitmap, CGRect rect, bool shouldInterpolate); | ||
| HRESULT ClipToCGImageMask(CGImageRef image, CGRect rect); | ||
| }; | ||
|
|
||
| #define NOISY_RETURN_IF_NULL(param, ...) \ | ||
|
|
@@ -475,6 +478,7 @@ void CGContextSynchronize(CGContextRef context) { | |
| newDrawingState.shadowColor = { 0.f, 0.f, 0.f, 0.f }; | ||
| // newDrawingState.blendMode = kCGBlendModeNormal; // TODO GH#1389 | ||
| newDrawingState.clippingGeometry = nullptr; | ||
| newDrawingState.opacityBrush = nullptr; | ||
| if (rect) { | ||
| CGRect transformedClippingRegion = CGContextConvertRectToDeviceSpace(this, *rect); | ||
|
|
||
|
|
@@ -1015,13 +1019,132 @@ void CGContextClipToRects(CGContextRef context, const CGRect* rects, unsigned co | |
| CGContextClip(context); | ||
| } | ||
|
|
||
| HRESULT __CGContext::ClipToD2DMaskBitmap(ID2D1Bitmap* bitmap, CGRect rect, bool shouldInterpolate) { | ||
| RETURN_HR_IF_NULL(E_INVALIDARG, bitmap); | ||
|
|
||
| D2D1_SIZE_U bitmapSize = bitmap->GetPixelSize(); | ||
|
|
||
| RETURN_HR_IF(E_INVALIDARG, bitmapSize.width == 0 || bitmapSize.height == 0); | ||
|
|
||
| auto& state = CurrentGState(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
move it closer to use
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a good use of auto when knowing the type can add clarity. https://www.quora.com/When-should-you-use-the-auto-keyword-in-C++
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this would damage clarity: it could be
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure. It wasn't clear to me, hence the comment :). |
||
|
|
||
| CGFloat sx = rect.size.width / bitmapSize.width; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. blow up when width, height = 0? #Resolved |
||
| CGFloat sy = rect.size.height / bitmapSize.height; | ||
| CGFloat m = rect.origin.y + (rect.size.height / 2.f); | ||
|
|
||
| // |1 0 0| is the transformation matrix for flipping a rect about its Y midpoint m. (m = (y + h/2)) | ||
| // |0 -1 0| | ||
| // |0 2m 1| | ||
| // | ||
| // Combined with [scale sx * sy] * [translate X, Y], that becomes: | ||
| // |sx 0 0| | ||
| // | 0 -sy 0| | ||
| // | x -y+2m 0| | ||
| // Or, the transformation matrix for drawing a flipped rect at a scale and offset. | ||
| CGAffineTransform transform{ sx, 0, 0, -sy, rect.origin.x, (2 * m) - rect.origin.y }; | ||
|
|
||
| // Transform that by applying the device global transform (LLO<->ULO flip, plus user transform.) | ||
| CGAffineTransform userToDeviceTransform = CGContextGetUserSpaceToDeviceSpaceTransform(this); | ||
| transform = CGAffineTransformConcat(transform, userToDeviceTransform); | ||
|
|
||
| D2D1_INTERPOLATION_MODE interpolationMode = shouldInterpolate ? state.bitmapInterpolationMode : D2D1_INTERPOLATION_MODE_NEAREST_NEIGHBOR; | ||
| ComPtr<ID2D1BitmapBrush1> newOpacityBrush; | ||
| RETURN_IF_FAILED( | ||
| deviceContext->CreateBitmapBrush(bitmap, D2D1::BitmapBrushProperties1(D2D1_EXTEND_MODE_CLAMP, D2D1_EXTEND_MODE_CLAMP, interpolationMode), D2D1::BrushProperties(), &newOpacityBrush)); | ||
|
|
||
| newOpacityBrush->SetTransform(__CGAffineTransformToD2D_F(transform)); | ||
|
|
||
| // Compliance with reference platform: D2D extends the last pixel of the opacity brush out to the | ||
| // ends of the earth. The reference platform truncates the clipping region at the edge of the mask. | ||
| ComPtr<ID2D1RectangleGeometry> rectGeometry; | ||
| ComPtr<ID2D1TransformedGeometry> transformedRectClippingGeometry; | ||
| RETURN_IF_FAILED(Factory()->CreateRectangleGeometry(__CGRectToD2D_F(rect), &rectGeometry)); | ||
| RETURN_IF_FAILED(Factory()->CreateTransformedGeometry(rectGeometry.Get(), | ||
| __CGAffineTransformToD2D_F(userToDeviceTransform), | ||
| &transformedRectClippingGeometry)); | ||
|
|
||
| if (state.opacityBrush) { | ||
| // If we already have an opacity brush, we have to go to great lengths to compose the two clipping images. | ||
| // - Create a compatible render target with a backing bitmap | ||
| // - Using two layers (reasons detailed below), fill a geometry with the intersection of the two | ||
| // masks. | ||
| // - Use that bitmap (untransformed, as the render has resolved the global coordinate system conflict) | ||
| // as the opacity brush for future drawing. | ||
| ComPtr<ID2D1BitmapRenderTarget> compatibleTarget; | ||
| RETURN_IF_FAILED(deviceContext->CreateCompatibleRenderTarget(&compatibleTarget)); | ||
| ComPtr<ID2D1DeviceContext> compatibleContext; | ||
| RETURN_IF_FAILED(compatibleTarget.As(&compatibleContext)); | ||
|
|
||
| compatibleContext->BeginDraw(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be perf issue, but the good news is that the clip to mask with multiple masks is not typical scenario.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed! And this hit is only taken the first time the mask is composed. |
||
| compatibleContext->PushLayer(D2D1::LayerParameters(D2D1::InfiniteRect(), | ||
| nullptr, | ||
| D2D1_ANTIALIAS_MODE_PER_PRIMITIVE, | ||
| D2D1::IdentityMatrix(), | ||
| 1.0, // 1.0 global alpha for brush composition | ||
| state.opacityBrush.Get()), | ||
| nullptr); | ||
| compatibleContext->PushLayer(D2D1::LayerParameters(D2D1::InfiniteRect(), | ||
| transformedRectClippingGeometry.Get(), | ||
| D2D1_ANTIALIAS_MODE_PER_PRIMITIVE, | ||
| D2D1::IdentityMatrix(), | ||
| 1.0, // 1.0 global alpha for brush composition | ||
| newOpacityBrush.Get()), | ||
| nullptr); | ||
|
|
||
| ComPtr<ID2D1SolidColorBrush> brush; | ||
| RETURN_IF_FAILED(compatibleContext->CreateSolidColorBrush({ 0, 0, 0, 1 }, &brush)); | ||
| // FillGeometry takes three parameters: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
nit, This comment sounds great for the design doc/follow up questions to D2D but seems a bit excessive for in code comments. You could shorten it to something like "Using two stacked layers as brush opacity results in failure of other, preferable APIs." #Pending
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| // * geometry | ||
| // * fill brush | ||
| // * opacity brush | ||
| // | ||
| // It looks like the perfect candidate for creating a new final opacity brush -- | ||
| // one layer, one opacity-bound geometry fill! Except that for some reason, | ||
| // Direct2D returns D2DERR_INCOMPATIBLE_BRUSH_TYPES if you try to opacify a | ||
| // solid color brush with an opacity brush directly. | ||
| // Drawing through two stacked layers (original opacity brush, new opacity brush) | ||
| // however does work. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lets vet this approach with the D2D experts. |
||
| compatibleContext->FillGeometry(transformedRectClippingGeometry.Get(), brush.Get()); | ||
| compatibleContext->PopLayer(); | ||
| compatibleContext->PopLayer(); | ||
| RETURN_IF_FAILED(compatibleContext->EndDraw()); | ||
|
|
||
| ComPtr<ID2D1Bitmap> mergedAlphaMask; | ||
| RETURN_IF_FAILED(compatibleTarget->GetBitmap(&mergedAlphaMask)); | ||
| newOpacityBrush->SetBitmap(mergedAlphaMask.Get()); | ||
|
|
||
| // This brush is backed by a bitmap that is 1:1 scale/transform with the existing device context. | ||
| // Since its boundaries match the context boundaries, we don't need to intersect another global clip. | ||
| newOpacityBrush->SetTransform(D2D1::IdentityMatrix()); | ||
| } else { | ||
| // Since we are not composing the opacity brushes, we have to clip the global region to the bounds of the opacity brush. | ||
| RETURN_IF_FAILED(state.IntersectClippingGeometry(transformedRectClippingGeometry.Get(), kCGPathFill)); | ||
| } | ||
|
|
||
| state.opacityBrush = std::move(newOpacityBrush); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move a comptr? I love it! #Resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we leak a reference here? the assignment operator will addref, and if std::move() will appropriately release, we aren't gaining much with the std::move? |
||
|
|
||
| return S_OK; | ||
| } | ||
|
|
||
| HRESULT __CGContext::ClipToCGImageMask(CGImageRef image, CGRect rect) { | ||
| ComPtr<IWICBitmap> maskWicBitmap; | ||
| RETURN_IF_FAILED(_CGImageConvertToMaskCompatibleWICBitmap(image, &maskWicBitmap)); | ||
|
|
||
| ComPtr<ID2D1Bitmap> maskD2DBitmap; | ||
| RETURN_IF_FAILED(deviceContext->CreateBitmapFromWicBitmap(maskWicBitmap.Get(), nullptr, &maskD2DBitmap)); | ||
|
|
||
| return ClipToD2DMaskBitmap(maskD2DBitmap.Get(), rect, CGImageGetShouldInterpolate(image)); | ||
| } | ||
|
|
||
| /** | ||
| @Status Caveat | ||
| @Notes Limited bitmap format support | ||
| */ | ||
| void CGContextClipToMask(CGContextRef context, CGRect dest, CGImageRef image) { | ||
| void CGContextClipToMask(CGContextRef context, CGRect rect, CGImageRef image) { | ||
| NOISY_RETURN_IF_NULL(context); | ||
| UNIMPLEMENTED(); | ||
| NOISY_RETURN_IF_NULL(image); | ||
|
|
||
| FAIL_FAST_IF_FAILED(context->ClipToCGImageMask(image, rect)); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -1703,13 +1826,14 @@ void CGContextClearRect(CGContextRef context, CGRect rect) { | |
| deviceContext->BeginDraw(); | ||
|
|
||
| bool layer = false; | ||
| if (state.clippingGeometry || !IS_NEAR(state.globalAlpha, 1.0, .0001f)) { | ||
| if (state.clippingGeometry || !IS_NEAR(state.globalAlpha, 1.0, .0001f) || state.opacityBrush) { | ||
| layer = true; | ||
| deviceContext->PushLayer(D2D1::LayerParameters(D2D1::InfiniteRect(), | ||
| state.clippingGeometry.Get(), | ||
| D2D1_ANTIALIAS_MODE_PER_PRIMITIVE, | ||
| D2D1::IdentityMatrix(), | ||
| state.globalAlpha), | ||
| state.globalAlpha, | ||
| state.opacityBrush.Get()), | ||
| nullptr); | ||
| } | ||
|
|
||
|
|
@@ -1861,7 +1985,7 @@ void CGContextFillEllipseInRect(CGContextRef context, CGRect rect) { | |
| @Status Interoperable | ||
| @Notes The current path is cleared as a side effect of this function. | ||
| */ | ||
| void CGContextStrokeLineSegments(CGContextRef context, const CGPoint* points, unsigned count) { | ||
| void CGContextStrokeLineSegments(CGContextRef context, const CGPoint* points, size_t count) { | ||
| NOISY_RETURN_IF_NULL(context); | ||
| RETURN_IF(!context->ShouldDraw()); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,8 @@ | |
| #import "CGImageInternal.h" | ||
| #import "CGIWICBitmap.h" | ||
|
|
||
| #import <algorithm> | ||
|
|
||
| using namespace Microsoft::WRL; | ||
|
|
||
| static const wchar_t* TAG = L"CGImage"; | ||
|
|
@@ -786,4 +788,108 @@ REFGUID _CGImageGetWICPixelFormatFromImageProperties(unsigned int bitsPerCompone | |
| return GUID_WICPixelFormat32bppRGBA; | ||
| } | ||
|
|
||
| static void __InvertMemcpy(void* dest, const void* src, size_t len) { | ||
| uint32_t* d32 = (uint32_t*)dest; | ||
| const uint32_t* s32 = (const uint32_t*)src; | ||
|
|
||
| for(; len >= 4; len -= 4) { | ||
| *d32++ = ~*s32++; | ||
| } | ||
|
|
||
| uint8_t* d8 = (uint8_t*)d32; | ||
| const uint8_t* s8 = (const uint8_t*)s32; | ||
|
|
||
| for(; len > 0; --len) { | ||
| *d8++ = ~*s8++; | ||
| } | ||
| } | ||
|
|
||
| // __CGImageMaskConvertToWICAlphaBitmap converts a 1, 2, 4, or 8-bpp grayscale "mask" into an alpha-only image for a D2D opacity brush. | ||
| // On the reference platform, mask images are grayscale images whose pixel values signify the pixel's alpha transmissivity. | ||
| // A fully black region (S = 0.0) is translated to a fully opaque region (A = 1.0). | ||
| // A fully white region (S = 1.0), on the other hand, becomes a fully transparent region (A = 0.0). | ||
| // Values that fall within the range (0.0, 1.0) are converted into complementary alpha values (A = 1.0 - S). | ||
| // | ||
| // Conversion of 8bpp grayscale images is simple: Subtract the pixel's value from 255 and use the result as the alpha value. | ||
| // Images of other bit depths have their pixels scaled to values between 0 and 255. | ||
| static HRESULT __CGImageMaskConvertToWICAlphaBitmap(CGImageRef image, IWICBitmap** pAlphaBitmap) { | ||
| RETURN_HR_IF_NULL(E_INVALIDARG, image); | ||
| RETURN_HR_IF_NULL(E_POINTER, pAlphaBitmap); | ||
|
|
||
| RETURN_HR_IF(E_INVALIDARG, !CGImageIsMask(image)); | ||
|
|
||
| ComPtr<IWICImagingFactory> imagingFactory; | ||
| RETURN_IF_FAILED(_CGGetWICFactory(&imagingFactory)); | ||
|
|
||
| woc::unique_cf<CGImageRef> gray8Bitmap{ _CGImageCreateCopyWithPixelFormat(image, GUID_WICPixelFormat8bppGray) }; | ||
| RETURN_HR_IF_NULL(E_INVALIDARG, gray8Bitmap); | ||
|
|
||
| ComPtr<IWICBitmap> gray8Source; | ||
| RETURN_IF_FAILED(_CGImageGetWICImageSource(gray8Bitmap.get(), &gray8Source)); | ||
|
|
||
| unsigned int w = 0, h = 0; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
nit:I guess using width and height might lighten the mood up? |
||
| RETURN_IF_FAILED(gray8Source->GetSize(&w, &h)); | ||
|
|
||
| ComPtr<IWICBitmapLock> gray8Lock; | ||
| RETURN_IF_FAILED(gray8Source->Lock(nullptr, WICBitmapLockRead, &gray8Lock)); | ||
|
|
||
| unsigned char* gray8Data; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: move these around so they're closer to where they're used. #WontFix
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That sacrifices the locality of something else (alpha8 image stuff.) In reply to: 92092724 [](ancestors = 92092724) |
||
| size_t gray8Len, gray8Stride; | ||
| RETURN_IF_FAILED(gray8Lock->GetStride(&gray8Stride)); | ||
| RETURN_IF_FAILED(gray8Lock->GetDataPointer(&gray8Len, &gray8Data)); | ||
|
|
||
| ComPtr<IWICBitmap> alphaBitmap; | ||
| RETURN_IF_FAILED(imagingFactory->CreateBitmap(w, h, GUID_WICPixelFormat8bppAlpha, WICBitmapCacheOnDemand, &alphaBitmap)); | ||
|
|
||
| ComPtr<IWICBitmapLock> alpha8Lock; | ||
| RETURN_IF_FAILED(alphaBitmap->Lock(nullptr, WICBitmapLockWrite, &alpha8Lock)); | ||
|
|
||
| unsigned char* alpha8Data; | ||
| size_t alpha8Len, alpha8Stride; | ||
| RETURN_IF_FAILED(alpha8Lock->GetStride(&alpha8Stride)); | ||
| RETURN_IF_FAILED(alpha8Lock->GetDataPointer(&alpha8Len, &alpha8Data)); | ||
|
|
||
| RETURN_HR_IF(E_UNEXPECTED, alpha8Len < gray8Len || alpha8Stride < gray8Stride); | ||
|
|
||
| if (alpha8Len == gray8Len && alpha8Stride == gray8Stride) { | ||
| __InvertMemcpy(alpha8Data, gray8Data, gray8Len); | ||
| } else { | ||
| // stride or length (likely both) differ | ||
| uint8_t* destEnd = alpha8Data + alpha8Len; | ||
| for(uint8_t *src = gray8Data, *dest = alpha8Data; | ||
| dest < destEnd; | ||
| src += gray8Stride, dest += alpha8Stride) { | ||
| __InvertMemcpy(dest, src, gray8Stride); | ||
| } | ||
| } | ||
|
|
||
| *pAlphaBitmap = alphaBitmap.Detach(); | ||
| return S_OK; | ||
| } | ||
|
|
||
| HRESULT _CGImageConvertToMaskCompatibleWICBitmap(CGImageRef image, IWICBitmap** pBitmap) { | ||
| RETURN_HR_IF_NULL(E_INVALIDARG, image); | ||
| RETURN_HR_IF_NULL(E_POINTER, pBitmap); | ||
|
|
||
| if (CGImageIsMask(image)) { | ||
| // Hard way: Convert the image's gray values to alpha values A where G = <pixel gray value>; A = (1 - G) | ||
| // We can perhaps take the easy way out and create an A8 only image, since D2D supports them. | ||
|
|
||
| // We can safely assume the image is already in Gray, so its pixels will be 1, 2, 4, or 8bpp grayscale. | ||
| // Upconvert to 8bpp grayscale to simplify the code here. If it's bad perf-wise we can break it down. | ||
| return __CGImageMaskConvertToWICAlphaBitmap(image, pBitmap); | ||
| } | ||
|
|
||
| // "Easy" way: Convert the image to an acceptable D2D pixel format (if necessary) and turn it into a D2D bitmap. | ||
| WICPixelFormatGUID imagePixelFormat = _CGImageGetWICPixelFormat(image); | ||
|
|
||
| woc::unique_cf<CGImageRef> convertedImage{ CGImageRetain(image) }; | ||
| if (!_CGIsValidRenderTargetPixelFormat(imagePixelFormat)) { | ||
| // convert it to a valid pixelformat | ||
| convertedImage.reset(_CGImageCreateCopyWithPixelFormat(image, GUID_WICPixelFormat32bppPRGBA)); | ||
| } | ||
|
|
||
| return _CGImageGetWICImageSource(convertedImage.get(), pBitmap); | ||
| } | ||
|
|
||
| #pragma endregion WIC_HELPERS | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -135,3 +135,5 @@ COREGRAPHICS_EXPORT CGImageRef _CGImageCreateCopyWithPixelFormat(CGImageRef imag | |
|
|
||
| typedef void (*CGImageDestructionListener)(CGImageRef img); | ||
| COREGRAPHICS_EXPORT void CGImageAddDestructionListener(CGImageDestructionListener listener); | ||
|
|
||
| HRESULT _CGImageConvertToMaskCompatibleWICBitmap(CGImageRef image, IWICBitmap** pBitmap); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That'd work! |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: these should already be initialized, so no need to set to nullptr.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are potentially copied from the base state before them (as comment above.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(so they do have values(!))